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
48 changes: 29 additions & 19 deletions grovedb-query/src/read_mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,17 @@ use crate::{axis_query::AxisQuery, error::Error, query::Query};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SumBudgetRead {
/// Stop once the running sum of matched sum-item values reaches
/// this. Distinct from a result-count limit: how many entries that
/// takes depends on the data.
/// Stop once the running **net** sum of matched sum-item values
/// reaches this. Distinct from a result-count limit: how many
/// entries that takes depends on the data (and negative values give
/// budget back). Must fit in `i64` — the budget arithmetic is the
/// engine's signed saturating subtraction.
pub sum_limit: u64,
/// Cap on elements scanned (matched or skipped), on top of the
/// grove-version global scan cap. `None` = only the global cap.
pub max_items_checked: Option<u16>,
/// Stop after this many **matched** sum items, regardless of
/// budget. `None` = no match cap. (Elements scanned but skipped —
/// non-sum elements, references — do not count; the grove-version
/// global scan cap bounds those separately.)
pub match_limit: Option<u16>,
}

impl SumBudgetRead {
Expand All @@ -56,10 +60,16 @@ impl SumBudgetRead {
selecting anything",
));
}
if self.max_items_checked == Some(0) {
if self.sum_limit > i64::MAX as u64 {
return Err(Error::InvalidOperation(
"sum-budget read: `max_items_checked` must be at least 1 when set; a zero \
scan cap selects nothing",
"sum-budget read: `sum_limit` must fit in i64 — the budget arithmetic is \
signed",
));
}
if self.match_limit == Some(0) {
return Err(Error::InvalidOperation(
"sum-budget read: `match_limit` must be at least 1 when set; a zero match cap \
selects nothing",
));
}
Ok(())
Expand All @@ -69,15 +79,15 @@ impl SumBudgetRead {
impl Encode for SumBudgetRead {
fn encode<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
self.sum_limit.encode(encoder)?;
self.max_items_checked.encode(encoder)
self.match_limit.encode(encoder)
}
}

impl<Context> Decode<Context> for SumBudgetRead {
fn decode<D: Decoder<Context = Context>>(decoder: &mut D) -> Result<Self, DecodeError> {
Ok(Self {
sum_limit: u64::decode(decoder)?,
max_items_checked: Option::<u16>::decode(decoder)?,
match_limit: Option::<u16>::decode(decoder)?,
})
}
}
Expand All @@ -94,8 +104,8 @@ impl fmt::Display for SumBudgetRead {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"SumBudget {{ sum_limit: {}, max_items_checked: {:?} }}",
self.sum_limit, self.max_items_checked
"SumBudget {{ sum_limit: {}, match_limit: {:?} }}",
self.sum_limit, self.match_limit
)
}
}
Expand Down Expand Up @@ -209,11 +219,11 @@ mod tests {
ReadMode::Axis(AxisQuery::top_k(IndexAxis::Sum, 10, 20, true)),
ReadMode::SumBudget(SumBudgetRead {
sum_limit: 1000,
max_items_checked: Some(50),
match_limit: Some(50),
}),
ReadMode::SumBudget(SumBudgetRead {
sum_limit: 1,
max_items_checked: None,
match_limit: None,
}),
];
for mode in modes {
Expand All @@ -234,7 +244,7 @@ mod tests {
);
let budget = ReadMode::SumBudget(SumBudgetRead {
sum_limit: 1,
max_items_checked: None,
match_limit: None,
});
assert_eq!(
bincode::encode_to_vec(&budget, config::standard()).unwrap()[0],
Expand All @@ -250,19 +260,19 @@ mod tests {
fn sum_budget_validation() {
assert!(SumBudgetRead {
sum_limit: 0,
max_items_checked: None
match_limit: None
}
.validate()
.is_err());
assert!(SumBudgetRead {
sum_limit: 1,
max_items_checked: Some(0)
match_limit: Some(0)
}
.validate()
.is_err());
assert!(SumBudgetRead {
sum_limit: 1,
max_items_checked: Some(1)
match_limit: Some(1)
}
.validate()
.is_ok());
Expand Down
2 changes: 1 addition & 1 deletion grovedb-query/tests/query_encoding_golden.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ fn read_mode_queries_use_version_2_and_round_trip() {
let mut budget_query = Query::new_single_query_item(QueryItem::RangeFull(..));
budget_query.read_mode = Some(Box::new(ReadMode::SumBudget(SumBudgetRead {
sum_limit: 500,
max_items_checked: Some(100),
match_limit: Some(100),
})));
let bytes = encode(&budget_query);
assert_eq!(bytes[0], 2);
Expand Down
15 changes: 15 additions & 0 deletions grovedb-version/src/version/grovedb_versions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,21 @@ pub struct GroveDBOperationsProofVersions {
/// exists. Indexed trees themselves cannot exist in pre-V4
/// production data, so `0` never rejects anything real.
pub axis_descent_in_v1_envelope: FeatureVersion,
/// Whether the V1 proof envelope carries **sum-budget windows**
/// (`ProofBytes::SumBudgetWindow`), serving `PathQuery`s whose root
/// query node holds `ReadMode::SumBudget` — an ordinary Merk proof
/// over exactly the window the budget walk scanned, replayed by the
/// verifier with the engine's own fold arithmetic.
///
/// - `0` (V1..V3): the prover refuses sum-budget queries and the
/// verifier rejects any proof/query pair involving one.
/// - `1` (V4+): served, with the fold replay attesting the stop
/// condition (budget reached / match limit / hard scan cap /
/// range exhausted).
///
/// Gated because it adds an acceptance rule to the live V1
/// envelope, same as `axis_descent_in_v1_envelope`.
pub sum_budget_in_v1_envelope: FeatureVersion,
}

#[derive(Clone, Debug, Default)]
Expand Down
1 change: 1 addition & 0 deletions grovedb-version/src/version/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ pub const GROVE_V1: GroveVersion = GroveVersion {
verify_query_get_parent_tree_info_with_options: 0,
terminal_non_merk_tree_child_hash: 0,
axis_descent_in_v1_envelope: 0,
sum_budget_in_v1_envelope: 0,
},
average_case: GroveDBOperationsAverageCaseVersions {
add_average_case_get_merk_at_path: 0,
Expand Down
1 change: 1 addition & 0 deletions grovedb-version/src/version/v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ pub const GROVE_V2: GroveVersion = GroveVersion {
verify_query_get_parent_tree_info_with_options: 0,
terminal_non_merk_tree_child_hash: 0,
axis_descent_in_v1_envelope: 0,
sum_budget_in_v1_envelope: 0,
},
average_case: GroveDBOperationsAverageCaseVersions {
add_average_case_get_merk_at_path: 0,
Expand Down
1 change: 1 addition & 0 deletions grovedb-version/src/version/v3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ pub const GROVE_V3: GroveVersion = GroveVersion {
verify_query_get_parent_tree_info_with_options: 0,
terminal_non_merk_tree_child_hash: 0,
axis_descent_in_v1_envelope: 0,
sum_budget_in_v1_envelope: 0,
},
average_case: GroveDBOperationsAverageCaseVersions {
add_average_case_get_merk_at_path: 0,
Expand Down
8 changes: 8 additions & 0 deletions grovedb-version/src/version/v4.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@
//! supplied raw. V1..V3 refuse the shape on both sides. Gated because it
//! adds an acceptance rule to the live V1 envelope.
//!
//! - `proof.sum_budget_in_v1_envelope: 1` — the V1 proof envelope carries
//! sum-budget windows (`ProofBytes::SumBudgetWindow`): an ordinary Merk
//! proof over exactly the window the budget walk scanned, whose stop
//! condition the verifier attests by replaying the engine's fold over the
//! proved elements. V1..V3 refuse the shape on both sides. Gated because
//! it adds an acceptance rule to the live V1 envelope.
//!
//! - `path_query_methods.unified_read_mode: 1` — `PathQuery` read modes
//! (axis-ordered and sum-budget reads carried in `Query::read_mode`) are
//! served by the unified dispatch (`run_path_query`, and the unified
Expand Down Expand Up @@ -239,6 +246,7 @@ pub const GROVE_V4: GroveVersion = GroveVersion {
verify_query_get_parent_tree_info_with_options: 0,
terminal_non_merk_tree_child_hash: 1, // bind terminal non-Merk tree element bytes to the parent value_hash
axis_descent_in_v1_envelope: 1, // axis-ordered descents in the V1 envelope (ReadMode::Axis)
sum_budget_in_v1_envelope: 1, // sum-budget windows in the V1 envelope (ReadMode::SumBudget)
},
average_case: GroveDBOperationsAverageCaseVersions {
add_average_case_get_merk_at_path: 0,
Expand Down
6 changes: 6 additions & 0 deletions grovedb/src/element/aggregate_sum_query/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ pub struct AggregateSumQueryResult {
/// the query completed naturally. When true, more results may exist
/// beyond what was returned.
pub hard_limit_reached: bool,
/// Total elements the walk encountered (matched or skipped),
/// including the element that tripped the hard limit if it did. The
/// sum-budget proof shape uses this as its window size.
pub elements_scanned: u16,
}

/// Options controlling how an aggregate sum query is executed.
Expand Down Expand Up @@ -302,6 +306,7 @@ impl ElementAggregateSumQueryExtensions for Element {
return Ok(AggregateSumQueryResult {
results,
hard_limit_reached: false,
elements_scanned: 0,
})
.wrap_with_cost(cost);
}
Expand Down Expand Up @@ -376,6 +381,7 @@ impl ElementAggregateSumQueryExtensions for Element {

Ok(AggregateSumQueryResult {
hard_limit_reached: elements_scanned > max_elements_scanned,
elements_scanned,
results,
})
.wrap_with_cost(cost)
Expand Down
17 changes: 13 additions & 4 deletions grovedb/src/operations/get/run_path_query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -319,15 +319,24 @@ impl GroveDb {
items: items.to_vec(),
left_to_right: path_query.query.query.left_to_right,
sum_limit: budget.sum_limit,
limit_of_items_to_check: budget.max_items_checked,
limit_of_items_to_check: budget.match_limit,
},
};
// The unified sum-budget read uses the PROVABLE fold
// semantics — skip non-sum elements, skip references —
// so the trusted read and the sum-budget proof replay
// agree over any state. (The legacy AggregateSumPathQuery
// surface keeps its configurable options.)
let result = cost_return_on_error!(
&mut cost,
self.query_aggregate_sums(
self.query_aggregate_sums_with_options(
&aggregate_sum_path_query,
allow_cache,
error_if_intermediate_path_tree_not_present,
crate::element::aggregate_sum_query::AggregateSumQueryOptions {
allow_cache,
error_if_intermediate_path_tree_not_present,
error_if_non_sum_item_found: false,
ignore_references: true,
},
transaction,
grove_version,
)
Expand Down
Loading
Loading