diff --git a/CHANGELOG.md b/CHANGELOG.md index db01808a..6ee91951 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Improve `--version` by adding build metadata (#495). - [BREAKING] Introduced additional limits for note/account number (#503). - [BREAKING] Removed support for basic wallets in genesis creation (#510). +- Added `GetAccountStates` endpoint (#506). ## 0.5.1 (2024-09-12) diff --git a/Cargo.lock b/Cargo.lock index b40e17d5..cf34633e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1891,7 +1891,7 @@ dependencies = [ [[package]] name = "miden-lib" version = "0.6.0" -source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#7dbd6083f49fceee41e328807805ebaef0029aef" +source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#9dc06d705b8f4cae86ff08a293f379c206f13421" dependencies = [ "miden-assembly", "miden-objects", @@ -2085,7 +2085,7 @@ dependencies = [ [[package]] name = "miden-objects" version = "0.6.0" -source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#7dbd6083f49fceee41e328807805ebaef0029aef" +source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#9dc06d705b8f4cae86ff08a293f379c206f13421" dependencies = [ "miden-assembly", "miden-core", @@ -2156,7 +2156,7 @@ dependencies = [ [[package]] name = "miden-tx" version = "0.6.0" -source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#7dbd6083f49fceee41e328807805ebaef0029aef" +source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#9dc06d705b8f4cae86ff08a293f379c206f13421" dependencies = [ "miden-lib", "miden-objects", diff --git a/crates/proto/src/domain/accounts.rs b/crates/proto/src/domain/accounts.rs index eaea327e..3a7de73d 100644 --- a/crates/proto/src/domain/accounts.rs +++ b/crates/proto/src/domain/accounts.rs @@ -2,7 +2,7 @@ use std::fmt::{Debug, Display, Formatter}; use miden_node_utils::formatting::format_opt; use miden_objects::{ - accounts::{Account, AccountId}, + accounts::{Account, AccountHeader, AccountId}, crypto::{hash::rpo::RpoDigest, merkle::MerklePath}, utils::Serializable, Digest, @@ -12,8 +12,8 @@ use crate::{ errors::{ConversionError, MissingFieldHelper}, generated::{ account::{ - AccountId as AccountIdPb, AccountInfo as AccountInfoPb, - AccountSummary as AccountSummaryPb, + AccountHeader as AccountHeaderPb, AccountId as AccountIdPb, + AccountInfo as AccountInfoPb, AccountSummary as AccountSummaryPb, }, responses::{AccountBlockInputRecord, AccountTransactionInputRecord}, }, @@ -180,6 +180,17 @@ impl From for AccountTransactionInputRecord { } } +impl From for AccountHeaderPb { + fn from(from: AccountHeader) -> Self { + Self { + vault_root: Some(from.vault_root().into()), + storage_commitment: Some(from.storage_commitment().into()), + code_commitment: Some(from.code_commitment().into()), + nonce: from.nonce().into(), + } + } +} + impl TryFrom for AccountState { type Error = ConversionError; diff --git a/crates/proto/src/domain/merkle.rs b/crates/proto/src/domain/merkle.rs index 6b71d18f..f21bec08 100644 --- a/crates/proto/src/domain/merkle.rs +++ b/crates/proto/src/domain/merkle.rs @@ -19,6 +19,12 @@ impl From<&MerklePath> for generated::merkle::MerklePath { } } +impl From for generated::merkle::MerklePath { + fn from(value: MerklePath) -> Self { + (&value).into() + } +} + impl TryFrom<&generated::merkle::MerklePath> for MerklePath { type Error = ConversionError; diff --git a/crates/proto/src/generated/account.rs b/crates/proto/src/generated/account.rs index bcf60995..fc59f1fe 100644 --- a/crates/proto/src/generated/account.rs +++ b/crates/proto/src/generated/account.rs @@ -24,3 +24,18 @@ pub struct AccountInfo { #[prost(bytes = "vec", optional, tag = "2")] pub details: ::core::option::Option<::prost::alloc::vec::Vec>, } +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct AccountHeader { + /// Vault root hash. + #[prost(message, optional, tag = "1")] + pub vault_root: ::core::option::Option, + /// Storage root hash. + #[prost(message, optional, tag = "2")] + pub storage_commitment: ::core::option::Option, + /// Code root hash. + #[prost(message, optional, tag = "3")] + pub code_commitment: ::core::option::Option, + /// Account nonce. + #[prost(uint64, tag = "4")] + pub nonce: u64, +} diff --git a/crates/proto/src/generated/requests.rs b/crates/proto/src/generated/requests.rs index e0b1dc08..34db2cb6 100644 --- a/crates/proto/src/generated/requests.rs +++ b/crates/proto/src/generated/requests.rs @@ -148,3 +148,12 @@ pub struct GetAccountStateDeltaRequest { #[prost(fixed32, tag = "3")] pub to_block_num: u32, } +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetAccountProofsRequest { + /// List of account IDs to get states. + #[prost(message, repeated, tag = "1")] + pub account_ids: ::prost::alloc::vec::Vec, + /// Optional flag to include header in the response. `false` by default. + #[prost(bool, optional, tag = "2")] + pub include_headers: ::core::option::Option, +} diff --git a/crates/proto/src/generated/responses.rs b/crates/proto/src/generated/responses.rs index 7afa7c89..a871cbe3 100644 --- a/crates/proto/src/generated/responses.rs +++ b/crates/proto/src/generated/responses.rs @@ -195,3 +195,36 @@ pub struct GetAccountStateDeltaResponse { #[prost(bytes = "vec", optional, tag = "1")] pub delta: ::core::option::Option<::prost::alloc::vec::Vec>, } +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetAccountProofsResponse { + /// Block number at which the state of the account was returned. + #[prost(fixed32, tag = "1")] + pub block_num: u32, + /// List of account state infos for the requested account keys. + #[prost(message, repeated, tag = "2")] + pub account_proofs: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AccountProofsResponse { + /// Account ID. + #[prost(message, optional, tag = "1")] + pub account_id: ::core::option::Option, + /// Account hash. + #[prost(message, optional, tag = "2")] + pub account_hash: ::core::option::Option, + /// Authentication path from the `account_root` of the block header to the account. + #[prost(message, optional, tag = "3")] + pub account_proof: ::core::option::Option, + /// State header for public accounts. Filled only if `include_headers` flag is set to `true`. + #[prost(message, optional, tag = "4")] + pub state_header: ::core::option::Option, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AccountStateHeader { + /// Account header. + #[prost(message, optional, tag = "1")] + pub header: ::core::option::Option, + /// Values of all account storage slots (max 255). + #[prost(bytes = "vec", tag = "2")] + pub storage_header: ::prost::alloc::vec::Vec, +} diff --git a/crates/proto/src/generated/rpc.rs b/crates/proto/src/generated/rpc.rs index d1840c5f..c7181943 100644 --- a/crates/proto/src/generated/rpc.rs +++ b/crates/proto/src/generated/rpc.rs @@ -164,6 +164,29 @@ pub mod api_client { req.extensions_mut().insert(GrpcMethod::new("rpc.Api", "GetAccountDetails")); self.inner.unary(req, path, codec).await } + pub async fn get_account_proofs( + &mut self, + request: impl tonic::IntoRequest< + super::super::requests::GetAccountProofsRequest, + >, + ) -> 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::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static("/rpc.Api/GetAccountProofs"); + let mut req = request.into_request(); + req.extensions_mut().insert(GrpcMethod::new("rpc.Api", "GetAccountProofs")); + self.inner.unary(req, path, codec).await + } pub async fn get_account_state_delta( &mut self, request: impl tonic::IntoRequest< @@ -366,6 +389,13 @@ pub mod api_server { tonic::Response, tonic::Status, >; + async fn get_account_proofs( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; async fn get_account_state_delta( &self, request: tonic::Request, @@ -641,6 +671,54 @@ pub mod api_server { }; Box::pin(fut) } + "/rpc.Api/GetAccountProofs" => { + #[allow(non_camel_case_types)] + struct GetAccountProofsSvc(pub Arc); + impl< + T: Api, + > tonic::server::UnaryService< + super::super::requests::GetAccountProofsRequest, + > for GetAccountProofsSvc { + type Response = super::super::responses::GetAccountProofsResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request< + super::super::requests::GetAccountProofsRequest, + >, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::get_account_proofs(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = GetAccountProofsSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } "/rpc.Api/GetAccountStateDelta" => { #[allow(non_camel_case_types)] struct GetAccountStateDeltaSvc(pub Arc); diff --git a/crates/proto/src/generated/store.rs b/crates/proto/src/generated/store.rs index 66c49e9d..0a9055a9 100644 --- a/crates/proto/src/generated/store.rs +++ b/crates/proto/src/generated/store.rs @@ -188,6 +188,32 @@ pub mod api_client { .insert(GrpcMethod::new("store.Api", "GetAccountDetails")); self.inner.unary(req, path, codec).await } + pub async fn get_account_proofs( + &mut self, + request: impl tonic::IntoRequest< + super::super::requests::GetAccountProofsRequest, + >, + ) -> 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::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/store.Api/GetAccountProofs", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("store.Api", "GetAccountProofs")); + self.inner.unary(req, path, codec).await + } pub async fn get_account_state_delta( &mut self, request: impl tonic::IntoRequest< @@ -514,6 +540,13 @@ pub mod api_server { tonic::Response, tonic::Status, >; + async fn get_account_proofs( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; async fn get_account_state_delta( &self, request: tonic::Request, @@ -872,6 +905,54 @@ pub mod api_server { }; Box::pin(fut) } + "/store.Api/GetAccountProofs" => { + #[allow(non_camel_case_types)] + struct GetAccountProofsSvc(pub Arc); + impl< + T: Api, + > tonic::server::UnaryService< + super::super::requests::GetAccountProofsRequest, + > for GetAccountProofsSvc { + type Response = super::super::responses::GetAccountProofsResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request< + super::super::requests::GetAccountProofsRequest, + >, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::get_account_proofs(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = GetAccountProofsSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } "/store.Api/GetAccountStateDelta" => { #[allow(non_camel_case_types)] struct GetAccountStateDeltaSvc(pub Arc); diff --git a/crates/rpc-proto/proto/account.proto b/crates/rpc-proto/proto/account.proto index a0eb3697..8f7becff 100644 --- a/crates/rpc-proto/proto/account.proto +++ b/crates/rpc-proto/proto/account.proto @@ -20,3 +20,14 @@ message AccountInfo { AccountSummary summary = 1; optional bytes details = 2; } + +message AccountHeader { + // Vault root hash. + digest.Digest vault_root = 1; + // Storage root hash. + digest.Digest storage_commitment = 2; + // Code root hash. + digest.Digest code_commitment = 3; + // Account nonce. + uint64 nonce = 4; +} diff --git a/crates/rpc-proto/proto/requests.proto b/crates/rpc-proto/proto/requests.proto index a9c210a2..9f9554b4 100644 --- a/crates/rpc-proto/proto/requests.proto +++ b/crates/rpc-proto/proto/requests.proto @@ -131,3 +131,10 @@ message GetAccountStateDeltaRequest { // Block number up to which the delta is requested (inclusive). fixed32 to_block_num = 3; } + +message GetAccountProofsRequest { + // List of account IDs to get states. + repeated account.AccountId account_ids = 1; + // Optional flag to include header in the response. `false` by default. + optional bool include_headers = 2; +} diff --git a/crates/rpc-proto/proto/responses.proto b/crates/rpc-proto/proto/responses.proto index 7342294f..7d7d0003 100644 --- a/crates/rpc-proto/proto/responses.proto +++ b/crates/rpc-proto/proto/responses.proto @@ -173,3 +173,28 @@ message GetAccountStateDeltaResponse { // The calculated `AccountStateDelta` encoded using miden native format optional bytes delta = 1; } + +message GetAccountProofsResponse { + // Block number at which the state of the account was returned. + fixed32 block_num = 1; + // List of account state infos for the requested account keys. + repeated AccountProofsResponse account_proofs = 2; +} + +message AccountProofsResponse { + // Account ID. + account.AccountId account_id = 1; + // Account hash. + digest.Digest account_hash = 2; + // Authentication path from the `account_root` of the block header to the account. + merkle.MerklePath account_proof = 3; + // State header for public accounts. Filled only if `include_headers` flag is set to `true`. + optional AccountStateHeader state_header = 4; +} + +message AccountStateHeader { + // Account header. + account.AccountHeader header = 1; + // Values of all account storage slots (max 255). + bytes storage_header = 2; +} diff --git a/crates/rpc-proto/proto/rpc.proto b/crates/rpc-proto/proto/rpc.proto index da2c2bdd..13934b2c 100644 --- a/crates/rpc-proto/proto/rpc.proto +++ b/crates/rpc-proto/proto/rpc.proto @@ -9,6 +9,7 @@ service Api { rpc CheckNullifiers(requests.CheckNullifiersRequest) returns (responses.CheckNullifiersResponse) {} rpc CheckNullifiersByPrefix(requests.CheckNullifiersByPrefixRequest) returns (responses.CheckNullifiersByPrefixResponse) {} rpc GetAccountDetails(requests.GetAccountDetailsRequest) returns (responses.GetAccountDetailsResponse) {} + rpc GetAccountProofs(requests.GetAccountProofsRequest) returns (responses.GetAccountProofsResponse) {} rpc GetAccountStateDelta(requests.GetAccountStateDeltaRequest) returns (responses.GetAccountStateDeltaResponse) {} rpc GetBlockByNumber(requests.GetBlockByNumberRequest) returns (responses.GetBlockByNumberResponse) {} rpc GetBlockHeaderByNumber(requests.GetBlockHeaderByNumberRequest) returns (responses.GetBlockHeaderByNumberResponse) {} diff --git a/crates/rpc-proto/proto/store.proto b/crates/rpc-proto/proto/store.proto index a9e6fbd8..ec5a1127 100644 --- a/crates/rpc-proto/proto/store.proto +++ b/crates/rpc-proto/proto/store.proto @@ -12,6 +12,7 @@ service Api { rpc CheckNullifiers(requests.CheckNullifiersRequest) returns (responses.CheckNullifiersResponse) {} rpc CheckNullifiersByPrefix(requests.CheckNullifiersByPrefixRequest) returns (responses.CheckNullifiersByPrefixResponse) {} rpc GetAccountDetails(requests.GetAccountDetailsRequest) returns (responses.GetAccountDetailsResponse) {} + rpc GetAccountProofs(requests.GetAccountProofsRequest) returns (responses.GetAccountProofsResponse) {} rpc GetAccountStateDelta(requests.GetAccountStateDeltaRequest) returns (responses.GetAccountStateDeltaResponse) {} rpc GetBlockByNumber(requests.GetBlockByNumberRequest) returns (responses.GetBlockByNumberResponse) {} rpc GetBlockHeaderByNumber(requests.GetBlockHeaderByNumberRequest) returns (responses.GetBlockHeaderByNumberResponse) {} diff --git a/crates/rpc/src/server/api.rs b/crates/rpc/src/server/api.rs index 4ee3ed57..5c25bd48 100644 --- a/crates/rpc/src/server/api.rs +++ b/crates/rpc/src/server/api.rs @@ -3,14 +3,15 @@ use miden_node_proto::{ block_producer::api_client as block_producer_client, requests::{ CheckNullifiersByPrefixRequest, CheckNullifiersRequest, GetAccountDetailsRequest, - GetAccountStateDeltaRequest, GetBlockByNumberRequest, GetBlockHeaderByNumberRequest, - GetNotesByIdRequest, SubmitProvenTransactionRequest, SyncNoteRequest, SyncStateRequest, + GetAccountProofsRequest, GetAccountStateDeltaRequest, GetBlockByNumberRequest, + GetBlockHeaderByNumberRequest, GetNotesByIdRequest, SubmitProvenTransactionRequest, + SyncNoteRequest, SyncStateRequest, }, responses::{ CheckNullifiersByPrefixResponse, CheckNullifiersResponse, GetAccountDetailsResponse, - GetAccountStateDeltaResponse, GetBlockByNumberResponse, GetBlockHeaderByNumberResponse, - GetNotesByIdResponse, SubmitProvenTransactionResponse, SyncNoteResponse, - SyncStateResponse, + GetAccountProofsResponse, GetAccountStateDeltaResponse, GetBlockByNumberResponse, + GetBlockHeaderByNumberResponse, GetNotesByIdResponse, SubmitProvenTransactionResponse, + SyncNoteResponse, SyncStateResponse, }, rpc::api_server, store::api_client as store_client, @@ -19,7 +20,7 @@ use miden_node_proto::{ }; use miden_objects::{ accounts::AccountId, crypto::hash::rpo::RpoDigest, transaction::ProvenTransaction, - utils::serde::Deserializable, Digest, MIN_PROOF_SECURITY_LEVEL, + utils::serde::Deserializable, Digest, MAX_NUM_FOREIGN_ACCOUNTS, MIN_PROOF_SECURITY_LEVEL, }; use miden_tx::TransactionVerifier; use tonic::{ @@ -250,4 +251,29 @@ impl api_server::Api for RpcApi { self.store.clone().get_account_state_delta(request).await } + + #[instrument( + target = "miden-rpc", + name = "rpc:get_account_proofs", + skip_all, + ret(level = "debug"), + err + )] + async fn get_account_proofs( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + + debug!(target: COMPONENT, ?request); + + if request.account_ids.len() > MAX_NUM_FOREIGN_ACCOUNTS as usize { + return Err(Status::invalid_argument(format!( + "Too many accounts requested: {}, limit: {MAX_NUM_FOREIGN_ACCOUNTS}", + request.account_ids.len() + ))); + } + + self.store.clone().get_account_proofs(request).await + } } diff --git a/crates/store/src/db/mod.rs b/crates/store/src/db/mod.rs index a43dd4d3..6e04e67b 100644 --- a/crates/store/src/db/mod.rs +++ b/crates/store/src/db/mod.rs @@ -200,10 +200,15 @@ impl Db { /// Loads all the nullifiers from the DB. #[instrument(target = "miden-store", skip_all, ret(level = "debug"), err)] - pub async fn select_nullifiers(&self) -> Result> { - self.pool.get().await?.interact(sql::select_nullifiers).await.map_err(|err| { - DatabaseError::InteractError(format!("Select nullifiers task failed: {err}")) - })? + pub async fn select_all_nullifiers(&self) -> Result> { + self.pool + .get() + .await? + .interact(sql::select_all_nullifiers) + .await + .map_err(|err| { + DatabaseError::InteractError(format!("Select nullifiers task failed: {err}")) + })? } /// Loads the nullifiers that match the prefixes from the DB. @@ -229,16 +234,16 @@ impl Db { /// Loads all the notes from the DB. #[instrument(target = "miden-store", skip_all, ret(level = "debug"), err)] - pub async fn select_notes(&self) -> Result> { - self.pool.get().await?.interact(sql::select_notes).await.map_err(|err| { + pub async fn select_all_notes(&self) -> Result> { + self.pool.get().await?.interact(sql::select_all_notes).await.map_err(|err| { DatabaseError::InteractError(format!("Select notes task failed: {err}")) })? } /// Loads all the accounts from the DB. #[instrument(target = "miden-store", skip_all, ret(level = "debug"), err)] - pub async fn select_accounts(&self) -> Result> { - self.pool.get().await?.interact(sql::select_accounts).await.map_err(|err| { + pub async fn select_all_accounts(&self) -> Result> { + self.pool.get().await?.interact(sql::select_all_accounts).await.map_err(|err| { DatabaseError::InteractError(format!("Select accounts task failed: {err}")) })? } @@ -291,11 +296,11 @@ impl Db { /// Loads all the account hashes from the DB. #[instrument(target = "miden-store", skip_all, ret(level = "debug"), err)] - pub async fn select_account_hashes(&self) -> Result> { + pub async fn select_all_account_hashes(&self) -> Result> { self.pool .get() .await? - .interact(sql::select_account_hashes) + .interact(sql::select_all_account_hashes) .await .map_err(|err| { DatabaseError::InteractError(format!("Select account hashes task failed: {err}")) @@ -315,6 +320,22 @@ impl Db { })? } + /// Loads public accounts details from the DB. + #[instrument(target = "miden-store", skip_all, ret(level = "debug"), err)] + pub async fn select_accounts_by_ids( + &self, + account_ids: Vec, + ) -> Result> { + self.pool + .get() + .await? + .interact(move |conn| sql::select_accounts_by_ids(conn, &account_ids)) + .await + .map_err(|err| { + DatabaseError::InteractError(format!("Get accounts details task failed: {err}")) + })? + } + #[instrument(target = "miden-store", skip_all, ret(level = "debug"), err)] pub async fn get_state_sync( &self, diff --git a/crates/store/src/db/sql.rs b/crates/store/src/db/sql.rs index 0ef7094b..9980f40d 100644 --- a/crates/store/src/db/sql.rs +++ b/crates/store/src/db/sql.rs @@ -39,7 +39,7 @@ use crate::{ /// # Returns /// /// A vector with accounts, or an error. -pub fn select_accounts(conn: &mut Connection) -> Result> { +pub fn select_all_accounts(conn: &mut Connection) -> Result> { let mut stmt = conn.prepare_cached( " SELECT @@ -67,7 +67,7 @@ pub fn select_accounts(conn: &mut Connection) -> Result> { /// # Returns /// /// The vector with the account id and corresponding hash, or an error. -pub fn select_account_hashes(conn: &mut Connection) -> Result> { +pub fn select_all_account_hashes(conn: &mut Connection) -> Result> { let mut stmt = conn .prepare_cached("SELECT account_id, account_hash FROM accounts ORDER BY block_num ASC;")?; let mut rows = stmt.query([])?; @@ -150,6 +150,40 @@ pub fn select_account(conn: &mut Connection, account_id: AccountId) -> Result Result> { + let mut stmt = conn.prepare_cached( + " + SELECT + account_id, + account_hash, + block_num, + details + FROM + accounts + WHERE + account_id IN rarray(?1); + ", + )?; + + let account_ids: Vec = account_ids.iter().copied().map(u64_to_value).collect(); + let mut rows = stmt.query(params![Rc::new(account_ids)])?; + + let mut result = Vec::new(); + while let Some(row) = rows.next()? { + result.push(account_info_from_row(row)?) + } + + Ok(result) +} + /// Select account deltas by account id and block range from the DB using the given [Connection]. /// /// # Note: @@ -297,7 +331,7 @@ pub fn insert_nullifiers_for_block( /// # Returns /// /// A vector with nullifiers and the block height at which they were created, or an error. -pub fn select_nullifiers(conn: &mut Connection) -> Result> { +pub fn select_all_nullifiers(conn: &mut Connection) -> Result> { let mut stmt = conn.prepare_cached("SELECT nullifier, block_num FROM nullifiers ORDER BY block_num ASC;")?; let mut rows = stmt.query([])?; @@ -415,7 +449,7 @@ pub fn select_nullifiers_by_prefix( /// # Returns /// /// A vector with notes, or an error. -pub fn select_notes(conn: &mut Connection) -> Result> { +pub fn select_all_notes(conn: &mut Connection) -> Result> { let mut stmt = conn.prepare_cached( " SELECT diff --git a/crates/store/src/db/tests.rs b/crates/store/src/db/tests.rs index f33d258b..a7916600 100644 --- a/crates/store/src/db/tests.rs +++ b/crates/store/src/db/tests.rs @@ -133,7 +133,7 @@ fn test_sql_select_nullifiers() { create_block(&mut conn, block_num); // test querying empty table - let nullifiers = sql::select_nullifiers(&mut conn).unwrap(); + let nullifiers = sql::select_all_nullifiers(&mut conn).unwrap(); assert!(nullifiers.is_empty()); // test multiple entries @@ -146,7 +146,7 @@ fn test_sql_select_nullifiers() { let res = sql::insert_nullifiers_for_block(&transaction, &[nullifier], block_num); assert_eq!(res.unwrap(), 1, "One element must have been inserted"); transaction.commit().unwrap(); - let nullifiers = sql::select_nullifiers(&mut conn).unwrap(); + let nullifiers = sql::select_all_nullifiers(&mut conn).unwrap(); assert_eq!(nullifiers, state); } } @@ -159,7 +159,7 @@ fn test_sql_select_notes() { create_block(&mut conn, block_num); // test querying empty table - let notes = sql::select_notes(&mut conn).unwrap(); + let notes = sql::select_all_notes(&mut conn).unwrap(); assert!(notes.is_empty()); // test multiple entries @@ -186,7 +186,7 @@ fn test_sql_select_notes() { let res = sql::insert_notes(&transaction, &[note]); assert_eq!(res.unwrap(), 1, "One element must have been inserted"); transaction.commit().unwrap(); - let notes = sql::select_notes(&mut conn).unwrap(); + let notes = sql::select_all_notes(&mut conn).unwrap(); assert_eq!(notes, state); } } @@ -199,7 +199,7 @@ fn test_sql_select_notes_different_execution_hints() { create_block(&mut conn, block_num); // test querying empty table - let notes = sql::select_notes(&mut conn).unwrap(); + let notes = sql::select_all_notes(&mut conn).unwrap(); assert!(notes.is_empty()); // test multiple entries @@ -286,7 +286,7 @@ fn test_sql_select_accounts() { create_block(&mut conn, block_num); // test querying empty table - let accounts = sql::select_accounts(&mut conn).unwrap(); + let accounts = sql::select_all_accounts(&mut conn).unwrap(); assert!(accounts.is_empty()); // test multiple entries let mut state = vec![]; @@ -316,7 +316,7 @@ fn test_sql_select_accounts() { ); assert_eq!(res.unwrap(), 1, "One element must have been inserted"); transaction.commit().unwrap(); - let accounts = sql::select_accounts(&mut conn).unwrap(); + let accounts = sql::select_all_accounts(&mut conn).unwrap(); assert_eq!(accounts, state); } } @@ -362,7 +362,7 @@ fn test_sql_public_account_details() { ); // test querying empty table - let accounts_in_db = sql::select_accounts(&mut conn).unwrap(); + let accounts_in_db = sql::select_all_accounts(&mut conn).unwrap(); assert!(accounts_in_db.is_empty()); let transaction = conn.transaction().unwrap(); @@ -382,7 +382,7 @@ fn test_sql_public_account_details() { transaction.commit().unwrap(); - let mut accounts_in_db = sql::select_accounts(&mut conn).unwrap(); + let mut accounts_in_db = sql::select_all_accounts(&mut conn).unwrap(); assert_eq!(accounts_in_db.len(), 1, "One element must have been inserted"); @@ -422,7 +422,7 @@ fn test_sql_public_account_details() { transaction.commit().unwrap(); - let mut accounts_in_db = sql::select_accounts(&mut conn).unwrap(); + let mut accounts_in_db = sql::select_all_accounts(&mut conn).unwrap(); assert_eq!(accounts_in_db.len(), 1, "One element must have been inserted"); @@ -475,7 +475,7 @@ fn test_sql_public_account_details() { transaction.commit().unwrap(); - let mut accounts_in_db = sql::select_accounts(&mut conn).unwrap(); + let mut accounts_in_db = sql::select_all_accounts(&mut conn).unwrap(); assert_eq!(accounts_in_db.len(), 1, "One element must have been inserted"); @@ -532,7 +532,7 @@ fn test_sql_select_nullifiers_by_block_range() { sql::insert_nullifiers_for_block(&transaction, &[nullifier2], block_number2).unwrap(); transaction.commit().unwrap(); - let nullifiers = sql::select_nullifiers(&mut conn).unwrap(); + let nullifiers = sql::select_all_nullifiers(&mut conn).unwrap(); assert_eq!(nullifiers, vec![(nullifier1, block_number1), (nullifier2, block_number2)]); // only the nullifiers matching the prefix are included @@ -649,7 +649,7 @@ fn test_select_nullifiers_by_prefix() { sql::insert_nullifiers_for_block(&transaction, &[nullifier2], block_number2).unwrap(); transaction.commit().unwrap(); - let nullifiers = sql::select_nullifiers(&mut conn).unwrap(); + let nullifiers = sql::select_all_nullifiers(&mut conn).unwrap(); assert_eq!(nullifiers, vec![(nullifier1, block_number1), (nullifier2, block_number2)]); // only the nullifiers matching the prefix are included diff --git a/crates/store/src/errors.rs b/crates/store/src/errors.rs index 90083718..2666c138 100644 --- a/crates/store/src/errors.rs +++ b/crates/store/src/errors.rs @@ -68,6 +68,8 @@ pub enum DatabaseError { ApplyBlockFailedClosedChannel(RecvError), #[error("Account {0} not found in the database")] AccountNotFoundInDb(AccountId), + #[error("Accounts {0:?} not found in the database")] + AccountsNotFoundInDb(Vec), #[error("Account {0} is not on the chain")] AccountNotOnChain(AccountId), #[error("Failed to apply block because of public account final hashes mismatch (expected {expected}, \ diff --git a/crates/store/src/server/api.rs b/crates/store/src/server/api.rs index 47b5922f..38e4bdaf 100644 --- a/crates/store/src/server/api.rs +++ b/crates/store/src/server/api.rs @@ -10,18 +10,20 @@ use miden_node_proto::{ note::NoteAuthenticationInfo as NoteAuthenticationInfoProto, requests::{ ApplyBlockRequest, CheckNullifiersByPrefixRequest, CheckNullifiersRequest, - GetAccountDetailsRequest, GetAccountStateDeltaRequest, GetBlockByNumberRequest, - GetBlockHeaderByNumberRequest, GetBlockInputsRequest, GetNoteAuthenticationInfoRequest, - GetNotesByIdRequest, GetTransactionInputsRequest, ListAccountsRequest, - ListNotesRequest, ListNullifiersRequest, SyncNoteRequest, SyncStateRequest, + GetAccountDetailsRequest, GetAccountProofsRequest, GetAccountStateDeltaRequest, + GetBlockByNumberRequest, GetBlockHeaderByNumberRequest, GetBlockInputsRequest, + GetNoteAuthenticationInfoRequest, GetNotesByIdRequest, GetTransactionInputsRequest, + ListAccountsRequest, ListNotesRequest, ListNullifiersRequest, SyncNoteRequest, + SyncStateRequest, }, responses::{ AccountTransactionInputRecord, ApplyBlockResponse, CheckNullifiersByPrefixResponse, - CheckNullifiersResponse, GetAccountDetailsResponse, GetAccountStateDeltaResponse, - GetBlockByNumberResponse, GetBlockHeaderByNumberResponse, GetBlockInputsResponse, - GetNoteAuthenticationInfoResponse, GetNotesByIdResponse, GetTransactionInputsResponse, - ListAccountsResponse, ListNotesResponse, ListNullifiersResponse, - NullifierTransactionInputRecord, NullifierUpdate, SyncNoteResponse, SyncStateResponse, + CheckNullifiersResponse, GetAccountDetailsResponse, GetAccountProofsResponse, + GetAccountStateDeltaResponse, GetBlockByNumberResponse, GetBlockHeaderByNumberResponse, + GetBlockInputsResponse, GetNoteAuthenticationInfoResponse, GetNotesByIdResponse, + GetTransactionInputsResponse, ListAccountsResponse, ListNotesResponse, + ListNullifiersResponse, NullifierTransactionInputRecord, NullifierUpdate, + SyncNoteResponse, SyncStateResponse, }, smt::SmtLeafEntry, store::api_server, @@ -36,7 +38,7 @@ use miden_objects::{ utils::{Deserializable, Serializable}, Felt, ZERO, }; -use tonic::{Response, Status}; +use tonic::{Request, Response, Status}; use tracing::{debug, info, instrument}; use crate::{state::State, types::AccountId, COMPONENT}; @@ -421,7 +423,7 @@ impl api_server::Api for StoreApi { debug!(target: COMPONENT, ?request); - let account_id = request.account_id.ok_or(invalid_argument("Account_id missing"))?.id; + let account_id = request.account_id.ok_or(invalid_argument("`account_id` missing"))?.id; let nullifiers = validate_nullifiers(&request.nullifiers)?; let unauthenticated_notes = validate_notes(&request.unauthenticated_notes)?; @@ -475,6 +477,35 @@ impl api_server::Api for StoreApi { Ok(Response::new(GetBlockByNumberResponse { block })) } + #[instrument( + target = "miden-store", + name = "store:get_account_proofs", + skip_all, + ret(level = "debug"), + err + )] + async fn get_account_proofs( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + + debug!(target: COMPONENT, ?request); + + let account_ids = convert(request.account_ids); + let include_headers = request.include_headers.unwrap_or_default(); + let (block_num, infos) = self + .state + .get_account_states(account_ids, include_headers) + .await + .map_err(internal_error)?; + + Ok(Response::new(GetAccountProofsResponse { + block_num, + account_proofs: infos.into_iter().map(Into::into).collect(), + })) + } + #[instrument( target = "miden-store", name = "store:get_account_state_delta", diff --git a/crates/store/src/state.rs b/crates/store/src/state.rs index 0ae2b8bb..44e03425 100644 --- a/crates/store/src/state.rs +++ b/crates/store/src/state.rs @@ -11,12 +11,12 @@ use std::{ use miden_node_proto::{ convert, domain::{accounts::AccountInfo, blocks::BlockInclusionProof, notes::NoteAuthenticationInfo}, - generated::responses::GetBlockInputsResponse, + generated::responses::{AccountProofsResponse, AccountStateHeader, GetBlockInputsResponse}, AccountInputRecord, NullifierWitness, }; use miden_node_utils::formatting::{format_account_id, format_array}; use miden_objects::{ - accounts::AccountDelta, + accounts::{AccountDelta, AccountHeader}, block::Block, crypto::{ hash::rpo::RpoDigest, @@ -95,6 +95,13 @@ struct InnerState { account_tree: SimpleSmt, } +impl InnerState { + /// Returns the latest block number. + fn latest_block_num(&self) -> BlockNumber { + (self.chain_mmr.forest() + 1).try_into().expect("block number overflow") + } +} + /// The rollup state pub struct State { /// The database which stores block headers, nullifiers, notes, and the latest states of @@ -653,25 +660,83 @@ impl State { /// Lists all known nullifiers with their inclusion blocks, intended for testing. pub async fn list_nullifiers(&self) -> Result, DatabaseError> { - self.db.select_nullifiers().await + self.db.select_all_nullifiers().await } /// Lists all known accounts, with their ids, latest state hash, and block at which the account /// was last modified, intended for testing. pub async fn list_accounts(&self) -> Result, DatabaseError> { - self.db.select_accounts().await + self.db.select_all_accounts().await } /// Lists all known notes, intended for testing. pub async fn list_notes(&self) -> Result, DatabaseError> { - self.db.select_notes().await + self.db.select_all_notes().await } - /// Returns details for public (public) account. + /// Returns details for public (on-chain) account. pub async fn get_account_details(&self, id: AccountId) -> Result { self.db.select_account(id).await } + /// Returns account states with details for public accounts. + pub async fn get_account_states( + &self, + account_ids: Vec, + include_headers: bool, + ) -> Result<(BlockNumber, Vec), DatabaseError> { + // Lock inner state for the whole operation. We need to hold this lock to prevent the + // database, account tree and latest block number from changing during the operation, + // because changing one of them would lead to inconsistent state. + let inner_state = self.inner.read().await; + + let state_headers = if !include_headers { + BTreeMap::::default() + } else { + let infos = self.db.select_accounts_by_ids(account_ids.clone()).await?; + + if account_ids.len() > infos.len() { + let found_ids: BTreeSet = + infos.iter().map(|info| info.summary.account_id.into()).collect(); + return Err(DatabaseError::AccountsNotFoundInDb( + BTreeSet::from_iter(account_ids).difference(&found_ids).copied().collect(), + )); + } + + infos + .into_iter() + .filter_map(|info| { + info.details.map(|details| { + ( + info.summary.account_id.into(), + AccountStateHeader { + header: Some(AccountHeader::from(&details).into()), + storage_header: details.storage().get_header().to_bytes(), + }, + ) + }) + }) + .collect() + }; + + let responses = account_ids + .into_iter() + .map(|account_id| { + let acc_leaf_idx = LeafIndex::new_max_depth(account_id); + let opening = inner_state.account_tree.open(&acc_leaf_idx); + + AccountProofsResponse { + account_id: Some(account_id.into()), + account_hash: Some(opening.value.into()), + account_proof: Some(opening.path.into()), + state_header: state_headers.get(&account_id).cloned(), + } + }) + .collect(); + + Ok((inner_state.latest_block_num(), responses)) + } + /// Returns the state delta between `from_block` (exclusive) and `to_block` (inclusive) for the /// given account. pub(crate) async fn get_account_state_delta( @@ -703,7 +768,7 @@ impl State { /// Returns the latest block number. pub async fn latest_block_num(&self) -> BlockNumber { - (self.inner.read().await.chain_mmr.forest() + 1) as BlockNumber + self.inner.read().await.latest_block_num() } } @@ -712,7 +777,7 @@ impl State { #[instrument(target = "miden-store", skip_all)] async fn load_nullifier_tree(db: &mut Db) -> Result { - let nullifiers = db.select_nullifiers().await?; + let nullifiers = db.select_all_nullifiers().await?; let len = nullifiers.len(); let now = Instant::now(); @@ -742,7 +807,7 @@ async fn load_accounts( db: &mut Db, ) -> Result, StateInitializationError> { let account_data: Vec<_> = db - .select_account_hashes() + .select_all_account_hashes() .await? .into_iter() .map(|(id, account_hash)| (id, account_hash.into())) diff --git a/proto/account.proto b/proto/account.proto index a0eb3697..8f7becff 100644 --- a/proto/account.proto +++ b/proto/account.proto @@ -20,3 +20,14 @@ message AccountInfo { AccountSummary summary = 1; optional bytes details = 2; } + +message AccountHeader { + // Vault root hash. + digest.Digest vault_root = 1; + // Storage root hash. + digest.Digest storage_commitment = 2; + // Code root hash. + digest.Digest code_commitment = 3; + // Account nonce. + uint64 nonce = 4; +} diff --git a/proto/requests.proto b/proto/requests.proto index a9c210a2..9f9554b4 100644 --- a/proto/requests.proto +++ b/proto/requests.proto @@ -131,3 +131,10 @@ message GetAccountStateDeltaRequest { // Block number up to which the delta is requested (inclusive). fixed32 to_block_num = 3; } + +message GetAccountProofsRequest { + // List of account IDs to get states. + repeated account.AccountId account_ids = 1; + // Optional flag to include header in the response. `false` by default. + optional bool include_headers = 2; +} diff --git a/proto/responses.proto b/proto/responses.proto index 7342294f..7d7d0003 100644 --- a/proto/responses.proto +++ b/proto/responses.proto @@ -173,3 +173,28 @@ message GetAccountStateDeltaResponse { // The calculated `AccountStateDelta` encoded using miden native format optional bytes delta = 1; } + +message GetAccountProofsResponse { + // Block number at which the state of the account was returned. + fixed32 block_num = 1; + // List of account state infos for the requested account keys. + repeated AccountProofsResponse account_proofs = 2; +} + +message AccountProofsResponse { + // Account ID. + account.AccountId account_id = 1; + // Account hash. + digest.Digest account_hash = 2; + // Authentication path from the `account_root` of the block header to the account. + merkle.MerklePath account_proof = 3; + // State header for public accounts. Filled only if `include_headers` flag is set to `true`. + optional AccountStateHeader state_header = 4; +} + +message AccountStateHeader { + // Account header. + account.AccountHeader header = 1; + // Values of all account storage slots (max 255). + bytes storage_header = 2; +} diff --git a/proto/rpc.proto b/proto/rpc.proto index da2c2bdd..13934b2c 100644 --- a/proto/rpc.proto +++ b/proto/rpc.proto @@ -9,6 +9,7 @@ service Api { rpc CheckNullifiers(requests.CheckNullifiersRequest) returns (responses.CheckNullifiersResponse) {} rpc CheckNullifiersByPrefix(requests.CheckNullifiersByPrefixRequest) returns (responses.CheckNullifiersByPrefixResponse) {} rpc GetAccountDetails(requests.GetAccountDetailsRequest) returns (responses.GetAccountDetailsResponse) {} + rpc GetAccountProofs(requests.GetAccountProofsRequest) returns (responses.GetAccountProofsResponse) {} rpc GetAccountStateDelta(requests.GetAccountStateDeltaRequest) returns (responses.GetAccountStateDeltaResponse) {} rpc GetBlockByNumber(requests.GetBlockByNumberRequest) returns (responses.GetBlockByNumberResponse) {} rpc GetBlockHeaderByNumber(requests.GetBlockHeaderByNumberRequest) returns (responses.GetBlockHeaderByNumberResponse) {} diff --git a/proto/store.proto b/proto/store.proto index a9e6fbd8..ec5a1127 100644 --- a/proto/store.proto +++ b/proto/store.proto @@ -12,6 +12,7 @@ service Api { rpc CheckNullifiers(requests.CheckNullifiersRequest) returns (responses.CheckNullifiersResponse) {} rpc CheckNullifiersByPrefix(requests.CheckNullifiersByPrefixRequest) returns (responses.CheckNullifiersByPrefixResponse) {} rpc GetAccountDetails(requests.GetAccountDetailsRequest) returns (responses.GetAccountDetailsResponse) {} + rpc GetAccountProofs(requests.GetAccountProofsRequest) returns (responses.GetAccountProofsResponse) {} rpc GetAccountStateDelta(requests.GetAccountStateDeltaRequest) returns (responses.GetAccountStateDeltaResponse) {} rpc GetBlockByNumber(requests.GetBlockByNumberRequest) returns (responses.GetBlockByNumberResponse) {} rpc GetBlockHeaderByNumber(requests.GetBlockHeaderByNumberRequest) returns (responses.GetBlockHeaderByNumberResponse) {}