A blockchain node written entirely by AI. Can we trust it?
rustock is a Rootstock node written in Rust. I didn’t write any of it myself; AI coding agents did. This post is how that went.
A disclaimer before we start: rustock is a personal experiment to see how far today’s AI coding tools can go. It is not an official Rootstock node, it is not production ready, and no human has reviewed it. Use it at your own risk.
The code is on GitHub: github.com/javicid/rustock.
Two ideas had floated around RootstockLabs for a long time: a light node, and a second Rootstock client written from scratch. Both would make the network more accessible, and so more decentralized. And both are a lot of effort.
The light node is the easier of the two. Writing a second full client is the hard one, and Rootstock makes it harder than most. Ethereum has a yellow paper. Rootstock’s consensus, by contrast, is defined by its reference client: rskj is the spec. If a second client executes a single transaction even slightly differently, it forks the network. And that kind of bug can sit dormant for months before some block finally triggers it, at which point you’re left arguing about which client was right all along. Then there are the precompiles. REMASC and the Bridge are big, complex pieces of code. REMASC distributes fees to miners, and the Bridge is the one that moves BTC between Bitcoin and Rootstock. A bug there doesn’t just fork the chain, it can lose people’s bitcoin. For a sidechain, that is about as bad as it gets.
That is why nobody had done it. So why try now? Up to this point my own use of AI for coding had been cautious. I ran Cursor mostly as a fancy autocomplete, off to the side of the screen, and wrote the actual code myself. Then Google shipped Antigravity, the models kept getting visibly better at programming, and I started wondering how much I could build if I wrote none of it. A rewrite of rskj was the obvious stress test. The team had already experimented with porting parts of rskj to Go and it had gone well, and a light client in Rust was something we had wanted to build for a while. This was a way to test the tools and make progress on it at the same time, for almost nothing. One caveat before going further: I started on this about eight months ago, so expect some of what follows to be out of date already, given how fast these tools are moving.
Building the light node
A light node is the stripped-down version: it downloads block headers and verifies them, and that’s it. No transactions, no execution. My target was easy to state, if not to reach: validate every Rootstock header from genesis.
I started on Antigravity, since Gemini 3 was the coding model to beat at the time. I had no workflow yet, so the first thing I did was ask the model to write a markdown document listing the modules a light node would need. The nice thing about porting an existing system is that the agent has an answer key: it could read rskj whenever it got stuck. Once the high-level plan looked right, I gave it a handful of standing rules and told it to iterate:
- Build the smallest unit of work possible at each step
- Stay fully compatible with rskj
- Keep the code simple and idiomatic Rust
- Write unit tests after every step
The early days were clumsy. I had no proper harness, so I did an embarrassing amount of babysitting: re-pasting instructions every time the agent lost the thread, clicking to accept file after file, and typing “please continue” whenever it wandered off and stopped. Looking back, it was like doing arithmetic on an abacus. Eventually I settled into a loop:
- Split a module into small units of work
- Port one unit, using rskj as the reference
- Write the tests and make them pass
- Clear the warnings
- Refactor to simplify and make it more Rust-like
- Re-run the tests, clear the warnings again
- Back to step 2
One pattern kept biting me. The agent liked to leave holes: a stubbed method here, a TODO there. I had to regenerate the master plan two or three times, each time asking the agent to audit whether the node was actually complete. I think I brought it on myself by pushing so hard for tiny units and maximum simplicity. Hand an agent a task it finds too big, and it quietly does the least it can get away with, stubbing the hard parts to make the rest fit. If I did it again, I’d spend far longer on a detailed plan before letting it write anything.
After a few rounds, the agent declared the node finished and ready to sync headers. Time to find out.
The first sync
For the first sync I moved to Cursor, because the new Opus 4.5 was clearly ahead of Gemini. Opus turned up more of those hidden gaps and closed them, and the jump in quality was hard to miss. It wrote features and fixed bugs faster and with more precision. Where Gemini might grind for thirty minutes on a bug, Opus tended to nail it on the first try.
The work was still mostly manual, but now it had a rhythm. I’d run the node, tail the logs, wait for it to fall over, and hand the error to the agent to diagnose and fix. Syncing against the real chain turned out to be the single most useful thing in the whole project. It’s an oracle. Paired with the rskj source, it meant almost every failure came with a correct answer attached. Better still, some of rskj’s own unit tests ship with hardcoded groundtruth, like expected hashes, so the agent often didn’t have to hunt for where reality diverged. It could go straight to the mismatch. That collapsed debugging time.
It also exposed something less comfortable. The implementation was riddled with bugs that only showed up once real data flowed through it. The code the agent had written looked fine and was quietly wrong in a dozen places.
Without a real blockchain to check against, I don’t think this would have been buildable, at least not without pouring enormous effort into tests and manual review. And even the chain only gets you so far. Matching rskj across every existing header proves the node reproduces rskj’s past behavior. It does not prove the node is correct. Some block years from now could take a path nothing has ever exercised and hit a bug that’s been waiting the whole time. That problem gets much bigger once you add execution, which is where this is heading.
Eventually, after a long stretch of fixing, rustock validated every header from genesis. The last missing piece was a follow mode, so it could keep verifying new headers as they arrived instead of stopping at the tip. I had the agent improve the logging along the way and, finally, write a README. And that was it: a Rootstock light node, in Rust, without a line of code from me.
That could have been the end of the post. It wasn’t.
From light node to full node
If the node could follow the header chain, the next question wrote itself: could it be a full node? I asked the agent what was still missing. In my head the answer was “just execution and transaction handling.” That “just” was doing a lot of hiding. Underneath it sat state trie persistence, transaction receipts, gas accounting, and the two precompiles I had been dreading, the Bridge and REMASC.
The method didn’t change from the light node. The tooling did. Opus was faster and needed fewer round trips than Gemini, though it also burned through my quota faster, which became its own bottleneck. The two hardest parts were exactly where I expected the pain. Execution I handled by taking revm and bending it to Rootstock’s quirks. The precompiles were worse. The Bridge in particular took several attempts, because the agent kept finding polite ways to postpone it. It is a large, complex contract and the model knew it. I kept the scope tight: everything needed to sync the chain, and nothing else, which meant no mining.
After enough iterations, Opus assured me the full node could download and validate the entire chain. It could not.
The full sync, for real this time
Now I actually had to run the thing end to end. This is where I switched to Claude Code, and it changed the shape of the work. I’d already been through a header sync, so I knew what was coming: a lot of bugs, probably more than last time. Doing that by hand, block by block, would have taken forever. So instead of driving, I wrote the rules down and dropped a CLAUDE.md in the repo:
- The goal is a full RSK Mainnet sync from genesis to tip, as fast as possible.
- rustock must stay 100% compatible with rskj.
- Reuse rskj’s own unit tests wherever possible, especially the ones with hardcoded groundtruth like an expected output hash.
- Fix anything that serves the sync without asking first: any bug, including latent ones that won’t surface until later; anything that makes a divergence harder to debug, like missing or noisy logging; and any change that makes the sync measurably faster.
- On consensus-critical paths, only make a change if it is provably behavior-preserving, with the tests and a groundtruth resync still matching. Otherwise, log it and leave it to me.
- Flag any place where rskj’s consensus depends on a quirk of the Java implementation rather than on Rootstock’s design, such as byte-exact encodings, iteration order, or overflow and rounding. A from-scratch client could implement these to spec and still fork.
- Treat the local database as disposable: wipe it when a fix invalidates stored state, but prefer resuming the sync over starting from genesis again.
With that in place, I could point the agent at the goal and let it run in the background. Claude would build and start the node, watch the logs until the sync errored out or stalled, fix whatever broke, write a test if it needed one, and start the sync again. I could go to sleep and let it work. The only real ceiling was quota. My one complaint about Claude Code is that it has no way to pause and pick a task back up on its own once it runs out of tokens.
The sync did what I expected and coughed up bug after bug. Getting a clean sync actually took longer than turning the light node into a full node in the first place, purely because of how many bugs there were. That tells you my build process was sloppier than I’d like to admit; I brute-forced a fair amount. The agent also suggested a couple of real improvements on its own: downloading blocks in parallel, and pipelining the work so it fetched the next batch while executing the current one. In stretches of the chain, that made a real difference.
The bugs behaved the way they had in the light node. New blocks kept reaching corners of the code that earlier blocks never touched, and each corner had something wrong in it. I’d assumed the rate would taper off as the node climbed to higher blocks. It didn’t. I was still finding bugs at roughly the same pace near the tip as near genesis. My best guess is that it comes down to hard forks and RSKIP activations: every fork switches on new behavior and new code paths that nothing before it had tested. The Bridge and REMASC accounted for a big share of these, both because they’re complex and because some of their paths, like a federation change, almost never come up.
Another class of bug came from the implementation itself. Every so often the agent traced a divergence to a low-level detail of how rskj does something: the byte encoding of a BigInteger, the internal ordering of a HashSet. Details like that end up baked into a hash or a stored value, so rustock would compute a perfectly reasonable result that still didn’t match rskj’s. This is what it means to work from a reference implementation rather than a written spec. rskj defines the consensus, so a second client has to match its behavior down to these details, not just the high-level design. It is the same model Bitcoin has always run on, with Bitcoin Core as the de facto specification. Reproducing those details exactly was part of the job.
Two things made the later stretch of the sync much easier, and I found both further along than I should have. First, the agent stumbled onto an old rskj database I’d left on disk and started treating it as groundtruth, which let it compare rustock’s state against rskj’s down to the individual storage cell after any transaction. That database only reached block 3 million, though, so past that height there was nothing to check against. Second, and more embarrassingly, it took me far too long to realize I could download a full, up-to-date snapshot of the rskj database. That did two things at once: rustock could take the blocks straight from it instead of downloading them over the network, and its state gave me groundtruth all the way to the tip.
Around 80% of the way through the sync, I ran into a different kind of problem: the node kept losing its peers. The peer set seemed to degrade over time, and I suspect some peers were banning it outright. The sync slowed to a crawl. The cause was obvious in hindsight. I had never stress-tested the networking layer, because it isn’t part of consensus and all my attention had gone to the parts that could fork the chain. So I set one agent on the networking code while another kept the sync running. It found a couple of issues in how the node was handling its connections and fixed them, though I wouldn’t bet it caught all of them. There’s probably more worth digging into there later.
The other running cost was context. Most of my token spend went to an ever-growing context window. Claude Code compacts context automatically when it gets close to full, but it won’t let you trigger a compaction yourself, which is a shame; I would have cleaned the context after every fix. At some point I started spawning a subagent per bug. That helped, with caveats. Spawning one has a fixed cost, so the bug has to be meaty enough to justify it, and if a subagent dies partway through, from quota or anything else, its work dies with it. On balance I think subagents saved me tokens. I can’t prove it.
Fable 5 solved tasks faster than Opus, but it didn’t give me the same lift, at least not while grinding through sync bugs. It ran out of quota sooner, so the work I got done per session came out about the same.
And then, after a great many fixes, it worked. The node downloaded and executed the entire chain and kept pace with new blocks. A full Rootstock node, in Rust, and still not a line of code written by hand. The part I didn’t expect: it synced the whole chain in about 24 hours, well under what rskj takes. My guess is that it comes down to some mix of the parallel downloading and pipelining and Rust being faster than Java. But I don’t actually know why, and a gap that size is worth investigating properly rather than hand-waving at it.
Taking stock
This was one of the more fun things I’ve built in a while. I came out of it a lot more comfortable with the current crop of AI tools, and a lot more aware of where my own process went wrong. I feel like I brute-forced too much. With more planning up front, a lot of the later pain would have been avoidable. Then again, I’m wary of the opposite failure, spending days studying and planning with nothing to show for it. Building something rough and learning as I went did get the job done. Could I have done it faster, or for fewer tokens? Maybe. If you have ideas about how, I’d like to hear them.
I should also be honest about what I haven’t done. I’ve barely read rustock’s source. It’s probably a mess in places: redundant, over-engineered, full of the kind of slop an agent leaves behind. The natural next step is a serious refactor to simplify it and make it properly idiomatic, which I did for the light node but never for the full one. After that, a proper security review. I fully expect rustock is still full of bugs, and quite possibly security holes.
Which is the whole reason for the disclaimer at the top. Use it at your own risk. It could fork the network without warning, and it hasn’t been hardened against eclipse attacks or other network-level trouble. Neither I nor RootstockLabs are responsible for what anyone does with it. And to be clear, RootstockLabs has no plans to take rustock to production in the near future; it stays a personal experiment for now. Where it might actually be useful is low-stakes territory: a sandbox to experiment in, or a read-only node behind something like a block explorer.
Don’t trust, verify
Building rustock made me think about a bigger question: is the blockchain domain ready to trust 100% agent-generated code? Two narrower versions of it stuck with me, and I have a clean answer to neither. Could a better process and better testing have produced code that fully syncs on the first try? And can agents carry rustock the rest of the way to production, security audit included?
Doing the port by hand would have been a massive undertaking, and AI shrank it to something one person could do on the side. But shipping to production is still a lot of work, and I can’t tell whether the AI saved us time and money or just moved that work downstream, into reviewing, testing, and auditing. Both questions are really about that downstream work.
Take the first one. A better process and a better test suite would certainly have cut the number of bugs. But code that syncs the whole chain on the first try, with no bug-fixing in between? I doubt it. For one, the unit tests were written by an agent too, and I didn’t review them. For another, even with most of rskj’s own tests ported over, and some of them ported wrong, the number of bugs the sync turned up was staggering. The full sync was the ultimate test, and the agent failed it miserably.
And it would have failed no matter how good my tests were, because a whole class of bugs is close to impossible to catch in a test suite at all. In this project they came in three kinds. One is emergent behavior: bugs that arise from the timing and order of interactions across the network, rather than from what any single function does. The networking problems were exactly this, and you can’t write a unit test for a behavior you can’t picture. Another is execution divergence. Emulating a virtual machine like the EVM opens up an enormous space of states and paths, and a subtle mismatch in any of them produces a different result. The precompiles, the Bridge above all, are the sharpest edge of this. The third comes from the reference implementation itself, like the way rskj encodes a BigInteger, where being correct means matching what the original client does rather than any written spec.
What protected me from all three was groundtruth. A written spec would have helped, but a reference implementation gives you the next best thing: a live chain to diff against and rskj itself. That was enough. The uncomfortable question is what happens when even that isn’t there. Take away the chain and the reference client, and no amount of testing I’m realistically going to write would have kept these bugs out of the code.
There is a counter-argument, though. Are humans actually any better at catching those bugs than an agent is? An agent doesn’t have to write perfect code, or run a perfect review. It only has to be as good as a human would be.
That brings me to the second question. Should the security review be done by a human at all, or can we hand it to a set of agents? The newest models are getting better at finding security problems, and we have been seeing it in the blockchain domain lately. But finding a problem and fixing it correctly are not the same thing. Would I trust it if agents did the security review and implemented the fixes by themselves? Not today. Not in a domain where a bug can cost me my savings. Though I do believe that the day when we can fully trust agent-generated software will come eventually. Do you?
Originally published at medium.com/@javicid/a-blockchain-node-written-entirely-by-ai-can-we-trust-it-06ffaf8fad6d.
