//! Operation proofs with fixed-size or runtime-sized bitmap chunks. use super::{RangeProof, chunk_bits}; use crate::{ merkle::{ElementPlan, Graftable, Location, Position, storage::Storage}, qmdb::Error, }; use bytes::{BufMut, Bytes}; use commonware_codec::{Buf, Encode, EncodeSize, Read, ReadExt as _, Write, util::at_least}; use commonware_cryptography::{Digest, Hasher}; use commonware_utils::bitmap::{Prunable as BitMap, Readable as BitmapReadable}; use tracing::debug; /// A proof that a specific operation is currently active in the database. /// /// `C` stores the bitmap chunk. Arrays provide fixed-size storage, while [Bytes] supports a /// chunk size supplied when decoding. Both representations encode the chunk without a length /// prefix. #[derive(Clone, Eq, PartialEq, Debug)] pub struct Proof { /// The location of the operation in the database. pub loc: Location, /// The status bitmap chunk containing the operation's activity bit. pub chunk: C, /// The range proof authenticating the operation and its activity status. pub range_proof: RangeProof, } impl Proof { /// Return an inclusion proof that incorporates activity status for the operation at `loc`, /// fetching the range proof over `loc..loc + 1` with [RangeProof::new]. /// /// # Errors /// /// Returns [Error::OperationPruned] if `loc` falls in a pruned bitmap chunk. pub async fn new, S: Storage>( status: &impl BitmapReadable, storage: &S, inactivity_floor: Location, loc: Location, ops_root: D, ) -> Result> { if BitMap::::to_chunk_index(*loc) < status.pruned_chunks() { return Err(Error::OperationPruned(loc)); } let range_proof = RangeProof::new::(status, storage, inactivity_floor, loc..loc + 1, ops_root) .await?; let chunk = status.get_chunk(BitMap::::to_chunk_index(*loc)); Ok(Self { loc, chunk, range_proof, }) } /// Build an inclusion proof for the operation `plan` covers from digests already in memory, /// building the range proof with [RangeProof::build]. /// /// # Errors /// /// Returns [Error::OperationPruned] if the location falls in a pruned bitmap chunk, and /// otherwise the errors of [RangeProof::build]. pub fn build>( status: &impl BitmapReadable, plan: ElementPlan, get_node: impl Fn(Position) -> Option, inactivity_floor: Location, ops_root: D, ) -> Result> { let loc = plan.location(); if BitMap::::to_chunk_index(*loc) < status.pruned_chunks() { return Err(Error::OperationPruned(loc)); } let range_proof = RangeProof::build::(status, plan.into(), get_node, inactivity_floor, ops_root)?; let chunk = status.get_chunk(BitMap::::to_chunk_index(*loc)); Ok(Self { loc, chunk, range_proof, }) } } impl> Proof { /// Return true if the proof authenticates that `operation` is active in the database with /// the provided `root`. pub fn verify, O: Encode>(&self, operation: O, root: &D) -> bool { let chunk = self.chunk.as_ref(); let Ok(bits) = chunk_bits(chunk.len()) else { debug!("proof verification failed, invalid chunk size"); return false; }; let bit = *self.loc % bits; if chunk[(bit / 8) as usize] & (1 << (bit % 8)) == 0 { debug!(?self.loc, "proof verification failed, operation is inactive"); return false; } self.range_proof.verify_with_chunk_size::( self.loc, &[operation], core::slice::from_ref(&self.chunk), chunk.len(), root, ) } } impl> Write for Proof { fn write(&self, buf: &mut impl BufMut) { self.loc.write(buf); buf.put_slice(self.chunk.as_ref()); self.range_proof.write(buf); } } impl> EncodeSize for Proof { fn encode_size(&self) -> usize { self.loc.encode_size() + self.chunk.as_ref().len() + self.range_proof.encode_size() } } impl Proof { fn read_with( buf: &mut B, max_digests: &usize, read_chunk: impl FnOnce(&mut B) -> Result, ) -> Result { let loc = Location::::read(buf)?; let chunk = read_chunk(buf)?; let range_proof = RangeProof::::read_cfg(buf, max_digests)?; Ok(Self { loc, chunk, range_proof, }) } } impl Read for Proof { /// The maximum number of digests forwarded to the embedded range proof. type Cfg = usize; fn read_cfg( buf: &mut impl Buf, max_digests: &Self::Cfg, ) -> Result { Self::read_with(buf, max_digests, <[u8; N]>::read) } } impl Read for Proof { /// `(chunk_size, max_digests)`: the exact bitmap chunk size in bytes and the maximum number /// of digests forwarded to the embedded range proof. /// /// The chunk size must be a nonzero power of two whose bit width has grafting height below /// 63. It is supplied by the caller and is not encoded in the proof. type Cfg = (usize, usize); fn read_cfg( buf: &mut impl Buf, (chunk_size, max_digests): &Self::Cfg, ) -> Result { chunk_bits(*chunk_size)?; Self::read_with(buf, max_digests, |buf| { at_least(buf, *chunk_size)?; Ok(buf.copy_to_bytes(*chunk_size)) }) } } #[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for Proof where D: for<'a> arbitrary::Arbitrary<'a>, F::PendingChunk: for<'a> arbitrary::Arbitrary<'a>, { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { Ok(Self { loc: u.arbitrary()?, chunk: u.arbitrary()?, range_proof: u.arbitrary()?, }) } } #[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for Proof where D: for<'a> arbitrary::Arbitrary<'a>, F::PendingChunk: for<'a> arbitrary::Arbitrary<'a>, { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { let loc = u.arbitrary()?; let chunk_size = 1usize << u.int_in_range(0u8..=8)?; let chunk = Bytes::copy_from_slice(u.bytes(chunk_size)?); let range_proof = u.arbitrary()?; Ok(Self { loc, chunk, range_proof, }) } }