use alloc::collections::BTreeMap;
use alloc::vec::Vec;
use core::fmt::{self, Debug, Formatter};
use miden_protocol::account::{
Account, AccountCode, AccountHeader, AccountId, AccountStorage, AccountStorageHeader,
StorageMap, StorageMapKey, StorageSlot, StorageSlotName, StorageSlotType,
};
use miden_protocol::asset::{Asset, AssetVault};
use miden_protocol::block::BlockNumber;
use miden_protocol::block::account_tree::AccountWitness;
use miden_protocol::crypto::merkle::SparseMerklePath;
use miden_protocol::crypto::merkle::smt::PartialSmt;
use miden_objects::DecodeMessageExt;
use miden_protocol::{EMPTY_WORD, Word};
use miden_tx::utils::serde::{Deserializable, Serializable};
use thiserror::Error;
use crate::alloc::string::ToString;
use crate::rpc::{AccountStateAt, RpcError};
use crate::rpc::domain::MissingFieldHelper;
use crate::rpc::generated::rpc::account_request::account_detail_request::storage_map_detail_request::{MapKeys, SlotData};
use crate::rpc::generated::rpc::account_request::account_detail_request::{
StorageMapDetailRequest, StorageMapDetailRequests, StorageRequest,
};
use crate::rpc::generated::{self as proto};
impl Debug for proto::rpc::RegisterAccountRequest {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("RegisterAccountRequest")
.field("account_id", &self.account_id)
.finish_non_exhaustive()
}
}
#[cfg(feature = "tonic")]
impl proto::rpc::account_response::AccountDetails {
pub fn into_domain(
self,
known_account_codes: &BTreeMap<Word, AccountCode>,
storage_requirements: &AccountStorageRequirements,
) -> Result<AccountDetails, crate::rpc::RpcError> {
use crate::rpc::RpcError;
use crate::rpc::domain::MissingFieldHelper;
let proto::rpc::account_response::AccountDetails {
header,
storage_details,
code,
vault_details,
} = self;
let header: AccountHeader = header
.ok_or(proto::rpc::account_response::AccountDetails::missing_field(stringify!(header)))?
.decode_and_verify()?;
let storage_details: AccountStorageDetails = storage_details
.ok_or(proto::rpc::account_response::AccountDetails::missing_field(stringify!(
storage_details
)))?
.try_into()?;
storage_details.validate_against_request(storage_requirements)?;
let code = {
let received_code: Option<AccountCode> =
code.map(DecodeMessageExt::decode_and_verify).transpose()?;
match received_code {
Some(code) => code,
None => known_account_codes
.get(&header.code_commitment())
.ok_or(RpcError::InvalidResponse(
"Account code was not provided, but the response did not contain it either"
.into(),
))?
.clone(),
}
};
let vault_details = vault_details
.ok_or(proto::rpc::AccountVaultDetails::missing_field(stringify!(vault_details)))?
.try_into()?;
Ok(AccountDetails {
header,
storage_details,
code,
vault_details,
})
}
}
pub type AccountProofs = (BlockNumber, Vec<AccountProof>);
#[derive(Clone, Debug)]
pub struct AccountDetails {
pub header: AccountHeader,
pub storage_details: AccountStorageDetails,
pub code: AccountCode,
pub vault_details: AccountVaultDetails,
}
impl TryFrom<&AccountDetails> for Account {
type Error = RpcError;
fn try_from(details: &AccountDetails) -> Result<Self, Self::Error> {
if details.vault_details.too_many_assets {
return Err(RpcError::ExpectedDataMissing(
"cannot build account: vault has too many assets".into(),
));
}
if let Some(slot_name) = details
.storage_details
.map_details
.iter()
.find(|m| m.is_limit_exceeded())
.map(|m| &m.slot_name)
{
return Err(RpcError::ExpectedDataMissing(format!(
"cannot build account: storage map slot '{slot_name}' has too many entries",
)));
}
let mut slots: Vec<StorageSlot> = Vec::new();
for slot_header in details.storage_details.header.slots() {
match slot_header.slot_type() {
StorageSlotType::Value => {
slots.push(StorageSlot::with_value(
slot_header.name().clone(),
slot_header.value(),
));
},
StorageSlotType::Map => {
let map_details = details
.storage_details
.find_map_details(slot_header.name())
.ok_or_else(|| {
RpcError::ExpectedDataMissing(format!(
"slot '{}' is a map but has no map_details in response",
slot_header.name()
))
})?;
let storage_map = map_details
.entries
.clone()
.into_storage_map()
.ok_or_else(|| {
RpcError::ExpectedDataMissing(format!(
"slot '{}' did not come back with all its entries, so the full \
account cannot be built",
slot_header.name(),
))
})?
.map_err(|err| {
RpcError::InvalidResponse(format!(
"the rpc api returned a non-valid map entry: {err}"
))
})?;
slots.push(StorageSlot::with_map(slot_header.name().clone(), storage_map));
},
}
}
let asset_vault = AssetVault::new(&details.vault_details.assets).map_err(|err| {
RpcError::InvalidResponse(format!("rpc api returned non-valid assets: {err}"))
})?;
let account_storage = AccountStorage::new(slots).map_err(|err| {
RpcError::InvalidResponse(format!("rpc api returned non-valid storage slots: {err}"))
})?;
Account::new(
details.header.id(),
asset_vault,
account_storage,
details.code.clone(),
details.header.nonce(),
None,
)
.map_err(|err| {
RpcError::InvalidResponse(format!(
"failed to construct account from rpc api response: {err}"
))
})
}
}
#[derive(Clone, Debug)]
pub struct AccountStorageDetails {
pub header: AccountStorageHeader,
pub map_details: Vec<AccountStorageMapDetails>,
}
impl AccountStorageDetails {
pub fn find_map_details(&self, target: &StorageSlotName) -> Option<&AccountStorageMapDetails> {
self.map_details.iter().find(|map_detail| map_detail.slot_name == *target)
}
pub fn validate_against_request(
&self,
storage_requirements: &AccountStorageRequirements,
) -> Result<(), RpcError> {
for map_detail in &self.map_details {
let StorageMapEntries::PartialMap { map_keys, .. } = &map_detail.entries else {
continue;
};
let requested_keys = storage_requirements.keys_for_slot(&map_detail.slot_name);
if let Some(key) = requested_keys.iter().find(|key| !map_keys.contains(key)) {
return Err(RpcError::InvalidResponse(format!(
"partial storage map for slot '{}' does not cover requested key {}",
map_detail.slot_name,
key.to_hex(),
)));
}
if let Some(key) = map_keys.iter().find(|key| !requested_keys.contains(key)) {
return Err(RpcError::InvalidResponse(format!(
"partial storage map for slot '{}' covers key {}, which was not requested",
map_detail.slot_name,
key.to_hex(),
)));
}
}
Ok(())
}
}
impl TryFrom<proto::rpc::AccountStorageDetails> for AccountStorageDetails {
type Error = RpcError;
fn try_from(value: proto::rpc::AccountStorageDetails) -> Result<Self, Self::Error> {
let header: AccountStorageHeader = value
.header
.ok_or(proto::account::AccountStorageHeader::missing_field(stringify!(header)))?
.decode_and_verify()?;
let map_details = value
.map_details
.into_iter()
.map(core::convert::TryInto::try_into)
.collect::<Result<Vec<AccountStorageMapDetails>, RpcError>>()?;
for map_detail in &map_details {
let StorageMapEntries::PartialMap { partial_smt, .. } = &map_detail.entries else {
continue;
};
let slot = header
.slots()
.find(|slot| *slot.name() == map_detail.slot_name)
.ok_or_else(|| {
RpcError::InvalidResponse(format!(
"partial storage map references slot '{}', which is absent from the \
storage header",
map_detail.slot_name,
))
})?;
if slot.slot_type() != StorageSlotType::Map {
return Err(RpcError::InvalidResponse(format!(
"partial storage map references slot '{}', which is not a map",
map_detail.slot_name,
)));
}
if partial_smt.root() != slot.value() {
return Err(RpcError::InvalidResponse(format!(
"partial storage map for slot '{}' has root {} but the storage header reports \
{}",
map_detail.slot_name,
partial_smt.root(),
slot.value(),
)));
}
}
Ok(Self { header, map_details })
}
}
#[derive(Clone, Debug)]
pub struct AccountStorageMapDetails {
pub slot_name: StorageSlotName,
pub entries: StorageMapEntries,
}
impl AccountStorageMapDetails {
pub const MAX_PARTIAL_MAP_KEYS: usize = 64;
pub fn is_limit_exceeded(&self) -> bool {
matches!(self.entries, StorageMapEntries::LimitExceeded)
}
}
impl TryFrom<proto::rpc::account_storage_details::AccountStorageMapDetails>
for AccountStorageMapDetails
{
type Error = RpcError;
fn try_from(
value: proto::rpc::account_storage_details::AccountStorageMapDetails,
) -> Result<Self, Self::Error> {
use proto::rpc::account_storage_details::account_storage_map_details::Result as ProtoResult;
let slot_name = StorageSlotName::new(value.slot_name)
.map_err(|err| RpcError::ExpectedDataMissing(err.to_string()))?;
let entries = match value.result {
Some(ProtoResult::TooManyEntries(true)) => StorageMapEntries::LimitExceeded,
Some(ProtoResult::TooManyEntries(false)) => {
return Err(RpcError::InvalidResponse(
"too_many_entries must be true when set".into(),
));
},
Some(ProtoResult::AllEntries(all_entries)) => {
let entries = all_entries
.entries
.into_iter()
.map(core::convert::TryInto::try_into)
.collect::<Result<Vec<StorageMapEntry>, RpcError>>()?;
StorageMapEntries::AllEntries(entries)
},
Some(ProtoResult::PartialMap(partial_map)) => {
if partial_map.map_keys.len() > Self::MAX_PARTIAL_MAP_KEYS {
return Err(RpcError::InvalidResponse(format!(
"partial storage map for slot '{slot_name}' contains {} keys, exceeding \
the limit of {}",
partial_map.map_keys.len(),
Self::MAX_PARTIAL_MAP_KEYS,
)));
}
let map_keys = partial_map
.map_keys
.into_iter()
.map(|key| Word::try_from(key).map(StorageMapKey::new))
.collect::<Result<Vec<_>, _>>()?;
if let Some(key) = first_duplicate_key(&map_keys) {
return Err(RpcError::InvalidResponse(format!(
"partial storage map for slot '{slot_name}' repeats key {}",
key.to_hex(),
)));
}
let partial_smt: PartialSmt = partial_map
.partial_smt
.ok_or(proto::rpc::account_storage_details::account_storage_map_details::PartialStorageMap::missing_field(
stringify!(partial_smt),
))?
.decode_and_verify()?;
for key in &map_keys {
partial_smt.get_value(&key.hash().as_word()).map_err(|_| {
RpcError::InvalidResponse(format!(
"partial storage map for slot '{slot_name}' does not track key {}",
key.to_hex(),
))
})?;
}
StorageMapEntries::PartialMap { map_keys, partial_smt }
},
None => {
return Err(RpcError::InvalidResponse(format!(
"storage map details for slot '{slot_name}' carry no result",
)));
},
};
Ok(Self { slot_name, entries })
}
}
fn first_duplicate_key(keys: &[StorageMapKey]) -> Option<&StorageMapKey> {
keys.iter()
.enumerate()
.find_map(|(index, key)| keys[..index].contains(key).then_some(key))
}
#[derive(Clone, Debug)]
pub struct StorageMapEntry {
pub key: StorageMapKey,
pub value: Word,
}
impl TryFrom<proto::rpc::account_storage_details::account_storage_map_details::all_map_entries::StorageMapEntry>
for StorageMapEntry
{
type Error = RpcError;
fn try_from(value: proto::rpc::account_storage_details::account_storage_map_details::all_map_entries::StorageMapEntry) -> Result<Self, Self::Error> {
let key = Word::try_from(value.key.ok_or(RpcError::ExpectedDataMissing("key".into()))?)
.map(StorageMapKey::new)?;
let value = value.value.ok_or(RpcError::ExpectedDataMissing("value".into()))?.try_into()?;
Ok(Self { key, value })
}
}
#[derive(Clone, Debug)]
pub enum StorageMapEntries {
LimitExceeded,
AllEntries(Vec<StorageMapEntry>),
PartialMap {
map_keys: Vec<StorageMapKey>,
partial_smt: PartialSmt,
},
}
impl StorageMapEntries {
pub fn into_storage_map(
self,
) -> Option<Result<StorageMap, miden_protocol::errors::StorageMapError>> {
match self {
StorageMapEntries::AllEntries(entries) => {
Some(StorageMap::with_entries(entries.into_iter().map(|e| (e.key, e.value))))
},
StorageMapEntries::LimitExceeded | StorageMapEntries::PartialMap { .. } => None,
}
}
}
#[derive(Clone, Debug)]
pub struct AccountVaultDetails {
pub too_many_assets: bool,
pub assets: Vec<Asset>,
}
impl TryFrom<proto::rpc::AccountVaultDetails> for AccountVaultDetails {
type Error = RpcError;
fn try_from(value: proto::rpc::AccountVaultDetails) -> Result<Self, Self::Error> {
let too_many_assets = value.too_many_assets;
let assets = value
.assets
.into_iter()
.map(DecodeMessageExt::decode_and_verify)
.collect::<Result<Vec<Asset>, _>>()?;
Ok(Self { too_many_assets, assets })
}
}
#[derive(Clone, Debug)]
pub struct AccountProof {
account_witness: AccountWitness,
state_headers: Option<AccountDetails>,
}
impl AccountProof {
pub fn new(
account_witness: AccountWitness,
account_details: Option<AccountDetails>,
) -> Result<Self, AccountProofError> {
if let Some(AccountDetails {
header: account_header,
storage_details: _,
code,
..
}) = &account_details
{
if account_header.to_commitment() != account_witness.state_commitment() {
return Err(AccountProofError::InconsistentAccountCommitment);
}
if account_header.id() != account_witness.id() {
return Err(AccountProofError::InconsistentAccountId);
}
if code.commitment() != account_header.code_commitment() {
return Err(AccountProofError::InconsistentCodeCommitment);
}
}
Ok(Self {
account_witness,
state_headers: account_details,
})
}
pub fn account_id(&self) -> AccountId {
self.account_witness.id()
}
pub fn account_header(&self) -> Option<&AccountHeader> {
self.state_headers.as_ref().map(|account_details| &account_details.header)
}
pub fn storage_header(&self) -> Option<&AccountStorageHeader> {
self.state_headers
.as_ref()
.map(|account_details| &account_details.storage_details.header)
}
pub fn storage_details(&self) -> Option<&AccountStorageDetails> {
self.state_headers.as_ref().map(|d| &d.storage_details)
}
pub fn vault_details(&self) -> Option<&AccountVaultDetails> {
self.state_headers.as_ref().map(|d| &d.vault_details)
}
pub fn find_map_details(
&self,
slot_name: &StorageSlotName,
) -> Option<&AccountStorageMapDetails> {
self.state_headers
.as_ref()
.and_then(|details| details.storage_details.find_map_details(slot_name))
}
pub fn account_code(&self) -> Option<&AccountCode> {
self.state_headers.as_ref().map(|headers| &headers.code)
}
pub fn code_commitment(&self) -> Option<Word> {
self.account_code().map(AccountCode::commitment)
}
pub fn account_commitment(&self) -> Word {
self.account_witness.state_commitment()
}
pub fn account_witness(&self) -> &AccountWitness {
&self.account_witness
}
pub fn merkle_proof(&self) -> &SparseMerklePath {
self.account_witness.path()
}
pub fn into_parts(self) -> (AccountWitness, Option<AccountDetails>) {
(self.account_witness, self.state_headers)
}
pub fn into_details(self) -> Option<AccountDetails> {
self.state_headers
}
pub fn details_mut(&mut self) -> Option<&mut AccountDetails> {
self.state_headers.as_mut()
}
}
#[cfg(feature = "tonic")]
impl TryFrom<proto::rpc::AccountResponse> for AccountProof {
type Error = RpcError;
fn try_from(account_proof: proto::rpc::AccountResponse) -> Result<Self, Self::Error> {
let Some(witness) = account_proof.witness else {
return Err(RpcError::ExpectedDataMissing(
"GetAccount returned an account without witness".to_string(),
));
};
let details: Option<AccountDetails> = {
match account_proof.details {
None => None,
Some(details) => Some(
details
.into_domain(&BTreeMap::new(), &AccountStorageRequirements::default())?,
),
}
};
AccountProof::new(witness.decode_and_verify()?, details)
.map_err(|err| RpcError::InvalidResponse(format!("{err}")))
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct AccountStorageRequirements(BTreeMap<StorageSlotName, Vec<StorageMapKey>>);
impl AccountStorageRequirements {
pub fn new<'a>(
slots_and_keys: impl IntoIterator<
Item = (StorageSlotName, impl IntoIterator<Item = &'a StorageMapKey>),
>,
) -> Self {
let map = slots_and_keys
.into_iter()
.map(|(slot_name, keys_iter)| {
let mut keys_vec: Vec<StorageMapKey> = Vec::new();
for key in keys_iter {
if !keys_vec.contains(key) {
keys_vec.push(*key);
}
}
(slot_name, keys_vec)
})
.collect();
AccountStorageRequirements(map)
}
pub fn all_entries(slot_names: &[StorageSlotName]) -> Self {
AccountStorageRequirements(
slot_names.iter().map(|name| (name.clone(), Vec::new())).collect(),
)
}
pub fn inner(&self) -> &BTreeMap<StorageSlotName, Vec<StorageMapKey>> {
&self.0
}
pub fn keys_for_slot(&self, slot_name: &StorageSlotName) -> &[StorageMapKey] {
self.0.get(slot_name).map_or(&[], Vec::as_slice)
}
}
impl From<AccountStorageRequirements> for Vec<StorageMapDetailRequest> {
fn from(value: AccountStorageRequirements) -> Vec<StorageMapDetailRequest> {
let request_map = value.0;
let mut requests = Vec::with_capacity(request_map.len());
for (slot_name, map_keys) in request_map {
let slot_data = if map_keys.is_empty() {
Some(SlotData::AllEntries(true))
} else {
let keys = map_keys.into_iter().map(|key| Word::from(key).into()).collect();
Some(SlotData::MapKeys(MapKeys { map_keys: keys }))
};
requests.push(StorageMapDetailRequest {
slot_name: slot_name.to_string(),
slot_data,
});
}
requests
}
}
impl Serializable for AccountStorageRequirements {
fn write_into<W: miden_tx::utils::serde::ByteWriter>(&self, target: &mut W) {
target.write(&self.0);
}
}
impl Deserializable for AccountStorageRequirements {
fn read_from<R: miden_tx::utils::serde::ByteReader>(
source: &mut R,
) -> Result<Self, miden_tx::utils::serde::DeserializationError> {
Ok(AccountStorageRequirements(source.read()?))
}
}
#[derive(Clone, Debug, Default)]
pub enum VaultFetch {
#[default]
Skip,
Always,
IfChangedFrom(Word),
}
impl From<VaultFetch> for Option<proto::primitives::Word> {
fn from(vault: VaultFetch) -> Self {
match vault {
VaultFetch::Skip => None,
VaultFetch::Always => Some(EMPTY_WORD.into()),
VaultFetch::IfChangedFrom(commitment) => Some(commitment.into()),
}
}
}
#[derive(Clone, Debug, Default)]
pub enum StorageMapFetch {
#[default]
Skip,
All,
Slots(AccountStorageRequirements),
}
impl From<StorageMapFetch> for Option<StorageRequest> {
fn from(storage: StorageMapFetch) -> Self {
match storage {
StorageMapFetch::Skip => None,
StorageMapFetch::All => Some(StorageRequest::AllStorageMaps(true)),
StorageMapFetch::Slots(reqs) => {
Some(StorageRequest::StorageMaps(StorageMapDetailRequests {
storage_maps: reqs.into(),
}))
},
}
}
}
#[derive(Clone, Debug, Default)]
pub struct GetAccountRequest {
pub storage: StorageMapFetch,
pub at: AccountStateAt,
pub known_code: Option<AccountCode>,
pub vault: VaultFetch,
}
impl GetAccountRequest {
#[must_use]
pub fn new() -> Self {
Self {
storage: StorageMapFetch::Skip,
at: AccountStateAt::ChainTip,
known_code: None,
vault: VaultFetch::Skip,
}
}
#[must_use]
pub fn with_storage(mut self, storage: StorageMapFetch) -> Self {
self.storage = storage;
self
}
#[must_use]
pub fn at(mut self, at: AccountStateAt) -> Self {
self.at = at;
self
}
#[must_use]
pub fn with_known_code(mut self, known_code: Option<AccountCode>) -> Self {
self.known_code = known_code;
self
}
#[must_use]
pub fn with_vault(mut self, vault: VaultFetch) -> Self {
self.vault = vault;
self
}
}
#[derive(Debug, Error)]
pub enum AccountProofError {
#[error(
"the received account commitment doesn't match the received account header's commitment"
)]
InconsistentAccountCommitment,
#[error("the received account id doesn't match the received account header's id")]
InconsistentAccountId,
#[error(
"the received code commitment doesn't match the received account header's code commitment"
)]
InconsistentCodeCommitment,
}