diff --git a/block-producer/src/batch_builder/batch.rs b/block-producer/src/batch_builder/batch.rs index b5784c433..9f6b550bd 100644 --- a/block-producer/src/batch_builder/batch.rs +++ b/block-producer/src/batch_builder/batch.rs @@ -1,11 +1,12 @@ use std::collections::BTreeMap; -use miden_node_proto::domain::accounts::UpdatedAccount; +use miden_node_proto::domain::accounts::AccountUpdateDetails; use miden_objects::{ accounts::AccountId, batches::BatchNoteTree, crypto::hash::blake::{Blake3Digest, Blake3_256}, notes::{NoteEnvelope, Nullifier}, + transaction::AccountDetails, Digest, MAX_NOTES_PER_BATCH, }; use tracing::instrument; @@ -54,6 +55,7 @@ impl TransactionBatch { AccountStates { initial_state: tx.initial_account_hash(), final_state: tx.final_account_hash(), + details: tx.account_details().cloned(), }, ) }) @@ -107,15 +109,15 @@ impl TransactionBatch { .map(|(account_id, account_states)| (*account_id, account_states.initial_state)) } - /// Returns an iterator over (account_id, new_state_hash) tuples for accounts that were + /// Returns an iterator over (account_id, details, new_state_hash) tuples for accounts that were /// modified in this transaction batch. - pub fn updated_accounts(&self) -> impl Iterator + '_ { + pub fn updated_accounts(&self) -> impl Iterator + '_ { self.updated_accounts .iter() - .map(|(&account_id, account_states)| UpdatedAccount { + .map(|(&account_id, account_states)| AccountUpdateDetails { account_id, final_state_hash: account_states.final_state, - details: None, // TODO: In the next PR: account_states.details.clone(), + details: account_states.details.clone(), }) } @@ -150,8 +152,9 @@ impl TransactionBatch { /// account. /// /// TODO: should this be moved into domain objects? -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Clone, PartialEq, Eq)] struct AccountStates { initial_state: Digest, final_state: Digest, + details: Option, } diff --git a/block-producer/src/block.rs b/block-producer/src/block.rs index 6e43674c5..8ca9d0b4c 100644 --- a/block-producer/src/block.rs +++ b/block-producer/src/block.rs @@ -1,7 +1,7 @@ use std::collections::BTreeMap; use miden_node_proto::{ - domain::accounts::UpdatedAccount, + domain::accounts::AccountUpdateDetails, errors::{ConversionError, MissingFieldHelper}, generated::responses::GetBlockInputsResponse, AccountInputRecord, NullifierWitness, @@ -18,11 +18,10 @@ use crate::store::BlockInputsError; #[derive(Debug, Clone)] pub struct Block { pub header: BlockHeader, - pub updated_accounts: Vec, + pub updated_accounts: Vec, pub created_notes: Vec<(usize, usize, NoteEnvelope)>, pub produced_nullifiers: Vec, // TODO: - // - full states for updated public accounts // - full states for created public notes // - zk proof } diff --git a/block-producer/src/block_builder/mod.rs b/block-producer/src/block_builder/mod.rs index cb0502528..eb92612b4 100644 --- a/block-producer/src/block_builder/mod.rs +++ b/block-producer/src/block_builder/mod.rs @@ -119,7 +119,7 @@ where info!(target: COMPONENT, block_num, %block_hash, "block built"); debug!(target: COMPONENT, ?block); - self.state_view.apply_block(block).await?; + self.state_view.apply_block(&block).await?; info!(target: COMPONENT, block_num, %block_hash, "block committed"); diff --git a/block-producer/src/block_builder/prover/block_witness.rs b/block-producer/src/block_builder/prover/block_witness.rs index c72c84ddc..026dff08f 100644 --- a/block-producer/src/block_builder/prover/block_witness.rs +++ b/block-producer/src/block_builder/prover/block_witness.rs @@ -1,6 +1,6 @@ use std::collections::{BTreeMap, BTreeSet}; -use miden_node_proto::domain::accounts::UpdatedAccount; +use miden_node_proto::domain::accounts::AccountUpdateDetails; use miden_objects::{ accounts::AccountId, crypto::merkle::{EmptySubtreeRoots, MerklePath, MerkleStore, MmrPeaks, SmtProof}, @@ -38,7 +38,7 @@ impl BlockWitness { let updated_accounts = { let mut account_initial_states: BTreeMap = - batches.iter().flat_map(|batch| batch.account_initial_states()).collect(); + batches.iter().flat_map(TransactionBatch::account_initial_states).collect(); let mut account_merkle_proofs: BTreeMap = block_inputs .accounts @@ -48,9 +48,9 @@ impl BlockWitness { batches .iter() - .flat_map(|batch| batch.updated_accounts()) + .flat_map(TransactionBatch::updated_accounts) .map( - |UpdatedAccount { + |AccountUpdateDetails { account_id, final_state_hash, .. diff --git a/block-producer/src/block_builder/prover/tests.rs b/block-producer/src/block_builder/prover/tests.rs index 1c1c38ab9..d7b3a292b 100644 --- a/block-producer/src/block_builder/prover/tests.rs +++ b/block-producer/src/block_builder/prover/tests.rs @@ -1,6 +1,6 @@ use std::{collections::BTreeMap, iter}; -use miden_node_proto::domain::accounts::UpdatedAccount; +use miden_node_proto::domain::accounts::AccountUpdateDetails; use miden_objects::{ accounts::{ AccountId, ACCOUNT_ID_OFF_CHAIN_SENDER, ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN, @@ -239,7 +239,7 @@ async fn test_compute_account_root_success() { account_ids .iter() .zip(account_final_states.iter()) - .map(|(&account_id, &account_hash)| UpdatedAccount { + .map(|(&account_id, &account_hash)| AccountUpdateDetails { account_id, final_state_hash: account_hash.into(), details: None, diff --git a/block-producer/src/state_view/mod.rs b/block-producer/src/state_view/mod.rs index 8af9ebff6..89fde23a3 100644 --- a/block-producer/src/state_view/mod.rs +++ b/block-producer/src/state_view/mod.rs @@ -119,9 +119,9 @@ where #[instrument(target = "miden-block-producer", skip_all, err)] async fn apply_block( &self, - block: Block, + block: &Block, ) -> Result<(), ApplyBlockError> { - self.store.apply_block(block.clone()).await?; + self.store.apply_block(block).await?; let mut locked_accounts_in_flight = self.accounts_in_flight.write().await; let mut locked_nullifiers_in_flight = self.nullifiers_in_flight.write().await; diff --git a/block-producer/src/state_view/tests/apply_block.rs b/block-producer/src/state_view/tests/apply_block.rs index 0db199277..af143700d 100644 --- a/block-producer/src/state_view/tests/apply_block.rs +++ b/block-producer/src/state_view/tests/apply_block.rs @@ -6,7 +6,7 @@ use std::iter; -use miden_node_proto::domain::accounts::UpdatedAccount; +use miden_node_proto::domain::accounts::AccountUpdateDetails; use super::*; use crate::test_utils::{block::MockBlockBuilder, MockStoreSuccessBuilder}; @@ -34,7 +34,7 @@ async fn test_apply_block_ab1() { .await .account_updates( std::iter::once(account) - .map(|mock_account| UpdatedAccount { + .map(|mock_account| AccountUpdateDetails { account_id: mock_account.id, final_state_hash: mock_account.states[1], details: None, @@ -43,7 +43,7 @@ async fn test_apply_block_ab1() { ) .build(); - let apply_block_res = state_view.apply_block(block).await; + let apply_block_res = state_view.apply_block(&block).await; assert!(apply_block_res.is_ok()); assert_eq!(*store.num_apply_block_called.read().await, 1); @@ -81,7 +81,7 @@ async fn test_apply_block_ab2() { .account_updates( accounts_in_block .into_iter() - .map(|mock_account| UpdatedAccount { + .map(|mock_account| AccountUpdateDetails { account_id: mock_account.id, final_state_hash: mock_account.states[1], details: None, @@ -90,7 +90,7 @@ async fn test_apply_block_ab2() { ) .build(); - let apply_block_res = state_view.apply_block(block).await; + let apply_block_res = state_view.apply_block(&block).await; assert!(apply_block_res.is_ok()); let accounts_still_in_flight = state_view.accounts_in_flight.read().await; @@ -130,7 +130,7 @@ async fn test_apply_block_ab3() { accounts .clone() .into_iter() - .map(|mock_account| UpdatedAccount { + .map(|mock_account| AccountUpdateDetails { account_id: mock_account.id, final_state_hash: mock_account.states[1], details: None, @@ -139,7 +139,7 @@ async fn test_apply_block_ab3() { ) .build(); - let apply_block_res = state_view.apply_block(block).await; + let apply_block_res = state_view.apply_block(&block).await; assert!(apply_block_res.is_ok()); // Craft a new transaction which tries to consume the same note that was consumed in the diff --git a/block-producer/src/store/mod.rs b/block-producer/src/store/mod.rs index b02f9fa41..2407f6acd 100644 --- a/block-producer/src/store/mod.rs +++ b/block-producer/src/store/mod.rs @@ -49,7 +49,7 @@ pub trait Store: ApplyBlock { pub trait ApplyBlock: Send + Sync + 'static { async fn apply_block( &self, - block: Block, + block: &Block, ) -> Result<(), ApplyBlockError>; } @@ -131,13 +131,13 @@ impl ApplyBlock for DefaultStore { #[instrument(target = "miden-block-producer", skip_all, err)] async fn apply_block( &self, - block: Block, + block: &Block, ) -> Result<(), ApplyBlockError> { let request = tonic::Request::new(ApplyBlockRequest { - block: Some(block.header.into()), + block: Some((&block.header).into()), accounts: convert(&block.updated_accounts), - nullifiers: convert(block.produced_nullifiers), - notes: convert(block.created_notes), + nullifiers: convert(&block.produced_nullifiers), + notes: convert(&block.created_notes), }); let _ = self diff --git a/block-producer/src/test_utils/block.rs b/block-producer/src/test_utils/block.rs index 02c8a28cd..f3d877273 100644 --- a/block-producer/src/test_utils/block.rs +++ b/block-producer/src/test_utils/block.rs @@ -1,4 +1,4 @@ -use miden_node_proto::domain::accounts::UpdatedAccount; +use miden_node_proto::domain::accounts::AccountUpdateDetails; use miden_objects::{ block::BlockNoteTree, crypto::merkle::{Mmr, SimpleSmt}, @@ -68,7 +68,7 @@ pub async fn build_actual_block_header( let updated_accounts: Vec<_> = batches.iter().flat_map(TransactionBatch::updated_accounts).collect(); let produced_nullifiers: Vec = - batches.iter().flat_map(|batch| batch.produced_nullifiers()).collect(); + batches.iter().flat_map(TransactionBatch::produced_nullifiers).collect(); let block_inputs_from_store: BlockInputs = store .get_block_inputs( @@ -89,7 +89,7 @@ pub struct MockBlockBuilder { store_chain_mmr: Mmr, last_block_header: BlockHeader, - updated_accounts: Option>, + updated_accounts: Option>, created_notes: Option>, produced_nullifiers: Option>, } @@ -109,7 +109,7 @@ impl MockBlockBuilder { pub fn account_updates( mut self, - updated_accounts: Vec, + updated_accounts: Vec, ) -> Self { for update in &updated_accounts { self.store_accounts diff --git a/block-producer/src/test_utils/store.rs b/block-producer/src/test_utils/store.rs index abd6cba22..41c1f3192 100644 --- a/block-producer/src/test_utils/store.rs +++ b/block-producer/src/test_utils/store.rs @@ -174,7 +174,7 @@ impl MockStoreSuccess { impl ApplyBlock for MockStoreSuccess { async fn apply_block( &self, - block: Block, + block: &Block, ) -> Result<(), ApplyBlockError> { // Intentionally, we take and hold both locks, to prevent calls to `get_tx_inputs()` from going through while we're updating the store's data structure let mut locked_accounts = self.accounts.write().await; @@ -187,7 +187,7 @@ impl ApplyBlock for MockStoreSuccess { debug_assert_eq!(locked_accounts.root(), block.header.account_root()); // update nullifiers - for nullifier in block.produced_nullifiers { + for nullifier in &block.produced_nullifiers { locked_produced_nullifiers .insert(nullifier.inner(), [block.header.block_num().into(), ZERO, ZERO, ZERO]); } @@ -291,7 +291,7 @@ pub struct MockStoreFailure; impl ApplyBlock for MockStoreFailure { async fn apply_block( &self, - _block: Block, + _block: &Block, ) -> Result<(), ApplyBlockError> { Err(ApplyBlockError::GrpcClientError(String::new())) } diff --git a/proto/proto/account.proto b/proto/proto/account.proto index ec4b5b7eb..f73c5dac7 100644 --- a/proto/proto/account.proto +++ b/proto/proto/account.proto @@ -10,13 +10,13 @@ message AccountId { fixed64 id = 1; } -message AccountHashUpdate { - account.AccountId account_id = 1; +message AccountSummary { + AccountId account_id = 1; digest.Digest account_hash = 2; uint32 block_num = 3; } message AccountInfo { - AccountHashUpdate update = 1; + AccountSummary summary = 1; optional bytes details = 2; } diff --git a/proto/proto/requests.proto b/proto/proto/requests.proto index 529fc92fe..43e8301dc 100644 --- a/proto/proto/requests.proto +++ b/proto/proto/requests.proto @@ -10,6 +10,8 @@ import "note.proto"; message AccountUpdate { account.AccountId account_id = 1; digest.Digest account_hash = 2; + // Details for public (on-chain) account. + optional bytes details = 3; } message ApplyBlockRequest { diff --git a/proto/proto/responses.proto b/proto/proto/responses.proto index 27913a34f..b9f548d1d 100644 --- a/proto/proto/responses.proto +++ b/proto/proto/responses.proto @@ -36,7 +36,7 @@ message SyncStateResponse { mmr.MmrDelta mmr_delta = 3; // a list of account hashes updated after `block_num + 1` but not after `block_header.block_num` - repeated account.AccountHashUpdate accounts = 5; + repeated account.AccountSummary accounts = 5; // a list of all notes together with the Merkle paths from `block_header.note_root` repeated note.NoteSyncRecord notes = 6; diff --git a/proto/src/domain/accounts.rs b/proto/src/domain/accounts.rs index d8a1e383a..3f298cd63 100644 --- a/proto/src/domain/accounts.rs +++ b/proto/src/domain/accounts.rs @@ -13,8 +13,8 @@ use crate::{ errors::{ConversionError, MissingFieldHelper}, generated::{ account::{ - AccountHashUpdate as AccountHashUpdatePb, AccountId as AccountIdPb, - AccountInfo as AccountInfoPb, + AccountId as AccountIdPb, AccountInfo as AccountInfoPb, + AccountSummary as AccountSummaryPb, }, requests::AccountUpdate, responses::{AccountBlockInputRecord, AccountTransactionInputRecord}, @@ -51,6 +51,12 @@ impl From for AccountIdPb { } } +impl From<&AccountId> for AccountIdPb { + fn from(account_id: &AccountId) -> Self { + (*account_id).into() + } +} + impl From for AccountIdPb { fn from(account_id: AccountId) -> Self { Self { @@ -80,14 +86,14 @@ impl TryFrom for AccountId { // ================================================================================================ #[derive(Debug, PartialEq)] -pub struct AccountHashUpdate { +pub struct AccountSummary { pub account_id: AccountId, pub account_hash: RpoDigest, pub block_num: u32, } -impl From<&AccountHashUpdate> for AccountHashUpdatePb { - fn from(update: &AccountHashUpdate) -> Self { +impl From<&AccountSummary> for AccountSummaryPb { + fn from(update: &AccountSummary) -> Self { Self { account_id: Some(update.account_id.into()), account_hash: Some(update.account_hash.into()), @@ -98,32 +104,32 @@ impl From<&AccountHashUpdate> for AccountHashUpdatePb { #[derive(Debug, PartialEq)] pub struct AccountInfo { - pub update: AccountHashUpdate, + pub summary: AccountSummary, pub details: Option, } impl From<&AccountInfo> for AccountInfoPb { - fn from(AccountInfo { update, details }: &AccountInfo) -> Self { + fn from(AccountInfo { summary, details }: &AccountInfo) -> Self { Self { - update: Some(update.into()), + summary: Some(summary.into()), details: details.as_ref().map(|account| account.to_bytes()), } } } #[derive(Debug, Clone, PartialEq, Eq)] -pub struct UpdatedAccount { +pub struct AccountUpdateDetails { pub account_id: AccountId, pub final_state_hash: Digest, pub details: Option, } -impl From<&UpdatedAccount> for AccountUpdate { - fn from(update: &UpdatedAccount) -> Self { +impl From<&AccountUpdateDetails> for AccountUpdate { + fn from(update: &AccountUpdateDetails) -> Self { Self { account_id: Some(update.account_id.into()), account_hash: Some(update.final_state_hash.into()), - // details: update.details.to_bytes(), + details: update.details.as_ref().map(|details| details.to_bytes()), } } } diff --git a/proto/src/domain/blocks.rs b/proto/src/domain/blocks.rs index c33ace75f..64ed90af6 100644 --- a/proto/src/domain/blocks.rs +++ b/proto/src/domain/blocks.rs @@ -8,8 +8,8 @@ use crate::{ // BLOCK HEADER // ================================================================================================ -impl From for block_header::BlockHeader { - fn from(header: BlockHeader) -> Self { +impl From<&BlockHeader> for block_header::BlockHeader { + fn from(header: &BlockHeader) -> Self { Self { prev_hash: Some(header.prev_hash().into()), block_num: header.block_num(), @@ -27,6 +27,12 @@ impl From for block_header::BlockHeader { } } +impl From for block_header::BlockHeader { + fn from(header: BlockHeader) -> Self { + (&header).into() + } +} + impl TryFrom<&block_header::BlockHeader> for BlockHeader { type Error = ConversionError; diff --git a/proto/src/domain/notes.rs b/proto/src/domain/notes.rs index 1ed040cef..893997499 100644 --- a/proto/src/domain/notes.rs +++ b/proto/src/domain/notes.rs @@ -5,11 +5,11 @@ use crate::generated::note; // NoteCreated // ================================================================================================ -impl From<(usize, usize, NoteEnvelope)> for note::NoteCreated { - fn from((batch_idx, note_idx, note): (usize, usize, NoteEnvelope)) -> Self { +impl From<&(usize, usize, NoteEnvelope)> for note::NoteCreated { + fn from((batch_idx, note_idx, note): &(usize, usize, NoteEnvelope)) -> Self { Self { - batch_index: batch_idx as u32, - note_index: note_idx as u32, + batch_index: *batch_idx as u32, + note_index: *note_idx as u32, note_id: Some(note.note_id().into()), sender: Some(note.metadata().sender().into()), tag: note.metadata().tag().into(), diff --git a/proto/src/generated/account.rs b/proto/src/generated/account.rs index 44ff6017b..c09f39298 100644 --- a/proto/src/generated/account.rs +++ b/proto/src/generated/account.rs @@ -13,7 +13,7 @@ pub struct AccountId { #[derive(Eq, PartialOrd, Ord, Hash)] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct AccountHashUpdate { +pub struct AccountSummary { #[prost(message, optional, tag = "1")] pub account_id: ::core::option::Option, #[prost(message, optional, tag = "2")] @@ -26,7 +26,7 @@ pub struct AccountHashUpdate { #[derive(Clone, PartialEq, ::prost::Message)] pub struct AccountInfo { #[prost(message, optional, tag = "1")] - pub update: ::core::option::Option, + pub summary: ::core::option::Option, #[prost(bytes = "vec", optional, tag = "2")] pub details: ::core::option::Option<::prost::alloc::vec::Vec>, } diff --git a/proto/src/generated/requests.rs b/proto/src/generated/requests.rs index c39110684..6a62c2f1a 100644 --- a/proto/src/generated/requests.rs +++ b/proto/src/generated/requests.rs @@ -7,6 +7,9 @@ pub struct AccountUpdate { pub account_id: ::core::option::Option, #[prost(message, optional, tag = "2")] pub account_hash: ::core::option::Option, + /// Details for public (on-chain) account. + #[prost(bytes = "vec", optional, tag = "3")] + pub details: ::core::option::Option<::prost::alloc::vec::Vec>, } #[derive(Eq, PartialOrd, Ord, Hash)] #[allow(clippy::derive_partial_eq_without_eq)] diff --git a/proto/src/generated/responses.rs b/proto/src/generated/responses.rs index c16613513..8caf31ad1 100644 --- a/proto/src/generated/responses.rs +++ b/proto/src/generated/responses.rs @@ -42,7 +42,7 @@ pub struct SyncStateResponse { pub mmr_delta: ::core::option::Option, /// a list of account hashes updated after `block_num + 1` but not after `block_header.block_num` #[prost(message, repeated, tag = "5")] - pub accounts: ::prost::alloc::vec::Vec, + pub accounts: ::prost::alloc::vec::Vec, /// a list of all notes together with the Merkle paths from `block_header.note_root` #[prost(message, repeated, tag = "6")] pub notes: ::prost::alloc::vec::Vec, diff --git a/rpc/README.md b/rpc/README.md index 28bfdf9cf..17e3e20df 100644 --- a/rpc/README.md +++ b/rpc/README.md @@ -82,8 +82,7 @@ Returns the latest state of an account with the specified ID. **Returns** -- `account`: `AccountInfo` – account state information. For public accounts there is also details describing current state, stored on-chain; - for private accounts only hash of the latest known state is returned. +- `account`: `AccountInfo` – latest state of the account. For public accounts, this will include full details describing the current account state. For private accounts, only the hash of the latest state and the time of the last update is returned. ### SyncState @@ -111,7 +110,7 @@ contains excessive notes and nullifiers, client can make additional filtering of - `chain_tip`: `uint32` – number of the latest block in the chain. - `block_header`: `BlockHeader` – block header of the block with the first note matching the specified criteria. - `mmr_delta`: `MmrDelta` – data needed to update the partial MMR from `block_num + 1` to `block_header.block_num`. -- `accounts`: `[AccountHashUpdate]` – a list of account hashes updated after `block_num + 1` but not after `block_header.block_num`. +- `accounts`: `[AccountSummary]` – account summaries for accounts updated after `block_num + 1` but not after `block_header.block_num`. - `notes`: `[NoteSyncRecord]` – a list of all notes together with the Merkle paths from `block_header.note_root`. - `nullifiers`: `[NullifierUpdate]` – a list of nullifiers created between `block_num + 1` and `block_header.block_num`. diff --git a/store/README.md b/store/README.md index ddcbb3bfb..e264b06d2 100644 --- a/store/README.md +++ b/store/README.md @@ -128,8 +128,7 @@ Returns the latest state of an account with the specified ID. **Returns** -- `account`: `AccountInfo` – account state information. For public accounts there is also details describing current state, stored on-chain; - for private accounts only hash of the latest known state is returned. +- `account`: `AccountInfo` – latest state of the account. For public accounts, this will include full details describing the current account state. For private accounts, only the hash of the latest state and the time of the last update is returned. ### SyncState @@ -157,7 +156,7 @@ contains excessive notes and nullifiers, client can make additional filtering of - `chain_tip`: `uint32` – number of the latest block in the chain. - `block_header`: `BlockHeader` – block header of the block with the first note matching the specified criteria. - `mmr_delta`: `MmrDelta` – data needed to update the partial MMR from `block_num + 1` to `block_header.block_num`. -- `accounts`: `[AccountHashUpdate]` – a list of account hashes updated after `block_num + 1` but not after `block_header.block_num`. +- `accounts`: `[AccountSummary]` – account summaries for accounts updated after `block_num + 1` but not after `block_header.block_num`. - `notes`: `[NoteSyncRecord]` – a list of all notes together with the Merkle paths from `block_header.note_root`. - `nullifiers`: `[NullifierUpdate]` – a list of nullifiers created between `block_num + 1` and `block_header.block_num`. diff --git a/store/src/db/mod.rs b/store/src/db/mod.rs index eaef75d60..52dd5ace1 100644 --- a/store/src/db/mod.rs +++ b/store/src/db/mod.rs @@ -1,12 +1,11 @@ use std::fs::{self, create_dir_all}; use deadpool_sqlite::{Config as SqliteConfig, Hook, HookError, Pool, Runtime}; -use miden_node_proto::domain::accounts::{AccountHashUpdate, AccountInfo}; +use miden_node_proto::domain::accounts::{AccountInfo, AccountSummary, AccountUpdateDetails}; use miden_objects::{ block::BlockNoteTree, crypto::{hash::rpo::RpoDigest, merkle::MerklePath, utils::Deserializable}, notes::{NoteId, Nullifier}, - transaction::AccountDetails, BlockHeader, GENESIS_BLOCK, }; use rusqlite::vtab::array; @@ -68,7 +67,7 @@ pub struct StateSyncUpdate { pub notes: Vec, pub block_header: BlockHeader, pub chain_tip: BlockNumber, - pub account_updates: Vec, + pub account_updates: Vec, pub nullifiers: Vec, } @@ -280,7 +279,7 @@ impl Db { block_header: BlockHeader, notes: Vec, nullifiers: Vec, - accounts: Vec<(AccountId, Option, RpoDigest)>, + accounts: Vec, ) -> Result<()> { self.pool .get() @@ -363,8 +362,14 @@ impl Db { let transaction = conn.transaction()?; let accounts: Vec<_> = account_smt .leaves() - .map(|(account_id, state_hash)| (account_id, None, state_hash.into())) - .collect(); + .map(|(account_id, state_hash)| { + Ok(AccountUpdateDetails { + account_id: account_id.try_into()?, + final_state_hash: state_hash.into(), + details: None, + }) + }) + .collect::>()?; sql::apply_block( &transaction, &expected_genesis_header, diff --git a/store/src/db/sql.rs b/store/src/db/sql.rs index 354bc666c..cbd4ee1f5 100644 --- a/store/src/db/sql.rs +++ b/store/src/db/sql.rs @@ -1,17 +1,21 @@ //! Wrapper functions for SQL statements. -use std::rc::Rc; +use std::{borrow::Cow, rc::Rc}; -use miden_node_proto::domain::accounts::{AccountHashUpdate, AccountInfo}; +use miden_node_proto::domain::accounts::{AccountInfo, AccountSummary, AccountUpdateDetails}; use miden_objects::{ - accounts::Account, + accounts::{Account, AccountDelta}, crypto::{hash::rpo::RpoDigest, merkle::MerklePath}, notes::{NoteId, Nullifier}, transaction::AccountDetails, utils::serde::{Deserializable, Serializable}, BlockHeader, }; -use rusqlite::{params, types::Value, Connection, Transaction}; +use rusqlite::{ + params, + types::{Value, ValueRef}, + Connection, Transaction, +}; use super::{Note, NoteCreated, NullifierInfo, Result, StateSyncUpdate}; use crate::{ @@ -72,18 +76,18 @@ pub fn select_account_hashes(conn: &mut Connection) -> Result Result> { +) -> Result> { let account_ids: Vec = account_ids.iter().copied().map(u64_to_value).collect(); let mut stmt = conn.prepare( @@ -154,28 +158,58 @@ pub fn select_account( /// transaction. pub fn upsert_accounts( transaction: &Transaction, - accounts: &[(AccountId, Option, RpoDigest)], + accounts: &[AccountUpdateDetails], block_num: BlockNumber, ) -> Result { - let mut stmt = transaction.prepare( - " - INSERT OR REPLACE INTO - accounts (account_id, account_hash, block_num, details) - VALUES (?1, ?2, ?3, ?4); - ", + let mut upsert_stmt = transaction.prepare( + "INSERT OR REPLACE INTO accounts (account_id, account_hash, block_num, details) VALUES (?1, ?2, ?3, ?4);", )?; + let mut select_details_stmt = + transaction.prepare("SELECT details FROM accounts WHERE account_id = ?1;")?; let mut count = 0; - for (account_id, details, account_hash) in accounts.iter() { - // TODO: Process account details/delta (in the next PR) + for update in accounts.iter() { + let account_id = update.account_id.into(); + let full_account = match &update.details { + None => None, + Some(AccountDetails::Full(account)) => { + debug_assert!(account.is_new()); + debug_assert_eq!(account_id, u64::from(account.id())); + + if account.hash() != update.final_state_hash { + return Err(DatabaseError::ApplyBlockFailedAccountHashesMismatch { + calculated: account.hash(), + expected: update.final_state_hash, + }); + } + + Some(Cow::Borrowed(account)) + }, + Some(AccountDetails::Delta(delta)) => { + let mut rows = select_details_stmt.query(params![u64_to_value(account_id)])?; + let Some(row) = rows.next()? else { + return Err(DatabaseError::AccountNotFoundInDb(account_id)); + }; - count += stmt.execute(params![ - u64_to_value(*account_id), - account_hash.to_bytes(), + let account = + apply_delta(account_id, &row.get_ref(0)?, delta, &update.final_state_hash)?; + + Some(Cow::Owned(account)) + }, + }; + + let inserted = upsert_stmt.execute(params![ + u64_to_value(account_id), + update.final_state_hash.to_bytes(), block_num, - details.as_ref().map(|details| details.to_bytes()), - ])? + full_account.as_ref().map(|account| account.to_bytes()), + ])?; + + debug_assert_eq!(inserted, 1); + + count += inserted; } + Ok(count) } @@ -653,7 +687,7 @@ pub fn apply_block( block_header: &BlockHeader, notes: &[Note], nullifiers: &[Nullifier], - accounts: &[(AccountId, Option, RpoDigest)], + accounts: &[AccountUpdateDetails], ) -> Result { let mut count = 0; count += insert_block_header(transaction, block_header)?; @@ -699,16 +733,16 @@ fn column_value_as_u64( Ok(value as u64) } -/// Constructs `AccountHashUpdate` from the row of `accounts` table. +/// Constructs `AccountSummary` from the row of `accounts` table. /// /// Note: field ordering must be the same, as in `accounts` table! -fn account_hash_update_from_row(row: &rusqlite::Row<'_>) -> Result { +fn account_hash_update_from_row(row: &rusqlite::Row<'_>) -> Result { let account_id = column_value_as_u64(row, 0)?; let account_hash_data = row.get_ref(1)?.as_blob()?; let account_hash = RpoDigest::read_from_bytes(account_hash_data)?; let block_num = row.get(2)?; - Ok(AccountHashUpdate { + Ok(AccountSummary { account_id: account_id.try_into()?, account_hash, block_num, @@ -721,8 +755,38 @@ fn account_hash_update_from_row(row: &rusqlite::Row<'_>) -> Result) -> Result { let update = account_hash_update_from_row(row)?; - let details = row.get_ref(3)?.as_bytes_or_null()?; + let details = row.get_ref(3)?.as_blob_or_null()?; let details = details.map(Account::read_from_bytes).transpose()?; - Ok(AccountInfo { update, details }) + Ok(AccountInfo { + summary: update, + details, + }) +} + +/// Deserializes account and applies account delta. +fn apply_delta( + account_id: u64, + value: &ValueRef<'_>, + delta: &AccountDelta, + final_state_hash: &RpoDigest, +) -> Result { + let account = value.as_blob_or_null()?; + let account = account.map(Account::read_from_bytes).transpose()?; + + let Some(mut account) = account else { + return Err(DatabaseError::AccountNotOnChain(account_id)); + }; + + account.apply_delta(delta)?; + + let actual_hash = account.hash(); + if &actual_hash != final_state_hash { + return Err(DatabaseError::ApplyBlockFailedAccountHashesMismatch { + calculated: actual_hash, + expected: *final_state_hash, + }); + } + + Ok(account) } diff --git a/store/src/db/tests.rs b/store/src/db/tests.rs index af00ad4aa..db860ae27 100644 --- a/store/src/db/tests.rs +++ b/store/src/db/tests.rs @@ -1,12 +1,20 @@ -use miden_node_proto::domain::accounts::AccountHashUpdate; +use miden_lib::transaction::TransactionKernel; +use miden_node_proto::domain::accounts::{AccountSummary, AccountUpdateDetails}; use miden_objects::{ accounts::{ - AccountId, ACCOUNT_ID_OFF_CHAIN_SENDER, ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN, + Account, AccountCode, AccountDelta, AccountId, AccountStorage, AccountStorageDelta, + AccountVaultDelta, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN, + ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN, ACCOUNT_ID_OFF_CHAIN_SENDER, + ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN, + ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN, }, + assembly::{Assembler, ModuleAst}, + assets::{Asset, AssetVault, FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails}, block::BlockNoteTree, crypto::{hash::rpo::RpoDigest, merkle::MerklePath}, notes::{NoteId, NoteMetadata, NoteType, Nullifier}, - BlockHeader, Felt, FieldElement, ZERO, + transaction::AccountDetails, + BlockHeader, Felt, FieldElement, Word, ONE, ZERO, }; use rusqlite::{vtab::array, Connection}; @@ -160,7 +168,6 @@ fn test_sql_select_accounts() { // test querying empty table let accounts = sql::select_accounts(&mut conn).unwrap(); assert!(accounts.is_empty()); - // test multiple entries let mut state = vec![]; for i in 0..10 { @@ -168,7 +175,7 @@ fn test_sql_select_accounts() { ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN + (i << 32) + 0b1111100000; let account_hash = num_to_rpo_digest(i); state.push(AccountInfo { - update: AccountHashUpdate { + summary: AccountSummary { account_id: account_id.try_into().unwrap(), account_hash, block_num, @@ -177,8 +184,15 @@ fn test_sql_select_accounts() { }); let transaction = conn.transaction().unwrap(); - let res = - sql::upsert_accounts(&transaction, &[(account_id, None, account_hash)], block_num); + let res = sql::upsert_accounts( + &transaction, + &[AccountUpdateDetails { + account_id: account_id.try_into().unwrap(), + final_state_hash: account_hash, + details: None, + }], + block_num, + ); assert_eq!(res.unwrap(), 1, "One element must have been inserted"); transaction.commit().unwrap(); let accounts = sql::select_accounts(&mut conn).unwrap(); @@ -186,6 +200,134 @@ fn test_sql_select_accounts() { } } +#[test] +fn test_sql_public_account_details() { + let mut conn = create_db(); + + let block_num = 1; + create_block(&mut conn, block_num); + + let account_id = + AccountId::try_from(ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN).unwrap(); + let fungible_faucet_id = AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN).unwrap(); + let non_fungible_faucet_id = + AccountId::try_from(ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN).unwrap(); + + let mut storage = AccountStorage::new(vec![]).unwrap(); + storage.set_item(1, num_to_word(1)).unwrap(); + storage.set_item(3, num_to_word(3)).unwrap(); + storage.set_item(5, num_to_word(5)).unwrap(); + + let nft1 = Asset::NonFungible( + NonFungibleAsset::new( + &NonFungibleAssetDetails::new(non_fungible_faucet_id, vec![1, 2, 3]).unwrap(), + ) + .unwrap(), + ); + + let mut account = Account::new( + account_id, + AssetVault::new(&[ + Asset::Fungible(FungibleAsset::new(fungible_faucet_id, 150).unwrap()), + nft1, + ]) + .unwrap(), + storage, + mock_account_code(&TransactionKernel::assembler()), + ZERO, + ); + + // test querying empty table + let accounts_in_db = sql::select_accounts(&mut conn).unwrap(); + assert!(accounts_in_db.is_empty()); + + let transaction = conn.transaction().unwrap(); + let inserted = sql::upsert_accounts( + &transaction, + &[AccountUpdateDetails { + account_id, + final_state_hash: account.hash(), + details: Some(AccountDetails::Full(account.clone())), + }], + block_num, + ) + .unwrap(); + + assert_eq!(inserted, 1, "One element must have been inserted"); + + transaction.commit().unwrap(); + + let mut accounts_in_db = sql::select_accounts(&mut conn).unwrap(); + + assert_eq!(accounts_in_db.len(), 1, "One element must have been inserted"); + + let account_read = accounts_in_db.pop().unwrap().details.unwrap(); + assert_eq!(account_read, account); + + let storage_delta = AccountStorageDelta { + cleared_items: vec![3], + updated_items: vec![(4, num_to_word(5)), (5, num_to_word(6))], + }; + + let nft2 = Asset::NonFungible( + NonFungibleAsset::new( + &NonFungibleAssetDetails::new(non_fungible_faucet_id, vec![4, 5, 6]).unwrap(), + ) + .unwrap(), + ); + + let vault_delta = AccountVaultDelta { + added_assets: vec![nft2], + removed_assets: vec![nft1], + }; + + let delta = AccountDelta::new(storage_delta, vault_delta, Some(ONE)).unwrap(); + + account.apply_delta(&delta).unwrap(); + + let transaction = conn.transaction().unwrap(); + let inserted = sql::upsert_accounts( + &transaction, + &[AccountUpdateDetails { + account_id, + final_state_hash: account.hash(), + details: Some(AccountDetails::Delta(delta.clone())), + }], + block_num, + ) + .unwrap(); + + assert_eq!(inserted, 1, "One element must have been inserted"); + + transaction.commit().unwrap(); + + let mut accounts_in_db = sql::select_accounts(&mut conn).unwrap(); + + assert_eq!(accounts_in_db.len(), 1, "One element must have been inserted"); + + let mut account_read = accounts_in_db.pop().unwrap().details.unwrap(); + + assert_eq!(account_read.id(), account.id()); + assert_eq!(account_read.vault(), account.vault()); + assert_eq!(account_read.nonce(), account.nonce()); + + // Cleared item was not serialized, check it and apply delta only with clear item second time: + assert_eq!(account_read.storage().get_item(3), RpoDigest::default()); + + let storage_delta = AccountStorageDelta { + cleared_items: vec![3], + updated_items: vec![], + }; + account_read + .apply_delta( + &AccountDelta::new(storage_delta, AccountVaultDelta::default(), Some(Felt::new(2))) + .unwrap(), + ) + .unwrap(); + + assert_eq!(account_read.storage(), account.storage()); +} + #[test] fn test_sql_select_nullifiers_by_block_range() { let mut conn = create_db(); @@ -392,8 +534,16 @@ fn test_db_account() { let account_hash = num_to_rpo_digest(0); let transaction = conn.transaction().unwrap(); - let row_count = - sql::upsert_accounts(&transaction, &[(account_id, None, account_hash)], block_num).unwrap(); + let row_count = sql::upsert_accounts( + &transaction, + &[AccountUpdateDetails { + account_id: account_id.try_into().unwrap(), + final_state_hash: account_hash, + details: None, + }], + block_num, + ) + .unwrap(); transaction.commit().unwrap(); assert_eq!(row_count, 1); @@ -402,7 +552,7 @@ fn test_db_account() { let res = sql::select_accounts_by_block_range(&mut conn, 0, u32::MAX, &account_ids).unwrap(); assert_eq!( res, - vec![AccountHashUpdate { + vec![AccountSummary { account_id: account_id.try_into().unwrap(), account_hash, block_num, @@ -541,9 +691,24 @@ fn test_notes() { // UTILITIES // ------------------------------------------------------------------------------------------- fn num_to_rpo_digest(n: u64) -> RpoDigest { - RpoDigest::new([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::new(n)]) + RpoDigest::new(num_to_word(n)) +} + +fn num_to_word(n: u64) -> Word { + [Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::new(n)] } fn num_to_nullifier(n: u64) -> Nullifier { Nullifier::from(num_to_rpo_digest(n)) } + +pub fn mock_account_code(assembler: &Assembler) -> AccountCode { + let account_code = "\ + export.account_procedure_1 + push.1.2 + add + end + "; + let account_module_ast = ModuleAst::parse(account_code).unwrap(); + AccountCode::new(account_module_ast, assembler).unwrap() +} diff --git a/store/src/errors.rs b/store/src/errors.rs index 4aa2d27fd..78e7e455d 100644 --- a/store/src/errors.rs +++ b/store/src/errors.rs @@ -3,6 +3,7 @@ use std::io; use deadpool_sqlite::PoolError; use miden_objects::{ crypto::{ + hash::rpo::RpoDigest, merkle::{MerkleError, MmrError}, utils::DeserializationError, }, @@ -48,10 +49,20 @@ pub enum DatabaseError { InteractError(String), #[error("Deserialization of BLOB data from database failed: {0}")] DeserializationError(DeserializationError), + #[error("Corrupted data: {0}")] + CorruptedData(String), #[error("Block applying was broken because of closed channel on state side: {0}")] ApplyBlockFailedClosedChannel(RecvError), #[error("Account {0} not found in the database")] AccountNotFoundInDb(AccountId), + #[error("Account {0} is not on the chain")] + AccountNotOnChain(AccountId), + #[error("Failed to apply block because of on-chain account final hashes mismatch (expected {expected}, \ + but calculated is {calculated}")] + ApplyBlockFailedAccountHashesMismatch { + expected: RpoDigest, + calculated: RpoDigest, + }, } impl From for DatabaseError { diff --git a/store/src/server/api.rs b/store/src/server/api.rs index 0ba6a7608..1b74daa84 100644 --- a/store/src/server/api.rs +++ b/store/src/server/api.rs @@ -2,10 +2,11 @@ use std::sync::Arc; use miden_node_proto::{ convert, + domain::accounts::AccountUpdateDetails, errors::ConversionError, generated::{ self, - account::AccountHashUpdate, + account::AccountSummary, note::NoteSyncRecord, requests::{ ApplyBlockRequest, CheckNullifiersRequest, GetAccountDetailsRequest, @@ -28,6 +29,8 @@ use miden_node_proto::{ use miden_objects::{ crypto::hash::rpo::RpoDigest, notes::{NoteId, Nullifier}, + transaction::AccountDetails, + utils::Deserializable, BlockHeader, Felt, ZERO, }; use tonic::{Response, Status}; @@ -127,7 +130,7 @@ impl api_server::Api for StoreApi { let accounts = state .account_updates .into_iter() - .map(|account_info| AccountHashUpdate { + .map(|account_info| AccountSummary { account_id: Some(account_info.account_id.into()), account_hash: Some(account_info.account_hash.into()), block_num: account_info.block_num, @@ -262,18 +265,38 @@ impl api_server::Api for StoreApi { let nullifiers = validate_nullifiers(&request.nullifiers)?; let accounts = request .accounts - .into_iter() + .iter() .map(|account_update| { let account_state: AccountState = account_update .try_into() .map_err(|err: ConversionError| Status::invalid_argument(err.to_string()))?; - Ok(( - account_state.account_id.into(), - None, // TODO: Process account details (next PR) - account_state + + match (account_state.account_id.is_on_chain(), account_update.details.is_some()) { + (true, false) => { + return Err(Status::invalid_argument("On-chain account must have details")); + }, + (false, true) => { + return Err(Status::invalid_argument( + "Off-chain account must not have details", + )); + }, + _ => (), + } + + let details = account_update + .details + .as_ref() + .map(|data| AccountDetails::read_from_bytes(data)) + .transpose() + .map_err(|err| Status::invalid_argument(err.to_string()))?; + + Ok(AccountUpdateDetails { + account_id: account_state.account_id, + details, + final_state_hash: account_state .account_hash .ok_or(invalid_argument("Account update missing account hash"))?, - )) + }) }) .collect::, Status>>()?; diff --git a/store/src/state.rs b/store/src/state.rs index 099964971..1eb02e860 100644 --- a/store/src/state.rs +++ b/store/src/state.rs @@ -4,7 +4,10 @@ //! data is atomically written, and that reads are consistent. use std::{mem, sync::Arc}; -use miden_node_proto::{domain::accounts::AccountInfo, AccountInputRecord, NullifierWitness}; +use miden_node_proto::{ + domain::accounts::{AccountInfo, AccountUpdateDetails}, + AccountInputRecord, NullifierWitness, +}; use miden_node_utils::formatting::{format_account_id, format_array}; use miden_objects::{ block::BlockNoteTree, @@ -13,7 +16,6 @@ use miden_objects::{ merkle::{LeafIndex, Mmr, MmrDelta, MmrPeaks, SimpleSmt, SmtProof, ValuePath}, }, notes::{NoteId, NoteMetadata, NoteType, Nullifier}, - transaction::AccountDetails, AccountError, BlockHeader, NoteError, ACCOUNT_TREE_DEPTH, ZERO, }; use tokio::{ @@ -107,7 +109,7 @@ impl State { &self, block_header: BlockHeader, nullifiers: Vec, - accounts: Vec<(AccountId, Option, RpoDigest)>, + accounts: Vec, notes: Vec, ) -> Result<(), ApplyBlockError> { let _ = self.writer.try_lock().map_err(|_| ApplyBlockError::ConcurrentWrite)?; @@ -181,8 +183,11 @@ impl State { // update account tree let mut account_tree = inner.account_tree.clone(); - for (account_id, _details, account_hash) in accounts.iter() { - account_tree.insert(LeafIndex::new_max_depth(*account_id), account_hash.into()); + for update in &accounts { + account_tree.insert( + LeafIndex::new_max_depth(update.account_id.into()), + update.final_state_hash.into(), + ); } if account_tree.root() != block_header.account_root() {