From db2839f6eb2d8c1f4245d5f91e5604dbb970f863 Mon Sep 17 00:00:00 2001 From: ananas-block Date: Thu, 24 Sep 2026 01:25:26 +0100 Subject: [PATCH] fix: write only defined bytes into v1 tree and queue accounts The v1 concurrent Merkle tree changelog and the v1 hash set queues copied undefined bytes into account data, so account bytes depended on runtime stack contents. Those bytes can hold VM pointers, which makes account state diverge when the virtual_address_space_adjustments feature changes the address layout. Concurrent Merkle tree changelog: - CyclicBoundedVec::push copies a ChangelogEntry with ptr::write, including the undefined value bytes of None nodes and the repr(C) padding between path and index. Route all pushes through push_changelog_entry, which zeroes the slot and then writes only the index and the Some nodes. Hash set buckets: - Writing None or Some(HashSetCell { sequence_number: None, .. }) left the unused enum payload bytes undefined. Write buckets through a repr(C) RawHashSetCell that defines all 48 bytes. - Reject zero-copy buffers that are missing the reserved 8-byte gap before the buckets instead of reading past the end. Pin the deployed layouts with compile-time assertions: changelog node tag values, entry sizes and index offsets for heights 22/26/32/40, the hash set bucket layout and niche tag values, and the v1 indexed changelog entry. Items used only by these assertions live inside the const blocks, because older rustc versions (platform-tools) report them as dead code. Types, account sizes and on-chain layout are unchanged. Add tests that prefill account buffers with marker bytes and check that every written byte is defined. --- .../concurrent-merkle-tree/src/changelog.rs | 21 ++++ .../concurrent-merkle-tree/src/lib.rs | 30 ++++- .../concurrent-merkle-tree/tests/tests.rs | 72 ++++++++++- program-libs/hash-set/src/lib.rs | 116 +++++++++++++---- program-libs/hash-set/src/zero_copy.rs | 18 ++- program-libs/hash-set/tests/defined_bytes.rs | 119 ++++++++++++++++++ .../indexed-merkle-tree/src/changelog.rs | 20 +++ .../tests/defined_bytes.rs | 107 ++++++++++++++++ 8 files changed, 460 insertions(+), 43 deletions(-) create mode 100644 program-libs/hash-set/tests/defined_bytes.rs create mode 100644 program-libs/indexed-merkle-tree/tests/defined_bytes.rs diff --git a/program-libs/concurrent-merkle-tree/src/changelog.rs b/program-libs/concurrent-merkle-tree/src/changelog.rs index 4ec6b6c00b..5bedff6302 100644 --- a/program-libs/concurrent-merkle-tree/src/changelog.rs +++ b/program-libs/concurrent-merkle-tree/src/changelog.rs @@ -4,6 +4,27 @@ use light_bounded_vec::BoundedVec; use crate::errors::ConcurrentMerkleTreeError; +// Pin the deployed v1 changelog account layout: a node is a tag byte +// (0 = None, 1 = Some) followed by the 32 value bytes, and `index` is the +// last u64 of the repr(C) entry. +const _: () = { + use std::mem::{offset_of, size_of}; + + assert!(size_of::>() == 33); + // SAFETY: The tag byte is always initialized. + assert!(unsafe { *(&Some([0u8; 32]) as *const Option<[u8; 32]> as *const u8) } == 1); + assert!(unsafe { *(&None::<[u8; 32]> as *const Option<[u8; 32]> as *const u8) } == 0); + + assert!(size_of::>() == 736); + assert!(size_of::>() == 872); + assert!(size_of::>() == 1064); + assert!(size_of::>() == 1328); + assert!(offset_of!(ChangelogEntry<22>, index) == 736 - 8); + assert!(offset_of!(ChangelogEntry<26>, index) == 872 - 8); + assert!(offset_of!(ChangelogEntry<32>, index) == 1064 - 8); + assert!(offset_of!(ChangelogEntry<40>, index) == 1328 - 8); +}; + #[derive(Clone, Debug, PartialEq, Eq)] #[repr(transparent)] pub struct ChangelogPath(pub [Option<[u8; 32]>; HEIGHT]); diff --git a/program-libs/concurrent-merkle-tree/src/lib.rs b/program-libs/concurrent-merkle-tree/src/lib.rs index 18e9cb6f94..15b0255a5e 100644 --- a/program-libs/concurrent-merkle-tree/src/lib.rs +++ b/program-libs/concurrent-merkle-tree/src/lib.rs @@ -17,7 +17,7 @@ use std::{ alloc::{self, handle_alloc_error, Layout}, iter::Skip, marker::PhantomData, - mem, + mem, ptr, }; use changelog::ChangelogPath; @@ -228,7 +228,7 @@ where // Initialize changelog. let path = ChangelogPath::from_fn(|i| Some(H::zero_bytes()[i])); let changelog_entry = ChangelogEntry { path, index: 0 }; - self.changelog.push(changelog_entry); + self.push_changelog_entry(changelog_entry); // Initialize filled subtrees. for i in 0..self.height { @@ -252,6 +252,27 @@ where self.changelog.last_index() } + /// Pushes `entry` so that every byte of its slot is defined. + /// + /// `CyclicBoundedVec::push` copies the struct with `ptr::write`, which + /// also copies the undefined value bytes of `None` nodes and the struct + /// padding between `path` and `index` from the stack into the account. + /// Instead, the slot is zeroed and only defined bytes are written. + fn push_changelog_entry(&mut self, entry: ChangelogEntry) { + self.changelog.push(ChangelogEntry::default_with_index(0)); + if let Some(slot) = self.changelog.last_mut() { + // SAFETY: All-zero bytes are a valid `ChangelogEntry` (all `None` + // nodes, index 0). This also zeroes the padding before `index`. + unsafe { ptr::write_bytes(slot as *mut ChangelogEntry, 0, 1) }; + slot.index = entry.index; + for (dst, src) in slot.path.iter_mut().zip(entry.path.iter()) { + if src.is_some() { + *dst = *src; + } + } + } + } + /// Returns the index of the current root in the tree's root buffer. pub fn root_index(&self) -> usize { self.roots.last_index() @@ -448,7 +469,7 @@ where self.set_rightmost_leaf(new_leaf); } } - self.changelog.push(changelog_entry); + self.push_changelog_entry(changelog_entry); if self.canopy_depth > 0 { self.update_canopy(self.changelog.last_index(), 1); @@ -569,8 +590,7 @@ where for (leaf_i, leaf) in leaves.iter().enumerate() { let mut current_index = self.next_index(); - self.changelog - .push(ChangelogEntry::::default_with_index(current_index)); + self.push_changelog_entry(ChangelogEntry::::default_with_index(current_index)); let changelog_index = self.changelog_index(); let mut current_node = **leaf; diff --git a/program-libs/concurrent-merkle-tree/tests/tests.rs b/program-libs/concurrent-merkle-tree/tests/tests.rs index 40a498228e..fcf48959f6 100644 --- a/program-libs/concurrent-merkle-tree/tests/tests.rs +++ b/program-libs/concurrent-merkle-tree/tests/tests.rs @@ -1,4 +1,7 @@ -use std::cmp; +use std::{ + cmp, + mem::{offset_of, size_of}, +}; use ark_bn254::Fr; use ark_ff::{BigInteger, PrimeField, UniformRand}; @@ -3546,3 +3549,70 @@ fn test_update_with_canopy_poseidon() { fn test_update_with_canopy_sha256() { update_with_canopy::() } + +/// Every byte of every changelog entry written into the account buffer must +/// be defined: `None` nodes must be all-zero and the struct padding between +/// `path` and `index` must be zero. The buffer is pre-filled with a marker so +/// any byte that is merely left untouched (instead of written) is detected. +#[test] +fn test_changelog_bytes_are_defined() { + // 33 * 10 = 330 bytes of path, leaving 6 bytes of padding before `index`. + const HEIGHT: usize = 10; + const CHANGELOG: usize = 8; + const ROOTS: usize = 8; + const CANOPY: usize = 0; + let path_size = size_of::>(); + let index_offset = offset_of!(ChangelogEntry, index); + assert_eq!(index_offset - path_size, 6); + + let mut bytes = vec![ + 0xFFu8; + ConcurrentMerkleTree::::size_in_account( + HEIGHT, CHANGELOG, ROOTS, CANOPY + ) + ]; + let mut merkle_tree = + ConcurrentMerkleTreeZeroCopyMut::::from_bytes_zero_copy_init( + bytes.as_mut_slice(), + HEIGHT, + CANOPY, + CHANGELOG, + ROOTS, + ) + .unwrap(); + // `init` writes a full path, `append_batch` writes partial paths with + // `None` nodes. + merkle_tree.init().unwrap(); + merkle_tree + .append_batch(&[&[1; 32], &[2; 32], &[3; 32]]) + .unwrap(); + + for changelog_index in 0..merkle_tree.changelog.len() { + let entry = merkle_tree.changelog.get(changelog_index).unwrap(); + // SAFETY: The entry lives in `bytes`, which was fully initialized + // with the marker before the tree was created. + let entry_bytes = unsafe { + std::slice::from_raw_parts( + entry as *const ChangelogEntry as *const u8, + size_of::>(), + ) + }; + let (path_bytes, rest) = entry_bytes.split_at(path_size); + let (padding, _index) = rest.split_at(index_offset - path_size); + for (level, node) in path_bytes.chunks_exact(33).enumerate() { + let (tag, value) = node.split_first().unwrap(); + match *tag { + 1 => {} + 0 => assert!( + value.iter().all(|b| *b == 0), + "entry {changelog_index} level {level}: None node has non-zero value bytes" + ), + tag => panic!("entry {changelog_index} level {level}: invalid tag {tag}"), + } + } + assert!( + padding.iter().all(|b| *b == 0), + "entry {changelog_index}: padding bytes are not zero" + ); + } +} diff --git a/program-libs/hash-set/src/lib.rs b/program-libs/hash-set/src/lib.rs index 8e5ac67ec1..87a205e383 100644 --- a/program-libs/hash-set/src/lib.rs +++ b/program-libs/hash-set/src/lib.rs @@ -16,7 +16,7 @@ use std::{ cmp::Ordering, marker::Send, mem, - ptr::NonNull, + ptr::{self, NonNull}, }; use light_hasher::{bigint::bigint_to_be_bytes_array, HasherError}; @@ -73,6 +73,81 @@ pub struct HashSetCell { pub sequence_number: Option, } +const UNMARKED_BUCKET_TAG: usize = 0; +const EMPTY_BUCKET_TAG: usize = 2; + +#[repr(C)] +struct RawHashSetCell { + tag: usize, + sequence_number: usize, + value: [u8; 32], +} + +// `Option` uses the unused discriminants of the nested +// `Option` for its outer `None`. These are the deployed v1 account +// bytes, so fail compilation if the Rust layout changes. +const _: () = { + assert!(mem::size_of::() == 8); + assert!(mem::size_of::>() == 16); + assert!(mem::size_of::() == 48); + assert!(mem::align_of::() == 8); + assert!(mem::offset_of!(HashSetCell, sequence_number) == 0); + assert!(mem::offset_of!(HashSetCell, value) == 16); + assert!(mem::size_of::>() == 48); + assert!(mem::align_of::>() == 8); + + assert!(mem::size_of::() == mem::size_of::>()); + assert!(mem::align_of::() == mem::align_of::>()); + assert!(mem::offset_of!(RawHashSetCell, tag) == 0); + assert!(mem::offset_of!(RawHashSetCell, sequence_number) == 8); + assert!(mem::offset_of!(RawHashSetCell, value) == 16); + + // Only `mark_with_sequence_number` produces marked buckets, via a field + // write of `Some`, so this tag is only asserted, never written raw. + const MARKED_BUCKET_TAG: usize = 1; + const fn bucket_tag(bucket: &Option) -> usize { + // SAFETY: The assertions above pin the enum tag to the first + // `usize`, which rustc always initializes. + unsafe { *(bucket as *const Option as *const usize) } + } + let unmarked = HashSetCell { + value: [0; 32], + sequence_number: None, + }; + assert!(bucket_tag(&Some(unmarked)) == UNMARKED_BUCKET_TAG); + let marked = HashSetCell { + value: [0; 32], + sequence_number: Some(0), + }; + assert!(bucket_tag(&Some(marked)) == MARKED_BUCKET_TAG); + assert!(bucket_tag(&None) == EMPTY_BUCKET_TAG); +}; + +/// Writes every byte of a bucket without copying undefined enum payload bytes. +unsafe fn write_bucket( + bucket: *mut Option, + tag: usize, + sequence_number: usize, + value: [u8; 32], +) { + ptr::write( + bucket.cast::(), + RawHashSetCell { + tag, + sequence_number, + value, + }, + ); +} + +unsafe fn write_empty_bucket(bucket: *mut Option) { + write_bucket(bucket, EMPTY_BUCKET_TAG, 0, [0; 32]); +} + +unsafe fn write_unmarked_bucket(bucket: *mut Option, value: [u8; 32]) { + write_bucket(bucket, UNMARKED_BUCKET_TAG, 0, value); +} + unsafe impl Send for HashSet {} impl HashSetCell { @@ -145,14 +220,11 @@ impl HashSet { /// Size which needs to be allocated on Solana account to fit the hash set. pub fn size_in_account(capacity_values: usize) -> usize { - let dyn_fields_size = Self::non_dyn_fields_size(); - - let buckets_size_unaligned = mem::size_of::>() * capacity_values; - // Make sure that alignment of `values` matches the alignment of `usize`. - let buckets_size = buckets_size_unaligned + mem::align_of::() - - (buckets_size_unaligned % mem::align_of::()); + Self::buckets_offset() + mem::size_of::>() * capacity_values + } - dyn_fields_size + buckets_size + pub(crate) fn buckets_offset() -> usize { + Self::non_dyn_fields_size() + mem::size_of::() } // Create a new hash set with the given capacity @@ -166,7 +238,7 @@ impl HashSet { let values = NonNull::new(values_ptr).unwrap(); for i in 0..capacity_values { unsafe { - std::ptr::write(values_ptr.add(i), None); + write_empty_bucket(values_ptr.add(i)); } } @@ -213,11 +285,7 @@ impl HashSet { handle_alloc_error(buckets_layout); } let buckets = NonNull::new(buckets_dst_ptr).unwrap(); - for i in 0..capacity { - std::ptr::write(buckets_dst_ptr.add(i), None); - } - - let offset = Self::non_dyn_fields_size() + mem::size_of::(); + let offset = Self::buckets_offset(); let buckets_src_ptr = bytes.as_ptr().add(offset) as *const Option; std::ptr::copy(buckets_src_ptr, buckets_dst_ptr, capacity); @@ -286,25 +354,23 @@ impl HashSet { // PANICS: We trust the bounds of `value_index` here. let bucket = self.get_bucket_mut(value_index).unwrap(); - match bucket { + match *bucket { // The cell in the value array is already taken. - Some(bucket) => { + Some(cell) => { // We can overwrite that cell only if the element // is expired - when the difference between its // sequence number and provided sequence number is // greater than the threshold. - if let Some(element_sequence_number) = bucket.sequence_number { + if let Some(element_sequence_number) = cell.sequence_number { if current_sequence_number >= element_sequence_number { - *bucket = HashSetCell { - value: bigint_to_be_bytes_array(value)?, - sequence_number: None, - }; + let value = bigint_to_be_bytes_array(value)?; + unsafe { write_unmarked_bucket(bucket, value) }; return Ok(true); } } // Otherwise, we need to prevent having multiple valid // elements with the same value. - if &BigUint::from_be_bytes(bucket.value.as_slice()) == value { + if &BigUint::from_be_bytes(cell.value.as_slice()) == value { return Err(HashSetError::ElementAlreadyExists); } } @@ -347,10 +413,8 @@ impl HashSet { // PANICS: We trust the bounds of `index`. let bucket = self.get_bucket_mut(index).unwrap(); - *bucket = Some(HashSetCell { - value: bigint_to_be_bytes_array(value)?, - sequence_number: None, - }); + let value = bigint_to_be_bytes_array(value)?; + unsafe { write_unmarked_bucket(bucket, value) }; return Ok(index); } } diff --git a/program-libs/hash-set/src/zero_copy.rs b/program-libs/hash-set/src/zero_copy.rs index e4e35d6da8..3b0d865136 100644 --- a/program-libs/hash-set/src/zero_copy.rs +++ b/program-libs/hash-set/src/zero_copy.rs @@ -5,7 +5,7 @@ use std::{ ptr::NonNull, }; -use crate::{HashSet, HashSetCell, HashSetError}; +use crate::{write_empty_bucket, HashSet, HashSetCell, HashSetError}; /// A `HashSet` wrapper which can be instantiated from Solana account bytes /// without copying them. @@ -43,11 +43,9 @@ impl<'a> HashSetZeroCopy<'a> { let capacity_values = usize::from_le_bytes(bytes[0..8].try_into().unwrap()); let sequence_threshold = usize::from_le_bytes(bytes[8..16].try_into().unwrap()); - let offset = HashSet::non_dyn_fields_size() + mem::size_of::(); + let offset = HashSet::buckets_offset(); - let values_size = mem::size_of::>() * capacity_values; - - let expected_size = HashSet::non_dyn_fields_size() + values_size; + let expected_size = HashSet::size_in_account(capacity_values); if bytes.len() < expected_size { return Err(HashSetError::BufferSize(expected_size, bytes.len())); } @@ -98,11 +96,9 @@ impl<'a> HashSetZeroCopy<'a> { capacity_values: usize, sequence_threshold: usize, ) -> Result { - if bytes.len() < HashSet::non_dyn_fields_size() { - return Err(HashSetError::BufferSize( - HashSet::non_dyn_fields_size(), - bytes.len(), - )); + let expected_size = HashSet::size_in_account(capacity_values); + if bytes.len() < expected_size { + return Err(HashSetError::BufferSize(expected_size, bytes.len())); } bytes[0..8].copy_from_slice(&capacity_values.to_le_bytes()); @@ -112,7 +108,7 @@ impl<'a> HashSetZeroCopy<'a> { let hash_set = Self::from_bytes_zero_copy_mut(bytes)?; for i in 0..capacity_values { - std::ptr::write(hash_set.hash_set.buckets.as_ptr().add(i), None); + write_empty_bucket(hash_set.hash_set.buckets.as_ptr().add(i)); } Ok(hash_set) diff --git a/program-libs/hash-set/tests/defined_bytes.rs b/program-libs/hash-set/tests/defined_bytes.rs new file mode 100644 index 0000000000..b8bc1e6685 --- /dev/null +++ b/program-libs/hash-set/tests/defined_bytes.rs @@ -0,0 +1,119 @@ +use light_hash_set::{zero_copy::HashSetZeroCopy, HashSet, HashSetError}; +use num_bigint::BigUint; + +const BUCKET_OFFSET: usize = 24; +const BUCKET_SIZE: usize = 48; + +fn expected_bucket(tag: usize, sequence_number: usize, value: [u8; 32]) -> [u8; BUCKET_SIZE] { + let mut bytes = [0; BUCKET_SIZE]; + bytes[..8].copy_from_slice(&tag.to_ne_bytes()); + bytes[8..16].copy_from_slice(&sequence_number.to_ne_bytes()); + bytes[16..].copy_from_slice(&value); + bytes +} + +fn value_bytes(value: u8) -> [u8; 32] { + let mut bytes = [0; 32]; + bytes[31] = value; + bytes +} + +fn run_bucket_transitions(prefill: u8) -> Vec<[u8; BUCKET_SIZE]> { + const CAPACITY: usize = 1; + const SEQUENCE_THRESHOLD: usize = 10; + + let mut bytes = vec![prefill; HashSet::size_in_account(CAPACITY)]; + unsafe { + HashSetZeroCopy::from_bytes_zero_copy_init(&mut bytes, CAPACITY, SEQUENCE_THRESHOLD) + .unwrap(); + } + + let mut snapshots = Vec::new(); + { + let mut hash_set = + unsafe { HashSetZeroCopy::from_bytes_zero_copy_mut(&mut bytes).unwrap() }; + assert_eq!(hash_set.insert(&BigUint::from(7_u8), 0).unwrap(), 0); + } + snapshots.push(bytes[BUCKET_OFFSET..].try_into().unwrap()); + + { + let mut hash_set = + unsafe { HashSetZeroCopy::from_bytes_zero_copy_mut(&mut bytes).unwrap() }; + hash_set.mark_with_sequence_number(0, 0).unwrap(); + } + snapshots.push(bytes[BUCKET_OFFSET..].try_into().unwrap()); + + { + let mut hash_set = + unsafe { HashSetZeroCopy::from_bytes_zero_copy_mut(&mut bytes).unwrap() }; + assert_eq!( + hash_set + .insert(&BigUint::from(9_u8), SEQUENCE_THRESHOLD) + .unwrap(), + 0 + ); + } + snapshots.push(bytes[BUCKET_OFFSET..].try_into().unwrap()); + + snapshots +} + +#[test] +fn zero_copy_writes_every_bucket_byte() { + const CAPACITY: usize = 3; + const SEQUENCE_THRESHOLD: usize = 10; + + let initialize = |prefill| { + let mut bytes = vec![prefill; HashSet::size_in_account(CAPACITY)]; + unsafe { + HashSetZeroCopy::from_bytes_zero_copy_init(&mut bytes, CAPACITY, SEQUENCE_THRESHOLD) + .unwrap(); + } + bytes + }; + + let initialized_from_ff = initialize(0xFF); + let initialized_from_aa = initialize(0xAA); + assert_eq!(initialized_from_ff, initialized_from_aa); + assert_eq!( + &initialized_from_ff[16..BUCKET_OFFSET], + &[0; BUCKET_OFFSET - 16] + ); + for bucket in initialized_from_ff[BUCKET_OFFSET..].chunks_exact(BUCKET_SIZE) { + assert_eq!(bucket, expected_bucket(2, 0, [0; 32])); + } + + let snapshots_from_ff = run_bucket_transitions(0xFF); + let snapshots_from_aa = run_bucket_transitions(0xAA); + assert_eq!(snapshots_from_ff, snapshots_from_aa); + assert_eq!( + snapshots_from_ff, + vec![ + expected_bucket(0, 0, value_bytes(7)), + expected_bucket(1, SEQUENCE_THRESHOLD, value_bytes(7)), + expected_bucket(0, 0, value_bytes(9)), + ] + ); +} + +#[test] +fn zero_copy_rejects_buffer_without_reserved_gap() { + const CAPACITY: usize = 2; + let expected_size = HashSet::size_in_account(CAPACITY); + assert_eq!(expected_size, BUCKET_OFFSET + CAPACITY * BUCKET_SIZE); + + let mut bytes = vec![0xAA; expected_size - 8]; + let before = bytes.clone(); + let error = unsafe { HashSetZeroCopy::from_bytes_zero_copy_init(&mut bytes, CAPACITY, 10) } + .unwrap_err(); + assert!( + matches!(error, HashSetError::BufferSize(expected, actual) if expected == expected_size && actual == expected_size - 8) + ); + assert_eq!(bytes, before); + + bytes[..8].copy_from_slice(&CAPACITY.to_le_bytes()); + let error = unsafe { HashSetZeroCopy::from_bytes_zero_copy_mut(&mut bytes) }.unwrap_err(); + assert!( + matches!(error, HashSetError::BufferSize(expected, actual) if expected == expected_size && actual == expected_size - 8) + ); +} diff --git a/program-libs/indexed-merkle-tree/src/changelog.rs b/program-libs/indexed-merkle-tree/src/changelog.rs index d2515d431a..be3292b0b5 100644 --- a/program-libs/indexed-merkle-tree/src/changelog.rs +++ b/program-libs/indexed-merkle-tree/src/changelog.rs @@ -14,3 +14,23 @@ where /// the same operation. pub changelog_index: usize, } + +// Pin the deployed v1 address-tree account layout. These types use Rust's +// native layout, which currently reorders their fields. +const _: () = { + type Element = RawIndexedElement; + type Entry = IndexedChangelogEntry; + + assert!(std::mem::size_of::() == 80); + assert!(std::mem::align_of::() == 8); + assert!(std::mem::offset_of!(Element, value) == 0); + assert!(std::mem::offset_of!(Element, next_value) == 32); + assert!(std::mem::offset_of!(Element, next_index) == 64); + assert!(std::mem::offset_of!(Element, index) == 72); + + assert!(std::mem::size_of::() == 600); + assert!(std::mem::align_of::() == 8); + assert!(std::mem::offset_of!(Entry, proof) == 0); + assert!(std::mem::offset_of!(Entry, element) == 512); + assert!(std::mem::offset_of!(Entry, changelog_index) == 592); +}; diff --git a/program-libs/indexed-merkle-tree/tests/defined_bytes.rs b/program-libs/indexed-merkle-tree/tests/defined_bytes.rs new file mode 100644 index 0000000000..1be3d5da16 --- /dev/null +++ b/program-libs/indexed-merkle-tree/tests/defined_bytes.rs @@ -0,0 +1,107 @@ +use light_bounded_vec::BoundedVec; +use light_hasher::Poseidon; +use light_indexed_merkle_tree::{ + array::IndexedArray, changelog::IndexedChangelogEntry, reference, + zero_copy::IndexedMerkleTreeZeroCopyMut, IndexedMerkleTree, +}; +use num_bigint::BigUint; + +const HEIGHT: usize = 26; +const NET_HEIGHT: usize = 16; +const CANOPY_DEPTH: usize = HEIGHT - NET_HEIGHT; +const CHANGELOG_CAPACITY: usize = 16; +const ROOTS_CAPACITY: usize = 16; +const INDEXED_CHANGELOG_CAPACITY: usize = 6; +const ENTRY_SIZE: usize = 600; + +fn expected_entry(entry: &IndexedChangelogEntry) -> [u8; ENTRY_SIZE] { + let mut bytes = [0; ENTRY_SIZE]; + for (dst, node) in bytes[..512].chunks_exact_mut(32).zip(entry.proof.iter()) { + dst.copy_from_slice(node); + } + bytes[512..544].copy_from_slice(&entry.element.value); + bytes[544..576].copy_from_slice(&entry.element.next_value); + bytes[576..584].copy_from_slice(&entry.element.next_index.to_ne_bytes()); + bytes[584..592].copy_from_slice(&entry.element.index.to_ne_bytes()); + bytes[592..600].copy_from_slice(&entry.changelog_index.to_ne_bytes()); + bytes +} + +fn run_all_push_paths(prefill: u8) -> Vec<[u8; ENTRY_SIZE]> { + let mut bytes = vec![ + prefill; + IndexedMerkleTree::::size_in_account( + HEIGHT, + CHANGELOG_CAPACITY, + ROOTS_CAPACITY, + CANOPY_DEPTH, + INDEXED_CHANGELOG_CAPACITY, + ) + ]; + + let mut tree = IndexedMerkleTreeZeroCopyMut:::: + from_bytes_zero_copy_init( + &mut bytes, + HEIGHT, + CANOPY_DEPTH, + CHANGELOG_CAPACITY, + ROOTS_CAPACITY, + INDEXED_CHANGELOG_CAPACITY, + ) + .unwrap(); + tree.init().unwrap(); + tree.add_highest_element().unwrap(); + + let mut indexed_array = IndexedArray::::default(); + indexed_array.init().unwrap(); + let mut reference_tree = + reference::IndexedMerkleTree::::new(HEIGHT, CANOPY_DEPTH).unwrap(); + reference_tree.init().unwrap(); + + let address = BigUint::from(7_u8); + let (low_element, low_element_next_value) = indexed_array + .find_low_element_for_nonexistent(&address) + .unwrap(); + let net_height_proof = reference_tree + .get_proof_of_leaf(low_element.index(), false) + .unwrap(); + let mut proof = BoundedVec::with_capacity(HEIGHT); + for node in net_height_proof.iter() { + proof.push(*node).unwrap(); + } + let changelog_index = tree.changelog_index(); + let indexed_changelog_index = tree.indexed_changelog_index(); + tree.update( + changelog_index, + indexed_changelog_index, + address, + low_element, + low_element_next_value, + &mut proof, + ) + .unwrap(); + + assert_eq!(tree.indexed_changelog.len(), INDEXED_CHANGELOG_CAPACITY); + tree.indexed_changelog + .as_slice() + .iter() + .map(|entry| { + // SAFETY: The layout assertions pin the entry to 600 bytes and + // prove that its fields cover the entire value without padding. + let actual = unsafe { + std::slice::from_raw_parts( + (entry as *const IndexedChangelogEntry).cast::(), + ENTRY_SIZE, + ) + }; + let actual: [u8; ENTRY_SIZE] = actual.try_into().unwrap(); + assert_eq!(actual, expected_entry(entry)); + actual + }) + .collect() +} + +#[test] +fn indexed_changelog_pushes_write_every_byte() { + assert_eq!(run_all_push_paths(0xFF), run_all_push_paths(0xAA)); +}