Description
At startup, load_mmr (crates/store/src/state/loader.rs) rebuilds the in-memory chain MMR from DB block-header commitments and wraps it with Blockchain::from_mmr_unchecked:
let mmr = Mmr::try_from_iter(block_commitments.into_iter().map(BlockHeaderCommitment::word))
.expect("loaded MMR exceeds maximum allowed size");
let chain_mmr = Blockchain::from_mmr_unchecked(mmr);
verify_tree_consistency (same file) checks the account and nullifier tree roots against the latest block header, but there is no equivalent check for the chain MMR: the rebuilt peaks are never compared to latest_header.chain_commitment(). So the startup integrity checks enforce the account/nullifier half of the invariant that apply_block enforces per block (NewBlockInvalidChainCommitment), but not the chain-MMR half.
Impact
A corrupted, truncated, or partially-written block_headers table produces a node whose in-memory MMR disagrees with the header chain - and State::load accepts it silently. Because SyncChainMmr deltas, GetBlockHeaderByNumber proofs, and SyncNotes paths are all computed from that in-memory MMR while headers are read from the DB, the node serves internally consistent but wrong data. The mismatch only surfaces far from the root cause: when a client's post-delta closure check fails, or at the next apply_block (NewBlockInvalidChainCommitment).
Severity: startup-integrity / defense-in-depth. Triggering it requires DB corruption or a partial restore, not a remote attacker - but the failure mode is silent serving of bad data, and the cheap load-time check converts that into a clean startup failure at the root cause.
Description
At startup,
load_mmr(crates/store/src/state/loader.rs) rebuilds the in-memory chain MMR from DB block-header commitments and wraps it withBlockchain::from_mmr_unchecked:verify_tree_consistency(same file) checks the account and nullifier tree roots against the latest block header, but there is no equivalent check for the chain MMR: the rebuilt peaks are never compared tolatest_header.chain_commitment(). So the startup integrity checks enforce the account/nullifier half of the invariant thatapply_blockenforces per block (NewBlockInvalidChainCommitment), but not the chain-MMR half.Impact
A corrupted, truncated, or partially-written
block_headerstable produces a node whose in-memory MMR disagrees with the header chain - andState::loadaccepts it silently. BecauseSyncChainMmrdeltas,GetBlockHeaderByNumberproofs, andSyncNotespaths are all computed from that in-memory MMR while headers are read from the DB, the node serves internally consistent but wrong data. The mismatch only surfaces far from the root cause: when a client's post-delta closure check fails, or at the nextapply_block(NewBlockInvalidChainCommitment).Severity: startup-integrity / defense-in-depth. Triggering it requires DB corruption or a partial restore, not a remote attacker - but the failure mode is silent serving of bad data, and the cheap load-time check converts that into a clean startup failure at the root cause.