Skip to main content

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}