Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- [BREAKING] Add `NetworkAccount` configuration (#1275).
- Added support for environment variables to set up the `miden-proving-service` worker (#1281).
- Added field identifier structs for component metadata (#1292).
- Move `NullifierTree` and `BlockChain` from node to base (#1304).

## 0.8.2 (2025-04-18) - `miden-proving-service` crate only

Expand Down
8 changes: 3 additions & 5 deletions crates/miden-block-prover/src/tests/proposed_block_errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use assert_matches::assert_matches;
use miden_objects::{
MAX_BATCHES_PER_BLOCK, ProposedBlockError,
account::AccountId,
block::{BlockInputs, BlockNumber, NullifierWitness, ProposedBlock},
block::{BlockInputs, BlockNumber, ProposedBlock},
note::NoteInclusionProof,
testing::account_id::ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
transaction::ProvenTransaction,
Expand Down Expand Up @@ -476,16 +476,14 @@ fn proposed_block_fails_on_spent_nullifier_witness() -> anyhow::Result<()> {
);
alternative_chain.apply_executed_transaction(&transaction);
alternative_chain.seal_next_block();
let spent_proof = alternative_chain.nullifiers().open(&note0.nullifier().inner());
let spent_proof = alternative_chain.nullifiers().open(&note0.nullifier());

let batches = vec![batch0.clone()];
let mut block_inputs = chain.get_block_inputs(&batches);

// Insert the spent nullifier proof from the alternative chain into the block inputs from the
// actual chain.
block_inputs
.nullifier_witnesses_mut()
.insert(note0.nullifier(), NullifierWitness::new(spent_proof));
block_inputs.nullifier_witnesses_mut().insert(note0.nullifier(), spent_proof);

let error = ProposedBlock::new(block_inputs, batches).unwrap_err();
assert_matches!(error, ProposedBlockError::NullifierSpent(nullifier) if nullifier == note0.nullifier());
Expand Down
9 changes: 4 additions & 5 deletions crates/miden-block-prover/src/tests/proven_block_success.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::{collections::BTreeMap, vec::Vec};
use anyhow::Context;
use miden_crypto::merkle::Smt;
use miden_objects::{
Felt, FieldElement, MIN_PROOF_SECURITY_LEVEL,
MIN_PROOF_SECURITY_LEVEL,
batch::BatchNoteTree,
block::{AccountTree, BlockInputs, BlockNoteIndex, BlockNoteTree, ProposedBlock},
transaction::InputNoteCommitment,
Expand Down Expand Up @@ -98,10 +98,9 @@ fn proven_block_success() -> anyhow::Result<()> {

let mut expected_nullifier_tree = chain.nullifiers().clone();
for nullifier in proposed_block.created_nullifiers().keys() {
expected_nullifier_tree.insert(
nullifier.inner(),
[Felt::from(proposed_block.block_num()), Felt::ZERO, Felt::ZERO, Felt::ZERO],
);
expected_nullifier_tree
.mark_spent(*nullifier, proposed_block.block_num())
.context("failed to mark nullifier as spent")?;
}

// Compute expected account root on the full account tree.
Expand Down
163 changes: 163 additions & 0 deletions crates/miden-objects/src/block/block_chain.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
use alloc::collections::BTreeSet;

use miden_crypto::merkle::{Mmr, MmrError, MmrPeaks, MmrProof, PartialMmr};

use crate::{Digest, block::BlockNumber};

/// The [Merkle Mountain Range](Mmr) defining the Miden blockchain.
///
/// The values of the leaves in the MMR are the commitments of blocks, i.e.
/// [`BlockHeader::commitment`](crate::block::BlockHeader::commitment).
///
/// Each new block updates the blockchain by adding **the previous block's commitment** to the MMR.
/// This means the chain commitment found in block 10's header commits to all blocks 0..=9, but not
/// 10 itself. This results from the fact that block 10 cannot compute its own block commitment
/// and thus cannot add itself to the chain. Hence, the blockchain MMR is lagging behind by one
/// block.
///
/// Some APIs take a _state block_ which is equivalent to the concept of _forest_ of the underlying
/// MMR. As an example, if the blockchain has 20 blocks in total, and the state block is 10, then
/// the API works in the context of the chain at the time it had 10 blocks, i.e. it contains blocks
/// 0..=9. This is useful, for example, to retrieve proofs that are valid when verified against the
/// chain commitment of block 10.
///
/// The maximum number of supported blocks is [`u32::MAX`]. This is not validated however.
#[derive(Debug, Clone)]
pub struct BlockChain {
mmr: Mmr,
}
Comment thread
PhilippGackstatter marked this conversation as resolved.

impl BlockChain {
// CONSTRUCTORS
// --------------------------------------------------------------------------------------------

/// Returns a new, empty blockchain.
pub fn new() -> Self {
Self { mmr: Mmr::new() }
}

/// Construct a new blockchain from an [`Mmr`] without validation.
pub fn from_mmr_unchecked(mmr: Mmr) -> Self {
Self { mmr }
}

// PUBLIC ACCESSORS
// --------------------------------------------------------------------------------------------

/// Returns the number of blocks in the chain.
pub fn num_blocks(&self) -> u32 {
// SAFETY: The chain should never contain more than u32::MAX blocks, so a non-panicking cast
// should be fine.
self.mmr.forest() as u32
}

/// Returns the tip of the chain, i.e. the number of the latest block in the chain, unless the
/// chain is empty.
pub fn chain_tip(&self) -> Option<BlockNumber> {
if self.num_blocks() == 0 {
return None;
}

Some(BlockNumber::from(self.num_blocks() - 1))
}

/// Returns the current peaks of the MMR.
pub fn peaks(&self) -> MmrPeaks {
self.mmr.peaks()
}

/// Returns the peaks of the chain at the state of the given block.
///
/// Note that this represents the state of the chain where the block at the given number **is
/// not yet** in the chain. For example, if the given block number is 5, then the returned peaks
/// represent the chain whose latest block is 4. See the type-level documentation for why this
/// is the case.
///
/// # Errors
///
/// Returns an error if the specified `block` exceeds the number of blocks in the chain.
pub fn peaks_at(&self, state_block: BlockNumber) -> Result<MmrPeaks, MmrError> {
Comment thread
PhilippGackstatter marked this conversation as resolved.
Outdated
self.mmr.peaks_at(state_block.as_usize())
}

/// Returns an [`MmrProof`] for the `block` with the given number.
///
/// # Errors
///
/// Returns an error if:
/// - The specified block number does not exist in the chain.
pub fn open(&self, block: BlockNumber) -> Result<MmrProof, MmrError> {
self.mmr.open(block.as_usize())
}

/// Returns an [`MmrProof`] for the `block` with the given number at the state of the given
/// `state_block`.
///
/// # Errors
///
/// Returns an error if:
/// - The specified block number does not exist in the chain.
/// - The specified state block number exceeds the number of blocks in the chain.
pub fn open_at(
&self,
block: BlockNumber,
state_block: BlockNumber,
) -> Result<MmrProof, MmrError> {
Comment thread
PhilippGackstatter marked this conversation as resolved.
self.mmr.open_at(block.as_usize(), state_block.as_usize())
}

/// Returns a reference to the underlying [`Mmr`].
pub fn as_mmr(&self) -> &Mmr {
&self.mmr
}

/// Creates a [`PartialMmr`] at the state of the given block. This means the hashed peaks of the
/// returned partial MMR will match the state block's chain commitment. This MMR will include
/// authentication paths for all blocks in the provided `blocks` set.
///
/// # Errors
///
/// Returns an error if:
/// - the specified `latest_block_number` exceeds the number of blocks in the chain.
/// - any block in `blocks` is not in the state of the chain specified by `latest_block_number`.
pub fn partial_mmr_from_blocks(
&self,
blocks: &BTreeSet<BlockNumber>,
state_block: BlockNumber,
) -> Result<PartialMmr, MmrError> {
// Using latest block as the target state means we take the state of the MMR one before
// the latest block.
let peaks = self.peaks_at(state_block)?;

// Track the merkle paths of the requested blocks in the partial MMR.
let mut partial_mmr = PartialMmr::from_peaks(peaks);
for block_num in blocks.iter() {
let leaf = self.mmr.get(block_num.as_usize())?;
let path = self.open_at(*block_num, state_block)?.merkle_path;

// SAFETY: We should be able to fill the partial MMR with data from the chain MMR
// without errors, otherwise it indicates the blockchain is invalid.
partial_mmr
.track(block_num.as_usize(), leaf, &path)
.expect("filling partial mmr with data from mmr should succeed");
}

Ok(partial_mmr)
}

// PUBLIC MUTATORS
// --------------------------------------------------------------------------------------------

/// Adds a block commitment to the MMR.
///
/// The caller must ensure that this commitent is the one for the next block in the chain.
pub fn push(&mut self, block_commitment: Digest) {
self.mmr.add(block_commitment);
}
}

impl Default for BlockChain {
fn default() -> Self {
Self::new()
}
}
6 changes: 6 additions & 0 deletions crates/miden-objects/src/block/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ pub use partial_account_tree::PartialAccountTree;
pub(super) mod account_tree;
pub use account_tree::AccountTree;

mod nullifier_tree;
pub use nullifier_tree::NullifierTree;

mod block_chain;
pub use block_chain::BlockChain;

mod partial_nullifier_tree;
pub use partial_nullifier_tree::PartialNullifierTree;

Expand Down
Loading