miden_node_store/state/view/sync.rs
1use std::ops::RangeInclusive;
2
3use miden_node_tracing::miden_instrument;
4use miden_protocol::account::AccountId;
5use miden_protocol::block::{BlockHeader, BlockNumber, BlockSignatures};
6use miden_protocol::crypto::merkle::mmr::{Forest, MmrDelta, MmrProof};
7
8use super::StateView;
9use crate::COMPONENT;
10use crate::db::models::queries::StorageMapValuesPage;
11use crate::db::{AccountVaultValue, NoteSyncUpdate, NullifierInfo};
12use crate::errors::{DatabaseError, NoteSyncError, StateSyncError};
13
14// STATE SYNCHRONIZATION ENDPOINTS
15// ================================================================================================
16
17impl StateView {
18 /// Returns the complete transaction records for the specified accounts within the specified
19 /// block range, including state commitments and note IDs.
20 ///
21 /// Returns [`RangeBeyondTip`](crate::errors::RangeBeyondTip) if the range extends beyond this
22 /// view's chain tip.
23 pub async fn sync_transactions(
24 &self,
25 account_ids: Vec<AccountId>,
26 block_range: RangeInclusive<BlockNumber>,
27 ) -> Result<(BlockNumber, Vec<crate::db::TransactionRecord>), DatabaseError> {
28 let block_range = self.scope_range(block_range)?;
29 self.db.select_transactions_records(account_ids, block_range).await
30 }
31
32 /// Returns the chain MMR delta and the block header at the range's end for the specified
33 /// block range.
34 ///
35 /// Returns [`RangeBeyondTip`](crate::errors::RangeBeyondTip) if the range extends beyond this
36 /// view's chain tip.
37 #[miden_instrument(
38 level = "debug",
39 target = COMPONENT,
40 err,
41 )]
42 pub async fn sync_chain_mmr(
43 &self,
44 block_range: RangeInclusive<BlockNumber>,
45 ) -> Result<(MmrDelta, BlockHeader, BlockSignatures), StateSyncError> {
46 let block_range = self.scope_range(block_range)?;
47
48 let block_from = block_range.start();
49 let block_to = block_range.end();
50
51 // The scoped range's end is committed (at or below this view's tip), so its header must
52 // exist in the database.
53 let (block_header, signatures) = self
54 .db
55 .select_block_header_and_signatures_by_block_num(block_range.scoped_end())
56 .await?
57 .expect("the range-end header should exist in the database");
58
59 if block_from == block_to {
60 return Ok((
61 MmrDelta {
62 forest: Forest::new(block_from.as_usize()).expect("block index fits in u32"),
63 data: vec![],
64 },
65 block_header,
66 signatures,
67 ));
68 }
69
70 // Important notes about the boundary conditions:
71 //
72 // - The Mmr forest is 1-indexed whereas the block number is 0-indexed. The Mmr root
73 // contained in the block header always lag behind by one block, this is because the Mmr
74 // leaves are hashes of block headers, and we can't have self-referential hashes. These
75 // two points cancel out and don't require adjusting.
76 // - Mmr::get_delta is inclusive, whereas the sync request block_from is defined to be the
77 // last block already present in the caller's MMR. The delta should therefore start at the
78 // next block, so the from_forest has to be adjusted with a +1.
79 let from_forest = (block_from + 1).as_usize();
80 let to_forest = block_to.as_usize();
81
82 let mmr_delta = self
83 .blockchain()
84 .as_mmr()
85 .get_delta(
86 Forest::new(from_forest).expect("from_forest fits in u32"),
87 Forest::new(to_forest).expect("to_forest fits in u32"),
88 )
89 .map_err(StateSyncError::FailedToBuildMmrDelta)?;
90
91 Ok((mmr_delta, block_header, signatures))
92 }
93
94 /// Loads data to synchronize a client's notes.
95 ///
96 /// Returns as many blocks with matching notes as fit within the response payload limit
97 /// ([`MAX_RESPONSE_PAYLOAD_BYTES`](miden_node_utils::limiter::MAX_RESPONSE_PAYLOAD_BYTES)).
98 /// Each block includes its header and MMR proof at forest `block_range.end() + 1`.
99 ///
100 /// Also returns the last block number checked. If this equals `block_range.end()`, the
101 /// sync is complete.
102 ///
103 /// Returns [`RangeBeyondTip`](crate::errors::RangeBeyondTip) if the range extends beyond this
104 /// view's chain tip.
105 #[miden_instrument(
106 level = "debug",
107 target = COMPONENT,
108 err,
109 )]
110 pub async fn sync_notes(
111 &self,
112 note_tags: Vec<u32>,
113 block_range: RangeInclusive<BlockNumber>,
114 ) -> Result<(Vec<(NoteSyncUpdate, MmrProof)>, BlockNumber), NoteSyncError> {
115 let block_range = self.scope_range(block_range)?;
116
117 let block_end = block_range.end();
118 // The MMR at forest N contains proofs for blocks 0..N-1, so we use block_end + 1 to include
119 // the proof for block_end. SAFETY: block_end <= this view's tip (checked above), and the
120 // view's blockchain MMR always has at least tip + 1 leaves.
121 let mmr_checkpoint = block_end + 1;
122
123 let note_syncs = self.db.get_note_sync_multi(block_range, note_tags.into()).await?;
124
125 let mut results = Vec::new();
126
127 for note_sync in note_syncs {
128 let mmr_proof =
129 self.blockchain().open_at(note_sync.block_header.block_num(), mmr_checkpoint)?;
130 results.push((note_sync, mmr_proof));
131 }
132
133 // if results is empty, return `block_end` since the sync is complete.
134 let last_block_checked =
135 results.last().map_or(block_end, |(update, _)| update.block_header.block_num());
136
137 Ok((results, last_block_checked))
138 }
139
140 /// Returns nullifiers matching the given prefixes that were created within a block range.
141 ///
142 /// Returns [`RangeBeyondTip`](crate::errors::RangeBeyondTip) if the range extends beyond this
143 /// view's chain tip.
144 pub async fn sync_nullifiers(
145 &self,
146 prefix_len: u32,
147 nullifier_prefixes: Vec<u32>,
148 block_range: RangeInclusive<BlockNumber>,
149 ) -> Result<(Vec<NullifierInfo>, BlockNumber), DatabaseError> {
150 let block_range = self.scope_range(block_range)?;
151 self.db
152 .select_nullifiers_by_prefix(prefix_len, nullifier_prefixes, block_range)
153 .await
154 }
155
156 // ACCOUNT STATE SYNCHRONIZATION
157 // --------------------------------------------------------------------------------------------
158
159 /// Returns account vault updates for specified account within a block range.
160 ///
161 /// Returns [`RangeBeyondTip`](crate::errors::RangeBeyondTip) if the range extends beyond this
162 /// view's chain tip.
163 pub async fn sync_account_vault(
164 &self,
165 account_id: AccountId,
166 block_range: RangeInclusive<BlockNumber>,
167 ) -> Result<(BlockNumber, Vec<AccountVaultValue>), DatabaseError> {
168 let block_range = self.scope_range(block_range)?;
169 self.db.get_account_vault_sync(account_id, block_range).await
170 }
171
172 /// Returns storage map values for syncing within a block range.
173 ///
174 /// Returns [`RangeBeyondTip`](crate::errors::RangeBeyondTip) if the range extends beyond this
175 /// view's chain tip.
176 pub async fn sync_account_storage_maps(
177 &self,
178 account_id: AccountId,
179 block_range: RangeInclusive<BlockNumber>,
180 ) -> Result<StorageMapValuesPage, DatabaseError> {
181 let block_range = self.scope_range(block_range)?;
182 self.db.select_storage_map_sync_values(account_id, block_range, None).await
183 }
184}