Skip to content

Reconcile against the real pool balance before freezing a checkpoint - #255

Merged
Marketen merged 1 commit into
mainfrom
marc/reconcile-before-checkpoint
Aug 14, 2026
Merged

Reconcile against the real pool balance before freezing a checkpoint#255
Marketen merged 1 commit into
mainfrom
marc/reconcile-before-checkpoint

Conversation

@Marketen

Copy link
Copy Markdown
Contributor

The gap

RunOffchainReconciliation was the only check gating checkpoint submission. It compares the oracle's books against themselves, so it passes whenever they are internally consistent — even when they disagree with the chain.

It cannot detect:

  • a reward credited for money that never arrived
  • money that arrived and was never allocated

In both cases liabilities and modelled assets move together, so it balances. It passed during the failed run on 2026-08-13, an hour before the real check found a 96,156,146,415,005,252 wei shortfall.

The check that reads the actual pool balance ran only on a 3-hour timer, and only once the oracle was caught up. So a root could be published while the books were already wrong — and with quorum 1/1 it consolidates unopposed.

Why it matters concretely

A subscriber whose forced payment arrives in a different amount than its paying tx states is classified wrong-fee and banned, while the ETH sits unallocated. Bans are only reversible by governance, so publishing that state is expensive to undo.

Reconciliation would catch the accounting gap eventually — but potentially after the root was already on chain.

The change

Run RunOnchainReconciliation before FreezeCheckpoint, and refuse to freeze if it fails.

Why it was disabled

The original comment reads:

// TODO: Temporally commented. Doesn't work well with non archival node

It needs execution state at the processed block, which a non-archival node cannot serve while the oracle is catching up — it fails with missing trie node. That constraint is a deployment choice, not a property of the check: point --execution-endpoint at a node that retains state history and it works. Verified against a reth archive node, where the equivalent query at an arbitrary historical block returns correctly.

Cost

One balance read plus one claimedBalance call per withdrawal address, at each checkpoint only (every 28,800 slots ≈ 4 days). Measured at ~17s for 440 addresses.

Note for operators

If your execution endpoint cannot serve state at LatestProcessedBlock, the oracle will now stop at the checkpoint rather than publish an unverified root. That is the intended trade: refusing to decide beats deciding wrongly.

🤖 Generated with Claude Code

RunOffchainReconciliation compares the oracle's books against themselves,
so it passes whenever they are internally consistent, even when they
disagree with the chain. It cannot see a reward credited for money that
never arrived, nor money that arrived and was never allocated: both sides
of its comparison move together.

That was the only check gating checkpoint submission. The one that reads
the real pool balance ran on a three hour timer and only once the oracle
was caught up, so a root could be published while the books were already
wrong, and with a quorum of one it would consolidate unopposed.

Concretely, a subscriber whose forced payment arrives in a different amount
than its paying tx states is classified wrong fee and banned, and the ETH
is left unallocated. Bans are only reversible by governance, so publishing
that state is expensive to undo.

Run the onchain reconciliation before freezing, and refuse to freeze if it
fails. It needs execution state at the processed block, which is why it was
disabled: a non archival node cannot serve it during catch up. Point
--execution-endpoint at a node that keeps state history.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Marketen
Marketen merged commit 284123c into main Aug 14, 2026
7 checks passed
@Marketen
Marketen deleted the marc/reconcile-before-checkpoint branch August 14, 2026 11:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant