We replaced an interactive bisection game with a one-shot disproof, and the argument for doing it was sound. The premise it rested on was not. An account of how a correct deduction from a false assumption nearly shipped.
Croom's chain does not store prices. It stores a 32-byte Merkle root per epoch — a batch of rounds — posted by somebody who escrows a bond asserting it is the correct continuation of the previously finalised state. If nobody objects within a challenge window, it finalises. Optimistic, in the usual sense.
The reference implementation, which runs on Solana, resolves disputes by bisection: the challenger and the poster alternately commit to the midpoint of a disputed range until they disagree about a single round, which is then re-executed on-chain. Seven rounds of interaction for a hundred-round epoch.
When porting to the EVM under a deadline, we cut bisection. This is the argument we used, and I still think every sentence of it is true.
Bisection exists for games where the challenger cannot cheaply compute the truth and must search for the divergence interactively. That is the situation in an optimistic rollup: re-executing a disputed block is expensive, so you binary-search to isolate one instruction.
It is not our situation. The derivation is deterministic, pure, and defined entirely by public data — a frozen specification plus a public randomness beacon. A challenger running the reference implementation already knows precisely which round diverges. There is nothing to search for.
Better still, a correct epoch is exactly one where three locally-checkable things hold:
Each is checkable in a single transaction. So if a committed root is wrong, at least one of them fails — and naming the failure is a complete disproof, not an approximation of one. There is nothing bisection could find that a challenger with the reference cannot name directly.
Two consequences follow, and both are improvements:
We implemented it, wrote an adversarial suite that plants a lie at every index in the epoch and asserts the poster loses at each one, fuzzed the bond conservation, and shipped it to testnet. A liar posted a false checkpoint and a second address took their bond. All green.
Three days before the deadline, re-reading that argument looking for its premises rather than its conclusion, I found this:
A disproof must exhibit a committed leaf together with its Merkle path. Nobody can construct a path for a leaf they have never seen.
The contract stored a root. Our post function took a bytes32 and
emitted it. A poster could therefore commit a root and simply never reveal its
preimages.
Walk through what happens then.
An honest watcher re-derives the epoch from the beacon, computes its own root, and sees that it differs from the committed one. They know the checkpoint is false. Now they try to prove it — and every entry point demands a leaf and a path. They have their own leaves, which are not in the poster's tree, so their proofs fail membership. They have no way to obtain the poster's leaves, because the poster never published them and is not obliged to.
The checkpoint survives the window and finalises. And from that moment the poster is the only party who can produce a valid inclusion proof against that root — which means the poster alone decides what price any market settles against.
Every test still passed, because every test constructed the liar's leaves itself. The test harness was supplying the data availability that the protocol did not.
This is the part worth internalising. Bisection is usually described as a search procedure, and that framing is what misled us. It is also a forced-disclosure procedure. Every round of the game compels the poster to commit to a midpoint state, on-chain, or forfeit by timeout. By the time the game terminates, enough of the disputed range has been disclosed for the final step to be checkable.
Data availability was never an explicit feature of the reference's design. It fell out of the interaction pattern. We removed the interaction pattern and the property went with it, silently, because nothing in the code was labelled "this is what makes disproofs constructible".
The deduction was valid. The premise — that a challenger can exhibit the committed leaves — was an assumption we never wrote down, and therefore never checked.
post now takes the leaves themselves and derives the root from them:
function post(
RoomSpec.Spec calldata spec,
uint64 epoch,
bytes32 prevStateHash,
bytes32 endStateHash,
PostedLeaf[] calldata leaves
) external payable
Three properties follow, and the second was a bonus we had not been looking for.
The preimages are in the transaction's calldata and in the emitted log. A disproof is always constructible by anyone who can read the chain. The premise is now enforced rather than assumed.
Because the contract walks the leaves to build the root, it can insist that the rounds are exactly the epoch's, contiguous and ascending. That closes a hole we had not noticed either.
Our Merkle construction uses sorted pairs — node(a,b) =
H(min(a,b) ‖ max(a,b)) — so proofs carry no direction bits. The usual advantage is
smaller proofs and one fewer class of bug. The cost, which we had not thought through, is
that a sorted-pair proof cannot bind a leaf's position.
So a poster could have committed a tree containing two different leaves for the same round. Both would verify. At settlement they would present whichever suited them. Checking the shape at post time makes that tree unpostable, rather than merely disprovable by somebody paying attention.
It is computed from them. There is no longer a poster-supplied root to be inconsistent with anything.
Measured against the live contract on Monad, with eth_estimateGas rather than
a local simulation — a distinction that matters, and which we learned the expensive way:
| Variant | gas |
|---|---|
| Root only | 107,421 |
| With 20 leaves published (3,044 bytes of calldata) | 209,976 |
| Price of the guarantee | +102,555 |
About 0.011 MON per checkpoint at current prices. It scales at 56 bytes per round, so a 100-round epoch costs roughly five times as much and remains comfortably inside Monad's 30M per-transaction limit.
That is cheap for turning a fraud proof from plausible into sound. It is also cheaper than bisection, which would have cost seven on-chain interactions per dispute plus bonds on both sides plus the timeout machinery to handle a party that walks away.
Measuring that figure produced a second lesson. forge script estimated 273,128
for the heavier post, against a real cost of 209,976 — it
over-estimated by 1.3×.
Two days earlier the same tool had under-estimated
Entropy.proveRound by 2.42×, and the transaction died out of gas. Monad charges
the gas limit rather than the gas used, so that failure was billed in full.
The correction factor is a property of the transaction, not the chain. Monad reprices BN254 operations upward by up to 5× while calldata is priced identically to Ethereum — so a pairing-heavy call swings one way and a calldata-heavy call swings the other. Across three real transactions the range is 0.77× to 2.42×, in a direction you cannot infer from reading the source. There is no safe multiplier. Measure each call shape against the deployed contract.
The failure was not a coding error and no test would have caught it, because the tests shared the assumption. It was a design argument whose conclusion we checked and whose premises we did not.
The practical version, which we now apply deliberately: when you remove a mechanism because you have an argument that it is unnecessary, enumerate what that mechanism was incidentally providing. Interactive protocols in particular tend to supply liveness and data-availability properties as side effects of their interaction, and those properties rarely have a name anywhere in the code.
The one-shot disproof survives, and it is genuinely a better fit for a deterministic, publicly-derivable state function than bisection is. But it only works because posters are forced to publish — and that sentence is now in the contract's documentation, immediately above the function that enforces it, where the next person to consider an optimisation will read it.
The commit that fixed this is
fix(ladder): publish every leaf at post time so a disproof is always possible,
and the live fraud proof on the hardened contracts is
linked from the front page.