diff --git a/CHANGELOG.md b/CHANGELOG.md index 74025a3d73..4a2582abe2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Data directory is no longer created but is instead expected to exist. - The genesis block can no longer be configured which also removes the `store dump-genesis` command. - [BREAKING] Use `AccountTree` and update account witness proto definitions (#783). +- [BREAKING] Update name of `ChainMmr` to `PartialBlockChain` (#807). ## v0.8.0 (2025-03-26) diff --git a/Cargo.lock b/Cargo.lock index 4d57efe5e4..b2a472f77f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1813,7 +1813,7 @@ dependencies = [ [[package]] name = "miden-block-prover" version = "0.9.0" -source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#c9bcf372696a6a4a9cb8789e6feb86a2d169b55b" +source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=pgackst-partial-blockchain#732d98dc9fd48a58692b19d98012d2fd5714d957" dependencies = [ "miden-crypto", "miden-lib", @@ -1906,7 +1906,7 @@ dependencies = [ [[package]] name = "miden-lib" version = "0.9.0" -source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#c9bcf372696a6a4a9cb8789e6feb86a2d169b55b" +source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=pgackst-partial-blockchain#732d98dc9fd48a58692b19d98012d2fd5714d957" dependencies = [ "miden-assembly", "miden-objects", @@ -2136,7 +2136,7 @@ dependencies = [ [[package]] name = "miden-objects" version = "0.9.0" -source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#c9bcf372696a6a4a9cb8789e6feb86a2d169b55b" +source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=pgackst-partial-blockchain#732d98dc9fd48a58692b19d98012d2fd5714d957" dependencies = [ "bech32", "getrandom 0.3.2", @@ -2183,7 +2183,7 @@ dependencies = [ [[package]] name = "miden-proving-service-client" version = "0.9.0" -source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#c9bcf372696a6a4a9cb8789e6feb86a2d169b55b" +source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=pgackst-partial-blockchain#732d98dc9fd48a58692b19d98012d2fd5714d957" dependencies = [ "async-trait", "getrandom 0.3.2", @@ -2213,7 +2213,7 @@ dependencies = [ [[package]] name = "miden-tx" version = "0.9.0" -source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#c9bcf372696a6a4a9cb8789e6feb86a2d169b55b" +source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=pgackst-partial-blockchain#732d98dc9fd48a58692b19d98012d2fd5714d957" dependencies = [ "async-trait", "miden-lib", @@ -2230,7 +2230,7 @@ dependencies = [ [[package]] name = "miden-tx-batch-prover" version = "0.9.0" -source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#c9bcf372696a6a4a9cb8789e6feb86a2d169b55b" +source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=pgackst-partial-blockchain#732d98dc9fd48a58692b19d98012d2fd5714d957" dependencies = [ "miden-core", "miden-crypto", diff --git a/Cargo.toml b/Cargo.toml index d5c89d49e0..a692b9a8a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,7 @@ assert_matches = { version = "1.5" } http = { version = "1.3" } itertools = { version = "0.14" } miden-air = { version = "0.13" } -miden-lib = { git = "https://github.com/0xPolygonMiden/miden-base.git", branch = "next" } +miden-lib = { git = "https://github.com/0xPolygonMiden/miden-base.git", branch = "pgackst-partial-blockchain" } miden-node-block-producer = { path = "crates/block-producer", version = "0.9" } miden-node-proto = { path = "crates/proto", version = "0.9" } miden-node-proto-build = { path = "proto", version = "0.9" } @@ -43,9 +43,9 @@ miden-node-rpc = { path = "crates/rpc", version = "0.9" } miden-node-store = { path = "crates/store", version = "0.9" } miden-node-test-macro = { path = "crates/test-macro" } miden-node-utils = { path = "crates/utils", version = "0.9" } -miden-objects = { git = "https://github.com/0xPolygonMiden/miden-base.git", branch = "next" } +miden-objects = { git = "https://github.com/0xPolygonMiden/miden-base.git", branch = "pgackst-partial-blockchain" } miden-processor = { version = "0.13" } -miden-tx = { git = "https://github.com/0xPolygonMiden/miden-base.git", branch = "next" } +miden-tx = { git = "https://github.com/0xPolygonMiden/miden-base.git", branch = "pgackst-partial-blockchain" } prost = { version = "0.13" } rand = { version = "0.9" } thiserror = { version = "2.0", default-features = false } diff --git a/bin/faucet/src/client.rs b/bin/faucet/src/client.rs index f470029c3f..82fa34baea 100644 --- a/bin/faucet/src/client.rs +++ b/bin/faucet/src/client.rs @@ -19,7 +19,7 @@ use miden_objects::{ }, note::{Note, NoteType}, transaction::{ - ChainMmr, ExecutedTransaction, ForeignAccountInputs, InputNote, InputNotes, + ExecutedTransaction, ForeignAccountInputs, InputNote, InputNotes, PartialBlockChain, TransactionArgs, TransactionScript, }, utils::Deserializable, @@ -61,7 +61,7 @@ impl FaucetClient { /// If the faucet account is not found on chain, it will be created on submission of the first /// minting transaction. pub async fn new(config: &FaucetConfig) -> Result { - let (mut rpc_api, root_block_header, root_chain_mmr) = + let (mut rpc_api, root_block_header, root_partial_block_chain) = initialize_faucet_client(config).await?; let faucet_account_data = AccountFile::read(&config.faucet_account_path) @@ -98,7 +98,7 @@ impl FaucetClient { faucet_account, faucet_account_data.account_seed, root_block_header, - root_chain_mmr, + root_partial_block_chain, )); let public_key = match &faucet_account_data.auth_secret_key { @@ -209,7 +209,7 @@ impl FaucetClient { /// Initializes the faucet client by connecting to the node and fetching the root block header. pub async fn initialize_faucet_client( config: &FaucetConfig, -) -> Result<(ApiClient, BlockHeader, ChainMmr), ClientError> { +) -> Result<(ApiClient, BlockHeader, PartialBlockChain), ClientError> { let endpoint = tonic::transport::Endpoint::try_from(config.node_url.to_string()) .context("Failed to parse node URL from configuration file")? .timeout(Duration::from_millis(config.timeout_ms)); @@ -232,15 +232,15 @@ pub async fn initialize_faucet_client( let root_block_header = root_block_header.try_into().context("Failed to parse block header")?; - let root_chain_mmr = ChainMmr::new( + let root_partial_block_chain = PartialBlockChain::new( PartialMmr::from_peaks( MmrPeaks::new(0, Vec::new()).expect("Empty MmrPeak should be valid"), ), Vec::new(), ) - .expect("Empty ChainMmr should be valid"); + .expect("Empty PartialBlockChain should be valid"); - Ok((rpc_api, root_block_header, root_chain_mmr)) + Ok((rpc_api, root_block_header, root_partial_block_chain)) } /// Requests account state from the node. diff --git a/bin/faucet/src/store.rs b/bin/faucet/src/store.rs index 9ac069bc21..0759ba208d 100644 --- a/bin/faucet/src/store.rs +++ b/bin/faucet/src/store.rs @@ -4,7 +4,7 @@ use miden_objects::{ MastForest, Word, account::{Account, AccountId}, block::{BlockHeader, BlockNumber}, - transaction::{ChainMmr, TransactionScript}, + transaction::{PartialBlockChain, TransactionScript}, }; use miden_tx::{DataStore, DataStoreError, MastForestStore, TransactionMastStore}; @@ -13,7 +13,7 @@ pub struct FaucetDataStore { /// Optional initial seed used for faucet account creation. init_seed: Option, block_header: BlockHeader, - chain_mmr: ChainMmr, + partial_block_chain: PartialBlockChain, mast_store: TransactionMastStore, } @@ -25,7 +25,7 @@ impl FaucetDataStore { faucet_account: Account, init_seed: Option, block_header: BlockHeader, - chain_mmr: ChainMmr, + partial_block_chain: PartialBlockChain, ) -> Self { let mast_store = TransactionMastStore::new(); mast_store.insert(faucet_account.code().mast()); @@ -34,7 +34,7 @@ impl FaucetDataStore { faucet_account: Mutex::new(faucet_account), init_seed, block_header, - chain_mmr, + partial_block_chain, mast_store, } } @@ -63,7 +63,7 @@ impl DataStore for FaucetDataStore { &self, account_id: AccountId, _ref_blocks: BTreeSet, - ) -> Result<(Account, Option, BlockHeader, ChainMmr), DataStoreError> { + ) -> Result<(Account, Option, BlockHeader, PartialBlockChain), DataStoreError> { let account = self.faucet_account.lock().expect("Poisoned lock"); if account_id != account.id() { return Err(DataStoreError::AccountNotFound(account_id)); @@ -73,7 +73,7 @@ impl DataStore for FaucetDataStore { account.clone(), account.is_new().then_some(self.init_seed).flatten(), self.block_header.clone(), - self.chain_mmr.clone(), + self.partial_block_chain.clone(), )) } } diff --git a/bin/stress-test/Cargo.toml b/bin/stress-test/Cargo.toml index 30236937ec..7298aea929 100644 --- a/bin/stress-test/Cargo.toml +++ b/bin/stress-test/Cargo.toml @@ -19,7 +19,7 @@ workspace = true clap = { version = "4.5", features = ["derive"] } futures = { version = "0.3" } miden-air = { workspace = true } -miden-block-prover = { git = "https://github.com/0xPolygonMiden/miden-base.git", branch = "next", features = [ +miden-block-prover = { git = "https://github.com/0xPolygonMiden/miden-base.git", branch = "pgackst-partial-blockchain", features = [ "testing", ] } miden-lib = { workspace = true } diff --git a/crates/block-producer/Cargo.toml b/crates/block-producer/Cargo.toml index 356e097d97..a9b4033014 100644 --- a/crates/block-producer/Cargo.toml +++ b/crates/block-producer/Cargo.toml @@ -22,18 +22,18 @@ anyhow = { workspace = true } async-trait = { version = "0.1" } futures = { version = "0.3" } itertools = { workspace = true } -miden-block-prover = { git = "https://github.com/0xPolygonMiden/miden-base.git", branch = "next" } +miden-block-prover = { git = "https://github.com/0xPolygonMiden/miden-base.git", branch = "pgackst-partial-blockchain" } miden-lib = { workspace = true } miden-node-proto = { workspace = true } miden-node-utils = { workspace = true } miden-objects = { workspace = true } miden-processor = { workspace = true } -miden-proving-service-client = { git = "https://github.com/0xPolygonMiden/miden-base.git", branch = "next", features = [ +miden-proving-service-client = { git = "https://github.com/0xPolygonMiden/miden-base.git", branch = "pgackst-partial-blockchain", features = [ "batch-prover", "block-prover", ] } miden-tx = { workspace = true } -miden-tx-batch-prover = { git = "https://github.com/0xPolygonMiden/miden-base.git", branch = "next" } +miden-tx-batch-prover = { git = "https://github.com/0xPolygonMiden/miden-base.git", branch = "pgackst-partial-blockchain" } rand = { version = "0.9" } thiserror = { workspace = true } tokio = { workspace = true, features = ["macros", "net", "rt-multi-thread", "sync", "time"] } diff --git a/crates/block-producer/src/batch_builder/mod.rs b/crates/block-producer/src/batch_builder/mod.rs index 00b9579698..450f3334ed 100644 --- a/crates/block-producer/src/batch_builder/mod.rs +++ b/crates/block-producer/src/batch_builder/mod.rs @@ -222,7 +222,7 @@ impl BatchJob { ProposedBatch::new( transactions, inputs.batch_reference_block_header, - inputs.chain_mmr, + inputs.partial_block_chain, inputs.note_proofs, ) .map_err(BuildBatchError::ProposeBatchError) diff --git a/crates/block-producer/src/test_utils/store.rs b/crates/block-producer/src/test_utils/store.rs index 382002943c..b863b00c2c 100644 --- a/crates/block-producer/src/test_utils/store.rs +++ b/crates/block-producer/src/test_utils/store.rs @@ -36,12 +36,17 @@ pub struct MockStoreSuccessBuilder { impl MockStoreSuccessBuilder { pub fn from_batches<'a>(batches_iter: impl Iterator + Clone) -> Self { let accounts_smt = { - let accounts = batches_iter.clone().flat_map(|batch| { - batch - .account_updates() - .iter() - .map(|(account_id, update)| (*account_id, update.initial_state_commitment())) - }); + // We have to allocate the vector so we can call AccountTree::with_entries with an + // ExactSizeIterator. + let accounts = batches_iter + .clone() + .flat_map(|batch| { + batch.account_updates().iter().map(|(account_id, update)| { + (*account_id, update.initial_state_commitment()) + }) + }) + .collect::>(); + AccountTree::with_entries(accounts).unwrap() }; @@ -54,7 +59,7 @@ impl MockStoreSuccessBuilder { } } - pub fn from_accounts(accounts: impl Iterator) -> Self { + pub fn from_accounts(accounts: impl ExactSizeIterator) -> Self { let accounts_smt = AccountTree::with_entries(accounts).unwrap(); Self { diff --git a/crates/proto/src/domain/batch.rs b/crates/proto/src/domain/batch.rs index 2a29247eab..ac02fc094e 100644 --- a/crates/proto/src/domain/batch.rs +++ b/crates/proto/src/domain/batch.rs @@ -3,7 +3,7 @@ use std::collections::BTreeMap; use miden_objects::{ block::BlockHeader, note::{NoteId, NoteInclusionProof}, - transaction::ChainMmr, + transaction::PartialBlockChain, utils::{Deserializable, Serializable}, }; @@ -17,7 +17,7 @@ use crate::{ pub struct BatchInputs { pub batch_reference_block_header: BlockHeader, pub note_proofs: BTreeMap, - pub chain_mmr: ChainMmr, + pub partial_block_chain: PartialBlockChain, } impl From for proto::GetBatchInputsResponse { @@ -25,7 +25,7 @@ impl From for proto::GetBatchInputsResponse { Self { batch_reference_block_header: Some(inputs.batch_reference_block_header.into()), note_proofs: inputs.note_proofs.iter().map(Into::into).collect(), - chain_mmr: inputs.chain_mmr.to_bytes(), + partial_block_chain: inputs.partial_block_chain.to_bytes(), } } } @@ -44,8 +44,10 @@ impl TryFrom for BatchInputs { .iter() .map(<(NoteId, NoteInclusionProof)>::try_from) .collect::>()?, - chain_mmr: ChainMmr::read_from_bytes(&response.chain_mmr) - .map_err(|source| ConversionError::deserialization_error("ChainMmr", source))?, + partial_block_chain: PartialBlockChain::read_from_bytes(&response.partial_block_chain) + .map_err(|source| { + ConversionError::deserialization_error("PartialBlockChain", source) + })?, }; Ok(result) diff --git a/crates/proto/src/domain/block.rs b/crates/proto/src/domain/block.rs index 5f05efca25..483e3a5af5 100644 --- a/crates/proto/src/domain/block.rs +++ b/crates/proto/src/domain/block.rs @@ -3,7 +3,7 @@ use std::collections::BTreeMap; use miden_objects::{ block::{BlockHeader, BlockInputs, NullifierWitness}, note::{NoteId, NoteInclusionProof}, - transaction::ChainMmr, + transaction::PartialBlockChain, utils::{Deserializable, Serializable}, }; @@ -101,7 +101,7 @@ impl From for GetBlockInputsResponse { fn from(inputs: BlockInputs) -> Self { let ( prev_block_header, - chain_mmr, + partial_block_chain, account_witnesses, nullifier_witnesses, unauthenticated_note_proofs, @@ -120,7 +120,7 @@ impl From for GetBlockInputsResponse { NullifierWitnessRecord { nullifier, proof }.into() }) .collect(), - chain_mmr: chain_mmr.to_bytes(), + partial_block_chain: partial_block_chain.to_bytes(), unauthenticated_note_proofs: unauthenticated_note_proofs .iter() .map(NoteInclusionInBlockProof::from) @@ -162,12 +162,14 @@ impl TryFrom for BlockInputs { .map(<(NoteId, NoteInclusionProof)>::try_from) .collect::>()?; - let chain_mmr = ChainMmr::read_from_bytes(&response.chain_mmr) - .map_err(|source| ConversionError::deserialization_error("ChainMmr", source))?; + let partial_block_chain = PartialBlockChain::read_from_bytes(&response.partial_block_chain) + .map_err(|source| { + ConversionError::deserialization_error("PartialBlockChain", source) + })?; Ok(BlockInputs::new( latest_block_header, - chain_mmr, + partial_block_chain, account_witnesses, nullifier_witnesses, unauthenticated_note_proofs, diff --git a/crates/proto/src/generated/responses.rs b/crates/proto/src/generated/responses.rs index bfd3baf09f..e57f89af3f 100644 --- a/crates/proto/src/generated/responses.rs +++ b/crates/proto/src/generated/responses.rs @@ -125,7 +125,7 @@ pub struct GetBlockInputsResponse { /// above note inclusion proofs as well as proofs for inclusion of the requested blocks /// referenced by the batches in the block. #[prost(bytes = "vec", tag = "3")] - pub chain_mmr: ::prost::alloc::vec::Vec, + pub partial_block_chain: ::prost::alloc::vec::Vec, /// The state commitments of the requested accounts and their authentication paths. #[prost(message, repeated, tag = "4")] pub account_witnesses: ::prost::alloc::vec::Vec, @@ -146,7 +146,7 @@ pub struct GetBatchInputsResponse { /// above note inclusion proofs as well as proofs for inclusion of the blocks referenced /// by the transactions in the batch. #[prost(bytes = "vec", tag = "3")] - pub chain_mmr: ::prost::alloc::vec::Vec, + pub partial_block_chain: ::prost::alloc::vec::Vec, } /// An account returned as a response to the `GetTransactionInputs`. #[derive(Clone, PartialEq, ::prost::Message)] diff --git a/crates/store/src/errors.rs b/crates/store/src/errors.rs index e8dbaa3e87..98644f8c59 100644 --- a/crates/store/src/errors.rs +++ b/crates/store/src/errors.rs @@ -2,14 +2,10 @@ use std::io; use deadpool::managed::PoolError; use miden_objects::{ - AccountDeltaError, AccountError, AccountTreeError, NoteError, + AccountDeltaError, AccountError, AccountTreeError, NoteError, NullifierTreeError, account::AccountId, block::BlockNumber, - crypto::{ - hash::rpo::RpoDigest, - merkle::{MerkleError, MmrError}, - utils::DeserializationError, - }, + crypto::{hash::rpo::RpoDigest, merkle::MmrError, utils::DeserializationError}, note::Nullifier, transaction::OutputNote, }; @@ -18,18 +14,6 @@ use thiserror::Error; use tokio::sync::oneshot::error::RecvError; use tonic::Status; -// INTERNAL ERRORS -// ================================================================================================= - -#[derive(Debug, Error)] -pub enum NullifierTreeError { - #[error("failed to create nullifier tree")] - CreationFailed(#[source] MerkleError), - - #[error("failed to mutate nullifier tree")] - MutationFailed(#[source] MerkleError), -} - // DATABASE ERRORS // ================================================================================================= @@ -162,6 +146,10 @@ pub enum InvalidBlockError { NewBlockInvalidNullifierRoot, #[error("new block `prev_block_commitment` must match the chain's tip")] NewBlockInvalidPrevCommitment, + #[error("nullifier in new block is already spent")] + NewBlockNullifierAlreadySpent(#[source] NullifierTreeError), + #[error("duplicate account ID prefix in new block")] + NewBlockDuplicateAccountIdPrefix(#[source] AccountTreeError), } #[derive(Error, Debug)] diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs index bbbf7e538c..c741278104 100644 --- a/crates/store/src/lib.rs +++ b/crates/store/src/lib.rs @@ -4,7 +4,6 @@ mod blocks; mod db; mod errors; mod genesis; -mod nullifier_tree; mod server; mod state; diff --git a/crates/store/src/nullifier_tree.rs b/crates/store/src/nullifier_tree.rs deleted file mode 100644 index 7d8b04d5f9..0000000000 --- a/crates/store/src/nullifier_tree.rs +++ /dev/null @@ -1,112 +0,0 @@ -use miden_objects::{ - Felt, FieldElement, Word, - block::BlockNumber, - crypto::{ - hash::rpo::RpoDigest, - merkle::{MutationSet, SMT_DEPTH, Smt, SmtProof}, - }, - note::Nullifier, -}; - -use crate::errors::NullifierTreeError; - -/// Nullifier SMT. -#[derive(Debug, Clone)] -pub struct NullifierTree(Smt); - -impl NullifierTree { - /// Construct new nullifier tree from list of items. - pub fn with_entries( - entries: impl IntoIterator, - ) -> Result { - let leaves = entries.into_iter().map(|(nullifier, block_num)| { - (nullifier.inner(), Self::block_num_to_leaf_value(block_num)) - }); - - let inner = Smt::with_entries(leaves).map_err(NullifierTreeError::CreationFailed)?; - - Ok(Self(inner)) - } - - /// Returns the root of the nullifier SMT. - pub fn root(&self) -> RpoDigest { - self.0.root() - } - - /// Returns an opening of the leaf associated with the given nullifier. - pub fn open(&self, nullifier: &Nullifier) -> SmtProof { - self.0.open(&nullifier.inner()) - } - - /// Returns block number stored for the given nullifier or `None` if the nullifier wasn't - /// consumed. - pub fn get_block_num(&self, nullifier: &Nullifier) -> Option { - let value = self.0.get_value(&nullifier.inner()); - if value == Smt::EMPTY_VALUE { - return None; - } - - Some(Self::leaf_value_to_block_num(value)) - } - - /// Computes mutations for the nullifier SMT. - pub fn compute_mutations( - &self, - kv_pairs: impl IntoIterator, - ) -> MutationSet { - self.0.compute_mutations(kv_pairs.into_iter().map(|(nullifier, block_num)| { - (nullifier.inner(), Self::block_num_to_leaf_value(block_num)) - })) - } - - /// Applies mutations to the nullifier SMT. - pub fn apply_mutations( - &mut self, - mutations: MutationSet, - ) -> Result<(), NullifierTreeError> { - self.0.apply_mutations(mutations).map_err(NullifierTreeError::MutationFailed) - } - - // HELPER FUNCTIONS - // -------------------------------------------------------------------------------------------- - - /// Returns the nullifier's leaf value in the SMT by its block number. - fn block_num_to_leaf_value(block: BlockNumber) -> Word { - [Felt::from(block), Felt::ZERO, Felt::ZERO, Felt::ZERO] - } - - /// Given the leaf value of the nullifier SMT, returns the nullifier's block number. - /// - /// There are no nullifiers in the genesis block. The value zero is instead used to signal - /// absence of a value. - fn leaf_value_to_block_num(value: Word) -> BlockNumber { - let block_num: u32 = - value[0].as_int().try_into().expect("invalid block number found in store"); - - block_num.into() - } -} - -#[cfg(test)] -mod tests { - use miden_objects::{Felt, ZERO}; - - use super::NullifierTree; - - #[test] - fn leaf_value_encoding() { - let block_num = 123; - let nullifier_value = NullifierTree::block_num_to_leaf_value(block_num.into()); - - assert_eq!(nullifier_value, [Felt::from(block_num), ZERO, ZERO, ZERO]); - } - - #[test] - fn leaf_value_decoding() { - let block_num = 123; - let nullifier_value = [Felt::from(block_num), ZERO, ZERO, ZERO]; - let decoded_block_num = NullifierTree::leaf_value_to_block_num(nullifier_value); - - assert_eq!(decoded_block_num, block_num.into()); - } -} diff --git a/crates/store/src/state.rs b/crates/store/src/state.rs index 56ac506a6b..4167ae91cf 100644 --- a/crates/store/src/state.rs +++ b/crates/store/src/state.rs @@ -22,15 +22,15 @@ use miden_objects::{ AccountError, account::{AccountDelta, AccountHeader, AccountId, StorageSlot}, block::{ - AccountTree, AccountWitness, BlockHeader, BlockInputs, BlockNumber, NullifierWitness, - ProvenBlock, + AccountTree, AccountWitness, BlockChain, BlockHeader, BlockInputs, BlockNumber, + NullifierTree, NullifierWitness, ProvenBlock, }, crypto::{ hash::rpo::RpoDigest, - merkle::{Mmr, MmrDelta, MmrError, MmrPeaks, MmrProof, PartialMmr, SmtProof}, + merkle::{Mmr, MmrDelta, MmrProof, PartialMmr, SmtProof}, }, note::{NoteId, Nullifier}, - transaction::{ChainMmr, OutputNote}, + transaction::{OutputNote, PartialBlockChain}, utils::Serializable, }; use tokio::{ @@ -48,7 +48,6 @@ use crate::{ GetBlockInputsError, InvalidBlockError, NoteSyncError, StateInitializationError, StateSyncError, }, - nullifier_tree::NullifierTree, }; // STRUCTURES // ================================================================================================ @@ -60,106 +59,19 @@ pub struct TransactionInputs { pub found_unauthenticated_notes: BTreeSet, } -/// A [Merkle Mountain Range](Mmr) defining a chain of blocks. -#[derive(Debug, Clone)] -pub struct Blockchain(Mmr); - -impl Blockchain { - /// Returns a new Blockchain. - pub fn new(chain_mmr: Mmr) -> Self { - Self(chain_mmr) - } - - /// Returns the tip of the chain, i.e. the number of the latest block in the chain. - pub fn chain_tip(&self) -> BlockNumber { - let block_number: u32 = (self.0.forest() - 1) - .try_into() - .expect("chain_mmr always has, at least, the genesis block"); - - block_number.into() - } - - /// Returns the current peaks of the MMR. - pub fn peaks(&self) -> MmrPeaks { - self.0.peaks() - } - - /// Returns the peaks of the MMR at the state specified by `forest`. - /// - /// # Errors - /// - /// Returns an error if the specified `forest` value is not valid for this MMR. - pub fn peaks_at(&self, forest: usize) -> Result { - self.0.peaks_at(forest) - } - - /// 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: RpoDigest) { - self.0.add(block_commitment); - } - - /// Returns an [`MmrProof`] for the leaf at the specified position. - pub fn open(&self, pos: usize) -> Result { - self.0.open_at(pos, self.0.forest()) - } - - /// Returns a reference to the underlying [`Mmr`]. - pub fn as_mmr(&self) -> &Mmr { - &self.0 - } - - /// Creates a [`PartialMmr`] at the state of the latest block (i.e. the block's chain root will - /// match the hashed peaks of the returned partial MMR). This MMR will include authentication - /// paths for all blocks in the provided set. - pub fn partial_mmr_from_blocks( - &self, - blocks: &BTreeSet, - latest_block_number: BlockNumber, - ) -> PartialMmr { - // Using latest block as the target forest means we take the state of the MMR one before - // the latest block. This is because the latest block will be used as the reference - // block of the batch and will be added to the MMR by the batch kernel. - let target_forest = latest_block_number.as_usize(); - let peaks = self - .peaks_at(target_forest) - .expect("target_forest should be smaller than forest of the chain mmr"); - // Grab the block merkle paths from the inner state. - let mut partial_mmr = PartialMmr::from_peaks(peaks); - - for block_num in blocks.iter().map(BlockNumber::as_usize) { - // SAFETY: We have ensured block nums are less than chain length. - let leaf = self - .0 - .get(block_num) - .expect("block num less than chain length should exist in chain mmr"); - let path = self - .0 - .open_at(block_num, target_forest) - .expect("block num and target forest should be valid for this mmr") - .merkle_path; - // SAFETY: We should be able to fill the partial MMR with data from the chain MMR - // without errors, otherwise it indicates the chain mmr is invalid. - partial_mmr - .track(block_num, leaf, &path) - .expect("filling partial mmr with data from mmr should succeed"); - } - - partial_mmr - } -} - /// Container for state that needs to be updated atomically. struct InnerState { nullifier_tree: NullifierTree, - blockchain: Blockchain, + blockchain: BlockChain, account_tree: AccountTree, } impl InnerState { /// Returns the latest block number. fn latest_block_num(&self) -> BlockNumber { - self.blockchain.chain_tip() + self.blockchain + .chain_tip() + .expect("chain should always have at least the genesis block") } } @@ -195,7 +107,9 @@ impl State { let inner = RwLock::new(InnerState { nullifier_tree, - blockchain: Blockchain::new(chain_mmr), + // SAFETY: We assume the loaded MMR is valid and does not have more than u32::MAX + // entries. + blockchain: BlockChain::from_mmr_unchecked(chain_mmr), account_tree, }); @@ -305,21 +219,27 @@ impl State { } // compute update for nullifier tree - let nullifier_tree_update = inner.nullifier_tree.compute_mutations( - block.created_nullifiers().iter().map(|nullifier| (*nullifier, block_num)), - ); + let nullifier_tree_update = inner + .nullifier_tree + .compute_mutations( + block.created_nullifiers().iter().map(|nullifier| (*nullifier, block_num)), + ) + .map_err(InvalidBlockError::NewBlockNullifierAlreadySpent)?; - if nullifier_tree_update.root() != header.nullifier_root() { + if nullifier_tree_update.as_mutation_set().root() != header.nullifier_root() { return Err(InvalidBlockError::NewBlockInvalidNullifierRoot.into()); } // compute update for account tree - let account_tree_update = inner.account_tree.compute_mutations( - block - .updated_accounts() - .iter() - .map(|update| (update.account_id(), update.final_state_commitment())), - ); + let account_tree_update = inner + .account_tree + .compute_mutations( + block + .updated_accounts() + .iter() + .map(|update| (update.account_id(), update.final_state_commitment())), + ) + .map_err(InvalidBlockError::NewBlockDuplicateAccountIdPrefix)?; if account_tree_update.as_mutation_set().root() != header.account_root() { return Err(InvalidBlockError::NewBlockInvalidAccountRoot.into()); @@ -450,7 +370,7 @@ impl State { if let Some(header) = block_header { let mmr_proof = if include_mmr_proof { let inner = self.inner.read().await; - let mmr_proof = inner.blockchain.open(header.block_num().as_usize())?; + let mmr_proof = inner.blockchain.open(header.block_num())?; Some(mmr_proof) } else { None @@ -479,7 +399,11 @@ impl State { #[instrument(target = COMPONENT, skip_all, ret(level = "debug"))] pub async fn check_nullifiers(&self, nullifiers: &[Nullifier]) -> Vec { let inner = self.inner.read().await; - nullifiers.iter().map(|n| inner.nullifier_tree.open(n)).collect() + nullifiers + .iter() + .map(|n| inner.nullifier_tree.open(n)) + .map(NullifierWitness::into_proof) + .collect() } /// Queries a list of [`NoteRecord`] from the database. @@ -543,7 +467,7 @@ impl State { let (batch_reference_block, partial_mmr) = { let inner_state = self.inner.read().await; - let latest_block_num = inner_state.blockchain.chain_tip(); + let latest_block_num = inner_state.latest_block_num(); let highest_block_num = *blocks.last().expect("we should have checked for empty block references"); @@ -559,10 +483,19 @@ impl State { // there is no need to prove its inclusion. blocks.remove(&latest_block_num); - ( - latest_block_num, - inner_state.blockchain.partial_mmr_from_blocks(&blocks, latest_block_num), - ) + // SAFETY: + // - The latest block num was retrieved from the inner blockchain from which we will + // also retrieve the proofs, so it is guaranteed to exist in that chain. + // - We have checked that no block number in the blocks set is greater than latest block + // number *and* latest block num was removed from the set. Therefore only block + // numbers smaller than latest block num remain in the set. Therefore all the block + // numbers are guaranteed to exist in the chain state at latest block num. + let partial_mmr = inner_state + .blockchain + .partial_mmr_from_blocks(&blocks, latest_block_num) + .expect("latest block num should exist and all blocks in set should be < than latest block"); + + (latest_block_num, partial_mmr) }; // Fetch the reference block of the batch as part of this query, so we can avoid looking it @@ -582,7 +515,7 @@ impl State { }) .expect("DB should have returned the header of the batch reference block"); - // The order doesn't matter for ChainMmr::new, so swap remove is fine. + // The order doesn't matter for PartialBlockChain::new, so swap remove is fine. let batch_reference_block_header = headers.swap_remove(header_index); // SAFETY: This should not error because: @@ -590,13 +523,13 @@ impl State { // - so none of the block headers block numbers should exceed the chain length of the // partial MMR, // - and we've added blocks to a BTreeSet, so there can be no duplicates. - let chain_mmr = ChainMmr::new(partial_mmr, headers) + let partial_block_chain = PartialBlockChain::new(partial_mmr, headers) .expect("partial mmr and block headers should be consistent"); Ok(BatchInputs { batch_reference_block_header, note_proofs, - chain_mmr, + partial_block_chain, }) } @@ -673,7 +606,7 @@ impl State { let note_sync = self.db.get_note_sync(block_num, note_tags).await?; - let mmr_proof = inner.blockchain.open(note_sync.block_header.block_num().as_usize())?; + let mmr_proof = inner.blockchain.open(note_sync.block_header.block_num())?; Ok((note_sync, mmr_proof)) } @@ -724,7 +657,7 @@ impl State { }) .expect("DB should have returned the header of the latest block header"); - // The order doesn't matter for ChainMmr::new, so swap remove is fine. + // The order doesn't matter for PartialBlockChain::new, so swap remove is fine. let latest_block_header = headers.swap_remove(latest_block_header_index); // SAFETY: This should not error because: @@ -732,12 +665,12 @@ impl State { // - so none of the block header's block numbers should exceed the chain length of the // partial MMR, // - and we've added blocks to a BTreeSet, so there can be no duplicates. - let chain_mmr = ChainMmr::new(partial_mmr, headers) + let partial_block_chain = PartialBlockChain::new(partial_mmr, headers) .expect("partial mmr and block headers should be consistent"); Ok(BlockInputs::new( latest_block_header, - chain_mmr, + partial_block_chain, account_witnesses, nullifier_witnesses, unauthenticated_note_proofs, @@ -783,7 +716,18 @@ impl State { // Fetch the partial MMR at the state of the latest block with authentication paths for the // provided set of blocks. - let partial_mmr = inner.blockchain.partial_mmr_from_blocks(blocks, latest_block_number); + // + // SAFETY: + // - The latest block num was retrieved from the inner blockchain from which we will also + // retrieve the proofs, so it is guaranteed to exist in that chain. + // - We have checked that no block number in the blocks set is greater than latest block + // number *and* latest block num was removed from the set. Therefore only block numbers + // smaller than latest block num remain in the set. Therefore all the block numbers are + // guaranteed to exist in the chain state at latest block num. + let partial_mmr = + inner.blockchain.partial_mmr_from_blocks(blocks, latest_block_number).expect( + "latest block num should exist and all blocks in set should be < than latest block", + ); // Fetch witnesses for all acounts. let account_witnesses = account_ids @@ -797,10 +741,7 @@ impl State { let nullifier_witnesses: BTreeMap = nullifiers .iter() .copied() - .map(|nullifier| { - let proof = inner.nullifier_tree.open(&nullifier); - (nullifier, NullifierWitness::new(proof)) - }) + .map(|nullifier| (nullifier, inner.nullifier_tree.open(&nullifier))) .collect(); Ok((latest_block_number, account_witnesses, nullifier_witnesses, partial_mmr)) diff --git a/proto/proto/responses.proto b/proto/proto/responses.proto index 8c68ce0e54..1564125f77 100644 --- a/proto/proto/responses.proto +++ b/proto/proto/responses.proto @@ -124,7 +124,7 @@ message GetBlockInputsResponse { // The serialized chain MMR which includes proofs for all blocks referenced by the // above note inclusion proofs as well as proofs for inclusion of the requested blocks // referenced by the batches in the block. - bytes chain_mmr = 3; + bytes partial_block_chain = 3; // The state commitments of the requested accounts and their authentication paths. repeated AccountWitness account_witnesses = 4; @@ -144,7 +144,7 @@ message GetBatchInputsResponse { // The serialized chain MMR which includes proofs for all blocks referenced by the // above note inclusion proofs as well as proofs for inclusion of the blocks referenced // by the transactions in the batch. - bytes chain_mmr = 3; + bytes partial_block_chain = 3; } // An account returned as a response to the `GetTransactionInputs`.