-
Notifications
You must be signed in to change notification settings - Fork 105
feat: make SyncNotes return multiple blocks
#1843
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: next
Are you sure you want to change the base?
Changes from 7 commits
051de21
6e0dd69
cc7ce76
4ddb750
2d16d93
adbefa6
ff4a02b
736c937
01823ca
cbb997a
12401ce
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -130,26 +130,34 @@ impl rpc_server::Rpc for StoreApi { | |
| let request = request.into_inner(); | ||
|
|
||
| let chain_tip = self.state.latest_block_num().await; | ||
| let requested_block_to = request.block_range.as_ref().and_then(|r| r.block_to); | ||
|
||
| let block_range = | ||
| read_block_range::<NoteSyncError>(request.block_range, "SyncNotesRequest")? | ||
| .into_inclusive_range::<NoteSyncError>(&chain_tip)?; | ||
|
|
||
| let block_from = block_range.start().as_u32(); | ||
| let response_block_to = requested_block_to.unwrap_or(chain_tip.as_u32()); | ||
|
|
||
| // Validate note tags count | ||
| check::<QueryParamNoteTagLimit>(request.note_tags.len())?; | ||
|
|
||
| let (state, mmr_proof, last_block_included) = | ||
| self.state.sync_notes(request.note_tags, block_range).await?; | ||
| let results = self.state.sync_notes(request.note_tags, block_range).await?; | ||
|
|
||
| let notes = state.notes.into_iter().map(Into::into).collect(); | ||
| let blocks = results | ||
| .into_iter() | ||
| .map(|(state, mmr_proof)| proto::rpc::sync_notes_response::NoteSyncBlock { | ||
| block_header: Some(state.block_header.into()), | ||
| mmr_path: Some(mmr_proof.merkle_path().clone().into()), | ||
| notes: state.notes.into_iter().map(Into::into).collect(), | ||
| }) | ||
| .collect(); | ||
|
|
||
| Ok(Response::new(proto::rpc::SyncNotesResponse { | ||
| pagination_info: Some(proto::rpc::PaginationInfo { | ||
| chain_tip: chain_tip.as_u32(), | ||
| block_num: last_block_included.as_u32(), | ||
| block_range: Some(proto::rpc::BlockRange { | ||
| block_from, | ||
| block_to: Some(response_block_to), | ||
| }), | ||
| block_header: Some(state.block_header.into()), | ||
| mmr_path: Some(mmr_proof.merkle_path().clone().into()), | ||
| notes, | ||
| blocks, | ||
| })) | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| use std::ops::RangeInclusive; | ||
|
|
||
| use miden_node_utils::limiter::MAX_RESPONSE_PAYLOAD_BYTES; | ||
| use miden_protocol::account::AccountId; | ||
| use miden_protocol::block::BlockNumber; | ||
| use miden_protocol::crypto::merkle::mmr::{Forest, MmrDelta, MmrProof}; | ||
|
|
@@ -11,6 +12,17 @@ use crate::db::models::queries::StorageMapValuesPage; | |
| use crate::db::{AccountVaultValue, NoteSyncUpdate, NullifierInfo}; | ||
| use crate::errors::{DatabaseError, NoteSyncError, StateSyncError}; | ||
|
|
||
| /// Estimated byte size of a [`NoteSyncBlock`] excluding its notes. | ||
| /// | ||
| /// `BlockHeader` (~341 bytes) + MMR proof with 32 siblings (~1216 bytes). | ||
| const BLOCK_OVERHEAD_BYTES: usize = 1600; | ||
|
Comment on lines
+16
to
+19
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not for this PR, but it would be good to make this based on the actual serialization sizes. For example, we could have Let's create an issue for this (and include |
||
|
|
||
| /// Estimated byte size of a single [`NoteSyncRecord`]. | ||
| /// | ||
| /// Note ID (~38 bytes) + index + metadata (~26 bytes) + sparse merkle path with 16 | ||
| /// siblings (~608 bytes). | ||
| const NOTE_RECORD_BYTES: usize = 700; | ||
|
Comment on lines
+21
to
+25
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is fine for now, but most likely will be a significant overestimate because sparse Merkle paths get compressed, and in most cases shouldn't be more than a couple hundred bytes. But the compression depends on how many paths there are (the more paths, the worse the compression) - so, taking the worst case is fine for now. |
||
|
|
||
| // STATE SYNCHRONIZATION ENDPOINTS | ||
| // ================================================================================================ | ||
|
|
||
|
|
@@ -64,29 +76,48 @@ impl State { | |
|
|
||
| /// 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 | ||
| /// | ||
| /// - `note_tags`: The tags the client is interested in, resulting notes are restricted to the | ||
| /// first block containing a matching note. | ||
| /// - `block_range`: The range of blocks from which to synchronize notes. | ||
| /// Returns as many blocks with matching notes as fit within the response payload | ||
|
||
| /// limit. Each block includes its header and MMR proof at `block_range.end()`. | ||
| #[instrument(level = "debug", target = COMPONENT, skip_all, ret(level = "debug"), err)] | ||
| pub async fn sync_notes( | ||
| &self, | ||
| note_tags: Vec<u32>, | ||
| block_range: RangeInclusive<BlockNumber>, | ||
| ) -> Result<(NoteSyncUpdate, MmrProof, BlockNumber), NoteSyncError> { | ||
| ) -> Result<Vec<(NoteSyncUpdate, MmrProof)>, NoteSyncError> { | ||
| let inner = self.inner.read().await; | ||
|
||
| let checkpoint = *block_range.end(); | ||
|
|
||
| let mut results = Vec::new(); | ||
| let mut accumulated_size: usize = 0; | ||
| let mut current_from = *block_range.start(); | ||
|
|
||
| loop { | ||
| let range = current_from..=checkpoint; | ||
| let Some(note_sync) = self.db.get_note_sync(range, note_tags.clone()).await? else { | ||
|
||
| break; | ||
| }; | ||
|
|
||
| let (note_sync, last_included_block) = | ||
| self.db.get_note_sync(block_range, note_tags).await?; | ||
| accumulated_size += BLOCK_OVERHEAD_BYTES + note_sync.notes.len() * NOTE_RECORD_BYTES; | ||
|
|
||
| let mmr_proof = inner.blockchain.open(note_sync.block_header.block_num())?; | ||
| if accumulated_size > MAX_RESPONSE_PAYLOAD_BYTES { | ||
| break; | ||
| } | ||
|
||
|
|
||
| let block_num = note_sync.block_header.block_num(); | ||
|
|
||
| if block_num >= checkpoint { | ||
| break; | ||
| } | ||
|
Comment on lines
+111
to
+113
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We break here but AFAIK we are supposed to return notes for the checkpoint block, right? I think this might be a special case. If the user calls |
||
|
|
||
| let mmr_proof = inner.blockchain.open_at(block_num, checkpoint)?; | ||
|
||
| results.push((note_sync, mmr_proof)); | ||
|
|
||
| // The DB query uses `committed_at > block_range.start()` (exclusive), | ||
| // so setting current_from to the found block is sufficient to skip it. | ||
| current_from = block_num; | ||
| } | ||
|
|
||
| Ok((note_sync, mmr_proof, last_included_block)) | ||
| Ok(results) | ||
| } | ||
|
|
||
| pub async fn sync_nullifiers( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit (feel free to disregard): There's no need to read the latest block number if the user set a
block_to