diff --git a/crates/rpc/src/server/api.rs b/crates/rpc/src/server/api.rs index 59ffffb0c..be6455359 100644 --- a/crates/rpc/src/server/api.rs +++ b/crates/rpc/src/server/api.rs @@ -1,4 +1,12 @@ -use std::sync::Arc; +//! RPC API server implementation. +//! +//! This module provides the main RPC service that handles all API requests. +//! It delegates to various backend services (store, block producer, validator) +//! and performs validation and processing of requests. + +// IMPORTS +// ================================================================================================ + use std::time::Duration; use anyhow::Context; @@ -10,17 +18,13 @@ use miden_node_proto::generated::{self as proto}; use miden_node_proto::try_convert; use miden_node_utils::ErrorReport; use miden_node_utils::limiter::{ - QueryParamAccountIdLimit, - QueryParamLimiter, - QueryParamNoteIdLimit, - QueryParamNoteTagLimit, + QueryParamAccountIdLimit, QueryParamLimiter, QueryParamNoteIdLimit, QueryParamNoteTagLimit, QueryParamNullifierLimit, }; use miden_objects::account::AccountId; use miden_objects::batch::ProvenBatch; use miden_objects::block::{BlockHeader, BlockNumber}; -use miden_objects::note::{Note, NoteRecipient, NoteScript}; -use miden_objects::transaction::{OutputNote, ProvenTransaction, ProvenTransactionBuilder}; +use miden_objects::transaction::ProvenTransaction; use miden_objects::utils::serde::{Deserializable, Serializable}; use miden_objects::{MIN_PROOF_SECURITY_LEVEL, Word}; use miden_tx::TransactionVerifier; @@ -29,10 +33,20 @@ use tracing::{debug, info, instrument, warn}; use url::Url; use crate::COMPONENT; +use crate::server::transaction_helpers::{ + rebuild_batch_without_decorators, rebuild_transaction_without_decorators, + validate_batch_network_account_restrictions, validate_network_account_restriction, +}; // RPC SERVICE // ================================================================================================ +/// Main RPC service that handles all API requests. +/// +/// The service maintains connections to: +/// - Store: For querying blockchain state +/// - Block Producer: For submitting transactions (optional, for read-only mode) +/// - Validator: For validating transactions pub struct RpcService { store: StoreRpcClient, block_producer: Option, @@ -41,6 +55,9 @@ pub struct RpcService { } impl RpcService { + /// Creates a new RPC service with connections to backend services. + /// + /// All connections are established lazily and will be created when first used. pub(super) fn new(store_url: Url, block_producer_url: Option, validator_url: Url) -> Self { let store = { info!(target: COMPONENT, store_endpoint = %store_url, "Initializing store client"); @@ -106,6 +123,7 @@ impl RpcService { /// Fetches the genesis block header from the store. /// /// Automatically retries until the store connection becomes available. + /// Uses exponential backoff with a base delay of 500ms and maximum delay of 30s. pub async fn get_genesis_header_with_retry(&self) -> anyhow::Result { let mut retry_counter = 0; loop { @@ -152,8 +170,18 @@ impl RpcService { } } +// API IMPLEMENTATION +// ================================================================================================ + #[tonic::async_trait] impl api_server::Api for RpcService { + // NULLIFIER OPERATIONS + // -------------------------------------------------------------------------------------------- + + /// Checks whether the provided nullifiers have been consumed. + /// + /// Validates that all nullifiers are within the modulus range and checks + /// their consumption status via the store. #[instrument( parent = None, target = COMPONENT, @@ -170,7 +198,7 @@ impl api_server::Api for RpcService { check::(request.get_ref().nullifiers.len())?; - // validate all the nullifiers from the user request + // Validate all the nullifiers from the user request for nullifier in &request.get_ref().nullifiers { let _: Word = nullifier .try_into() @@ -180,6 +208,9 @@ impl api_server::Api for RpcService { self.store.clone().check_nullifiers(request).await } + /// Synchronizes nullifiers with the store. + /// + /// Used to check which nullifiers from the provided list have been consumed. #[instrument( parent = None, target = COMPONENT, @@ -199,6 +230,12 @@ impl api_server::Api for RpcService { self.store.clone().sync_nullifiers(request).await } + // BLOCK OPERATIONS + // -------------------------------------------------------------------------------------------- + + /// Retrieves a block header by its block number. + /// + /// Optionally includes an MMR (Merkle Mountain Range) proof if requested. #[instrument( parent = None, target = COMPONENT, @@ -216,62 +253,107 @@ impl api_server::Api for RpcService { self.store.clone().get_block_header_by_number(request).await } + /// Retrieves a full block by its block number. + /// + /// Returns `None` if the block doesn't exist. #[instrument( parent = None, target = COMPONENT, - name = "rpc.server.sync_state", + name = "rpc.server.get_block_by_number", skip_all, ret(level = "debug"), err )] - async fn sync_state( + async fn get_block_by_number( &self, - request: Request, - ) -> Result, Status> { - debug!(target: COMPONENT, request = ?request.get_ref()); + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); - check::(request.get_ref().account_ids.len())?; - check::(request.get_ref().note_tags.len())?; + debug!(target: COMPONENT, ?request); - self.store.clone().sync_state(request).await + self.store.clone().get_block_by_number(request).await } + // ACCOUNT OPERATIONS + // -------------------------------------------------------------------------------------------- + + /// Returns details for a public account by its ID. + /// + /// Validates the account ID format before querying the store. #[instrument( parent = None, target = COMPONENT, - name = "rpc.server.sync_storage_maps", + name = "rpc.server.get_account_details", skip_all, ret(level = "debug"), err )] - async fn sync_storage_maps( + async fn get_account_details( &self, - request: Request, - ) -> Result, Status> { + request: Request, + ) -> std::result::Result, Status> { debug!(target: COMPONENT, request = ?request.get_ref()); - self.store.clone().sync_storage_maps(request).await + // Validate account ID using conversion + let _account_id: AccountId = request + .get_ref() + .clone() + .try_into() + .map_err(|err| Status::invalid_argument(format!("Invalid account id: {err}")))?; + + self.store.clone().get_account_details(request).await } + /// Retrieves an account proof for the specified account. + /// + /// The proof can be used to verify the account's state in the blockchain. #[instrument( parent = None, target = COMPONENT, - name = "rpc.server.sync_notes", + name = "rpc.server.get_account_proof", skip_all, ret(level = "debug"), err )] - async fn sync_notes( + async fn get_account_proof( &self, - request: Request, - ) -> Result, Status> { - debug!(target: COMPONENT, request = ?request.get_ref()); + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); - check::(request.get_ref().note_tags.len())?; + debug!(target: COMPONENT, ?request); - self.store.clone().sync_notes(request).await + self.store.clone().get_account_proof(request).await + } + + /// Synchronizes account vault state. + /// + /// Used to fetch the current state of an account's vault. + #[instrument( + parent = None, + target = COMPONENT, + name = "rpc.server.sync_account_vault", + skip_all, + ret(level = "debug"), + err + )] + async fn sync_account_vault( + &self, + request: tonic::Request, + ) -> std::result::Result, tonic::Status> + { + debug!(target: COMPONENT, request = ?request.get_ref()); + + self.store.clone().sync_account_vault(request).await } + // NOTE OPERATIONS + // -------------------------------------------------------------------------------------------- + + /// Retrieves notes by their IDs. + /// + /// Validates that all note IDs are in the correct format before querying. #[instrument( parent = None, target = COMPONENT, @@ -288,7 +370,7 @@ impl api_server::Api for RpcService { check::(request.get_ref().ids.len())?; - // Validation checking for correct NoteId's + // Validate all note IDs are in correct format let note_ids = request.get_ref().ids.clone(); let _: Vec = @@ -301,25 +383,134 @@ impl api_server::Api for RpcService { self.store.clone().get_notes_by_id(request).await } + /// Retrieves a note script by its root hash. + /// + /// The script root uniquely identifies a note script in the system. #[instrument( parent = None, target = COMPONENT, - name = "rpc.server.sync_account_vault", + name = "rpc.server.get_note_script_by_root", skip_all, ret(level = "debug"), err )] - async fn sync_account_vault( + async fn get_note_script_by_root( &self, - request: tonic::Request, - ) -> std::result::Result, tonic::Status> - { + request: Request, + ) -> Result, Status> { + debug!(target: COMPONENT, request = ?request); + + self.store.clone().get_note_script_by_root(request).await + } + + // SYNC OPERATIONS + // -------------------------------------------------------------------------------------------- + + /// Synchronizes state for accounts and notes. + /// + /// Fetches the latest state for the specified account IDs and note tags. + #[instrument( + parent = None, + target = COMPONENT, + name = "rpc.server.sync_state", + skip_all, + ret(level = "debug"), + err + )] + async fn sync_state( + &self, + request: Request, + ) -> Result, Status> { debug!(target: COMPONENT, request = ?request.get_ref()); - self.store.clone().sync_account_vault(request).await + check::(request.get_ref().account_ids.len())?; + check::(request.get_ref().note_tags.len())?; + + self.store.clone().sync_state(request).await + } + + /// Synchronizes storage maps for accounts. + /// + /// Used to fetch the latest storage map state for accounts. + #[instrument( + parent = None, + target = COMPONENT, + name = "rpc.server.sync_storage_maps", + skip_all, + ret(level = "debug"), + err + )] + async fn sync_storage_maps( + &self, + request: Request, + ) -> Result, Status> { + debug!(target: COMPONENT, request = ?request.get_ref()); + + self.store.clone().sync_storage_maps(request).await } - #[instrument(parent = None, target = COMPONENT, name = "rpc.server.submit_proven_transaction", skip_all, err)] + /// Synchronizes notes by their tags. + /// + /// Fetches all notes matching the specified tags. + #[instrument( + parent = None, + target = COMPONENT, + name = "rpc.server.sync_notes", + skip_all, + ret(level = "debug"), + err + )] + async fn sync_notes( + &self, + request: Request, + ) -> Result, Status> { + debug!(target: COMPONENT, request = ?request.get_ref()); + + check::(request.get_ref().note_tags.len())?; + + self.store.clone().sync_notes(request).await + } + + /// Synchronizes transactions. + /// + /// Fetches transactions based on the provided criteria. + #[instrument( + parent = None, + target = COMPONENT, + name = "rpc.server.sync_transactions", + skip_all, + ret(level = "debug"), + err + )] + async fn sync_transactions( + &self, + request: Request, + ) -> Result, Status> { + debug!(target: COMPONENT, request = ?request); + + self.store.clone().sync_transactions(request).await + } + + // TRANSACTION SUBMISSION + // -------------------------------------------------------------------------------------------- + + /// Submits a proven transaction to the network. + /// + /// This method: + /// 1. Validates the transaction format and proof + /// 2. Strips decorators from output notes + /// 3. Validates network account restrictions + /// 4. Optionally re-executes the transaction if inputs are provided + /// 5. Submits to the block producer + /// + /// Returns an error if the service is in read-only mode (no block producer). + #[instrument( + parent = None, + target = COMPONENT, + name = "rpc.server.submit_proven_transaction", + skip_all, + err + )] async fn submit_proven_transaction( &self, request: Request, @@ -334,51 +525,22 @@ impl api_server::Api for RpcService { let request = request.into_inner(); + // Parse and validate the transaction let tx = ProvenTransaction::read_from_bytes(&request.transaction).map_err(|err| { Status::invalid_argument(err.as_report_context("invalid transaction")) })?; - // Rebuild a new ProvenTransaction with decorators removed from output notes - let mut builder = ProvenTransactionBuilder::new( - tx.account_id(), - tx.account_update().initial_state_commitment(), - tx.account_update().final_state_commitment(), - tx.account_update().account_delta_commitment(), - tx.ref_block_num(), - tx.ref_block_commitment(), - tx.fee(), - tx.expiration_block_num(), - tx.proof().clone(), - ) - .account_update_details(tx.account_update().details().clone()) - .add_input_notes(tx.input_notes().iter().cloned()); - - let stripped_outputs = tx.output_notes().iter().map(|note| match note { - OutputNote::Full(note) => { - let mut mast = note.script().mast().clone(); - Arc::make_mut(&mut mast).strip_decorators(); - let script = NoteScript::from_parts(mast, note.script().entrypoint()); - let recipient = - NoteRecipient::new(note.serial_num(), script, note.inputs().clone()); - let new_note = Note::new(note.assets().clone(), *note.metadata(), recipient); - OutputNote::Full(new_note) - }, - other => other.clone(), - }); - builder = builder.add_output_notes(stripped_outputs); - let rebuilt_tx = builder.build().map_err(|e| Status::invalid_argument(e.to_string()))?; + // Rebuild transaction with decorators stripped from output notes + let rebuilt_tx = + rebuild_transaction_without_decorators(&tx).map_err(Status::invalid_argument)?; + let mut request = request; request.transaction = rebuilt_tx.to_bytes(); - // Only allow deployment transactions for new network accounts - if tx.account_id().is_network() - && !tx.account_update().initial_state_commitment().is_empty() - { - return Err(Status::invalid_argument( - "Network transactions may not be submitted by users yet", - )); - } + // Validate network account restrictions + validate_network_account_restriction(&tx).map_err(Status::invalid_argument)?; + // Verify the transaction proof let tx_verifier = TransactionVerifier::new(MIN_PROOF_SECURITY_LEVEL); tx_verifier.verify(&tx).map_err(|err| { @@ -389,20 +551,20 @@ impl api_server::Api for RpcService { )) })?; - // If transaction inputs are provided, re-execute the transaction to validate it. + // If transaction inputs are provided, re-execute the transaction to validate it if request.transaction_inputs.is_some() { - // Re-execute the transaction via the Validator. + // Re-execute the transaction via the Validator match self.validator.clone().submit_proven_transaction(request.clone()).await { Ok(_) => { debug!( - target = COMPONENT, + target: COMPONENT, tx_id = %tx.id().to_hex(), "Transaction validation successful" ); }, Err(e) => { warn!( - target = COMPONENT, + target: COMPONENT, tx_id = %tx.id().to_hex(), error = %e, "Transaction validation failed, but continuing with submission" @@ -414,7 +576,22 @@ impl api_server::Api for RpcService { block_producer.clone().submit_proven_transaction(request).await } - #[instrument(parent = None, target = COMPONENT, name = "rpc.server.submit_proven_batch", skip_all, err)] + /// Submits a proven batch of transactions to the network. + /// + /// This method: + /// 1. Validates the batch format + /// 2. Strips decorators from output notes + /// 3. Validates network account restrictions for all transactions + /// 4. Submits to the block producer + /// + /// Returns an error if the service is in read-only mode (no block producer). + #[instrument( + parent = None, + target = COMPONENT, + name = "rpc.server.submit_proven_batch", + skip_all, + err + )] async fn submit_proven_batch( &self, request: tonic::Request, @@ -425,116 +602,32 @@ impl api_server::Api for RpcService { let mut request = request.into_inner(); + // Parse and validate the batch let batch = ProvenBatch::read_from_bytes(&request.encoded) .map_err(|err| Status::invalid_argument(err.as_report_context("invalid batch")))?; - // Build a new batch with output notes' decorators removed - let stripped_outputs: Vec = batch - .output_notes() - .iter() - .map(|note| match note { - OutputNote::Full(note) => { - let mut mast = note.script().mast().clone(); - Arc::make_mut(&mut mast).strip_decorators(); - let script = NoteScript::from_parts(mast, note.script().entrypoint()); - let recipient = - NoteRecipient::new(note.serial_num(), script, note.inputs().clone()); - let new_note = Note::new(note.assets().clone(), *note.metadata(), recipient); - OutputNote::Full(new_note) - }, - other => other.clone(), - }) - .collect(); - - let rebuilt_batch = ProvenBatch::new( - batch.id(), - batch.reference_block_commitment(), - batch.reference_block_num(), - batch.account_updates().clone(), - batch.input_notes().clone(), - stripped_outputs, - batch.batch_expiration_block_num(), - batch.transactions().clone(), - ) - .map_err(|e| Status::invalid_argument(e.to_string()))?; + // Rebuild batch with decorators stripped from output notes + let rebuilt_batch = + rebuild_batch_without_decorators(&batch).map_err(Status::invalid_argument)?; request.encoded = rebuilt_batch.to_bytes(); - // Only allow deployment transactions for new network accounts - for tx in batch.transactions().as_slice() { - if tx.account_id().is_network() && !tx.initial_state_commitment().is_empty() { - return Err(Status::invalid_argument( - "Network transactions may not be submitted by users yet", - )); - } - } + // Validate network account restrictions for all transactions in the batch + validate_batch_network_account_restrictions(&batch).map_err(Status::invalid_argument)?; block_producer.clone().submit_proven_batch(request).await } - /// Returns details for public (public) account by id. - #[instrument( - parent = None, - target = COMPONENT, - name = "rpc.server.get_account_details", - skip_all, - ret(level = "debug"), - err - )] - async fn get_account_details( - &self, - request: Request, - ) -> std::result::Result, Status> { - debug!(target: COMPONENT, request = ?request.get_ref()); - - // Validating account using conversion: - let _account_id: AccountId = request - .get_ref() - .clone() - .try_into() - .map_err(|err| Status::invalid_argument(format!("Invalid account id: {err}")))?; - - self.store.clone().get_account_details(request).await - } - - #[instrument( - parent = None, - target = COMPONENT, - name = "rpc.server.get_block_by_number", - skip_all, - ret(level = "debug"), - err - )] - async fn get_block_by_number( - &self, - request: Request, - ) -> Result, Status> { - let request = request.into_inner(); - - debug!(target: COMPONENT, ?request); - - self.store.clone().get_block_by_number(request).await - } - - #[instrument( - parent = None, - target = COMPONENT, - name = "rpc.server.get_account_proof", - skip_all, - ret(level = "debug"), - err - )] - async fn get_account_proof( - &self, - request: Request, - ) -> Result, Status> { - let request = request.into_inner(); - - debug!(target: COMPONENT, ?request); - - self.store.clone().get_account_proof(request).await - } + // STATUS OPERATIONS + // -------------------------------------------------------------------------------------------- + /// Returns the current status of the RPC service and its dependencies. + /// + /// Includes status information for: + /// - The RPC service itself (version) + /// - The store service + /// - The block producer service (if available) + /// - The genesis commitment #[instrument( parent = None, target = COMPONENT, @@ -578,51 +671,20 @@ impl api_server::Api for RpcService { genesis_commitment: self.genesis_commitment.map(Into::into), })) } - - #[instrument( - parent = None, - target = COMPONENT, - name = "rpc.server.get_note_script_by_root", - skip_all, - ret(level = "debug"), - err - )] - async fn get_note_script_by_root( - &self, - request: Request, - ) -> Result, Status> { - debug!(target: COMPONENT, request = ?request); - - self.store.clone().get_note_script_by_root(request).await - } - - #[instrument( - parent = None, - target = COMPONENT, - name = "rpc.server.sync_transactions", - skip_all, - ret(level = "debug"), - err - )] - async fn sync_transactions( - &self, - request: Request, - ) -> Result, Status> { - debug!(target: COMPONENT, request = ?request); - - self.store.clone().sync_transactions(request).await - } } -// LIMIT HELPERS +// HELPER FUNCTIONS // ================================================================================================ -/// Formats an "Out of range" error +/// Formats an "Out of range" error for limit violations. fn out_of_range_error(err: E) -> Status { Status::out_of_range(err.to_string()) } -/// Check, but don't repeat ourselves mapping the error +/// Checks if a value is within the allowed limit for a query parameter. +/// +/// This is a generic helper that works with any type implementing `QueryParamLimiter`. +/// It maps limit violations to appropriate gRPC status errors. #[allow(clippy::result_large_err)] fn check(n: usize) -> Result<(), Status> { ::check(n).map_err(out_of_range_error) diff --git a/crates/rpc/src/server/mod.rs b/crates/rpc/src/server/mod.rs index 229907207..bfd3aebd1 100644 --- a/crates/rpc/src/server/mod.rs +++ b/crates/rpc/src/server/mod.rs @@ -21,6 +21,7 @@ use crate::server::health::HealthCheckLayer; mod accept; mod api; mod health; +mod transaction_helpers; /// The RPC server component. /// diff --git a/crates/rpc/src/server/transaction_helpers.rs b/crates/rpc/src/server/transaction_helpers.rs new file mode 100644 index 000000000..39ff0b9c5 --- /dev/null +++ b/crates/rpc/src/server/transaction_helpers.rs @@ -0,0 +1,109 @@ +//! Helper functions for processing and validating transactions and batches. +//! +//! This module contains utilities for: +//! - Stripping decorators from output notes +//! - Rebuilding transactions and batches +//! - Validating network account restrictions + +use std::sync::Arc; + +use miden_objects::batch::ProvenBatch; +use miden_objects::note::{Note, NoteRecipient, NoteScript}; +use miden_objects::transaction::{OutputNote, ProvenTransaction, ProvenTransactionBuilder}; + +// TRANSACTION PROCESSING +// ================================================================================================ + +/// Strips decorators from a single output note. +/// +/// Decorators are removed from the note's script MAST (Merkle Abstract Syntax Tree) +/// to ensure consistency when submitting transactions to the network. +pub fn strip_note_decorators(note: &OutputNote) -> OutputNote { + match note { + OutputNote::Full(note) => { + let mut mast = note.script().mast().clone(); + Arc::make_mut(&mut mast).strip_decorators(); + let script = NoteScript::from_parts(mast, note.script().entrypoint()); + let recipient = NoteRecipient::new(note.serial_num(), script, note.inputs().clone()); + let new_note = Note::new(note.assets().clone(), *note.metadata(), recipient); + OutputNote::Full(new_note) + }, + other => other.clone(), + } +} + +/// Rebuilds a transaction with decorators stripped from output notes. +/// +/// This function creates a new `ProvenTransaction` from the original, but with +/// all decorators removed from the output notes' scripts. +pub fn rebuild_transaction_without_decorators( + tx: &ProvenTransaction, +) -> Result { + let mut builder = ProvenTransactionBuilder::new( + tx.account_id(), + tx.account_update().initial_state_commitment(), + tx.account_update().final_state_commitment(), + tx.account_update().account_delta_commitment(), + tx.ref_block_num(), + tx.ref_block_commitment(), + tx.fee(), + tx.expiration_block_num(), + tx.proof().clone(), + ) + .account_update_details(tx.account_update().details().clone()) + .add_input_notes(tx.input_notes().iter().cloned()); + + let stripped_outputs = tx.output_notes().iter().map(strip_note_decorators); + builder = builder.add_output_notes(stripped_outputs); + + builder.build().map_err(|e| e.to_string()) +} + +/// Rebuilds a batch with decorators stripped from output notes. +/// +/// This function creates a new `ProvenBatch` from the original, but with +/// all decorators removed from the output notes' scripts. +pub fn rebuild_batch_without_decorators(batch: &ProvenBatch) -> Result { + let stripped_outputs: Vec = + batch.output_notes().iter().map(strip_note_decorators).collect(); + + ProvenBatch::new( + batch.id(), + batch.reference_block_commitment(), + batch.reference_block_num(), + batch.account_updates().clone(), + batch.input_notes().clone(), + stripped_outputs, + batch.batch_expiration_block_num(), + batch.transactions().clone(), + ) + .map_err(|e| e.to_string()) +} + +// VALIDATION HELPERS +// ================================================================================================ + +/// Validates that a transaction does not violate network account restrictions. +/// +/// Network accounts cannot be used for user-submitted transactions that modify +/// their initial state. Only deployment transactions for new network accounts are allowed. +pub fn validate_network_account_restriction(tx: &ProvenTransaction) -> Result<(), &'static str> { + if tx.account_id().is_network() && !tx.account_update().initial_state_commitment().is_empty() { + return Err("Network transactions may not be submitted by users yet"); + } + Ok(()) +} + +/// Validates that all transactions in a batch do not violate network account restrictions. +/// +/// See [`validate_network_account_restriction`] for details on the restriction. +pub fn validate_batch_network_account_restrictions( + batch: &ProvenBatch, +) -> Result<(), &'static str> { + for tx in batch.transactions().as_slice() { + if tx.account_id().is_network() && !tx.initial_state_commitment().is_empty() { + return Err("Network transactions may not be submitted by users yet"); + } + } + Ok(()) +}