Skip to main content

miden_node_store/state/
apply_block.rs

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