1use std::collections::BTreeMap;
2
3use penumbra_sdk_compact_block::{CompactBlock, StatePayload};
4use penumbra_sdk_dex::swap::{SwapPayload, SwapPlaintext};
5use penumbra_sdk_fee::GasPrices;
6use penumbra_sdk_keys::FullViewingKey;
7use penumbra_sdk_sct::Nullifier;
8use penumbra_sdk_shielded_pool::{fmd, Note, NotePayload};
9use penumbra_sdk_tct::{self as tct, StateCommitment};
10use tracing::Instrument;
11
12use crate::{SpendableNoteRecord, Storage, SwapRecord};
13
14#[derive(Debug, Clone)]
16pub struct FilteredBlock {
17 pub new_notes: BTreeMap<StateCommitment, SpendableNoteRecord>,
18 pub new_swaps: BTreeMap<StateCommitment, SwapRecord>,
19 pub spent_nullifiers: Vec<Nullifier>,
20 pub height: u64,
21 pub fmd_parameters: Option<fmd::Parameters>,
22 pub app_parameters_updated: bool,
23 pub gas_prices: Option<GasPrices>,
24}
25
26#[tracing::instrument(skip_all, fields(height = %height))]
27pub async fn scan_block(
28 fvk: &FullViewingKey,
29 state_commitment_tree: &mut tct::Tree,
30 CompactBlock {
31 height,
32 state_payloads,
33 nullifiers,
34 block_root,
35 epoch_root,
36 fmd_parameters,
37 swap_outputs,
38 app_parameters_updated,
39 gas_prices,
40 ..
43 }: CompactBlock,
44 storage: &Storage,
45) -> anyhow::Result<FilteredBlock> {
46 let trial_decrypt_note = |note_payload: NotePayload| -> tokio::task::JoinHandle<Option<Note>> {
48 let fvk2 = fvk.clone();
51 tokio::spawn(
52 async move { note_payload.trial_decrypt(&fvk2) }.instrument(tracing::Span::current()),
53 )
54 };
55 let trial_decrypt_swap =
57 |swap_payload: SwapPayload| -> tokio::task::JoinHandle<Option<SwapPlaintext>> {
58 let fvk2 = fvk.clone();
61 tokio::spawn(
62 async move { swap_payload.trial_decrypt(&fvk2) }
63 .instrument(tracing::Span::current()),
64 )
65 };
66
67 let spent_nullifiers: Vec<Nullifier> = nullifiers;
69
70 let mut note_decryptions = Vec::new();
72 let mut swap_decryptions = Vec::new();
73 let mut unknown_commitments = Vec::new();
74
75 for payload in state_payloads.iter() {
76 match payload {
77 StatePayload::Note { note, .. } => {
78 note_decryptions.push(trial_decrypt_note((**note).clone()));
79 }
80 StatePayload::Swap { swap, .. } => {
81 swap_decryptions.push(trial_decrypt_swap((**swap).clone()));
82 }
83 StatePayload::RolledUp { commitment, .. } => unknown_commitments.push(*commitment),
84 }
85 }
86 let mut note_advice = storage.scan_advice(unknown_commitments).await?;
88 for decryption in note_decryptions {
89 if let Some(note) = decryption
90 .await
91 .expect("able to join tokio note decryption handle")
92 {
93 note_advice.insert(note.commit(), note);
94 }
95 }
96 let mut swap_advice = BTreeMap::new();
97 for decryption in swap_decryptions {
98 if let Some(swap) = decryption
99 .await
100 .expect("able to join tokio swap decryption handle")
101 {
102 swap_advice.insert(swap.swap_commitment(), swap);
103 }
104 }
105
106 let mut new_notes = BTreeMap::new();
108 let mut new_swaps = BTreeMap::new();
110
111 if note_advice.is_empty() && swap_advice.is_empty() {
112 state_commitment_tree
115 .insert_block(block_root)
116 .expect("inserting a block root must succeed");
117 } else {
118 tracing::debug!("found at least one relevant SCT entry, reconstructing block subtree");
121
122 for payload in state_payloads.into_iter() {
123 match (
126 note_advice.get(payload.commitment()),
127 swap_advice.get(payload.commitment()),
128 ) {
129 (Some(note), None) => {
130 let position = state_commitment_tree
132 .insert(tct::Witness::Keep, *payload.commitment())
133 .expect("inserting a commitment must succeed");
134
135 let source = payload.source().clone();
136 let nullifier =
137 Nullifier::derive(fvk.nullifier_key(), position, payload.commitment());
138 let address_index = fvk.incoming().index_for_diversifier(note.diversifier());
139
140 new_notes.insert(
141 *payload.commitment(),
142 SpendableNoteRecord {
143 note_commitment: *payload.commitment(),
144 height_spent: None,
145 height_created: height,
146 note: note.clone(),
147 address_index,
148 nullifier,
149 position,
150 source,
151 return_address: None,
152 },
153 );
154 }
155 (None, Some(swap)) => {
156 let position = state_commitment_tree
158 .insert(tct::Witness::Keep, *payload.commitment())
159 .expect("inserting a commitment must succeed");
160
161 let Some(output_data) = swap_outputs.get(&swap.trading_pair).cloned() else {
162 tracing::warn!("invalid compact block, batch swap output data missing for trading pair {:?}", swap.trading_pair);
169 continue;
170 };
171
172 let (output_1, output_2) = swap.output_notes(&output_data);
175 storage.give_advice(output_1).await?;
176 storage.give_advice(output_2).await?;
177
178 let source = payload.source().clone();
179 let nullifier =
180 Nullifier::derive(fvk.nullifier_key(), position, payload.commitment());
181
182 new_swaps.insert(
183 *payload.commitment(),
184 SwapRecord {
185 swap_commitment: *payload.commitment(),
186 swap: swap.clone(),
187 position,
188 nullifier,
189 source,
190 output_data,
191 height_claimed: None,
192 },
193 );
194 }
195 (None, None) => {
196 state_commitment_tree
198 .insert(tct::Witness::Forget, *payload.commitment())
199 .expect("inserting a commitment must succeed");
200 }
201 (Some(_), Some(_)) => unreachable!("swap and note commitments are distinct"),
202 }
203 }
204
205 state_commitment_tree
207 .end_block()
208 .expect("ending the block must succeed");
209 }
210
211 let is_epoch_end = epoch_root.is_some();
213 if is_epoch_end {
214 tracing::debug!(?height, "end of epoch");
215 state_commitment_tree
216 .end_epoch()
217 .expect("ending the epoch must succeed");
218 }
219
220 #[cfg(feature = "sct-divergence-check")]
222 tracing::debug!(tct_root = %state_commitment_tree.root(), "tct root");
223
224 let filtered_nullifiers = storage.filter_nullifiers(spent_nullifiers).await?;
228
229 let result = FilteredBlock {
231 new_notes,
232 new_swaps,
233 spent_nullifiers: filtered_nullifiers,
234 height,
235 fmd_parameters,
236 app_parameters_updated,
237 gas_prices,
238 };
239
240 Ok(result)
241}