Skip to content

fix(smt): bind insert result before debug_assert to keep side effect#4

Open
TaprootFreak wants to merge 1 commit intoZeroSync:masterfrom
TaprootFreak:fix/smt-debug-assert-side-effects
Open

fix(smt): bind insert result before debug_assert to keep side effect#4
TaprootFreak wants to merge 1 commit intoZeroSync:masterfrom
TaprootFreak:fix/smt-debug-assert-side-effects

Conversation

@TaprootFreak
Copy link
Copy Markdown

Summary

SparseMerkleTree::insert wraps two mutating HashMap::insert calls inside debug_assert! macros. In release builds these calls are silently dropped, so the tree state is never updated.

Mechanism

debug_assert_eq!/debug_assert_ne! expand to:

if cfg!(debug_assertions) { assert!(...) }

Under --release the workspace has no [profile.release] override, so debug_assertions is false. The compiler eliminates if false { ... } entirely — the argument expression, including any side effects, is never evaluated.

The current code in program/src/merkle/sparse_merkle_tree.rs:

debug_assert_eq!(self.leaf_values.insert(key, leaf), None);   // L273
// ...
debug_assert_ne!(self.nodes.insert((0, [0; 32]), current_hash), Some(current_hash));  // L299

In release, neither HashMap::insert runs. Result: leaf_values stays empty, the root node mapping stays empty, tree.root() always returns ZERO_HASH. Any consistency check that compares the post-insert root against an externally computed root (e.g. via NonInclusionProof::insert) diverges.

Reproduction

On master (a91939f), no other changes needed:

cargo test -p zkcoins-program --lib merkle::sparse_merkle_tree
# test result: ok. 10 passed; 0 failed.

cargo test --release -p zkcoins-program --lib merkle::sparse_merkle_tree
# test result: FAILED. 2 passed; 8 failed.

The eight failing tests cover both inclusion and non-inclusion paths, including test_verify_and_insert and test_verify_and_insert_sibling.

CI (prove.yml) only builds the SP1 program and does not run cargo test in either profile, which explains why this has gone undetected.

Standalone illustration

For anyone who wants to see the macro behavior outside this codebase:

use std::collections::HashMap;

fn main() {
    let mut map: HashMap<i32, i32> = HashMap::new();
    debug_assert_eq!(map.insert(1, 100), None);
    println!("len = {}", map.len()); // debug: 1, release: 0
}

cargo run prints len = 1. cargo run --release prints len = 0.

Fix

Bind the HashMap::insert result to a local before the assertion. Mutation runs in both profiles; the assertion stays debug-only as originally intended.

Diff: 8 insertions, 4 deletions, both in program/src/merkle/sparse_merkle_tree.rs.

Test plan

  • cargo test -p zkcoins-program --lib merkle::sparse_merkle_tree — 10/10 pass
  • cargo test --release -p zkcoins-program --lib merkle::sparse_merkle_tree — 10/10 pass (was 2/10)

`debug_assert_eq!`/`debug_assert_ne!` expand to
`if cfg!(debug_assertions) { assert!(...) }`. In release builds
`cfg!(debug_assertions)` is `false`, so the entire expression is
eliminated and its argument is never evaluated — including any side
effects.

`SparseMerkleTree::insert` wrapped two mutating `HashMap::insert`
calls inside `debug_assert!` macros. In release this meant
`leaf_values.insert(...)` and the root-node `nodes.insert(...)` were
never executed: the tree state was never updated, `tree.root()` always
returned `ZERO_HASH`, and any code that compared the post-insert root
against an externally computed root (e.g. via `NonInclusionProof::insert`)
would diverge.

Reproduction on master:

  cargo test -p zkcoins-program --lib merkle::sparse_merkle_tree
  # 10/10 pass

  cargo test --release -p zkcoins-program --lib merkle::sparse_merkle_tree
  # 8/10 fail, including test_verify_and_insert and
  # test_verify_and_insert_sibling

Fix: bind the `HashMap::insert` result to a local first, then run the
assertion on the local. The mutation now always runs; the assertion
remains debug-only as originally intended.
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