1use anyhow::{anyhow, Result};
2use async_trait::async_trait;
3use cnidarium::{StateRead, StateWrite};
4use penumbra_sdk_proto::{DomainType as _, StateReadProto, StateWriteProto};
5use penumbra_sdk_tct as tct;
6use tct::builder::{block, epoch};
7use tracing::instrument;
89use crate::{
10 component::clock::EpochRead, event, state_key, CommitmentSource, NullificationInfo, Nullifier,
11};
1213#[async_trait]
14/// Provides read access to the state commitment tree and related data.
15pub trait SctRead: StateRead {
16/// Fetch the state commitment tree from nonverifiable storage, preferring the cached tree if
17 /// it exists.
18async fn get_sct(&self) -> tct::Tree {
19// If we have a cached tree, use that.
20if let Some(tree) = self.object_get(state_key::cache::cached_state_commitment_tree()) {
21return tree;
22 }
2324match self
25.nonverifiable_get_raw(state_key::tree::state_commitment_tree().as_bytes())
26 .await
27.expect("able to retrieve state commitment tree from nonverifiable storage")
28 {
29Some(bytes) => bincode::deserialize(&bytes).expect(
30"able to deserialize stored state commitment tree from nonverifiable storage",
31 ),
32None => tct::Tree::new(),
33 }
34 }
3536/// Return the SCT root for the given height, if it exists.
37 /// If the height is not found, return `None`.
38async fn get_anchor_by_height(&self, height: u64) -> Result<Option<tct::Root>> {
39self.get(&state_key::tree::anchor_by_height(height)).await
40}
4142/// Return metadata on the specified nullifier, if it has been spent.
43async fn spend_info(&self, nullifier: Nullifier) -> Result<Option<NullificationInfo>> {
44self.get(&state_key::nullifier_set::spent_nullifier_lookup(
45&nullifier,
46 ))
47 .await
48}
4950/// Return the set of nullifiers that have been spent in the current block.
51fn pending_nullifiers(&self) -> im::Vector<Nullifier> {
52self.object_get(state_key::nullifier_set::pending_nullifiers())
53 .unwrap_or_default()
54 }
55}
5657impl<T: StateRead + ?Sized> SctRead for T {}
5859#[async_trait]
60/// Provides write access to the state commitment tree and related data.
61pub trait SctManager: StateWrite {
62/// Write an SCT instance to nonverifiable storage and record
63 /// the block and epoch roots in the JMT.
64 ///
65 /// # Panics
66 /// If the epoch has not been set, or if a serialization failure occurs.
67async fn write_sct(
68&mut self,
69 height: u64,
70 sct: tct::Tree,
71 block_root: block::Root,
72 epoch_root: Option<epoch::Root>,
73 ) {
74let sct_anchor = sct.root();
75let block_timestamp = self
76.get_current_block_timestamp()
77 .await
78.map(|t| t.unix_timestamp())
79 .unwrap_or(0);
8081// Write the anchor as a key, so we can check claimed anchors...
82self.put_proto(state_key::tree::anchor_lookup(sct_anchor), height);
83// ... and as a value, so we can check SCT consistency.
84 // TODO: can we move this out to NV storage?
85self.put(state_key::tree::anchor_by_height(height), sct_anchor);
8687self.record_proto(event::anchor(height, sct_anchor, block_timestamp));
88self.record_proto(
89 event::EventBlockRoot {
90 height,
91 root: block_root,
92 timestamp_seconds: block_timestamp,
93 }
94 .to_proto(),
95 );
96// Only record an epoch root event if we are ending the epoch.
97if let Some(epoch_root) = epoch_root {
98let index = self
99.get_current_epoch()
100 .await
101.expect("epoch must be set")
102 .index;
103self.record_proto(event::epoch_root(index, epoch_root, block_timestamp));
104 }
105106self.write_sct_cache(sct);
107self.persist_sct_cache();
108 }
109110/// Add a state commitment into the SCT, emitting an event recording its
111 /// source, and return the insert position in the tree.
112async fn add_sct_commitment(
113&mut self,
114 commitment: tct::StateCommitment,
115 source: CommitmentSource,
116 ) -> Result<tct::Position> {
117// Record in the SCT
118let mut tree = self.get_sct().await;
119let position = tree.insert(tct::Witness::Forget, commitment)?;
120self.write_sct_cache(tree);
121122// Record the commitment source in an event
123self.record_proto(event::commitment(commitment, position, source));
124125Ok(position)
126 }
127128#[instrument(skip(self, source))]
129/// Record a nullifier as spent in the verifiable storage.
130async fn nullify(&mut self, nullifier: Nullifier, source: CommitmentSource) {
131tracing::debug!("marking as spent");
132133// We need to record the nullifier as spent in the JMT (to prevent
134 // double spends), as well as in the CompactBlock (so that clients
135 // can learn that their note was spent).
136self.put(
137 state_key::nullifier_set::spent_nullifier_lookup(&nullifier),
138// We don't use the value for validity checks, but writing the source
139 // here lets us find out what transaction spent the nullifier.
140NullificationInfo {
141 id: source
142 .id()
143 .expect("nullifiers are only consumed by transactions"),
144 spend_height: self.get_block_height().await.expect("block height is set"),
145 },
146 );
147148// Record the nullifier to be inserted into the compact block
149let mut nullifiers = self.pending_nullifiers();
150 nullifiers.push_back(nullifier);
151self.object_put(state_key::nullifier_set::pending_nullifiers(), nullifiers);
152 }
153154/// Seal the current block in the SCT, and produce an epoch root if
155 /// we are ending an epoch as well.
156 ///
157 /// # Panics
158 /// This method panic if the block is full, or if a serialization failure occurs.
159async fn end_sct_block(
160&mut self,
161 end_epoch: bool,
162 ) -> Result<(block::Root, Option<epoch::Root>)> {
163let height = self.get_block_height().await?;
164165let mut tree = self.get_sct().await;
166167// Close the block in the SCT
168let block_root = tree
169 .end_block()
170 .expect("ending a block in the state commitment tree can never fail");
171172// If the block ends an epoch, also close the epoch in the SCT
173let epoch_root = if end_epoch {
174let epoch_root = tree
175 .end_epoch()
176 .expect("ending an epoch in the state commitment tree can never fail");
177Some(epoch_root)
178 } else {
179None
180};
181182self.write_sct(height, tree, block_root, epoch_root).await;
183184Ok((block_root, epoch_root))
185 }
186187// Set the state commitment tree in memory, but without committing to it in the nonverifiable
188 // storage (very cheap).
189fn write_sct_cache(&mut self, tree: tct::Tree) {
190self.object_put(state_key::cache::cached_state_commitment_tree(), tree);
191 }
192193/// Persist the object-store SCT instance to nonverifiable storage.
194 /// Note that this doesn't actually persist the SCT to disk, see the
195 /// cndiarium documentation for more information.
196 ///
197 /// # Panics
198 /// This method panics if a serialization failure occurs.
199fn persist_sct_cache(&mut self) {
200// If the cached tree is dirty, flush it to storage
201if let Some(tree) =
202self.object_get::<tct::Tree>(state_key::cache::cached_state_commitment_tree())
203 {
204let bytes = bincode::serialize(&tree)
205 .expect("able to serialize state commitment tree to bincode");
206self.nonverifiable_put_raw(
207 state_key::tree::state_commitment_tree().as_bytes().to_vec(),
208 bytes,
209 );
210 }
211 }
212}
213214impl<T: StateWrite + ?Sized> SctManager for T {}
215216#[async_trait]
217pub trait VerificationExt: StateRead {
218async fn check_claimed_anchor(&self, anchor: tct::Root) -> Result<()> {
219if anchor.is_empty() {
220return Ok(());
221 }
222223if let Some(anchor_height) = self
224.get_proto::<u64>(&state_key::tree::anchor_lookup(anchor))
225 .await?
226{
227tracing::debug!(?anchor, ?anchor_height, "anchor is valid");
228Ok(())
229 } else {
230Err(anyhow!(
231"provided anchor {} is not a valid SCT root",
232 anchor
233 ))
234 }
235 }
236237async fn check_nullifier_unspent(&self, nullifier: Nullifier) -> Result<()> {
238if let Some(info) = self
239.get::<NullificationInfo>(&state_key::nullifier_set::spent_nullifier_lookup(
240&nullifier,
241 ))
242 .await?
243{
244anyhow::bail!(
245"nullifier {} was already spent in {:?}",
246 nullifier,
247 hex::encode(info.id),
248 );
249 }
250Ok(())
251 }
252}
253254impl<T: StateRead + ?Sized> VerificationExt for T {}