fix(evm): NEDB single source of truth — remove executor cache split-brain + oracle nonce clobber#84
Open
Eth-Interchained wants to merge 1 commit into
Open
Conversation
…che split-brain
Two field bugs, one root cause, found live on mainnet 2026-07-06:
burns of freshly-minted aITC reverted ("nothing was burned"), and
plain aITC sends toasted success but transferred nothing.
Root cause 1 — the long-lived CacheDB. ItcEvm held a CacheDB<NedbState>
created once at boot and never invalidated. The bridge oracle credits
deposits by writing straight into NEDB (evm_accounts), so any account
already resident in the cache kept its stale pre-mint balance for
EXECUTION while RPC reads (which already bypassed the cache via
NedbState::basic) served the true balance. Wallet says 184 aITC,
execution says less: burn reverts, send dies. Two views of one account,
silently diverged.
Root cause 2 — the mint clobbered the nonce. oracle.rs::mint_net wrote
the account document with a hardcoded nonce: 0, erasing the account's
real transaction count on every deposit. Wallets then signed with the
clobbered nonce, execution rejected the mismatch, and every send/burn
after a mint silently died. With the cache removed this would have
graduated from "txs fail" to "nonce-0 replay accepted" — fixed in the
same change, mint now preserves nonce and code_hash and touches ONLY
the balance.
Changes:
- itc-evm/state.rs: NedbState now implements revm's Database (mutable)
by delegating to its DatabaseRef impl — revm executes directly
against live NEDB state. commit_changes() rewritten to emit the whole
post-tx state diff as ONE atomic put_batch (a transfer's debit and
credit land together or not at all) and now returns Result so a
failed durable commit is a failed execution, never a phantom success.
- itc-evm/executor.rs: ItcEvm.cache (CacheDB) removed; the executor
holds NedbState directly. execute_tx = transact + atomic NEDB commit.
simulate_tx reads the same live state as real execution.
seed_genesis_account writes NEDB only.
- itc-rpc/handler.rs: evm_balance/evm_nonce/evm_code read evm.state —
RPC and execution now share one state view by construction.
- itc-oracle/oracle.rs: mint_net reads the full existing account and
preserves nonce + code_hash; only the balance changes on a mint.
Intra-tx consistency needs no cache: revm's JournaledState loads each
touched account once per transaction and journals all intermediate
reads/writes internally. The database only has to be right at
transaction boundaries — which is exactly when NEDB is now consulted.
Per-block cost is a handful of NEDB point reads (pread-cached since
v2.6.1) every 5s: noise.
Tests:
- external_nedb_write_visible_to_next_execution — THE regression: an
oracle-style direct NEDB credit must be spendable by the very next
execution (fails on the old cached design).
- nonce_reads_live_state_across_executions — sequential nonces validate
against durable state; stale-nonce replay rejected.
- mint_preserves_existing_nonce_and_adds_balance — mint leaves nonce 7
alone, adds exactly net_sats, keeps code_hash.
- transfer_persists_to_nedb_with_provenance extended to assert the
debit and credit land together (atomic batch).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Removes the executor's long-lived
CacheDBso NEDB is the single source of truth for all L2 EVM state, and fixes the deposit oracle's nonce clobber. Together these fix both bugs Mark hit live on 2026-07-06: burns of freshly-minted aITC reverting ("nothing was burned"), and aITC sends that toasted success with a real txid but transferred nothing.Why this matters
The system had two memories of the truth. The oracle minted deposits straight into NEDB; the executor validated transactions against a boot-time in-memory cache that never heard about those writes. RPC reads (
eth_getBalance,eth_getTransactionCount) already bypassed the cache — so the wallet showed the true balance while execution used a stale one. Depending on the direction of divergence you got an honest failure on good funds (burn revert) or a dishonest success on nothing (phantom send). On top of that, every mint hardcodednonce: 0into the account document, erasing real transaction history — wallets then signed with a nonce execution rejected.Engine impact (itc-evm)
NedbStateimplements revm'sDatabasedirectly (delegates to itsDatabaseRefimpl) — execution reads live NEDB, no cache layer.commit_changes()emits the whole post-tx diff as one atomicput_batch— a transfer's debit and credit land together or not at all — and returnsResult, so a failed durable commit is a failed execution. No phantom successes by construction.ItcEvm.cacheis gone; the struct holdsstate: NedbState.execute_tx= transact + atomic commit.simulate_tx(eth_call / estimateGas) reads the same live state as real execution.Oracle impact (itc-oracle)
mint_netnow reads the full existing account and preservesnonce+code_hash— a mint changes the balance and only the balance. (Pre-fix, with the cache removed, the clobber would have graduated from "txs fail" to "nonce-0 replay accepted" — that's why both fixes ship in one PR.)Wallet/bridge impact
No Elara changes required. Mint → immediate burn now validates against the true balance; sends after a mint sign with a nonce execution accepts. Exit detection, receipts, fees, and the exactly-once payout guard are untouched.
Tests run
Written blind — no Rust toolchain in the sandbox, so these are written but not executed here. Please run:
New/updated tests:
external_nedb_write_visible_to_next_execution— THE regression: an oracle-style direct NEDB credit must be spendable by the very next execution. Fails on the old cached design.nonce_reads_live_state_across_executions— sequential nonces validate against durable state; stale-nonce replay rejected.mint_preserves_existing_nonce_and_adds_balance— mint leaves nonce 7 alone, adds exactly net_sats, keeps code_hash.transfer_persists_to_nedb_with_provenance— extended to assert debit + credit land together (atomic batch).Verification sequence after deploy (falsifiable)
success: true.Follow-up work (not this PR)
Errfrom execute_tx in the receipt as a distinctdroppedstatus (currentlysuccess: falsereceipts cover both reverts and pre-validation drops).🤖 Generated with Claude Code