Building, Breaking, and Scripting Iron Fish: A Black Hat Python Field Log

The real payoff came last: the official HTTP API silently hung with no error, so reading the node's own source code on GitHub uncovered its raw socket protocol instead — and a hand-built Python client, no SDK, no libraries beyond socket and json, ended up pulling live chain data straight off the wire.

Why this exists

This is a reference write-up of an end-to-end session: installing the Iron Fish privacy cryptocurrency node from source on Debian, getting it fully synced, and then writing a raw Python client against its RPC interface as a deliberate Black Hat Python (BHP) style exercise. Almost nothing worked on the first try. That’s the point of keeping this log — the failures are more instructive than the eventual one-line success message.

The throughline: BHP builds intuition for writing your own clients against real network services instead of leaning on someone else’s SDK. Iron Fish, being a real, actively-syncing, cryptographically real service running on your own machine, turned out to be a genuinely good target for that kind of practice — accidentally more so than planned, because its official HTTP RPC adapter turned out to be broken in a way that forced a pivot into exactly the kind of raw-socket, hand-rolled-protocol work BHP is about.


Part 1 — Installing Iron Fish: five stacked build failures

Installing Iron Fish (npm install -g ironfish) meant compiling several native Node.js addons from source, and each one failed for a different, unrelated reason. They had to be solved in order, one at a time.

The bad: Missing system library. node-hid (used for hardware wallet support) requires libusb-1.0 at build time. Debian didn’t have the -dev package installed, so pkg-config couldn’t find it and the build died in node-gyp. Fix: sudo apt install -y libusb-1.0-0-dev pkg-config

The bad: Node was too new. node-datachannel‘s fallback build system uses an old, unmaintained prebuild package (v12.1.0) that doesn’t understand how to read the N-API version on very recent Node releases. On Node 24 it crashed instantly with TypeError: expected first argument to be an array before even reaching the compiler. Fix: Installed Node 20 LTS via nvm and switched to it (nvm install 20, nvm use 20).

The ugly: A genuinely obscure toolchain incompatibility. With Node 20 sorted, node-datachannel got much further — 37 of 131 objects compiled — before failing inside a vendored dependency, libjuice (a WebRTC ICE library), with:

error: implicit declaration of function 'ATOMIC_VAR_INIT'

Root cause: GCC 15 removed ATOMIC_VAR_INIT from its own stdatomic.h, following its deprecation and removal in the C23 standard. The vendored libjuice source, written years before GCC 15 existed, still uses it. This wasn’t an Iron Fish bug or a Debian packaging bug — it was two otherwise-correct pieces of software from different eras colliding. Fix: Installed an older compiler alongside the system one (gcc-12, g++-12) and built explicitly with CC=gcc-12 CXX=g++-12.

The ugly, round two: With the compiler fixed, all 131 objects compiled successfully — and the link step failed:

relocation R_X86_64_PC32 against symbol `z_errmsg` can not be used when
making a shared object; recompile with -fPIC

Debian’s system libz.a (static zlib) wasn’t compiled with -fPIC, so it couldn’t be linked into the .node shared library the addon needed to produce. The first fix attempt — temporarily moving libz.a out of the way so the linker would fall back to the shared libz.so — backfired badly: CMake’s find_package(ZLIB) failed entirely, because OpenSSL’s own static library declares zlib as a link dependency and needs some zlib target to resolve, static or not. That attempt had to be reverted. Real fix: Downloaded zlib 1.3.1 source directly (zlib.net’s own tarball link was broken/serving an error page — had to use the GitHub mirror at github.com/madler/zlib instead), rebuilt it from source with CFLAGS="-fPIC", and replaced the broken system libz.a with the PIC-compiled version.

The good: After that fifth fix, npm install -g ironfish finally completed cleanly, and ironfish --help printed the CLI’s usage text. Total elapsed troubleshooting: five distinct, unrelated root causes, solved one layer at a time, each only visible after the previous one was fixed.

Lesson for next time: when a native Node addon fails to build, don’t assume the first error is the only error. Missing system libs, toolchain version mismatches, and static-linking flag mismatches (-fPIC) are three completely independent failure classes that commonly stack on top of each other in exactly this order (missing headers → compiler-version C incompatibilities → linker flag mismatches).


Part 2 — Getting the chain synced: the snapshot detour

ironfish start connected to the network immediately and auto-created a default wallet account — but ironfish status showed it parked at block 1, 0% sync progress, with only 2 peers (both of them the official bootstrap nodes, not real sync sources). Peer-to-peer syncing three-plus years of blockchain history from a genesis block with only two peers would have taken an unreasonable amount of time.

The fix: Iron Fish ships an official fast path for exactly this — ironfish chain:download, which pulls a pre-verified snapshot of the chain database instead of replaying every block from peers. After confirming enough free disk space (531 GB available against a 52.3 GB requirement), the download kicked off.

The bad: The 26.15 GB download aborted repeatedly — at 69%, 77%, 82%, 89% — almost certainly due to network interruptions (the machine had also been left idle over a lunch break at one point). The good: each retry of ironfish chain:download resumed from where it left off rather than restarting from zero, and it eventually completed, unzipped, and replaced the local chain database.

The ugly: Restarting the node afterward hit Another node is using the database, waiting for that node to close — caused by confusion over which terminal window was running what, resulting in two stray duplicate ironfish start processes plus a finished-but-not-exited ironfish chain:download process all still alive. Diagnosing this required ps aux | grep ironfish and killing specific PIDs by hand — and notably, not using a blanket pkill -f ironfish, because a completely unrelated process (the Makepad synth demo, named makepad-example-ironfish) also matched that pattern and would have been killed by accident.

The good, finally: Once the stray processes were cleared, a clean ironfish start picked up the imported snapshot immediately — block height jumped from 1 to 1,769,426 in one step, sync progress at 99.96%, down from “3 years 4 months behind” to about 12 hours behind. The wallet then began its own independent background scan of the full chain history to check for any transactions belonging to the account.

Lesson for next time: always track which terminal is running the long-lived node process versus which terminal is for one-off commands. Confusing the two is what caused the duplicate-process/database-lock problem. Also: when killing processes by name pattern, check first that the pattern isn’t shared by something unrelated.


Part 3 — An unrelated but useful detour: Makepad

While investigating a YouTube talk, a demo app called “Ironfish Desktop” showed up — a full-featured synthesizer UI built with Makepad, a Rust cross-platform UI framework. This turned out to have no connection to the Iron Fish cryptocurrency at all: it’s an unrelated project made by a hardware synth company (“This Is Not Rocket Science,” makers of the BigFish synthesizer) to showcase Makepad’s UI framework. Pure naming coincidence between two unrelated teams.

Getting it running was its own small saga: the Makepad monorepo’s examples/ironfish directory had been removed from every current branch (dev, main, work) by the time it was cloned, so cargo run -p makepad-example-ironfish failed no matter which branch was checked out. The actual fix was simpler than the git archaeology suggested — the demo is also published standalone on crates.io, so cargo install makepad-example-ironfish pulled a self-contained, known-good build directly, sidestepping the monorepo’s moving target entirely. It launched successfully on the first attempt after that.

Lesson for next time: when a documented example is missing from a cloned monorepo, check whether it’s also published as a standalone crate/ package before chasing branches — a versioned registry snapshot is often more reliable than an actively-changing repo.


Part 4 — The real BHP exercise: writing an RPC client from scratch

This is the part that ties most directly back to Black Hat Python: writing a client against a real network service’s protocol, without using an official SDK (Iron Fish only ships a JavaScript one).

Attempt 1: HTTP RPC (the ugly, unresolved mystery)

Iron Fish supports enabling an HTTP RPC adapter (ironfish config:set enableRpcHttp true), documented with a simple curl -X POST http://localhost:8021/node/getStatus example. A Python script using the requests library was written to hit the same endpoint.

It hung. Indefinitely. No response, no error, no timeout from the server side — confirmed independently with raw curl (both with and without a JSON body, both with and without an Authorization header). Diagnosis ruled out several plausible causes one at a time:

  • The port genuinely was open and owned by the right process (ss -tlnp confirmed it).
  • The node itself wasn’t stuck or overloaded — ironfish status (which talks to the RPC server over IPC) returned instantly the whole time.
  • Reading Iron Fish’s actual httpAdapter.ts source from GitHub showed no authentication check at all in that code path, ruling out a missing-auth theory for HTTP specifically.

The exact root cause of the HTTP hang was never fully pinned down within the session — but reading the source paid off anyway, because it revealed something more useful: the IPC/TCP socket adapter uses a completely different, well-documented, custom protocol.

Attempt 2: raw Unix domain socket (the good)

Reading socketAdapter.ts and protocol.ts directly from Iron Fish’s GitHub repository revealed the real wire protocol used by IPC and TCP:

  • Connect to the IPC socket file (~/.ironfish/ironfish.ipc, a genuine Unix domain socket) or a TCP port.
  • Send a JSON message shaped like:

json

  {
    "type": "message",
    "data": {
      "mid": 1,
      "type": "node/getStatus",
      "auth": "<token from ~/.ironfish/internal.json>",
      "data": {}
    }
  }

terminated by a single \f (form feed, 0x0C) byte as a message delimiter — not HTTP framing, not newline-delimited JSON, a delimiter byte.

  • Read bytes back until the next \f, then parse the JSON response.

A ~100-line Python script (ironfish_ipc_client.py) was built from scratch using only the standard socket and json modules — no third-party libraries at all — to speak this protocol directly.

The one remaining bug: the first real run returned a clean response from the server (progress — no more hanging!) but with a validation error: the route’s schema required data to be an object, and the script had been sending JSON null (Python’s None) when no payload was given. Fix: default to {} instead of None when no data is provided.

The good, for real this time: the very next run returned live, correct data — chain height, peer count, sync progress — pulled straight from the node over a hand-built socket protocol. The script was then extended with a --watch flag (polls every 5 seconds) and a screen-clear plus timestamp header, turning it into a simple live-updating terminal dashboard.

Lesson for next time — the single most useful one from this whole session: when a documented API doesn’t behave as documented, and there’s no clear error message to explain why, go read the actual server-side source code before spending more time guessing at the client side. The official docs for Iron Fish’s HTTP adapter looked complete and simple — but the socket adapter’s source revealed the real, working protocol design in about five minutes of reading, after nearly an hour of blind HTTP-request debugging led nowhere.


Summary table

StageFailure modeRoot causeFix
Install: node-hidBuild failsMissing libusb-1.0 dev headersapt install libusb-1.0-0-dev
Install: node-datachannelBuild tool crashesOld prebuild tool can’t read Node 24’s N-API versionDowngrade to Node 20 LTS
Install: node-datachannelCompile errorGCC 15 removed ATOMIC_VAR_INIT (C23)Build with gcc-12/g++-12
Install: node-datachannelLink errorSystem libz.a not built with -fPICRebuild zlib from source with -fPIC
SyncStuck at block 1, 0%Only 2 bootstrap peers, no real sync sourcesironfish chain:download snapshot
SyncDownload aborts repeatedlyNetwork interruptionsRetried; download resumed each time
Restart“Another node is using the database”Duplicate stray ironfish start processesKilled specific PIDs (not a blanket pkill)
MakepadExample missing from repoRemoved from all current monorepo branchesInstalled standalone published crate instead
RPC scriptingRequests hang foreverUnknown bug in HTTP RPC adapterAbandoned HTTP; used IPC socket protocol instead
RPC scripting400 validation errorSent JSON null instead of {} for empty payloadDefault empty payload to {}