Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 0 additions & 1 deletion packages/rs-drive-proof-verifier/src/proof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1030,7 +1030,6 @@ impl FromProof<platform::GetRecentCompactedAddressBalanceChangesRequest>
&proof.grovedb_proof,
start_block_height,
limit,
false,
platform_version,
)
.map_drive_error(proof, mtd)?;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ pub type CompactedAddressBalanceChanges =
impl Drive {
/// Version 0 implementation of fetching compacted address balance changes.
///
/// Retrieves all compacted address balance change records from `start_block_height` onwards.
/// Retrieves all compacted address balance change records where `end_block >= start_block_height`.
/// This includes ranges that contain `start_block_height` (e.g., range 400-600 when querying
/// from block 505) as well as ranges that start after `start_block_height`.
///
/// Returns a vector of (start_block, end_block, address_balance_map) tuples.
pub(super) fn fetch_compacted_address_balance_changes_v0(
&self,
Expand All @@ -27,34 +30,118 @@ impl Drive {
) -> Result<CompactedAddressBalanceChanges, Error> {
let path = Self::saved_compacted_block_transactions_address_balances_path_vec();

// Create a range query starting from the specified height
// Keys are 16 bytes: (start_block, end_block), both big-endian
// We query from (start_block_height, 0) onwards
let mut start_key = Vec::with_capacity(16);
start_key.extend_from_slice(&start_block_height.to_be_bytes());
start_key.extend_from_slice(&0u64.to_be_bytes());
// Keys are 16 bytes: (start_block, end_block), both big-endian.
// We want ranges where end_block >= start_block_height, which includes:
// - Ranges that contain start_block_height (e.g., 400-600 contains 505)
// - Ranges that start at or after start_block_height
//
// Strategy:
// 1. First query: descending from (start_block_height, u64::MAX) with limit 1
// to find any range where start_block <= start_block_height that might contain it
// 2. Second query: ascending from (start_block_height, 0) to get ranges
// that start at or after start_block_height

let mut query = Query::new();
query.insert_range_from(start_key..);
let config = bincode::config::standard()
.with_big_endian()
.with_no_limit();

let path_query = PathQuery::new(path, SizedQuery::new(query, limit, None));
let mut compacted_changes = Vec::new();
let limit_usize = limit.map(|l| l as usize);

let (results, _) = self.grove_get_path_query(
&path_query,
// 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 mut desc_query = Query::new_with_direction(false); // descending
desc_query.insert_range_to_inclusive(..=desc_end_key);

let desc_path_query =
PathQuery::new(path.clone(), SizedQuery::new(desc_query, Some(1), None));

let (desc_results, _) = self.grove_get_path_query(
&desc_path_query,
transaction,
QueryResultType::QueryKeyElementPairResultType,
&mut vec![],
&platform_version.drive,
)?;

let config = bincode::config::standard()
.with_big_endian()
.with_no_limit();
// Check if we found a range that contains start_block_height
if let Some((key, element)) = desc_results.to_key_elements().into_iter().next() {
if key.len() != 16 {
return Err(Error::Protocol(Box::new(
ProtocolError::CorruptedSerialization(
"invalid compacted block key length, expected 16 bytes".to_string(),
),
)));
}

let mut compacted_changes = Vec::new();
let start_block = u64::from_be_bytes(key[0..8].try_into().unwrap());
let end_block = u64::from_be_bytes(key[8..16].try_into().unwrap());

// Only include if end_block >= start_block_height (range contains our block)
if end_block >= start_block_height {
let Element::Item(serialized_data, _) = element else {
return Err(Error::Protocol(Box::new(
ProtocolError::CorruptedSerialization(
"expected item element for compacted address balances".to_string(),
),
)));
};

let (address_balances, _): (BTreeMap<PlatformAddress, CreditOperation>, usize) =
bincode::decode_from_slice(&serialized_data, config).map_err(|e| {
Error::Protocol(Box::new(ProtocolError::CorruptedSerialization(format!(
"cannot decode compacted address balances: {}",
e
))))
})?;

compacted_changes.push((start_block, end_block, address_balances));
}
}

// Check if we've already hit the limit
if let Some(l) = limit_usize {
if compacted_changes.len() >= l {
return Ok(compacted_changes);
}
}

// Query 2: Get ranges that start at or after start_block_height (ascending)
// 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 mut asc_query = Query::new();
asc_query.insert_range_from(asc_start_key..);

let asc_path_query = PathQuery::new(path, SizedQuery::new(asc_query, limit, None));

let (asc_results, _) = self.grove_get_path_query(
&asc_path_query,
transaction,
QueryResultType::QueryKeyElementPairResultType,
&mut vec![],
&platform_version.drive,
)?;

// Track the start_block from descending query to avoid duplicates
let desc_start_block = compacted_changes.first().map(|(start, _, _)| *start);

for (key, element) in asc_results.to_key_elements() {
// Check if we've reached the limit
if let Some(l) = limit_usize {
if compacted_changes.len() >= l {
break;
}
}

for (key, element) in results.to_key_elements() {
// Parse start_block and end_block from key (16 bytes total, both big-endian)
if key.len() != 16 {
return Err(Error::Protocol(Box::new(
ProtocolError::CorruptedSerialization(
Expand All @@ -66,7 +153,11 @@ impl Drive {
let start_block = u64::from_be_bytes(key[0..8].try_into().unwrap());
let end_block = u64::from_be_bytes(key[8..16].try_into().unwrap());

// Get the serialized data from the Item element
// Skip if this is the same range we got from descending query
if Some(start_block) == desc_start_block {
continue;
}

let Element::Item(serialized_data, _) = element else {
return Err(Error::Protocol(Box::new(
ProtocolError::CorruptedSerialization(
Expand All @@ -75,7 +166,6 @@ impl Drive {
)));
};

// Deserialize the address balance map
let (address_balances, _): (BTreeMap<PlatformAddress, CreditOperation>, usize) =
bincode::decode_from_slice(&serialized_data, config).map_err(|e| {
Error::Protocol(Box::new(ProtocolError::CorruptedSerialization(format!(
Expand All @@ -91,6 +181,12 @@ 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
///
/// This ensures the proof covers all relevant ranges efficiently.
pub(super) fn prove_compacted_address_balance_changes_v0(
&self,
start_block_height: u64,
Expand All @@ -100,10 +196,55 @@ impl Drive {
) -> Result<Vec<u8>, Error> {
let path = Self::saved_compacted_block_transactions_address_balances_path_vec();

// Create a range query starting from the specified height
let mut start_key = Vec::with_capacity(16);
start_key.extend_from_slice(&start_block_height.to_be_bytes());
start_key.extend_from_slice(&0u64.to_be_bytes());
// 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());

let mut desc_query = Query::new_with_direction(false); // descending
desc_query.insert_range_to_inclusive(..=desc_end_key);

let desc_path_query =
PathQuery::new(path.clone(), SizedQuery::new(desc_query, Some(1), None));

let (desc_results, _) = self.grove_get_path_query(
&desc_path_query,
transaction,
QueryResultType::QueryKeyElementPairResultType,
&mut vec![],
&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
}
} 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
}
} 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
};

// Step 2: Proved ascending query from start_key

let mut query = Query::new();
query.insert_range_from(start_key..);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1090,4 +1090,132 @@ mod tests {
panic!("expected Item element for block ranges");
}
}

#[test]
fn should_fetch_compacted_range_when_start_height_falls_within_range() {
let drive = setup_drive_with_initial_state_structure(None);
// Set compaction threshold to 4 blocks so we can trigger compaction easily
let platform_version = create_test_platform_version_for_compaction(4, 1000);

// Store 4 blocks (100-103) to trigger compaction into range (100, 103)
for block_height in 100u64..=103 {
let mut balances = BTreeMap::new();
balances.insert(ADDR_1, CreditOperation::AddToCredits(1000));

drive
.store_address_balances_for_block_v0(
&balances,
block_height,
1700000000000,
None,
&platform_version,
)
.expect("should store block");
}

// Verify compaction happened - fetch from block 0 should return the compacted range
let all_results = drive
.fetch_compacted_address_balance_changes(0, None, None, &platform_version)
.expect("should fetch compacted changes");

assert_eq!(all_results.len(), 1, "should have 1 compacted entry");
let (start, end, _) = &all_results[0];
assert_eq!(*start, 100, "compacted range should start at 100");
assert_eq!(*end, 103, "compacted range should end at 103");

// Now query with start_height=101, which falls WITHIN the compacted range (100-103)
// We should still get this range since block 101 is within it
let results_from_101 = drive
.fetch_compacted_address_balance_changes(101, None, None, &platform_version)
.expect("should fetch compacted changes from 101");

assert_eq!(
results_from_101.len(),
1,
"should include compacted range (100, 103) when querying from block 101 \
since 101 falls within that range"
);
}

#[test]
fn should_prove_and_verify_compacted_address_balances() {
use crate::drive::Drive;

let drive = setup_drive_with_initial_state_structure(None);
// Set compaction threshold to 4 blocks
let platform_version = create_test_platform_version_for_compaction(4, 1000);

// Store 4 blocks (100-103) to trigger compaction into range (100, 103)
for block_height in 100u64..=103 {
let mut balances = BTreeMap::new();
balances.insert(ADDR_1, CreditOperation::AddToCredits(1000));

drive
.store_address_balances_for_block_v0(
&balances,
block_height,
1700000000000,
None,
&platform_version,
)
.expect("should store block");
}

// Generate proof with limit (required for verify_query_with_absence_proof)
let limit = Some(100u16);
let proof = drive
.prove_compacted_address_balance_changes(0, limit, None, &platform_version)
.expect("should generate proof");

// Verify proof
let (root_hash, verified_changes) =
Drive::verify_compacted_address_balance_changes(&proof, 0, limit, &platform_version)
.expect("should verify proof");

assert!(!root_hash.is_empty(), "root hash should not be empty");
assert_eq!(verified_changes.len(), 1, "should have 1 compacted entry");
assert_eq!(verified_changes[0].0, 100, "start block should be 100");
assert_eq!(verified_changes[0].1, 103, "end block should be 103");
}

#[test]
fn should_prove_and_verify_with_containing_range() {
use crate::drive::Drive;

let drive = setup_drive_with_initial_state_structure(None);
// Set compaction threshold to 4 blocks
let platform_version = create_test_platform_version_for_compaction(4, 1000);

// Store 4 blocks (100-103) to trigger compaction
for block_height in 100u64..=103 {
let mut balances = BTreeMap::new();
balances.insert(ADDR_1, CreditOperation::AddToCredits(1000));

drive
.store_address_balances_for_block_v0(
&balances,
block_height,
1700000000000,
None,
&platform_version,
)
.expect("should store block");
}

// Generate proof starting from block 101 (which falls within range 100-103)
let limit = Some(100u16);
let proof = drive
.prove_compacted_address_balance_changes(101, limit, None, &platform_version)
.expect("should generate proof");

// Verify proof - should include range (100, 103) since 101 is within it
let (root_hash, verified_changes) =
Drive::verify_compacted_address_balance_changes(&proof, 101, limit, &platform_version)
.expect("should verify proof");

assert!(!root_hash.is_empty(), "root hash should not be empty");
assert_eq!(verified_changes.len(), 1, "should have 1 compacted entry");
assert_eq!(verified_changes[0].0, 100, "start block should be 100");
assert_eq!(verified_changes[0].1, 103, "end block should be 103");
}
}
Loading
Loading