use alloc::collections::{BTreeMap, BTreeSet};
use alloc::vec::Vec;
use core::fmt::{self, Debug, Formatter};
use miden_protocol::account::{
Account, AccountCode, AccountHeader, AccountId, AccountStorage, AccountStorageHeader,
PartialAccount, PartialStorage, PartialStorageMap, StorageMap, StorageMapKey, StorageSlot,
StorageSlotHeader, StorageSlotName, StorageSlotType,
};
use miden_protocol::asset::{Asset, AssetId, AssetVault, PartialVault};
use miden_protocol::block::account_tree::AccountWitness;
use miden_protocol::crypto::merkle::SparseMerklePath;
use miden_protocol::crypto::merkle::smt::PartialSmt;
use miden_protocol::transaction::AccountInputs;
use miden_objects::DecodeMessageExt;
use miden_protocol::{EMPTY_WORD, Word};
use miden_tx::utils::serde::{Deserializable, DeserializationError, Serializable};
use thiserror::Error;
use crate::alloc::string::ToString;
use crate::rpc::{AccountStateAt, RpcError};
use crate::rpc::domain::MissingFieldHelper;
use crate::rpc::generated::rpc::get_account_request::account_detail_request::storage_map_detail_request::{MapKeys, SlotData};
use crate::rpc::generated::rpc::get_account_request::account_detail_request::{
StorageMapDetailRequest, StorageMapDetailRequests, StorageRequest, VaultAssetIds, VaultRequest,
};
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()
}
}
impl Debug for proto::rpc::IsInvitationCodeValidRequest {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("IsInvitationCodeValidRequest").finish_non_exhaustive()
}
}
#[cfg(feature = "tonic")]
impl proto::rpc::get_account_response::AccountDetails {
pub fn into_domain(
self,
known_account_codes: &BTreeMap<Word, AccountCode>,
fetch: &AccountFetch,
) -> Result<AccountDetails, crate::rpc::RpcError> {
use crate::rpc::RpcError;
use crate::rpc::domain::MissingFieldHelper;
let proto::rpc::get_account_response::AccountDetails {
header,
storage_details,
code,
vault_details,
} = self;
let header: AccountHeader = header
.ok_or(proto::rpc::get_account_response::AccountDetails::missing_field(stringify!(
header
)))?
.decode_and_verify()?;
let storage_details: AccountStorageDetails = storage_details
.ok_or(proto::rpc::get_account_response::AccountDetails::missing_field(stringify!(
storage_details
)))?
.try_into()?;
storage_details.validate_against_request(fetch.storage())?;
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 = AccountVaultDetails::try_from(
vault_details
.ok_or(proto::rpc::AccountVaultDetails::missing_field(stringify!(vault_details)))?,
)?
.validate_against_request(header.vault_root(), fetch.vault())?;
Ok(AccountDetails {
header,
storage_details,
code,
vault_details,
})
}
}
#[derive(Clone, Debug)]
pub struct AccountDetails {
pub header: AccountHeader,
pub storage_details: AccountStorageDetails,
pub code: AccountCode,
pub vault_details: AccountVaultDetails,
}
impl TryFrom<AccountDetails> for PartialAccount {
type Error = RpcError;
fn try_from(details: AccountDetails) -> Result<Self, Self::Error> {
let AccountDetails {
header,
storage_details,
code,
vault_details,
} = details;
let mut storage_maps = Vec::with_capacity(storage_details.map_details.len());
for map_details in storage_details.map_details {
let map = match map_details.entries {
StorageMapEntries::AllEntries(entries) => {
let map = StorageMap::with_entries(
entries.iter().map(|entry| (entry.key, entry.value)),
)
.map_err(|err| {
RpcError::InvalidResponse(format!("invalid storage map entries: {err}"))
})?;
let slot_root = storage_details
.header
.slots()
.find(|slot| *slot.name() == map_details.slot_name)
.map(StorageSlotHeader::value);
if Some(map.root()) != slot_root {
return Err(RpcError::InvalidResponse(format!(
"storage map entries for slot '{}' do not match the slot root",
map_details.slot_name
)));
}
PartialStorageMap::new_full(map)
},
StorageMapEntries::PartialMap { map_keys, partial_smt } => {
PartialStorageMap::try_from_parts(partial_smt, map_keys).map_err(|err| {
RpcError::InvalidResponse(format!("invalid storage map proof: {err}"))
})?
},
StorageMapEntries::LimitExceeded => continue,
};
storage_maps.push(map);
}
let vault = match vault_details {
AccountVaultDetails::Assets(assets) => {
let vault = AssetVault::new(&assets).map_err(|err| {
RpcError::InvalidResponse(format!("invalid asset list: {err}"))
})?;
if vault.root() == header.vault_root() {
PartialVault::new_full(vault)
} else if assets.is_empty() {
PartialVault::new(header.vault_root())
} else {
return Err(RpcError::InvalidResponse(
"asset list does not match the vault root".into(),
));
}
},
AccountVaultDetails::Partial(vault) => {
if vault.root() != header.vault_root() {
return Err(RpcError::InvalidResponse(
"vault proofs do not match the vault root".into(),
));
}
vault
},
AccountVaultDetails::TooManyAssets => PartialVault::new(header.vault_root()),
};
let storage = PartialStorage::new(storage_details.header, storage_maps)
.map_err(|err| RpcError::InvalidResponse(format!("invalid partial storage: {err}")))?;
let account = PartialAccount::new(header.id(), header.nonce(), code, storage, vault, None)
.map_err(|err| RpcError::InvalidResponse(format!("invalid partial account: {err}")))?;
if account.to_commitment() != header.to_commitment() {
return Err(RpcError::InvalidResponse(
"partial account does not match its header".into(),
));
}
Ok(account)
}
}
impl TryFrom<&AccountDetails> for Account {
type Error = RpcError;
fn try_from(details: &AccountDetails) -> Result<Self, Self::Error> {
let assets = match &details.vault_details {
AccountVaultDetails::Assets(assets) => assets,
AccountVaultDetails::TooManyAssets => {
return Err(RpcError::ExpectedDataMissing(
"cannot build account: vault has too many assets".into(),
));
},
AccountVaultDetails::Partial(_) => {
return Err(RpcError::ExpectedDataMissing(
"cannot build account: only specific vault assets were requested".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(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: &StorageMapFetch) -> Result<(), RpcError> {
for map_detail in &self.map_details {
let StorageMapEntries::PartialMap { map_keys, .. } = &map_detail.entries else {
continue;
};
let requested_keys = match storage {
StorageMapFetch::Slots(requirements) => {
requirements.keys_for_slot(&map_detail.slot_name)
},
StorageMapFetch::Skip | StorageMapFetch::All => &[],
};
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 enum AccountVaultDetails {
TooManyAssets,
Assets(Vec<Asset>),
Partial(PartialVault),
}
impl AccountVaultDetails {
pub fn is_too_many_assets(&self) -> bool {
matches!(self, Self::TooManyAssets)
}
pub fn assets(&self) -> Option<&[Asset]> {
match self {
Self::Assets(assets) => Some(assets),
Self::TooManyAssets | Self::Partial(_) => None,
}
}
pub fn into_partial_vault(self) -> Option<PartialVault> {
match self {
Self::Partial(partial_vault) => Some(partial_vault),
Self::TooManyAssets | Self::Assets(_) => None,
}
}
#[cfg(feature = "tonic")]
pub(crate) fn validate_against_request(
self,
vault_root: Word,
vault_request: &VaultFetch,
) -> Result<Self, RpcError> {
let VaultFetch::Assets(asset_ids) = vault_request else {
if matches!(self, Self::Partial(_)) {
return Err(RpcError::InvalidResponse(
"the response contains vault proofs that were not requested".into(),
));
}
return Ok(self);
};
let partial_vault = match self {
Self::Partial(partial_vault) => {
if let Some(asset_id) =
partial_vault.asset_ids().find(|asset_id| !asset_ids.contains(asset_id))
{
return Err(RpcError::InvalidResponse(format!(
"the vault proofs track asset {asset_id}, which was not requested"
)));
}
partial_vault
},
Self::TooManyAssets => {
return Err(RpcError::InvalidResponse(
"the node did not prove the requested assets because the vault is too large"
.into(),
));
},
Self::Assets(assets) => {
PartialVault::new_full(AssetVault::new(&assets).map_err(|err| {
RpcError::InvalidResponse(format!("rpc api returned non-valid assets: {err}"))
})?)
},
};
if partial_vault.root() != vault_root {
return Err(RpcError::InvalidResponse(format!(
"the vault proofs verify against root {} but the account header has vault root \
{vault_root}",
partial_vault.root(),
)));
}
if let Some(asset_id) =
asset_ids.iter().find(|asset_id| partial_vault.open(**asset_id).is_err())
{
return Err(RpcError::InvalidResponse(format!(
"the vault proofs do not cover requested asset {asset_id}"
)));
}
Ok(Self::Partial(partial_vault))
}
}
impl TryFrom<proto::rpc::AccountVaultDetails> for AccountVaultDetails {
type Error = RpcError;
fn try_from(value: proto::rpc::AccountVaultDetails) -> Result<Self, Self::Error> {
let proto::rpc::AccountVaultDetails { too_many_assets, assets, partial_vault } = value;
match (too_many_assets, assets.is_empty(), partial_vault) {
(false, _, None) => Ok(Self::Assets(
assets
.into_iter()
.map(DecodeMessageExt::decode_and_verify)
.collect::<Result<Vec<Asset>, _>>()?,
)),
(true, true, None) => Ok(Self::TooManyAssets),
(false, true, Some(partial_vault)) => Ok(Self::Partial(partial_vault.try_into()?)),
_ => Err(RpcError::InvalidResponse(
"the vault details set more than one of the asset list, the vault proofs, and the \
too many assets flag"
.into(),
)),
}
}
}
impl TryFrom<proto::rpc::account_vault_details::PartialVault> for PartialVault {
type Error = RpcError;
fn try_from(
value: proto::rpc::account_vault_details::PartialVault,
) -> Result<Self, Self::Error> {
let asset_ids = value
.asset_ids
.into_iter()
.map(DecodeMessageExt::decode_and_verify)
.collect::<Result<Vec<AssetId>, _>>()?;
let partial_smt: PartialSmt = value
.partial_smt
.ok_or(proto::rpc::account_vault_details::PartialVault::missing_field(stringify!(
partial_smt
)))?
.decode_and_verify()?;
PartialVault::try_from_parts(partial_smt, asset_ids).map_err(|err| {
RpcError::InvalidResponse(format!("rpc api returned a non-valid partial vault: {err}"))
})
}
}
#[derive(Clone, Debug)]
pub struct AccountProof {
account_witness: AccountWitness,
state_headers: Option<AccountDetails>,
}
impl AccountProof {
pub fn into_account_inputs(self) -> Result<AccountInputs, RpcError> {
let (witness, details) = self.into_parts();
let details = details
.ok_or_else(|| RpcError::ExpectedDataMissing("public account details".into()))?;
Ok(AccountInputs::new(details.try_into()?, witness))
}
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::GetAccountResponse> for AccountProof {
type Error = RpcError;
fn try_from(account_proof: proto::rpc::GetAccountResponse) -> 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(), &AccountFetch::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, Eq, PartialEq)]
pub enum VaultFetch {
#[default]
Skip,
Always,
IfChangedFrom(Word),
Assets(BTreeSet<AssetId>),
}
impl Serializable for VaultFetch {
fn write_into<W: miden_tx::utils::serde::ByteWriter>(&self, target: &mut W) {
match self {
Self::Skip => target.write_u8(0),
Self::Always => target.write_u8(1),
Self::IfChangedFrom(root) => {
target.write_u8(2);
target.write(root);
},
Self::Assets(asset_ids) => {
target.write_u8(3);
target.write(asset_ids);
},
}
}
}
impl Deserializable for VaultFetch {
fn read_from<R: miden_tx::utils::serde::ByteReader>(
source: &mut R,
) -> Result<Self, DeserializationError> {
match source.read_u8()? {
0 => Ok(Self::Skip),
1 => Ok(Self::Always),
2 => Ok(Self::IfChangedFrom(source.read()?)),
3 => Ok(Self::Assets(source.read()?)),
tag => Err(DeserializationError::InvalidValue(format!(
"invalid vault requirements tag: {tag}"
))),
}
}
}
impl From<VaultFetch> for Option<VaultRequest> {
fn from(vault: VaultFetch) -> Self {
let commitment = match vault {
VaultFetch::Skip => return None,
VaultFetch::Always => EMPTY_WORD,
VaultFetch::IfChangedFrom(commitment) => commitment,
VaultFetch::Assets(asset_ids) => {
return Some(VaultRequest::AssetIds(VaultAssetIds {
asset_ids: asset_ids.into_iter().map(Into::into).collect(),
}));
},
};
Some(VaultRequest::AssetVaultCommitment(commitment.into()))
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub enum StorageMapFetch {
#[default]
Skip,
All,
Slots(AccountStorageRequirements),
}
impl Serializable for StorageMapFetch {
fn write_into<W: miden_tx::utils::serde::ByteWriter>(&self, target: &mut W) {
match self {
Self::Skip => target.write_u8(0),
Self::All => target.write_u8(1),
Self::Slots(requirements) => {
target.write_u8(2);
target.write(requirements);
},
}
}
}
impl Deserializable for StorageMapFetch {
fn read_from<R: miden_tx::utils::serde::ByteReader>(
source: &mut R,
) -> Result<Self, DeserializationError> {
match source.read_u8()? {
0 => Ok(Self::Skip),
1 => Ok(Self::All),
2 => Ok(Self::Slots(source.read()?)),
tag => Err(DeserializationError::InvalidValue(format!(
"invalid storage map fetch tag: {tag}"
))),
}
}
}
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, Eq, PartialEq)]
pub struct AccountFetch {
storage: StorageMapFetch,
vault: VaultFetch,
}
impl AccountFetch {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_storage(mut self, storage: StorageMapFetch) -> Self {
self.storage = storage;
self
}
#[must_use]
pub fn with_vault(mut self, vault: VaultFetch) -> Self {
self.vault = vault;
self
}
pub fn storage(&self) -> &StorageMapFetch {
&self.storage
}
pub fn vault(&self) -> &VaultFetch {
&self.vault
}
}
impl Serializable for AccountFetch {
fn write_into<W: miden_tx::utils::serde::ByteWriter>(&self, target: &mut W) {
target.write(&self.storage);
target.write(&self.vault);
}
}
impl Deserializable for AccountFetch {
fn read_from<R: miden_tx::utils::serde::ByteReader>(
source: &mut R,
) -> Result<Self, DeserializationError> {
Ok(Self {
storage: source.read()?,
vault: source.read()?,
})
}
}
#[derive(Clone, Debug)]
pub struct GetAccountRequest {
pub account_id: AccountId,
pub fetch: AccountFetch,
pub at: AccountStateAt,
pub known_code: Option<AccountCode>,
}
impl GetAccountRequest {
#[must_use]
pub fn new(account_id: AccountId) -> Self {
Self {
account_id,
fetch: AccountFetch::default(),
at: AccountStateAt::ChainTip,
known_code: None,
}
}
#[must_use]
pub fn with_fetch(mut self, fetch: AccountFetch) -> Self {
self.fetch = fetch;
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
}
}
#[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,
}
#[cfg(test)]
mod tests {
use miden_protocol::Word;
use miden_protocol::account::{StorageMapKey, StorageSlotName};
use miden_protocol::asset::AssetId;
use miden_protocol::testing::account_id::ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET;
use miden_tx::utils::serde::{Deserializable, Serializable};
use super::{AccountFetch, AccountStorageRequirements, StorageMapFetch, VaultFetch, proto};
use crate::alloc::string::ToString;
#[test]
fn invitation_code_validation_request_debug_hides_code() {
let code = "secret-invitation-code";
let request =
proto::rpc::IsInvitationCodeValidRequest { invitation_code: code.to_string() };
let debug = format!("{request:?}");
assert!(debug.contains("IsInvitationCodeValidRequest"));
assert!(!debug.contains(code));
}
#[test]
fn account_fetch_roundtrip() {
let slot = StorageSlotName::new("miden::testing::map").unwrap();
let key = StorageMapKey::new(Word::from([1u32; 4]));
let storage = AccountStorageRequirements::new([(slot, &[key])]);
let asset_id = AssetId::new_fungible(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into().unwrap());
for storage in
[StorageMapFetch::Skip, StorageMapFetch::All, StorageMapFetch::Slots(storage)]
{
let fetch = AccountFetch::new().with_storage(storage);
for fetch in [
fetch.clone(),
fetch.clone().with_vault(VaultFetch::Skip),
fetch.clone().with_vault(VaultFetch::Always),
fetch.clone().with_vault(VaultFetch::IfChangedFrom(Word::from([2u32; 4]))),
fetch.with_vault(VaultFetch::Assets([asset_id].into())),
] {
let decoded = AccountFetch::read_from_bytes(&fetch.to_bytes()).unwrap();
assert_eq!(decoded, fetch);
}
}
}
}
#[cfg(all(test, feature = "testing", feature = "tonic"))]
mod vault_details_tests {
use alloc::collections::BTreeSet;
use alloc::vec::Vec;
use miden_protocol::account::AccountId;
use miden_protocol::asset::{Asset, AssetId, AssetVault, FungibleAsset, PartialVault};
use miden_protocol::testing::account_id::{
ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1,
};
use super::{AccountVaultDetails, VaultAssetIds, VaultFetch, VaultRequest, proto};
fn vault_with_one_asset() -> (AssetVault, Asset, AssetId) {
let held_faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
let absent_faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1).unwrap();
let asset: Asset = FungibleAsset::new(held_faucet, 100).unwrap().into();
let vault = AssetVault::new(&[asset]).unwrap();
(vault, asset, AssetId::new_fungible(absent_faucet))
}
fn partial_vault(vault: &AssetVault, asset_ids: &[AssetId]) -> PartialVault {
PartialVault::with_witnesses(asset_ids.iter().map(|asset_id| vault.open(*asset_id)))
.unwrap()
}
fn proof_details(vault: &AssetVault, asset_ids: &[AssetId]) -> AccountVaultDetails {
AccountVaultDetails::Partial(partial_vault(vault, asset_ids))
}
fn asset_request(asset_ids: &[AssetId]) -> VaultFetch {
VaultFetch::Assets(asset_ids.iter().copied().collect())
}
#[test]
fn asset_request_encodes_the_asset_ids() {
let (_, asset, absent) = vault_with_one_asset();
let asset_ids = BTreeSet::from([asset.id(), absent]);
let request: Option<VaultRequest> = VaultFetch::Assets(asset_ids.clone()).into();
let expected = asset_ids.into_iter().map(Into::into).collect();
assert_eq!(request, Some(VaultRequest::AssetIds(VaultAssetIds { asset_ids: expected })));
}
#[test]
fn proofs_that_cover_the_request_are_accepted() {
let (vault, asset, absent) = vault_with_one_asset();
let request = asset_request(&[asset.id(), absent]);
let details = proof_details(&vault, &[asset.id(), absent])
.validate_against_request(vault.root(), &request)
.unwrap();
let partial_vault = details.into_partial_vault().expect("expected vault proofs");
assert_eq!(partial_vault.get(asset.id()).unwrap(), Some(asset));
assert_eq!(partial_vault.get(absent).unwrap(), None);
}
#[test]
fn proofs_against_another_root_are_rejected() {
let (vault, asset, _) = vault_with_one_asset();
let other_root = AssetVault::default().root();
let result = proof_details(&vault, &[asset.id()])
.validate_against_request(other_root, &asset_request(&[asset.id()]));
assert!(result.is_err());
}
#[test]
fn proofs_that_miss_a_requested_asset_are_rejected() {
let (_, asset, _) = vault_with_one_asset();
let other_faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1).unwrap();
let other_asset: Asset = FungibleAsset::new(other_faucet, 5).unwrap().into();
let vault = AssetVault::new(&[asset, other_asset]).unwrap();
let result = proof_details(&vault, &[asset.id()]).validate_against_request(
vault.root(),
&asset_request(&[asset.id(), other_asset.id()]),
);
assert!(result.is_err());
}
#[test]
fn proofs_that_were_not_requested_are_rejected() {
let (vault, asset, _) = vault_with_one_asset();
let result = proof_details(&vault, &[asset.id()])
.validate_against_request(vault.root(), &VaultFetch::Always);
assert!(result.is_err());
}
#[test]
fn full_vault_answer_is_converted_to_proofs() {
let (vault, asset, absent) = vault_with_one_asset();
let details = AccountVaultDetails::Assets(vec![asset])
.validate_against_request(vault.root(), &asset_request(&[asset.id(), absent]))
.unwrap();
let partial_vault = details.into_partial_vault().expect("expected vault proofs");
assert_eq!(partial_vault.get(asset.id()).unwrap(), Some(asset));
assert_eq!(partial_vault.get(absent).unwrap(), None);
}
#[test]
fn too_many_assets_answer_to_an_asset_request_is_rejected() {
let (vault, asset, _) = vault_with_one_asset();
let result = AccountVaultDetails::TooManyAssets
.validate_against_request(vault.root(), &asset_request(&[asset.id()]));
assert!(result.is_err());
}
#[test]
fn partial_vault_round_trips_through_proto() {
let (vault, asset, absent) = vault_with_one_asset();
let expected = partial_vault(&vault, &[asset.id(), absent]);
let response = proto::rpc::AccountVaultDetails {
too_many_assets: false,
assets: Vec::new(),
partial_vault: Some(proto::rpc::account_vault_details::PartialVault {
asset_ids: vec![asset.id().into(), absent.into()],
partial_smt: Some(expected.partial_smt().clone().into()),
}),
};
let details = AccountVaultDetails::try_from(response).unwrap();
assert_eq!(details.into_partial_vault(), Some(expected));
}
#[test]
fn partial_vault_with_an_asset_list_is_rejected() {
let (vault, asset, _) = vault_with_one_asset();
let partial_vault = partial_vault(&vault, &[asset.id()]);
let response = proto::rpc::AccountVaultDetails {
too_many_assets: false,
assets: vec![asset.into()],
partial_vault: Some(proto::rpc::account_vault_details::PartialVault {
asset_ids: vec![asset.id().into()],
partial_smt: Some(partial_vault.partial_smt().clone().into()),
}),
};
assert!(AccountVaultDetails::try_from(response).is_err());
}
#[test]
fn too_many_assets_flag_converts_to_its_variant() {
let response = proto::rpc::AccountVaultDetails {
too_many_assets: true,
assets: Vec::new(),
partial_vault: None,
};
let details = AccountVaultDetails::try_from(response).unwrap();
assert!(details.is_too_many_assets());
}
#[test]
fn too_many_assets_flag_with_an_asset_list_is_rejected() {
let (_, asset, _) = vault_with_one_asset();
let response = proto::rpc::AccountVaultDetails {
too_many_assets: true,
assets: vec![asset.into()],
partial_vault: None,
};
assert!(AccountVaultDetails::try_from(response).is_err());
}
#[test]
fn partial_vault_with_the_too_many_assets_flag_is_rejected() {
let (vault, asset, _) = vault_with_one_asset();
let partial_vault = partial_vault(&vault, &[asset.id()]);
let response = proto::rpc::AccountVaultDetails {
too_many_assets: true,
assets: Vec::new(),
partial_vault: Some(proto::rpc::account_vault_details::PartialVault {
asset_ids: vec![asset.id().into()],
partial_smt: Some(partial_vault.partial_smt().clone().into()),
}),
};
assert!(AccountVaultDetails::try_from(response).is_err());
}
#[test]
fn proofs_that_track_an_asset_that_was_not_requested_are_rejected() {
let (_, asset, _) = vault_with_one_asset();
let other_faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1).unwrap();
let other_asset: Asset = FungibleAsset::new(other_faucet, 5).unwrap().into();
let vault = AssetVault::new(&[asset, other_asset]).unwrap();
let result = proof_details(&vault, &[asset.id(), other_asset.id()])
.validate_against_request(vault.root(), &asset_request(&[asset.id()]));
assert!(result.is_err());
}
}