penumbra_sdk_sct/component/
tree.rs

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;
8
9use crate::{
10    component::clock::EpochRead, event, state_key, CommitmentSource, NullificationInfo, Nullifier,
11};
12
13#[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.
18    async fn get_sct(&self) -> tct::Tree {
19        // If we have a cached tree, use that.
20        if let Some(tree) = self.object_get(state_key::cache::cached_state_commitment_tree()) {
21            return tree;
22        }
23
24        match 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        {
29            Some(bytes) => bincode::deserialize(&bytes).expect(
30                "able to deserialize stored state commitment tree from nonverifiable storage",
31            ),
32            None => tct::Tree::new(),
33        }
34    }
35
36    /// Return the SCT root for the given height, if it exists.
37    /// If the height is not found, return `None`.
38    async fn get_anchor_by_height(&self, height: u64) -> Result<Option<tct::Root>> {
39        self.get(&state_key::tree::anchor_by_height(height)).await
40    }
41
42    /// Return metadata on the specified nullifier, if it has been spent.
43    async fn spend_info(&self, nullifier: Nullifier) -> Result<Option<NullificationInfo>> {
44        self.get(&state_key::nullifier_set::spent_nullifier_lookup(
45            &nullifier,
46        ))
47        .await
48    }
49
50    /// Return the set of nullifiers that have been spent in the current block.
51    fn pending_nullifiers(&self) -> im::Vector<Nullifier> {
52        self.object_get(state_key::nullifier_set::pending_nullifiers())
53            .unwrap_or_default()
54    }
55}
56
57impl<T: StateRead + ?Sized> SctRead for T {}
58
59#[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.
67    async 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    ) {
74        let sct_anchor = sct.root();
75        let block_timestamp = self
76            .get_current_block_timestamp()
77            .await
78            .map(|t| t.unix_timestamp())
79            .unwrap_or(0);
80
81        // Write the anchor as a key, so we can check claimed anchors...
82        self.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?
85        self.put(state_key::tree::anchor_by_height(height), sct_anchor);
86
87        self.record_proto(event::anchor(height, sct_anchor, block_timestamp));
88        self.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.
97        if let Some(epoch_root) = epoch_root {
98            let index = self
99                .get_current_epoch()
100                .await
101                .expect("epoch must be set")
102                .index;
103            self.record_proto(event::epoch_root(index, epoch_root, block_timestamp));
104        }
105
106        self.write_sct_cache(sct);
107        self.persist_sct_cache();
108    }
109
110    /// Add a state commitment into the SCT, emitting an event recording its
111    /// source, and return the insert position in the tree.
112    async fn add_sct_commitment(
113        &mut self,
114        commitment: tct::StateCommitment,
115        source: CommitmentSource,
116    ) -> Result<tct::Position> {
117        // Record in the SCT
118        let mut tree = self.get_sct().await;
119        let position = tree.insert(tct::Witness::Forget, commitment)?;
120        self.write_sct_cache(tree);
121
122        // Record the commitment source in an event
123        self.record_proto(event::commitment(commitment, position, source));
124
125        Ok(position)
126    }
127
128    #[instrument(skip(self, source))]
129    /// Record a nullifier as spent in the verifiable storage.
130    async fn nullify(&mut self, nullifier: Nullifier, source: CommitmentSource) {
131        tracing::debug!("marking as spent");
132
133        // 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).
136        self.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.
140            NullificationInfo {
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        );
147
148        // Record the nullifier to be inserted into the compact block
149        let mut nullifiers = self.pending_nullifiers();
150        nullifiers.push_back(nullifier);
151        self.object_put(state_key::nullifier_set::pending_nullifiers(), nullifiers);
152    }
153
154    /// 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.
159    async fn end_sct_block(
160        &mut self,
161        end_epoch: bool,
162    ) -> Result<(block::Root, Option<epoch::Root>)> {
163        let height = self.get_block_height().await?;
164
165        let mut tree = self.get_sct().await;
166
167        // Close the block in the SCT
168        let block_root = tree
169            .end_block()
170            .expect("ending a block in the state commitment tree can never fail");
171
172        // If the block ends an epoch, also close the epoch in the SCT
173        let epoch_root = if end_epoch {
174            let epoch_root = tree
175                .end_epoch()
176                .expect("ending an epoch in the state commitment tree can never fail");
177            Some(epoch_root)
178        } else {
179            None
180        };
181
182        self.write_sct(height, tree, block_root, epoch_root).await;
183
184        Ok((block_root, epoch_root))
185    }
186
187    // Set the state commitment tree in memory, but without committing to it in the nonverifiable
188    // storage (very cheap).
189    fn write_sct_cache(&mut self, tree: tct::Tree) {
190        self.object_put(state_key::cache::cached_state_commitment_tree(), tree);
191    }
192
193    /// 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.
199    fn persist_sct_cache(&mut self) {
200        // If the cached tree is dirty, flush it to storage
201        if let Some(tree) =
202            self.object_get::<tct::Tree>(state_key::cache::cached_state_commitment_tree())
203        {
204            let bytes = bincode::serialize(&tree)
205                .expect("able to serialize state commitment tree to bincode");
206            self.nonverifiable_put_raw(
207                state_key::tree::state_commitment_tree().as_bytes().to_vec(),
208                bytes,
209            );
210        }
211    }
212}
213
214impl<T: StateWrite + ?Sized> SctManager for T {}
215
216#[async_trait]
217pub trait VerificationExt: StateRead {
218    async fn check_claimed_anchor(&self, anchor: tct::Root) -> Result<()> {
219        if anchor.is_empty() {
220            return Ok(());
221        }
222
223        if let Some(anchor_height) = self
224            .get_proto::<u64>(&state_key::tree::anchor_lookup(anchor))
225            .await?
226        {
227            tracing::debug!(?anchor, ?anchor_height, "anchor is valid");
228            Ok(())
229        } else {
230            Err(anyhow!(
231                "provided anchor {} is not a valid SCT root",
232                anchor
233            ))
234        }
235    }
236
237    async fn check_nullifier_unspent(&self, nullifier: Nullifier) -> Result<()> {
238        if let Some(info) = self
239            .get::<NullificationInfo>(&state_key::nullifier_set::spent_nullifier_lookup(
240                &nullifier,
241            ))
242            .await?
243        {
244            anyhow::bail!(
245                "nullifier {} was already spent in {:?}",
246                nullifier,
247                hex::encode(info.id),
248            );
249        }
250        Ok(())
251    }
252}
253
254impl<T: StateRead + ?Sized> VerificationExt for T {}