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