From 4433778894a1047f1f2b53d849b3cd7ca3bab30e Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 19 May 2026 04:53:54 +0700 Subject: [PATCH 1/2] feat(element): add required_{item,reference}_with_sum_item_space helpers Adds two worst-case storage-sizing helpers paralleling Element::required_item_space for the sum-bearing variants introduced in #670 / #667: - Element::required_item_with_sum_item_space - Element::required_reference_with_sum_item_space Both reserve 10 bytes for the i64 sum_value as an upper bound (bincode 2.x maxes at 9 bytes for a zigzag-encoded u64; 10 is a safety margin) so dry-run / stateless-cost callers in dash-platform never undercharge on summable-index writes. Wires required_item_with_sum_item_space and required_reference_with_sum_item_space FeatureVersion fields through GroveDBElementMethodVersions (initialized to 0 in v1, v2, v3). Tests cover the manual-formula contract and exhaustively sweep boundary sum values (0, +-1, +-250, i64::MAX, i64::MIN), payload sizes, max_hop, and flag variants to assert helper >= serialize().len() for every combination. Co-Authored-By: Claude Opus 4.7 (1M context) --- grovedb-element/src/element/helpers.rs | 64 ++++++++ .../tests/element_constructors_helpers.rs | 145 ++++++++++++++++++ .../src/version/grovedb_versions.rs | 2 + grovedb-version/src/version/v1.rs | 2 + grovedb-version/src/version/v2.rs | 2 + grovedb-version/src/version/v3.rs | 2 + 6 files changed, 217 insertions(+) diff --git a/grovedb-element/src/element/helpers.rs b/grovedb-element/src/element/helpers.rs index 932c6e4fb..66a0b3de1 100644 --- a/grovedb-element/src/element/helpers.rs +++ b/grovedb-element/src/element/helpers.rs @@ -622,6 +622,70 @@ impl Element { Ok(len + len.required_space() as u32 + flag_len + flag_len.required_space() as u32 + 1) } + /// Worst-case serialized-storage cost of an + /// [`Element::ItemWithSumItem`]. Same shape as + /// [`Self::required_item_space`] with an extra worst-case allowance + /// for the `i64` sum_value field. + /// + /// The sum_value is bincode varint-encoded (with zigzag for negative + /// values) inside `Element::serialize`. The bincode 2.x varint encoding + /// for a u64 is at most 9 bytes (1 marker + 8 bytes). We use 10 here + /// as a deliberate upper-bound margin so callers using this for + /// stateless-cost / dry-run fee estimation never undercharge. + pub fn required_item_with_sum_item_space( + len: u32, + flag_len: u32, + grove_version: &GroveVersion, + ) -> Result { + check_grovedb_v0!( + "required_item_with_sum_item_space", + grove_version + .grovedb_versions + .element + .required_item_with_sum_item_space + ); + Ok( + len + len.required_space() as u32 + + flag_len + + flag_len.required_space() as u32 + + 10 + + 1, + ) + } + + /// Worst-case serialized-storage cost of an + /// [`Element::ReferenceWithSumItem`]. Parallels + /// [`Self::required_item_with_sum_item_space`] with the reference path + /// as the variable-length payload. + /// + /// `path_len` is the worst-case serialized size of the + /// `ReferencePathType` payload (mirroring how callers compute the + /// `reference_path` upper bound for plain references). The `+10` + /// covers the worst-case `i64` sum_value bincode varint; the `+1` + /// covers the element variant discriminant byte. The `max_hop` + /// (`Option`, ≤ 2 bytes) is small and should be folded into + /// `path_len` by the caller if exact upper-bound accounting matters + /// for their dry-run. + pub fn required_reference_with_sum_item_space( + path_len: u32, + flag_len: u32, + grove_version: &GroveVersion, + ) -> Result { + check_grovedb_v0!( + "required_reference_with_sum_item_space", + grove_version + .grovedb_versions + .element + .required_reference_with_sum_item_space + ); + Ok(path_len + + path_len.required_space() as u32 + + flag_len + + flag_len.required_space() as u32 + + 10 + + 1) + } + /// Convert the reference to an absolute reference. Looks through a /// `NonCounted` wrapper, converting the inner reference and re-wrapping. pub fn convert_if_reference_to_absolute_reference( diff --git a/grovedb-element/tests/element_constructors_helpers.rs b/grovedb-element/tests/element_constructors_helpers.rs index 10db14808..9a7038c87 100644 --- a/grovedb-element/tests/element_constructors_helpers.rs +++ b/grovedb-element/tests/element_constructors_helpers.rs @@ -479,6 +479,151 @@ fn required_item_space_matches_manual_formula() { assert_eq!(required, expected); } +#[test] +fn required_item_with_sum_item_space_matches_manual_formula() { + let grove_version = GroveVersion::latest(); + let len: u32 = 127; + let flag_len: u32 = 511; + + let required = + Element::required_item_with_sum_item_space(len, flag_len, grove_version).unwrap(); + let expected = + len + len.required_space() as u32 + flag_len + flag_len.required_space() as u32 + 10 + 1; + + assert_eq!(required, expected); +} + +/// The load-bearing contract for fee estimation: the helper must return +/// at least the actual serialized size of every `ItemWithSumItem`. Exact +/// equality is not required — undercounting is what would break dry-run +/// fees. We test boundary sum values (small / large positive / large +/// negative) because bincode varint encoding has different lengths at +/// each step. +#[test] +fn required_item_with_sum_item_space_is_upper_bound() { + let grove_version = GroveVersion::latest(); + + let payloads: &[&[u8]] = &[&[], b"a", &[0xAB; 250], &[0xCD; 65_500]]; + let flag_variants: &[Option>] = + &[None, Some(vec![]), Some(vec![1]), Some(vec![9; 250])]; + let sum_values: &[i64] = &[0, 1, -1, 250, -250, i64::MAX, i64::MIN, i32::MAX as i64]; + + for payload in payloads { + for flags in flag_variants { + for &sum in sum_values { + let element = Element::new_item_with_sum_item_with_flags( + payload.to_vec(), + sum, + flags.clone(), + ); + let serialized_len = element.serialize(grove_version).unwrap().len() as u32; + let flag_len = flags.as_ref().map(|f| f.len()).unwrap_or(0) as u32; + let required = Element::required_item_with_sum_item_space( + payload.len() as u32, + flag_len, + grove_version, + ) + .unwrap(); + + assert!( + required >= serialized_len, + "required={} must be >= serialized={} for payload_len={} flag_len={} sum={}", + required, + serialized_len, + payload.len(), + flag_len, + sum, + ); + } + } + } +} + +#[test] +fn required_reference_with_sum_item_space_matches_manual_formula() { + let grove_version = GroveVersion::latest(); + let path_len: u32 = 64; + let flag_len: u32 = 32; + + let required = + Element::required_reference_with_sum_item_space(path_len, flag_len, grove_version).unwrap(); + let expected = path_len + + path_len.required_space() as u32 + + flag_len + + flag_len.required_space() as u32 + + 10 + + 1; + + assert_eq!(required, expected); +} + +/// Upper-bound contract for `ReferenceWithSumItem`. Builds a reference +/// from a known absolute path and asserts the helper returns at least +/// the actual serialized length once the caller has folded `max_hop` +/// into `path_len`. We serialize the reference path separately to +/// compute the worst-case path payload size the way real callers do +/// (e.g. dash-platform's `add_document_to_primary_storage`). +#[test] +fn required_reference_with_sum_item_space_is_upper_bound() { + use bincode::config; + + let grove_version = GroveVersion::latest(); + + let path_variants: &[Vec>] = &[ + vec![vec![0]], + vec![vec![0], b"contracts".to_vec(), b"documents".to_vec()], + vec![vec![0xFF; 32], vec![0xAB; 32], vec![0xCD; 32]], + ]; + let max_hops: &[Option] = &[None, Some(0), Some(255)]; + let flag_variants: &[Option>] = &[None, Some(vec![]), Some(vec![1, 2, 3])]; + let sum_values: &[i64] = &[0, 1, -1, i64::MAX, i64::MIN]; + + let bincode_cfg = config::standard().with_big_endian().with_no_limit(); + + for raw_path in path_variants { + let reference_path = ReferencePathType::AbsolutePathReference(raw_path.clone()); + // Caller-side worst-case for the variable-length payload: the + // serialized size of the ReferencePathType plus the max_hop + // (`Option` ≤ 2 bytes) that the helper expects folded in. + let path_payload_len = bincode::encode_to_vec(&reference_path, bincode_cfg) + .unwrap() + .len() as u32 + + 2; + + for &max_hop in max_hops { + for flags in flag_variants { + for &sum in sum_values { + let element = Element::new_reference_with_sum_item_with_max_hops_and_flags( + reference_path.clone(), + max_hop, + sum, + flags.clone(), + ); + let serialized_len = element.serialize(grove_version).unwrap().len() as u32; + let flag_len = flags.as_ref().map(|f| f.len()).unwrap_or(0) as u32; + let required = Element::required_reference_with_sum_item_space( + path_payload_len, + flag_len, + grove_version, + ) + .unwrap(); + + assert!( + required >= serialized_len, + "required={} must be >= serialized={} for path_payload_len={} flag_len={} max_hop={:?} sum={}", + required, + serialized_len, + path_payload_len, + flag_len, + max_hop, + sum, + ); + } + } + } + } +} + #[test] fn convert_if_reference_to_absolute_reference_converts_and_preserves_other_types() { let path = [b"root".as_ref(), b"branch".as_ref()]; diff --git a/grovedb-version/src/version/grovedb_versions.rs b/grovedb-version/src/version/grovedb_versions.rs index 88f2bf963..6081afc94 100644 --- a/grovedb-version/src/version/grovedb_versions.rs +++ b/grovedb-version/src/version/grovedb_versions.rs @@ -227,6 +227,8 @@ pub struct GroveDBElementMethodVersions { pub value_defined_cost_for_serialized_value: FeatureVersion, pub specialized_costs_for_key_value: FeatureVersion, pub required_item_space: FeatureVersion, + pub required_item_with_sum_item_space: FeatureVersion, + pub required_reference_with_sum_item_space: FeatureVersion, pub insert: FeatureVersion, pub insert_into_batch_operations: FeatureVersion, pub insert_if_not_exists: FeatureVersion, diff --git a/grovedb-version/src/version/v1.rs b/grovedb-version/src/version/v1.rs index 2fdb29c7e..ad7b882d5 100644 --- a/grovedb-version/src/version/v1.rs +++ b/grovedb-version/src/version/v1.rs @@ -47,6 +47,8 @@ pub const GROVE_V1: GroveVersion = GroveVersion { value_defined_cost_for_serialized_value: 0, specialized_costs_for_key_value: 0, required_item_space: 0, + required_item_with_sum_item_space: 0, + required_reference_with_sum_item_space: 0, insert: 0, insert_into_batch_operations: 0, insert_if_not_exists: 0, diff --git a/grovedb-version/src/version/v2.rs b/grovedb-version/src/version/v2.rs index 14320c791..959bfa0f2 100644 --- a/grovedb-version/src/version/v2.rs +++ b/grovedb-version/src/version/v2.rs @@ -47,6 +47,8 @@ pub const GROVE_V2: GroveVersion = GroveVersion { value_defined_cost_for_serialized_value: 0, specialized_costs_for_key_value: 0, required_item_space: 0, + required_item_with_sum_item_space: 0, + required_reference_with_sum_item_space: 0, insert: 0, insert_into_batch_operations: 0, insert_if_not_exists: 0, diff --git a/grovedb-version/src/version/v3.rs b/grovedb-version/src/version/v3.rs index c7f49a81e..10a32fb3a 100644 --- a/grovedb-version/src/version/v3.rs +++ b/grovedb-version/src/version/v3.rs @@ -47,6 +47,8 @@ pub const GROVE_V3: GroveVersion = GroveVersion { value_defined_cost_for_serialized_value: 0, specialized_costs_for_key_value: 0, required_item_space: 0, + required_item_with_sum_item_space: 0, + required_reference_with_sum_item_space: 0, insert: 0, insert_into_batch_operations: 0, insert_if_not_exists: 0, From 807f69ad990fcc1d3cef68a6cd3ccf32b76f7d54 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 19 May 2026 04:59:43 +0700 Subject: [PATCH 2/2] test(element): cover version-mismatch path for new sum_item helpers Adds required_with_sum_item_space_helpers_reject_unknown_version to exercise the check_grovedb_v0! mismatch arm on both new helpers, lifting diff coverage above the 90% codecov gate (the macro's error branch was the only uncovered region in the previous diff). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../tests/element_constructors_helpers.rs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/grovedb-element/tests/element_constructors_helpers.rs b/grovedb-element/tests/element_constructors_helpers.rs index 9a7038c87..acb4e07e4 100644 --- a/grovedb-element/tests/element_constructors_helpers.rs +++ b/grovedb-element/tests/element_constructors_helpers.rs @@ -479,6 +479,37 @@ fn required_item_space_matches_manual_formula() { assert_eq!(required, expected); } +/// Exercise the `check_grovedb_v0!` mismatch arm for both new helpers +/// so the macro-expanded error path is covered. The helpers only know +/// version `0`; flipping the version field to `1` must return a +/// `VersionError` instead of a value. +#[test] +fn required_with_sum_item_space_helpers_reject_unknown_version() { + let mut bad_version = GroveVersion::latest().clone(); + bad_version + .grovedb_versions + .element + .required_item_with_sum_item_space = 1; + bad_version + .grovedb_versions + .element + .required_reference_with_sum_item_space = 1; + + let item_err = Element::required_item_with_sum_item_space(1, 1, &bad_version).unwrap_err(); + assert!( + matches!(item_err, ElementError::VersionError(_)), + "expected VersionError, got {:?}", + item_err + ); + + let ref_err = Element::required_reference_with_sum_item_space(1, 1, &bad_version).unwrap_err(); + assert!( + matches!(ref_err, ElementError::VersionError(_)), + "expected VersionError, got {:?}", + ref_err + ); +} + #[test] fn required_item_with_sum_item_space_matches_manual_formula() { let grove_version = GroveVersion::latest();