Skip to main content

commonware_storage/bitmap/
authenticated.rs

1//! An authenticated bitmap.
2//!
3//! The authenticated bitmap is an in-memory data structure that does not persist its contents other
4//! than the data corresponding to its "pruned" section, allowing full restoration by "replaying"
5//! all retained elements.
6//!
7//! Authentication is provided by a Merkle tree that is maintained over the bitmap, with each leaf
8//! covering a chunk of N bytes. This Merkle tree isn't balanced, but instead mimics the structure
9//! of an MMR with an equivalent number of leaves. This structure reduces overhead of updating the
10//! most recently added elements, and (more importantly) simplifies aligning the bitmap with an MMR
11//! over elements whose activity state is reflected by the bitmap.
12
13use crate::{
14    merkle::{
15        hasher::Hasher,
16        mmr::{
17            self,
18            mem::{Config, Mmr},
19            verification, Error, Location, Position, Proof,
20        },
21        storage::Storage,
22        Family as _,
23    },
24    metadata::{Config as MConfig, Metadata},
25    Context,
26};
27use commonware_codec::DecodeExt;
28use commonware_cryptography::Digest;
29use commonware_parallel::Strategy;
30use commonware_utils::{
31    bitmap::{BitMap as UtilsBitMap, Prunable as PrunableBitMap},
32    sequence::prefixed_u64::U64,
33};
34use std::collections::HashSet;
35use tracing::{debug, error, warn};
36
37/// Returns a root digest that incorporates bits not yet part of the MMR because they
38/// belong to the last (unfilled) chunk.
39pub(crate) fn partial_chunk_root<H: Hasher<mmr::Family>, const N: usize>(
40    hasher: &H,
41    mmr_root: &H::Digest,
42    next_bit: u64,
43    last_chunk_digest: &H::Digest,
44) -> H::Digest {
45    assert!(next_bit > 0);
46    assert!(next_bit < UtilsBitMap::<N>::CHUNK_SIZE_BITS);
47    let next_bit = next_bit.to_be_bytes();
48    hasher.hash([
49        mmr_root.as_ref(),
50        next_bit.as_slice(),
51        last_chunk_digest.as_ref(),
52    ])
53}
54
55mod private {
56    pub trait Sealed {}
57}
58
59/// Trait for valid [BitMap] type states.
60pub trait State<D: Digest>: private::Sealed + Sized + Send + Sync {}
61
62/// Merkleized state: the bitmap has been merkleized and the root is cached.
63pub struct Merkleized<D: Digest> {
64    /// The cached root of the bitmap.
65    root: D,
66}
67
68impl<D: Digest> private::Sealed for Merkleized<D> {}
69impl<D: Digest> State<D> for Merkleized<D> {}
70
71/// Unmerkleized state: the bitmap has pending changes not yet merkleized.
72pub struct Unmerkleized {
73    /// Bitmap chunks that have been changed but whose changes are not yet reflected in the
74    /// root digest.
75    ///
76    /// Each dirty chunk is identified by its absolute index, including pruned chunks.
77    ///
78    /// Invariant: Indices are always in the range [pruned_chunks, authenticated_len).
79    dirty_chunks: HashSet<usize>,
80}
81
82impl private::Sealed for Unmerkleized {}
83impl<D: Digest> State<D> for Unmerkleized {}
84
85/// A merkleized bitmap whose root digest has been computed and cached.
86pub type MerkleizedBitMap<E, D, const N: usize, S> = BitMap<E, D, N, Merkleized<D>, S>;
87
88/// An unmerkleized bitmap whose root digest has not been computed.
89pub type UnmerkleizedBitMap<E, D, const N: usize, S> = BitMap<E, D, N, Unmerkleized, S>;
90
91/// A bitmap supporting inclusion proofs through Merkleization.
92///
93/// Merkleization of the bitmap is performed over chunks of N bytes. If the goal is to minimize
94/// proof sizes, choose an N that is equal to the size or double the size of the hasher's digest.
95///
96/// # Type States
97///
98/// The bitmap uses the type-state pattern to enforce at compile-time whether the bitmap has
99/// pending updates that must be merkleized before computing proofs. [MerkleizedBitMap] represents
100/// a bitmap whose root digest has been computed and cached. [UnmerkleizedBitMap] represents a
101/// bitmap with pending updates. An unmerkleized bitmap can be converted into a merkleized bitmap
102/// by calling [UnmerkleizedBitMap::merkleize].
103///
104/// # Warning
105///
106/// Even though we use u64 identifiers for bits, on 32-bit machines, the maximum addressable bit is
107/// limited to (u32::MAX * N * 8).
108pub struct BitMap<E: Context, D: Digest, const N: usize, M: State<D>, S: Strategy> {
109    /// The underlying bitmap.
110    bitmap: PrunableBitMap<N>,
111
112    /// Invariant: Chunks in range [0, authenticated_len) are in `mmr`.
113    /// This is an absolute index that includes pruned chunks.
114    authenticated_len: usize,
115
116    /// A Merkle tree with each leaf representing an N*8 bit "chunk" of the bitmap.
117    ///
118    /// After calling `merkleize` all chunks are guaranteed to be included in the Merkle tree. The
119    /// last chunk of the bitmap is never part of the tree.
120    ///
121    /// Because leaf elements can be updated when bits in the bitmap are flipped, this tree, while
122    /// based on an MMR structure, is not an MMR but a Merkle tree. The MMR structure results in
123    /// reduced update overhead for elements being appended or updated near the tip compared to a
124    /// more typical balanced Merkle tree.
125    mmr: Mmr<D>,
126
127    /// The strategy used for parallelizing merkleization.
128    strategy: S,
129
130    /// Merkleization-dependent state.
131    state: M,
132
133    /// Metadata for persisting pruned state.
134    metadata: Metadata<E, U64, Vec<u8>>,
135}
136
137/// Prefix used for the metadata key identifying node digests.
138const NODE_PREFIX: u8 = 0;
139
140/// Prefix used for the metadata key identifying the pruned_chunks value.
141const PRUNED_CHUNKS_PREFIX: u8 = 1;
142
143impl<E: Context, D: Digest, const N: usize, M: State<D>, S: Strategy> BitMap<E, D, N, M, S> {
144    /// The size of a chunk in bits.
145    pub const CHUNK_SIZE_BITS: u64 = PrunableBitMap::<N>::CHUNK_SIZE_BITS;
146
147    /// Return the size of the bitmap in bits.
148    #[inline]
149    pub fn size(&self) -> Position {
150        self.mmr.size()
151    }
152
153    /// Return the number of bits currently stored in the bitmap, irrespective of any pruning.
154    #[inline]
155    pub const fn len(&self) -> u64 {
156        self.bitmap.len()
157    }
158
159    /// Returns true if the bitmap is empty.
160    #[inline]
161    pub const fn is_empty(&self) -> bool {
162        self.len() == 0
163    }
164
165    /// Return the number of bits that have been pruned from this bitmap.
166    #[inline]
167    pub const fn pruned_bits(&self) -> u64 {
168        self.bitmap.pruned_bits()
169    }
170
171    /// Returns the number of complete chunks (excludes partial chunk at end, if any).
172    /// The returned index is absolute and includes pruned chunks.
173    #[inline]
174    fn complete_chunks(&self) -> usize {
175        let chunks_len = self.bitmap.chunks_len();
176        if self.bitmap.is_chunk_aligned() {
177            chunks_len
178        } else {
179            // Last chunk is partial
180            chunks_len.checked_sub(1).unwrap()
181        }
182    }
183
184    /// Return the last chunk of the bitmap and its size in bits. The size can be 0 (meaning the
185    /// last chunk is empty).
186    #[inline]
187    pub fn last_chunk(&self) -> (&[u8; N], u64) {
188        self.bitmap.last_chunk()
189    }
190
191    /// Returns the bitmap chunk containing the specified bit.
192    ///
193    /// # Warning
194    ///
195    /// Panics if the bit doesn't exist or has been pruned.
196    #[inline]
197    pub fn get_chunk_containing(&self, bit: u64) -> &[u8; N] {
198        self.bitmap.get_chunk_containing(bit)
199    }
200
201    /// Get the value of a bit.
202    ///
203    /// # Warning
204    ///
205    /// Panics if the bit doesn't exist or has been pruned.
206    #[inline]
207    pub fn get_bit(&self, bit: u64) -> bool {
208        self.bitmap.get_bit(bit)
209    }
210
211    /// Get the value of a bit from its chunk.
212    /// `bit` is an index into the entire bitmap, not just the chunk.
213    #[inline]
214    pub const fn get_bit_from_chunk(chunk: &[u8; N], bit: u64) -> bool {
215        PrunableBitMap::<N>::get_bit_from_chunk(chunk, bit)
216    }
217
218    /// Verify whether `proof` proves that the `chunk` containing the given bit belongs to the
219    /// bitmap corresponding to `root`.
220    pub fn verify_bit_inclusion(
221        hasher: &impl Hasher<mmr::Family, Digest = D>,
222        proof: &Proof<D>,
223        chunk: &[u8; N],
224        bit: u64,
225        root: &D,
226    ) -> bool {
227        let bit_len = *proof.leaves;
228        if bit >= bit_len {
229            debug!(bit_len, bit, "tried to verify non-existent bit");
230            return false;
231        }
232
233        // Since we support only full bagging, inactive peaks must always be 0.
234        if proof.inactive_peaks != 0 {
235            debug!(
236                inactive_peaks = proof.inactive_peaks,
237                "bitmap proof must have inactive_peaks == 0"
238            );
239            return false;
240        }
241
242        // The chunk index should always be < MAX_LEAVES.
243        let chunked_leaves = Location::new(PrunableBitMap::<N>::to_chunk_index(bit_len) as u64);
244        let mut mmr_proof = Proof {
245            leaves: chunked_leaves,
246            inactive_peaks: 0,
247            digests: proof.digests.clone(),
248        };
249
250        let loc = Location::new(PrunableBitMap::<N>::to_chunk_index(bit) as u64);
251        if bit_len.is_multiple_of(Self::CHUNK_SIZE_BITS) {
252            return mmr_proof.verify_element_inclusion(hasher, chunk, loc, root);
253        }
254
255        if proof.digests.is_empty() {
256            debug!("proof has no digests");
257            return false;
258        }
259        let last_digest = mmr_proof.digests.pop().unwrap();
260
261        if chunked_leaves == loc {
262            // The proof is over a bit in the partial chunk. In this case the proof's only digest
263            // should be the MMR's root, otherwise it is invalid. Since we've popped off the last
264            // digest already, there should be no remaining digests.
265            if !mmr_proof.digests.is_empty() {
266                debug!(
267                    digests = mmr_proof.digests.len() + 1,
268                    "proof over partial chunk should have exactly 1 digest"
269                );
270                return false;
271            }
272            let last_chunk_digest = hasher.digest(chunk);
273            let next_bit = bit_len % Self::CHUNK_SIZE_BITS;
274            let reconstructed_root =
275                partial_chunk_root::<_, N>(hasher, &last_digest, next_bit, &last_chunk_digest);
276            return reconstructed_root == *root;
277        };
278
279        // For the case where the proof is over a bit in a full chunk, `last_digest` contains the
280        // digest of that chunk.
281        let mmr_root = match mmr_proof.reconstruct_root(hasher, &[chunk], loc) {
282            Ok(root) => root,
283            Err(error) => {
284                debug!(error = ?error, "invalid proof input");
285                return false;
286            }
287        };
288
289        let next_bit = bit_len % Self::CHUNK_SIZE_BITS;
290        let reconstructed_root =
291            partial_chunk_root::<_, N>(hasher, &mmr_root, next_bit, &last_digest);
292
293        reconstructed_root == *root
294    }
295}
296
297impl<E: Context, D: Digest, const N: usize, S: Strategy> MerkleizedBitMap<E, D, N, S> {
298    /// Initialize a bitmap from the metadata in the given partition. If the partition is empty,
299    /// returns an empty bitmap. Otherwise restores the pruned state (the caller must replay
300    /// retained elements to restore its full state).
301    ///
302    /// Returns an error if the bitmap could not be restored, e.g. because of data corruption or
303    /// underlying storage error.
304    pub async fn init(
305        context: E,
306        partition: &str,
307        strategy: S,
308        hasher: &impl Hasher<mmr::Family, Digest = D>,
309    ) -> Result<Self, Error> {
310        let metadata_cfg = MConfig {
311            partition: partition.into(),
312            codec_config: ((0..).into(), ()),
313        };
314        let metadata =
315            Metadata::<_, U64, Vec<u8>>::init(context.child("metadata"), metadata_cfg).await?;
316
317        let key: U64 = U64::new(PRUNED_CHUNKS_PREFIX, 0);
318        let pruned_chunks = match metadata.get(&key) {
319            Some(bytes) => u64::from_be_bytes(bytes.as_slice().try_into().map_err(|_| {
320                error!("pruned chunks value not a valid u64");
321                Error::DataCorrupted("pruned chunks value not a valid u64")
322            })?),
323            None => {
324                warn!("bitmap metadata does not contain pruned chunks, initializing as empty");
325                0
326            }
327        } as usize;
328        if pruned_chunks == 0 {
329            let mmr = Mmr::new();
330            let cached_root = mmr.root(hasher, 0)?;
331            return Ok(Self {
332                bitmap: PrunableBitMap::new(),
333                authenticated_len: 0,
334                mmr,
335                strategy,
336                metadata,
337                state: Merkleized { root: cached_root },
338            });
339        }
340        let pruned_loc = Location::new(pruned_chunks as u64);
341        if !pruned_loc.is_valid() {
342            return Err(Error::DataCorrupted("pruned chunks exceeds MAX_LEAVES"));
343        }
344
345        let mut pinned_nodes = Vec::new();
346        for (index, pos) in mmr::Family::nodes_to_pin(pruned_loc).enumerate() {
347            let Some(bytes) = metadata.get(&U64::new(NODE_PREFIX, index as u64)) else {
348                error!(?pruned_loc, ?pos, "missing pinned node");
349                return Err(Error::MissingNode(pos));
350            };
351            let digest = D::decode(bytes.as_ref());
352            let Ok(digest) = digest else {
353                error!(?pruned_loc, ?pos, "could not convert node bytes to digest");
354                return Err(Error::MissingNode(pos));
355            };
356            pinned_nodes.push(digest);
357        }
358
359        let mmr = Mmr::init(Config {
360            nodes: Vec::new(),
361            pruning_boundary: Location::new(pruned_chunks as u64),
362            pinned_nodes,
363        })?;
364
365        let bitmap = PrunableBitMap::new_with_pruned_chunks(pruned_chunks)
366            .expect("pruned_chunks should never overflow");
367        let cached_root = mmr.root(hasher, 0)?;
368        Ok(Self {
369            bitmap,
370            // Pruned chunks are already authenticated in the MMR
371            authenticated_len: pruned_chunks,
372            mmr,
373            strategy,
374            metadata,
375            state: Merkleized { root: cached_root },
376        })
377    }
378
379    pub fn get_node(&self, position: Position) -> Option<D> {
380        self.mmr.get_node(position)
381    }
382
383    /// Write the information necessary to restore the bitmap in its fully pruned state at its last
384    /// pruning boundary. Restoring the entire bitmap state is then possible by replaying the
385    /// retained elements.
386    pub async fn write_pruned(&mut self) -> Result<(), Error> {
387        self.metadata.clear();
388
389        // Write the number of pruned chunks.
390        let key = U64::new(PRUNED_CHUNKS_PREFIX, 0);
391        self.metadata
392            .put(key, self.bitmap.pruned_chunks().to_be_bytes().to_vec());
393
394        // Write the pinned nodes.
395        let pruned_loc = Location::new(self.bitmap.pruned_chunks() as u64);
396        assert!(
397            pruned_loc.is_valid(),
398            "expected valid location from pruned_chunks"
399        );
400        for (i, digest) in mmr::Family::nodes_to_pin(pruned_loc).enumerate() {
401            let digest = self.mmr.get_node_unchecked(digest);
402            let key = U64::new(NODE_PREFIX, i as u64);
403            self.metadata.put(key, digest.to_vec());
404        }
405
406        self.metadata.sync().await.map_err(Error::Metadata)
407    }
408
409    /// Destroy the bitmap metadata from disk.
410    pub async fn destroy(self) -> Result<(), Error> {
411        self.metadata.destroy().await.map_err(Error::Metadata)
412    }
413
414    /// Prune all complete chunks before the chunk containing the given bit.
415    ///
416    /// The chunk containing `bit` and all subsequent chunks are retained. All chunks
417    /// before it are pruned from the bitmap and the underlying MMR.
418    ///
419    /// If `bit` equals the bitmap length, this prunes all complete chunks while retaining
420    /// the empty trailing chunk, preparing the bitmap for appending new data.
421    pub fn prune_to_bit(&mut self, bit: u64) -> Result<(), Error> {
422        let chunk = PrunableBitMap::<N>::to_chunk_index(bit);
423        if chunk < self.bitmap.pruned_chunks() {
424            return Ok(());
425        }
426
427        // Prune inner bitmap
428        self.bitmap.prune_to_bit(bit);
429
430        // Update authenticated length
431        self.authenticated_len = self.complete_chunks();
432
433        self.mmr.prune(Location::new(chunk as u64))?;
434        Ok(())
435    }
436
437    /// Return the cached root digest against which inclusion proofs can be verified.
438    ///
439    /// # Format
440    ///
441    /// The root digest is simply that of the underlying MMR whenever the bit count falls on a chunk
442    /// boundary. Otherwise, the root is computed as follows in order to capture the bits that are
443    /// not yet part of the MMR:
444    ///
445    /// hash(mmr_root || next_bit as u64 be_bytes || last_chunk_digest)
446    ///
447    /// The root is computed during merkleization and cached, so this method is cheap to call.
448    pub const fn root(&self) -> D {
449        self.state.root
450    }
451
452    /// Return an inclusion proof for the specified bit, along with the chunk of the bitmap
453    /// containing that bit. The proof can be used to prove any bit in the chunk.
454    ///
455    /// The bitmap proof stores the number of bits in the bitmap within the proof's `leaves` field
456    /// instead of the underlying MMR leaf count, since the MMR does not reflect the number of bits
457    /// in any partial chunk. The underlying MMR size can be derived from the number of bits as
458    /// `leaf_num_to_pos(proof.leaves / BitMap<_, N>::CHUNK_SIZE_BITS)`.
459    ///
460    /// # Errors
461    ///
462    /// Returns [Error::BitOutOfBounds] if `bit` is out of bounds.
463    pub async fn proof(
464        &self,
465        hasher: &impl Hasher<mmr::Family, Digest = D>,
466        bit: u64,
467    ) -> Result<(Proof<D>, [u8; N]), Error> {
468        if bit >= self.len() {
469            return Err(Error::BitOutOfBounds(bit, self.len()));
470        }
471
472        let chunk = *self.get_chunk_containing(bit);
473        let chunk_loc = Location::from(PrunableBitMap::<N>::to_chunk_index(bit));
474        let (last_chunk, next_bit) = self.bitmap.last_chunk();
475
476        if chunk_loc == self.mmr.leaves() {
477            assert!(next_bit > 0);
478            // Proof is over a bit in the partial chunk. In this case only a single digest is
479            // required in the proof: the mmr's root.
480            return Ok((
481                Proof {
482                    leaves: Location::new(self.len()),
483                    inactive_peaks: 0,
484                    digests: vec![self.mmr.root(hasher, 0)?],
485                },
486                chunk,
487            ));
488        }
489
490        let range = chunk_loc..chunk_loc + 1;
491        let mut proof = verification::range_proof(hasher, &self.mmr, range, 0).await?;
492        proof.leaves = Location::new(self.len());
493        if next_bit == Self::CHUNK_SIZE_BITS {
494            // Bitmap is chunk aligned.
495            return Ok((proof, chunk));
496        }
497
498        // Since the bitmap wasn't chunk aligned, we'll need to include the digest of the last chunk
499        // in the proof to be able to re-derive the root.
500        let last_chunk_digest = hasher.digest(last_chunk);
501        proof.digests.push(last_chunk_digest);
502
503        Ok((proof, chunk))
504    }
505
506    /// Convert this merkleized bitmap into an unmerkleized bitmap without making any changes to it.
507    pub fn into_dirty(self) -> UnmerkleizedBitMap<E, D, N, S> {
508        UnmerkleizedBitMap {
509            bitmap: self.bitmap,
510            authenticated_len: self.authenticated_len,
511            mmr: self.mmr,
512            strategy: self.strategy,
513            state: Unmerkleized {
514                dirty_chunks: HashSet::new(),
515            },
516            metadata: self.metadata,
517        }
518    }
519}
520
521impl<E: Context, D: Digest, const N: usize, S: Strategy> UnmerkleizedBitMap<E, D, N, S> {
522    /// Add a single bit to the end of the bitmap.
523    ///
524    /// # Warning
525    ///
526    /// The update will not affect the root until `merkleize` is called.
527    pub fn push(&mut self, bit: bool) {
528        self.bitmap.push(bit);
529    }
530
531    /// Set the value of the given bit.
532    ///
533    /// # Warning
534    ///
535    /// The update will not impact the root until `merkleize` is called.
536    pub fn set_bit(&mut self, bit: u64, value: bool) {
537        // Apply the change to the inner bitmap
538        self.bitmap.set_bit(bit, value);
539
540        // If the updated chunk is already in the MMR, mark it as dirty.
541        let chunk = PrunableBitMap::<N>::to_chunk_index(bit);
542        if chunk < self.authenticated_len {
543            self.state.dirty_chunks.insert(chunk);
544        }
545    }
546
547    /// The chunks that have been modified or added since the last call to `merkleize`.
548    pub fn dirty_chunks(&self) -> Vec<Location> {
549        let mut chunks: Vec<Location> = self
550            .state
551            .dirty_chunks
552            .iter()
553            .map(|&chunk| Location::new(chunk as u64))
554            .collect();
555
556        // Include complete chunks that haven't been authenticated yet
557        for i in self.authenticated_len..self.complete_chunks() {
558            chunks.push(Location::new(i as u64));
559        }
560
561        chunks
562    }
563
564    /// Merkleize all updates not yet reflected in the bitmap's root.
565    pub fn merkleize(
566        mut self,
567        hasher: &impl Hasher<mmr::Family, Digest = D>,
568    ) -> Result<MerkleizedBitMap<E, D, N, S>, Error> {
569        // Build a batch backed by the configured strategy.
570        let mut batch = self.mmr.new_batch_with_strategy(self.strategy.clone());
571        let start = self.authenticated_len;
572        let end = self.complete_chunks();
573        for i in start..end {
574            batch = batch.add(hasher, self.bitmap.get_chunk(i));
575        }
576        self.authenticated_len = end;
577
578        // Pre-hash dirty chunks into digests and update in the batch.
579        let updates: Vec<(Location, &[u8; N])> = self
580            .state
581            .dirty_chunks
582            .iter()
583            .map(|&chunk| {
584                let loc = Location::new(chunk as u64);
585                (loc, self.bitmap.get_chunk(chunk))
586            })
587            .collect();
588        let dirty: Vec<(Location, D)> = self.strategy.map_init_collect_vec(
589            &updates,
590            || hasher.clone(),
591            |h, &(loc, chunk)| {
592                let pos = Position::try_from(loc).unwrap();
593                (loc, h.leaf_digest(pos, chunk.as_ref()))
594            },
595        );
596        batch = batch.update_leaf_batched(&dirty)?;
597
598        // Merkleize and apply.
599        let batch = batch.merkleize(&self.mmr, hasher);
600        self.mmr.apply_batch(&batch)?;
601
602        // Compute the bitmap root.
603        let mmr_root = self.mmr.root(hasher, 0)?;
604        let cached_root = if self.bitmap.is_chunk_aligned() {
605            mmr_root
606        } else {
607            let (last_chunk, next_bit) = self.bitmap.last_chunk();
608            let last_chunk_digest = hasher.digest(last_chunk);
609            partial_chunk_root::<_, N>(hasher, &mmr_root, next_bit, &last_chunk_digest)
610        };
611
612        Ok(MerkleizedBitMap {
613            bitmap: self.bitmap,
614            authenticated_len: self.authenticated_len,
615            mmr: self.mmr,
616            strategy: self.strategy,
617            metadata: self.metadata,
618            state: Merkleized { root: cached_root },
619        })
620    }
621}
622
623impl<E: Context, D: Digest, const N: usize, S: Strategy> Storage<mmr::Family>
624    for MerkleizedBitMap<E, D, N, S>
625{
626    type Digest = D;
627
628    async fn size(&self) -> Position {
629        self.size()
630    }
631
632    async fn get_node(&self, position: Position) -> Result<Option<D>, Error> {
633        Ok(self.get_node(position))
634    }
635}
636
637#[cfg(test)]
638mod tests {
639    use super::*;
640    use crate::merkle::Bagging::ForwardFold;
641    use commonware_codec::FixedSize;
642    use commonware_cryptography::{sha256, Hasher, Sha256};
643    use commonware_macros::test_traced;
644    use commonware_parallel::Sequential;
645    use commonware_runtime::{deterministic, Runner as _, Supervisor as _};
646    use mmr::StandardHasher;
647
648    const SHA256_SIZE: usize = sha256::Digest::SIZE;
649
650    type TestContext = deterministic::Context;
651    type TestMerkleizedBitMap<const N: usize> =
652        MerkleizedBitMap<TestContext, sha256::Digest, N, Sequential>;
653
654    impl<E: Context, D: Digest, const N: usize> UnmerkleizedBitMap<E, D, N, Sequential> {
655        // Add a byte's worth of bits to the bitmap.
656        //
657        // # Warning
658        //
659        // - The update will not impact the root until `merkleize` is called.
660        //
661        // - Assumes self.next_bit is currently byte aligned, and panics otherwise.
662        fn push_byte(&mut self, byte: u8) {
663            self.bitmap.push_byte(byte);
664        }
665
666        /// Add a chunk of bits to the bitmap.
667        ///
668        /// # Warning
669        ///
670        /// - The update will not impact the root until `merkleize` is called.
671        ///
672        /// - Panics if self.next_bit is not chunk aligned.
673        fn push_chunk(&mut self, chunk: &[u8; N]) {
674            self.bitmap.push_chunk(chunk);
675        }
676    }
677
678    fn test_chunk<const N: usize>(s: &[u8]) -> [u8; N] {
679        assert_eq!(N % 32, 0);
680        let mut vec: Vec<u8> = Vec::new();
681        for _ in 0..N / 32 {
682            vec.extend(Sha256::hash(s).iter());
683        }
684
685        vec.try_into().unwrap()
686    }
687
688    #[test_traced]
689    fn test_bitmap_verify_empty_proof() {
690        let executor = deterministic::Runner::default();
691        executor.start(|_context| async move {
692            let hasher = StandardHasher::<Sha256>::new(ForwardFold);
693            let proof = Proof {
694                leaves: Location::new(100),
695                inactive_peaks: 0,
696                digests: Vec::new(),
697            };
698            assert!(
699                !TestMerkleizedBitMap::<SHA256_SIZE>::verify_bit_inclusion(
700                    &hasher,
701                    &proof,
702                    &[0u8; SHA256_SIZE],
703                    0,
704                    &Sha256::fill(0x00),
705                ),
706                "proof without digests shouldn't verify or panic"
707            );
708        });
709    }
710
711    /// Regression: bitmap proofs always have `inactive_peaks == 0`. Mutating only that field
712    /// must invalidate verification so the encoded proof is canonical.
713    #[test_traced]
714    fn test_bitmap_verify_rejects_nonzero_inactive_peaks() {
715        let executor = deterministic::Runner::default();
716        executor.start(|context| async move {
717            let hasher = StandardHasher::<Sha256>::new(ForwardFold);
718            let mut bitmap: TestMerkleizedBitMap<SHA256_SIZE> = TestMerkleizedBitMap::init(
719                context.child("bitmap"),
720                "inactive_peaks_canonical",
721                Sequential,
722                &hasher,
723            )
724            .await
725            .unwrap();
726
727            // Build a multi-chunk bitmap so the proof carries non-trivial digests.
728            let mut dirty = bitmap.into_dirty();
729            for i in 0..(TestMerkleizedBitMap::<SHA256_SIZE>::CHUNK_SIZE_BITS * 4) {
730                dirty.push(i % 3 == 0);
731            }
732            bitmap = dirty.merkleize(&hasher).unwrap();
733            let root = bitmap.root();
734
735            let bit = TestMerkleizedBitMap::<SHA256_SIZE>::CHUNK_SIZE_BITS + 5;
736            let (proof, chunk) = bitmap.proof(&hasher, bit).await.unwrap();
737            assert_eq!(proof.inactive_peaks, 0);
738
739            // Canonical proof verifies.
740            assert!(
741                TestMerkleizedBitMap::<SHA256_SIZE>::verify_bit_inclusion(
742                    &hasher, &proof, &chunk, bit, &root
743                ),
744                "canonical bitmap proof should verify"
745            );
746
747            // Mutating only inactive_peaks must invalidate the proof.
748            let mut tampered = proof;
749            tampered.inactive_peaks = 1;
750            assert!(
751                !TestMerkleizedBitMap::<SHA256_SIZE>::verify_bit_inclusion(
752                    &hasher, &tampered, &chunk, bit, &root
753                ),
754                "bitmap proof with nonzero inactive_peaks must not verify"
755            );
756        });
757    }
758
759    #[test_traced]
760    fn test_bitmap_empty_then_one() {
761        let executor = deterministic::Runner::default();
762        executor.start(|context| async move {
763            let hasher = StandardHasher::<Sha256>::new(ForwardFold);
764            let mut bitmap: TestMerkleizedBitMap<SHA256_SIZE> =
765                TestMerkleizedBitMap::init(context.child("bitmap"), "test", Sequential, &hasher)
766                    .await
767                    .unwrap();
768            assert_eq!(bitmap.len(), 0);
769            assert_eq!(bitmap.bitmap.pruned_chunks(), 0);
770            bitmap.prune_to_bit(0).unwrap();
771            assert_eq!(bitmap.bitmap.pruned_chunks(), 0);
772
773            // Add a single bit
774            let root = bitmap.root();
775            let mut dirty = bitmap.into_dirty();
776            dirty.push(true);
777            bitmap = dirty.merkleize(&hasher).unwrap();
778            // Root should change
779            let new_root = bitmap.root();
780            assert_ne!(root, new_root);
781            let root = new_root;
782            bitmap.prune_to_bit(1).unwrap();
783            assert_eq!(bitmap.len(), 1);
784            assert_ne!(bitmap.last_chunk().0, &[0u8; SHA256_SIZE]);
785            assert_eq!(bitmap.last_chunk().1, 1);
786            // Pruning should be a no-op since we're not beyond a chunk boundary.
787            assert_eq!(bitmap.bitmap.pruned_chunks(), 0);
788            assert_eq!(root, bitmap.root());
789
790            // Fill up a full chunk
791            let mut dirty = bitmap.into_dirty();
792            for i in 0..(TestMerkleizedBitMap::<SHA256_SIZE>::CHUNK_SIZE_BITS - 1) {
793                dirty.push(i % 2 != 0);
794            }
795            bitmap = dirty.merkleize(&hasher).unwrap();
796            assert_eq!(bitmap.len(), 256);
797            assert_ne!(root, bitmap.root());
798            let root = bitmap.root();
799
800            // Chunk should be provable.
801            let (proof, chunk) = bitmap.proof(&hasher, 0).await.unwrap();
802            assert!(
803                TestMerkleizedBitMap::<SHA256_SIZE>::verify_bit_inclusion(
804                    &hasher, &proof, &chunk, 255, &root
805                ),
806                "failed to prove bit in only chunk"
807            );
808            // bit outside range should not verify
809            assert!(
810                !TestMerkleizedBitMap::<SHA256_SIZE>::verify_bit_inclusion(
811                    &hasher, &proof, &chunk, 256, &root
812                ),
813                "should not be able to prove bit outside of chunk"
814            );
815
816            // Now pruning all bits should matter.
817            bitmap.prune_to_bit(256).unwrap();
818            assert_eq!(bitmap.len(), 256);
819            assert_eq!(bitmap.bitmap.pruned_chunks(), 1);
820            assert_eq!(bitmap.bitmap.pruned_bits(), 256);
821            assert_eq!(root, bitmap.root());
822
823            // Pruning to an earlier point should be a no-op.
824            bitmap.prune_to_bit(10).unwrap();
825            assert_eq!(root, bitmap.root());
826        });
827    }
828
829    #[test_traced]
830    fn test_bitmap_building() {
831        // Build the same bitmap with 2 chunks worth of bits in multiple ways and make sure they are
832        // equivalent based on their roots.
833        let executor = deterministic::Runner::default();
834        executor.start(|context| async move {
835            let test_chunk = test_chunk(b"test");
836            let hasher: StandardHasher<Sha256> = StandardHasher::new(ForwardFold);
837
838            // Add each bit one at a time after the first chunk.
839            let bitmap: TestMerkleizedBitMap<SHA256_SIZE> = TestMerkleizedBitMap::init(
840                context.child("bitmap").with_attribute("index", 1),
841                "test1",
842                Sequential,
843                &hasher,
844            )
845            .await
846            .unwrap();
847            let mut dirty = bitmap.into_dirty();
848            dirty.push_chunk(&test_chunk);
849            for b in test_chunk {
850                for j in 0..8 {
851                    let mask = 1 << j;
852                    let bit = (b & mask) != 0;
853                    dirty.push(bit);
854                }
855            }
856            assert_eq!(dirty.len(), 256 * 2);
857
858            let bitmap = dirty.merkleize(&hasher).unwrap();
859            let root = bitmap.root();
860            let inner_root = bitmap.mmr.root(&hasher, 0).unwrap();
861            assert_eq!(root, inner_root);
862
863            {
864                // Repeat the above MMR build only using push_chunk instead, and make
865                // sure root digests match.
866                let bitmap: TestMerkleizedBitMap<SHA256_SIZE> = TestMerkleizedBitMap::init(
867                    context.child("bitmap").with_attribute("index", 2),
868                    "test2",
869                    Sequential,
870                    &hasher,
871                )
872                .await
873                .unwrap();
874                let mut dirty = bitmap.into_dirty();
875                dirty.push_chunk(&test_chunk);
876                dirty.push_chunk(&test_chunk);
877                let bitmap = dirty.merkleize(&hasher).unwrap();
878                let same_root = bitmap.root();
879                assert_eq!(root, same_root);
880            }
881            {
882                // Repeat build again using push_byte this time.
883                let bitmap: TestMerkleizedBitMap<SHA256_SIZE> = TestMerkleizedBitMap::init(
884                    context.child("bitmap").with_attribute("index", 3),
885                    "test3",
886                    Sequential,
887                    &hasher,
888                )
889                .await
890                .unwrap();
891                let mut dirty = bitmap.into_dirty();
892                dirty.push_chunk(&test_chunk);
893                for b in test_chunk {
894                    dirty.push_byte(b);
895                }
896                let bitmap = dirty.merkleize(&hasher).unwrap();
897                let same_root = bitmap.root();
898                assert_eq!(root, same_root);
899            }
900        });
901    }
902
903    #[test_traced]
904    #[should_panic(expected = "cannot add chunk")]
905    fn test_bitmap_build_chunked_panic() {
906        let executor = deterministic::Runner::default();
907        executor.start(|context| async move {
908            let hasher: StandardHasher<Sha256> = StandardHasher::new(ForwardFold);
909            let bitmap: TestMerkleizedBitMap<SHA256_SIZE> =
910                TestMerkleizedBitMap::init(context.child("bitmap"), "test", Sequential, &hasher)
911                    .await
912                    .unwrap();
913            let mut dirty = bitmap.into_dirty();
914            dirty.push_chunk(&test_chunk(b"test"));
915            dirty.push(true);
916            dirty.push_chunk(&test_chunk(b"panic"));
917        });
918    }
919
920    #[test_traced]
921    #[should_panic(expected = "cannot add byte")]
922    fn test_bitmap_build_byte_panic() {
923        let executor = deterministic::Runner::default();
924        executor.start(|context| async move {
925            let hasher: StandardHasher<Sha256> = StandardHasher::new(ForwardFold);
926            let bitmap: TestMerkleizedBitMap<SHA256_SIZE> =
927                TestMerkleizedBitMap::init(context.child("bitmap"), "test", Sequential, &hasher)
928                    .await
929                    .unwrap();
930            let mut dirty = bitmap.into_dirty();
931            dirty.push_chunk(&test_chunk(b"test"));
932            dirty.push(true);
933            dirty.push_byte(0x01);
934        });
935    }
936
937    #[test_traced]
938    #[should_panic(expected = "out of bounds")]
939    fn test_bitmap_get_out_of_bounds_bit_panic() {
940        let executor = deterministic::Runner::default();
941        executor.start(|context| async move {
942            let hasher: StandardHasher<Sha256> = StandardHasher::new(ForwardFold);
943            let bitmap: TestMerkleizedBitMap<SHA256_SIZE> =
944                TestMerkleizedBitMap::init(context.child("bitmap"), "test", Sequential, &hasher)
945                    .await
946                    .unwrap();
947            let mut dirty = bitmap.into_dirty();
948            dirty.push_chunk(&test_chunk(b"test"));
949            dirty.get_bit(256);
950        });
951    }
952
953    #[test_traced]
954    #[should_panic(expected = "pruned")]
955    fn test_bitmap_get_pruned_bit_panic() {
956        let executor = deterministic::Runner::default();
957        executor.start(|context| async move {
958            let hasher: StandardHasher<Sha256> = StandardHasher::new(ForwardFold);
959            let bitmap: TestMerkleizedBitMap<SHA256_SIZE> =
960                TestMerkleizedBitMap::init(context.child("bitmap"), "test", Sequential, &hasher)
961                    .await
962                    .unwrap();
963            let mut dirty = bitmap.into_dirty();
964            dirty.push_chunk(&test_chunk(b"test"));
965            dirty.push_chunk(&test_chunk(b"test2"));
966            let mut bitmap = dirty.merkleize(&hasher).unwrap();
967
968            bitmap.prune_to_bit(256).unwrap();
969            bitmap.get_bit(255);
970        });
971    }
972
973    #[test_traced]
974    fn test_bitmap_root_boundaries() {
975        let executor = deterministic::Runner::default();
976        executor.start(|context| async move {
977            // Build a starting test MMR with two chunks worth of bits.
978            let hasher = StandardHasher::<Sha256>::new(ForwardFold);
979            let bitmap: TestMerkleizedBitMap<SHA256_SIZE> =
980                TestMerkleizedBitMap::init(context.child("bitmap"), "test", Sequential, &hasher)
981                    .await
982                    .unwrap();
983            let mut dirty = bitmap.into_dirty();
984            dirty.push_chunk(&test_chunk(b"test"));
985            dirty.push_chunk(&test_chunk(b"test2"));
986            let mut bitmap = dirty.merkleize(&hasher).unwrap();
987
988            let root = bitmap.root();
989
990            // Confirm that root changes if we add a 1 bit, even though we won't fill a chunk.
991            let mut dirty = bitmap.into_dirty();
992            dirty.push(true);
993            bitmap = dirty.merkleize(&hasher).unwrap();
994            let new_root = bitmap.root();
995            assert_ne!(root, new_root);
996            assert_eq!(bitmap.mmr.size(), 3); // shouldn't include the trailing bits
997
998            // Add 0 bits to fill up entire chunk.
999            for _ in 0..(SHA256_SIZE * 8 - 1) {
1000                let mut dirty = bitmap.into_dirty();
1001                dirty.push(false);
1002                bitmap = dirty.merkleize(&hasher).unwrap();
1003                let newer_root = bitmap.root();
1004                // root will change when adding 0s within the same chunk
1005                assert_ne!(new_root, newer_root);
1006            }
1007            assert_eq!(bitmap.mmr.size(), 4); // chunk we filled should have been added to mmr
1008
1009            // Confirm the root changes when we add the next 0 bit since it's part of a new chunk.
1010            let mut dirty = bitmap.into_dirty();
1011            dirty.push(false);
1012            assert_eq!(dirty.len(), 256 * 3 + 1);
1013            bitmap = dirty.merkleize(&hasher).unwrap();
1014            let newer_root = bitmap.root();
1015            assert_ne!(new_root, newer_root);
1016
1017            // Confirm pruning everything doesn't affect the root.
1018            bitmap.prune_to_bit(bitmap.len()).unwrap();
1019            assert_eq!(bitmap.bitmap.pruned_chunks(), 3);
1020            assert_eq!(bitmap.len(), 256 * 3 + 1);
1021            assert_eq!(newer_root, bitmap.root());
1022        });
1023    }
1024
1025    #[test_traced]
1026    fn test_bitmap_get_set_bits() {
1027        let executor = deterministic::Runner::default();
1028        executor.start(|context| async move {
1029            // Build a test MMR with a few chunks worth of bits.
1030            let hasher = StandardHasher::<Sha256>::new(ForwardFold);
1031            let bitmap: TestMerkleizedBitMap<SHA256_SIZE> =
1032                TestMerkleizedBitMap::init(context.child("bitmap"), "test", Sequential, &hasher)
1033                    .await
1034                    .unwrap();
1035            let mut dirty = bitmap.into_dirty();
1036            dirty.push_chunk(&test_chunk(b"test"));
1037            dirty.push_chunk(&test_chunk(b"test2"));
1038            dirty.push_chunk(&test_chunk(b"test3"));
1039            dirty.push_chunk(&test_chunk(b"test4"));
1040            // Add a few extra bits to exercise not being on a chunk or byte boundary.
1041            dirty.push_byte(0xF1);
1042            dirty.push(true);
1043            dirty.push(false);
1044            dirty.push(true);
1045
1046            let mut bitmap = dirty.merkleize(&hasher).unwrap();
1047            let root = bitmap.root();
1048
1049            // Flip each bit and confirm the root changes, then flip it back to confirm it is safely
1050            // restored.
1051            for bit_pos in (0..bitmap.len()).rev() {
1052                let bit = bitmap.get_bit(bit_pos);
1053                let mut dirty = bitmap.into_dirty();
1054                dirty.set_bit(bit_pos, !bit);
1055                bitmap = dirty.merkleize(&hasher).unwrap();
1056                let new_root = bitmap.root();
1057                assert_ne!(root, new_root, "failed at bit {bit_pos}");
1058                // flip it back
1059                let mut dirty = bitmap.into_dirty();
1060                dirty.set_bit(bit_pos, bit);
1061                bitmap = dirty.merkleize(&hasher).unwrap();
1062                let new_root = bitmap.root();
1063                assert_eq!(root, new_root);
1064            }
1065
1066            // Repeat the test after pruning.
1067            let start_bit = (SHA256_SIZE * 8 * 2) as u64;
1068            bitmap.prune_to_bit(start_bit).unwrap();
1069            for bit_pos in (start_bit..bitmap.len()).rev() {
1070                let bit = bitmap.get_bit(bit_pos);
1071                let mut dirty = bitmap.into_dirty();
1072                dirty.set_bit(bit_pos, !bit);
1073                bitmap = dirty.merkleize(&hasher).unwrap();
1074                let new_root = bitmap.root();
1075                assert_ne!(root, new_root, "failed at bit {bit_pos}");
1076                // flip it back
1077                let mut dirty = bitmap.into_dirty();
1078                dirty.set_bit(bit_pos, bit);
1079                bitmap = dirty.merkleize(&hasher).unwrap();
1080                let new_root = bitmap.root();
1081                assert_eq!(root, new_root);
1082            }
1083        });
1084    }
1085
1086    fn flip_bit<const N: usize>(bit: u64, chunk: &[u8; N]) -> [u8; N] {
1087        let byte = PrunableBitMap::<N>::chunk_byte_offset(bit);
1088        let mask = PrunableBitMap::<N>::chunk_byte_bitmask(bit);
1089        let mut tmp = chunk.to_vec();
1090        tmp[byte] ^= mask;
1091        tmp.try_into().unwrap()
1092    }
1093
1094    #[test_traced]
1095    fn test_bitmap_mmr_proof_verification() {
1096        test_bitmap_mmr_proof_verification_n::<32>();
1097        test_bitmap_mmr_proof_verification_n::<64>();
1098    }
1099
1100    fn test_bitmap_mmr_proof_verification_n<const N: usize>() {
1101        let executor = deterministic::Runner::default();
1102        executor.start(|context| async move {
1103            // Build a bitmap with 10 chunks worth of bits.
1104            let hasher = StandardHasher::<Sha256>::new(ForwardFold);
1105            let bitmap: MerkleizedBitMap<TestContext, sha256::Digest, N, Sequential> =
1106                MerkleizedBitMap::init(context.child("bitmap"), "test", Sequential, &hasher)
1107                    .await
1108                    .unwrap();
1109            let mut dirty = bitmap.into_dirty();
1110            for i in 0u32..10 {
1111                dirty.push_chunk(&test_chunk(format!("test{i}").as_bytes()));
1112            }
1113            // Add a few extra bits to exercise not being on a chunk or byte boundary.
1114            dirty.push_byte(0xA6);
1115            dirty.push(true);
1116            dirty.push(false);
1117            dirty.push(true);
1118            dirty.push(true);
1119            dirty.push(false);
1120
1121            let mut bitmap = dirty.merkleize(&hasher).unwrap();
1122            let root = bitmap.root();
1123
1124            // Make sure every bit is provable, even after pruning in intervals of 251 bits (251 is
1125            // the largest prime that is less than the size of one 32-byte chunk in bits).
1126            for prune_to_bit in (0..bitmap.len()).step_by(251) {
1127                assert_eq!(bitmap.root(), root);
1128                bitmap.prune_to_bit(prune_to_bit).unwrap();
1129                for i in prune_to_bit..bitmap.len() {
1130                    let (proof, chunk) = bitmap.proof(&hasher, i).await.unwrap();
1131
1132                    // Proof should verify for the original chunk containing the bit.
1133                    assert!(
1134                        MerkleizedBitMap::<TestContext, _, N, Sequential>::verify_bit_inclusion(
1135                            &hasher, &proof, &chunk, i, &root
1136                        ),
1137                        "failed to prove bit {i}",
1138                    );
1139
1140                    // Flip the bit in the chunk and make sure the proof fails.
1141                    let corrupted = flip_bit(i, &chunk);
1142                    assert!(
1143                        !MerkleizedBitMap::<TestContext, _, N, Sequential>::verify_bit_inclusion(
1144                            &hasher, &proof, &corrupted, i, &root
1145                        ),
1146                        "proving bit {i} after flipping should have failed",
1147                    );
1148                }
1149            }
1150        })
1151    }
1152
1153    #[test_traced]
1154    fn test_bitmap_persistence() {
1155        const PARTITION: &str = "bitmap-test";
1156        const FULL_CHUNK_COUNT: usize = 100;
1157
1158        let executor = deterministic::Runner::default();
1159        executor.start(|context| async move {
1160            let hasher = StandardHasher::<Sha256>::new(ForwardFold);
1161            // Initializing from an empty partition should result in an empty bitmap.
1162            let bitmap: TestMerkleizedBitMap<SHA256_SIZE> = TestMerkleizedBitMap::init(
1163                context.child("initial"),
1164                PARTITION,
1165                Sequential,
1166                &hasher,
1167            )
1168            .await
1169            .unwrap();
1170            assert_eq!(bitmap.len(), 0);
1171
1172            // Add a non-trivial amount of data.
1173            let mut dirty = bitmap.into_dirty();
1174            for i in 0..FULL_CHUNK_COUNT {
1175                dirty.push_chunk(&test_chunk(format!("test{i}").as_bytes()));
1176            }
1177            let mut bitmap = dirty.merkleize(&hasher).unwrap();
1178            let chunk_aligned_root = bitmap.root();
1179
1180            // Add a few extra bits beyond the last chunk boundary.
1181            let mut dirty = bitmap.into_dirty();
1182            dirty.push_byte(0xA6);
1183            dirty.push(true);
1184            dirty.push(false);
1185            dirty.push(true);
1186            bitmap = dirty.merkleize(&hasher).unwrap();
1187            let root = bitmap.root();
1188
1189            // prune 10 chunks at a time and make sure replay will restore the bitmap every time.
1190            for i in (10..=FULL_CHUNK_COUNT).step_by(10) {
1191                bitmap
1192                    .prune_to_bit(
1193                        (i * TestMerkleizedBitMap::<SHA256_SIZE>::CHUNK_SIZE_BITS as usize) as u64,
1194                    )
1195                    .unwrap();
1196                bitmap.write_pruned().await.unwrap();
1197                bitmap = TestMerkleizedBitMap::init(
1198                    context.child("restore").with_attribute("index", i),
1199                    PARTITION,
1200                    Sequential,
1201                    &hasher,
1202                )
1203                .await
1204                .unwrap();
1205                let _ = bitmap.root();
1206
1207                // Replay missing chunks.
1208                let mut dirty = bitmap.into_dirty();
1209                for j in i..FULL_CHUNK_COUNT {
1210                    dirty.push_chunk(&test_chunk(format!("test{j}").as_bytes()));
1211                }
1212                assert_eq!(dirty.bitmap.pruned_chunks(), i);
1213                assert_eq!(dirty.len(), FULL_CHUNK_COUNT as u64 * 256);
1214                bitmap = dirty.merkleize(&hasher).unwrap();
1215                assert_eq!(bitmap.root(), chunk_aligned_root);
1216
1217                // Replay missing partial chunk.
1218                let mut dirty = bitmap.into_dirty();
1219                dirty.push_byte(0xA6);
1220                dirty.push(true);
1221                dirty.push(false);
1222                dirty.push(true);
1223                bitmap = dirty.merkleize(&hasher).unwrap();
1224                assert_eq!(bitmap.root(), root);
1225            }
1226        });
1227    }
1228
1229    #[test_traced]
1230    fn test_bitmap_proof_out_of_bounds() {
1231        let executor = deterministic::Runner::default();
1232        executor.start(|context| async move {
1233            let hasher = StandardHasher::<Sha256>::new(ForwardFold);
1234            let bitmap: TestMerkleizedBitMap<SHA256_SIZE> =
1235                TestMerkleizedBitMap::init(context.child("bitmap"), "test", Sequential, &hasher)
1236                    .await
1237                    .unwrap();
1238            let mut dirty = bitmap.into_dirty();
1239            dirty.push_chunk(&test_chunk(b"test"));
1240            let bitmap = dirty.merkleize(&hasher).unwrap();
1241
1242            // Proof for bit_offset >= bit_count should fail
1243            let result = bitmap.proof(&hasher, 256).await;
1244            assert!(matches!(result, Err(Error::BitOutOfBounds(offset, size))
1245                    if offset == 256 && size == 256));
1246
1247            let result = bitmap.proof(&hasher, 1000).await;
1248            assert!(matches!(result, Err(Error::BitOutOfBounds(offset, size))
1249                    if offset == 1000 && size == 256));
1250
1251            // Valid proof should work
1252            assert!(bitmap.proof(&hasher, 0).await.is_ok());
1253            assert!(bitmap.proof(&hasher, 255).await.is_ok());
1254        });
1255    }
1256}