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)); +}