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