diff --git a/CHANGELOG.md b/CHANGELOG.md index e7ec49256..31202bbc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - CI check for proto file consistency (#412). - Added warning on CI for `CHANGELOG.md` (#413). - Now accounts for genesis are optional. Accounts directory will be overwritten, if `--force` flag is set (#420). +- Added `GetAccountStateDelta` endpoint (#418). ### Fixes diff --git a/Cargo.lock b/Cargo.lock index 34fa61675..f80178bb4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1558,7 +1558,7 @@ dependencies = [ [[package]] name = "miden-lib" version = "0.5.0" -source = "git+https://github.com/0xPolygonMiden/miden-base?rev=5cb5047#5cb5047f67a4164ae875336d9beae75095242ea7" +source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#5cb5047f67a4164ae875336d9beae75095242ea7" dependencies = [ "miden-assembly", "miden-objects", @@ -1707,7 +1707,7 @@ dependencies = [ [[package]] name = "miden-objects" version = "0.5.0" -source = "git+https://github.com/0xPolygonMiden/miden-base?rev=5cb5047#5cb5047f67a4164ae875336d9beae75095242ea7" +source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#5cb5047f67a4164ae875336d9beae75095242ea7" dependencies = [ "miden-assembly", "miden-core", @@ -1758,7 +1758,7 @@ dependencies = [ [[package]] name = "miden-tx" version = "0.5.0" -source = "git+https://github.com/0xPolygonMiden/miden-base?rev=5cb5047#5cb5047f67a4164ae875336d9beae75095242ea7" +source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#5cb5047f67a4164ae875336d9beae75095242ea7" dependencies = [ "miden-lib", "miden-objects", diff --git a/Cargo.toml b/Cargo.toml index fd7c186e9..2c06e2d3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ readme = "README.md" [workspace.dependencies] miden-air = { version = "0.9", default-features = false } -miden-lib = { rev = "5cb5047", git = "https://github.com/0xPolygonMiden/miden-base" } +miden-lib = { git = "https://github.com/0xPolygonMiden/miden-base.git", branch = "next" } miden-node-block-producer = { path = "crates/block-producer", version = "0.5" } miden-node-faucet = { path = "bin/faucet", version = "0.5" } miden-node-proto = { path = "crates/proto", version = "0.5" } @@ -36,10 +36,10 @@ miden-node-rpc-proto = { path = "crates/rpc-proto", version = "0.5" } miden-node-store = { path = "crates/store", version = "0.5" } miden-node-test-macro = { path = "crates/test-macro" } miden-node-utils = { path = "crates/utils", version = "0.5" } -miden-objects = { rev = "5cb5047", git = "https://github.com/0xPolygonMiden/miden-base" } +miden-objects = { git = "https://github.com/0xPolygonMiden/miden-base.git", branch = "next" } miden-processor = { version = "0.9" } miden-stdlib = { version = "0.9", default-features = false } -miden-tx = { rev = "5cb5047", git = "https://github.com/0xPolygonMiden/miden-base" } +miden-tx = { git = "https://github.com/0xPolygonMiden/miden-base.git", branch = "next" } thiserror = { version = "1.0" } tonic = { version = "0.11" } tracing = { version = "0.1" } diff --git a/crates/proto/src/generated/requests.rs b/crates/proto/src/generated/requests.rs index 7ce1e69ac..24abd6ef5 100644 --- a/crates/proto/src/generated/requests.rs +++ b/crates/proto/src/generated/requests.rs @@ -122,3 +122,18 @@ pub struct GetBlockByNumberRequest { #[prost(fixed32, tag = "1")] pub block_num: u32, } +/// Returns delta of the account states in the range from `from_block_num` (exclusive) to +/// `to_block_num` (inclusive). +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetAccountStateDeltaRequest { + /// ID of the account for which the delta is requested. + #[prost(message, optional, tag = "1")] + pub account_id: ::core::option::Option, + /// Block number from which the delta is requested (exclusive). + #[prost(fixed32, tag = "2")] + pub from_block_num: u32, + /// Block number up to which the delta is requested (inclusive). + #[prost(fixed32, tag = "3")] + pub to_block_num: u32, +} diff --git a/crates/proto/src/generated/responses.rs b/crates/proto/src/generated/responses.rs index fce40c797..0bf67c1e2 100644 --- a/crates/proto/src/generated/responses.rs +++ b/crates/proto/src/generated/responses.rs @@ -170,3 +170,10 @@ pub struct GetBlockByNumberResponse { #[prost(bytes = "vec", optional, tag = "1")] pub block: ::core::option::Option<::prost::alloc::vec::Vec>, } +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetAccountStateDeltaResponse { + /// The calculated `AccountStateDelta` encoded using miden native format + #[prost(bytes = "vec", optional, tag = "1")] + pub delta: ::core::option::Option<::prost::alloc::vec::Vec>, +} diff --git a/crates/proto/src/generated/rpc.rs b/crates/proto/src/generated/rpc.rs index 99169aeaa..87d7960ea 100644 --- a/crates/proto/src/generated/rpc.rs +++ b/crates/proto/src/generated/rpc.rs @@ -134,6 +134,33 @@ 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_state_delta( + &mut self, + request: impl tonic::IntoRequest< + super::super::requests::GetAccountStateDeltaRequest, + >, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/rpc.Api/GetAccountStateDelta", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("rpc.Api", "GetAccountStateDelta")); + self.inner.unary(req, path, codec).await + } pub async fn get_block_by_number( &mut self, request: impl tonic::IntoRequest< @@ -279,6 +306,13 @@ pub mod api_server { tonic::Response, tonic::Status, >; + async fn get_account_state_delta( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; async fn get_block_by_number( &self, request: tonic::Request, @@ -496,6 +530,55 @@ pub mod api_server { }; Box::pin(fut) } + "/rpc.Api/GetAccountStateDelta" => { + #[allow(non_camel_case_types)] + struct GetAccountStateDeltaSvc(pub Arc); + impl< + T: Api, + > tonic::server::UnaryService< + super::super::requests::GetAccountStateDeltaRequest, + > for GetAccountStateDeltaSvc { + type Response = super::super::responses::GetAccountStateDeltaResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request< + super::super::requests::GetAccountStateDeltaRequest, + >, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::get_account_state_delta(&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 inner = inner.0; + let method = GetAccountStateDeltaSvc(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/GetBlockByNumber" => { #[allow(non_camel_case_types)] struct GetBlockByNumberSvc(pub Arc); diff --git a/crates/proto/src/generated/store.rs b/crates/proto/src/generated/store.rs index 8a5d6ac15..70ac0ebac 100644 --- a/crates/proto/src/generated/store.rs +++ b/crates/proto/src/generated/store.rs @@ -159,6 +159,33 @@ pub mod api_client { .insert(GrpcMethod::new("store.Api", "GetAccountDetails")); self.inner.unary(req, path, codec).await } + pub async fn get_account_state_delta( + &mut self, + request: impl tonic::IntoRequest< + super::super::requests::GetAccountStateDeltaRequest, + >, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/store.Api/GetAccountStateDelta", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("store.Api", "GetAccountStateDelta")); + self.inner.unary(req, path, codec).await + } pub async fn get_block_by_number( &mut self, request: impl tonic::IntoRequest< @@ -406,6 +433,13 @@ pub mod api_server { tonic::Response, tonic::Status, >; + async fn get_account_state_delta( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; async fn get_block_by_number( &self, request: tonic::Request, @@ -698,6 +732,55 @@ pub mod api_server { }; Box::pin(fut) } + "/store.Api/GetAccountStateDelta" => { + #[allow(non_camel_case_types)] + struct GetAccountStateDeltaSvc(pub Arc); + impl< + T: Api, + > tonic::server::UnaryService< + super::super::requests::GetAccountStateDeltaRequest, + > for GetAccountStateDeltaSvc { + type Response = super::super::responses::GetAccountStateDeltaResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request< + super::super::requests::GetAccountStateDeltaRequest, + >, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::get_account_state_delta(&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 inner = inner.0; + let method = GetAccountStateDeltaSvc(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/GetBlockByNumber" => { #[allow(non_camel_case_types)] struct GetBlockByNumberSvc(pub Arc); diff --git a/crates/rpc-proto/proto/requests.proto b/crates/rpc-proto/proto/requests.proto index 79dc3004f..0277094a4 100644 --- a/crates/rpc-proto/proto/requests.proto +++ b/crates/rpc-proto/proto/requests.proto @@ -100,3 +100,14 @@ message GetBlockByNumberRequest { // The block number of the target block. fixed32 block_num = 1; } + +// Returns delta of the account states in the range from `from_block_num` (exclusive) to +// `to_block_num` (inclusive). +message GetAccountStateDeltaRequest { + // ID of the account for which the delta is requested. + account.AccountId account_id = 1; + // Block number from which the delta is requested (exclusive). + fixed32 from_block_num = 2; + // Block number up to which the delta is requested (inclusive). + fixed32 to_block_num = 3; +} diff --git a/crates/rpc-proto/proto/responses.proto b/crates/rpc-proto/proto/responses.proto index 25a4100cc..3aef8f0a6 100644 --- a/crates/rpc-proto/proto/responses.proto +++ b/crates/rpc-proto/proto/responses.proto @@ -138,3 +138,8 @@ message GetBlockByNumberResponse { // The requested `Block` data encoded using miden native format optional bytes block = 1; } + +message GetAccountStateDeltaResponse { + // The calculated `AccountStateDelta` encoded using miden native format + optional bytes delta = 1; +} diff --git a/crates/rpc-proto/proto/rpc.proto b/crates/rpc-proto/proto/rpc.proto index 50bd14a34..caf36dcdd 100644 --- a/crates/rpc-proto/proto/rpc.proto +++ b/crates/rpc-proto/proto/rpc.proto @@ -8,6 +8,7 @@ import "responses.proto"; service Api { rpc CheckNullifiers(requests.CheckNullifiersRequest) returns (responses.CheckNullifiersResponse) {} rpc GetAccountDetails(requests.GetAccountDetailsRequest) returns (responses.GetAccountDetailsResponse) {} + rpc GetAccountStateDelta(requests.GetAccountStateDeltaRequest) returns (responses.GetAccountStateDeltaResponse) {} rpc GetBlockByNumber(requests.GetBlockByNumberRequest) returns (responses.GetBlockByNumberResponse) {} rpc GetBlockHeaderByNumber(requests.GetBlockHeaderByNumberRequest) returns (responses.GetBlockHeaderByNumberResponse) {} rpc GetNotesById(requests.GetNotesByIdRequest) returns (responses.GetNotesByIdResponse) {} diff --git a/crates/rpc-proto/proto/store.proto b/crates/rpc-proto/proto/store.proto index 4a8b31ee5..9aab4dccd 100644 --- a/crates/rpc-proto/proto/store.proto +++ b/crates/rpc-proto/proto/store.proto @@ -11,6 +11,7 @@ service Api { rpc ApplyBlock(requests.ApplyBlockRequest) returns (responses.ApplyBlockResponse) {} rpc CheckNullifiers(requests.CheckNullifiersRequest) returns (responses.CheckNullifiersResponse) {} rpc GetAccountDetails(requests.GetAccountDetailsRequest) returns (responses.GetAccountDetailsResponse) {} + rpc GetAccountStateDelta(requests.GetAccountStateDeltaRequest) returns (responses.GetAccountStateDeltaResponse) {} rpc GetBlockByNumber(requests.GetBlockByNumberRequest) returns (responses.GetBlockByNumberResponse) {} rpc GetBlockHeaderByNumber(requests.GetBlockHeaderByNumberRequest) returns (responses.GetBlockHeaderByNumberResponse) {} rpc GetBlockInputs(requests.GetBlockInputsRequest) returns (responses.GetBlockInputsResponse) {} diff --git a/crates/rpc/src/server/api.rs b/crates/rpc/src/server/api.rs index 72504c2b0..69a866511 100644 --- a/crates/rpc/src/server/api.rs +++ b/crates/rpc/src/server/api.rs @@ -2,14 +2,14 @@ use miden_node_proto::{ generated::{ block_producer::api_client as block_producer_client, requests::{ - CheckNullifiersRequest, GetAccountDetailsRequest, GetBlockByNumberRequest, - GetBlockHeaderByNumberRequest, GetNotesByIdRequest, SubmitProvenTransactionRequest, - SyncStateRequest, + CheckNullifiersRequest, GetAccountDetailsRequest, GetAccountStateDeltaRequest, + GetBlockByNumberRequest, GetBlockHeaderByNumberRequest, GetNotesByIdRequest, + SubmitProvenTransactionRequest, SyncStateRequest, }, responses::{ - CheckNullifiersResponse, GetAccountDetailsResponse, GetBlockByNumberResponse, - GetBlockHeaderByNumberResponse, GetNotesByIdResponse, SubmitProvenTransactionResponse, - SyncStateResponse, + CheckNullifiersResponse, GetAccountDetailsResponse, GetAccountStateDeltaResponse, + GetBlockByNumberResponse, GetBlockHeaderByNumberResponse, GetNotesByIdResponse, + SubmitProvenTransactionResponse, SyncStateResponse, }, rpc::api_server, store::api_client as store_client, @@ -192,7 +192,7 @@ impl api_server::Api for RpcApi { )] async fn get_block_by_number( &self, - request: tonic::Request, + request: Request, ) -> Result, Status> { let request = request.into_inner(); @@ -200,4 +200,22 @@ impl api_server::Api for RpcApi { self.store.clone().get_block_by_number(request).await } + + #[instrument( + target = "miden-rpc", + name = "rpc:get_account_state_delta", + skip_all, + ret(level = "debug"), + err + )] + async fn get_account_state_delta( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + + debug!(target: COMPONENT, ?request); + + self.store.clone().get_account_state_delta(request).await + } } diff --git a/crates/store/src/db/migrations/001-init.sql b/crates/store/src/db/migrations/001-init.sql index b97ae36b8..6f36ff93b 100644 --- a/crates/store/src/db/migrations/001-init.sql +++ b/crates/store/src/db/migrations/001-init.sql @@ -18,7 +18,7 @@ CREATE TABLE PRIMARY KEY (block_num), CONSTRAINT block_header_block_num_is_u32 CHECK (block_num BETWEEN 0 AND 0xFFFFFFFF) -) STRICT, WITHOUT ROWID; +) STRICT; CREATE TABLE notes @@ -35,12 +35,12 @@ CREATE TABLE details BLOB, PRIMARY KEY (block_num, batch_index, note_index), - CONSTRAINT fk_block_num FOREIGN KEY (block_num) REFERENCES block_headers(block_num), + FOREIGN KEY (block_num) REFERENCES block_headers(block_num), CONSTRAINT notes_type_in_enum CHECK (note_type BETWEEN 1 AND 3), CONSTRAINT notes_block_num_is_u32 CHECK (block_num BETWEEN 0 AND 0xFFFFFFFF), CONSTRAINT notes_batch_index_is_u32 CHECK (batch_index BETWEEN 0 AND 0xFFFFFFFF), CONSTRAINT notes_note_index_is_u32 CHECK (note_index BETWEEN 0 AND 0xFFFFFFFF) -) STRICT, WITHOUT ROWID; +) STRICT; CREATE TABLE accounts @@ -51,9 +51,20 @@ CREATE TABLE details BLOB, PRIMARY KEY (account_id), - CONSTRAINT fk_block_num FOREIGN KEY (block_num) REFERENCES block_headers(block_num), + FOREIGN KEY (block_num) REFERENCES block_headers(block_num), CONSTRAINT accounts_block_num_is_u32 CHECK (block_num BETWEEN 0 AND 0xFFFFFFFF) -) STRICT, WITHOUT ROWID; +) STRICT; + +CREATE TABLE + account_deltas +( + account_id INTEGER NOT NULL, + block_num INTEGER NOT NULL, + delta BLOB NOT NULL, + + PRIMARY KEY (account_id, block_num), + FOREIGN KEY (block_num) REFERENCES block_headers(block_num) +) STRICT; CREATE TABLE nullifiers @@ -63,7 +74,7 @@ CREATE TABLE block_num INTEGER NOT NULL, PRIMARY KEY (nullifier), - CONSTRAINT fk_block_num FOREIGN KEY (block_num) REFERENCES block_headers(block_num), + FOREIGN KEY (block_num) REFERENCES block_headers(block_num), CONSTRAINT nullifiers_nullifier_is_digest CHECK (length(nullifier) = 32), CONSTRAINT nullifiers_nullifier_prefix_is_u16 CHECK (nullifier_prefix BETWEEN 0 AND 0xFFFF), CONSTRAINT nullifiers_block_num_is_u32 CHECK (block_num BETWEEN 0 AND 0xFFFFFFFF) @@ -77,7 +88,7 @@ CREATE TABLE block_num INTEGER NOT NULL, PRIMARY KEY (transaction_id), - CONSTRAINT fk_block_num FOREIGN KEY (block_num) REFERENCES block_headers(block_num), + FOREIGN KEY (block_num) REFERENCES block_headers(block_num), CONSTRAINT transactions_block_num_is_u32 CHECK (block_num BETWEEN 0 AND 0xFFFFFFFF) ) STRICT, WITHOUT ROWID; diff --git a/crates/store/src/db/mod.rs b/crates/store/src/db/mod.rs index 49cad92b6..3ab061aac 100644 --- a/crates/store/src/db/mod.rs +++ b/crates/store/src/db/mod.rs @@ -10,6 +10,7 @@ use miden_node_proto::{ generated::note::Note as NotePb, }; use miden_objects::{ + accounts::AccountDelta, block::{Block, BlockNoteIndex}, crypto::{hash::rpo::RpoDigest, merkle::MerklePath, utils::Deserializable}, notes::{NoteId, NoteMetadata, Nullifier}, @@ -334,6 +335,25 @@ impl Db { Ok(()) } + /// Loads account deltas from the DB for given account ID and block range. + /// Note, that `from_block` is exclusive and `to_block` is inclusive. + pub(crate) async fn select_account_state_deltas( + &self, + account_id: AccountId, + from_block: BlockNumber, + to_block: BlockNumber, + ) -> Result> { + self.pool + .get() + .await + .map_err(DatabaseError::MissingDbConnection)? + .interact(move |conn| -> Result> { + sql::select_account_deltas(conn, account_id, from_block, to_block) + }) + .await + .map_err(|err| DatabaseError::InteractError(err.to_string()))? + } + // HELPERS // --------------------------------------------------------------------------------------------- diff --git a/crates/store/src/db/sql.rs b/crates/store/src/db/sql.rs index 018c2bb31..720f52737 100644 --- a/crates/store/src/db/sql.rs +++ b/crates/store/src/db/sql.rs @@ -144,6 +144,43 @@ pub fn select_account(conn: &mut Connection, account_id: AccountId) -> Result Result> { + let mut stmt = conn.prepare( + " + SELECT + delta + FROM + account_deltas + WHERE + account_id = ?1 AND block_num > ?2 AND block_num <= ?3 + ORDER BY + block_num ASC + ", + )?; + + let mut rows = stmt.query(params![u64_to_value(account_id), block_start, block_end])?; + let mut result = Vec::new(); + while let Some(row) = rows.next()? { + let delta = AccountDelta::read_from_bytes(row.get_ref(0)?.as_blob()?)?; + result.push(delta); + } + Ok(result) +} + /// Inserts or updates accounts to the DB using the given [Transaction]. /// /// # Returns @@ -162,6 +199,9 @@ pub fn upsert_accounts( 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 insert_delta_stmt = transaction.prepare( + "INSERT INTO account_deltas (account_id, block_num, delta) VALUES (?1, ?2, ?3);", + )?; let mut select_details_stmt = transaction.prepare("SELECT details FROM accounts WHERE account_id = ?1;")?; @@ -188,6 +228,12 @@ pub fn upsert_accounts( return Err(DatabaseError::AccountNotFoundInDb(account_id)); }; + insert_delta_stmt.execute(params![ + u64_to_value(account_id), + block_num, + delta.to_bytes() + ])?; + let account = apply_delta(account_id, &row.get_ref(0)?, delta, &update.new_state_hash())?; @@ -662,7 +708,7 @@ pub fn insert_transactions( accounts: &[BlockAccountUpdate], ) -> Result { let mut stmt = transaction.prepare( - "INSERT INTO transactions(transaction_id, account_id, block_num) VALUES (?1, ?2, ?3);", + "INSERT INTO transactions (transaction_id, account_id, block_num) VALUES (?1, ?2, ?3);", )?; let mut count = 0; for update in accounts { diff --git a/crates/store/src/db/tests.rs b/crates/store/src/db/tests.rs index 56e69710e..50f0f1f05 100644 --- a/crates/store/src/db/tests.rs +++ b/crates/store/src/db/tests.rs @@ -365,6 +365,58 @@ fn test_sql_public_account_details() { .unwrap(); assert_eq!(account_read.storage(), account.storage()); + + let storage_delta2 = AccountStorageDelta { + cleared_items: vec![5], + updated_items: vec![], + updated_maps: vec![], + }; + + let delta2 = AccountDelta::new( + storage_delta2, + AccountVaultDelta { + added_assets: vec![nft1], + removed_assets: vec![], + }, + Some(Felt::new(3)), + ) + .unwrap(); + + account.apply_delta(&delta2).unwrap(); + + create_block(&mut conn, block_num + 1); + + let transaction = conn.transaction().unwrap(); + let inserted = sql::upsert_accounts( + &transaction, + &[BlockAccountUpdate::new( + account_id, + account.hash(), + AccountUpdateDetails::Delta(delta2.clone()), + vec![], + )], + block_num + 1, + ) + .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.id(), account.id()); + assert_eq!(account_read.vault(), account.vault()); + assert_eq!(account_read.nonce(), account.nonce()); + + let read_deltas = + sql::select_account_deltas(&mut conn, account_id.into(), 0, block_num + 1).unwrap(); + + assert_eq!(read_deltas, vec![delta, delta2]); } #[test] diff --git a/crates/store/src/errors.rs b/crates/store/src/errors.rs index e6ea10cfb..c1bfd2f01 100644 --- a/crates/store/src/errors.rs +++ b/crates/store/src/errors.rs @@ -9,7 +9,7 @@ use miden_objects::{ }, notes::Nullifier, transaction::OutputNote, - AccountError, BlockError, BlockHeader, NoteError, + AccountDeltaError, AccountError, BlockError, BlockHeader, NoteError, }; use rusqlite::types::FromSqlError; use thiserror::Error; @@ -52,6 +52,8 @@ pub enum DatabaseError { NoteError(#[from] NoteError), #[error("Migration error: {0}")] MigrationError(#[from] rusqlite_migration::Error), + #[error("Account delta error: {0}")] + AccountDeltaError(#[from] AccountDeltaError), #[error("SQLite pool interaction task failed: {0}")] InteractError(String), #[error("Deserialization of BLOB data from database failed: {0}")] diff --git a/crates/store/src/server/api.rs b/crates/store/src/server/api.rs index c94f10c7d..07083c729 100644 --- a/crates/store/src/server/api.rs +++ b/crates/store/src/server/api.rs @@ -9,16 +9,17 @@ use miden_node_proto::{ note::NoteSyncRecord, requests::{ ApplyBlockRequest, CheckNullifiersRequest, GetAccountDetailsRequest, - GetBlockByNumberRequest, GetBlockHeaderByNumberRequest, GetBlockInputsRequest, - GetNotesByIdRequest, GetTransactionInputsRequest, ListAccountsRequest, - ListNotesRequest, ListNullifiersRequest, SyncStateRequest, + GetAccountStateDeltaRequest, GetBlockByNumberRequest, GetBlockHeaderByNumberRequest, + GetBlockInputsRequest, GetNotesByIdRequest, GetTransactionInputsRequest, + ListAccountsRequest, ListNotesRequest, ListNullifiersRequest, SyncStateRequest, }, responses::{ AccountTransactionInputRecord, ApplyBlockResponse, CheckNullifiersResponse, - GetAccountDetailsResponse, GetBlockByNumberResponse, GetBlockHeaderByNumberResponse, - GetBlockInputsResponse, GetNotesByIdResponse, GetTransactionInputsResponse, - ListAccountsResponse, ListNotesResponse, ListNullifiersResponse, - NullifierTransactionInputRecord, NullifierUpdate, SyncStateResponse, + GetAccountDetailsResponse, GetAccountStateDeltaResponse, GetBlockByNumberResponse, + GetBlockHeaderByNumberResponse, GetBlockInputsResponse, GetNotesByIdResponse, + GetTransactionInputsResponse, ListAccountsResponse, ListNotesResponse, + ListNullifiersResponse, NullifierTransactionInputRecord, NullifierUpdate, + SyncStateResponse, }, smt::SmtLeafEntry, store::api_server, @@ -30,7 +31,7 @@ use miden_objects::{ block::Block, crypto::hash::rpo::RpoDigest, notes::{NoteId, Nullifier}, - utils::Deserializable, + utils::{Deserializable, Serializable}, Felt, ZERO, }; use tonic::{Response, Status}; @@ -376,6 +377,34 @@ impl api_server::Api for StoreApi { Ok(Response::new(GetBlockByNumberResponse { block })) } + #[instrument( + target = "miden-store", + name = "store:get_account_state_delta", + skip_all, + ret(level = "debug"), + err + )] + async fn get_account_state_delta( + &self, + request: tonic::Request, + ) -> Result, Status> { + let request = request.into_inner(); + + debug!(target: COMPONENT, ?request); + + let delta = self + .state + .get_account_state_delta( + request.account_id.ok_or(invalid_argument("account_id is missing"))?.id, + request.from_block_num, + request.to_block_num, + ) + .await + .map_err(internal_error)?; + + Ok(Response::new(GetAccountStateDeltaResponse { delta: Some(delta.to_bytes()) })) + } + // TESTING ENDPOINTS // -------------------------------------------------------------------------------------------- diff --git a/crates/store/src/state.rs b/crates/store/src/state.rs index 78ac5748e..8cb8fe818 100644 --- a/crates/store/src/state.rs +++ b/crates/store/src/state.rs @@ -10,6 +10,7 @@ use miden_node_proto::{ }; use miden_node_utils::formatting::{format_account_id, format_array}; use miden_objects::{ + accounts::AccountDelta, block::Block, crypto::{ hash::rpo::RpoDigest, @@ -566,6 +567,21 @@ impl State { self.db.select_account(id).await } + /// Returns the state delta between `from_block` (exclusive) and `to_block` (inclusive) for the given account. + pub(crate) async fn get_account_state_delta( + &self, + account_id: AccountId, + from_block: BlockNumber, + to_block: BlockNumber, + ) -> Result { + let deltas = self.db.select_account_state_deltas(account_id, from_block, to_block).await?; + + deltas + .into_iter() + .try_fold(AccountDelta::default(), |accumulator, delta| accumulator.merge(delta)) + .map_err(Into::into) + } + /// Loads a block from the block store. Return `Ok(None)` if the block is not found. pub async fn load_block( &self, diff --git a/proto/requests.proto b/proto/requests.proto index 79dc3004f..0277094a4 100644 --- a/proto/requests.proto +++ b/proto/requests.proto @@ -100,3 +100,14 @@ message GetBlockByNumberRequest { // The block number of the target block. fixed32 block_num = 1; } + +// Returns delta of the account states in the range from `from_block_num` (exclusive) to +// `to_block_num` (inclusive). +message GetAccountStateDeltaRequest { + // ID of the account for which the delta is requested. + account.AccountId account_id = 1; + // Block number from which the delta is requested (exclusive). + fixed32 from_block_num = 2; + // Block number up to which the delta is requested (inclusive). + fixed32 to_block_num = 3; +} diff --git a/proto/responses.proto b/proto/responses.proto index 25a4100cc..3aef8f0a6 100644 --- a/proto/responses.proto +++ b/proto/responses.proto @@ -138,3 +138,8 @@ message GetBlockByNumberResponse { // The requested `Block` data encoded using miden native format optional bytes block = 1; } + +message GetAccountStateDeltaResponse { + // The calculated `AccountStateDelta` encoded using miden native format + optional bytes delta = 1; +} diff --git a/proto/rpc.proto b/proto/rpc.proto index 50bd14a34..caf36dcdd 100644 --- a/proto/rpc.proto +++ b/proto/rpc.proto @@ -8,6 +8,7 @@ import "responses.proto"; service Api { rpc CheckNullifiers(requests.CheckNullifiersRequest) returns (responses.CheckNullifiersResponse) {} rpc GetAccountDetails(requests.GetAccountDetailsRequest) returns (responses.GetAccountDetailsResponse) {} + rpc GetAccountStateDelta(requests.GetAccountStateDeltaRequest) returns (responses.GetAccountStateDeltaResponse) {} rpc GetBlockByNumber(requests.GetBlockByNumberRequest) returns (responses.GetBlockByNumberResponse) {} rpc GetBlockHeaderByNumber(requests.GetBlockHeaderByNumberRequest) returns (responses.GetBlockHeaderByNumberResponse) {} rpc GetNotesById(requests.GetNotesByIdRequest) returns (responses.GetNotesByIdResponse) {} diff --git a/proto/store.proto b/proto/store.proto index 4a8b31ee5..9aab4dccd 100644 --- a/proto/store.proto +++ b/proto/store.proto @@ -11,6 +11,7 @@ service Api { rpc ApplyBlock(requests.ApplyBlockRequest) returns (responses.ApplyBlockResponse) {} rpc CheckNullifiers(requests.CheckNullifiersRequest) returns (responses.CheckNullifiersResponse) {} rpc GetAccountDetails(requests.GetAccountDetailsRequest) returns (responses.GetAccountDetailsResponse) {} + rpc GetAccountStateDelta(requests.GetAccountStateDeltaRequest) returns (responses.GetAccountStateDeltaResponse) {} rpc GetBlockByNumber(requests.GetBlockByNumberRequest) returns (responses.GetBlockByNumberResponse) {} rpc GetBlockHeaderByNumber(requests.GetBlockHeaderByNumberRequest) returns (responses.GetBlockHeaderByNumberResponse) {} rpc GetBlockInputs(requests.GetBlockInputsRequest) returns (responses.GetBlockInputsResponse) {}