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, 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
32// RESPONSE VERIFICATION HELPERS
33// ================================================================================================
34
35/// Returns [`RpcError::InvalidResponse`] if `requested` is `Some` and `returned` does not equal it.
36fn 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
47/// Returns [`RpcError::InvalidResponse`] if any returned note ID was not in `requested`.
48fn 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
63/// Returns [`RpcError::InvalidResponse`] if any returned note tag was not in `requested`.
64fn 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
79/// Returns [`RpcError::InvalidResponse`] if any update carries a nullifier whose prefix was not in
80/// `requested_prefixes`.
81fn 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
101/// Returns [`RpcError::InvalidResponse`] if any returned transaction record carries an account ID
102/// that was not in `requested`.
103fn 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
119/// Returns [`RpcError::InvalidResponse`] if `script`'s root does not equal the `requested` root.
120fn 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
130// VERIFYING RPC CLIENT
131// ================================================================================================
132
133/// A [`NodeRpcClient`] wrapper that verifies that responses correspond to the method's arguments,
134/// rejecting mismatches with [`RpcError::InvalidResponse`]:
135///
136/// - [`get_block_header_by_number`](NodeRpcClient::get_block_header_by_number) and
137///   [`get_block_by_number`](NodeRpcClient::get_block_by_number): the returned block's number must
138///   match the requested one.
139/// - [`get_notes_by_id`](NodeRpcClient::get_notes_by_id): every returned note's ID must have been
140///   requested.
141/// - [`sync_notes`](NodeRpcClient::sync_notes): every returned note's tag must have been requested.
142/// - [`sync_nullifiers`](NodeRpcClient::sync_nullifiers): every returned nullifier's prefix must
143///   have been requested.
144/// - [`get_account`](NodeRpcClient::get_account): when the state at a specific block was requested,
145///   the response must be for that block.
146/// - [`get_note_script_by_root`](NodeRpcClient::get_note_script_by_root): a returned script's root
147///   must match the requested one.
148/// - [`sync_transactions`](NodeRpcClient::sync_transactions): every returned transaction record's
149///   account ID must have been requested.
150///
151/// All other methods delegate to the wrapped client unchanged.
152pub struct VerifyingRpcClient<T>(T);
153
154impl<T: NodeRpcClient> VerifyingRpcClient<T> {
155    /// Wraps `client` so that its responses are verified against the request.
156    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        // Nothing to verify here: the request carries no payload to check the response against,
176        // and trust in the served key comes from the validator attestation, which the caller
177        // verifies via `AttestedTransactionEncryptionKey::verify`.
178        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;