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