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
186 changes: 142 additions & 44 deletions packages/rs-sdk/src/platform/address_sync/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -495,25 +495,27 @@ fn seed_base_balances<P: AddressProvider>(
/// address balance changes RPCs.
///
/// The function queries recent changes **first** to obtain a GroveDB proof, then
/// uses [`Drive::verify_key_exists_as_boundary`] to check whether the boundary
/// height still exists in the recent address balances tree. If it does, the
/// compacted phase is skipped entirely -- this is the common hot path for
/// frequent 15-second re-syncs where only the recent zone has changed.
/// reads the boundary nodes of the recent address balances tree from it. If a
/// boundary at or above the checkpoint is present, the compacted phase is
/// skipped entirely -- this is the common hot path for frequent 15-second
/// re-syncs where only the recent zone has changed.
///
/// When `last_known_recent_block > 0`, the recent query uses **exclusive start**
/// (`RangeAfter`) with that height. This causes the height to appear as a
/// **boundary node** in the proof, which `key_exists_as_boundary` can detect.
/// When `last_known_recent_block == 0`, the query falls back to inclusive start
/// (`RangeFrom` on `start_height`) and the boundary check is skipped (compacted
/// phase always runs).
/// (`RangeAfter`) with that height. While the height is still in the recent
/// tree, it appears as a **boundary node** in the proof. Once compacted away,
/// it does not. When `last_known_recent_block == 0`, the query falls back to
/// inclusive start (`RangeFrom` on `start_height`), whose proof does not carry
/// its start key as a boundary. A proof with no boundary cannot show that the
/// recent zone covers the blocks since `start_height`, so the compacted phase
/// runs.
///
/// - **Phase 1 (recent, always first)**: Single non-paginated query via
/// `fetch_with_metadata_and_proof`. The recent (uncompacted) zone covers at
/// most 64 blocks, well under the server limit of 100 entries per request.
///
/// - **Phase 2 (compacted, conditional)**: Paginated query (25-entry batches)
/// covering the compacted range. Only runs when the boundary check detects that
/// the cursor height was compacted away.
/// covering the compacted range. Skipped only when the recent proof shows the
/// recent zone reaches back to the checkpoint.
///
/// - **Phase 3 (apply recent)**: The recent results fetched in Phase 1 are applied
/// after the (optional) compacted phase.
Expand Down Expand Up @@ -607,44 +609,20 @@ async fn incremental_catch_up<P: AddressProvider>(
//
// Based on three values:
// 1. checkpoint_height: the tree scan snapshot height
// 2. last_recent_block: highest boundary in the recent proof (the actual
// last block in the recent tree)
// 2. last_recent_block: highest boundary in the recent proof, a block
// proven to still be in the recent tree
// 3. Whether boundaries exist at all
//
// If last_recent_block >= checkpoint_height → recent covers the full range,
// no compacted data can exist in the gap. Skip compacted.
// If last_recent_block < checkpoint_height → there's a gap that may contain
// compacted data. Query compacted.
// If no boundaries → recent tree is empty, nothing to compact. Skip.
let need_compacted = match get_last_recent_block_from_proof(&recent_proof) {
Ok(Some(last_recent_block)) => {
if last_recent_block >= result.checkpoint_height {
debug!(
"Address sync: last recent block {} >= checkpoint {} — skipping compacted",
last_recent_block, result.checkpoint_height
);
false
} else {
debug!(
"Address sync: last recent block {} < checkpoint {} — need compacted",
last_recent_block, result.checkpoint_height
);
true
}
}
Ok(None) => {
// No boundaries in recent tree → empty, nothing to compact
debug!("Address sync: recent tree empty (no boundaries) — skipping compacted");
false
}
Err(e) => {
debug!(
"Address sync: boundary extraction failed ({}), running compacted to be safe",
e
);
true
}
};
// If no boundaries → nothing proves the recent tree reaches back to
// start_height. A `RangeFrom` proof does not carry its start key as
// one, and a `RangeAfter` start key compacted away leaves none, while
// the blocks since start_height may sit in the compacted tree. Query
// compacted.
let need_compacted = needs_compacted_phase(&recent_proof, result.checkpoint_height);

// Phase 2b — Compacted (historical) catch-up (conditional)
if need_compacted {
Expand Down Expand Up @@ -786,6 +764,39 @@ async fn incremental_catch_up<P: AddressProvider>(
Ok(())
}

/// Phase 2 of [`incremental_catch_up`]: whether the compacted phase must run,
/// judged from the boundary nodes of the recent query's proof.
fn needs_compacted_phase(recent_proof: &Proof, checkpoint_height: u64) -> bool {
match get_last_recent_block_from_proof(recent_proof) {
Ok(Some(last_recent_block)) => {
if last_recent_block >= checkpoint_height {
debug!(
"Address sync: last recent block {} >= checkpoint {} — skipping compacted",
last_recent_block, checkpoint_height
);
false
} else {
debug!(
"Address sync: last recent block {} < checkpoint {} — need compacted",
last_recent_block, checkpoint_height
);
true
}
}
Ok(None) => {
debug!("Address sync: recent proof has no boundary — need compacted");
true
}
Err(e) => {
debug!(
"Address sync: boundary extraction failed ({}), running compacted to be safe",
e
);
true
}
}
}

// ── Address-balance change application ────────────────────────────────

/// A single borrowed address balance change, abstracting the recent
Expand Down Expand Up @@ -1172,7 +1183,8 @@ async fn refresh_and_replay_unknown<P: AddressProvider>(
///
/// Returns:
/// - `Ok(Some(height))` — the highest block height found as a boundary
/// - `Ok(None)` — no boundaries found (recent tree is empty)
/// - `Ok(None)` — no boundaries found; this does not mean the recent tree is
/// empty, only that the proof shows no block still in it
/// - `Err(...)` — proof decoding failed
fn get_last_recent_block_from_proof(proof: &Proof) -> Result<Option<u64>, Error> {
let path: [&[u8]; 2] = [
Expand Down Expand Up @@ -1394,6 +1406,12 @@ impl Sdk {
#[cfg(test)]
mod tests {
use super::*;
use drive::grovedb::operations::proof::{
GroveDBProof, GroveDBProofV0, MerkOnlyLayerProof, ProveOptions,
};
use drive::grovedb::{MerkProofNode, MerkProofOp};
use drive::grovedb_query::proofs::encode_into;
use std::collections::BTreeMap;

#[test]
fn test_extract_funds_from_element() {
Expand Down Expand Up @@ -1526,6 +1544,86 @@ mod tests {
assert!(result.is_err());
}

/// A decodable recent-query proof whose `[SavedBlockTransactions, 'm']`
/// layer holds `ops`. Mock expectations return an empty proof, which takes
/// the decode-error arm, so the boundary arms need a real one.
fn recent_proof(ops: &[MerkProofOp]) -> Proof {
let mut merk_proof = Vec::new();
encode_into(ops.iter(), &mut merk_proof);
let layer = |merk_proof, key: u8, child| MerkOnlyLayerProof {
merk_proof,
lower_layers: BTreeMap::from([(vec![key], child)]),
};
let address_balances = MerkOnlyLayerProof {
merk_proof,
lower_layers: BTreeMap::new(),
};
let root_layer = layer(
Vec::new(),
RootTree::SavedBlockTransactions as u8,
layer(Vec::new(), ADDRESS_BALANCES_KEY_U8, address_balances),
);
let proof = GroveDBProof::V0(GroveDBProofV0 {
root_layer,
prove_options: ProveOptions::default(),
});
let config = dpp::bincode::config::standard()
.with_big_endian()
.with_limit::<{ 256 * 1024 * 1024 }>();
Proof {
grovedb_proof: dpp::bincode::encode_to_vec(&proof, config).expect("encode proof"),
..Default::default()
}
}

/// A block returned as a query result: a `KV` node, never a boundary.
fn block_result(height: u64) -> MerkProofOp {
MerkProofOp::Push(MerkProofNode::KV(height.to_be_bytes().to_vec(), vec![0; 4]))
}

/// The start key of an exclusive range that is still in the recent tree.
fn boundary(height: u64) -> MerkProofOp {
MerkProofOp::Push(MerkProofNode::KVDigest(
height.to_be_bytes().to_vec(),
[0; 32],
))
}

/// Testnet SH-040: the trunk checkpoint (642131) predates a funding in a
/// block the server then compacted. The `RangeFrom` recent proof holds only
/// the later block 642139 and no boundary, so only the compacted phase can
/// deliver the credit. A genuinely empty recent zone proves no more.
#[test]
fn should_run_compacted_phase_when_recent_proof_has_no_boundary() {
for proof in [recent_proof(&[block_result(642_139)]), recent_proof(&[])] {
assert!(matches!(get_last_recent_block_from_proof(&proof), Ok(None)));
assert!(needs_compacted_phase(&proof, 642_131));
}
}

#[test]
fn should_skip_compacted_phase_when_a_boundary_reaches_the_checkpoint() {
for height in [642_131, 642_135] {
let proof = recent_proof(&[boundary(height), block_result(height + 1)]);
assert!(!needs_compacted_phase(&proof, 642_131));
}
}

#[test]
fn should_run_compacted_phase_when_the_boundary_predates_the_checkpoint() {
let proof = recent_proof(&[boundary(642_125), block_result(642_126)]);
assert!(needs_compacted_phase(&proof, 642_131));
}

#[test]
fn should_run_compacted_phase_when_recent_proof_is_undecodable() {
let proof = Proof {
grovedb_proof: vec![0xFF, 0xFE, 0xFD, 0xFC],
..Default::default()
};
assert!(needs_compacted_phase(&proof, 642_131));
}

#[test]
fn test_result_new_sync_height_max() {
// new_sync_height should be max of current and observed tip
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ mod tests {
let settings = QuerySettings {
request_settings: &request_settings,
protocol_version: PlatformVersion::latest(),
verified_protocol_version: 0,
prove: true,
};
let sent: GetIdentitiesContractKeysRequest = query.query(&settings).expect("request");
Expand All @@ -243,6 +244,7 @@ mod tests {
let settings = QuerySettings {
request_settings: &request_settings,
protocol_version: PlatformVersion::latest(),
verified_protocol_version: 0,
prove: false,
};
let sent: GetIdentitiesContractKeysRequest = query.query(&settings).expect("request");
Expand Down
Loading