Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::drive::Drive;
use crate::error::Error;
use crate::util::common::compacted_key;
use dpp::address_funds::PlatformAddress;
use dpp::balances::credits::BlockAwareCreditOperation;
use dpp::ProtocolError;
Expand Down Expand Up @@ -51,11 +52,15 @@ impl Drive {
let mut compacted_changes = Vec::new();
let limit_usize = limit.map(|l| l as usize);

// A zero limit can never include any entry — return empty before the
// boundary probe, which would otherwise still surface a containing range.
if limit_usize == Some(0) {
return Ok(compacted_changes);
}

// Query 1: Find if there's a range containing start_block_height
// Query descending from (start_block_height, u64::MAX) with limit 1
let mut desc_end_key = Vec::with_capacity(16);
desc_end_key.extend_from_slice(&start_block_height.to_be_bytes());
desc_end_key.extend_from_slice(&u64::MAX.to_be_bytes());
let desc_end_key = compacted_key(start_block_height, u64::MAX);

let mut desc_query = Query::new_with_direction(false); // descending
desc_query.insert_range_to_inclusive(..=desc_end_key);
Expand Down Expand Up @@ -119,9 +124,7 @@ impl Drive {
// Always use (start_block_height, 0) for consistent proof verification
// The result may overlap with descending query if descending found a range
// starting exactly at start_block_height - we dedupe below
let mut asc_start_key = Vec::with_capacity(16);
asc_start_key.extend_from_slice(&start_block_height.to_be_bytes());
asc_start_key.extend_from_slice(&0u64.to_be_bytes());
let asc_start_key = compacted_key(start_block_height, 0);

let mut asc_query = Query::new();
asc_query.insert_range_from(asc_start_key..);
Expand Down Expand Up @@ -189,11 +192,23 @@ impl Drive {

/// Version 0 implementation for proving compacted address balance changes.
///
/// Uses a two-step approach:
/// 1. First query (non-proving): descending to find any range containing start_block_height
/// 2. Second query (proving): ascending from the found start_block or start_block_height
/// The proof must let the verifier authenticate **two** things against the
/// same root hash (see `verify_compacted_address_balance_changes_v0`):
///
/// 1. A **boundary query** — the single greatest compacted key
/// `<= (start_block_height, u64::MAX)`. This is what protects against the
/// compacted-range absence-proof attack: a range like `(100, 200)` that
/// contains the requested height sorts *before* `(150, 150)`, so the
/// verifier cannot trust a forward-only proof to surface it.
/// 2. A **forward query** — `range_from(start_key..)` where `start_key` is
/// the containing range (if any) or `(start_block_height,
/// start_block_height)`.
///
/// This ensures the proof covers all relevant ranges efficiently.
/// We discover `start_key` with a non-proving descending query, then prove a
/// single combined `PathQuery` (boundary key via `insert_key` + the forward
/// `range_from`, capped at the caller's limit) through
/// `grove_get_proved_path_query`, so the verifier's chained boundary and
/// forward queries are both satisfiable.
pub(super) fn prove_compacted_address_balance_changes_v0(
&self,
start_block_height: u64,
Expand All @@ -203,10 +218,9 @@ impl Drive {
) -> Result<Vec<u8>, Error> {
let path = Self::saved_compacted_block_transactions_address_balances_path_vec();

// Step 1: Non-proving descending query to find any range containing start_block_height
let mut desc_end_key = Vec::with_capacity(16);
desc_end_key.extend_from_slice(&start_block_height.to_be_bytes());
desc_end_key.extend_from_slice(&u64::MAX.to_be_bytes());
// Step 1: Non-proving descending query to find the greatest compacted
// key <= (start_block_height, u64::MAX).
let desc_end_key = compacted_key(start_block_height, u64::MAX);

let mut desc_query = Query::new_with_direction(false); // descending
desc_query.insert_range_to_inclusive(..=desc_end_key);
Expand All @@ -222,48 +236,74 @@ impl Drive {
&platform_version.drive,
)?;

// Determine the actual start key for the proved query
// If we found a containing range, use its exact key
// Otherwise use (start_block_height, start_block_height) since end_block >= start_block always
let start_key = if let Some((key, _)) = desc_results.to_key_elements().into_iter().next() {
if key.len() == 16 {
let end_block = u64::from_be_bytes(key[8..16].try_into().unwrap());
// If this range contains start_block_height, use its exact key
if end_block >= start_block_height {
key
} else {
// No containing range, use (start_block_height, start_block_height)
let mut key = Vec::with_capacity(16);
key.extend_from_slice(&start_block_height.to_be_bytes());
key.extend_from_slice(&start_block_height.to_be_bytes());
key
// `boundary_key` is the authenticated lower-bound anchor the verifier
// will re-derive from its boundary query. `forward_start` is the lower
// bound of the ascending result scan.
let (boundary_key, forward_start) = match desc_results.to_key_elements().into_iter().next()
{
Some((key, _)) => {
if key.len() != 16 {
return Err(Error::Protocol(Box::new(
ProtocolError::CorruptedSerialization(
"invalid compacted block key length, expected 16 bytes".to_string(),
),
)));
}
} else {
let mut key = Vec::with_capacity(16);
key.extend_from_slice(&start_block_height.to_be_bytes());
key.extend_from_slice(&start_block_height.to_be_bytes());
key
let end_block = u64::from_be_bytes(key[8..16].try_into().map_err(|_| {
Error::Protocol(Box::new(ProtocolError::CorruptedSerialization(
"invalid compacted key slice".to_string(),
)))
})?);
let forward_start = if end_block >= start_block_height {
key.clone()
} else {
compacted_key(start_block_height, start_block_height)
};
(Some(key), forward_start)
}
} else {
let mut key = Vec::with_capacity(16);
key.extend_from_slice(&start_block_height.to_be_bytes());
key.extend_from_slice(&start_block_height.to_be_bytes());
key
None => (None, compacted_key(start_block_height, start_block_height)),
};

// Step 2: Proved ascending query from start_key

let mut query = Query::new();
query.insert_range_from(start_key..);

let path_query = PathQuery::new(path, SizedQuery::new(query, limit, None));

self.grove_get_proved_path_query(
&path_query,
transaction,
&mut vec![],
&platform_version.drive,
)
// Step 2: build the proof. See the nullifier prover and the verifier docs
// for the soundness rationale.
match boundary_key {
Some(boundary_key) => {
// Cap the proof at the caller's `limit` (+1 for the authenticated
// boundary point) so a client requesting `prove=true` from an early
// height cannot force a proof spanning the entire compacted history
// (the public handler passes `limit = Some(25)` "to stay within proof
// size limits"). The verifier re-applies `limit` to its forward subset
// query, so this cap is soundness-neutral. Routed through the
// transaction-aware proof path (not `prove_query_many`, which ignores
// the transaction) for snapshot consistency.
let capped_limit = limit.map(|l| l.saturating_add(1));
let mut query = Query::new();
query.insert_key(boundary_key);
query.insert_range_from(forward_start..);
let path_query =
PathQuery::new(path.clone(), SizedQuery::new(query, capped_limit, None));

self.grove_get_proved_path_query(
&path_query,
transaction,
&mut vec![],
&platform_version.drive,
)
}
Comment thread
QuantumExplorer marked this conversation as resolved.
Comment thread
QuantumExplorer marked this conversation as resolved.
None => {
let mut forward_query = Query::new();
forward_query.insert_range_from(forward_start..);
let forward_path_query =
PathQuery::new(path.clone(), SizedQuery::new(forward_query, limit, None));

self.grove_get_proved_path_query(
&forward_path_query,
transaction,
&mut vec![],
&platform_version.drive,
)
}
}
}
}

Expand Down
28 changes: 20 additions & 8 deletions packages/rs-drive/src/drive/saved_block_transactions/queries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,21 @@ pub const ADDRESS_BALANCES_KEY: &[u8; 1] = b"m";
/// The subtree key for address balances storage as u8
pub const ADDRESS_BALANCES_KEY_U8: u8 = b'm';

/// The subtree key for address balances storage
pub const COMPACTED_ADDRESS_BALANCES_KEY: &[u8; 1] = b"c";
/// The subtree key for compacted address balances storage.
///
/// Derived from [`COMPACTED_ADDRESS_BALANCES_KEY_U8`] so the `b'c'` byte is
/// written in exactly one place (the `verify`-available `crate::util::common`)
/// and the slice form cannot drift from the u8.
pub const COMPACTED_ADDRESS_BALANCES_KEY: &[u8; 1] = &[COMPACTED_ADDRESS_BALANCES_KEY_U8];

/// The subtree key for compacted address balances storage as u8
pub const COMPACTED_ADDRESS_BALANCES_KEY_U8: u8 = b'c';
/// The subtree key for compacted address balances storage as u8.
///
/// Re-exported from the `verify`-available [`crate::util::common`] so the proof
/// verifier and this server-side storage path share a single definition of the
/// byte (it is part of the proof contract and must not drift). The downstream
/// import path (`saved_block_transactions::COMPACTED_ADDRESS_BALANCES_KEY_U8`,
/// via `pub use queries::*`) is unchanged.
pub use crate::util::common::COMPACTED_ADDRESS_BALANCES_KEY_U8;

/// The subtree key for compacted addresses expiration time storage
pub const COMPACTED_ADDRESSES_EXPIRATION_TIME_KEY: &[u8; 1] = b"e";
Expand Down Expand Up @@ -42,11 +52,13 @@ impl Drive {
}

/// Path to compacted address balances under saved block transactions.
///
/// Delegates to the `verify`-available canonical
/// [`crate::util::common::compacted_address_balances_path`] so the
/// storage/fetch (server) path and the proof verifier (verify) path share a
/// single definition and cannot drift.
pub fn saved_compacted_block_transactions_address_balances_path_vec() -> Vec<Vec<u8>> {
vec![
vec![RootTree::SavedBlockTransactions as u8],
vec![COMPACTED_ADDRESS_BALANCES_KEY_U8],
]
crate::util::common::compacted_address_balances_path()
}

/// Path to compacted address balances under saved block transactions.
Expand Down
38 changes: 38 additions & 0 deletions packages/rs-drive/src/util/common/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,40 @@
pub mod decode;
pub mod encode;

/// Builds the 16-byte big-endian compacted-block key `(start_block, end_block)`
/// shared by the shielded-nullifier and address-balance compacted trees.
///
/// This 16-byte boundary-key encoding is part of the chained-proof contract:
/// every prover and its matching verifier MUST construct keys identically, or
/// chained verification silently breaks. It therefore lives in exactly one
/// place (the four compacted prove/verify modules import it) so the encoding
/// cannot drift between them.
pub(crate) fn compacted_key(start_block: u64, end_block: u64) -> Vec<u8> {
let mut key = Vec::with_capacity(16);
key.extend_from_slice(&start_block.to_be_bytes());
key.extend_from_slice(&end_block.to_be_bytes());
key
}

/// Canonical subtree key for the compacted address-balance tree under
/// `SavedBlockTransactions`.
///
/// Defined here (a `verify`-available module) rather than in the `server`-gated
/// `saved_block_transactions::queries` so the **verify-side** proof verifier and
/// the **server-side** storage/fetch path reference one definition — the subtree
/// location is part of the proof contract and must not drift between them.
/// `saved_block_transactions::queries` re-exports this constant for its
/// server-side callers, so the `b'c'` byte is written in exactly one place.
pub const COMPACTED_ADDRESS_BALANCES_KEY_U8: u8 = b'c';

/// Path to the compacted address-balance subtree under `SavedBlockTransactions`.
///
/// Shared by the server-side storage/fetch path and the verify-side proof
/// verifier; both sides reach it through this one helper. See
/// [`COMPACTED_ADDRESS_BALANCES_KEY_U8`] for why the byte lives in this module.
pub(crate) fn compacted_address_balances_path() -> Vec<Vec<u8>> {
vec![
vec![crate::drive::RootTree::SavedBlockTransactions as u8],
vec![COMPACTED_ADDRESS_BALANCES_KEY_U8],
]
}
Comment thread
QuantumExplorer marked this conversation as resolved.
Comment thread
QuantumExplorer marked this conversation as resolved.
Loading
Loading