use crate::{
Context, SyncCompletion,
journal::contiguous::{Contiguous, variable},
merkle::{self, Family, Location, MAX_PINNED_NODES, Proof, compact},
qmdb::{
self, Error,
operation::Floored,
sync::{CompactTarget, Request, Response},
},
};
use commonware_codec::{Decode as _, EncodeSize, Read, Write};
use commonware_cryptography::{Digest, Hasher};
use commonware_parallel::Strategy;
use commonware_runtime::{Error as RError, Handle};
use commonware_utils::sync::RwLock;
use futures::FutureExt as _;
use std::sync::atomic::{AtomicBool, Ordering};
#[derive(Clone)]
pub(crate) struct Witness<F: Family, D: Digest> {
pub(crate) op_bytes: Vec<u8>,
pub(crate) size: Location<F>,
pub(crate) pinned_nodes: Vec<D>,
}
impl<F: Family, D: Digest> EncodeSize for Witness<F, D> {
fn encode_size(&self) -> usize {
self.op_bytes.encode_size() + self.size.encode_size() + self.pinned_nodes.encode_size()
}
}
impl<F: Family, D: Digest> Write for Witness<F, D> {
fn write(&self, buf: &mut impl bytes::BufMut) {
self.op_bytes.write(buf);
self.size.write(buf);
self.pinned_nodes.write(buf);
}
}
impl<F: Family, D: Digest> Read for Witness<F, D> {
type Cfg = ();
fn read_cfg(buf: &mut impl bytes::Buf, _: &()) -> Result<Self, commonware_codec::Error> {
let op_bytes = Vec::<u8>::read_cfg(buf, &((..).into(), ()))?;
let size = Location::<F>::read_cfg(buf, &())?;
let pinned_nodes = Vec::<D>::read_cfg(buf, &((..=MAX_PINNED_NODES).into(), ()))?;
Ok(Self {
op_bytes,
size,
pinned_nodes,
})
}
}
#[cfg(feature = "arbitrary")]
impl<F: Family, D: Digest> arbitrary::Arbitrary<'_> for Witness<F, D>
where
D: for<'a> arbitrary::Arbitrary<'a>,
{
fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
Ok(Self {
op_bytes: u.arbitrary()?,
size: Location::new(u.int_in_range(1..=*F::MAX_LEAVES)?),
pinned_nodes: u.arbitrary()?,
})
}
}
#[derive(Clone)]
pub(crate) struct VerifiedWitness<F: Family, D: Digest> {
pub(crate) witness: Witness<F, D>,
pub(crate) root: D,
pub(crate) proof: Proof<F, D>,
}
impl<F: Family, D: Digest> VerifiedWitness<F, D> {
pub(crate) const fn size(&self) -> Location<F> {
self.witness.size
}
pub(crate) const fn target(&self) -> CompactTarget<F, D> {
CompactTarget {
root: self.root,
size: self.size(),
}
}
}
pub(crate) type Journal<E, F, D> = variable::Journal<E, Witness<F, D>>;
#[derive(Clone, Copy)]
enum Durability {
Commit,
Sync,
}
pub(crate) struct Store<E: Context, F: Family, D: Digest> {
journal: Journal<E, F, D>,
tip_witness: RwLock<VerifiedWitness<F, D>>,
import_pending: AtomicBool,
uncommitted: bool,
pending_sync: Option<SyncCompletion>,
}
impl<E: Context, F: Family, D: Digest> Store<E, F, D> {
pub(crate) const fn new(journal: Journal<E, F, D>, witness: VerifiedWitness<F, D>) -> Self {
Self {
journal,
tip_witness: RwLock::new(witness),
import_pending: AtomicBool::new(false),
uncommitted: false,
pending_sync: None,
}
}
pub(crate) const fn from_import(
journal: Journal<E, F, D>,
witness: VerifiedWitness<F, D>,
) -> Self {
Self {
journal,
tip_witness: RwLock::new(witness),
import_pending: AtomicBool::new(true),
uncommitted: false,
pending_sync: None,
}
}
pub(crate) fn with<R>(&self, f: impl FnOnce(&VerifiedWitness<F, D>) -> R) -> R {
f(&self.tip_witness.read())
}
#[allow(clippy::type_complexity)]
#[tracing::instrument(
name = "qmdb.sync.serve",
level = "info",
skip_all,
fields(
size = *request.size(),
start = *request.start(),
max_ops = request.max_ops().get(),
),
)]
pub(crate) fn compact_state<Op: Read>(
&self,
cfg: &Op::Cfg,
request: Request<F>,
) -> Result<Response<F, Op, D>, Error<F>> {
let (entry, proof) = self.with(|w| -> Result<(Witness<F, D>, Proof<F, D>), Error<F>> {
let current = w.size();
let last_commit_loc = current - 1;
if request.size() > current || request.size() == 0 {
return Err(merkle::Error::RangeOutOfBounds(request.size()).into());
}
if request.size() < current {
return Err(crate::journal::Error::ItemPruned(*request.size() - 1).into());
}
if request.start() >= request.size() {
return Err(merkle::Error::RangeOutOfBounds(request.start()).into());
}
if request.start() < last_commit_loc {
return Err(crate::journal::Error::ItemPruned(*request.start()).into());
}
Ok((w.witness.clone(), w.proof.clone()))
})?;
let Witness {
op_bytes,
pinned_nodes,
..
} = entry;
let op = Op::decode_cfg(op_bytes.as_ref(), cfg)
.map_err(|_| Error::DataCorrupted("invalid commit operation"))?;
Ok(match request {
Request::Operations { .. } => Response::Operations {
proof,
operations: vec![op],
},
Request::Boundary { .. } => Response::Boundary {
proof,
op,
pinned_nodes,
},
})
}
pub(crate) fn replace(&self, witness: VerifiedWitness<F, D>) {
*self.tip_witness.write() = witness;
}
pub(crate) async fn apply<H, S>(
mut self,
merkle: &compact::Merkle<F, D, S>,
inactivity_floor_loc: Location<F>,
last_commit_op_bytes: impl FnOnce() -> Vec<u8>,
) -> Result<Self, Error<F>>
where
H: Hasher<Digest = D>,
S: Strategy,
{
let verified;
(self, verified) = self
.stage::<H, S>(merkle, inactivity_floor_loc, last_commit_op_bytes)
.await?;
let Some(verified) = verified else {
return Ok(self);
};
(self.journal, _) = self.journal.append(&verified.witness).await?;
self.import_pending.store(false, Ordering::Relaxed);
self.uncommitted = true;
merkle.prune_to_frontier();
self.replace(verified);
Ok(self)
}
pub(crate) async fn commit<H, S>(
self,
merkle: &compact::Merkle<F, D, S>,
inactivity_floor_loc: Location<F>,
last_commit_op_bytes: impl FnOnce() -> Vec<u8>,
) -> Result<Self, Error<F>>
where
H: Hasher<Digest = D>,
S: Strategy,
{
self.wait_for_sync().await?;
self.persist::<H, S>(
merkle,
inactivity_floor_loc,
last_commit_op_bytes,
Durability::Commit,
)
.await
}
pub(crate) async fn sync<H, S>(
self,
merkle: &compact::Merkle<F, D, S>,
inactivity_floor_loc: Location<F>,
last_commit_op_bytes: impl FnOnce() -> Vec<u8>,
) -> Result<Self, Error<F>>
where
H: Hasher<Digest = D>,
S: Strategy,
{
self.persist::<H, S>(
merkle,
inactivity_floor_loc,
last_commit_op_bytes,
Durability::Sync,
)
.await
}
async fn persist<H, S>(
mut self,
merkle: &compact::Merkle<F, D, S>,
inactivity_floor_loc: Location<F>,
last_commit_op_bytes: impl FnOnce() -> Vec<u8>,
durability: Durability,
) -> Result<Self, Error<F>>
where
H: Hasher<Digest = D>,
S: Strategy,
{
self = self
.apply::<H, S>(merkle, inactivity_floor_loc, last_commit_op_bytes)
.await?;
match durability {
Durability::Commit if self.uncommitted => {
self.journal = self.journal.commit().await?;
self.uncommitted = false;
}
Durability::Sync if self.uncommitted || self.pending_sync.is_some() => {
let journal = self.journal.sync().await?;
self.pending_sync = None;
self.uncommitted = false;
self.journal = journal;
}
Durability::Commit | Durability::Sync => {}
}
Ok(self)
}
pub(crate) async fn start_sync<H, S>(
mut self,
merkle: &compact::Merkle<F, D, S>,
inactivity_floor_loc: Location<F>,
last_commit_op_bytes: impl FnOnce() -> Vec<u8>,
) -> Result<(Self, Handle<()>), Error<F>>
where
H: Hasher<Digest = D>,
S: Strategy,
{
if let Err(err) = self.wait_for_sync().await {
return Ok((self, Handle::ready(Err(err))));
}
self = self
.apply::<H, S>(merkle, inactivity_floor_loc, last_commit_op_bytes)
.await?;
let handle;
(self.journal, handle) = self.journal.start_sync().await?;
let completion: SyncCompletion = handle.boxed().shared();
self.uncommitted = false;
self.pending_sync = Some(completion.clone());
Ok((self, Handle::from_future(completion)))
}
pub(crate) async fn wait_for_sync(&self) -> Result<(), RError> {
let Some(pending) = self.pending_sync.clone() else {
return Ok(());
};
pending.await
}
async fn stage<H, S>(
mut self,
merkle: &compact::Merkle<F, D, S>,
inactivity_floor_loc: Location<F>,
last_commit_op_bytes: impl FnOnce() -> Vec<u8>,
) -> Result<(Self, Option<VerifiedWitness<F, D>>), Error<F>>
where
H: Hasher<Digest = D>,
S: Strategy,
{
let cached_size = self.with(|w| w.size());
let verified = if cached_size == merkle.leaves() {
if !self.import_pending.load(Ordering::Relaxed) {
return Ok((self, None));
}
self.with(|w| w.clone())
} else if cached_size > merkle.leaves() {
return Err(Error::DataCorrupted("witness ahead of in-memory state"));
} else {
build_witness::<F, H, S>(merkle, inactivity_floor_loc, last_commit_op_bytes())?
};
if self.import_pending.load(Ordering::Relaxed) {
self = self.clear_for_import().await?;
}
Ok((self, Some(verified)))
}
pub(crate) async fn rewind<H, S, Op>(
mut self,
merkle: &compact::Merkle<F, D, S>,
target: Location<F>,
commit_codec_config: &Op::Cfg,
) -> Result<(Self, Op), Error<F>>
where
H: Hasher<Digest = D>,
S: Strategy,
Op: Read + Floored<F>,
{
self.check_import_applied()?;
let (pos, entry) = self
.position_of(target)
.await?
.ok_or(Error::Merkle(merkle::Error::RewindBeyondHistory))?;
let (witness, op) = rebuild::<F, D, H, S, Op>(entry, merkle, commit_codec_config)?;
self.journal = self.journal.rewind(pos + 1).await?.sync().await?;
self.pending_sync = None;
self.uncommitted = false;
self.replace(witness);
Ok((self, op))
}
pub(crate) async fn prune(mut self, pruning_boundary: Location<F>) -> Result<Self, Error<F>> {
self.check_import_applied()?;
let bounds = self.journal.bounds();
if bounds.is_empty() {
return Ok(self);
}
let pos = Self::first_at_or_above(&self.journal, pruning_boundary)
.await?
.min(bounds.end - 1);
(self.journal, _) = self.journal.prune(pos).await?;
self.journal = self.journal.sync().await?;
self.pending_sync = None;
self.uncommitted = false;
Ok(self)
}
pub(crate) fn has_uncommitted_state(&self) -> bool {
self.import_pending.load(Ordering::Relaxed) || self.uncommitted
}
fn check_import_applied(&self) -> Result<(), Error<F>> {
if self.import_pending.load(Ordering::Relaxed) {
return Err(Error::DataCorrupted("compact-sync import not applied"));
}
Ok(())
}
async fn position_of(
&self,
target: Location<F>,
) -> Result<Option<(u64, Witness<F, D>)>, Error<F>> {
let pos = Self::first_at_or_above(&self.journal, target).await?;
if pos >= self.journal.bounds().end {
return Ok(None);
}
let entry = self.journal.read(pos).await?;
Ok((entry.size == target).then_some((pos, entry)))
}
async fn first_at_or_above(
reader: &impl Contiguous<Item = Witness<F, D>>,
size: Location<F>,
) -> Result<u64, Error<F>> {
let bounds = reader.bounds();
let (mut lo, mut hi) = (bounds.start, bounds.end);
while lo < hi {
let mid = lo + (hi - lo) / 2;
if reader.read(mid).await?.size < size {
lo = mid + 1;
} else {
hi = mid;
}
}
Ok(lo)
}
async fn clear_for_import(mut self) -> Result<Self, Error<F>> {
let size = self.journal.size();
self.journal = self.journal.clear_to_size(size.max(1)).await?;
Ok(self)
}
pub(crate) async fn destroy(self) -> Result<(), Error<F>> {
self.journal.destroy().await?;
Ok(())
}
}
pub(crate) fn build_witness<F, H, S>(
merkle: &compact::Merkle<F, H::Digest, S>,
inactivity_floor_loc: Location<F>,
last_commit_op_bytes: Vec<u8>,
) -> Result<VerifiedWitness<F, H::Digest>, Error<F>>
where
F: Family,
H: Hasher,
S: Strategy,
{
let hasher = qmdb::hasher::<H>();
merkle.with_mem(|mem| {
let size = mem.leaves();
let last_commit_loc = size - 1;
let inactive_peaks = F::inactive_peaks(size, inactivity_floor_loc);
let root = mem.root(&hasher, inactive_peaks)?;
let pinned_nodes = F::nodes_to_pin(last_commit_loc)
.map(|pos| *mem.get_node_unchecked(pos))
.collect::<Vec<_>>();
let proof = mem.proof(&hasher, last_commit_loc, inactive_peaks)?;
Ok(VerifiedWitness {
witness: Witness {
op_bytes: last_commit_op_bytes,
size,
pinned_nodes,
},
root,
proof,
})
})
}
pub(crate) fn validate_inactivity_floor<F: Family>(
inactivity_floor_loc: Location<F>,
last_commit_loc: Location<F>,
) -> Result<(), Error<F>> {
if inactivity_floor_loc > last_commit_loc {
return Err(Error::DataCorrupted("invalid compact witness"));
}
Ok(())
}
async fn load_tip<E, F, H, S, Op>(
journal: &Journal<E, F, H::Digest>,
merkle: &compact::Merkle<F, H::Digest, S>,
commit_codec_config: &Op::Cfg,
) -> Result<(VerifiedWitness<F, H::Digest>, Op), Error<F>>
where
E: Context,
F: Family,
H: Hasher,
S: Strategy,
Op: Read + Floored<F>,
{
let size = journal.size();
if size == 0 {
return Err(Error::DataCorrupted("missing compact witness"));
}
let entry = journal.read(size - 1).await?;
rebuild::<F, H::Digest, H, S, Op>(entry, merkle, commit_codec_config)
}
fn rebuild<F, D, H, S, Op>(
witness: Witness<F, D>,
merkle: &compact::Merkle<F, D, S>,
commit_codec_config: &Op::Cfg,
) -> Result<(VerifiedWitness<F, D>, Op), Error<F>>
where
F: Family,
D: Digest,
H: Hasher<Digest = D>,
S: Strategy,
Op: Read + Floored<F>,
{
let size = witness.size;
if size == 0 {
return Err(Error::DataCorrupted("invalid compact witness"));
}
let last_commit_loc = size - 1;
let last_commit_op = Op::decode_cfg(witness.op_bytes.as_ref(), commit_codec_config)
.map_err(|_| Error::DataCorrupted("invalid commit operation"))?;
let inactivity_floor_loc = last_commit_op
.has_floor()
.ok_or(Error::DataCorrupted("last operation was not a commit"))?;
validate_inactivity_floor(inactivity_floor_loc, last_commit_loc)?;
let hasher = qmdb::hasher::<H>();
merkle
.reset_to(last_commit_loc, witness.pinned_nodes.clone())
.map_err(|_| Error::DataCorrupted("invalid compact witness"))?;
merkle
.append_leaf(&hasher, &witness.op_bytes)
.map_err(|_| Error::DataCorrupted("invalid compact witness"))?;
let verified = build_witness::<F, H, S>(merkle, inactivity_floor_loc, witness.op_bytes)
.map_err(|_| Error::DataCorrupted("invalid compact witness"))?;
merkle.prune_to_frontier();
Ok((verified, last_commit_op))
}
pub(crate) async fn init<E, F, H, S, Op>(
mut journal: Journal<E, F, H::Digest>,
merkle: &mut compact::Merkle<F, H::Digest, S>,
commit_codec_config: &Op::Cfg,
initial_commit_op_bytes: Vec<u8>,
) -> Result<(Store<E, F, H::Digest>, Op), Error<F>>
where
E: Context,
F: Family,
H: Hasher,
S: Strategy,
Op: Read + Floored<F>,
{
if journal.size() == 0 {
journal = bootstrap_initial_commit::<E, F, H, S>(journal, merkle, initial_commit_op_bytes)
.await?;
}
let (witness, op) = load_tip::<E, F, H, S, Op>(&journal, merkle, commit_codec_config).await?;
Ok((Store::new(journal, witness), op))
}
async fn bootstrap_initial_commit<E, F, H, S>(
journal: Journal<E, F, H::Digest>,
merkle: &mut compact::Merkle<F, H::Digest, S>,
last_commit_op_bytes: Vec<u8>,
) -> Result<Journal<E, F, H::Digest>, Error<F>>
where
E: Context,
F: Family,
H: Hasher,
S: Strategy,
{
let hasher = qmdb::hasher::<H>();
let batch = {
let batch = merkle.new_batch().add(&hasher, &last_commit_op_bytes);
merkle.with_mem(|mem| batch.merkleize(mem, &hasher))
};
merkle.apply_batch(&batch)?;
let verified = build_witness::<F, H, S>(merkle, Location::new(0), last_commit_op_bytes)?;
let (journal, _) = journal.append(&verified.witness).await?;
let journal = journal.sync().await?;
Ok(journal)
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
#[cfg(feature = "arbitrary")]
mod conformance {
use super::*;
use crate::merkle::{mmb, mmr};
use commonware_codec::conformance::CodecConformance;
use commonware_cryptography::sha256;
commonware_conformance::conformance_tests! {
CodecConformance<Witness<mmr::Family, sha256::Digest>>,
CodecConformance<Witness<mmb::Family, sha256::Digest>>,
}
}
pub(crate) async fn corrupt_entry<E, F, D>(
journal: Journal<E, F, D>,
pos: u64,
f: impl FnOnce(&mut Witness<F, D>),
) -> Journal<E, F, D>
where
E: Context,
F: Family,
D: Digest,
{
let mut entries = Vec::new();
{
for p in pos..journal.bounds().end {
entries.push(journal.read(p).await.unwrap());
}
}
f(&mut entries[0]);
let mut journal = journal.rewind(pos).await.unwrap();
for entry in &entries {
(journal, _) = journal.append(entry).await.unwrap();
}
journal.sync().await.unwrap()
}
pub(crate) async fn tip<E, F, D>(journal: &Journal<E, F, D>) -> (Vec<u8>, Location<F>, Vec<D>)
where
E: Context,
F: Family,
D: Digest,
{
let size = journal.size();
let entry = journal.read(size - 1).await.unwrap();
(entry.op_bytes, entry.size, entry.pinned_nodes)
}
pub(crate) async fn append_unsynced<E, F, D>(
journal: Journal<E, F, D>,
op_bytes: Vec<u8>,
size: Location<F>,
pinned_nodes: Vec<D>,
) -> Journal<E, F, D>
where
E: Context,
F: Family,
D: Digest,
{
let (journal, _) = journal
.append(&Witness {
op_bytes,
size,
pinned_nodes,
})
.await
.unwrap();
journal
}
pub(crate) async fn overwrite_tip<E, F, D>(
journal: Journal<E, F, D>,
op_bytes: Vec<u8>,
size: Location<F>,
pinned_nodes: Vec<D>,
) -> Journal<E, F, D>
where
E: Context,
F: Family,
D: Digest,
{
let entries = journal.size();
let journal = journal.rewind(entries - 1).await.unwrap();
let (journal, _) = journal
.append(&Witness {
op_bytes,
size,
pinned_nodes,
})
.await
.unwrap();
journal.sync().await.unwrap()
}
}