The Python raw-socket RPC client from the last Iron Fish post worked. It talked straight to ~/.ironfish/ironfish.ipc, no SDK, and pulled live node status off the wire. Porting it to Rust looked like it should be a clean, mechanical exercise — same protocol, different syntax. It was, mostly. What it actually turned into was a lesson in not trusting your own terminal.
Why this exists
Same reason as the Python client: understanding a service well enough to write your own client from scratch, no SDK, is the real BHP-adjacent skill. Doing it twice, in two languages, against the same protocol, is a good way to confirm you actually understood the protocol and didn’t just get lucky with one language’s ecosystem.
Part 1 — The port itself
The protocol didn’t change: connect to the Unix domain socket, send a JSON envelope terminated by a single \f (0x0C) byte, read until the next \f, parse that slice. Rust has no JSON in std, so serde_json is the direct equivalent of Python’s built-in json module — same spirit of “no SDK, just sockets and a JSON library.”
One deliberate improvement over the Python draft: the auth token is read at runtime from ~/.ironfish/internal.json (rpcAuthToken field) instead of being hardcoded in source. Small thing, but hardcoding a live secret in a script you’re about to publish to a blog is exactly the kind of habit worth breaking early.
The Rust-specific parts worth noting:
UnixStreamfromstd::os::unix::netfor the socket — no async runtime needed for a one-shot status check.- Reading the response required manually buffering bytes until the delimiter showed up (
buf.iter().position(|&b| b == DELIMITER)), since there’s no framing built intoUnixStream— same manual-framing logic the Python version needed, just with explicit types. serde_json::Valueindexing (response["data"]["status"]) mirrors Python’s dict access closely enough that the port was close to line-for-line in the parsing logic.
No compiler available in the sandbox that generated the code, so this one got a careful manual review instead of a live cargo build before handing it over — flagged clearly at the time, and it turned out to matter (see below).
Part 2 — Getting the files onto the actual machine: the paste-corruption detour
This is where the real story is.
The bad: the first attempt to get the project onto the Debian machine used the same heredoc trick that worked fine for the Python client — cat > file << 'EOF' ... EOF. This time, the paste came out mangled. Not missing entirely — interleaved. Lines from the middle of main.rs were showing up spliced with unrelated shell output, closing quotes were vanishing mid-paste, and the shell was left sitting at a bare > continuation prompt waiting for input that would never close correctly.
The ugly, unrelated detour: partway through, terminal output from a completely different task — installing Oracle Database XE via alien — got pasted into this conversation by mistake, twice. Good reminder that running multiple terminals side by side on the same machine means it’s easy to grab output from the wrong window. Also a good reminder to double check: was I root or lvydvy? Turned out to be relevant later.
The actual root cause, once traced down: two separate problems stacked on top of each other.
- A
sudo/susession had switched toroot, which has its own$HOME(/root) — andcargowas only ever installed forlvydvyviarustup, living in/home/lvydvy/.cargo/bin. Building as root just silently picked up an unrelated pre-existingaxum-serverproject sitting in/root‘s ownCargo.toml, with no error to flag the mismatch. - Independently, the terminal itself was dropping or scrambling characters on very long pasted lines — visible as garbled text like
let auth_token =ernal.json was not valid JSON");rnal_raw)splay()),{showing up where clean Rust source should have been. Multiple long-line heredoc attempts reproduced it consistently.
The good, and the actual fix: stop fighting the terminal’s paste handling and remove anything it could misinterpret. Encoded both files as base64 — pure [A-Za-z0-9+/=], nothing a shell, a heredoc, or a flaky paste buffer could possibly choke on — split into ~500-character chunks appended one echo -n "..." >> file at a time, then decoded in one shot with base64 -d. Verified the round-trip locally before ever handing over a single command: decode the same base64 back and diff it byte-for-byte against the source file.
Even that wasn’t perfectly smooth — one paste still visibly mangled its tail (the closing base64 -d/wc -l/next-file commands got spliced together into one unreadable line). But by then the actual file content had already been fully appended in isolated, short, checksummable chunks before the corruption hit, so nothing that mattered was lost.
The verification that actually closed the loop: rather than trust a line count (wc -l) as proof of an intact file — which turned out to still be ambiguous, since the expected line count for Cargo.toml needed double-checking too — the real fix was sha256sum on both ends. Computed the expected hashes locally, had the hashes run on the Debian machine, and compared. Byte-for-byte confirmed identical, corrupted-looking paste tail notwithstanding.
Lesson for next time: when a terminal, KVM console, or remote paste bridge is suspect, don’t trust line counts, don’t trust “it looked right scrolling by,” and don’t re-paste and hope. Move to an encoding that has no characters a shell can misinterpret (base64, not raw source), keep chunks short, and verify with a cryptographic checksum on both sides before trusting the result. This is a genuinely reusable technique any time a remote shell session is unreliable.
Part 3 — The anticlimactic finish: it just needed to warm up
Once the files were verified byte-identical and cargo build --release ran clean — zero warnings, ~16 seconds for serde_json and its handful of transitive dependencies — the first actual run hung with no output at all.
Reasonable first guess, given the earlier Python saga had trained instinct to distrust the HTTP adapter: history repeating itself. It wasn’t. The node had just been restarted seconds earlier via ironfish start, and was still settling — same reason ironfish status (the CLI) can be slow right after a fresh start when the wallet’s mid-scan. Isolating the question (“does the node respond to any IPC query right now, via the CLI, independent of my new client?”) was the fast way to tell “my code is broken” apart from “the thing I’m talking to isn’t ready yet.” A short wait later, the Rust client returned a clean status block on the first try:
Node status: started
Version: 2.12.0 @ b442f9b
Peers: 2
Chain height: 1769426
Synced: false
Syncer status: idle
Sync progress: 99.61%
Identical shape to the Python client’s output, from an independently written implementation talking the same raw protocol. That’s a decent sanity check that the protocol understanding from the first post was correct, not just something that happened to work once.
Summary table
| Stage | Failure mode | Root cause | Fix |
|---|---|---|---|
| Port Python → Rust | — | — | UnixStream + serde_json, token read from internal.json at runtime |
| Compile-check | Couldn’t verify in sandbox | No Rust toolchain, network blocked to rustup/apt | Manual line-by-line review instead |
| First deploy attempt | axum-server built instead of our project | Running as root, whose $HOME has an unrelated Cargo.toml; cargo not on root’s PATH anyway | exit back to lvydvy |
| Heredoc paste | Garbled/interleaved source mid-file | Terminal/paste bridge corrupting long pasted lines | Switched to base64-encoded, chunked, echo -n >> appends |
| Trusting the result | Line count alone wasn’t reassuring | Corruption doesn’t reliably change line counts | sha256sum on both ends, compared explicitly |
| First run | Hung with no output | Node still settling right after ironfish start | Isolated via ironfish status CLI check, then just waited |
Lesson for next time, overall
The Rust code itself was the least eventful part of this project — it compiled clean on the first real attempt and matched the Python client’s output immediately. The actual engineering problem turned out to be trust in a communication channel: how do you know the file on the remote machine is the file you meant to send, when the channel between you is silently lossy? Base64 plus checksums is the general-purpose answer, and it’s worth reaching for early next time a paste looks even slightly off, rather than after three garbled attempts.