penumbra_sct/component/
tree.rsuse anyhow::{anyhow, Result};
use async_trait::async_trait;
use cnidarium::{StateRead, StateWrite};
use penumbra_proto::{DomainType as _, StateReadProto, StateWriteProto};
use penumbra_tct as tct;
use tct::builder::{block, epoch};
use tracing::instrument;
use crate::{
component::clock::EpochRead, event, state_key, CommitmentSource, NullificationInfo, Nullifier,
};
#[async_trait]
pub trait SctRead: StateRead {
async fn get_sct(&self) -> tct::Tree {
if let Some(tree) = self.object_get(state_key::cache::cached_state_commitment_tree()) {
return tree;
}
match self
.nonverifiable_get_raw(state_key::tree::state_commitment_tree().as_bytes())
.await
.expect("able to retrieve state commitment tree from nonverifiable storage")
{
Some(bytes) => bincode::deserialize(&bytes).expect(
"able to deserialize stored state commitment tree from nonverifiable storage",
),
None => tct::Tree::new(),
}
}
async fn get_anchor_by_height(&self, height: u64) -> Result<Option<tct::Root>> {
self.get(&state_key::tree::anchor_by_height(height)).await
}
async fn spend_info(&self, nullifier: Nullifier) -> Result<Option<NullificationInfo>> {
self.get(&state_key::nullifier_set::spent_nullifier_lookup(
&nullifier,
))
.await
}
fn pending_nullifiers(&self) -> im::Vector<Nullifier> {
self.object_get(state_key::nullifier_set::pending_nullifiers())
.unwrap_or_default()
}
}
impl<T: StateRead + ?Sized> SctRead for T {}
#[async_trait]
pub trait SctManager: StateWrite {
async fn write_sct(
&mut self,
height: u64,
sct: tct::Tree,
block_root: block::Root,
epoch_root: Option<epoch::Root>,
) {
let sct_anchor = sct.root();
let block_timestamp = self
.get_current_block_timestamp()
.await
.map(|t| t.unix_timestamp())
.unwrap_or(0);
self.put_proto(state_key::tree::anchor_lookup(sct_anchor), height);
self.put(state_key::tree::anchor_by_height(height), sct_anchor);
self.record_proto(event::anchor(height, sct_anchor, block_timestamp));
self.record_proto(
event::EventBlockRoot {
height,
root: block_root,
timestamp_seconds: block_timestamp,
}
.to_proto(),
);
if let Some(epoch_root) = epoch_root {
let index = self
.get_current_epoch()
.await
.expect("epoch must be set")
.index;
self.record_proto(event::epoch_root(index, epoch_root, block_timestamp));
}
self.write_sct_cache(sct);
self.persist_sct_cache();
}
async fn add_sct_commitment(
&mut self,
commitment: tct::StateCommitment,
source: CommitmentSource,
) -> Result<tct::Position> {
let mut tree = self.get_sct().await;
let position = tree.insert(tct::Witness::Forget, commitment)?;
self.write_sct_cache(tree);
self.record_proto(event::commitment(commitment, position, source));
Ok(position)
}
#[instrument(skip(self, source))]
async fn nullify(&mut self, nullifier: Nullifier, source: CommitmentSource) {
tracing::debug!("marking as spent");
self.put(
state_key::nullifier_set::spent_nullifier_lookup(&nullifier),
NullificationInfo {
id: source
.id()
.expect("nullifiers are only consumed by transactions"),
spend_height: self.get_block_height().await.expect("block height is set"),
},
);
let mut nullifiers = self.pending_nullifiers();
nullifiers.push_back(nullifier);
self.object_put(state_key::nullifier_set::pending_nullifiers(), nullifiers);
}
async fn end_sct_block(
&mut self,
end_epoch: bool,
) -> Result<(block::Root, Option<epoch::Root>)> {
let height = self.get_block_height().await?;
let mut tree = self.get_sct().await;
let block_root = tree
.end_block()
.expect("ending a block in the state commitment tree can never fail");
let epoch_root = if end_epoch {
let epoch_root = tree
.end_epoch()
.expect("ending an epoch in the state commitment tree can never fail");
Some(epoch_root)
} else {
None
};
self.write_sct(height, tree, block_root, epoch_root).await;
Ok((block_root, epoch_root))
}
fn write_sct_cache(&mut self, tree: tct::Tree) {
self.object_put(state_key::cache::cached_state_commitment_tree(), tree);
}
fn persist_sct_cache(&mut self) {
if let Some(tree) =
self.object_get::<tct::Tree>(state_key::cache::cached_state_commitment_tree())
{
let bytes = bincode::serialize(&tree)
.expect("able to serialize state commitment tree to bincode");
self.nonverifiable_put_raw(
state_key::tree::state_commitment_tree().as_bytes().to_vec(),
bytes,
);
}
}
}
impl<T: StateWrite + ?Sized> SctManager for T {}
#[async_trait]
pub trait VerificationExt: StateRead {
async fn check_claimed_anchor(&self, anchor: tct::Root) -> Result<()> {
if anchor.is_empty() {
return Ok(());
}
if let Some(anchor_height) = self
.get_proto::<u64>(&state_key::tree::anchor_lookup(anchor))
.await?
{
tracing::debug!(?anchor, ?anchor_height, "anchor is valid");
Ok(())
} else {
Err(anyhow!(
"provided anchor {} is not a valid SCT root",
anchor
))
}
}
async fn check_nullifier_unspent(&self, nullifier: Nullifier) -> Result<()> {
if let Some(info) = self
.get::<NullificationInfo>(&state_key::nullifier_set::spent_nullifier_lookup(
&nullifier,
))
.await?
{
anyhow::bail!(
"nullifier {} was already spent in {:?}",
nullifier,
hex::encode(info.id),
);
}
Ok(())
}
}
impl<T: StateRead + ?Sized> VerificationExt for T {}