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(
104                event::EventEpochRoot {
105                    index,
106                    root: epoch_root,
107                    timestamp_seconds: block_timestamp,
108                }
109                .to_proto(),
110            );
111        }
112
113        self.write_sct_cache(sct);
114        self.persist_sct_cache();
115    }
116
117    /// Add a state commitment into the SCT, emitting an event recording its
118    /// source, and return the insert position in the tree.
119    async fn add_sct_commitment(
120        &mut self,
121        commitment: tct::StateCommitment,
122        source: CommitmentSource,
123    ) -> Result<tct::Position> {
124        // Record in the SCT
125        let mut tree = self.get_sct().await;
126        let position = tree.insert(tct::Witness::Forget, commitment)?;
127        self.write_sct_cache(tree);
128
129        // Record the commitment source in an event
130        self.record_proto(event::commitment(commitment, position, source));
131
132        Ok(position)
133    }
134
135    #[instrument(skip(self, source))]
136    /// Record a nullifier as spent in the verifiable storage.
137    async fn nullify(&mut self, nullifier: Nullifier, source: CommitmentSource) {
138        tracing::debug!("marking as spent");
139
140        // We need to record the nullifier as spent in the JMT (to prevent
141        // double spends), as well as in the CompactBlock (so that clients
142        // can learn that their note was spent).
143        self.put(
144            state_key::nullifier_set::spent_nullifier_lookup(&nullifier),
145            // We don't use the value for validity checks, but writing the source
146            // here lets us find out what transaction spent the nullifier.
147            NullificationInfo {
148                id: source
149                    .id()
150                    .expect("nullifiers are only consumed by transactions"),
151                spend_height: self.get_block_height().await.expect("block height is set"),
152            },
153        );
154
155        // Record the nullifier to be inserted into the compact block
156        let mut nullifiers = self.pending_nullifiers();
157        nullifiers.push_back(nullifier);
158        self.object_put(state_key::nullifier_set::pending_nullifiers(), nullifiers);
159    }
160
161    /// Seal the current block in the SCT, and produce an epoch root if
162    /// we are ending an epoch as well.
163    ///
164    /// # Panics
165    /// This method panic if the block is full, or if a serialization failure occurs.
166    async fn end_sct_block(
167        &mut self,
168        end_epoch: bool,
169    ) -> Result<(block::Root, Option<epoch::Root>)> {
170        let height = self.get_block_height().await?;
171
172        let mut tree = self.get_sct().await;
173
174        // Close the block in the SCT
175        let block_root = tree
176            .end_block()
177            .expect("ending a block in the state commitment tree can never fail");
178
179        // If the block ends an epoch, also close the epoch in the SCT
180        let epoch_root = if end_epoch {
181            let epoch_root = tree
182                .end_epoch()
183                .expect("ending an epoch in the state commitment tree can never fail");
184            Some(epoch_root)
185        } else {
186            None
187        };
188
189        self.write_sct(height, tree, block_root, epoch_root).await;
190
191        Ok((block_root, epoch_root))
192    }
193
194    // Set the state commitment tree in memory, but without committing to it in the nonverifiable
195    // storage (very cheap).
196    fn write_sct_cache(&mut self, tree: tct::Tree) {
197        self.object_put(state_key::cache::cached_state_commitment_tree(), tree);
198    }
199
200    /// Persist the object-store SCT instance to nonverifiable storage.
201    /// Note that this doesn't actually persist the SCT to disk, see the
202    /// cndiarium documentation for more information.
203    ///  
204    /// # Panics
205    /// This method panics if a serialization failure occurs.
206    fn persist_sct_cache(&mut self) {
207        // If the cached tree is dirty, flush it to storage
208        if let Some(tree) =
209            self.object_get::<tct::Tree>(state_key::cache::cached_state_commitment_tree())
210        {
211            let bytes = bincode::serialize(&tree)
212                .expect("able to serialize state commitment tree to bincode");
213            self.nonverifiable_put_raw(
214                state_key::tree::state_commitment_tree().as_bytes().to_vec(),
215                bytes,
216            );
217        }
218    }
219}
220
221impl<T: StateWrite + ?Sized> SctManager for T {}
222
223#[async_trait]
224pub trait VerificationExt: StateRead {
225    async fn check_claimed_anchor(&self, anchor: tct::Root) -> Result<()> {
226        if anchor.is_empty() {
227            return Ok(());
228        }
229
230        if let Some(anchor_height) = self
231            .get_proto::<u64>(&state_key::tree::anchor_lookup(anchor))
232            .await?
233        {
234            tracing::debug!(?anchor, ?anchor_height, "anchor is valid");
235            Ok(())
236        } else {
237            Err(anyhow!(
238                "provided anchor {} is not a valid SCT root",
239                anchor
240            ))
241        }
242    }
243
244    async fn check_nullifier_unspent(&self, nullifier: Nullifier) -> Result<()> {
245        if let Some(info) = self
246            .get::<NullificationInfo>(&state_key::nullifier_set::spent_nullifier_lookup(
247                &nullifier,
248            ))
249            .await?
250        {
251            anyhow::bail!(
252                "nullifier {} was already spent in {:?}",
253                nullifier,
254                hex::encode(info.id),
255            );
256        }
257        Ok(())
258    }
259}
260
261impl<T: StateRead + ?Sized> VerificationExt for T {}