From 481a7b63ab878aec7e911cf86ea20dbce6e420bf Mon Sep 17 00:00:00 2001 From: Mirko von Leipzig <48352201+Mirko-von-Leipzig@users.noreply.github.com> Date: Mon, 5 Aug 2024 20:25:05 +0200 Subject: [PATCH] feat: add `SyncNotes` endpoint (#424) --- CHANGELOG.md | 1 + .../block-producer/src/batch_builder/batch.rs | 1 + crates/proto/src/generated/requests.rs | 29 ++-- crates/proto/src/generated/responses.rs | 21 +++ crates/proto/src/generated/rpc.rs | 78 +++++++++++ crates/proto/src/generated/store.rs | 78 +++++++++++ crates/rpc-proto/proto/requests.proto | 27 ++-- crates/rpc-proto/proto/responses.proto | 19 +++ crates/rpc-proto/proto/rpc.proto | 1 + crates/rpc-proto/proto/store.proto | 1 + crates/rpc/src/server/api.rs | 21 ++- crates/store/src/db/mod.rs | 44 ++++-- crates/store/src/db/sql.rs | 128 +++++++++++++++++- crates/store/src/errors.rs | 10 ++ crates/store/src/server/api.rs | 45 +++++- crates/store/src/state.rs | 47 +++++-- proto/requests.proto | 27 ++-- proto/responses.proto | 19 +++ proto/rpc.proto | 1 + proto/store.proto | 1 + 20 files changed, 534 insertions(+), 65 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa9233d60..ee278e032 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - Added `GetAccountStateDelta` endpoint (#418). - Added `CheckNullifiersByPrefix` endpoint (#419). - Support multiple inflight transactions on the same account (#407). +- Added `SyncNotes` endpoint (#424). - Cache sql statements (#427). ### Fixes diff --git a/crates/block-producer/src/batch_builder/batch.rs b/crates/block-producer/src/batch_builder/batch.rs index 916b63bd8..cda4992e0 100644 --- a/crates/block-producer/src/batch_builder/batch.rs +++ b/crates/block-producer/src/batch_builder/batch.rs @@ -180,6 +180,7 @@ impl TransactionBatch { /// Returns an iterator over (account_id, init_state_hash) tuples for accounts that were /// modified in this transaction batch. + #[cfg(test)] pub fn account_initial_states(&self) -> impl Iterator + '_ { self.updated_accounts .iter() diff --git a/crates/proto/src/generated/requests.rs b/crates/proto/src/generated/requests.rs index 918186430..a75da6f50 100644 --- a/crates/proto/src/generated/requests.rs +++ b/crates/proto/src/generated/requests.rs @@ -59,20 +59,29 @@ pub struct SyncStateRequest { /// it won't be included in the response. #[prost(message, repeated, tag = "2")] pub account_ids: ::prost::alloc::vec::Vec, - /// Determines the tags which the client is interested in. These are only the 16high bits of the - /// note's complete tag. - /// - /// The above means it is not possible to request an specific note, but only a "note family", - /// this is done to increase the privacy of the client, by hiding the note's the client is - /// intereted on. - #[prost(uint32, repeated, tag = "3")] + /// Specifies the tags which the client is interested in. + #[prost(fixed32, repeated, tag = "3")] pub note_tags: ::prost::alloc::vec::Vec, - /// Determines the nullifiers the client is interested in. - /// - /// Similarly to the note_tags, this determins only the 16high bits of the target nullifier. + /// Determines the nullifiers the client is interested in by specifying the 16high bits of the + /// target nullifier. #[prost(uint32, repeated, tag = "4")] pub nullifiers: ::prost::alloc::vec::Vec, } +/// Note synchronization request. +/// +/// Specifies note tags that client is intersted in. The server will return the first block which +/// contains a note matching `note_tags` or the chain tip. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SyncNoteRequest { + /// Last block known by the client. The response will contain data starting from the next block, + /// until the first block which contains a note of matching the requested tag. + #[prost(fixed32, tag = "1")] + pub block_num: u32, + /// Specifies the tags which the client is interested in. + #[prost(fixed32, repeated, tag = "2")] + pub note_tags: ::prost::alloc::vec::Vec, +} #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetBlockInputsRequest { diff --git a/crates/proto/src/generated/responses.rs b/crates/proto/src/generated/responses.rs index 74f7fabb1..e5cc8d2f9 100644 --- a/crates/proto/src/generated/responses.rs +++ b/crates/proto/src/generated/responses.rs @@ -63,6 +63,27 @@ pub struct SyncStateResponse { #[prost(message, repeated, tag = "8")] pub nullifiers: ::prost::alloc::vec::Vec, } +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SyncNoteResponse { + /// Number of the latest block in the chain + #[prost(fixed32, tag = "1")] + pub chain_tip: u32, + /// Block header of the block with the first note matching the specified criteria + #[prost(message, optional, tag = "2")] + pub block_header: ::core::option::Option, + /// Proof for block header's MMR with respect to the chain tip. + /// + /// More specifically, the full proof consists of `forest`, `position` and `path` components. This + /// value constitutes the `path`. The other two components can be obtained as follows: + /// - `position` is simply `resopnse.block_header.block_num` + /// - `forest` is the same as `response.chain_tip + 1` + #[prost(message, optional, tag = "3")] + pub mmr_path: ::core::option::Option, + /// List of all notes together with the Merkle paths from `response.block_header.note_root` + #[prost(message, repeated, tag = "4")] + pub notes: ::prost::alloc::vec::Vec, +} /// An account returned as a response to the GetBlockInputs #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] diff --git a/crates/proto/src/generated/rpc.rs b/crates/proto/src/generated/rpc.rs index fe726b431..be76165b9 100644 --- a/crates/proto/src/generated/rpc.rs +++ b/crates/proto/src/generated/rpc.rs @@ -288,6 +288,28 @@ pub mod api_client { .insert(GrpcMethod::new("rpc.Api", "SubmitProvenTransaction")); self.inner.unary(req, path, codec).await } + pub async fn sync_notes( + &mut self, + request: impl tonic::IntoRequest, + ) -> 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/SyncNotes"); + let mut req = request.into_request(); + req.extensions_mut().insert(GrpcMethod::new("rpc.Api", "SyncNotes")); + self.inner.unary(req, path, codec).await + } pub async fn sync_state( &mut self, request: impl tonic::IntoRequest, @@ -381,6 +403,13 @@ pub mod api_server { tonic::Response, tonic::Status, >; + async fn sync_notes( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; async fn sync_state( &self, request: tonic::Request, @@ -862,6 +891,55 @@ pub mod api_server { }; Box::pin(fut) } + "/rpc.Api/SyncNotes" => { + #[allow(non_camel_case_types)] + struct SyncNotesSvc(pub Arc); + impl< + T: Api, + > tonic::server::UnaryService< + super::super::requests::SyncNoteRequest, + > for SyncNotesSvc { + type Response = super::super::responses::SyncNoteResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request< + super::super::requests::SyncNoteRequest, + >, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::sync_notes(&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 = SyncNotesSvc(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/SyncState" => { #[allow(non_camel_case_types)] struct SyncStateSvc(pub Arc); diff --git a/crates/proto/src/generated/store.rs b/crates/proto/src/generated/store.rs index 5fab84afa..35b9eb598 100644 --- a/crates/proto/src/generated/store.rs +++ b/crates/proto/src/generated/store.rs @@ -408,6 +408,28 @@ pub mod api_client { req.extensions_mut().insert(GrpcMethod::new("store.Api", "ListNullifiers")); self.inner.unary(req, path, codec).await } + pub async fn sync_notes( + &mut self, + request: impl tonic::IntoRequest, + ) -> 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/SyncNotes"); + let mut req = request.into_request(); + req.extensions_mut().insert(GrpcMethod::new("store.Api", "SyncNotes")); + self.inner.unary(req, path, codec).await + } pub async fn sync_state( &mut self, request: impl tonic::IntoRequest, @@ -534,6 +556,13 @@ pub mod api_server { tonic::Response, tonic::Status, >; + async fn sync_notes( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; async fn sync_state( &self, request: tonic::Request, @@ -1260,6 +1289,55 @@ pub mod api_server { }; Box::pin(fut) } + "/store.Api/SyncNotes" => { + #[allow(non_camel_case_types)] + struct SyncNotesSvc(pub Arc); + impl< + T: Api, + > tonic::server::UnaryService< + super::super::requests::SyncNoteRequest, + > for SyncNotesSvc { + type Response = super::super::responses::SyncNoteResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request< + super::super::requests::SyncNoteRequest, + >, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::sync_notes(&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 = SyncNotesSvc(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/SyncState" => { #[allow(non_camel_case_types)] struct SyncStateSvc(pub Arc); diff --git a/crates/rpc-proto/proto/requests.proto b/crates/rpc-proto/proto/requests.proto index 43a5e3be1..02222f994 100644 --- a/crates/rpc-proto/proto/requests.proto +++ b/crates/rpc-proto/proto/requests.proto @@ -54,20 +54,27 @@ message SyncStateRequest { // it won't be included in the response. repeated account.AccountId account_ids = 2; - // Determines the tags which the client is interested in. These are only the 16high bits of the - // note's complete tag. - // - // The above means it is not possible to request an specific note, but only a "note family", - // this is done to increase the privacy of the client, by hiding the note's the client is - // intereted on. - repeated uint32 note_tags = 3; + // Specifies the tags which the client is interested in. + repeated fixed32 note_tags = 3; - // Determines the nullifiers the client is interested in. - // - // Similarly to the note_tags, this determins only the 16high bits of the target nullifier. + // Determines the nullifiers the client is interested in by specifying the 16high bits of the + // target nullifier. repeated uint32 nullifiers = 4; } +// Note synchronization request. +// +// Specifies note tags that client is intersted in. The server will return the first block which +// contains a note matching `note_tags` or the chain tip. +message SyncNoteRequest { + // Last block known by the client. The response will contain data starting from the next block, + // until the first block which contains a note of matching the requested tag. + fixed32 block_num = 1; + + // Specifies the tags which the client is interested in. + repeated fixed32 note_tags = 2; +} + message GetBlockInputsRequest { // ID of the account against which a transaction is executed. repeated account.AccountId account_ids = 1; diff --git a/crates/rpc-proto/proto/responses.proto b/crates/rpc-proto/proto/responses.proto index 1bf7c8aa3..4a0c9a044 100644 --- a/crates/rpc-proto/proto/responses.proto +++ b/crates/rpc-proto/proto/responses.proto @@ -62,6 +62,25 @@ message SyncStateResponse { repeated NullifierUpdate nullifiers = 8; } +message SyncNoteResponse { + // Number of the latest block in the chain + fixed32 chain_tip = 1; + + // Block header of the block with the first note matching the specified criteria + block_header.BlockHeader block_header = 2; + + // Proof for block header's MMR with respect to the chain tip. + // + // More specifically, the full proof consists of `forest`, `position` and `path` components. This + // value constitutes the `path`. The other two components can be obtained as follows: + // - `position` is simply `resopnse.block_header.block_num` + // - `forest` is the same as `response.chain_tip + 1` + merkle.MerklePath mmr_path = 3; + + // List of all notes together with the Merkle paths from `response.block_header.note_root` + repeated note.NoteSyncRecord notes = 4; +} + // An account returned as a response to the GetBlockInputs message AccountBlockInputRecord { account.AccountId account_id = 1; diff --git a/crates/rpc-proto/proto/rpc.proto b/crates/rpc-proto/proto/rpc.proto index 8e75bca77..da2c2bdd0 100644 --- a/crates/rpc-proto/proto/rpc.proto +++ b/crates/rpc-proto/proto/rpc.proto @@ -14,5 +14,6 @@ service Api { rpc GetBlockHeaderByNumber(requests.GetBlockHeaderByNumberRequest) returns (responses.GetBlockHeaderByNumberResponse) {} rpc GetNotesById(requests.GetNotesByIdRequest) returns (responses.GetNotesByIdResponse) {} rpc SubmitProvenTransaction(requests.SubmitProvenTransactionRequest) returns (responses.SubmitProvenTransactionResponse) {} + rpc SyncNotes(requests.SyncNoteRequest) returns (responses.SyncNoteResponse) {} rpc SyncState(requests.SyncStateRequest) returns (responses.SyncStateResponse) {} } diff --git a/crates/rpc-proto/proto/store.proto b/crates/rpc-proto/proto/store.proto index ced0a3f4d..1563b1261 100644 --- a/crates/rpc-proto/proto/store.proto +++ b/crates/rpc-proto/proto/store.proto @@ -21,5 +21,6 @@ service Api { rpc ListAccounts(requests.ListAccountsRequest) returns (responses.ListAccountsResponse) {} rpc ListNotes(requests.ListNotesRequest) returns (responses.ListNotesResponse) {} rpc ListNullifiers(requests.ListNullifiersRequest) returns (responses.ListNullifiersResponse) {} + rpc SyncNotes(requests.SyncNoteRequest) returns (responses.SyncNoteResponse) {} rpc SyncState(requests.SyncStateRequest) returns (responses.SyncStateResponse) {} } diff --git a/crates/rpc/src/server/api.rs b/crates/rpc/src/server/api.rs index 0ffd9c4a7..fc4836d08 100644 --- a/crates/rpc/src/server/api.rs +++ b/crates/rpc/src/server/api.rs @@ -4,12 +4,13 @@ use miden_node_proto::{ requests::{ CheckNullifiersByPrefixRequest, CheckNullifiersRequest, GetAccountDetailsRequest, GetAccountStateDeltaRequest, GetBlockByNumberRequest, GetBlockHeaderByNumberRequest, - GetNotesByIdRequest, SubmitProvenTransactionRequest, SyncStateRequest, + GetNotesByIdRequest, SubmitProvenTransactionRequest, SyncNoteRequest, SyncStateRequest, }, responses::{ CheckNullifiersByPrefixResponse, CheckNullifiersResponse, GetAccountDetailsResponse, GetAccountStateDeltaResponse, GetBlockByNumberResponse, GetBlockHeaderByNumberResponse, - GetNotesByIdResponse, SubmitProvenTransactionResponse, SyncStateResponse, + GetNotesByIdResponse, SubmitProvenTransactionResponse, SyncNoteResponse, + SyncStateResponse, }, rpc::api_server, store::api_client as store_client, @@ -130,6 +131,22 @@ impl api_server::Api for RpcApi { self.store.clone().sync_state(request).await } + #[instrument( + target = "miden-rpc", + name = "rpc:sync_notes", + skip_all, + ret(level = "debug"), + err + )] + async fn sync_notes( + &self, + request: Request, + ) -> Result, Status> { + debug!(target: COMPONENT, request = ?request.get_ref()); + + self.store.clone().sync_notes(request).await + } + #[instrument( target = "miden-rpc", name = "rpc:get_notes_by_id", diff --git a/crates/store/src/db/mod.rs b/crates/store/src/db/mod.rs index 01e382c9b..f9febcd6c 100644 --- a/crates/store/src/db/mod.rs +++ b/crates/store/src/db/mod.rs @@ -26,7 +26,7 @@ use crate::{ blocks::BlockStore, config::StoreConfig, db::migrations::apply_migrations, - errors::{DatabaseError, DatabaseSetupError, GenesisError, StateSyncError}, + errors::{DatabaseError, DatabaseSetupError, GenesisError, NoteSyncError, StateSyncError}, genesis::GenesisState, types::{AccountId, BlockNumber}, COMPONENT, SQL_STATEMENT_CACHE_CAPACITY, @@ -91,6 +91,13 @@ pub struct StateSyncUpdate { pub nullifiers: Vec, } +#[derive(Debug, PartialEq)] +pub struct NoteSyncUpdate { + pub notes: Vec, + pub block_header: BlockHeader, + pub chain_tip: BlockNumber, +} + impl Db { /// Open a connection to the DB, apply any pending migrations, and ensure that the genesis block /// is as expected and present in the database. @@ -267,26 +274,16 @@ impl Db { pub async fn get_state_sync( &self, block_num: BlockNumber, - account_ids: &[AccountId], - note_tag_prefixes: &[u32], - nullifier_prefixes: &[u32], + account_ids: Vec, + note_tags: Vec, + nullifier_prefixes: Vec, ) -> Result { - let account_ids = account_ids.to_vec(); - let note_tag_prefixes = note_tag_prefixes.to_vec(); - let nullifier_prefixes = nullifier_prefixes.to_vec(); - self.pool .get() .await .map_err(DatabaseError::MissingDbConnection)? .interact(move |conn| { - sql::get_state_sync( - conn, - block_num, - &account_ids, - ¬e_tag_prefixes, - &nullifier_prefixes, - ) + sql::get_state_sync(conn, block_num, &account_ids, ¬e_tags, &nullifier_prefixes) }) .await .map_err(|err| { @@ -294,6 +291,23 @@ impl Db { })? } + #[instrument(target = "miden-store", skip_all, ret(level = "debug"), err)] + pub async fn get_note_sync( + &self, + block_num: BlockNumber, + note_tags: Vec, + ) -> Result { + self.pool + .get() + .await + .map_err(DatabaseError::MissingDbConnection)? + .interact(move |conn| sql::get_note_sync(conn, block_num, ¬e_tags)) + .await + .map_err(|err| { + DatabaseError::InteractError(format!("Get notes sync task failed: {err}")) + })? + } + /// Loads all the Note's matching a certain NoteId from the database. #[instrument(target = "miden-store", skip_all, ret(level = "debug"), err)] pub async fn select_notes_by_id(&self, note_ids: Vec) -> Result> { diff --git a/crates/store/src/db/sql.rs b/crates/store/src/db/sql.rs index 39dcc6aa0..4bfc6730d 100644 --- a/crates/store/src/db/sql.rs +++ b/crates/store/src/db/sql.rs @@ -18,9 +18,11 @@ use rusqlite::{ Connection, OptionalExtension, Transaction, }; -use super::{NoteRecord, NullifierInfo, Result, StateSyncUpdate, TransactionSummary}; +use super::{ + NoteRecord, NoteSyncUpdate, NullifierInfo, Result, StateSyncUpdate, TransactionSummary, +}; use crate::{ - errors::{DatabaseError, StateSyncError}, + errors::{DatabaseError, NoteSyncError, StateSyncError}, types::{AccountId, BlockNumber}, }; @@ -519,9 +521,9 @@ pub fn insert_notes(transaction: &Transaction, notes: &[NoteRecord]) -> Result Result> { + let tags: Vec = tags.iter().copied().map(u32_to_value).collect(); + + let mut stmt = conn.prepare( + " + SELECT + block_num, + batch_index, + note_index, + note_id, + note_type, + sender, + tag, + aux, + merkle_path, + details + FROM + notes + WHERE + -- find the next block which contains at least one note with a matching tag + block_num = ( + SELECT + block_num + FROM + notes + WHERE + tag IN rarray(?1) AND + block_num > ?2 + ORDER BY + block_num ASC + LIMIT + 1 + ) AND + -- filter the block's notes and return only the ones matching the requested tags + tag IN rarray(?1); + ", + )?; + let mut rows = stmt.query(params![Rc::new(tags), block_num])?; + + let mut res = Vec::new(); + while let Some(row) = rows.next()? { + let block_num = row.get(0)?; + let note_index = BlockNoteIndex::new(row.get(1)?, row.get(2)?); + let note_id_data = row.get_ref(3)?.as_blob()?; + let note_id = RpoDigest::read_from_bytes(note_id_data)?; + let note_type = row.get::<_, u8>(4)?; + let sender = column_value_as_u64(row, 5)?; + let tag: u32 = row.get(6)?; + let aux: u64 = row.get(7)?; + let aux = aux.try_into().map_err(DatabaseError::InvalidFelt)?; + let merkle_path_data = row.get_ref(8)?.as_blob()?; + let merkle_path = MerklePath::read_from_bytes(merkle_path_data)?; + let details_data = row.get_ref(9)?.as_blob_or_null()?; + let details = details_data.map(>::read_from_bytes).transpose()?; + + let metadata = + NoteMetadata::new(sender.try_into()?, NoteType::try_from(note_type)?, tag.into(), aux)?; + + let note = NoteRecord { + block_num, + note_index, + note_id, + details, + metadata, + merkle_path, + }; + res.push(note); + } + Ok(res) +} + /// Select Note's matching the NoteId using the given [Connection]. /// /// # Returns @@ -878,6 +967,35 @@ pub fn get_state_sync( }) } +// NOTE SYNC +// ================================================================================================ + +/// Loads the data necessary for a note sync. +pub fn get_note_sync( + conn: &mut Connection, + block_num: BlockNumber, + note_tags: &[u32], +) -> Result { + let notes = select_notes_since_block_by_tag(conn, note_tags, block_num)?; + + let (block_header, chain_tip) = if !notes.is_empty() { + let block_header = select_block_header_by_block_num(conn, Some(notes[0].block_num))? + .ok_or(NoteSyncError::EmptyBlockHeadersTable)?; + let tip = select_block_header_by_block_num(conn, None)? + .ok_or(NoteSyncError::EmptyBlockHeadersTable)?; + + (block_header, tip.block_num()) + } else { + let block_header = select_block_header_by_block_num(conn, None)? + .ok_or(NoteSyncError::EmptyBlockHeadersTable)?; + + let block_num = block_header.block_num(); + (block_header, block_num) + }; + + Ok(NoteSyncUpdate { notes, block_header, chain_tip }) +} + // APPLY BLOCK // ================================================================================================ diff --git a/crates/store/src/errors.rs b/crates/store/src/errors.rs index c1bfd2f01..65461186d 100644 --- a/crates/store/src/errors.rs +++ b/crates/store/src/errors.rs @@ -214,3 +214,13 @@ pub enum StateSyncError { #[error("Failed to build MMR delta: {0}")] FailedToBuildMmrDelta(MmrError), } + +#[derive(Error, Debug)] +pub enum NoteSyncError { + #[error("Database error: {0}")] + DatabaseError(#[from] DatabaseError), + #[error("Block headers table is empty")] + EmptyBlockHeadersTable, + #[error("Error retrieving the merkle proof for the block: {0}")] + MmrError(#[from] MmrError), +} diff --git a/crates/store/src/server/api.rs b/crates/store/src/server/api.rs index 4830eed4f..88fc63469 100644 --- a/crates/store/src/server/api.rs +++ b/crates/store/src/server/api.rs @@ -12,7 +12,7 @@ use miden_node_proto::{ GetAccountDetailsRequest, GetAccountStateDeltaRequest, GetBlockByNumberRequest, GetBlockHeaderByNumberRequest, GetBlockInputsRequest, GetNotesByIdRequest, GetTransactionInputsRequest, ListAccountsRequest, ListNotesRequest, - ListNullifiersRequest, SyncStateRequest, + ListNullifiersRequest, SyncNoteRequest, SyncStateRequest, }, responses::{ AccountTransactionInputRecord, ApplyBlockResponse, CheckNullifiersByPrefixResponse, @@ -20,7 +20,7 @@ use miden_node_proto::{ GetBlockByNumberResponse, GetBlockHeaderByNumberResponse, GetBlockInputsResponse, GetNotesByIdResponse, GetTransactionInputsResponse, ListAccountsResponse, ListNotesResponse, ListNullifiersResponse, NullifierTransactionInputRecord, - NullifierUpdate, SyncStateResponse, + NullifierUpdate, SyncNoteResponse, SyncStateResponse, }, smt::SmtLeafEntry, store::api_server, @@ -165,7 +165,7 @@ impl api_server::Api for StoreApi { let (state, delta) = self .state - .sync_state(request.block_num, &account_ids, &request.note_tags, &request.nullifiers) + .sync_state(request.block_num, account_ids, request.note_tags, request.nullifiers) .await .map_err(internal_error)?; @@ -220,6 +220,45 @@ impl api_server::Api for StoreApi { })) } + /// Returns info which can be used by the client to sync note state. + #[instrument( + target = "miden-store", + name = "store:sync_notes", + skip_all, + ret(level = "debug"), + err + )] + async fn sync_notes( + &self, + request: tonic::Request, + ) -> Result, Status> { + let request = request.into_inner(); + + let (state, mmr_proof) = self + .state + .sync_notes(request.block_num, request.note_tags) + .await + .map_err(internal_error)?; + + let notes = state + .notes + .into_iter() + .map(|note| NoteSyncRecord { + note_index: note.note_index.to_absolute_index(), + note_id: Some(note.note_id.into()), + metadata: Some(note.metadata.into()), + merkle_path: Some(note.merkle_path.into()), + }) + .collect(); + + Ok(Response::new(SyncNoteResponse { + chain_tip: state.chain_tip, + block_header: Some(state.block_header.into()), + mmr_path: Some(mmr_proof.merkle_path.into()), + notes, + })) + } + /// Returns a list of Note's for the specified NoteId's. /// /// If the list is empty or no Note matched the requested NoteId and empty list is returned. diff --git a/crates/store/src/state.rs b/crates/store/src/state.rs index 795f335bb..c15d90f7c 100644 --- a/crates/store/src/state.rs +++ b/crates/store/src/state.rs @@ -29,9 +29,9 @@ use tracing::{info, info_span, instrument}; use crate::{ blocks::BlockStore, - db::{Db, NoteRecord, NullifierInfo, StateSyncUpdate}, + db::{Db, NoteRecord, NoteSyncUpdate, NullifierInfo, StateSyncUpdate}, errors::{ - ApplyBlockError, DatabaseError, GetBlockHeaderError, GetBlockInputsError, + ApplyBlockError, DatabaseError, GetBlockHeaderError, GetBlockInputsError, NoteSyncError, StateInitializationError, StateSyncError, }, nullifier_tree::NullifierTree, @@ -405,27 +405,26 @@ impl State { /// /// # Arguments /// - /// - `block_num`: The last block *know* by the client, updates start from the next block. + /// - `block_num`: The last block *known* by the client, updates start from the next block. /// - `account_ids`: Include the account's hash if their _last change_ was in the result's block /// range. - /// - `note_tag_prefixes`: Only the 16 high bits of the tags the client is interested in, result - /// will include notes with matching prefixes, the first block with a matching note determines - /// the block range. + /// - `note_tags`: The tags the client is interested in, result is restricted to the first block + /// with any matches tags. /// - `nullifier_prefixes`: Only the 16 high bits of the nullifiers the client is interested in, /// results will include nullifiers matching prefixes produced in the given block range. #[instrument(target = "miden-store", skip_all, ret(level = "debug"), err)] pub async fn sync_state( &self, block_num: BlockNumber, - account_ids: &[AccountId], - note_tag_prefixes: &[u32], - nullifier_prefixes: &[u32], + account_ids: Vec, + note_tags: Vec, + nullifier_prefixes: Vec, ) -> Result<(StateSyncUpdate, MmrDelta), StateSyncError> { let inner = self.inner.read().await; let state_sync = self .db - .get_state_sync(block_num, account_ids, note_tag_prefixes, nullifier_prefixes) + .get_state_sync(block_num, account_ids, note_tags, nullifier_prefixes) .await?; let delta = if block_num == state_sync.block_header.block_num() { @@ -451,6 +450,34 @@ impl State { Ok((state_sync, delta)) } + /// Loads data to synchronize a client's notes. + /// + /// The client's request contains a list of tags, this method will return the first + /// block with a matching tag, or the chain tip. All the other values are filter based on this + /// block range. + /// + /// # Arguments + /// + /// - `block_num`: The last block *known* by the client, updates start from the next block. + /// - `note_tags`: The tags the client is interested in, resulting notes are restricted to + /// the first block containing a matching note. + #[instrument(target = "miden-store", skip_all, ret(level = "debug"), err)] + pub async fn sync_notes( + &self, + block_num: BlockNumber, + note_tags: Vec, + ) -> Result<(NoteSyncUpdate, MmrProof), NoteSyncError> { + let inner = self.inner.read().await; + + let note_sync = self.db.get_note_sync(block_num, note_tags).await?; + + let mmr_proof = inner + .chain_mmr + .open(note_sync.block_header.block_num() as usize, inner.chain_mmr.forest())?; + + Ok((note_sync, mmr_proof)) + } + /// Returns data needed by the block producer to construct and prove the next block. pub async fn get_block_inputs( &self, diff --git a/proto/requests.proto b/proto/requests.proto index 43a5e3be1..02222f994 100644 --- a/proto/requests.proto +++ b/proto/requests.proto @@ -54,20 +54,27 @@ message SyncStateRequest { // it won't be included in the response. repeated account.AccountId account_ids = 2; - // Determines the tags which the client is interested in. These are only the 16high bits of the - // note's complete tag. - // - // The above means it is not possible to request an specific note, but only a "note family", - // this is done to increase the privacy of the client, by hiding the note's the client is - // intereted on. - repeated uint32 note_tags = 3; + // Specifies the tags which the client is interested in. + repeated fixed32 note_tags = 3; - // Determines the nullifiers the client is interested in. - // - // Similarly to the note_tags, this determins only the 16high bits of the target nullifier. + // Determines the nullifiers the client is interested in by specifying the 16high bits of the + // target nullifier. repeated uint32 nullifiers = 4; } +// Note synchronization request. +// +// Specifies note tags that client is intersted in. The server will return the first block which +// contains a note matching `note_tags` or the chain tip. +message SyncNoteRequest { + // Last block known by the client. The response will contain data starting from the next block, + // until the first block which contains a note of matching the requested tag. + fixed32 block_num = 1; + + // Specifies the tags which the client is interested in. + repeated fixed32 note_tags = 2; +} + message GetBlockInputsRequest { // ID of the account against which a transaction is executed. repeated account.AccountId account_ids = 1; diff --git a/proto/responses.proto b/proto/responses.proto index 1bf7c8aa3..4a0c9a044 100644 --- a/proto/responses.proto +++ b/proto/responses.proto @@ -62,6 +62,25 @@ message SyncStateResponse { repeated NullifierUpdate nullifiers = 8; } +message SyncNoteResponse { + // Number of the latest block in the chain + fixed32 chain_tip = 1; + + // Block header of the block with the first note matching the specified criteria + block_header.BlockHeader block_header = 2; + + // Proof for block header's MMR with respect to the chain tip. + // + // More specifically, the full proof consists of `forest`, `position` and `path` components. This + // value constitutes the `path`. The other two components can be obtained as follows: + // - `position` is simply `resopnse.block_header.block_num` + // - `forest` is the same as `response.chain_tip + 1` + merkle.MerklePath mmr_path = 3; + + // List of all notes together with the Merkle paths from `response.block_header.note_root` + repeated note.NoteSyncRecord notes = 4; +} + // An account returned as a response to the GetBlockInputs message AccountBlockInputRecord { account.AccountId account_id = 1; diff --git a/proto/rpc.proto b/proto/rpc.proto index 8e75bca77..da2c2bdd0 100644 --- a/proto/rpc.proto +++ b/proto/rpc.proto @@ -14,5 +14,6 @@ service Api { rpc GetBlockHeaderByNumber(requests.GetBlockHeaderByNumberRequest) returns (responses.GetBlockHeaderByNumberResponse) {} rpc GetNotesById(requests.GetNotesByIdRequest) returns (responses.GetNotesByIdResponse) {} rpc SubmitProvenTransaction(requests.SubmitProvenTransactionRequest) returns (responses.SubmitProvenTransactionResponse) {} + rpc SyncNotes(requests.SyncNoteRequest) returns (responses.SyncNoteResponse) {} rpc SyncState(requests.SyncStateRequest) returns (responses.SyncStateResponse) {} } diff --git a/proto/store.proto b/proto/store.proto index ced0a3f4d..1563b1261 100644 --- a/proto/store.proto +++ b/proto/store.proto @@ -21,5 +21,6 @@ service Api { rpc ListAccounts(requests.ListAccountsRequest) returns (responses.ListAccountsResponse) {} rpc ListNotes(requests.ListNotesRequest) returns (responses.ListNotesResponse) {} rpc ListNullifiers(requests.ListNullifiersRequest) returns (responses.ListNullifiersResponse) {} + rpc SyncNotes(requests.SyncNoteRequest) returns (responses.SyncNoteResponse) {} rpc SyncState(requests.SyncStateRequest) returns (responses.SyncStateResponse) {} }