use alloc::boxed::Box;
use alloc::collections::BTreeSet;
use alloc::string::ToString;
use alloc::vec::Vec;
use miden_protocol::Word;
use miden_protocol::account::AccountId;
use miden_protocol::address::NetworkId;
use miden_protocol::batch::{ProposedBatch, ProvenBatch};
use miden_protocol::block::{BlockHeader, BlockNumber, ProvenBlock};
use miden_protocol::crypto::merkle::mmr::MmrProof;
use miden_protocol::note::{NoteId, NoteScript, NoteTag};
use miden_protocol::transaction::ProvenTransaction;
use super::domain::account::{AccountProof, GetAccountRequest};
use super::domain::account_vault::AccountVaultInfo;
use super::domain::note::{CommittedNote, FetchedNote, SyncNotesBlock};
use super::domain::nullifier::NullifierUpdate;
use super::domain::storage_map::StorageMapInfo;
use super::domain::sync::{ChainMmrInfo, SyncTarget};
use super::domain::transaction::TransactionRecord;
use super::encryption::{AttestedTransactionEncryptionKey, SealedTransactionInputs};
use super::{
AccountStateAt,
NetworkNoteStatusInfo,
NodeRpcClient,
RpcError,
RpcLimits,
RpcStatusInfo,
};
fn verify_block_num(requested: Option<BlockNumber>, returned: BlockNumber) -> Result<(), RpcError> {
if let Some(requested) = requested
&& returned != requested
{
return Err(RpcError::InvalidResponse(format!(
"node returned block {returned} but block {requested} was requested"
)));
}
Ok(())
}
fn verify_note_ids(
requested: &BTreeSet<NoteId>,
returned: impl IntoIterator<Item = NoteId>,
) -> Result<(), RpcError> {
for id in returned {
if !requested.contains(&id) {
let list = requested.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ");
return Err(RpcError::InvalidResponse(format!(
"node returned note {id} but [{list}] were requested"
)));
}
}
Ok(())
}
fn verify_note_tags(
requested: &BTreeSet<NoteTag>,
returned: impl IntoIterator<Item = NoteTag>,
) -> Result<(), RpcError> {
for tag in returned {
if !requested.contains(&tag) {
let list = requested.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ");
return Err(RpcError::InvalidResponse(format!(
"node returned note with tag {tag} but [{list}] were requested"
)));
}
}
Ok(())
}
fn verify_nullifier_prefixes(
requested_prefixes: &BTreeSet<u16>,
batch: &[NullifierUpdate],
) -> Result<(), RpcError> {
for update in batch {
let prefix = update.nullifier.prefix();
if !requested_prefixes.contains(&prefix) {
let requested = requested_prefixes
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ");
return Err(RpcError::InvalidResponse(format!(
"node returned nullifier with prefix {prefix} but [{requested}] were requested"
)));
}
}
Ok(())
}
fn verify_account_ids(
requested: &BTreeSet<AccountId>,
records: &[TransactionRecord],
) -> Result<(), RpcError> {
for record in records {
let id = record.transaction_header.account_id();
if !requested.contains(&id) {
let list = requested.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ");
return Err(RpcError::InvalidResponse(format!(
"node returned transaction for account {id} but [{list}] were requested"
)));
}
}
Ok(())
}
fn verify_note_script_root(requested: Word, script: &NoteScript) -> Result<(), RpcError> {
let fetched_root = script.root();
if Word::from(fetched_root) != requested {
return Err(RpcError::InvalidResponse(format!(
"node returned note script with root {fetched_root} for requested root {requested}"
)));
}
Ok(())
}
pub struct VerifyingRpcClient<T>(T);
impl<T: NodeRpcClient> VerifyingRpcClient<T> {
pub fn new(client: T) -> Self {
Self(client)
}
}
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
impl<T: NodeRpcClient> NodeRpcClient for VerifyingRpcClient<T> {
async fn set_genesis_commitment(&self, commitment: Word) -> Result<(), RpcError> {
self.0.set_genesis_commitment(commitment).await
}
fn has_genesis_commitment(&self) -> Option<Word> {
self.0.has_genesis_commitment()
}
async fn get_transaction_encryption_key(
&self,
) -> Result<AttestedTransactionEncryptionKey, RpcError> {
self.0.get_transaction_encryption_key().await
}
async fn submit_proven_transaction(
&self,
proven_transaction: ProvenTransaction,
sealed_transaction_inputs: SealedTransactionInputs,
) -> Result<BlockNumber, RpcError> {
self.0
.submit_proven_transaction(proven_transaction, sealed_transaction_inputs)
.await
}
async fn submit_proven_batch(
&self,
proven_batch: ProvenBatch,
proposed_batch: ProposedBatch,
sealed_transaction_inputs: Vec<SealedTransactionInputs>,
) -> Result<BlockNumber, RpcError> {
self.0
.submit_proven_batch(proven_batch, proposed_batch, sealed_transaction_inputs)
.await
}
async fn get_block_header_by_number(
&self,
block_num: Option<BlockNumber>,
include_mmr_proof: bool,
) -> Result<(BlockHeader, Option<MmrProof>), RpcError> {
let (header, mmr_proof) =
self.0.get_block_header_by_number(block_num, include_mmr_proof).await?;
verify_block_num(block_num, header.block_num())?;
Ok((header, mmr_proof))
}
async fn get_block_by_number(
&self,
block_num: BlockNumber,
include_proof: bool,
) -> Result<ProvenBlock, RpcError> {
let block = self.0.get_block_by_number(block_num, include_proof).await?;
verify_block_num(Some(block_num), block.header().block_num())?;
Ok(block)
}
async fn get_notes_by_id(&self, note_ids: &[NoteId]) -> Result<Vec<FetchedNote>, RpcError> {
let notes = self.0.get_notes_by_id(note_ids).await?;
let requested: BTreeSet<NoteId> = note_ids.iter().copied().collect();
verify_note_ids(&requested, notes.iter().map(FetchedNote::id))?;
Ok(notes)
}
async fn sync_chain_mmr(
&self,
current_block_height: BlockNumber,
upper_bound: SyncTarget,
) -> Result<ChainMmrInfo, RpcError> {
self.0.sync_chain_mmr(current_block_height, upper_bound).await
}
async fn sync_notes(
&self,
block_from: BlockNumber,
block_to: BlockNumber,
note_tags: &BTreeSet<NoteTag>,
) -> Result<Vec<SyncNotesBlock>, RpcError> {
let blocks = self.0.sync_notes(block_from, block_to, note_tags).await?;
verify_note_tags(
note_tags,
blocks.iter().flat_map(|block| block.notes.values().map(CommittedNote::tag)),
)?;
Ok(blocks)
}
async fn sync_nullifiers(
&self,
prefix: &[u16],
block_from: BlockNumber,
block_to: BlockNumber,
) -> Result<Vec<NullifierUpdate>, RpcError> {
let nullifiers = self.0.sync_nullifiers(prefix, block_from, block_to).await?;
let requested: BTreeSet<u16> = prefix.iter().copied().collect();
verify_nullifier_prefixes(&requested, &nullifiers)?;
Ok(nullifiers)
}
async fn get_account(
&self,
account_id: AccountId,
request: GetAccountRequest,
) -> Result<(BlockNumber, AccountProof), RpcError> {
let requested = match request.at {
AccountStateAt::Block(number) => Some(number),
AccountStateAt::ChainTip => None,
};
let (block_num, proof) = self.0.get_account(account_id, request).await?;
verify_block_num(requested, block_num)?;
Ok((block_num, proof))
}
async fn get_note_script_by_root(&self, root: Word) -> Result<Option<NoteScript>, RpcError> {
let script = self.0.get_note_script_by_root(root).await?;
if let Some(script) = &script {
verify_note_script_root(root, script)?;
}
Ok(script)
}
async fn sync_storage_maps(
&self,
block_from: BlockNumber,
block_to: BlockNumber,
account_id: AccountId,
) -> Result<StorageMapInfo, RpcError> {
self.0.sync_storage_maps(block_from, block_to, account_id).await
}
async fn sync_account_vault(
&self,
block_from: BlockNumber,
block_to: BlockNumber,
account_id: AccountId,
) -> Result<AccountVaultInfo, RpcError> {
self.0.sync_account_vault(block_from, block_to, account_id).await
}
async fn sync_transactions(
&self,
block_from: BlockNumber,
block_to: BlockNumber,
account_ids: Vec<AccountId>,
) -> Result<Vec<TransactionRecord>, RpcError> {
let requested: BTreeSet<AccountId> = account_ids.iter().copied().collect();
let records = self.0.sync_transactions(block_from, block_to, account_ids).await?;
verify_account_ids(&requested, &records)?;
Ok(records)
}
async fn get_network_id(&self) -> Result<NetworkId, RpcError> {
self.0.get_network_id().await
}
async fn get_rpc_limits(&self) -> Result<RpcLimits, RpcError> {
self.0.get_rpc_limits().await
}
fn has_rpc_limits(&self) -> Option<RpcLimits> {
self.0.has_rpc_limits()
}
async fn set_rpc_limits(&self, limits: RpcLimits) {
self.0.set_rpc_limits(limits).await;
}
async fn get_status_unversioned(&self) -> Result<RpcStatusInfo, RpcError> {
self.0.get_status_unversioned().await
}
async fn get_network_note_status(
&self,
note_id: NoteId,
) -> Result<NetworkNoteStatusInfo, RpcError> {
self.0.get_network_note_status(note_id).await
}
}
#[cfg(test)]
mod tests;