Skip to content

Commit bfd09ed

Browse files
refactor!(stump): Move updated data to its own function
Before this change, `modify` would return the data needed to update a proof for the new block. This requires additional internal computation and extra allocations. During IBD we may never use this, since proof update is only meant for updating a few blocks worth of changes. Now there's a method specific to pull the modify_data, and `modify` itself will only return a new Stump. When refactoring the addition function, I've modified it a little to allow a smarter one with a very nice property: it can pretend that it added a node, but actually represent it as deleted. The goal here is to do a Swift Sync-style protocol where you don't need deletions. If a txout is already spent, you give an empty hash (BitcoinNodeHash::empty()). This will be exactly equivalent as giving this txo's hash and later on calling delete for with this txo's hash. However, it does this with only one call to modify and doesn't require proofs to achieve that. This is an API breaking change, as modify now only returns one parameter, otherwise the behavior stays unchanged.
1 parent 22d74a4 commit bfd09ed

7 files changed

Lines changed: 182 additions & 83 deletions

File tree

benches/proof_benchmarks.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ fn proof_verification(c: &mut Criterion) {
5555
let accumulator_size = 100;
5656
let hashes = generate_test_hashes(accumulator_size, 42);
5757
let stump = Stump::new();
58-
let (stump, _) = stump.modify(&hashes, &[], &Proof::default()).unwrap();
58+
let stump = stump.modify(&hashes, &[], &Proof::default()).unwrap();
5959

6060
for target_count in [1, 10].iter() {
6161
let del_hashes = hashes[..*target_count].to_vec();

benches/stump_benchmarks.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ fn stump_verify(c: &mut Criterion) {
5050
let test_size = 1000;
5151
let hashes = generate_test_hashes(test_size, 42);
5252
let stump = Stump::new();
53-
let (stump, _) = stump.modify(&hashes, &[], &Proof::default()).unwrap();
53+
let stump = stump.modify(&hashes, &[], &Proof::default()).unwrap();
5454

5555
for proof_size in [1, 10, 100].iter() {
5656
let del_hashes = hashes[..*proof_size].to_vec();

examples/full-accumulator.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,7 @@ fn main() {
2929
// Verify the proof. Notice how we use the del_hashes returned by `prove` here.
3030
let s = Stump::new()
3131
.modify(&elements, &[], &Proof::default())
32-
.unwrap()
33-
.0;
32+
.unwrap();
3433
assert_eq!(s.verify(&proof, &[elements[0]]), Ok(true));
3534
// Now we want to update the MemForest, by removing the first utxo, and adding a new one.
3635
// This would be in case we received a new block with a transaction spending the first utxo,

examples/proof-update.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ fn main() {
2222
let utxos = get_utxo_hashes1();
2323
// Add the UTXOs to the accumulator. update_data is the data we need to update the proof
2424
// after the accumulator is updated.
25-
let (s, update_data) = s.modify(&utxos, &[], &Proof::default()).unwrap();
25+
let update_data = s.get_update_data(&utxos, &[], &Proof::default()).unwrap();
26+
let s = s.modify(&utxos, &[], &Proof::default()).unwrap();
27+
2628
// Create an empty proof, we'll update it to hold our UTXOs
2729
let p = Proof::default();
2830
// Update the proof with the UTXOs we added to the accumulator. This proof was initially empty,
@@ -48,8 +50,10 @@ fn main() {
4850
// We'll remove `0` as it got spent, and add 1..7 to our cache.
4951
let new_utxos = get_utxo_hashes2();
5052
// First, update the accumulator
51-
let (stump, update_data) = s.modify(&new_utxos, &[utxos[0]], &p1).unwrap();
53+
let stump = s.modify(&new_utxos, &[utxos[0]], &p1).unwrap();
54+
5255
// and the proof
56+
let update_data = s.get_update_data(&utxos, &[], &Proof::default()).unwrap();
5357
let (p2, cached_hashes) = p
5458
.update(
5559
cached_hashes,

examples/simple-stump-update.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,7 @@ fn main() {
2727
// Create a new Stump, and add the utxos to it. Notice how we don't use the full return here,
2828
// but only the Stump. To understand what is the second return value, see the documentation
2929
// for `Stump::modify`, or the proof-update example.
30-
let s = Stump::new()
31-
.modify(&utxos, &[], &Proof::default())
32-
.unwrap()
33-
.0;
30+
let s = Stump::new().modify(&utxos, &[], &Proof::default()).unwrap();
3431
// Create a proof that the first utxo is in the Stump.
3532
let proof = Proof::new(vec![0], vec![utxos[1]]);
3633
assert_eq!(s.verify(&proof, &[utxos[0]]), Ok(true));
@@ -42,7 +39,7 @@ fn main() {
4239
"d3bd63d53c5a70050a28612a2f4b2019f40951a653ae70736d93745efb1124fa",
4340
)
4441
.unwrap();
45-
let s = s.modify(&[new_utxo], &[utxos[0]], &proof).unwrap().0;
42+
let s = s.modify(&[new_utxo], &[utxos[0]], &proof).unwrap();
4643
// Now we can verify that the new utxo is in the Stump, and the old one is not.
4744
let new_proof = Proof::new(vec![2], vec![new_utxo]);
4845
assert_eq!(s.verify(&new_proof, &[new_utxo]), Ok(true));

src/accumulator/proof.rs

Lines changed: 34 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
//! hashes.push(sha256::Hash::from_engine(engine).into())
4949
//! }
5050
//! // Add the UTXOs to the accumulator
51-
//! let s = s.modify(&hashes, &vec![], &Proof::default()).unwrap().0;
51+
//! let s = s.modify(&hashes, &vec![], &Proof::default()).unwrap();
5252
//! // Create a proof for the targets
5353
//! let p = Proof::new(targets, proof_hashes);
5454
//! // Verify the proof
@@ -123,7 +123,7 @@ pub(crate) type NodesAndRootsCurrent<Hash> = (Vec<(u64, Hash)>, Vec<Hash>);
123123
/// This is used when we need to return the nodes and roots for a proof
124124
/// if we are concerned with deleting those elements. The difference is that
125125
/// we need to retun the old and updatated roots in the accumulator.
126-
pub(crate) type NodesAndRootsOldNew<Hash> = (Vec<(u64, Hash)>, Vec<(Hash, Hash)>);
126+
pub(crate) type RootsOldNew<Hash> = Vec<(Hash, Hash)>;
127127

128128
impl Proof {
129129
/// Creates a proof from a vector of target and hashes.
@@ -260,7 +260,7 @@ impl<Hash: AccumulatorHash> Proof<Hash> {
260260
/// engine.input(&[i]);
261261
/// hashes.push(sha256::Hash::from_engine(engine).into())
262262
/// }
263-
/// let s = s.modify(&hashes, &vec![], &Proof::default()).unwrap().0;
263+
/// let s = s.modify(&hashes, &vec![], &Proof::default()).unwrap();
264264
/// let p = Proof::new(targets, proof_hashes);
265265
/// assert!(s.verify(&p, &[hashes[0]]).expect("This proof is valid"));
266266
/// ```
@@ -434,7 +434,7 @@ impl<Hash: AccumulatorHash> Proof<Hash> {
434434
&self,
435435
del_hashes: &[(Hash, Hash)],
436436
num_leaves: u64,
437-
) -> Result<NodesAndRootsOldNew<Hash>, String> {
437+
) -> Result<RootsOldNew<Hash>, String> {
438438
// Where all the root hashes that we've calculated will go to.
439439
let total_rows = util::tree_rows(num_leaves);
440440

@@ -495,13 +495,7 @@ impl<Hash: AccumulatorHash> Proof<Hash> {
495495
computed.push((parent, (old_parent_hash, parent_hash)));
496496
}
497497

498-
// we shouldn't return the hashes in the proof
499-
nodes.extend(computed);
500-
let nodes = nodes
501-
.into_iter()
502-
.map(|(pos, (_, new_hash))| (pos, new_hash))
503-
.collect();
504-
Ok((nodes, calculated_root_hashes))
498+
Ok(calculated_root_hashes)
505499
}
506500

507501
/// This function computes a set of roots from a proof.
@@ -565,7 +559,13 @@ impl<Hash: AccumulatorHash> Proof<Hash> {
565559
return Err(format!("Missing sibling for {next_pos}"));
566560
}
567561

568-
let parent_hash = AccumulatorHash::parent_hash(&next_hash, &sibling_hash);
562+
let parent_hash = match (next_hash.is_empty(), sibling_hash.is_empty()) {
563+
(true, true) => AccumulatorHash::empty(),
564+
(true, false) => sibling_hash,
565+
(false, true) => next_hash,
566+
(false, false) => AccumulatorHash::parent_hash(&next_hash, &sibling_hash),
567+
};
568+
569569
let parent = util::parent(next_pos, total_rows);
570570
computed.push((parent, parent_hash));
571571
}
@@ -1006,7 +1006,10 @@ mod tests {
10061006

10071007
let block_proof =
10081008
Proof::new(case_values.update.proof.targets.clone(), block_proof_hashes);
1009-
let (stump, updated) = stump.modify(&utxos, &del_hashes, &block_proof).unwrap();
1009+
let new_stump = stump.modify(&utxos, &del_hashes, &block_proof).unwrap();
1010+
let updated = stump
1011+
.get_update_data(&utxos, &del_hashes, &block_proof)
1012+
.unwrap();
10101013
let (cached_proof, cached_hashes) = cached_proof
10111014
.update(
10121015
cached_hashes.clone(),
@@ -1017,7 +1020,7 @@ mod tests {
10171020
)
10181021
.unwrap();
10191022

1020-
let res = stump.verify(&cached_proof, &cached_hashes);
1023+
let res = new_stump.verify(&cached_proof, &cached_hashes);
10211024

10221025
let expected_roots: Vec<_> = case_values
10231026
.expected_roots
@@ -1032,7 +1035,7 @@ mod tests {
10321035
.collect();
10331036
assert_eq!(res, Ok(true));
10341037
assert_eq!(cached_proof.targets, case_values.expected_targets);
1035-
assert_eq!(stump.roots, expected_roots);
1038+
assert_eq!(new_stump.roots, expected_roots);
10361039
assert_eq!(cached_hashes, expected_cached_hashes);
10371040
}
10381041
}
@@ -1192,7 +1195,7 @@ mod tests {
11921195
fn test_update_proof_delete() {
11931196
let preimages = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
11941197
let hashes = preimages.into_iter().map(hash_from_u8).collect::<Vec<_>>();
1195-
let (stump, _) = Stump::new()
1198+
let stump = Stump::new()
11961199
.modify(&hashes, &[], &Proof::default())
11971200
.unwrap();
11981201

@@ -1220,13 +1223,22 @@ mod tests {
12201223

12211224
let proof = Proof::new(vec![1, 2, 6], proof_hashes);
12221225

1223-
let (stump, modified) = stump
1226+
let new_stump = stump
12241227
.modify(
12251228
&[],
12261229
&[hash_from_u8(1), hash_from_u8(2), hash_from_u8(6)],
12271230
&proof,
12281231
)
12291232
.unwrap();
1233+
1234+
let modified = stump
1235+
.get_update_data(
1236+
&[],
1237+
&[hash_from_u8(1), hash_from_u8(2), hash_from_u8(6)],
1238+
&proof,
1239+
)
1240+
.unwrap();
1241+
12301242
let (new_proof, _) = cached_proof
12311243
.update_proof_remove(
12321244
vec![1, 2, 6],
@@ -1236,7 +1248,7 @@ mod tests {
12361248
)
12371249
.unwrap();
12381250

1239-
let res = stump.verify(&new_proof, &[hash_from_u8(0), hash_from_u8(7)]);
1251+
let res = new_stump.verify(&new_proof, &[hash_from_u8(0), hash_from_u8(7)]);
12401252
assert_eq!(res, Ok(true));
12411253
}
12421254

@@ -1249,8 +1261,7 @@ mod tests {
12491261
// Create a new stump with 8 leaves and 1 root
12501262
let s = Stump::new()
12511263
.modify(&hashes, &[], &Proof::default())
1252-
.expect("This stump is valid")
1253-
.0;
1264+
.expect("This stump is valid");
12541265

12551266
// Nodes that will be deleted
12561267
let del_hashes = vec![hashes[0], hashes[2], hashes[4], hashes[6]];
@@ -1342,35 +1353,18 @@ mod tests {
13421353
.map(|hash| (hash, BitcoinNodeHash::empty()))
13431354
.collect::<Vec<_>>();
13441355

1345-
let (computed, roots) = p.calculate_hashes_delete(&del_hashes, 8).unwrap();
1356+
let roots = p.calculate_hashes_delete(&del_hashes, 8).unwrap();
13461357
let expected_root_old = BitcoinNodeHash::from_str(
13471358
"b151a956139bb821d4effa34ea95c17560e0135d1e4661fc23cedc3af49dac42",
13481359
)
13491360
.unwrap();
1361+
13501362
let expected_root_new = BitcoinNodeHash::from_str(
13511363
"726fdd3b432cc59e68487d126e70f0db74a236267f8daeae30b31839a4e7ebed",
13521364
)
13531365
.unwrap();
13541366

1355-
let computed_positions = [0_u64, 1, 9, 13, 8, 12, 14].to_vec();
1356-
let computed_hashes = [
1357-
"0000000000000000000000000000000000000000000000000000000000000000",
1358-
"4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a",
1359-
"9576f4ade6e9bc3a6458b506ce3e4e890df29cb14cb5d3d887672aef55647a2b",
1360-
"29590a14c1b09384b94a2c0e94bf821ca75b62eacebc47893397ca88e3bbcbd7",
1361-
"4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a",
1362-
"2b77298feac78ab51bc5079099a074c6d789bd350442f5079fcba2b3402694e5",
1363-
"726fdd3b432cc59e68487d126e70f0db74a236267f8daeae30b31839a4e7ebed",
1364-
]
1365-
.iter()
1366-
.map(|hash| BitcoinNodeHash::from_str(hash).unwrap())
1367-
.collect::<Vec<_>>();
1368-
let expected_computed: Vec<_> = computed_positions
1369-
.into_iter()
1370-
.zip(computed_hashes)
1371-
.collect();
13721367
assert_eq!(roots, vec![(expected_root_old, expected_root_new)]);
1373-
assert_eq!(computed, expected_computed);
13741368
}
13751369

13761370
#[test]
@@ -1392,8 +1386,7 @@ mod tests {
13921386
// Create a new stump with 8 leaves and 1 root
13931387
let s = Stump::new()
13941388
.modify(&hashes, &[], &Proof::default())
1395-
.expect("This stump is valid")
1396-
.0;
1389+
.expect("This stump is valid");
13971390

13981391
// Nodes that will be deleted
13991392
let del_hashes = vec![hashes[0], hashes[2], hashes[4], hashes[6]];

0 commit comments

Comments
 (0)