diff --git a/crates/block-producer/src/store/mod.rs b/crates/block-producer/src/store/mod.rs index 391f747ba..92c78cfe4 100644 --- a/crates/block-producer/src/store/mod.rs +++ b/crates/block-producer/src/store/mod.rs @@ -4,9 +4,10 @@ use std::num::NonZeroU32; use itertools::Itertools; use miden_node_proto::clients::{Builder, StoreBlockProducerClient}; +use miden_node_proto::decode::{ConversionResultExt, GrpcDecodeExt}; use miden_node_proto::domain::batch::BatchInputs; -use miden_node_proto::errors::{ConversionError, MissingFieldHelper}; -use miden_node_proto::{AccountState, generated as proto}; +use miden_node_proto::errors::ConversionError; +use miden_node_proto::{AccountState, decode, generated as proto}; use miden_node_utils::formatting::format_opt; use miden_protocol::Word; use miden_protocol::account::AccountId; @@ -70,21 +71,14 @@ impl TryFrom for TransactionInputs { type Error = ConversionError; fn try_from(response: proto::store::TransactionInputs) -> Result { - let AccountState { account_id, account_commitment } = response - .account_state - .ok_or(proto::store::TransactionInputs::missing_field(stringify!(account_state)))? - .try_into()?; + let decoder = response.decoder(); + let AccountState { account_id, account_commitment } = + decode!(decoder, response.account_state)?; let mut nullifiers = HashMap::new(); for nullifier_record in response.nullifiers { - let nullifier = nullifier_record - .nullifier - .ok_or( - proto::store::transaction_inputs::NullifierTransactionInputRecord::missing_field( - stringify!(nullifier), - ), - )? - .try_into()?; + let decoder = nullifier_record.decoder(); + let nullifier = decode!(decoder, nullifier_record.nullifier)?; // Note that this intentionally maps 0 to None as this is the definition used in // protobuf. @@ -95,7 +89,8 @@ impl TryFrom for TransactionInputs { .found_unauthenticated_notes .into_iter() .map(Word::try_from) - .collect::>()?; + .collect::>() + .context("found_unauthenticated_notes")?; let current_block_height = response.block_height.into(); @@ -148,11 +143,13 @@ impl StoreClient { .await? .into_inner() .block_header - .ok_or(miden_node_proto::generated::blockchain::BlockHeader::missing_field( - "block_header", - ))?; + .ok_or_else(|| { + StoreError::DeserializationError(ConversionError::missing_field::< + miden_node_proto::generated::blockchain::BlockHeader, + >("block_header")) + })?; - BlockHeader::try_from(response).map_err(Into::into) + BlockHeader::try_from(response).map_err(StoreError::DeserializationError) } #[instrument(target = COMPONENT, name = "store.client.get_tx_inputs", skip_all, err)] @@ -219,7 +216,7 @@ impl StoreClient { let store_response = self.client.clone().get_block_inputs(request).await?.into_inner(); - store_response.try_into().map_err(Into::into) + store_response.try_into().map_err(StoreError::DeserializationError) } #[instrument(target = COMPONENT, name = "store.client.get_batch_inputs", skip_all, err)] @@ -235,7 +232,7 @@ impl StoreClient { let store_response = self.client.clone().get_batch_inputs(request).await?.into_inner(); - store_response.try_into().map_err(Into::into) + store_response.try_into().map_err(StoreError::DeserializationError) } #[instrument(target = COMPONENT, name = "store.client.apply_block", skip_all, err)] diff --git a/crates/ntx-builder/src/clients/store.rs b/crates/ntx-builder/src/clients/store.rs index 598de6d67..8a7fb3d9f 100644 --- a/crates/ntx-builder/src/clients/store.rs +++ b/crates/ntx-builder/src/clients/store.rs @@ -3,6 +3,7 @@ use std::ops::RangeInclusive; use std::time::Duration; use miden_node_proto::clients::{Builder, StoreNtxBuilderClient}; +use miden_node_proto::decode::ConversionResultExt; use miden_node_proto::domain::account::{AccountDetails, AccountResponse, NetworkAccountId}; use miden_node_proto::errors::ConversionError; use miden_node_proto::generated::rpc::BlockRange; @@ -101,7 +102,10 @@ impl StoreClient { match response.current_block_header { // There are new blocks compared to the builder's latest state Some(block) => { - let peaks = try_convert(response.current_peaks).collect::>()?; + let peaks: Vec = try_convert(response.current_peaks) + .collect::>() + .context("current_peaks") + .map_err(StoreError::DeserializationError)?; let header = BlockHeader::try_from(block).map_err(StoreError::DeserializationError)?; @@ -140,9 +144,7 @@ impl StoreClient { // which implies details being public, so OK to error otherwise let account = match store_response.map(|acc| acc.details) { Some(Some(details)) => Some(Account::read_from_bytes(&details).map_err(|err| { - StoreError::DeserializationError(ConversionError::deserialization_error( - "account", err, - )) + StoreError::DeserializationError(ConversionError::from(err).context("details")) })?), _ => None, }; @@ -185,7 +187,8 @@ impl StoreClient { let account_details = account_response .details .ok_or(StoreError::MissingDetails("account details".into()))?; - let partial_account = build_minimal_foreign_account(&account_details)?; + let partial_account = build_minimal_foreign_account(&account_details) + .map_err(StoreError::DeserializationError)?; Ok(AccountInputs::new(partial_account, account_response.witness)) } @@ -216,7 +219,10 @@ impl StoreClient { all_notes.reserve(resp.notes.len()); for note in resp.notes { - all_notes.push(AccountTargetNetworkNote::try_from(note)?); + all_notes.push( + AccountTargetNetworkNote::try_from(note) + .map_err(StoreError::DeserializationError)?, + ); } match resp.next_token { @@ -317,10 +323,9 @@ impl StoreClient { .into_iter() .map(|account_id| { let account_id = AccountId::read_from_bytes(&account_id.id).map_err(|err| { - StoreError::DeserializationError(ConversionError::deserialization_error( - "account_id", - err, - )) + StoreError::DeserializationError( + ConversionError::from(err).context("account_id"), + ) })?; NetworkAccountId::try_from(account_id).map_err(|_| { StoreError::MalformedResponse( @@ -330,12 +335,9 @@ impl StoreClient { }) .collect::, StoreError>>()?; - let pagination_info = response.pagination_info.ok_or( - ConversionError::MissingFieldInProtobufRepresentation { - entity: "NetworkAccountIdList", - field_name: "pagination_info", - }, - )?; + let pagination_info = response.pagination_info.ok_or(ConversionError::missing_field::< + proto::store::NetworkAccountIdList, + >("pagination_info"))?; Ok((accounts, pagination_info)) } @@ -406,8 +408,10 @@ impl StoreClient { let smt_opening = asset_witness.proof.ok_or_else(|| { StoreError::MalformedResponse("missing proof in vault asset witness".to_string()) })?; - let proof: SmtProof = - smt_opening.try_into().map_err(StoreError::DeserializationError)?; + let proof: SmtProof = smt_opening + .try_into() + .context("proof") + .map_err(StoreError::DeserializationError)?; let witness = AssetWitness::new(proof) .map_err(|err| StoreError::DeserializationError(ConversionError::from(err)))?; @@ -445,7 +449,10 @@ impl StoreClient { StoreError::MalformedResponse("missing proof in storage map witness".to_string()) })?; - let proof: SmtProof = smt_opening.try_into().map_err(StoreError::DeserializationError)?; + let proof: SmtProof = smt_opening + .try_into() + .context("proof") + .map_err(StoreError::DeserializationError)?; // Create the storage map witness using the proof and raw map key. let witness = StorageMapWitness::new(proof, [map_key]).map_err(|_err| { @@ -482,10 +489,11 @@ pub fn build_minimal_foreign_account( account_details: &AccountDetails, ) -> Result { // Derive account code. - let account_code_bytes = account_details - .account_code - .as_ref() - .ok_or(ConversionError::AccountCodeMissing)?; + let account_code_bytes = account_details.account_code.as_ref().ok_or_else(|| { + ConversionError::missing_field::( + "account_code", + ) + })?; let account_code = AccountCode::from_bytes(account_code_bytes)?; // Derive partial storage. Storage maps are not required for foreign accounts. diff --git a/crates/proto/src/decode/mod.rs b/crates/proto/src/decode/mod.rs new file mode 100644 index 000000000..63693ce13 --- /dev/null +++ b/crates/proto/src/decode/mod.rs @@ -0,0 +1,204 @@ +use std::marker::PhantomData; + +use miden_protocol::utils::serde::Deserializable; + +use crate::errors::ConversionError; +// Re-export so callers can import from `conv`. +pub use crate::errors::ConversionResultExt; + +// GRPC STRUCT DECODER +// ================================================================================================ + +/// Zero-cost struct decoder that captures the parent proto message type. +/// +/// Created via [`GrpcDecodeExt::decoder`] which infers the parent type from the value: +/// +/// ```rust,ignore +/// // Before: +/// let body = block.body.try_convert_field::("body")?; +/// let header = block.header.try_convert_field::("header")?; +/// +/// // After: +/// let decoder = block.decoder(); +/// let body = decode!(decoder, block.body); +/// let header = decode!(decoder, block.header); +/// ``` +pub struct GrpcStructDecoder(PhantomData); + +impl Default for GrpcStructDecoder { + /// Create a decoder for the given parent message type directly. + /// + /// Prefer [`GrpcDecodeExt::decoder`] when a value of type `M` is available, as it infers + /// the type automatically. + fn default() -> Self { + Self(PhantomData) + } +} + +impl GrpcStructDecoder { + /// Decode a required optional field: checks for `None`, converts via `TryInto`, and adds + /// field context on error. + pub fn decode_field( + &self, + name: &'static str, + value: Option, + ) -> Result + where + T: TryInto, + T::Error: Into, + { + value + .ok_or_else(|| ConversionError::missing_field::(name))? + .try_into() + .context(name) + } +} + +/// Extension trait on [`prost::Message`] types to create a [`GrpcStructDecoder`] with the parent +/// type inferred from the value. +pub trait GrpcDecodeExt: prost::Message + Sized { + /// Create a decoder that uses `Self` as the parent message type for error reporting. + fn decoder(&self) -> GrpcStructDecoder { + GrpcStructDecoder(PhantomData) + } +} + +impl GrpcDecodeExt for T {} + +/// Decodes a required optional field from a protobuf message using the message's decoder. +/// +/// Uses `stringify!` to automatically derive the field name for error reporting, avoiding +/// the duplication between a string literal and the field access. +/// +/// Has two forms: +/// - `decode!(decoder, msg.field)` — expands to `decoder.decode_field("field", msg.field)`. Use +/// when accessing a field directly on the message value. +/// - `decode!(decoder, field)` — expands to `decoder.decode_field("field", field)`. Use after +/// destructuring the message, when the field is a bare identifier. +/// +/// # Usage +/// +/// ```ignore +/// let decoder = value.decoder(); +/// // With a field access: +/// let sender = decode!(decoder, value.sender)?; +/// +/// // With a bare identifier (after destructuring): +/// let Proto { sender, .. } = value; +/// let sender = decode!(decoder, sender)?; +/// +/// // Without `?` to return the Result directly: +/// decode!(decoder, value.id) +/// ``` +#[macro_export] +macro_rules! decode { + ($decoder:ident, $msg:ident . $field:ident) => { + $decoder.decode_field(stringify!($field), $msg.$field) + }; + ($decoder:ident, $field:ident) => { + $decoder.decode_field(stringify!($field), $field) + }; +} + +// BYTE DESERIALIZATION EXTENSION TRAIT +// ================================================================================================ + +/// Extension trait on [`Deserializable`](miden_protocol::utils::Deserializable) types to +/// deserialize from bytes and wrap errors as [`ConversionError`]. +/// +/// This removes the boilerplate of calling `T::read_from_bytes(&bytes)` followed by +/// `.map_err(|source| ConversionError::deserialization("T", source))`: +/// +/// ```rust,ignore +/// // Before: +/// BlockBody::read_from_bytes(&value.block_body) +/// .map_err(|source| ConversionError::deserialization("BlockBody", source)) +/// +/// // After: +/// BlockBody::decode_bytes(&value.block_body, "BlockBody") +/// ``` +pub trait DecodeBytesExt: Deserializable { + /// Deserialize from bytes, wrapping any error as a [`ConversionError`]. + fn decode_bytes(bytes: &[u8], entity: &'static str) -> Result { + Self::read_from_bytes(bytes) + .map_err(|source| ConversionError::deserialization(entity, source)) + } +} + +impl DecodeBytesExt for T {} + +#[cfg(test)] +mod tests { + use miden_protocol::Felt; + + use super::*; + use crate::generated::primitives::Digest; + + /// Simulates a deeply nested conversion where each layer adds its field context. + fn inner_conversion() -> Result<(), ConversionError> { + Err(ConversionError::message("value is not in range 0..MODULUS")) + } + + fn outer_conversion() -> Result<(), ConversionError> { + inner_conversion().context("account_root").context("header") + } + + #[test] + fn test_context_builds_dotted_field_path() { + let err = outer_conversion().unwrap_err(); + assert_eq!(err.to_string(), "header.account_root: value is not in range 0..MODULUS"); + } + + #[test] + fn test_context_single_field() { + let err = inner_conversion().context("nullifier").unwrap_err(); + assert_eq!(err.to_string(), "nullifier: value is not in range 0..MODULUS"); + } + + #[test] + fn test_context_deep_nesting() { + let err = outer_conversion().context("block").context("response").unwrap_err(); + assert_eq!( + err.to_string(), + "response.block.header.account_root: value is not in range 0..MODULUS" + ); + } + + #[test] + fn test_no_context_shows_source_only() { + let err = inner_conversion().unwrap_err(); + assert_eq!(err.to_string(), "value is not in range 0..MODULUS"); + } + + #[test] + fn test_context_on_external_error_type() { + let result: Result = u8::try_from(256u16); + let err = result.context("fee_amount").unwrap_err(); + assert!(err.to_string().starts_with("fee_amount: "), "expected field prefix, got: {err}",); + } + + #[test] + fn test_decode_field_missing() { + let decoder = GrpcStructDecoder::::default(); + let account_root: Option = None; + let result: Result<[Felt; 4], _> = decode!(decoder, account_root); + let err = result.unwrap_err(); + assert!( + err.to_string().contains("account_root") && err.to_string().contains("missing"), + "expected missing field error, got: {err}", + ); + } + + #[test] + fn test_decode_field_conversion_error() { + let decoder = GrpcStructDecoder::::default(); + // Create a digest with an out-of-range value. + let account_root = Some(Digest { d0: u64::MAX, d1: 0, d2: 0, d3: 0 }); + let result: Result<[Felt; 4], _> = decode!(decoder, account_root); + let err = result.unwrap_err(); + assert!( + err.to_string().starts_with("account_root: "), + "expected field prefix, got: {err}", + ); + } +} diff --git a/crates/proto/src/domain/account.rs b/crates/proto/src/domain/account.rs index 64f52d1a3..158e34feb 100644 --- a/crates/proto/src/domain/account.rs +++ b/crates/proto/src/domain/account.rs @@ -25,7 +25,9 @@ use miden_standards::note::{NetworkAccountTarget, NetworkAccountTargetError}; use thiserror::Error; use super::try_convert; -use crate::errors::{ConversionError, MissingFieldHelper}; +use crate::decode; +use crate::decode::{ConversionResultExt, GrpcDecodeExt}; +use crate::errors::ConversionError; use crate::generated::{self as proto}; #[cfg(test)] @@ -57,7 +59,8 @@ impl TryFrom for AccountId { type Error = ConversionError; fn try_from(account_id: proto::account::AccountId) -> Result { - AccountId::read_from_bytes(&account_id.id).map_err(|_| ConversionError::NotAValidFelt) + AccountId::read_from_bytes(&account_id.id) + .map_err(|_| ConversionError::message("value is not in the range 0..MODULUS")) } } @@ -123,13 +126,14 @@ impl TryFrom for AccountStorageHeader { let slot_headers = slots .into_iter() .map(|slot| { + let decoder = slot.decoder(); let slot_name = StorageSlotName::new(slot.slot_name)?; let slot_type = storage_slot_type_from_raw(slot.slot_type)?; - let commitment = - slot.commitment.ok_or(ConversionError::NotAValidFelt)?.try_into()?; + let commitment = decode!(decoder, slot.commitment)?; Ok(StorageSlotHeader::new(slot_name, slot_type, commitment)) }) - .collect::, ConversionError>>()?; + .collect::, ConversionError>>() + .context("slots")?; Ok(AccountStorageHeader::new(slot_headers)?) } @@ -150,14 +154,13 @@ impl TryFrom for AccountRequest { type Error = ConversionError; fn try_from(value: proto::rpc::AccountRequest) -> Result { + let decoder = value.decoder(); let proto::rpc::AccountRequest { account_id, block_num, details } = value; - let account_id = account_id - .ok_or(proto::rpc::AccountRequest::missing_field(stringify!(account_id)))? - .try_into()?; + let account_id = decode!(decoder, account_id)?; let block_num = block_num.map(Into::into); - let details = details.map(TryFrom::try_from).transpose()?; + let details = details.map(TryFrom::try_from).transpose().context("details")?; Ok(AccountRequest { account_id, block_num, details }) } @@ -182,9 +185,14 @@ impl TryFrom for AccountDetai storage_maps, } = value; - let code_commitment = code_commitment.map(TryFrom::try_from).transpose()?; - let asset_vault_commitment = asset_vault_commitment.map(TryFrom::try_from).transpose()?; - let storage_requests = try_convert(storage_maps).collect::>()?; + let code_commitment = + code_commitment.map(TryFrom::try_from).transpose().context("code_commitment")?; + let asset_vault_commitment = asset_vault_commitment + .map(TryFrom::try_from) + .transpose() + .context("asset_vault_commitment")?; + let storage_requests = + try_convert(storage_maps).collect::>().context("storage_maps")?; Ok(AccountDetailRequest { code_commitment, @@ -208,13 +216,14 @@ impl TryFrom Result { + let decoder = value.decoder(); let proto::rpc::account_request::account_detail_request::StorageMapDetailRequest { slot_name, slot_data, } = value; - let slot_name = StorageSlotName::new(slot_name)?; - let slot_data = slot_data.ok_or(proto::rpc::account_request::account_detail_request::StorageMapDetailRequest::missing_field(stringify!(slot_data)))?.try_into()?; + let slot_name = StorageSlotName::new(slot_name).context("slot_name")?; + let slot_data = decode!(decoder, slot_data)?; Ok(StorageMapRequest { slot_name, slot_data }) } @@ -242,7 +251,7 @@ impl Ok(match value { ProtoSlotData::AllEntries(true) => SlotData::All, ProtoSlotData::AllEntries(false) => { - return Err(ConversionError::EnumDiscriminantOutOfRange); + return Err(ConversionError::message("enum variant discriminant out of range")); }, ProtoSlotData::MapKeys(keys) => { let keys = try_convert(keys.map_keys).collect::, _>>()?; @@ -259,6 +268,7 @@ impl TryFrom for AccountHeader { type Error = ConversionError; fn try_from(value: proto::account::AccountHeader) -> Result { + let decoder = value.decoder(); let proto::account::AccountHeader { account_id, vault_root, @@ -267,19 +277,14 @@ impl TryFrom for AccountHeader { nonce, } = value; - let account_id = account_id - .ok_or(proto::account::AccountHeader::missing_field(stringify!(account_id)))? - .try_into()?; - let vault_root = vault_root - .ok_or(proto::account::AccountHeader::missing_field(stringify!(vault_root)))? - .try_into()?; - let storage_commitment = storage_commitment - .ok_or(proto::account::AccountHeader::missing_field(stringify!(storage_commitment)))? - .try_into()?; - let code_commitment = code_commitment - .ok_or(proto::account::AccountHeader::missing_field(stringify!(code_commitment)))? - .try_into()?; - let nonce = nonce.try_into().map_err(|_e| ConversionError::NotAValidFelt)?; + let account_id = decode!(decoder, account_id)?; + let vault_root = decode!(decoder, vault_root)?; + let storage_commitment = decode!(decoder, storage_commitment)?; + let code_commitment = decode!(decoder, code_commitment)?; + let nonce = nonce + .try_into() + .map_err(|e| ConversionError::message(format!("{e}"))) + .context("nonce")?; Ok(AccountHeader::new( account_id, @@ -515,8 +520,9 @@ impl TryFrom value: proto::rpc::account_storage_details::AccountStorageMapDetails, ) -> Result { use proto::rpc::account_storage_details::account_storage_map_details::{ - AllMapEntries, Entries as ProtoEntries, MapEntriesWithProofs, - all_map_entries::StorageMapEntry, map_entries_with_proofs::StorageMapEntryWithProof, + AllMapEntries, + Entries as ProtoEntries, + MapEntriesWithProofs, }; let proto::rpc::account_storage_details::AccountStorageMapDetails { @@ -525,47 +531,39 @@ impl TryFrom entries, } = value; - let slot_name = StorageSlotName::new(slot_name)?; + let slot_name = StorageSlotName::new(slot_name).context("slot_name")?; let entries = if too_many_entries { StorageMapEntries::LimitExceeded } else { match entries { None => { - return Err( - proto::rpc::account_storage_details::AccountStorageMapDetails::missing_field( - stringify!(entries), - ), - ); + return Err(ConversionError::missing_field::< + proto::rpc::account_storage_details::AccountStorageMapDetails, + >("entries")); }, Some(ProtoEntries::AllEntries(AllMapEntries { entries })) => { let entries = entries .into_iter() .map(|entry| { - let key = entry - .key - .ok_or(StorageMapEntry::missing_field(stringify!(key)))? - .try_into() - .map(StorageMapKey::new)?; - let value = entry - .value - .ok_or(StorageMapEntry::missing_field(stringify!(value)))? - .try_into()?; + let decoder = entry.decoder(); + let key = StorageMapKey::new(decode!(decoder, entry.key)?); + let value = decode!(decoder, entry.value)?; Ok((key, value)) }) - .collect::, ConversionError>>()?; + .collect::, ConversionError>>() + .context("entries")?; StorageMapEntries::AllEntries(entries) }, Some(ProtoEntries::EntriesWithProofs(MapEntriesWithProofs { entries })) => { let proofs = entries .into_iter() .map(|entry| { - let smt_opening = entry.proof.ok_or( - StorageMapEntryWithProof::missing_field(stringify!(proof)), - )?; - SmtProof::try_from(smt_opening) + let decoder = entry.decoder(); + decode!(decoder, entry.proof) }) - .collect::, ConversionError>>()?; + .collect::, ConversionError>>() + .context("entries")?; StorageMapEntries::EntriesWithProofs(proofs) }, } @@ -661,13 +659,13 @@ impl TryFrom for AccountStorageDetails { type Error = ConversionError; fn try_from(value: proto::rpc::AccountStorageDetails) -> Result { + let decoder = value.decoder(); let proto::rpc::AccountStorageDetails { header, map_details } = value; - let header = header - .ok_or(proto::rpc::AccountStorageDetails::missing_field(stringify!(header)))? - .try_into()?; + let header = decode!(decoder, header)?; - let map_details = try_convert(map_details).collect::, _>>()?; + let map_details = + try_convert(map_details).collect::, _>>().context("map_details")?; Ok(Self { header, map_details }) } @@ -684,11 +682,11 @@ impl From for proto::rpc::AccountStorageDetails { } } -const fn storage_slot_type_from_raw(slot_type: u32) -> Result { +fn storage_slot_type_from_raw(slot_type: u32) -> Result { Ok(match slot_type { 0 => StorageSlotType::Value, 1 => StorageSlotType::Map, - _ => return Err(ConversionError::EnumDiscriminantOutOfRange), + _ => return Err(ConversionError::message("enum variant discriminant out of range")), }) } @@ -713,17 +711,16 @@ impl TryFrom for AccountResponse { type Error = ConversionError; fn try_from(value: proto::rpc::AccountResponse) -> Result { + let decoder = value.decoder(); let proto::rpc::AccountResponse { block_num, witness, details } = value; let block_num = block_num - .ok_or(proto::rpc::AccountResponse::missing_field(stringify!(block_num)))? + .ok_or(ConversionError::missing_field::("block_num"))? .into(); - let witness = witness - .ok_or(proto::rpc::AccountResponse::missing_field(stringify!(witness)))? - .try_into()?; + let witness = decode!(decoder, witness)?; - let details = details.map(TryFrom::try_from).transpose()?; + let details = details.map(TryFrom::try_from).transpose().context("details")?; Ok(AccountResponse { block_num, witness, details }) } @@ -774,6 +771,7 @@ impl TryFrom for AccountDetails { type Error = ConversionError; fn try_from(value: proto::rpc::account_response::AccountDetails) -> Result { + let decoder = value.decoder(); let proto::rpc::account_response::AccountDetails { header, code, @@ -781,21 +779,11 @@ impl TryFrom for AccountDetails { storage_details, } = value; - let account_header = header - .ok_or(proto::rpc::account_response::AccountDetails::missing_field(stringify!(header)))? - .try_into()?; - - let storage_details = storage_details - .ok_or(proto::rpc::account_response::AccountDetails::missing_field(stringify!( - storage_details - )))? - .try_into()?; - - let vault_details = vault_details - .ok_or(proto::rpc::account_response::AccountDetails::missing_field(stringify!( - vault_details - )))? - .try_into()?; + let account_header = decode!(decoder, header)?; + + let storage_details = decode!(decoder, storage_details)?; + + let vault_details = decode!(decoder, vault_details)?; let account_code = code; Ok(AccountDetails { @@ -837,21 +825,13 @@ impl TryFrom for AccountWitness { type Error = ConversionError; fn try_from(account_witness: proto::account::AccountWitness) -> Result { - let witness_id = account_witness - .witness_id - .ok_or(proto::account::AccountWitness::missing_field(stringify!(witness_id)))? - .try_into()?; - let commitment = account_witness - .commitment - .ok_or(proto::account::AccountWitness::missing_field(stringify!(commitment)))? - .try_into()?; - let path = account_witness - .path - .ok_or(proto::account::AccountWitness::missing_field(stringify!(path)))? - .try_into()?; + let decoder = account_witness.decoder(); + let witness_id = decode!(decoder, account_witness.witness_id)?; + let commitment = decode!(decoder, account_witness.commitment)?; + let path = decode!(decoder, account_witness.path)?; AccountWitness::new(witness_id, commitment, path).map_err(|err| { - ConversionError::deserialization_error( + ConversionError::deserialization( "AccountWitness", DeserializationError::InvalidValue(err.to_string()), ) @@ -885,35 +865,20 @@ impl TryFrom for AccountWitnessRecord { fn try_from( account_witness_record: proto::account::AccountWitness, ) -> Result { - let witness_id = account_witness_record - .witness_id - .ok_or(proto::account::AccountWitness::missing_field(stringify!(witness_id)))? - .try_into()?; - let commitment = account_witness_record - .commitment - .ok_or(proto::account::AccountWitness::missing_field(stringify!(commitment)))? - .try_into()?; - let path: SparseMerklePath = account_witness_record - .path - .as_ref() - .ok_or(proto::account::AccountWitness::missing_field(stringify!(path)))? - .clone() - .try_into()?; + let decoder = account_witness_record.decoder(); + let witness_id = decode!(decoder, account_witness_record.witness_id)?; + let commitment = decode!(decoder, account_witness_record.commitment)?; + let account_id = decode!(decoder, account_witness_record.account_id)?; + let path: SparseMerklePath = decode!(decoder, account_witness_record.path)?; let witness = AccountWitness::new(witness_id, commitment, path).map_err(|err| { - ConversionError::deserialization_error( + ConversionError::deserialization( "AccountWitness", DeserializationError::InvalidValue(err.to_string()), ) })?; - Ok(Self { - account_id: account_witness_record - .account_id - .ok_or(proto::account::AccountWitness::missing_field(stringify!(account_id)))? - .try_into()?, - witness, - }) + Ok(Self { account_id, witness }) } } @@ -956,19 +921,10 @@ impl TryFrom fo fn try_from( from: proto::store::transaction_inputs::AccountTransactionInputRecord, ) -> Result { - let account_id = from - .account_id - .ok_or(proto::store::transaction_inputs::AccountTransactionInputRecord::missing_field( - stringify!(account_id), - ))? - .try_into()?; - - let account_commitment = from - .account_commitment - .ok_or(proto::store::transaction_inputs::AccountTransactionInputRecord::missing_field( - stringify!(account_commitment), - ))? - .try_into()?; + let decoder = from.decoder(); + let account_id = decode!(decoder, from.account_id)?; + + let account_commitment = decode!(decoder, from.account_commitment)?; // If the commitment is equal to `Word::empty()`, it signifies that this is a new // account which is not yet present in the Store. @@ -997,13 +953,13 @@ impl From for proto::store::transaction_inputs::AccountTransaction impl TryFrom for Asset { type Error = ConversionError; - fn try_from(value: proto::primitives::Asset) -> Result { - let key = value.key.ok_or(proto::primitives::Asset::missing_field("key"))?; - let key_word = Word::try_from(key)?; - let value = value.value.ok_or(proto::primitives::Asset::missing_field("value"))?; - let value_word = Word::try_from(value)?; + fn try_from(asset: proto::primitives::Asset) -> Result { + let decoder = asset.decoder(); + let key_word: Word = decode!(decoder, asset.key)?; + let value_word: Word = decode!(decoder, asset.value)?; - Asset::from_key_value_words(key_word, value_word).map_err(ConversionError::AssetError) + let asset = Asset::from_key_value_words(key_word, value_word)?; + Ok(asset) } } diff --git a/crates/proto/src/domain/batch.rs b/crates/proto/src/domain/batch.rs index 0c17a7ead..590ea026a 100644 --- a/crates/proto/src/domain/batch.rs +++ b/crates/proto/src/domain/batch.rs @@ -3,9 +3,10 @@ use std::collections::BTreeMap; use miden_protocol::block::BlockHeader; use miden_protocol::note::{NoteId, NoteInclusionProof}; use miden_protocol::transaction::PartialBlockchain; -use miden_protocol::utils::serde::{Deserializable, Serializable}; +use miden_protocol::utils::serde::Serializable; -use crate::errors::{ConversionError, MissingFieldHelper}; +use crate::decode::{ConversionResultExt, DecodeBytesExt, GrpcDecodeExt}; +use crate::errors::ConversionError; use crate::generated as proto; /// Data required for a transaction batch. @@ -30,20 +31,22 @@ impl TryFrom for BatchInputs { type Error = ConversionError; fn try_from(response: proto::store::BatchInputs) -> Result { + let decoder = response.decoder(); let result = Self { - batch_reference_block_header: response - .batch_reference_block_header - .ok_or(proto::store::BatchInputs::missing_field("block_header"))? - .try_into()?, + batch_reference_block_header: crate::decode!( + decoder, + response.batch_reference_block_header + )?, note_proofs: response .note_proofs .iter() .map(<(NoteId, NoteInclusionProof)>::try_from) - .collect::>()?, - partial_block_chain: PartialBlockchain::read_from_bytes(&response.partial_block_chain) - .map_err(|source| { - ConversionError::deserialization_error("PartialBlockchain", source) - })?, + .collect::>() + .context("note_proofs")?, + partial_block_chain: PartialBlockchain::decode_bytes( + &response.partial_block_chain, + "PartialBlockchain", + )?, }; Ok(result) diff --git a/crates/proto/src/domain/block.rs b/crates/proto/src/domain/block.rs index 6383c255d..7f2646db0 100644 --- a/crates/proto/src/domain/block.rs +++ b/crates/proto/src/domain/block.rs @@ -14,11 +14,12 @@ use miden_protocol::block::{ use miden_protocol::crypto::dsa::ecdsa_k256_keccak::{PublicKey, Signature}; use miden_protocol::note::{NoteId, NoteInclusionProof}; use miden_protocol::transaction::PartialBlockchain; -use miden_protocol::utils::serde::{Deserializable, Serializable}; +use miden_protocol::utils::serde::Serializable; use thiserror::Error; -use crate::errors::{ConversionError, MissingFieldHelper}; -use crate::{AccountWitnessRecord, NullifierWitnessRecord, generated as proto}; +use crate::decode::{ConversionResultExt, DecodeBytesExt, GrpcDecodeExt}; +use crate::errors::ConversionError; +use crate::{AccountWitnessRecord, NullifierWitnessRecord, decode, generated as proto}; // BLOCK NUMBER // ================================================================================================ @@ -75,48 +76,29 @@ impl TryFrom for BlockHeader { type Error = ConversionError; fn try_from(value: proto::blockchain::BlockHeader) -> Result { + let decoder = value.decoder(); + let prev_block_commitment = decode!(decoder, value.prev_block_commitment)?; + let chain_commitment = decode!(decoder, value.chain_commitment)?; + let account_root = decode!(decoder, value.account_root)?; + let nullifier_root = decode!(decoder, value.nullifier_root)?; + let note_root = decode!(decoder, value.note_root)?; + let tx_commitment = decode!(decoder, value.tx_commitment)?; + let tx_kernel_commitment = decode!(decoder, value.tx_kernel_commitment)?; + let validator_key = decode!(decoder, value.validator_key)?; + let fee_parameters = decode!(decoder, value.fee_parameters)?; + Ok(BlockHeader::new( value.version, - value - .prev_block_commitment - .ok_or(proto::blockchain::BlockHeader::missing_field(stringify!( - prev_block_commitment - )))? - .try_into()?, + prev_block_commitment, value.block_num.into(), - value - .chain_commitment - .ok_or(proto::blockchain::BlockHeader::missing_field(stringify!(chain_commitment)))? - .try_into()?, - value - .account_root - .ok_or(proto::blockchain::BlockHeader::missing_field(stringify!(account_root)))? - .try_into()?, - value - .nullifier_root - .ok_or(proto::blockchain::BlockHeader::missing_field(stringify!(nullifier_root)))? - .try_into()?, - value - .note_root - .ok_or(proto::blockchain::BlockHeader::missing_field(stringify!(note_root)))? - .try_into()?, - value - .tx_commitment - .ok_or(proto::blockchain::BlockHeader::missing_field(stringify!(tx_commitment)))? - .try_into()?, - value - .tx_kernel_commitment - .ok_or(proto::blockchain::BlockHeader::missing_field(stringify!( - tx_kernel_commitment - )))? - .try_into()?, - value - .validator_key - .ok_or(proto::blockchain::BlockHeader::missing_field(stringify!(validator_key)))? - .try_into()?, - FeeParameters::try_from(value.fee_parameters.ok_or( - proto::blockchain::FeeParameters::missing_field(stringify!(fee_parameters)), - )?)?, + chain_commitment, + account_root, + nullifier_root, + note_root, + tx_commitment, + tx_kernel_commitment, + validator_key, + fee_parameters, value.timestamp, )) } @@ -148,8 +130,7 @@ impl TryFrom<&proto::blockchain::BlockBody> for BlockBody { impl TryFrom for BlockBody { type Error = ConversionError; fn try_from(value: proto::blockchain::BlockBody) -> Result { - BlockBody::read_from_bytes(&value.block_body) - .map_err(|source| ConversionError::deserialization_error("BlockBody", source)) + BlockBody::decode_bytes(&value.block_body, "BlockBody") } } @@ -183,18 +164,10 @@ impl TryFrom<&proto::blockchain::SignedBlock> for SignedBlock { impl TryFrom for SignedBlock { type Error = ConversionError; fn try_from(value: proto::blockchain::SignedBlock) -> Result { - let header = value - .header - .ok_or(proto::blockchain::SignedBlock::missing_field(stringify!(header)))? - .try_into()?; - let body = value - .body - .ok_or(proto::blockchain::SignedBlock::missing_field(stringify!(body)))? - .try_into()?; - let signature = value - .signature - .ok_or(proto::blockchain::SignedBlock::missing_field(stringify!(signature)))? - .try_into()?; + let decoder = value.decoder(); + let header = decode!(decoder, value.header)?; + let body = decode!(decoder, value.body)?; + let signature = decode!(decoder, value.signature)?; Ok(SignedBlock::new_unchecked(header, body, signature)) } @@ -239,10 +212,8 @@ impl TryFrom for BlockInputs { type Error = ConversionError; fn try_from(response: proto::store::BlockInputs) -> Result { - let latest_block_header: BlockHeader = response - .latest_block_header - .ok_or(proto::blockchain::BlockHeader::missing_field("block_header"))? - .try_into()?; + let decoder = response.decoder(); + let latest_block_header: BlockHeader = decode!(decoder, response.latest_block_header)?; let account_witnesses = response .account_witnesses @@ -251,7 +222,8 @@ impl TryFrom for BlockInputs { let witness_record: AccountWitnessRecord = entry.try_into()?; Ok((witness_record.account_id, witness_record.witness)) }) - .collect::, ConversionError>>()?; + .collect::, ConversionError>>() + .context("account_witnesses")?; let nullifier_witnesses = response .nullifier_witnesses @@ -260,18 +232,18 @@ impl TryFrom for BlockInputs { let witness: NullifierWitnessRecord = entry.try_into()?; Ok((witness.nullifier, NullifierWitness::new(witness.proof))) }) - .collect::, ConversionError>>()?; + .collect::, ConversionError>>() + .context("nullifier_witnesses")?; let unauthenticated_note_proofs = response .unauthenticated_note_proofs .iter() .map(<(NoteId, NoteInclusionProof)>::try_from) - .collect::>()?; + .collect::>() + .context("unauthenticated_note_proofs")?; - let partial_block_chain = PartialBlockchain::read_from_bytes(&response.partial_block_chain) - .map_err(|source| { - ConversionError::deserialization_error("PartialBlockchain", source) - })?; + let partial_block_chain = + PartialBlockchain::decode_bytes(&response.partial_block_chain, "PartialBlockchain")?; Ok(BlockInputs::new( latest_block_header, @@ -289,8 +261,7 @@ impl TryFrom for BlockInputs { impl TryFrom for PublicKey { type Error = ConversionError; fn try_from(public_key: proto::blockchain::ValidatorPublicKey) -> Result { - PublicKey::read_from_bytes(&public_key.validator_key) - .map_err(|source| ConversionError::deserialization_error("PublicKey", source)) + PublicKey::decode_bytes(&public_key.validator_key, "PublicKey") } } @@ -312,8 +283,7 @@ impl From<&PublicKey> for proto::blockchain::ValidatorPublicKey { impl TryFrom for Signature { type Error = ConversionError; fn try_from(signature: proto::blockchain::BlockSignature) -> Result { - Signature::read_from_bytes(&signature.signature) - .map_err(|source| ConversionError::deserialization_error("Signature", source)) + Signature::decode_bytes(&signature.signature, "Signature") } } @@ -335,9 +305,13 @@ impl From<&Signature> for proto::blockchain::BlockSignature { impl TryFrom for FeeParameters { type Error = ConversionError; fn try_from(fee_params: proto::blockchain::FeeParameters) -> Result { - let native_asset_id = fee_params.native_asset_id.map(AccountId::try_from).ok_or( - proto::blockchain::FeeParameters::missing_field(stringify!(native_asset_id)), - )??; + let native_asset_id = fee_params + .native_asset_id + .map(AccountId::try_from) + .ok_or(ConversionError::missing_field::( + "native_asset_id", + ))? + .context("native_asset_id")?; let fee_params = FeeParameters::new(native_asset_id, fee_params.verification_base_fee)?; Ok(fee_params) } diff --git a/crates/proto/src/domain/digest.rs b/crates/proto/src/domain/digest.rs index d411c100f..fd7640ae5 100644 --- a/crates/proto/src/domain/digest.rs +++ b/crates/proto/src/domain/digest.rs @@ -65,12 +65,12 @@ impl FromHex for proto::primitives::Digest { let data = hex::decode(hex)?; match data.len() { - size if size < DIGEST_DATA_SIZE => { - Err(ConversionError::InsufficientData { expected: DIGEST_DATA_SIZE, got: size }) - }, - size if size > DIGEST_DATA_SIZE => { - Err(ConversionError::TooMuchData { expected: DIGEST_DATA_SIZE, got: size }) - }, + size if size < DIGEST_DATA_SIZE => Err(ConversionError::message(format!( + "not enough data, expected {DIGEST_DATA_SIZE}, got {size}" + ))), + size if size > DIGEST_DATA_SIZE => Err(ConversionError::message(format!( + "too much data, expected {DIGEST_DATA_SIZE}, got {size}" + ))), _ => { let d0 = u64::from_be_bytes(data[..8].try_into().unwrap()); let d1 = u64::from_be_bytes(data[8..16].try_into().unwrap()); @@ -175,7 +175,7 @@ impl TryFrom for [Felt; 4] { fn try_from(value: proto::primitives::Digest) -> Result { if [value.d0, value.d1, value.d2, value.d3].iter().any(|v| *v >= Felt::ORDER) { - return Err(ConversionError::NotAValidFelt); + return Err(ConversionError::message("value is not in the range 0..MODULUS")); } Ok([ diff --git a/crates/proto/src/domain/mempool.rs b/crates/proto/src/domain/mempool.rs index 12eba0d1b..4b278e479 100644 --- a/crates/proto/src/domain/mempool.rs +++ b/crates/proto/src/domain/mempool.rs @@ -4,11 +4,12 @@ use miden_protocol::account::delta::AccountUpdateDetails; use miden_protocol::block::BlockHeader; use miden_protocol::note::Nullifier; use miden_protocol::transaction::TransactionId; -use miden_protocol::utils::serde::{Deserializable, Serializable}; +use miden_protocol::utils::serde::Serializable; use miden_standards::note::AccountTargetNetworkNote; -use crate::errors::{ConversionError, MissingFieldHelper}; -use crate::generated as proto; +use crate::decode::{ConversionResultExt, DecodeBytesExt, GrpcDecodeExt}; +use crate::errors::ConversionError; +use crate::{decode, generated as proto}; #[derive(Debug, Clone, PartialEq)] pub enum MempoolEvent { @@ -82,30 +83,31 @@ impl TryFrom for MempoolEvent { type Error = ConversionError; fn try_from(event: proto::block_producer::MempoolEvent) -> Result { - let event = - event.event.ok_or(proto::block_producer::MempoolEvent::missing_field("event"))?; + let event = event.event.ok_or(ConversionError::missing_field::< + proto::block_producer::MempoolEvent, + >("event"))?; match event { proto::block_producer::mempool_event::Event::TransactionAdded(tx) => { - let id = tx - .id - .ok_or(proto::block_producer::mempool_event::TransactionAdded::missing_field( - "id", - ))? - .try_into()?; - let nullifiers = - tx.nullifiers.into_iter().map(TryInto::try_into).collect::>()?; + let decoder = tx.decoder(); + let id = decode!(decoder, tx.id)?; + let nullifiers = tx + .nullifiers + .into_iter() + .map(Nullifier::try_from) + .collect::>() + .context("nullifiers")?; let network_notes = tx .network_notes .into_iter() - .map(TryInto::try_into) - .collect::>()?; + .map(AccountTargetNetworkNote::try_from) + .collect::>() + .context("network_notes")?; let account_delta = tx .network_account_delta .as_deref() - .map(AccountUpdateDetails::read_from_bytes) - .transpose() - .map_err(|err| ConversionError::deserialization_error("account_delta", err))?; + .map(|bytes| AccountUpdateDetails::decode_bytes(bytes, "account_delta")) + .transpose()?; Ok(Self::TransactionAdded { id, @@ -115,18 +117,15 @@ impl TryFrom for MempoolEvent { }) }, proto::block_producer::mempool_event::Event::BlockCommitted(block_committed) => { - let header = block_committed - .block_header - .ok_or(proto::block_producer::mempool_event::BlockCommitted::missing_field( - "block_header", - ))? - .try_into()?; + let decoder = block_committed.decoder(); + let header = decode!(decoder, block_committed.block_header)?; let header = Box::new(header); let txs = block_committed .transactions .into_iter() .map(TransactionId::try_from) - .collect::>()?; + .collect::>() + .context("transactions")?; Ok(Self::BlockCommitted { header, txs }) }, @@ -135,7 +134,8 @@ impl TryFrom for MempoolEvent { .reverted .into_iter() .map(TransactionId::try_from) - .collect::>()?; + .collect::>() + .context("reverted")?; Ok(Self::TransactionsReverted(txs)) }, diff --git a/crates/proto/src/domain/merkle.rs b/crates/proto/src/domain/merkle.rs index 992e11562..a4d311976 100644 --- a/crates/proto/src/domain/merkle.rs +++ b/crates/proto/src/domain/merkle.rs @@ -3,9 +3,10 @@ use miden_protocol::crypto::merkle::mmr::{Forest, MmrDelta}; use miden_protocol::crypto::merkle::smt::{LeafIndex, SmtLeaf, SmtProof}; use miden_protocol::crypto::merkle::{MerklePath, SparseMerklePath}; +use crate::decode::{ConversionResultExt, GrpcDecodeExt}; use crate::domain::{convert, try_convert}; -use crate::errors::{ConversionError, MissingFieldHelper}; -use crate::generated as proto; +use crate::errors::ConversionError; +use crate::{decode, generated as proto}; // MERKLE PATH // ================================================================================================ @@ -62,7 +63,8 @@ impl TryFrom for SparseMerklePath { .siblings .into_iter() .map(Word::try_from) - .collect::, _>>()?, + .collect::, _>>() + .context("siblings")?, )?) } } @@ -84,12 +86,16 @@ impl TryFrom for MmrDelta { type Error = ConversionError; fn try_from(value: proto::primitives::MmrDelta) -> Result { - let data: Result, ConversionError> = - value.data.into_iter().map(Word::try_from).collect(); + let data: Vec<_> = value + .data + .into_iter() + .map(Word::try_from) + .collect::>() + .context("data")?; Ok(MmrDelta { forest: Forest::new(value.forest as usize), - data: data?, + data, }) } } @@ -104,20 +110,22 @@ impl TryFrom for SmtLeaf { type Error = ConversionError; fn try_from(value: proto::primitives::SmtLeaf) -> Result { - let leaf = value.leaf.ok_or(proto::primitives::SmtLeaf::missing_field(stringify!(leaf)))?; + let leaf = value + .leaf + .ok_or(ConversionError::missing_field::("leaf"))?; match leaf { proto::primitives::smt_leaf::Leaf::EmptyLeafIndex(leaf_index) => { Ok(Self::new_empty(LeafIndex::new_max_depth(leaf_index))) }, proto::primitives::smt_leaf::Leaf::Single(entry) => { - let (key, value): (Word, Word) = entry.try_into()?; + let (key, value): (Word, Word) = entry.try_into().context("entry")?; Ok(SmtLeaf::new_single(key, value)) }, proto::primitives::smt_leaf::Leaf::Multiple(entries) => { let domain_entries: Vec<(Word, Word)> = - try_convert(entries.entries).collect::>()?; + try_convert(entries.entries).collect::>().context("entries")?; Ok(SmtLeaf::new_multiple(domain_entries)?) }, @@ -148,14 +156,9 @@ impl TryFrom for (Word, Word) { type Error = ConversionError; fn try_from(entry: proto::primitives::SmtLeafEntry) -> Result { - let key: Word = entry - .key - .ok_or(proto::primitives::SmtLeafEntry::missing_field(stringify!(key)))? - .try_into()?; - let value: Word = entry - .value - .ok_or(proto::primitives::SmtLeafEntry::missing_field(stringify!(value)))? - .try_into()?; + let decoder = entry.decoder(); + let key: Word = decode!(decoder, entry.key)?; + let value: Word = decode!(decoder, entry.value)?; Ok((key, value)) } @@ -177,14 +180,9 @@ impl TryFrom for SmtProof { type Error = ConversionError; fn try_from(opening: proto::primitives::SmtOpening) -> Result { - let path: SparseMerklePath = opening - .path - .ok_or(proto::primitives::SmtOpening::missing_field(stringify!(path)))? - .try_into()?; - let leaf: SmtLeaf = opening - .leaf - .ok_or(proto::primitives::SmtOpening::missing_field(stringify!(leaf)))? - .try_into()?; + let decoder = opening.decoder(); + let path: SparseMerklePath = decode!(decoder, opening.path)?; + let leaf: SmtLeaf = decode!(decoder, opening.leaf)?; Ok(SmtProof::new(path, leaf)?) } diff --git a/crates/proto/src/domain/note.rs b/crates/proto/src/domain/note.rs index def699748..3ec63c2ad 100644 --- a/crates/proto/src/domain/note.rs +++ b/crates/proto/src/domain/note.rs @@ -15,12 +15,13 @@ use miden_protocol::note::{ NoteTag, NoteType, }; -use miden_protocol::utils::serde::{Deserializable, Serializable}; +use miden_protocol::utils::serde::Serializable; use miden_protocol::{MastForest, MastNodeId, Word}; use miden_standards::note::AccountTargetNetworkNote; -use crate::errors::{ConversionError, MissingFieldHelper}; -use crate::generated as proto; +use crate::decode::{ConversionResultExt, DecodeBytesExt, GrpcDecodeExt}; +use crate::errors::ConversionError; +use crate::{decode, generated as proto}; // NOTE TYPE // ================================================================================================ @@ -41,7 +42,9 @@ impl TryFrom for NoteType { match note_type { proto::note::NoteType::Public => Ok(NoteType::Public), proto::note::NoteType::Private => Ok(NoteType::Private), - proto::note::NoteType::Unspecified => Err(ConversionError::EnumDiscriminantOutOfRange), + proto::note::NoteType::Unspecified => { + Err(ConversionError::message("enum variant discriminant out of range")) + }, } } } @@ -68,7 +71,7 @@ impl TryFrom for NoteAttachmentKind { proto::note::NoteAttachmentKind::Word => Ok(NoteAttachmentKind::Word), proto::note::NoteAttachmentKind::Array => Ok(NoteAttachmentKind::Array), proto::note::NoteAttachmentKind::Unspecified => { - Err(ConversionError::EnumDiscriminantOutOfRange) + Err(ConversionError::message("enum variant discriminant out of range")) }, } } @@ -96,21 +99,19 @@ impl TryFrom for NoteMetadata { type Error = ConversionError; fn try_from(value: proto::note::NoteMetadata) -> Result { - let sender = value - .sender - .ok_or_else(|| proto::note::NoteMetadata::missing_field(stringify!(sender)))? - .try_into()?; + let decoder = value.decoder(); + let sender = decode!(decoder, value.sender)?; let note_type = proto::note::NoteType::try_from(value.note_type) - .map_err(|_| ConversionError::EnumDiscriminantOutOfRange)? - .try_into()?; + .map_err(|_| ConversionError::message("enum variant discriminant out of range"))? + .try_into() + .context("note_type")?; let tag = NoteTag::new(value.tag); // Deserialize attachment if present let attachment = if value.attachment.is_empty() { NoteAttachment::default() } else { - NoteAttachment::read_from_bytes(&value.attachment) - .map_err(|err| ConversionError::deserialization_error("NoteAttachment", err))? + NoteAttachment::decode_bytes(&value.attachment, "NoteAttachment")? }; Ok(NoteMetadata::new(sender, note_type).with_tag(tag).with_attachment(attachment)) @@ -149,15 +150,12 @@ impl TryFrom for AccountTargetNetworkNote { type Error = ConversionError; fn try_from(value: proto::note::NetworkNote) -> Result { - let details = NoteDetails::read_from_bytes(&value.details) - .map_err(|err| ConversionError::deserialization_error("NoteDetails", err))?; + let decoder = value.decoder(); + let details = NoteDetails::decode_bytes(&value.details, "NoteDetails")?; let (assets, recipient) = details.into_parts(); - let metadata: NoteMetadata = value - .metadata - .ok_or_else(|| proto::note::NetworkNote::missing_field(stringify!(metadata)))? - .try_into()?; + let metadata: NoteMetadata = decode!(decoder, value.metadata)?; let note = Note::new(assets, metadata, recipient); - AccountTargetNetworkNote::new(note).map_err(ConversionError::NetworkNoteError) + AccountTargetNetworkNote::new(note).map_err(ConversionError::from) } } @@ -185,7 +183,7 @@ impl TryFrom for Word { note_id .id .as_ref() - .ok_or(proto::note::NoteId::missing_field(stringify!(id)))? + .ok_or(ConversionError::missing_field::("id"))? .try_into() } } @@ -217,27 +215,31 @@ impl TryFrom<&proto::note::NoteInclusionInBlockProof> for (NoteId, NoteInclusion proof .inclusion_path .as_ref() - .ok_or(proto::note::NoteInclusionInBlockProof::missing_field(stringify!( - inclusion_path - )))? + .ok_or(ConversionError::missing_field::( + "inclusion_path", + ))? .clone(), - )?; + ) + .context("inclusion_path")?; let note_id = Word::try_from( proof .note_id .as_ref() - .ok_or(proto::note::NoteInclusionInBlockProof::missing_field(stringify!(note_id)))? + .ok_or(ConversionError::missing_field::( + "note_id", + ))? .id .as_ref() - .ok_or(proto::note::NoteId::missing_field(stringify!(id)))?, - )?; + .ok_or(ConversionError::missing_field::("id"))?, + ) + .context("note_id")?; Ok(( NoteId::from_raw(note_id), NoteInclusionProof::new( proof.block_num.into(), - proof.note_index_in_block.try_into()?, + proof.note_index_in_block.try_into().context("note_index_in_block")?, inclusion_path, )?, )) @@ -248,17 +250,14 @@ impl TryFrom for Note { type Error = ConversionError; fn try_from(proto_note: proto::note::Note) -> Result { - let metadata: NoteMetadata = proto_note - .metadata - .ok_or(proto::note::Note::missing_field(stringify!(metadata)))? - .try_into()?; + let decoder = proto_note.decoder(); + let metadata: NoteMetadata = decode!(decoder, proto_note.metadata)?; let details = proto_note .details - .ok_or(proto::note::Note::missing_field(stringify!(details)))?; + .ok_or(ConversionError::missing_field::("details"))?; - let note_details = NoteDetails::read_from_bytes(&details) - .map_err(|err| ConversionError::deserialization_error("NoteDetails", err))?; + let note_details = NoteDetails::decode_bytes(&details, "NoteDetails")?; let (assets, recipient) = note_details.into_parts(); Ok(Note::new(assets, metadata, recipient)) @@ -281,14 +280,9 @@ impl TryFrom for NoteHeader { type Error = ConversionError; fn try_from(value: proto::note::NoteHeader) -> Result { - let note_id_word: Word = value - .note_id - .ok_or_else(|| proto::note::NoteHeader::missing_field(stringify!(note_id)))? - .try_into()?; - let metadata: NoteMetadata = value - .metadata - .ok_or_else(|| proto::note::NoteHeader::missing_field(stringify!(metadata)))? - .try_into()?; + let decoder = value.decoder(); + let note_id_word: Word = decode!(decoder, value.note_id)?; + let metadata: NoteMetadata = decode!(decoder, value.metadata)?; Ok(NoteHeader::new(NoteId::from_raw(note_id_word), metadata)) } @@ -312,10 +306,9 @@ impl TryFrom for NoteScript { fn try_from(value: proto::note::NoteScript) -> Result { let proto::note::NoteScript { entrypoint, mast } = value; - let mast = MastForest::read_from_bytes(&mast) - .map_err(|err| Self::Error::deserialization_error("note_script.mast", err))?; + let mast = MastForest::decode_bytes(&mast, "note_script.mast")?; let entrypoint = MastNodeId::from_u32_safe(entrypoint, &mast) - .map_err(|err| Self::Error::deserialization_error("note_script.entrypoint", err))?; + .map_err(|err| ConversionError::deserialization("note_script.entrypoint", err))?; Ok(Self::from_parts(Arc::new(mast), entrypoint)) } diff --git a/crates/proto/src/domain/nullifier.rs b/crates/proto/src/domain/nullifier.rs index 3ccdf88ba..326cc4ebf 100644 --- a/crates/proto/src/domain/nullifier.rs +++ b/crates/proto/src/domain/nullifier.rs @@ -2,8 +2,9 @@ use miden_protocol::Word; use miden_protocol::crypto::merkle::smt::SmtProof; use miden_protocol::note::Nullifier; -use crate::errors::{ConversionError, MissingFieldHelper}; -use crate::generated as proto; +use crate::decode::GrpcDecodeExt; +use crate::errors::ConversionError; +use crate::{decode, generated as proto}; // FROM NULLIFIER // ================================================================================================ @@ -47,19 +48,10 @@ impl TryFrom for NullifierWitnessR fn try_from( nullifier_witness_record: proto::store::block_inputs::NullifierWitness, ) -> Result { + let decoder = nullifier_witness_record.decoder(); Ok(Self { - nullifier: nullifier_witness_record - .nullifier - .ok_or(proto::store::block_inputs::NullifierWitness::missing_field(stringify!( - nullifier - )))? - .try_into()?, - proof: nullifier_witness_record - .opening - .ok_or(proto::store::block_inputs::NullifierWitness::missing_field(stringify!( - opening - )))? - .try_into()?, + nullifier: decode!(decoder, nullifier_witness_record.nullifier)?, + proof: decode!(decoder, nullifier_witness_record.opening)?, }) } } diff --git a/crates/proto/src/domain/transaction.rs b/crates/proto/src/domain/transaction.rs index 0da1a0576..bc388c7b7 100644 --- a/crates/proto/src/domain/transaction.rs +++ b/crates/proto/src/domain/transaction.rs @@ -2,8 +2,9 @@ use miden_protocol::Word; use miden_protocol::note::Nullifier; use miden_protocol::transaction::{InputNoteCommitment, TransactionId}; -use crate::errors::{ConversionError, MissingFieldHelper}; -use crate::generated as proto; +use crate::decode::{ConversionResultExt, GrpcDecodeExt}; +use crate::errors::ConversionError; +use crate::{decode, generated as proto}; // FROM TRANSACTION ID // ================================================================================================ @@ -48,13 +49,8 @@ impl TryFrom for TransactionId { type Error = ConversionError; fn try_from(value: proto::transaction::TransactionId) -> Result { - value - .id - .ok_or(ConversionError::MissingFieldInProtobufRepresentation { - entity: "TransactionId", - field_name: "id", - })? - .try_into() + let decoder = value.decoder(); + decode!(decoder, value.id) } } @@ -74,15 +70,11 @@ impl TryFrom for InputNoteCommitment { type Error = ConversionError; fn try_from(value: proto::transaction::InputNoteCommitment) -> Result { - let nullifier: Nullifier = value - .nullifier - .ok_or_else(|| { - proto::transaction::InputNoteCommitment::missing_field(stringify!(nullifier)) - })? - .try_into()?; + let decoder = value.decoder(); + let nullifier: Nullifier = decode!(decoder, value.nullifier)?; let header: Option = - value.header.map(TryInto::try_into).transpose()?; + value.header.map(TryInto::try_into).transpose().context("header")?; Ok(InputNoteCommitment::from_parts_unchecked(nullifier, header)) } diff --git a/crates/proto/src/errors/mod.rs b/crates/proto/src/errors/mod.rs index 75d5bf870..9bf21e981 100644 --- a/crates/proto/src/errors/mod.rs +++ b/crates/proto/src/errors/mod.rs @@ -1,79 +1,96 @@ use std::any::type_name; -use std::num::TryFromIntError; +use std::fmt; // Re-export the GrpcError derive macro for convenience pub use miden_node_grpc_error_macro::GrpcError; -use miden_protocol::crypto::merkle::smt::{SmtLeafError, SmtProofError}; -use miden_protocol::errors::{AccountError, AssetError, FeeError, NoteError, StorageSlotNameError}; use miden_protocol::utils::serde::DeserializationError; -use miden_standards::note::NetworkAccountTargetError; -use thiserror::Error; #[cfg(test)] mod test_macro; -#[derive(Debug, Error)] -pub enum ConversionError { - #[error("asset error")] - AssetError(#[from] AssetError), - #[error("account code missing")] - AccountCodeMissing, - #[error("account error")] - AccountError(#[from] AccountError), - #[error("fee parameters error")] - FeeError(#[from] FeeError), - #[error("hex error")] - HexError(#[from] hex::FromHexError), - #[error("note error")] - NoteError(#[from] NoteError), - #[error("network note error")] - NetworkNoteError(#[source] NetworkAccountTargetError), - #[error("SMT leaf error")] - SmtLeafError(#[from] SmtLeafError), - #[error("SMT proof error")] - SmtProofError(#[from] SmtProofError), - #[error("storage slot name error")] - StorageSlotNameError(#[from] StorageSlotNameError), - #[error("integer conversion error: {0}")] - TryFromIntError(#[from] TryFromIntError), - #[error("too much data, expected {expected}, got {got}")] - TooMuchData { expected: usize, got: usize }, - #[error("not enough data, expected {expected}, got {got}")] - InsufficientData { expected: usize, got: usize }, - #[error("value is not in the range 0..MODULUS")] - NotAValidFelt, - #[error("merkle error")] - MerkleError(#[from] miden_protocol::crypto::merkle::MerkleError), - #[error("field `{entity}::{field_name}` is missing")] - MissingFieldInProtobufRepresentation { - entity: &'static str, - field_name: &'static str, - }, - #[error("failed to deserialize {entity}")] - DeserializationError { - entity: &'static str, - source: DeserializationError, - }, - #[error("enum variant discriminant out of range")] - EnumDiscriminantOutOfRange, +// CONVERSION ERROR +// ================================================================================================ + +/// Opaque error for protobuf-to-domain conversions. +/// +/// Captures an underlying error plus an optional field path stack that describes which nested +/// field caused the error (e.g. `"block.header.account_root: value is not in range 0..MODULUS"`). +/// +/// Always maps to [`tonic::Status::invalid_argument()`]. +#[derive(Debug)] +pub struct ConversionError { + path: Vec<&'static str>, + source: Box, } impl ConversionError { - pub fn deserialization_error(entity: &'static str, source: DeserializationError) -> Self { - Self::DeserializationError { entity, source } + /// Create a new `ConversionError` wrapping any error source. + pub fn new(source: impl std::error::Error + Send + Sync + 'static) -> Self { + Self { + path: Vec::new(), + source: Box::new(source), + } + } + + /// Add field context to the error path. + /// + /// Called from inner to outer, so the path accumulates in reverse + /// (outermost field pushed last). + /// + /// Use this to annotate errors from `try_into()` / `try_from()` where the underlying + /// error has no knowledge of which field it originated from. Do not use it with + /// [`missing_field`](Self::missing_field) which already embeds the field name in its + /// message. + #[must_use] + pub fn context(mut self, field: &'static str) -> Self { + self.path.push(field); + self + } + + /// Create a "missing field" error for a protobuf message type `T`. + pub fn missing_field(field_name: &'static str) -> Self { + Self { + path: Vec::new(), + source: Box::new(MissingFieldError { entity: type_name::(), field_name }), + } + } + + /// Create a deserialization error for a named entity. + pub fn deserialization(entity: &'static str, source: DeserializationError) -> Self { + Self { + path: Vec::new(), + source: Box::new(DeserializationErrorWrapper { entity, source }), + } } -} -pub trait MissingFieldHelper { - fn missing_field(field_name: &'static str) -> ConversionError; + /// Create a `ConversionError` from an ad-hoc error message. + pub fn message(msg: impl Into) -> Self { + Self { + path: Vec::new(), + source: Box::new(StringError(msg.into())), + } + } } -impl MissingFieldHelper for T { - fn missing_field(field_name: &'static str) -> ConversionError { - ConversionError::MissingFieldInProtobufRepresentation { - entity: type_name::(), - field_name, +impl fmt::Display for ConversionError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if !self.path.is_empty() { + // Path was pushed inner-to-outer, so reverse for display. + for (i, segment) in self.path.iter().rev().enumerate() { + if i > 0 { + f.write_str(".")?; + } + f.write_str(segment)?; + } + f.write_str(": ")?; } + write!(f, "{}", self.source) + } +} + +impl std::error::Error for ConversionError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&*self.source) } } @@ -82,3 +99,107 @@ impl From for tonic::Status { tonic::Status::invalid_argument(value.to_string()) } } + +// INTERNAL HELPER ERROR TYPES +// ================================================================================================ + +#[derive(Debug)] +struct MissingFieldError { + entity: &'static str, + field_name: &'static str, +} + +impl fmt::Display for MissingFieldError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "field `{}::{}` is missing", self.entity, self.field_name) + } +} + +impl std::error::Error for MissingFieldError {} + +#[derive(Debug)] +struct DeserializationErrorWrapper { + entity: &'static str, + source: DeserializationError, +} + +impl fmt::Display for DeserializationErrorWrapper { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "failed to deserialize {}: {}", self.entity, self.source) + } +} + +impl std::error::Error for DeserializationErrorWrapper { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&self.source) + } +} + +#[derive(Debug)] +struct StringError(String); + +impl fmt::Display for StringError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.0) + } +} + +impl std::error::Error for StringError {} + +// CONVERSION RESULT EXTENSION TRAIT +// ================================================================================================ + +/// Extension trait to ergonomically add field context to [`ConversionError`] results. +/// +/// This makes it easy to inject field names into the error path at each `?` site: +/// +/// ```rust,ignore +/// let account_root = value.account_root +/// .ok_or(ConversionError::missing_field::("account_root"))? +/// .try_into() +/// .context("account_root")?; +/// ``` +/// +/// The context stacks automatically through nested conversions, producing error paths like +/// `"header.account_root: value is not in range 0..MODULUS"`. +pub trait ConversionResultExt { + /// Add field context to the error, wrapping it in a [`ConversionError`] if needed. + fn context(self, field: &'static str) -> Result; +} + +impl> ConversionResultExt for Result { + fn context(self, field: &'static str) -> Result { + self.map_err(|e| e.into().context(field)) + } +} + +// FROM IMPLS FOR EXTERNAL ERROR TYPES +// ================================================================================================ + +macro_rules! impl_from_for_conversion_error { + ($($ty:ty),* $(,)?) => { + $( + impl From<$ty> for ConversionError { + fn from(e: $ty) -> Self { + Self::new(e) + } + } + )* + }; +} + +impl_from_for_conversion_error!( + hex::FromHexError, + miden_protocol::errors::AccountError, + miden_protocol::errors::AssetError, + miden_protocol::errors::AssetVaultError, + miden_protocol::errors::FeeError, + miden_protocol::errors::NoteError, + miden_protocol::errors::StorageSlotNameError, + miden_protocol::crypto::merkle::MerkleError, + miden_protocol::crypto::merkle::smt::SmtLeafError, + miden_protocol::crypto::merkle::smt::SmtProofError, + miden_standards::note::NetworkAccountTargetError, + std::num::TryFromIntError, + DeserializationError, +); diff --git a/crates/proto/src/lib.rs b/crates/proto/src/lib.rs index 0f5cbb8f5..1ec05672c 100644 --- a/crates/proto/src/lib.rs +++ b/crates/proto/src/lib.rs @@ -1,4 +1,5 @@ pub mod clients; +pub mod decode; pub mod domain; pub mod errors; @@ -12,3 +13,4 @@ pub use domain::account::{AccountState, AccountWitnessRecord}; pub use domain::nullifier::NullifierWitnessRecord; pub use domain::proof_request::BlockProofRequest; pub use domain::{convert, try_convert}; +pub use prost; diff --git a/crates/store/src/server/api.rs b/crates/store/src/server/api.rs index d4be86cdc..d73cad9c1 100644 --- a/crates/store/src/server/api.rs +++ b/crates/store/src/server/api.rs @@ -1,8 +1,9 @@ use std::collections::BTreeSet; use std::sync::Arc; +use miden_node_proto::decode::{ConversionResultExt, GrpcStructDecoder}; use miden_node_proto::errors::ConversionError; -use miden_node_proto::generated as proto; +use miden_node_proto::{decode, generated as proto}; use miden_node_utils::ErrorReport; use miden_protocol::Word; use miden_protocol::account::AccountId; @@ -111,8 +112,7 @@ where E: From, { block_range.ok_or_else(|| { - ConversionError::MissingFieldInProtobufRepresentation { entity, field_name: "block_range" } - .into() + ConversionError::message(format!("{entity}: missing field `block_range`")).into() }) } @@ -125,12 +125,10 @@ pub fn read_root( where E: From, { - root.ok_or_else(|| ConversionError::MissingFieldInProtobufRepresentation { - entity, - field_name: "root", - })? - .try_into() - .map_err(Into::into) + root.ok_or_else(|| ConversionError::message(format!("{entity}: missing field `root`")))? + .try_into() + .context("root") + .map_err(|e: ConversionError| e.into()) } /// Converts a collection of proto primitives to Words, returning a specific error type if @@ -145,6 +143,7 @@ where .into_iter() .map(TryInto::try_into) .collect::, ConversionError>>() + .context("digests") .map_err(Into::into) } @@ -158,23 +157,18 @@ where .cloned() .map(AccountId::try_from) .collect::>() + .context("account_ids") .map_err(Into::into) } -pub fn read_account_id(id: Option) -> Result +pub fn read_account_id( + account_id: Option, +) -> Result where E: From, { - id.ok_or_else(|| { - ConversionError::deserialization_error( - "AccountId", - miden_protocol::crypto::utils::DeserializationError::InvalidValue( - "Missing account ID".to_string(), - ), - ) - })? - .try_into() - .map_err(Into::into) + let decoder = GrpcStructDecoder::::default(); + decode!(decoder, account_id).map_err(|e: ConversionError| e.into()) } #[instrument( @@ -191,8 +185,9 @@ where nullifiers .iter() .copied() - .map(TryInto::try_into) + .map(Nullifier::try_from) .collect::>() + .context("nullifiers") .map_err(Into::into) } diff --git a/crates/store/src/server/block_producer.rs b/crates/store/src/server/block_producer.rs index 5cbe3563a..16c2ee488 100644 --- a/crates/store/src/server/block_producer.rs +++ b/crates/store/src/server/block_producer.rs @@ -1,11 +1,12 @@ use std::convert::Infallible; use miden_crypto::dsa::ecdsa_k256_keccak::Signature; +use miden_node_proto::decode::GrpcDecodeExt; use miden_node_proto::domain::proof_request::BlockProofRequest; -use miden_node_proto::errors::MissingFieldHelper; +use miden_node_proto::errors::ConversionError; use miden_node_proto::generated::store::block_producer_server; use miden_node_proto::generated::{self as proto}; -use miden_node_proto::try_convert; +use miden_node_proto::{decode, try_convert}; use miden_node_utils::ErrorReport; use miden_node_utils::tracing::OpenTelemetrySpanExt; use miden_protocol::Word; @@ -57,22 +58,12 @@ impl block_producer_server::BlockProducer for StoreApi { // Read block. let block = request .block - .ok_or(proto::store::ApplyBlockRequest::missing_field(stringify!(block)))?; - // Read block header. - let header: BlockHeader = block - .header - .ok_or(proto::blockchain::SignedBlock::missing_field(stringify!(header)))? - .try_into()?; - // Read block body. - let body: BlockBody = block - .body - .ok_or(proto::blockchain::SignedBlock::missing_field(stringify!(body)))? - .try_into()?; - // Read signature. - let signature: Signature = block - .signature - .ok_or(proto::blockchain::SignedBlock::missing_field(stringify!(signature)))? - .try_into()?; + .ok_or(ConversionError::missing_field::("block"))?; + // Decode block fields. + let decoder = block.decoder(); + let header: BlockHeader = decode!(decoder, block.header)?; + let body: BlockBody = decode!(decoder, block.body)?; + let signature: Signature = decode!(decoder, block.signature)?; // Get block inputs from ordered batches. let block_inputs = @@ -202,7 +193,8 @@ impl block_producer_server::BlockProducer for StoreApi { ) -> Result, Status> { let request = request.into_inner(); - let account_id = read_account_id::(request.account_id)?; + let account_id = + read_account_id::(request.account_id)?; let nullifiers = validate_nullifiers(&request.nullifiers) .map_err(|err| conversion_error_to_status(&err))?; let unauthenticated_note_commitments = diff --git a/crates/store/src/server/ntx_builder.rs b/crates/store/src/server/ntx_builder.rs index 41217c4da..f7973f51f 100644 --- a/crates/store/src/server/ntx_builder.rs +++ b/crates/store/src/server/ntx_builder.rs @@ -81,7 +81,8 @@ impl ntx_builder_server::NtxBuilder for StoreApi { &self, request: Request, ) -> Result, Status> { - let account_id = read_account_id::(Some(request.into_inner()))?; + let account_id = + read_account_id::(Some(request.into_inner()))?; let account_info: Option = self.state.get_network_account_details_by_id(account_id).await?; @@ -97,7 +98,9 @@ impl ntx_builder_server::NtxBuilder for StoreApi { ) -> Result, Status> { let request = request.into_inner(); let block_num = BlockNumber::from(request.block_num); - let account_id = read_account_id::(request.account_id)?; + let account_id = read_account_id::( + request.account_id, + )?; let state = self.state.clone(); @@ -223,8 +226,11 @@ impl ntx_builder_server::NtxBuilder for StoreApi { } // Read account ID. - let account_id = - read_account_id::(request.account_id).map_err(invalid_argument)?; + let account_id = read_account_id::< + proto::store::VaultAssetWitnessesRequest, + GetWitnessesError, + >(request.account_id) + .map_err(invalid_argument)?; // Read vault keys. let vault_keys = request @@ -280,7 +286,10 @@ impl ntx_builder_server::NtxBuilder for StoreApi { // Read the account ID. let account_id = - read_account_id::(request.account_id).map_err(invalid_argument)?; + read_account_id::( + request.account_id, + ) + .map_err(invalid_argument)?; // Read the map key. let map_key = read_root::(request.map_key, "MapKey") diff --git a/crates/store/src/server/rpc_api.rs b/crates/store/src/server/rpc_api.rs index 2ef33295e..7d4875e03 100644 --- a/crates/store/src/server/rpc_api.rs +++ b/crates/store/src/server/rpc_api.rs @@ -1,6 +1,6 @@ use miden_node_proto::convert; use miden_node_proto::domain::block::InvalidBlockRange; -use miden_node_proto::errors::MissingFieldHelper; +use miden_node_proto::errors::ConversionError; use miden_node_proto::generated::store::rpc_server; use miden_node_proto::generated::{self as proto}; use miden_node_utils::limiter::{ @@ -163,7 +163,9 @@ impl rpc_server::Rpc for StoreApi { let block_range = request .block_range - .ok_or_else(|| proto::rpc::SyncChainMmrRequest::missing_field(stringify!(block_range))) + .ok_or_else(|| { + ConversionError::missing_field::("block_range") + }) .map_err(SyncChainMmrError::DeserializationFailed)?; // Determine the effective tip based on the requested finality level. @@ -272,7 +274,10 @@ impl rpc_server::Rpc for StoreApi { let request = request.into_inner(); let chain_tip = self.state.latest_block_num().await; - let account_id: AccountId = read_account_id::(request.account_id)?; + let account_id: AccountId = read_account_id::< + proto::rpc::SyncAccountVaultRequest, + SyncAccountVaultError, + >(request.account_id)?; if !account_id.has_public_state() { return Err(SyncAccountVaultError::AccountNotPublic(account_id).into()); @@ -320,7 +325,10 @@ impl rpc_server::Rpc for StoreApi { ) -> Result, Status> { let request = request.into_inner(); - let account_id = read_account_id::(request.account_id)?; + let account_id = read_account_id::< + proto::rpc::SyncAccountStorageMapsRequest, + SyncAccountStorageMapsError, + >(request.account_id)?; if !account_id.has_public_state() { Err(SyncAccountStorageMapsError::AccountNotPublic(account_id))?;