diff --git a/grovedb/src/operations/proof/mod.rs b/grovedb/src/operations/proof/mod.rs index c10681c4b..74784f1c9 100644 --- a/grovedb/src/operations/proof/mod.rs +++ b/grovedb/src/operations/proof/mod.rs @@ -1,6 +1,6 @@ //! Proof operations -#[cfg(feature = "minimal")] +#[cfg(any(feature = "minimal", feature = "verify"))] mod aggregate_count; #[cfg(feature = "minimal")] mod generate; diff --git a/merk/src/proofs/query/aggregate_count.rs b/merk/src/proofs/query/aggregate_count.rs index 8cd493986..915de7223 100644 --- a/merk/src/proofs/query/aggregate_count.rs +++ b/merk/src/proofs/query/aggregate_count.rs @@ -12,19 +12,26 @@ //! mechanics). On any other tree type the entry point returns //! `Error::InvalidProofError`. +#[cfg(feature = "minimal")] use std::collections::LinkedList; use grovedb_costs::{cost_return_on_error, CostResult, CostsExt, OperationCost}; +#[cfg(feature = "minimal")] use grovedb_version::version::GroveVersion; +#[cfg(feature = "minimal")] +use crate::{ + proofs::Op, + tree::{kv::ValueDefinedCostType, AggregateData, Fetch, RefWalker}, + TreeType, +}; use crate::{ proofs::{ query::QueryItem, tree::{execute_with_options, Tree as ProofTree}, - Decoder, Node, Op, + Decoder, Node, }, - tree::{kv::ValueDefinedCostType, AggregateData, Fetch, RefWalker}, - CryptoHash, Error, TreeType, + CryptoHash, Error, }; /// All-zero `CryptoHash`, used in `Node::HashWithCount` for missing children. @@ -120,6 +127,7 @@ fn classify_subtree( /// Returns true if `tree_type` is one of the four tree types that can host an /// `AggregateCountOnRange` proof. Wrapper types are accepted by stripping /// down to the inner tree type via `is_provable_count_bearing`. +#[cfg(feature = "minimal")] fn is_provable_count_bearing(tree_type: TreeType) -> bool { matches!( tree_type, @@ -131,6 +139,7 @@ fn is_provable_count_bearing(tree_type: TreeType) -> bool { /// Returns `Err(InvalidProofError)` for any other variant — the entry point /// has already gated `tree_type`, so reaching the error means the tree's /// in-memory state disagrees with its declared type. +#[cfg(feature = "minimal")] fn provable_count_from_aggregate(data: AggregateData) -> Result { match data { AggregateData::ProvableCount(c) => Ok(c), @@ -142,6 +151,7 @@ fn provable_count_from_aggregate(data: AggregateData) -> Result { } } +#[cfg(feature = "minimal")] impl RefWalker<'_, S> where S: Fetch + Sized + Clone, @@ -189,6 +199,7 @@ where /// At entry, `subtree_lo_excl` / `subtree_hi_excl` are the inherited /// exclusive key bounds for the subtree this walker points at (both `None` /// at the root call). +#[cfg(feature = "minimal")] fn emit_count_proof( walker: &mut RefWalker<'_, S>, range: &QueryItem, @@ -654,6 +665,49 @@ fn key_strictly_inside(key: &[u8], lo: Option<&[u8]>, hi: Option<&[u8]>) -> bool mod tests { use super::*; + /// Asserts the hardcoded fixture in the `verify_only_tests` module + /// still matches the bytes a fresh prove run produces. If the proof + /// encoding ever changes, this test fails and prints the new + /// constants — copy them into `verify_only_tests`. + #[test] + fn verify_only_fixture_matches_fresh_prover_output() { + let v = GroveVersion::latest(); + let (merk, root) = make_15_key_provable_count_tree(v); + let inner_range = QueryItem::RangeInclusive(b"c".to_vec()..=b"l".to_vec()); + let (ops, count) = merk + .prove_aggregate_count_on_range(&inner_range, v) + .unwrap() + .expect("prove"); + let proof_hex = hex::encode(encode_proof(&ops)); + let root_hex = hex::encode(root); + + let drift_msg = format!( + "aggregate_count proof encoding has drifted — update verify_only_tests:\n\ + const FIXTURE_15_KEY_C_TO_L_PROOF_HEX: &str = \"{}\";\n\ + const FIXTURE_15_KEY_C_TO_L_ROOT_HEX: &str = \"{}\";\n\ + const FIXTURE_15_KEY_C_TO_L_COUNT: u64 = {};", + proof_hex, root_hex, count + ); + assert_eq!( + proof_hex, + super::verify_only_tests::FIXTURE_15_KEY_C_TO_L_PROOF_HEX, + "{}", + drift_msg + ); + assert_eq!( + root_hex, + super::verify_only_tests::FIXTURE_15_KEY_C_TO_L_ROOT_HEX, + "{}", + drift_msg + ); + assert_eq!( + count, + super::verify_only_tests::FIXTURE_15_KEY_C_TO_L_COUNT, + "{}", + drift_msg + ); + } + fn range_inclusive(lo: &[u8], hi: &[u8]) -> QueryItem { QueryItem::RangeInclusive(lo.to_vec()..=hi.to_vec()) } @@ -1589,3 +1643,110 @@ mod tests { } } } + +/// Verifier-only smoke tests that exercise the leaf-level verifier without +/// pulling in any prover-side machinery (no `TempMerk`, no `RefWalker`). +/// They consume hardcoded fixtures kept in lockstep with the prover by +/// `tests::verify_only_fixture_matches_fresh_prover_output` above — +/// when that drift check fails it prints fresh constants to paste here. +#[cfg(test)] +mod verify_only_tests { + use super::*; + + /// Hex-encoded proof bytes for a 15-key `ProvableCountTree` (keys + /// "a"..="o", each with feature `ProvableCountedMerkNode(1)`) queried + /// with `RangeInclusive("c"..="l")`. Captured from + /// `dump_verify_only_fixtures`; regenerate if the proof encoding ever + /// changes. + pub(super) const FIXTURE_15_KEY_C_TO_L_PROOF_HEX: &str = "1e76f2d62fbefb076d8902b2f25bcf9acbd1e903b740c98ea0d926473922f6bbb50000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011a01622022ec9d571ba774cf9e83d0194962f5d1e3aa1a48d486a67e2762a6c79590150000000000000003101a0163b7d770040f780e9deff6bc038abea66e108b88d098d16d24cd7486eb671060b20000000000000001111a0164d2ad1a0bb9fdf4450bd87151c08b9968cd046bda6654aabdba2430b0a981e7900000000000000007101e28b724715b1fab1f72e0be7e944488dcfbeeb875867d27c06ad9bad8c739997207ce95cd4d1789e01c0a079a3f2c18a3888f2d69fa6d0eabf51c2b434f7cb99e212f1a0042798fb890e8203f007f4f58b033a72cb4e070bfddceb27687d641fe0000000000000003111a01683fc14ed7ecde203a90425ee191e9db5966336d737f0398ec93b764517b6df400000000000000000f101e6da2e2f8e4bdead2a8ac51909f0fa0fb88d47d6bc3b84858bb739fb28a36501031b7c191d5ac70764f815bd7a6c7d0e628f48cef5b813933c07d5ce0ac1dbd5a995443ca10193ebf20e64468deaecc061a981a6dbf4f30e7154b5e9ab806866d00000000000000031a016c55c024f95ca4cc338f7cc2e25db37be2a3fa3a40b151017e460bfc0779cf369f0000000000000007101e3673296561a4d6c3e1ec5cd02c5c468acbd3c8ccd4a42906e8ed06d3fb587a0d2b6d9e310b7c94d3f91fcbb3d5f7547b76c6d1ab3ac3d3540752c5f0b46be24a2f66bf541434a53eae46fa4e6092c03511538c0e1a2c5fc0f0deb72de08a71e500000000000000031111"; + pub(super) const FIXTURE_15_KEY_C_TO_L_ROOT_HEX: &str = + "19ed16776ebe6643b342a238baf7508ddf687fc4bdd53e98f91df8bffb605d96"; + pub(super) const FIXTURE_15_KEY_C_TO_L_COUNT: u64 = 10; + + /// Empty proof bytes encode "empty merk" — the verifier returns + /// `(NULL_HASH, 0)`. This case has no prover dependency at all and is + /// the most basic compile-time signal that the verifier path is wired + /// up correctly under `--no-default-features --features verify`. + #[test] + fn empty_merk_returns_null_hash_and_zero_count() { + let inner = QueryItem::Range(b"a".to_vec()..b"z".to_vec()); + let (root, count) = verify_aggregate_count_on_range_proof(&[], &inner) + .unwrap() + .expect("verify on empty proof must succeed"); + assert_eq!(root, NULL_HASH); + assert_eq!(count, 0); + } + + /// A real `RangeInclusive("c"..="l")` proof against a 15-key + /// `ProvableCountTree`. The verifier must reconstruct the expected + /// merk root hash and recover count = 10. + #[test] + fn fixture_15_key_range_c_to_l_verifies() { + let proof = hex::decode(FIXTURE_15_KEY_C_TO_L_PROOF_HEX).expect("valid hex"); + let mut expected_root = [0u8; 32]; + expected_root + .copy_from_slice(&hex::decode(FIXTURE_15_KEY_C_TO_L_ROOT_HEX).expect("valid hex")); + + let inner = QueryItem::RangeInclusive(b"c".to_vec()..=b"l".to_vec()); + let (root, count) = verify_aggregate_count_on_range_proof(&proof, &inner) + .unwrap() + .expect("fixture proof must verify"); + assert_eq!( + root, expected_root, + "verifier reconstructed an unexpected root — fixture stale?" + ); + assert_eq!(count, FIXTURE_15_KEY_C_TO_L_COUNT); + } + + /// Mutating any single byte of the fixture proof must not yield a + /// `(honest_root, wrong_count)` outcome — the hash chain binds count via + /// `node_hash_with_count`, so any successful verify with the honest root + /// must reproduce the honest count. Single-fixture analogue of + /// `fuzz_byte_mutation_no_silent_forgery` that runs without the prover. + #[test] + fn fixture_byte_mutation_does_not_silently_forge_count() { + let proof = hex::decode(FIXTURE_15_KEY_C_TO_L_PROOF_HEX).expect("valid hex"); + let mut expected_root = [0u8; 32]; + expected_root + .copy_from_slice(&hex::decode(FIXTURE_15_KEY_C_TO_L_ROOT_HEX).expect("valid hex")); + let inner = QueryItem::RangeInclusive(b"c".to_vec()..=b"l".to_vec()); + + // Sanity check: the honest fixture verifies under the same code path + // the mutation loop will exercise. Without this, an `Err`-on-honest + // fixture would silently make every mutation a vacuous pass. + let (honest_root, honest_count) = verify_aggregate_count_on_range_proof(&proof, &inner) + .unwrap() + .expect("honest fixture must verify (regenerate fixture if this fails)"); + assert_eq!(honest_root, expected_root); + assert_eq!(honest_count, FIXTURE_15_KEY_C_TO_L_COUNT); + + for byte_idx in 0..proof.len() { + for &delta in &[1u8, 0x55, 0xff] { + let mut bytes = proof.clone(); + let original = bytes[byte_idx]; + let mutated = if delta == 0xff { + original ^ 0xff + } else { + original.wrapping_add(delta) + }; + if mutated == original { + continue; + } + bytes[byte_idx] = mutated; + if let Ok((root, count)) = + verify_aggregate_count_on_range_proof(&bytes, &inner).unwrap() + && root == expected_root + { + assert_eq!( + count, FIXTURE_15_KEY_C_TO_L_COUNT, + "SILENT FORGERY at byte {} (delta=0x{:02x}): \ + verifier returned the honest root but a wrong count \ + ({} != {}).", + byte_idx, delta, count, FIXTURE_15_KEY_C_TO_L_COUNT + ); + } + // Err and Ok-with-different-root are both safe outcomes. + } + } + } +} diff --git a/merk/src/proofs/query/mod.rs b/merk/src/proofs/query/mod.rs index 1fd556a2f..fcff0ad2b 100644 --- a/merk/src/proofs/query/mod.rs +++ b/merk/src/proofs/query/mod.rs @@ -5,14 +5,14 @@ pub use grovedb_query::*; #[cfg(test)] mod merk_integration_tests; -#[cfg(feature = "minimal")] +#[cfg(any(feature = "minimal", feature = "verify"))] pub mod aggregate_count; #[cfg(any(feature = "minimal", feature = "verify"))] mod map; #[cfg(any(feature = "minimal", feature = "verify"))] mod verify; -#[cfg(feature = "minimal")] +#[cfg(any(feature = "minimal", feature = "verify"))] pub use aggregate_count::verify_aggregate_count_on_range_proof; #[cfg(feature = "minimal")]