Skip to content

Commit 924b7ee

Browse files
authored
feat: Switch from sparse MT to concurrent MT (#449)
* feat: Switch from sparse MT to concurrent MT Our concurrent Merkle tree implementation supports an append-only case by ability to define the size of changelog as 0. Therefore, there is no point in keeping multiple implementations. The old implementation of sparse Merkle tree gets removed here. * fix(programs): Store Merkle trees as byte arrays Anchor is not smart enough to support const generics in account field types. It also doesn't support using constants in array fields. This change contains the following workaround: * Storing Merkle trees as byte arrays. * Defining their sizes as hard-coded integer... * Providing methods which use unsafe Rust to cast byte slices to MT struct references.
1 parent a8a6007 commit 924b7ee

File tree

29 files changed

+495
-787
lines changed

29 files changed

+495
-787
lines changed

Cargo.lock

+2-14
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli/accounts/merkle-tree-set-0.json

+3-3
Large diffs are not rendered by default.

merkle-tree/concurrent/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ description = "Concurrent Merkle tree implementation"
66
license = "Apache-2.0"
77

88
[dependencies]
9+
bytemuck = "1.14"
910
light-hasher = { path = "../hasher", version = "0.1.0" }
1011

1112
[dev-dependencies]

merkle-tree/concurrent/src/lib.rs

+72-17
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::{cmp::Ordering, marker::PhantomData};
22

3+
use bytemuck::{Pod, Zeroable};
34
use hash::compute_root;
45
use light_hasher::{errors::HasherError, Hasher};
56

@@ -23,6 +24,7 @@ use crate::{changelog::ChangelogEntry, hash::compute_parent_node};
2324
/// Due to ability to make a decent number of concurrent update requests to be
2425
/// valid, no lock is necessary.
2526
#[repr(C)]
27+
#[derive(Copy, Clone)]
2628
pub struct ConcurrentMerkleTree<
2729
H,
2830
const HEIGHT: usize,
@@ -31,18 +33,18 @@ pub struct ConcurrentMerkleTree<
3133
> where
3234
H: Hasher,
3335
{
36+
/// Index of the newest non-empty leaf.
37+
pub next_index: u64,
38+
/// History of roots.
39+
pub roots: [[u8; 32]; MAX_ROOTS],
3440
/// History of Merkle proofs.
3541
pub changelog: [ChangelogEntry<HEIGHT>; MAX_CHANGELOG],
3642
/// Index of the newest changelog.
3743
pub current_changelog_index: u64,
38-
/// History of roots.
39-
pub roots: [[u8; 32]; MAX_ROOTS],
4044
/// Index of the newest root.
4145
pub current_root_index: u64,
4246
/// The newest Merkle proof.
4347
pub rightmost_proof: [[u8; 32]; HEIGHT],
44-
/// Index of the newest non-empty leaf.
45-
pub rightmost_index: u64,
4648
/// The newest non-empty leaf.
4749
pub rightmost_leaf: [u8; 32],
4850

@@ -61,13 +63,54 @@ where
6163
roots: [[0u8; 32]; MAX_ROOTS],
6264
current_root_index: 0,
6365
rightmost_proof: [[0u8; 32]; HEIGHT],
64-
rightmost_index: 0,
66+
next_index: 0,
6567
rightmost_leaf: [0u8; 32],
6668
_hasher: PhantomData,
6769
}
6870
}
6971
}
7072

73+
/// Mark `ConcurrentMerkleTree` as `Zeroable`, providing Anchor a guarantee
74+
/// that it can be always initialized with zeros.
75+
///
76+
/// # Safety
77+
///
78+
/// [`bytemuck`](bytemuck) is not able to ensure that our custom types (`Hasher`
79+
/// and `ConcurrentMerkleTree`) can be a subject of initializing with zeros. It
80+
/// also doesn't support structs with const generics (it would need to ensure
81+
/// alignment).
82+
///
83+
/// Therefore, it's our responsibility to guarantee that `ConcurrentMerkleTree`
84+
/// doesn't contain any fields which are not zeroable.
85+
unsafe impl<H, const HEIGHT: usize, const MAX_CHANGELOG: usize, const MAX_ROOTS: usize> Zeroable
86+
for ConcurrentMerkleTree<H, HEIGHT, MAX_CHANGELOG, MAX_ROOTS>
87+
where
88+
H: Hasher,
89+
{
90+
}
91+
92+
/// Mark `ConcurrentMerkleTree` as `Pod` (Plain Old Data), providing Anchor a
93+
/// guarantee that it can be used in a zero-copy account.
94+
///
95+
/// # Safety
96+
///
97+
/// [`bytemuck`](bytemuck) is not able to ensure that our custom types (`Hasher`
98+
/// and `ConcurrentMerkleTree`) can be a subject of byte serialization. It also
99+
/// doesn't support structs with const generics (it would need to ensure
100+
/// alignment).
101+
///
102+
/// Therefore, it's our responsibility to guarantee that:
103+
///
104+
/// * `Hasher` and `ConcurrentMerkleTree` with given const generics are aligned.
105+
/// * They don't contain any fields which are not implementing `Copy` or are
106+
/// not an easy subject for byte serialization.
107+
unsafe impl<H, const HEIGHT: usize, const MAX_CHANGELOG: usize, const MAX_ROOTS: usize> Pod
108+
for ConcurrentMerkleTree<H, HEIGHT, MAX_CHANGELOG, MAX_ROOTS>
109+
where
110+
H: Hasher + Copy + 'static,
111+
{
112+
}
113+
71114
impl<H, const HEIGHT: usize, const MAX_CHANGELOG: usize, const MAX_ROOTS: usize>
72115
ConcurrentMerkleTree<H, HEIGHT, MAX_CHANGELOG, MAX_ROOTS>
73116
where
@@ -236,14 +279,14 @@ where
236279
.ok_or(HasherError::RootsZero)? = node;
237280

238281
// Update the rightmost proof. It has to be done only if tree is not full.
239-
if self.rightmost_index < (1 << HEIGHT) {
240-
if self.rightmost_index > 0 && leaf_index < self.rightmost_index as usize - 1 {
282+
if self.next_index < (1 << HEIGHT) {
283+
if self.next_index > 0 && leaf_index < self.next_index as usize - 1 {
241284
// Update the rightmost proof with the current changelog entry when:
242285
//
243286
// * `rightmost_index` is greater than 0 (tree is non-empty).
244287
// * The updated leaf is non-rightmost.
245288
if let Some(proof) = changelog_entry
246-
.update_proof(self.rightmost_index as usize - 1, &self.rightmost_proof)
289+
.update_proof(self.next_index as usize - 1, &self.rightmost_proof)
247290
{
248291
self.rightmost_proof = proof;
249292
}
@@ -272,7 +315,7 @@ where
272315
leaf_index: usize,
273316
proof: &[[u8; 32]; HEIGHT],
274317
) -> Result<(), HasherError> {
275-
let updated_proof = if self.rightmost_index > 0 && MAX_CHANGELOG > 0 {
318+
let updated_proof = if self.next_index > 0 && MAX_CHANGELOG > 0 {
276319
match self.update_proof_or_leaf(changelog_index, leaf_index, proof) {
277320
Some(proof) => proof,
278321
// This case means that the leaf we are trying to update was
@@ -284,7 +327,7 @@ where
284327
}
285328
}
286329
} else {
287-
if leaf_index != self.rightmost_index as usize {
330+
if leaf_index != self.next_index as usize {
288331
return Err(HasherError::AppendOnly);
289332
}
290333
proof.to_owned()
@@ -296,11 +339,11 @@ where
296339

297340
/// Appends a new leaf to the tree.
298341
pub fn append(&mut self, leaf: &[u8; 32]) -> Result<(), HasherError> {
299-
if self.rightmost_index >= 1 << HEIGHT {
342+
if self.next_index >= 1 << HEIGHT {
300343
return Err(HasherError::TreeFull);
301344
}
302345

303-
if self.rightmost_index == 0 {
346+
if self.next_index == 0 {
304347
// NOTE(vadorovsky): This is not mentioned in the whitepaper, but
305348
// appending to an empty Merkle tree is a special case, where
306349
// `computer_parent_node` can't be called, because the usual
@@ -321,7 +364,7 @@ where
321364
} else {
322365
let mut current_node = *leaf;
323366
let mut intersection_node = self.rightmost_leaf;
324-
let intersection_index = self.rightmost_index.trailing_zeros() as usize;
367+
let intersection_index = self.next_index.trailing_zeros() as usize;
325368
let mut changelog_path = [[0u8; 32]; HEIGHT];
326369

327370
for (i, item) in changelog_path.iter_mut().enumerate() {
@@ -334,7 +377,7 @@ where
334377
intersection_node = compute_parent_node::<H>(
335378
&intersection_node,
336379
&self.rightmost_proof[i],
337-
self.rightmost_index as usize - 1,
380+
self.next_index as usize - 1,
338381
i,
339382
)?;
340383
self.rightmost_proof[i] = empty_node;
@@ -347,7 +390,7 @@ where
347390
current_node = compute_parent_node::<H>(
348391
&current_node,
349392
&self.rightmost_proof[i],
350-
self.rightmost_index as usize - 1,
393+
self.next_index as usize - 1,
351394
i,
352395
)?;
353396
}
@@ -360,7 +403,7 @@ where
360403
.get_mut(self.current_changelog_index as usize)
361404
{
362405
*changelog_element =
363-
ChangelogEntry::new(current_node, changelog_path, self.rightmost_index as usize)
406+
ChangelogEntry::new(current_node, changelog_path, self.next_index as usize)
364407
}
365408
self.inc_current_root_index();
366409
*self
@@ -369,9 +412,21 @@ where
369412
.ok_or(HasherError::RootsZero)? = current_node;
370413
}
371414

372-
self.rightmost_index += 1;
415+
self.next_index += 1;
373416
self.rightmost_leaf = *leaf;
374417

375418
Ok(())
376419
}
420+
421+
/// Appends a new pair of leaves to the tree.
422+
pub fn append_two(
423+
&mut self,
424+
leaf_left: &[u8; 32],
425+
leaf_right: &[u8; 32],
426+
) -> Result<(), HasherError> {
427+
// TODO(vadorovsky): Instead of this naive double append, implement an
428+
// optimized insertion of two leaves.
429+
self.append(leaf_left)?;
430+
self.append(leaf_right)
431+
}
377432
}

merkle-tree/concurrent/tests/tests.rs

+10-10
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ where
8080
assert_eq!(merkle_tree.root().unwrap(), expected_root);
8181
assert_eq!(merkle_tree.current_root_index, 1);
8282
assert_eq!(merkle_tree.rightmost_proof, expected_proof);
83-
assert_eq!(merkle_tree.rightmost_index, 1);
83+
assert_eq!(merkle_tree.next_index, 1);
8484
assert_eq!(merkle_tree.rightmost_leaf, leaf1);
8585

8686
// Appending the 2nd leaf should result in recomputing the root due to the
@@ -123,7 +123,7 @@ where
123123
assert_eq!(merkle_tree.root().unwrap(), expected_root);
124124
assert_eq!(merkle_tree.current_root_index, 2);
125125
assert_eq!(merkle_tree.rightmost_proof, expected_proof);
126-
assert_eq!(merkle_tree.rightmost_index, 2);
126+
assert_eq!(merkle_tree.next_index, 2);
127127
assert_eq!(merkle_tree.rightmost_leaf, leaf2);
128128

129129
// Appending the 3rd leaf alters the next subtree on the right.
@@ -166,7 +166,7 @@ where
166166
assert_eq!(merkle_tree.root().unwrap(), expected_root);
167167
assert_eq!(merkle_tree.current_root_index, 3);
168168
assert_eq!(merkle_tree.rightmost_proof, expected_proof);
169-
assert_eq!(merkle_tree.rightmost_index, 3);
169+
assert_eq!(merkle_tree.next_index, 3);
170170
assert_eq!(merkle_tree.rightmost_leaf, leaf3);
171171

172172
// Appending the 4th leaf alters the next subtree on the right.
@@ -204,7 +204,7 @@ where
204204
assert_eq!(merkle_tree.root().unwrap(), expected_root);
205205
assert_eq!(merkle_tree.current_root_index, 4);
206206
assert_eq!(merkle_tree.rightmost_proof, expected_proof);
207-
assert_eq!(merkle_tree.rightmost_index, 4);
207+
assert_eq!(merkle_tree.next_index, 4);
208208
assert_eq!(merkle_tree.rightmost_leaf, leaf4);
209209
}
210210

@@ -257,7 +257,7 @@ where
257257
assert_eq!(merkle_tree.root().unwrap(), expected_root);
258258
assert_eq!(merkle_tree.current_root_index, 4);
259259
assert_eq!(merkle_tree.rightmost_proof, expected_proof);
260-
assert_eq!(merkle_tree.rightmost_index, 4);
260+
assert_eq!(merkle_tree.next_index, 4);
261261
assert_eq!(merkle_tree.rightmost_leaf, leaf4);
262262

263263
// Replace `leaf1`.
@@ -304,7 +304,7 @@ where
304304
// Note that we didn't create a new `expected_proof` here. We just re-used
305305
// the previous one.
306306
assert_eq!(merkle_tree.rightmost_proof, expected_proof);
307-
assert_eq!(merkle_tree.rightmost_index, 4);
307+
assert_eq!(merkle_tree.next_index, 4);
308308
assert_eq!(merkle_tree.rightmost_leaf, leaf4);
309309

310310
// Replace `leaf2`.
@@ -348,7 +348,7 @@ where
348348
assert_eq!(merkle_tree.root().unwrap(), expected_root);
349349
assert_eq!(merkle_tree.current_root_index, 6);
350350
assert_eq!(merkle_tree.rightmost_proof, expected_proof);
351-
assert_eq!(merkle_tree.rightmost_index, 4);
351+
assert_eq!(merkle_tree.next_index, 4);
352352
assert_eq!(merkle_tree.rightmost_leaf, leaf4);
353353

354354
// Replace `leaf3`.
@@ -392,7 +392,7 @@ where
392392
assert_eq!(merkle_tree.root().unwrap(), expected_root);
393393
assert_eq!(merkle_tree.current_root_index, 7);
394394
assert_eq!(merkle_tree.rightmost_proof, expected_proof);
395-
assert_eq!(merkle_tree.rightmost_index, 4);
395+
assert_eq!(merkle_tree.next_index, 4);
396396
assert_eq!(merkle_tree.rightmost_leaf, leaf4);
397397

398398
// Replace `leaf4`.
@@ -436,7 +436,7 @@ where
436436
assert_eq!(merkle_tree.current_root_index, 8);
437437
// This time `rightmost_*` fields should be changed.
438438
assert_eq!(merkle_tree.rightmost_proof, expected_proof);
439-
assert_eq!(merkle_tree.rightmost_index, 4);
439+
assert_eq!(merkle_tree.next_index, 4);
440440
assert_eq!(merkle_tree.rightmost_leaf, new_leaf4);
441441
}
442442

@@ -711,7 +711,7 @@ fn compare_trees<H, const HEIGHT: usize, const MAX_CHANGELOG: usize, const MAX_R
711711
spl_concurrent_mt.rightmost_proof.proof
712712
);
713713
assert_eq!(
714-
concurrent_mt.rightmost_index,
714+
concurrent_mt.next_index,
715715
spl_concurrent_mt.rightmost_proof.index as u64
716716
);
717717
assert_eq!(

merkle-tree/sparse/Cargo.toml

-15
This file was deleted.

merkle-tree/sparse/src/config.rs

-5
This file was deleted.

merkle-tree/sparse/src/constants/mod.rs

-4
This file was deleted.

0 commit comments

Comments
 (0)