From 2eb554ad3c8fb9159eb8ae6ba6926641b8f578de Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 21 May 2026 18:33:17 +0700 Subject: [PATCH] Version proof trailing-byte decoding --- .../src/version/grovedb_versions.rs | 1 + grovedb-version/src/version/v1.rs | 1 + grovedb-version/src/version/v2.rs | 1 + grovedb-version/src/version/v3.rs | 1 + .../operations/proof/aggregate_count/mod.rs | 4 +- .../proof/aggregate_count_and_sum/mod.rs | 4 +- .../src/operations/proof/aggregate_sum/mod.rs | 4 +- grovedb/src/operations/proof/mod.rs | 27 ++++++++++--- grovedb/src/operations/proof/verify.rs | 8 ++-- grovedb/src/tests/proof_advanced_tests.rs | 38 ++++++++++++++++++- .../provable_count_provable_sum_tree_tests.rs | 2 +- 11 files changed, 74 insertions(+), 17 deletions(-) diff --git a/grovedb-version/src/version/grovedb_versions.rs b/grovedb-version/src/version/grovedb_versions.rs index 1a75c7378..726056334 100644 --- a/grovedb-version/src/version/grovedb_versions.rs +++ b/grovedb-version/src/version/grovedb_versions.rs @@ -105,6 +105,7 @@ pub struct GroveDBOperationsProofVersions { pub verify_subset_query_with_absence_proof: FeatureVersion, pub verify_query_with_chained_path_queries: FeatureVersion, pub verify_query_get_parent_tree_info_with_options: FeatureVersion, + pub decode_rejects_trailing_bytes: FeatureVersion, } #[derive(Clone, Debug, Default)] diff --git a/grovedb-version/src/version/v1.rs b/grovedb-version/src/version/v1.rs index 43a58e46f..651b931d3 100644 --- a/grovedb-version/src/version/v1.rs +++ b/grovedb-version/src/version/v1.rs @@ -164,6 +164,7 @@ pub const GROVE_V1: GroveVersion = GroveVersion { verify_subset_query_with_absence_proof: 0, verify_query_with_chained_path_queries: 0, verify_query_get_parent_tree_info_with_options: 0, + decode_rejects_trailing_bytes: 0, }, average_case: GroveDBOperationsAverageCaseVersions { add_average_case_get_merk_at_path: 0, diff --git a/grovedb-version/src/version/v2.rs b/grovedb-version/src/version/v2.rs index 46351bf7a..611bce2a5 100644 --- a/grovedb-version/src/version/v2.rs +++ b/grovedb-version/src/version/v2.rs @@ -164,6 +164,7 @@ pub const GROVE_V2: GroveVersion = GroveVersion { verify_subset_query_with_absence_proof: 0, verify_query_with_chained_path_queries: 0, verify_query_get_parent_tree_info_with_options: 0, + decode_rejects_trailing_bytes: 0, }, average_case: GroveDBOperationsAverageCaseVersions { add_average_case_get_merk_at_path: 0, diff --git a/grovedb-version/src/version/v3.rs b/grovedb-version/src/version/v3.rs index 039891bd1..6bcfa9297 100644 --- a/grovedb-version/src/version/v3.rs +++ b/grovedb-version/src/version/v3.rs @@ -164,6 +164,7 @@ pub const GROVE_V3: GroveVersion = GroveVersion { verify_subset_query_with_absence_proof: 0, verify_query_with_chained_path_queries: 0, verify_query_get_parent_tree_info_with_options: 0, + decode_rejects_trailing_bytes: 1, }, average_case: GroveDBOperationsAverageCaseVersions { add_average_case_get_merk_at_path: 0, diff --git a/grovedb/src/operations/proof/aggregate_count/mod.rs b/grovedb/src/operations/proof/aggregate_count/mod.rs index e7179e209..67fa6b19e 100644 --- a/grovedb/src/operations/proof/aggregate_count/mod.rs +++ b/grovedb/src/operations/proof/aggregate_count/mod.rs @@ -118,7 +118,7 @@ impl GroveDb { // alongside the inner-Query shape rules. let inner_range = path_query.validate_leaf_aggregate_count_on_range()?.clone(); - let grovedb_proof = super::decode_grovedb_proof_canonical(proof)?; + let grovedb_proof = super::decode_grovedb_proof_versioned(proof, grove_version)?; let path_keys: Vec<&[u8]> = path_query.path.iter().map(|p| p.as_slice()).collect(); let root_layer = require_v1_envelope(&grovedb_proof, path_query)?; @@ -191,7 +191,7 @@ impl GroveDb { // descent below is skipped (carrier_outer_items is None). let classification = classification::classify_aggregate_count_path_query(path_query)?; - let grovedb_proof = super::decode_grovedb_proof_canonical(proof)?; + let grovedb_proof = super::decode_grovedb_proof_versioned(proof, grove_version)?; let path_keys: Vec<&[u8]> = path_query.path.iter().map(|p| p.as_slice()).collect(); let root_layer = require_v1_envelope(&grovedb_proof, path_query)?; diff --git a/grovedb/src/operations/proof/aggregate_count_and_sum/mod.rs b/grovedb/src/operations/proof/aggregate_count_and_sum/mod.rs index 4194dc0f4..933efee61 100644 --- a/grovedb/src/operations/proof/aggregate_count_and_sum/mod.rs +++ b/grovedb/src/operations/proof/aggregate_count_and_sum/mod.rs @@ -130,7 +130,7 @@ impl GroveDb { .validate_leaf_aggregate_count_and_sum_on_range()? .clone(); - let grovedb_proof = super::decode_grovedb_proof_canonical(proof)?; + let grovedb_proof = super::decode_grovedb_proof_versioned(proof, grove_version)?; let path_keys: Vec<&[u8]> = path_query.path.iter().map(|p| p.as_slice()).collect(); let root_layer = require_v1_envelope(&grovedb_proof, path_query)?; @@ -188,7 +188,7 @@ impl GroveDb { let classification = classification::classify_aggregate_count_and_sum_path_query(path_query)?; - let grovedb_proof = super::decode_grovedb_proof_canonical(proof)?; + let grovedb_proof = super::decode_grovedb_proof_versioned(proof, grove_version)?; let path_keys: Vec<&[u8]> = path_query.path.iter().map(|p| p.as_slice()).collect(); let root_layer = require_v1_envelope(&grovedb_proof, path_query)?; diff --git a/grovedb/src/operations/proof/aggregate_sum/mod.rs b/grovedb/src/operations/proof/aggregate_sum/mod.rs index 52cb322ca..da58eaecd 100644 --- a/grovedb/src/operations/proof/aggregate_sum/mod.rs +++ b/grovedb/src/operations/proof/aggregate_sum/mod.rs @@ -116,7 +116,7 @@ impl GroveDb { // use `verify_aggregate_sum_query_per_key` instead. let inner_range = path_query.validate_leaf_aggregate_sum_on_range()?.clone(); - let grovedb_proof = super::decode_grovedb_proof_canonical(proof)?; + let grovedb_proof = super::decode_grovedb_proof_versioned(proof, grove_version)?; let path_keys: Vec<&[u8]> = path_query.path.iter().map(|p| p.as_slice()).collect(); let root_layer = require_v1_envelope(&grovedb_proof, path_query)?; @@ -187,7 +187,7 @@ impl GroveDb { // descent below is skipped (carrier_outer_items is None). let classification = classification::classify_aggregate_sum_path_query(path_query)?; - let grovedb_proof = super::decode_grovedb_proof_canonical(proof)?; + let grovedb_proof = super::decode_grovedb_proof_versioned(proof, grove_version)?; let path_keys: Vec<&[u8]> = path_query.path.iter().map(|p| p.as_slice()).collect(); let root_layer = require_v1_envelope(&grovedb_proof, path_query)?; diff --git a/grovedb/src/operations/proof/mod.rs b/grovedb/src/operations/proof/mod.rs index 6eca1ec47..1c61303a1 100644 --- a/grovedb/src/operations/proof/mod.rs +++ b/grovedb/src/operations/proof/mod.rs @@ -25,7 +25,8 @@ use std::{collections::BTreeMap, fmt}; pub const MAX_PROOF_DEPTH: usize = 128; /// Decode a serialized [`GroveDBProof`] envelope using the same bincode -/// configuration the prover writes out. +/// configuration the prover writes out and the trailing-byte policy selected +/// by the GroveDB version. /// /// Decoding is canonical: trailing bytes beyond the encoded envelope are /// rejected. Without this check the same `(RootHash, payload)` could be @@ -35,15 +36,31 @@ pub const MAX_PROOF_DEPTH: usize = 128; /// assumption a caller might rely on (caching, deduplication, hashing /// the proof itself). /// -/// Shared by the aggregate-count and aggregate-sum verifier entry -/// points so the canonical-decode contract has exactly one definition. -pub(super) fn decode_grovedb_proof_canonical(proof: &[u8]) -> Result { +pub(super) fn decode_grovedb_proof_versioned( + proof: &[u8], + grove_version: &GroveVersion, +) -> Result { + decode_grovedb_proof_with_trailing_policy( + proof, + grove_version + .grovedb_versions + .operations + .proof + .decode_rejects_trailing_bytes + > 0, + ) +} + +fn decode_grovedb_proof_with_trailing_policy( + proof: &[u8], + reject_trailing_bytes: bool, +) -> Result { let config = bincode::config::standard() .with_big_endian() .with_limit::<{ 256 * 1024 * 1024 }>(); let (decoded, consumed) = bincode::decode_from_slice(proof, config) .map_err(|e| Error::CorruptedData(format!("unable to decode proof: {}", e)))?; - if consumed != proof.len() { + if reject_trailing_bytes && consumed != proof.len() { return Err(Error::CorruptedData(format!( "proof has {} trailing bytes after the encoded envelope", proof.len() - consumed diff --git a/grovedb/src/operations/proof/verify.rs b/grovedb/src/operations/proof/verify.rs index 30080f98b..5653dc35b 100644 --- a/grovedb/src/operations/proof/verify.rs +++ b/grovedb/src/operations/proof/verify.rs @@ -60,7 +60,7 @@ impl GroveDb { // (verify_query_with_options, verify_query_raw, // verify_query_get_parent_tree_info_with_options). - let grovedb_proof = super::decode_grovedb_proof_canonical(proof)?; + let grovedb_proof = super::decode_grovedb_proof_versioned(proof, grove_version)?; let (root_hash, _, result) = Self::verify_proof_internal(&grovedb_proof, query, options, grove_version)?; @@ -104,7 +104,7 @@ impl GroveDb { )); } - let grovedb_proof = super::decode_grovedb_proof_canonical(proof)?; + let grovedb_proof = super::decode_grovedb_proof_versioned(proof, grove_version)?; let (root_hash, tree_feature_type, result) = Self::verify_proof_internal(&grovedb_proof, query, options, grove_version)?; @@ -132,7 +132,7 @@ impl GroveDb { .proof .verify_query_raw ); - let grovedb_proof = super::decode_grovedb_proof_canonical(proof)?; + let grovedb_proof = super::decode_grovedb_proof_versioned(proof, grove_version)?; let (root_hash, _, result) = Self::verify_proof_raw_internal( &grovedb_proof, @@ -2221,7 +2221,7 @@ impl GroveDb { query: &PathTrunkChunkQuery, grove_version: &GroveVersion, ) -> Result<(CryptoHash, GroveTrunkQueryResult), Error> { - let grovedb_proof = super::decode_grovedb_proof_canonical(proof)?; + let grovedb_proof = super::decode_grovedb_proof_versioned(proof, grove_version)?; match grovedb_proof { GroveDBProof::V0(proof_v0) => { diff --git a/grovedb/src/tests/proof_advanced_tests.rs b/grovedb/src/tests/proof_advanced_tests.rs index 5297f9fdb..d3dd634a3 100644 --- a/grovedb/src/tests/proof_advanced_tests.rs +++ b/grovedb/src/tests/proof_advanced_tests.rs @@ -320,7 +320,7 @@ mod tests { /// /// `GroveDb::verify_query` (and its siblings `verify_query_raw`, /// `verify_query_with_options`, `verify_trunk_chunk_proof`) all - /// route through `decode_grovedb_proof_canonical`, which rejects + /// route through `decode_grovedb_proof_versioned`, which rejects /// any trailing bytes beyond the encoded envelope. Without this, /// the same logical proof would have many distinct byte encodings /// (a proof and the same proof with arbitrary suffix bytes), all @@ -376,6 +376,42 @@ mod tests { } } + #[test] + fn verify_query_legacy_version_accepts_proof_with_trailing_bytes() { + let mut legacy_version = GroveVersion::latest().clone(); + legacy_version + .grovedb_versions + .operations + .proof + .decode_rejects_trailing_bytes = 0; + let grove_version = &legacy_version; + let db = make_test_grovedb(grove_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"k", + Element::new_item(b"v".to_vec()), + None, + None, + grove_version, + ) + .unwrap() + .expect("insert"); + + let mut query = Query::new(); + query.insert_all(); + let path_query = PathQuery::new_unsized(vec![TEST_LEAF.to_vec()], query); + + let mut proof = db + .prove_query(&path_query, None, grove_version) + .unwrap() + .expect("prove_query"); + proof.push(0u8); + + GroveDb::verify_query(&proof, &path_query, grove_version) + .expect("legacy proof decoding should ignore trailing bytes"); + } + #[test] fn verify_query_with_options_limit() { // Prove a query with a limit and verify the results are correctly diff --git a/grovedb/src/tests/provable_count_provable_sum_tree_tests.rs b/grovedb/src/tests/provable_count_provable_sum_tree_tests.rs index af0f535ec..21db3530b 100644 --- a/grovedb/src/tests/provable_count_provable_sum_tree_tests.rs +++ b/grovedb/src/tests/provable_count_provable_sum_tree_tests.rs @@ -1668,7 +1668,7 @@ mod tests { } /// Unparsable envelope bytes → bincode-decode rejection arm in - /// `verify_aggregate_count_and_sum_query` (`decode_grovedb_proof_canonical`). + /// `verify_aggregate_count_and_sum_query` (`decode_grovedb_proof_versioned`). #[test] fn combined_unparsable_envelope_is_rejected() { use crate::tests::TEST_LEAF;