miden_client/rpc/verifying_client/
mod.rs1use alloc::boxed::Box;
2use alloc::collections::BTreeSet;
3use alloc::string::ToString;
4use alloc::vec::Vec;
5
6use miden_protocol::Word;
7use miden_protocol::account::AccountId;
8use miden_protocol::address::NetworkId;
9use miden_protocol::batch::{ProposedBatch, ProvenBatch};
10use miden_protocol::block::{BlockHeader, BlockNumber, ProvenBlock};
11use miden_protocol::crypto::merkle::mmr::MmrProof;
12use miden_protocol::note::{NoteId, NoteScript, NoteTag};
13use miden_protocol::transaction::ProvenTransaction;
14
15use super::domain::account::{AccountProof, GetAccountRequest};
16use super::domain::account_vault::AccountVaultInfo;
17use super::domain::note::{CommittedNote, FetchedNote, SyncNotesBlock};
18use super::domain::nullifier::NullifierUpdate;
19use super::domain::storage_map::StorageMapInfo;
20use super::domain::sync::{ChainMmrInfo, SyncTarget};
21use super::domain::transaction::TransactionRecord;
22use super::encryption::{AttestedTransactionEncryptionKey, SealedTransactionInputs};
23use super::{
24 AccountStateAt,
25 NetworkNoteStatusInfo,
26 NodeRpcClient,
27 RpcError,
28 RpcLimits,
29 RpcStatusInfo,
30};
31
32fn verify_block_num(requested: Option<BlockNumber>, returned: BlockNumber) -> Result<(), RpcError> {
37 if let Some(requested) = requested
38 && returned != requested
39 {
40 return Err(RpcError::InvalidResponse(format!(
41 "node returned block {returned} but block {requested} was requested"
42 )));
43 }
44 Ok(())
45}
46
47fn verify_note_ids(
49 requested: &BTreeSet<NoteId>,
50 returned: impl IntoIterator<Item = NoteId>,
51) -> Result<(), RpcError> {
52 for id in returned {
53 if !requested.contains(&id) {
54 let list = requested.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ");
55 return Err(RpcError::InvalidResponse(format!(
56 "node returned note {id} but [{list}] were requested"
57 )));
58 }
59 }
60 Ok(())
61}
62
63fn verify_note_tags(
65 requested: &BTreeSet<NoteTag>,
66 returned: impl IntoIterator<Item = NoteTag>,
67) -> Result<(), RpcError> {
68 for tag in returned {
69 if !requested.contains(&tag) {
70 let list = requested.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ");
71 return Err(RpcError::InvalidResponse(format!(
72 "node returned note with tag {tag} but [{list}] were requested"
73 )));
74 }
75 }
76 Ok(())
77}
78
79fn verify_nullifier_prefixes(
82 requested_prefixes: &BTreeSet<u16>,
83 batch: &[NullifierUpdate],
84) -> Result<(), RpcError> {
85 for update in batch {
86 let prefix = update.nullifier.prefix();
87 if !requested_prefixes.contains(&prefix) {
88 let requested = requested_prefixes
89 .iter()
90 .map(ToString::to_string)
91 .collect::<Vec<_>>()
92 .join(", ");
93 return Err(RpcError::InvalidResponse(format!(
94 "node returned nullifier with prefix {prefix} but [{requested}] were requested"
95 )));
96 }
97 }
98 Ok(())
99}
100
101fn verify_account_ids(
104 requested: &BTreeSet<AccountId>,
105 records: &[TransactionRecord],
106) -> Result<(), RpcError> {
107 for record in records {
108 let id = record.transaction_header.account_id();
109 if !requested.contains(&id) {
110 let list = requested.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ");
111 return Err(RpcError::InvalidResponse(format!(
112 "node returned transaction for account {id} but [{list}] were requested"
113 )));
114 }
115 }
116 Ok(())
117}
118
119fn verify_note_script_root(requested: Word, script: &NoteScript) -> Result<(), RpcError> {
121 let fetched_root = script.root();
122 if Word::from(fetched_root) != requested {
123 return Err(RpcError::InvalidResponse(format!(
124 "node returned note script with root {fetched_root} for requested root {requested}"
125 )));
126 }
127 Ok(())
128}
129
130pub struct VerifyingRpcClient<T>(T);
153
154impl<T: NodeRpcClient> VerifyingRpcClient<T> {
155 pub fn new(client: T) -> Self {
157 Self(client)
158 }
159}
160
161#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
162#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
163impl<T: NodeRpcClient> NodeRpcClient for VerifyingRpcClient<T> {
164 async fn set_genesis_commitment(&self, commitment: Word) -> Result<(), RpcError> {
165 self.0.set_genesis_commitment(commitment).await
166 }
167
168 fn has_genesis_commitment(&self) -> Option<Word> {
169 self.0.has_genesis_commitment()
170 }
171
172 async fn get_transaction_encryption_key(
173 &self,
174 ) -> Result<AttestedTransactionEncryptionKey, RpcError> {
175 self.0.get_transaction_encryption_key().await
179 }
180
181 async fn submit_proven_transaction(
182 &self,
183 proven_transaction: ProvenTransaction,
184 sealed_transaction_inputs: SealedTransactionInputs,
185 ) -> Result<BlockNumber, RpcError> {
186 self.0
187 .submit_proven_transaction(proven_transaction, sealed_transaction_inputs)
188 .await
189 }
190
191 async fn submit_proven_batch(
192 &self,
193 proven_batch: ProvenBatch,
194 proposed_batch: ProposedBatch,
195 sealed_transaction_inputs: Vec<SealedTransactionInputs>,
196 ) -> Result<BlockNumber, RpcError> {
197 self.0
198 .submit_proven_batch(proven_batch, proposed_batch, sealed_transaction_inputs)
199 .await
200 }
201
202 async fn get_block_header_by_number(
203 &self,
204 block_num: Option<BlockNumber>,
205 include_mmr_proof: bool,
206 ) -> Result<(BlockHeader, Option<MmrProof>), RpcError> {
207 let (header, mmr_proof) =
208 self.0.get_block_header_by_number(block_num, include_mmr_proof).await?;
209 verify_block_num(block_num, header.block_num())?;
210 Ok((header, mmr_proof))
211 }
212
213 async fn get_block_by_number(
214 &self,
215 block_num: BlockNumber,
216 include_proof: bool,
217 ) -> Result<ProvenBlock, RpcError> {
218 let block = self.0.get_block_by_number(block_num, include_proof).await?;
219 verify_block_num(Some(block_num), block.header().block_num())?;
220 Ok(block)
221 }
222
223 async fn get_notes_by_id(&self, note_ids: &[NoteId]) -> Result<Vec<FetchedNote>, RpcError> {
224 let notes = self.0.get_notes_by_id(note_ids).await?;
225 let requested: BTreeSet<NoteId> = note_ids.iter().copied().collect();
226 verify_note_ids(&requested, notes.iter().map(FetchedNote::id))?;
227 Ok(notes)
228 }
229
230 async fn sync_chain_mmr(
231 &self,
232 current_block_height: BlockNumber,
233 upper_bound: SyncTarget,
234 ) -> Result<ChainMmrInfo, RpcError> {
235 self.0.sync_chain_mmr(current_block_height, upper_bound).await
236 }
237
238 async fn sync_notes(
239 &self,
240 block_from: BlockNumber,
241 block_to: BlockNumber,
242 note_tags: &BTreeSet<NoteTag>,
243 ) -> Result<Vec<SyncNotesBlock>, RpcError> {
244 let blocks = self.0.sync_notes(block_from, block_to, note_tags).await?;
245 verify_note_tags(
246 note_tags,
247 blocks.iter().flat_map(|block| block.notes.values().map(CommittedNote::tag)),
248 )?;
249 Ok(blocks)
250 }
251
252 async fn sync_nullifiers(
253 &self,
254 prefix: &[u16],
255 block_from: BlockNumber,
256 block_to: BlockNumber,
257 ) -> Result<Vec<NullifierUpdate>, RpcError> {
258 let nullifiers = self.0.sync_nullifiers(prefix, block_from, block_to).await?;
259 let requested: BTreeSet<u16> = prefix.iter().copied().collect();
260 verify_nullifier_prefixes(&requested, &nullifiers)?;
261 Ok(nullifiers)
262 }
263
264 async fn get_account(
265 &self,
266 account_id: AccountId,
267 request: GetAccountRequest,
268 ) -> Result<(BlockNumber, AccountProof), RpcError> {
269 let requested = match request.at {
270 AccountStateAt::Block(number) => Some(number),
271 AccountStateAt::ChainTip => None,
272 };
273 let (block_num, proof) = self.0.get_account(account_id, request).await?;
274 verify_block_num(requested, block_num)?;
275 if proof.account_id() != account_id {
276 return Err(RpcError::InvalidResponse(format!(
277 "node returned proof for account {} but {} was requested",
278 proof.account_id(),
279 account_id,
280 )));
281 }
282 Ok((block_num, proof))
283 }
284
285 async fn get_note_script_by_root(&self, root: Word) -> Result<Option<NoteScript>, RpcError> {
286 let script = self.0.get_note_script_by_root(root).await?;
287 if let Some(script) = &script {
288 verify_note_script_root(root, script)?;
289 }
290 Ok(script)
291 }
292
293 async fn sync_storage_maps(
294 &self,
295 block_from: BlockNumber,
296 block_to: BlockNumber,
297 account_id: AccountId,
298 ) -> Result<StorageMapInfo, RpcError> {
299 self.0.sync_storage_maps(block_from, block_to, account_id).await
300 }
301
302 async fn sync_account_vault(
303 &self,
304 block_from: BlockNumber,
305 block_to: BlockNumber,
306 account_id: AccountId,
307 ) -> Result<AccountVaultInfo, RpcError> {
308 self.0.sync_account_vault(block_from, block_to, account_id).await
309 }
310
311 async fn sync_transactions(
312 &self,
313 block_from: BlockNumber,
314 block_to: BlockNumber,
315 account_ids: Vec<AccountId>,
316 ) -> Result<Vec<TransactionRecord>, RpcError> {
317 let requested: BTreeSet<AccountId> = account_ids.iter().copied().collect();
318 let records = self.0.sync_transactions(block_from, block_to, account_ids).await?;
319 verify_account_ids(&requested, &records)?;
320 Ok(records)
321 }
322
323 async fn get_network_id(&self) -> Result<NetworkId, RpcError> {
324 self.0.get_network_id().await
325 }
326
327 async fn get_rpc_limits(&self) -> Result<RpcLimits, RpcError> {
328 self.0.get_rpc_limits().await
329 }
330
331 fn has_rpc_limits(&self) -> Option<RpcLimits> {
332 self.0.has_rpc_limits()
333 }
334
335 async fn set_rpc_limits(&self, limits: RpcLimits) {
336 self.0.set_rpc_limits(limits).await;
337 }
338
339 async fn get_status_unversioned(&self) -> Result<RpcStatusInfo, RpcError> {
340 self.0.get_status_unversioned().await
341 }
342
343 async fn get_network_note_status(
344 &self,
345 note_id: NoteId,
346 ) -> Result<NetworkNoteStatusInfo, RpcError> {
347 self.0.get_network_note_status(note_id).await
348 }
349}
350
351#[cfg(test)]
352mod tests;