Skip to main content

miden_client/rpc/verifying_client/
mod.rs

1use 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
33// RESPONSE VERIFICATION HELPERS
34// ================================================================================================
35
36/// Returns [`RpcError::InvalidResponse`] if `requested` is `Some` and `returned` does not equal it.
37fn 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
48/// Returns [`RpcError::InvalidResponse`] if any returned note ID was not in `requested`.
49fn 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
64/// Returns [`RpcError::InvalidResponse`] if any returned note tag was not in `requested`.
65fn 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
80/// Returns [`RpcError::InvalidResponse`] if any update carries a nullifier whose prefix was not in
81/// `requested_prefixes`, or a block number outside the inclusive `[block_from, block_to]` window.
82fn 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
112/// Returns [`RpcError::InvalidResponse`] if any returned transaction record carries an account ID
113/// that was not in `requested`.
114fn 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
130/// Returns [`RpcError::InvalidResponse`] if `script`'s root does not equal the `requested` root.
131fn 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
141// VERIFYING RPC CLIENT
142// ================================================================================================
143
144/// A [`NodeRpcClient`] wrapper that verifies that responses correspond to the method's arguments,
145/// rejecting mismatches with [`RpcError::InvalidResponse`]:
146///
147/// - [`get_block_header_by_number`](NodeRpcClient::get_block_header_by_number) and
148///   [`get_block_by_number`](NodeRpcClient::get_block_by_number): the returned block's number must
149///   match the requested one.
150/// - [`get_notes_by_id`](NodeRpcClient::get_notes_by_id): every returned note's ID must have been
151///   requested.
152/// - [`sync_notes`](NodeRpcClient::sync_notes): every returned note's tag must have been requested.
153/// - [`sync_nullifiers`](NodeRpcClient::sync_nullifiers): every returned nullifier's prefix must
154///   have been requested, and its block number must fall in the requested window.
155/// - [`get_account`](NodeRpcClient::get_account): when the state at a specific block was requested,
156///   the response must be for that block.
157/// - [`get_note_script_by_root`](NodeRpcClient::get_note_script_by_root): a returned script's root
158///   must match the requested one.
159/// - [`sync_transactions`](NodeRpcClient::sync_transactions): every returned transaction record's
160///   account ID must have been requested.
161///
162/// All other methods delegate to the wrapped client unchanged.
163pub struct VerifyingRpcClient<T>(T);
164
165impl<T: NodeRpcClient> VerifyingRpcClient<T> {
166    /// Wraps `client` so that its responses are verified against the request.
167    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        // Nothing to verify here: the request carries no payload to check the response against, and
187        // trust in the served key comes from the validator attestation, which the caller verifies
188        // via `AttestedTransactionEncryptionKey::verify`.
189        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        // Nothing to verify here: a successful response carries no payload to check the request
302        // against.
303        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;