Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions grovedb-element/src/element/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u32, ElementError> {
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<u8>`, ≤ 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<u32, ElementError> {
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(
Expand Down
176 changes: 176 additions & 0 deletions grovedb-element/tests/element_constructors_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,182 @@ 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();
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<Vec<u8>>] =
&[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<u8>>] = &[
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<u8>] = &[None, Some(0), Some(255)];
let flag_variants: &[Option<Vec<u8>>] = &[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<u8>` ≤ 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()];
Expand Down
2 changes: 2 additions & 0 deletions grovedb-version/src/version/grovedb_versions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions grovedb-version/src/version/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions grovedb-version/src/version/v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions grovedb-version/src/version/v3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading