diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fc3b4dbf..bdff5169c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ * Fixed MMR reconstruction code and fixed how block authentication paths are adjusted ([#1633](https://github.com/0xMiden/miden-client/pull/1633)). * Added WebClient bindings and RPC helpers for additional account, note, and validation workflows ([#1638](https://github.com/0xMiden/miden-client/pull/1638)). * [BREAKING] Modified JS binding for `AccountComponent::compile` which now takes an `AccountComponentCode` built with the newly added binding `CodeBuilder::compile_account_component_code` ([#1627](https://github.com/0xMiden/miden-client/pull/1627)). +* Expanded the `GrpcClient` API with methods to fetch account proofs and rebuild the slots for an account ([#1591](https://github.com/0xMiden/miden-client/pull/1591)). * [BREAKING] Simplified the `NoteScreener` API, removing `NoteRelevance` in favor of `NoteConsumptionStatus`; exposed JS bindings for consumption check results ([#1630](https://github.com/0xMiden/miden-client/pull/1630)). * [BREAKING] Replaced `TransactionRequestBuilder::unauthenticated_input_notes` & `TransactionRequestBuilder::authenticated_input_notes` for `TransactionRequestBuilder::input_notes`, now the user passes a list of notes which the `Client` itself determines the authentication status of ([#1624](https://github.com/0xMiden/miden-client/issues/1624)). diff --git a/crates/rust-client/src/rpc/domain/account.rs b/crates/rust-client/src/rpc/domain/account.rs index 325c10b61..5878b97ce 100644 --- a/crates/rust-client/src/rpc/domain/account.rs +++ b/crates/rust-client/src/rpc/domain/account.rs @@ -94,12 +94,12 @@ pub struct AccountUpdateSummary { /// Commitment of the account, that represents a commitment to its updated state. pub commitment: Word, /// Block number of last account update. - pub last_block_num: u32, + pub last_block_num: BlockNumber, } impl AccountUpdateSummary { /// Creates a new [`AccountUpdateSummary`]. - pub fn new(commitment: Word, last_block_num: u32) -> Self { + pub fn new(commitment: Word, last_block_num: BlockNumber) -> Self { Self { commitment, last_block_num } } } @@ -327,6 +327,15 @@ pub struct AccountStorageDetails { pub map_details: Vec, } +impl AccountStorageDetails { + /// Find the matching details for a map, given its storage slot name. + // This linear search should be good enough since there can be + // only up to 256 slots, so locality probably wins here. + pub fn find_map_details(&self, target: &StorageSlotName) -> Option<&AccountStorageMapDetails> { + self.map_details.iter().find(|map_detail| map_detail.slot_name == *target) + } +} + impl TryFrom for AccountStorageDetails { type Error = RpcError; @@ -544,6 +553,27 @@ impl AccountProof { } } +#[cfg(feature = "tonic")] +impl TryFrom for AccountProof { + type Error = RpcError; + fn try_from(account_proof: proto::rpc::AccountProofResponse) -> Result { + let Some(witness) = account_proof.witness else { + return Err(RpcError::ExpectedDataMissing( + "GetAccountProof returned an account without witness".to_owned(), + )); + }; + + let details: Option = { + match account_proof.details { + None => None, + Some(details) => Some(details.into_domain(&BTreeMap::new())?), + } + }; + AccountProof::new(witness.try_into()?, details) + .map_err(|err| RpcError::InvalidResponse(format!("{err}"))) + } +} + // ACCOUNT WITNESS // ================================================================================================ diff --git a/crates/rust-client/src/rpc/generated/nostd/rpc_store.rs b/crates/rust-client/src/rpc/generated/nostd/rpc_store.rs index 175568022..80a13e61c 100644 --- a/crates/rust-client/src/rpc/generated/nostd/rpc_store.rs +++ b/crates/rust-client/src/rpc/generated/nostd/rpc_store.rs @@ -14,7 +14,7 @@ pub struct StoreStatus { } /// Returns the latest state proof of the specified account. #[derive(Clone, PartialEq, ::prost::Message)] -pub struct AccountProofRequest { +pub struct AccountRequest { /// ID of the account for which we want to get data #[prost(message, optional, tag = "1")] pub account_id: ::core::option::Option, @@ -25,10 +25,10 @@ pub struct AccountProofRequest { pub block_num: ::core::option::Option, /// Request for additional account details; valid only for public accounts. #[prost(message, optional, tag = "3")] - pub details: ::core::option::Option, + pub details: ::core::option::Option, } -/// Nested message and enum types in `AccountProofRequest`. -pub mod account_proof_request { +/// Nested message and enum types in `AccountRequest`. +pub mod account_request { /// Request the details for a public account. #[derive(Clone, PartialEq, ::prost::Message)] pub struct AccountDetailRequest { @@ -90,7 +90,7 @@ pub mod account_proof_request { } /// Represents the result of getting account proof. #[derive(Clone, PartialEq, ::prost::Message)] -pub struct AccountProofResponse { +pub struct AccountResponse { /// The block number at which the account witness was created and the account details were observed. #[prost(message, optional, tag = "1")] pub block_num: ::core::option::Option, @@ -99,10 +99,10 @@ pub struct AccountProofResponse { pub witness: ::core::option::Option, /// Additional details for public accounts. #[prost(message, optional, tag = "3")] - pub details: ::core::option::Option, + pub details: ::core::option::Option, } -/// Nested message and enum types in `AccountProofResponse`. -pub mod account_proof_response { +/// Nested message and enum types in `AccountResponse`. +pub mod account_response { #[derive(Clone, PartialEq, ::prost::Message)] pub struct AccountDetails { /// Account header. @@ -120,7 +120,7 @@ pub mod account_proof_response { pub vault_details: ::core::option::Option, } } -/// Account vault details for AccountProofResponse +/// Account vault details for AccountResponse #[derive(Clone, PartialEq, ::prost::Message)] pub struct AccountVaultDetails { /// A flag that is set to true if the account contains too many assets. This indicates @@ -133,7 +133,7 @@ pub struct AccountVaultDetails { #[prost(message, repeated, tag = "2")] pub assets: ::prost::alloc::vec::Vec, } -/// Account storage details for AccountProofResponse +/// Account storage details for AccountResponse #[derive(Clone, PartialEq, ::prost::Message)] pub struct AccountStorageDetails { /// Account storage header (storage slot info for up to 256 slots) @@ -587,37 +587,12 @@ pub mod rpc_client { .insert(GrpcMethod::new("rpc_store.Rpc", "CheckNullifiers")); self.inner.unary(req, path, codec).await } - /// Returns the latest state of an account with the specified ID. - pub async fn get_account_details( - &mut self, - request: impl tonic::IntoRequest, - ) -> core::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; - let codec = tonic_prost::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/rpc_store.Rpc/GetAccountDetails", - ); - let mut req = request.into_request(); - req.extensions_mut() - .insert(GrpcMethod::new("rpc_store.Rpc", "GetAccountDetails")); - self.inner.unary(req, path, codec).await - } /// Returns the latest state proof of the specified account. - pub async fn get_account_proof( + pub async fn get_account( &mut self, - request: impl tonic::IntoRequest, + request: impl tonic::IntoRequest, ) -> core::result::Result< - tonic::Response, + tonic::Response, tonic::Status, > { self.inner @@ -629,12 +604,9 @@ pub mod rpc_client { ) })?; let codec = tonic_prost::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/rpc_store.Rpc/GetAccountProof", - ); + let path = http::uri::PathAndQuery::from_static("/rpc_store.Rpc/GetAccount"); let mut req = request.into_request(); - req.extensions_mut() - .insert(GrpcMethod::new("rpc_store.Rpc", "GetAccountProof")); + req.extensions_mut().insert(GrpcMethod::new("rpc_store.Rpc", "GetAccount")); self.inner.unary(req, path, codec).await } /// Returns raw block data for the specified block number. diff --git a/crates/rust-client/src/rpc/generated/std/rpc_store.rs b/crates/rust-client/src/rpc/generated/std/rpc_store.rs index 60f36c10e..1af9c7663 100644 --- a/crates/rust-client/src/rpc/generated/std/rpc_store.rs +++ b/crates/rust-client/src/rpc/generated/std/rpc_store.rs @@ -14,7 +14,7 @@ pub struct StoreStatus { } /// Returns the latest state proof of the specified account. #[derive(Clone, PartialEq, ::prost::Message)] -pub struct AccountProofRequest { +pub struct AccountRequest { /// ID of the account for which we want to get data #[prost(message, optional, tag = "1")] pub account_id: ::core::option::Option, @@ -25,10 +25,10 @@ pub struct AccountProofRequest { pub block_num: ::core::option::Option, /// Request for additional account details; valid only for public accounts. #[prost(message, optional, tag = "3")] - pub details: ::core::option::Option, + pub details: ::core::option::Option, } -/// Nested message and enum types in `AccountProofRequest`. -pub mod account_proof_request { +/// Nested message and enum types in `AccountRequest`. +pub mod account_request { /// Request the details for a public account. #[derive(Clone, PartialEq, ::prost::Message)] pub struct AccountDetailRequest { @@ -90,7 +90,7 @@ pub mod account_proof_request { } /// Represents the result of getting account proof. #[derive(Clone, PartialEq, ::prost::Message)] -pub struct AccountProofResponse { +pub struct AccountResponse { /// The block number at which the account witness was created and the account details were observed. #[prost(message, optional, tag = "1")] pub block_num: ::core::option::Option, @@ -99,10 +99,10 @@ pub struct AccountProofResponse { pub witness: ::core::option::Option, /// Additional details for public accounts. #[prost(message, optional, tag = "3")] - pub details: ::core::option::Option, + pub details: ::core::option::Option, } -/// Nested message and enum types in `AccountProofResponse`. -pub mod account_proof_response { +/// Nested message and enum types in `AccountResponse`. +pub mod account_response { #[derive(Clone, PartialEq, ::prost::Message)] pub struct AccountDetails { /// Account header. @@ -120,7 +120,7 @@ pub mod account_proof_response { pub vault_details: ::core::option::Option, } } -/// Account vault details for AccountProofResponse +/// Account vault details for AccountResponse #[derive(Clone, PartialEq, ::prost::Message)] pub struct AccountVaultDetails { /// A flag that is set to true if the account contains too many assets. This indicates @@ -133,7 +133,7 @@ pub struct AccountVaultDetails { #[prost(message, repeated, tag = "2")] pub assets: ::prost::alloc::vec::Vec, } -/// Account storage details for AccountProofResponse +/// Account storage details for AccountResponse #[derive(Clone, PartialEq, ::prost::Message)] pub struct AccountStorageDetails { /// Account storage header (storage slot info for up to 256 slots) @@ -598,37 +598,12 @@ pub mod rpc_client { .insert(GrpcMethod::new("rpc_store.Rpc", "CheckNullifiers")); self.inner.unary(req, path, codec).await } - /// Returns the latest state of an account with the specified ID. - pub async fn get_account_details( - &mut self, - request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; - let codec = tonic_prost::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/rpc_store.Rpc/GetAccountDetails", - ); - let mut req = request.into_request(); - req.extensions_mut() - .insert(GrpcMethod::new("rpc_store.Rpc", "GetAccountDetails")); - self.inner.unary(req, path, codec).await - } /// Returns the latest state proof of the specified account. - pub async fn get_account_proof( + pub async fn get_account( &mut self, - request: impl tonic::IntoRequest, + request: impl tonic::IntoRequest, ) -> std::result::Result< - tonic::Response, + tonic::Response, tonic::Status, > { self.inner @@ -640,12 +615,9 @@ pub mod rpc_client { ) })?; let codec = tonic_prost::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/rpc_store.Rpc/GetAccountProof", - ); + let path = http::uri::PathAndQuery::from_static("/rpc_store.Rpc/GetAccount"); let mut req = request.into_request(); - req.extensions_mut() - .insert(GrpcMethod::new("rpc_store.Rpc", "GetAccountProof")); + req.extensions_mut().insert(GrpcMethod::new("rpc_store.Rpc", "GetAccount")); self.inner.unary(req, path, codec).await } /// Returns raw block data for the specified block number. diff --git a/crates/rust-client/src/rpc/tonic_client/mod.rs b/crates/rust-client/src/rpc/tonic_client/mod.rs index e6c45686f..53b874e32 100644 --- a/crates/rust-client/src/rpc/tonic_client/mod.rs +++ b/crates/rust-client/src/rpc/tonic_client/mod.rs @@ -4,8 +4,11 @@ use alloc::collections::{BTreeMap, BTreeSet}; use alloc::string::{String, ToString}; use alloc::vec::Vec; use core::error::Error; +use miden_protocol::asset::{Asset, AssetVault}; -use miden_protocol::account::{Account, AccountCode, AccountId}; +use miden_protocol::account::{ + Account, AccountCode, AccountId, AccountStorage, StorageMap, StorageSlot, StorageSlotType, +}; use miden_protocol::address::NetworkId; use miden_protocol::block::account_tree::AccountWitness; use miden_protocol::block::{BlockHeader, BlockNumber, ProvenBlock}; @@ -21,24 +24,21 @@ use miden_tx::utils::sync::RwLock; use tonic::Status; use tracing::info; -use super::domain::account::{AccountProof, AccountUpdateSummary}; -use super::domain::note::FetchedNote; -use super::domain::nullifier::NullifierUpdate; +use super::domain::account::{AccountProof, AccountStorageDetails, AccountUpdateSummary}; +use super::domain::{note::FetchedNote, nullifier::NullifierUpdate}; +use super::generated::rpc::account_proof_request::AccountDetailRequest; +use super::generated::rpc::AccountProofRequest; use super::{ - Endpoint, - FetchedAccount, - NodeRpcClient, - NodeRpcClientEndpoint, - NoteSyncInfo, - RpcError, + Endpoint, FetchedAccount, NodeRpcClient, NodeRpcClientEndpoint, NoteSyncInfo, RpcError, StateSyncInfo, }; use crate::rpc::domain::account_vault::AccountVaultInfo; use crate::rpc::domain::storage_map::StorageMapInfo; use crate::rpc::domain::transaction::TransactionsInfo; use crate::rpc::errors::{AcceptHeaderError, GrpcError, RpcConversionError}; -use crate::rpc::generated::rpc::BlockRange; +use crate::rpc::generated::rpc::account_proof_request::account_detail_request::storage_map_detail_request::SlotData; use crate::rpc::generated::rpc::account_proof_request::account_detail_request::StorageMapDetailRequest; +use crate::rpc::generated::rpc::BlockRange; use crate::rpc::{AccountStateAt, NOTE_IDS_LIMIT, NULLIFIER_PREFIXES_LIMIT, generated as proto}; use crate::transaction::ForeignAccount; @@ -65,6 +65,11 @@ pub struct GrpcClient { genesis_commitment: RwLock>, } +// Helper function to request extra details for an account. +// Keep in mind, this can potentially do **two** requests to the rpc endpoint. +// If the account has a non public state, it will simply return the first response. +// Otherwise it will request the RPC endpoint for the extra data for an account with public state. + impl GrpcClient { /// Returns a new instance of [`GrpcClient`] that'll do calls to the provided [`Endpoint`] /// with the given timeout in milliseconds. @@ -99,6 +104,164 @@ impl GrpcClient { Ok(()) } + + /// Given an id, return the proof for the account. + /// If the account also has public state, the details for it + /// will also be retrieve as part of the proof. + pub async fn fetch_full_account_proof( + &self, + account_id: AccountId, + ) -> Result<(BlockNumber, AccountProof), RpcError> { + let mut rpc_api = self.ensure_connected().await?; + let has_public_state = account_id.has_public_state(); + let account_request = { + AccountProofRequest { + account_id: Some(account_id.into()), + block_num: None, + details: { + if has_public_state { + // Since we have to request the storage maps for an account + // we *dont know* anything about, we'll have to do first this + // request, which will tell us about the account's storage slots, + // and then, request the slots in another request. + Some(AccountDetailRequest { + code_commitment: Some(EMPTY_WORD.into()), + asset_vault_commitment: Some(EMPTY_WORD.into()), + storage_maps: vec![], + }) + } else { + None + } + }, + } + }; + let account_response = rpc_api + .get_account_proof(account_request) + .await + .map_err(|status| { + RpcError::from_grpc_error(NodeRpcClientEndpoint::GetAccountDetails, status) + })? + .into_inner(); + let block_number = account_response.block_num.ok_or(RpcError::ExpectedDataMissing( + "GetAccountDetails returned an account without a matching block number for the witness" + .to_owned(), + ))?; + let account_proof = { + if has_public_state { + let account_details = account_response + .details + .ok_or(RpcError::ExpectedDataMissing("details in public account".to_owned()))? + .into_domain(&BTreeMap::new())?; + let storage_header = account_details.storage_details.header; + // This is variable will hold the storage slots that are maps, + // below we will use it to actually fetch the storage maps details, + // since we now know the names of each storage slot. + let maps_to_request = storage_header + .slots() + .filter(|header| header.slot_type().is_map()) + .map(|map| map.name().to_string()); + let account_request = AccountProofRequest { + account_id: Some(account_id.into()), + block_num: None, + details: Some(AccountDetailRequest { + code_commitment: Some(EMPTY_WORD.into()), + asset_vault_commitment: Some(EMPTY_WORD.into()), + storage_maps: maps_to_request + .map(|slot_name| StorageMapDetailRequest { + slot_name, + slot_data: Some(SlotData::AllEntries(true)), + }) + .collect(), + }), + }; + match rpc_api.get_account_proof(account_request).await { + Ok(account_proof) => account_proof.into_inner().try_into(), + Err(err) => Err(RpcError::ConnectionError( + format!( + "failed to fetch account proof for account: {account_id}, got: {err}" + ) + .into(), + )), + } + } else { + account_response.try_into() + } + }; + Ok((block_number.block_num.into(), account_proof?)) + } + + /// Given the storage details for an account and its id, returns a vector + /// with all of its storage slots. Keep in mind that if an account triggers + /// the `too_many_entries` flag, there will potentially be multiple requests. + pub async fn build_storage_slots( + &self, + account_id: AccountId, + storage_details: &AccountStorageDetails, + ) -> Result, RpcError> { + let mut slots = vec![]; + // It seems that sync_storage_maps will return information for *every* + // map for a given account, so this map_cache value should be + // fetched only once, hence the None placeholder + let mut map_cache: Option = None; + for slot_header in storage_details.header.slots() { + // We have two cases for each slot: + // - Slot is a value => We simply instance a StorageSlot + // - Slot is a map => If the map is 'small', we can simply + // build the map from the given entries. Otherwise we will have to + // call the SyncStorageMaps RPC method to obtain the data for the map. + // With the current setup, one RPC call should be enough. + match slot_header.slot_type() { + StorageSlotType::Value => { + slots.push(miden_protocol::account::StorageSlot::with_value( + slot_header.name().clone(), + slot_header.value(), + )); + }, + StorageSlotType::Map => { + let map_details = storage_details.find_map_details(slot_header.name()).ok_or( + RpcError::ExpectedDataMissing(format!( + "slot named '{}' was reported as a map, but it does not have a matching map_detail entry", + slot_header.name(), + )), + )?; + + let mut map_entries = vec![]; + if map_details.too_many_entries { + let map_info = if let Some(ref info) = map_cache { + info + } else { + let fetched_data = + self.sync_storage_maps(0_u32.into(), None, account_id).await?; + map_cache.insert(fetched_data) + }; + map_entries.extend( + map_info + .updates + .iter() + .filter(|slot_info| slot_info.slot_name == *slot_header.name()) + .map(|slot_info| (slot_info.key, slot_info.value)), + ); + } else { + map_entries.extend(map_details.entries.iter().map(|e| { + let key: Word = e.key; + let value: Word = e.value; + (key, value) + })); + } + + slots.push(miden_protocol::account::StorageSlot::with_map( + slot_header.name().clone(), + StorageMap::with_entries(map_entries).map_err(|err| { + RpcError::InvalidResponse(format!( + "the rpc api returned a non-valid map entry: {err}" + )) + })?, + )); + }, + } + } + Ok(slots) + } } #[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] @@ -256,36 +419,55 @@ impl NodeRpcClient for GrpcClient { /// `account_commitment`, `details`). /// - There is an error during [Account] deserialization. async fn get_account_details(&self, account_id: AccountId) -> Result { - let request = proto::account::AccountId { id: account_id.to_bytes() }; - - let mut rpc_api = self.ensure_connected().await?; - - let response = rpc_api.get_account_details(request).await.map_err(|status| { - RpcError::from_grpc_error(NodeRpcClientEndpoint::GetAccountDetails, status) - })?; - let response = response.into_inner(); - let account_summary = response.summary.ok_or(RpcError::ExpectedDataMissing( - "GetAccountDetails response should have an `summary`".to_string(), - ))?; - - let commitment = - account_summary.account_commitment.ok_or(RpcError::ExpectedDataMissing( - "GetAccountDetails response's account should have an `account_commitment`" - .to_string(), - ))?; + let (block_number, full_account_proof) = self.fetch_full_account_proof(account_id).await?; - let commitment = commitment.try_into()?; + let update_summary = + AccountUpdateSummary::new(full_account_proof.account_commitment(), block_number); - let update_summary = AccountUpdateSummary::new(commitment, account_summary.block_num); + // The case for a private account is simple, + // we simple use the commitment and its id. if account_id.is_private() { Ok(FetchedAccount::new_private(account_id, update_summary)) } else { - let account = Account::read_from_bytes(&response.details.ok_or( - RpcError::ExpectedDataMissing( - "GetAccountDetails response should have an `account`".to_string(), - ), - )?)?; + // An account with public state has to fetch all of its state. + // Even more so, an account with a large state will have to do + // a couple of extra requests to fetch all of its data. + let details = + full_account_proof.into_parts().1.ok_or(RpcError::ExpectedDataMissing( + "GetAccountDetails returned a public account without details".to_owned(), + ))?; + let account_id = details.header.id(); + let nonce = details.header.nonce(); + let assets: Vec = { + if details.vault_details.too_many_assets { + self.sync_account_vault(BlockNumber::from(0), None, account_id) + .await? + .updates + .into_iter() + .filter_map(|update| update.asset) + .collect() + } else { + details.vault_details.assets + } + }; + let slots = self.build_storage_slots(account_id, &details.storage_details).await?; + let seed = None; + let asset_vault = AssetVault::new(&assets).map_err(|err| { + RpcError::InvalidResponse(format!("api rpc returned non-valid assets: {err}")) + })?; + let account_storage = AccountStorage::new(slots).map_err(|err| { + RpcError::InvalidResponse(format!( + "api rpc returned non-valid storage slots: {err}" + )) + })?; + let account = + Account::new(account_id, asset_vault, account_storage, details.code, nonce, seed) + .map_err(|err| { + RpcError::InvalidResponse(format!( + "failed to instance an account from the rpc api response: {err}" + )) + })?; Ok(FetchedAccount::new_public(account, update_summary)) } } diff --git a/crates/rust-client/src/test_utils/mock.rs b/crates/rust-client/src/test_utils/mock.rs index a1474d8ee..7a92be840 100644 --- a/crates/rust-client/src/test_utils/mock.rs +++ b/crates/rust-client/src/test_utils/mock.rs @@ -523,7 +523,7 @@ impl NodeRpcClient for MockRpcApi { .find_map(|(block_num, updates)| { updates.get(&account_id).map(|commitment| AccountUpdateSummary { commitment: *commitment, - last_block_num: block_num.as_u32(), + last_block_num: *block_num, }) }) .unwrap(); diff --git a/crates/testing/node-builder/src/lib.rs b/crates/testing/node-builder/src/lib.rs index fe3538149..564ac1b91 100644 --- a/crates/testing/node-builder/src/lib.rs +++ b/crates/testing/node-builder/src/lib.rs @@ -9,7 +9,7 @@ use std::sync::Arc; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use ::rand::{Rng, random}; -use anyhow::{Context, Result}; +use anyhow::{Context, Error, Result}; use miden_node_block_producer::{ BlockProducer, DEFAULT_MAX_BATCHES_PER_BLOCK, @@ -22,14 +22,15 @@ use miden_node_store::{GenesisState, Store}; use miden_node_utils::crypto::get_rpo_random_coin; use miden_node_validator::Validator; use miden_protocol::account::auth::AuthSecretKey; -use miden_protocol::account::{Account, AccountFile}; -use miden_protocol::asset::TokenSymbol; +use miden_protocol::account::{Account, AccountBuilder, AccountComponent, AccountFile, StorageMap}; +use miden_protocol::asset::{Asset, FungibleAsset, TokenSymbol}; use miden_protocol::block::FeeParameters; use miden_protocol::crypto::dsa::ecdsa_k256_keccak; use miden_protocol::testing::account_id::ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET; use miden_protocol::utils::Serializable; -use miden_protocol::{Felt, ONE}; +use miden_protocol::{Felt, ONE, Word}; use miden_standards::AuthScheme; +use miden_standards::account::components::basic_wallet_library; use miden_standards::account::faucets::create_basic_fungible_faucet; use rand_chacha::ChaCha20Rng; use rand_chacha::rand_core::SeedableRng; @@ -95,6 +96,8 @@ impl NodeBuilder { miden_node_utils::logging::OpenTelemetry::Disabled, )?; + let test_faucets_and_account = build_test_faucets_and_account()?; + let account_file = generate_genesis_account().context("failed to create genesis account")?; @@ -119,7 +122,7 @@ impl NodeBuilder { let validator_signer = ecdsa_k256_keccak::SecretKey::new(); let genesis_state = GenesisState::new( - vec![account_file.account], + [&[account_file.account][..], &test_faucets_and_account[..]].concat(), FeeParameters::new(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into().unwrap(), 0u32) .unwrap(), version, @@ -392,6 +395,79 @@ impl NodeHandle { // UTILS // ================================================================================================ +const TEST_ACCOUNT_ID: &str = "0x0a0a0a0a0a0a0a100a0a0a0a0a0a0a"; + +// Builds an account that triggers the "too_many_assets" boolean +// flag when requested from the node. +fn build_test_faucets_and_account() -> anyhow::Result> { + let mut rng = ChaCha20Rng::from_seed(random()); + let secret = AuthSecretKey::new_falcon512_rpo_with_rng(&mut get_rpo_random_coin(&mut rng)); + let faucets = (0_u128..=1500_u128) + .map(|i| -> anyhow::Result { + let init_seed = [i.to_be_bytes(), i.to_be_bytes()] + .concat() + .try_into() + .expect("this won't fail because we have exactly 32 bytes"); + let symbol = TokenSymbol::new("TKN")?; + let decimals = 12; + let max_supply = Felt::from(1_u32 << 30); + let account_storage_mode = miden_protocol::account::AccountStorageMode::Public; + + let auth_scheme = AuthScheme::RpoFalcon512 { + pub_key: secret.public_key().to_commitment(), + }; + let faucet = create_basic_fungible_faucet( + init_seed, + symbol, + decimals, + max_supply, + account_storage_mode, + auth_scheme, + )?; + let (id, vault, storage, code, ..) = faucet.into_parts(); + Ok(Account::new_unchecked(id, vault, storage, code, ONE, None)) + }) + .collect::>>() + .map_err(|err| Error::msg(format!("could not instance tests faucets got: {err}")))?; + + let seed = [0xa; 32]; + let sk = AuthSecretKey::new_falcon512_rpo_with_rng(&mut ChaCha20Rng::from_seed(seed)); + + let map_entries = (0_u32..2001_u32).map(|i| (Word::from([i; 4]), Word::from([i; 4]))); + + let storage_map = miden_protocol::account::StorageSlot::with_map( + miden_protocol::account::StorageSlotName::new("miden::test_account::map::too_many_entries") + .expect("should be a valid slot name"), + StorageMap::with_entries(map_entries).expect("should be a valid map"), + ); + + let acc_component = AccountComponent::new(basic_wallet_library(), vec![storage_map]) + .expect( + "basic wallet component should satisfy the requirements of a valid account component", + ) + .with_supports_all_types(); + + let account = AccountBuilder::new(seed) + .with_auth_component(miden_standards::account::auth::AuthRpoFalcon512::new( + sk.public_key().to_commitment(), + )) + .account_type(miden_protocol::account::AccountType::RegularAccountUpdatableCode) + .with_component(acc_component) + .storage_mode(miden_protocol::account::AccountStorageMode::Public) + .with_assets(faucets.iter().map(|faucet| { + Asset::Fungible(FungibleAsset::new(faucet.id(), 100).expect("should be a valid faucet")) + })) + .build_existing()?; + + assert_eq!( + account.id().to_hex(), + TEST_ACCOUNT_ID, + "test account with a large number of assets was generated with a different id than the one expected" + ); + + Ok([&faucets[..], &[account][..]].concat()) +} + fn generate_genesis_account() -> anyhow::Result { let mut rng = ChaCha20Rng::from_seed(random()); let secret = AuthSecretKey::new_falcon512_rpo_with_rng(&mut get_rpo_random_coin(&mut rng)); diff --git a/crates/web-client/test/account.test.ts b/crates/web-client/test/account.test.ts index ab31e854c..ab956d932 100644 --- a/crates/web-client/test/account.test.ts +++ b/crates/web-client/test/account.test.ts @@ -159,3 +159,42 @@ test.describe("getAccounts tests", () => { expect(result.resultTypes.length).toEqual(0); }); }); + +test.describe("get public account with details", () => { + test("assets and storage with too many assets/entries are retrieved", async ({ + page, + }) => { + const [assetCount, balances, mapEntriesCount] = await page.evaluate( + async () => { + // This account is inserted into the genesis block when test node is started, + // it starts with assets from 1500 faucets, the function "build_test_faucets_and_accoung" + // is called when the node starts and does the setup for this account, you can find it + // in: miden-client/crates/testing/node-builder/src/lib.rs + const accountID = window.AccountId.fromHex( + "0x0a0a0a0a0a0a0a100a0a0a0a0a0a0a" + ); + await window.client.importAccountById(accountID); + const account = await window.client.getAccount(accountID); + const storage = account + ?.storage() + .getMapEntries("miden::test_account::map::too_many_entries"); + console.log("Storage length", storage?.length); + const vault = account?.vault(); + const assets = vault?.fungibleAssets()!; + const assetCount = assets.length; + const balances = []; + for (const asset of assets) { + balances.push(vault?.getBalance(asset.faucetId()).toString()); + } + const mapEntries = account + ?.storage() + .getMapEntries("miden::test_account::map::too_many_entries"); + return [assetCount, balances, mapEntries?.length]; + }, + {} + ); + expect(assetCount).toBe(1501); + expect(balances.every((balance) => balance === "100")).toBe(true); + expect(mapEntriesCount).toBe(2000); + }); +}); diff --git a/crates/web-client/yarn.lock b/crates/web-client/yarn.lock index 7ed42a49c..507184bb7 100644 --- a/crates/web-client/yarn.lock +++ b/crates/web-client/yarn.lock @@ -343,7 +343,14 @@ ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" -ansi-styles@^4.0.0, ansi-styles@^4.1.0: +ansi-styles@^4.0.0: + version "4.3.0" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^4.1.0: version "4.3.0" resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== @@ -554,7 +561,15 @@ chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.1.0, chalk@^4.1.2: +chalk@^4.1.0: + version "4.1.2" + resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chalk@^4.1.2: version "4.1.2" resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -1785,7 +1800,17 @@ picocolors@^1.0.0: resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz" integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw== -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: +picomatch@^2.0.4: + version "2.3.1" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +picomatch@^2.2.1: + version "2.3.1" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +picomatch@^2.3.1: version "2.3.1" resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==