Skip to main content

miden_node_store/state/
apply_block.rs

1use std::sync::Arc;
2
3use miden_node_proto::domain::proof_request::BlockProofRequest;
4use miden_node_utils::ErrorReport;
5use miden_node_utils::tracing::{miden_instrument, miden_span_record};
6use miden_protocol::Word;
7use miden_protocol::account::delta::AccountUpdateDetails;
8use miden_protocol::batch::OrderedBatches;
9use miden_protocol::block::account_tree::AccountMutationSet;
10use miden_protocol::block::nullifier_tree::NullifierMutationSet;
11use miden_protocol::block::{BlockBody, BlockHeader, BlockInputs, BlockNumber, SignedBlock};
12use miden_protocol::note::{NoteDetails, Nullifier};
13use miden_protocol::transaction::OutputNote;
14use miden_protocol::utils::serde::Serializable;
15use tokio::sync::oneshot;
16use tracing::{Instrument, info_span};
17
18use crate::db::NoteRecord;
19use crate::errors::{ApplyBlockError, ApplyBlockWithProvingInputsError, InvalidBlockError};
20use crate::state::{BlockNotification, State};
21use crate::{COMPONENT, HistoricalError, LOG_TARGET};
22
23impl State {
24    /// Saves proving inputs for a signed block and applies it to the state.
25    ///
26    /// Used by the in-process block producer after it has built and signed a block.
27    #[miden_instrument(
28        target = COMPONENT,
29        skip_all,
30        err,
31    )]
32    pub async fn apply_block_with_proving_inputs(
33        &self,
34        ordered_batches: OrderedBatches,
35        block_inputs: BlockInputs,
36        signed_block: SignedBlock,
37    ) -> Result<(), ApplyBlockWithProvingInputsError> {
38        let block_header = signed_block.header().clone();
39        let block_num = block_header.block_num();
40
41        let proving_inputs = BlockProofRequest {
42            tx_batches: ordered_batches,
43            block_header,
44            block_inputs,
45        };
46
47        self.save_proving_inputs(block_num, &proving_inputs)
48            .await
49            .map_err(ApplyBlockWithProvingInputsError::SaveProvingInputs)?;
50
51        self.apply_block(signed_block)
52            .await
53            .map_err(ApplyBlockWithProvingInputsError::ApplyBlock)
54    }
55
56    /// Apply changes of a new block to the DB and in-memory data structures.
57    ///
58    /// ## Note on state consistency
59    ///
60    /// The server contains in-memory representations of the existing trees, the in-memory
61    /// representation must be kept consistent with the committed data, this is necessary so to
62    /// provide consistent results for all endpoints. In order to achieve consistency, the
63    /// following steps are used:
64    ///
65    /// - the request data is validated, prior to starting any modifications.
66    /// - block is being saved into the store in parallel with updating the DB, but before
67    ///   committing. This block is considered as candidate and not yet available for reading
68    ///   because the latest block pointer is not updated yet.
69    /// - a transaction is open in the DB and the writes are started.
70    /// - while the transaction is not committed, concurrent reads are allowed, both the DB and the
71    ///   in-memory representations, which are consistent at this stage.
72    /// - prior to committing the changes to the DB, an exclusive lock to the in-memory data is
73    ///   acquired, preventing concurrent reads to the in-memory data, since that will be
74    ///   out-of-sync w.r.t. the DB.
75    /// - the DB transaction is committed, and requests that read only from the DB can proceed to
76    ///   use the fresh data.
77    /// - the in-memory structures are updated, including the latest block pointer and the lock is
78    ///   released.
79    // TODO: This span is logged in a root span, we should connect it to the parent span.
80    #[miden_instrument(
81        target = COMPONENT,
82        skip_all,
83        err,
84    )]
85    pub async fn apply_block(&self, signed_block: SignedBlock) -> Result<(), ApplyBlockError> {
86        let _lock = self.writer.try_lock().map_err(|_| ApplyBlockError::ConcurrentWrite)?;
87
88        let header = signed_block.header();
89        let body = signed_block.body();
90
91        let block_num = header.block_num();
92        let block_commitment = header.commitment();
93        let num_transactions = body.transactions().as_slice().len();
94
95        miden_span_record!(
96            block.number = %block_num,
97            block.commitment = %block_commitment,
98            block.transactions.count = num_transactions,
99        );
100
101        self.validate_block_header(header, body).await?;
102
103        // Save the block to the block store. In a case of a rolled-back DB transaction, the
104        // in-memory state will be unchanged, but the file might still be written. Such blocks
105        // should be considered candidates, not finalized blocks.
106        let signed_block_bytes = signed_block.to_bytes();
107        // Clone before moving into the block-save task so we can cache for replicas at commit.
108        let cache_bytes = signed_block_bytes.clone();
109        let store = Arc::clone(&self.block_store);
110        let block_save_task = tokio::spawn(
111            async move { store.save_block(block_num, &signed_block_bytes).await }.in_current_span(),
112        );
113
114        let (
115            nullifier_tree_old_root,
116            nullifier_tree_update,
117            account_tree_old_root,
118            account_tree_update,
119        ) = self.compute_tree_mutations(header, body).await?;
120
121        let notes = Self::build_note_records(header, body)?;
122
123        // Signals the transaction is ready to be committed, and the write lock can be acquired.
124        let (allow_acquire, acquired_allowed) = oneshot::channel::<()>();
125        // Signals the write lock has been acquired, and the transaction can be committed.
126        let (inform_acquire_done, acquire_done) = oneshot::channel::<()>();
127
128        // Extract public account updates with deltas before block is moved into async task. Private
129        // accounts are filtered out since they don't expose their state changes.
130        let account_deltas =
131            Vec::from_iter(body.updated_accounts().iter().filter_map(
132                |update| match update.details() {
133                    AccountUpdateDetails::Delta(delta) => Some(delta.clone()),
134                    AccountUpdateDetails::Private => None,
135                },
136            ));
137
138        // The DB and in-memory state updates need to be synchronized and are partially overlapping.
139        // Namely, the DB transaction only proceeds after this task acquires the in-memory write
140        // lock. This requires the DB update to run concurrently, so a new task is spawned.
141        let db = Arc::clone(&self.db);
142        let db_update_task = tokio::spawn(
143            async move { db.apply_block(allow_acquire, acquire_done, signed_block, notes).await }
144                .in_current_span(),
145        );
146
147        // Wait for the message from the DB update task, that we ready to commit the DB transaction.
148        acquired_allowed
149            .instrument(info_span!(target: COMPONENT, "await_db_readiness"))
150            .await
151            .map_err(ApplyBlockError::ClosedChannel)?;
152
153        // Awaiting the block saving task to complete without errors.
154        block_save_task.await??;
155
156        self.with_inner_write_blocking(|inner| {
157            // We need to check that neither the nullifier tree nor the account tree have changed
158            // while we were waiting for the DB preparation task to complete. If either of them did
159            // change, we do not proceed with in-memory and database updates, since it may lead to
160            // an inconsistent state.
161            if inner.nullifier_tree.root() != nullifier_tree_old_root
162                || inner.account_tree.root_latest() != account_tree_old_root
163            {
164                return Err(ApplyBlockError::ConcurrentWrite);
165            }
166
167            // Notify the DB update task that the write lock has been acquired, so it can commit the
168            // DB transaction.
169            inform_acquire_done
170                .send(())
171                .map_err(|_| ApplyBlockError::DbUpdateTaskFailed("Receiver was dropped".into()))?;
172
173            // TODO: shutdown #91 Await for successful commit of the DB transaction. If the commit
174            // fails, we mustn't change in-memory state, so we return a block applying error and
175            // don't proceed with in-memory updates.
176            tokio::runtime::Handle::current()
177                .block_on(db_update_task)?
178                .map_err(|err| ApplyBlockError::DbUpdateTaskFailed(err.as_report()))?;
179
180            // Update the in-memory data structures after successful commit of the DB transaction
181            inner
182                .nullifier_tree
183                .apply_mutations(nullifier_tree_update)
184                .expect("Unreachable: old nullifier tree root must be checked before this step");
185            inner
186                .account_tree
187                .apply_mutations(account_tree_update)
188                .expect("Unreachable: old account tree root must be checked before this step");
189
190            inner.blockchain.push(block_commitment);
191
192            Ok(())
193        })?;
194
195        self.with_forest_write_blocking(|forest| {
196            forest.apply_block_updates(block_num, account_deltas)
197        })?;
198
199        // Push to cache and notify replica subscribers.
200        self.block_cache
201            .push(block_num, BlockNotification::new(block_num, cache_bytes))
202            .expect("block cache receives sequential block numbers");
203        let _ = self.committed_tip_tx.send(block_num);
204
205        tracing::debug!(target: LOG_TARGET, "Block applied");
206
207        Ok(())
208    }
209
210    /// Saves the proving inputs for the given block to the block store.
211    pub async fn save_proving_inputs(
212        &self,
213        block_num: BlockNumber,
214        proving_inputs: &BlockProofRequest,
215    ) -> std::io::Result<()> {
216        self.block_store
217            .save_proving_inputs(block_num, &proving_inputs.to_bytes())
218            .await
219    }
220
221    /// Validates that the block header is consistent with the block body and the current state.
222    #[miden_instrument(
223        target = COMPONENT,
224        skip_all,
225        err,
226    )]
227    async fn validate_block_header(
228        &self,
229        header: &BlockHeader,
230        body: &BlockBody,
231    ) -> Result<(), ApplyBlockError> {
232        // Validate that header and body match.
233        let tx_commitment = body.transactions().commitment();
234        if header.tx_commitment() != tx_commitment {
235            return Err(InvalidBlockError::InvalidBlockTxCommitment {
236                expected: tx_commitment,
237                actual: header.tx_commitment(),
238            }
239            .into());
240        }
241
242        let block_num = header.block_num();
243
244        // Validate that the applied block is the next block in sequence.
245        let prev_block = self
246            .db
247            .select_block_header_by_block_num(None)
248            .await?
249            .ok_or(ApplyBlockError::DbBlockHeaderEmpty)?;
250        let expected_block_num = prev_block.block_num().child();
251        if block_num != expected_block_num {
252            return Err(InvalidBlockError::NewBlockInvalidBlockNum {
253                expected: expected_block_num,
254                submitted: block_num,
255            }
256            .into());
257        }
258        if header.prev_block_commitment() != prev_block.commitment() {
259            return Err(InvalidBlockError::NewBlockInvalidPrevCommitment.into());
260        }
261
262        Ok(())
263    }
264
265    /// Computes nullifier and account tree mutations, validating roots against the block header.
266    #[miden_instrument(
267        target = COMPONENT,
268        skip_all,
269        err,
270    )]
271    async fn compute_tree_mutations(
272        &self,
273        header: &BlockHeader,
274        body: &BlockBody,
275    ) -> Result<(Word, NullifierMutationSet, Word, AccountMutationSet), ApplyBlockError> {
276        self.with_inner_read_blocking(|inner| {
277            let block_num = header.block_num();
278
279            // nullifiers can be produced only once
280            let duplicate_nullifiers: Vec<_> = body
281                .created_nullifiers()
282                .iter()
283                .filter(|&nullifier| inner.nullifier_tree.get_block_num(nullifier).is_some())
284                .copied()
285                .collect();
286            if !duplicate_nullifiers.is_empty() {
287                return Err(InvalidBlockError::DuplicatedNullifiers(duplicate_nullifiers).into());
288            }
289
290            // new_block.chain_root must be equal to the chain MMR root prior to the update
291            let peaks = inner.blockchain.peaks();
292            if peaks.hash_peaks() != header.chain_commitment() {
293                return Err(InvalidBlockError::NewBlockInvalidChainCommitment.into());
294            }
295
296            // compute update for nullifier tree
297            let nullifier_tree_update = inner
298                .nullifier_tree
299                .compute_mutations(
300                    body.created_nullifiers().iter().map(|nullifier| (*nullifier, block_num)),
301                )
302                .map_err(InvalidBlockError::NewBlockNullifierAlreadySpent)?;
303
304            if nullifier_tree_update.as_mutation_set().root() != header.nullifier_root() {
305                return Err(InvalidBlockError::NewBlockInvalidNullifierRoot.into());
306            }
307
308            // compute update for account tree
309            let account_tree_update = inner
310                .account_tree
311                .compute_mutations(
312                    body.updated_accounts()
313                        .iter()
314                        .map(|update| (update.account_id(), update.final_state_commitment())),
315                )
316                .map_err(|e| match e {
317                    HistoricalError::AccountTreeError(err) => {
318                        InvalidBlockError::NewBlockDuplicateAccountIdPrefix(err)
319                    },
320                    HistoricalError::MerkleError(_) => {
321                        panic!("Unexpected MerkleError during account tree mutation computation")
322                    },
323                })?;
324
325            if account_tree_update.as_mutation_set().root() != header.account_root() {
326                return Err(InvalidBlockError::NewBlockInvalidAccountRoot.into());
327            }
328
329            Ok((
330                inner.nullifier_tree.root(),
331                nullifier_tree_update,
332                inner.account_tree.root_latest(),
333                account_tree_update,
334            ))
335        })
336    }
337
338    /// Builds note records with inclusion proofs from the block body.
339    #[miden_instrument(
340        target = COMPONENT,
341        skip_all,
342        err,
343    )]
344    fn build_note_records(
345        header: &BlockHeader,
346        body: &BlockBody,
347    ) -> Result<Vec<(NoteRecord, Option<Nullifier>)>, ApplyBlockError> {
348        let block_num = header.block_num();
349
350        let note_tree = body.compute_block_note_tree();
351        if note_tree.root() != header.note_root() {
352            return Err(InvalidBlockError::NewBlockInvalidNoteRoot.into());
353        }
354
355        let notes = body
356            .output_notes()
357            .map(|(note_index, note)| {
358                let (details, attachments, nullifier) = match note {
359                    OutputNote::Public(public) => (
360                        Some(NoteDetails::from(public.as_note())),
361                        public.as_note().attachments().clone(),
362                        Some(public.as_note().nullifier()),
363                    ),
364                    OutputNote::Private(private) => (None, private.attachments().clone(), None),
365                };
366
367                let inclusion_path = note_tree.open(note_index);
368
369                let note_record = NoteRecord {
370                    block_num,
371                    note_index,
372                    note_id: note.id().as_word(),
373                    metadata: *note.metadata(),
374                    details,
375                    attachments,
376                    inclusion_path,
377                };
378
379                Ok((note_record, nullifier))
380            })
381            .collect::<Result<Vec<_>, InvalidBlockError>>()?;
382
383        Ok(notes)
384    }
385}