Skip to content
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
0817f7d
feat: Implement account ID tree
PhilippGackstatter Mar 28, 2025
d86fad9
feat: Add `AccountTree::with_entries`
PhilippGackstatter Mar 28, 2025
0376218
feat: Add `PartialAccountTree`
PhilippGackstatter Apr 3, 2025
95d34e9
feat: Add `AccountMutationSet`
PhilippGackstatter Apr 3, 2025
a79544f
feat: Use uniqueness guarantee in account witness
PhilippGackstatter Apr 3, 2025
49e6134
Merge remote-tracking branch 'origin/next' into pgackst-account-tree
PhilippGackstatter Apr 3, 2025
83335ef
chore: Add `as_mutation_set`
PhilippGackstatter Apr 3, 2025
d89a74d
chore: Remove `LeafIndex` conversion on account ID
PhilippGackstatter Apr 3, 2025
c181bed
chore: Add changelog entry
PhilippGackstatter Apr 3, 2025
f65413f
feat: Document (partial) account tree methods
PhilippGackstatter Apr 3, 2025
569bc03
fix: partial account tree doc link
PhilippGackstatter Apr 3, 2025
b18b559
feat: Enforce account ID prefix validity in account witness
PhilippGackstatter Apr 4, 2025
867ebb8
chore: Fix docs, add todos
PhilippGackstatter Apr 4, 2025
cddbb29
feat: Add num created notes and commitment on txs
PhilippGackstatter Apr 4, 2025
59e1586
chore: Make error messages consistent
PhilippGackstatter Apr 4, 2025
d4cf130
feat: Add id suffix to account witness and add prefix test
PhilippGackstatter Apr 4, 2025
a0103bf
feat: Refactor account witness to contain merkle path & commitment
PhilippGackstatter Apr 14, 2025
fab77a8
feat: Add tests for partial account tree
PhilippGackstatter Apr 14, 2025
44eaa46
feat: Simplify duplicate id check, impl same block dup test
PhilippGackstatter Apr 14, 2025
c7e8e04
Merge remote-tracking branch 'origin/next' into pgackst-account-tree
PhilippGackstatter Apr 14, 2025
f62a18c
feat: Add `AccountWitness::new`
PhilippGackstatter Apr 15, 2025
8b3d858
chore: Use AccountTree over SimpleSmt in tests
PhilippGackstatter Apr 15, 2025
83a5673
feat: Simplify account witness constructors
PhilippGackstatter Apr 15, 2025
557c7df
chore: No-std imports
PhilippGackstatter Apr 15, 2025
04f6931
fix: use commitment from witness account ID
PhilippGackstatter Apr 15, 2025
6e5d6e7
chore: Better doc comment for `AccountMutationSet`
PhilippGackstatter Apr 16, 2025
deb30cc
chore: Update docs on account witness
PhilippGackstatter Apr 16, 2025
696129b
feat: Deduplicate witness construction from proof
PhilippGackstatter Apr 17, 2025
be9222c
feat: Move compute_tx_commitment to `OrderedTransactionHeaders`
PhilippGackstatter Apr 17, 2025
aa350cf
fix: witness doc link
PhilippGackstatter Apr 17, 2025
9c71d44
fix: state img in book
PhilippGackstatter Apr 17, 2025
e19eb3b
chore: update method names
bobbinth Apr 18, 2025
cda1a42
Merge branch 'next' into pgackst-account-tree
bobbinth Apr 18, 2025
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 @@ -7,6 +7,7 @@
- Added getter for proof security level in `ProvenBatch` and `ProvenBlock` (#1259).
- [BREAKING] Replaced the `ProvenBatch::new_unchecked` with the `ProvenBatch::new` method to initialize the struct with validations (#1260).
- Added a retry strategy for worker's health check (#1255).
- Add `AccountTree` and `PartialAccountTree` wrappers and enforce ID prefix uniqueness (#1254).

## 0.8.1 (2025-03-26) - `miden-objects` and `miden-tx` crates only.

Expand Down
15 changes: 6 additions & 9 deletions crates/miden-block-prover/src/errors.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
use miden_crypto::merkle::MerkleError;
use miden_objects::{Digest, NullifierTreeError, account::AccountId};
use miden_objects::{AccountTreeError, Digest, NullifierTreeError};
use thiserror::Error;

#[derive(Debug, Error)]
pub enum ProvenBlockError {
#[error("nullifier witness has a different root than the current nullifier tree root")]
NullifierWitnessRootMismatch(#[source] NullifierTreeError),

#[error(
"account witness for account {account_id} has a different root than the current account tree root"
)]
AccountWitnessRootMismatch {
account_id: AccountId,
source: MerkleError,
},
#[error("failed to track account witness")]
AccountWitnessTracking { source: AccountTreeError },

#[error("account ID prefix already exists in the tree")]
AccountIdPrefixDuplicate { source: AccountTreeError },

#[error(
"account tree root of the previous block header is {prev_block_account_root} but the root of the partial tree computed from account witnesses is {stale_account_root}, indicating that the witnesses are stale"
Expand Down
55 changes: 21 additions & 34 deletions crates/miden-block-prover/src/local_block_prover.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
use std::{collections::BTreeMap, vec::Vec};

use miden_crypto::merkle::{LeafIndex, PartialMerkleTree};
use miden_lib::transaction::TransactionKernel;
use miden_objects::{
Digest, Word,
Digest,
account::AccountId,
block::{
AccountUpdateWitness, BlockAccountUpdate, BlockHeader, BlockNoteIndex, BlockNoteTree,
BlockNumber, NullifierWitness, OutputNoteBatch, PartialNullifierTree, ProposedBlock,
ProvenBlock,
BlockNumber, NullifierWitness, OutputNoteBatch, PartialAccountTree, PartialNullifierTree,
ProposedBlock, ProvenBlock,
},
note::Nullifier,
transaction::ChainMmr,
Expand Down Expand Up @@ -89,7 +88,7 @@ impl LocalBlockProver {

let (
batches,
mut account_updated_witnesses,
account_updated_witnesses,
output_note_batches,
created_nullifiers,
chain_mmr,
Expand All @@ -115,7 +114,7 @@ impl LocalBlockProver {
// --------------------------------------------------------------------------------------------

let new_account_root =
compute_account_root(&mut account_updated_witnesses, &prev_block_header)?;
compute_account_root(&account_updated_witnesses, &prev_block_header)?;

// Insert the previous block header into the block chain MMR to get the new chain
// commitment.
Expand Down Expand Up @@ -208,7 +207,7 @@ fn compute_nullifiers(
// its corresponding nullifier witness, so we don't have to check again whether they match.
for witness in created_nullifiers.into_values() {
partial_nullifier_tree
.add_nullifier_witness(witness)
.track_nullifier_witness(witness)
.map_err(ProvenBlockError::NullifierWitnessRootMismatch)?;
}

Expand Down Expand Up @@ -248,34 +247,21 @@ fn compute_chain_commitment(mut chain_mmr: ChainMmr, prev_block_header: BlockHea
/// It uses a PartialMerkleTree for now while we use a SimpleSmt for the account tree. Once that is
/// updated to an Smt, we can use a PartialSmt instead.
fn compute_account_root(
updated_accounts: &mut Vec<(AccountId, AccountUpdateWitness)>,
updated_accounts: &[(AccountId, AccountUpdateWitness)],
prev_block_header: &BlockHeader,
) -> Result<Digest, ProvenBlockError> {
// If no accounts were updated, the account tree root is unchanged.
if updated_accounts.is_empty() {
return Ok(prev_block_header.account_root());
}

let mut partial_account_tree = PartialMerkleTree::new();

// First reconstruct the current account tree from the provided merkle paths.
for (account_id, witness) in updated_accounts.iter_mut() {
let account_leaf_index = LeafIndex::from(*account_id);
// Shouldn't the value in PartialMerkleTree::add_path be a Word instead of a Digest?
// PartialMerkleTree::update_leaf (below) takes a Word as a value, so this seems
// inconsistent.
partial_account_tree
.add_path(
account_leaf_index.value(),
witness.initial_state_commitment(),
// We don't need the merkle path later, so we can take it out.
core::mem::take(witness.initial_state_proof_mut()),
)
.map_err(|source| ProvenBlockError::AccountWitnessRootMismatch {
account_id: *account_id,
source,
})?;
}
// If a witness points to a leaf where multiple account IDs share the same prefix, this will
// return an error.
let mut partial_account_tree = PartialAccountTree::with_witnesses(
updated_accounts.iter().map(|(_, update_witness)| update_witness.to_witness()),
)
.map_err(|source| ProvenBlockError::AccountWitnessTracking { source })?;

// Check the account tree root in the previous block header matches the reconstructed tree's
// root.
Expand All @@ -288,13 +274,14 @@ fn compute_account_root(

// Second, update the account tree by inserting the new final account state commitments to
// compute the new root of the account tree.
// TODO: Move this loop into a method on `PartialAccountTree` once it is added.
for (account_id, witness) in updated_accounts {
let account_leaf_index = LeafIndex::from(*account_id);
partial_account_tree
.update_leaf(account_leaf_index.value(), Word::from(witness.final_state_commitment()))
.expect("every account leaf should have been inserted in the first loop");
}
// If an account ID's prefix already exists in the tree, this will return an error.
// Note that we have inserted all witnesses that we want to update into the partial account
// tree, so we should not run into the untracked key error.
partial_account_tree
.upsert_state_commitments(updated_accounts.iter().map(|(account_id, update_witness)| {
(*account_id, update_witness.final_state_commitment())
}))
.map_err(|source| ProvenBlockError::AccountIdPrefixDuplicate { source })?;

Ok(partial_account_tree.root())
}
Expand Down
219 changes: 212 additions & 7 deletions crates/miden-block-prover/src/tests/proven_block_error.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
use anyhow::Context;
use assert_matches::assert_matches;
use miden_crypto::merkle::MerkleError;
use miden_crypto::{EMPTY_WORD, Felt, FieldElement};
use miden_lib::transaction::TransactionKernel;
use miden_objects::{
NullifierTreeError,
AccountTreeError, Digest, NullifierTreeError,
account::{
Account, AccountBuilder, AccountId, AccountIdAnchor, StorageSlot,
delta::AccountUpdateDetails,
},
batch::ProvenBatch,
block::{BlockInputs, ProposedBlock},
block::{BlockInputs, BlockNumber, ProposedBlock},
testing::account_component::AccountMockComponent,
transaction::{ProvenTransaction, ProvenTransactionBuilder},
vm::ExecutionProof,
};
use miden_tx::testing::{MockChain, TransactionContextBuilder};
use winterfell::Proof;

use crate::{
LocalBlockProver, ProvenBlockError,
tests::utils::{
TestSetup, generate_batch, generate_executed_tx_with_authenticated_notes,
generate_tracked_note, setup_chain,
ProvenTransactionExt, TestSetup, generate_batch,
generate_executed_tx_with_authenticated_notes, generate_tracked_note, setup_chain,
},
};

Expand Down Expand Up @@ -186,8 +196,8 @@ fn proven_block_fails_on_account_tree_root_mismatch() -> anyhow::Result<()> {

assert_matches!(
error,
ProvenBlockError::AccountWitnessRootMismatch {
source: MerkleError::ConflictingRoots { .. },
ProvenBlockError::AccountWitnessTracking {
source: AccountTreeError::TreeRootConflict { .. },
..
}
);
Expand Down Expand Up @@ -240,3 +250,198 @@ fn proven_block_fails_on_nullifier_tree_root_mismatch() -> anyhow::Result<()> {

Ok(())
}

/// Tests that creating an account when an existing account with the same account ID prefix exists,
/// results in an error.
#[test]
fn proven_block_fails_on_creating_account_with_existing_account_id_prefix() -> anyhow::Result<()> {
// Construct a new account.
// --------------------------------------------------------------------------------------------

let mut mock_chain = MockChain::new();
let anchor_block = mock_chain.block_header(0);
let (account, seed) = AccountBuilder::new([5; 32])
.anchor(AccountIdAnchor::try_from(&anchor_block)?)
.with_component(
AccountMockComponent::new_with_slots(
TransactionKernel::testing_assembler(),
vec![StorageSlot::Value([5u32.into(); 4])],
)
.unwrap(),
)
.build()
.context("failed to build account")?;

let new_id = account.id();

// Construct a second account whose ID matches the prefix of the first and insert it into the
// chain, as if that account already existed. That way we can check if the block prover errors
// when we attempt to create the first account.
// --------------------------------------------------------------------------------------------

// Set some bits on the hash part of the suffix to make the account id distinct from the
// original one, but their prefix is still the same.
let existing_id = AccountId::try_from(u128::from(new_id) | 0xffff00)
.context("failed to convert account ID")?;

assert_eq!(
new_id.prefix(),
existing_id.prefix(),
"test requires that prefixes are the same"
);
assert_ne!(
new_id.suffix(),
existing_id.suffix(),
"test should work if suffixes are different, so we want to ensure it"
);
assert_eq!(account.init_commitment(), miden_objects::Digest::from(EMPTY_WORD));

let existing_account =
Account::mock(existing_id.into(), Felt::ZERO, TransactionKernel::testing_assembler());
mock_chain.add_pending_account(existing_account.clone());
mock_chain.seal_next_block();

// Execute the account-creating transaction.
// --------------------------------------------------------------------------------------------

let tx_inputs = mock_chain.get_transaction_inputs(account.clone(), Some(seed), &[], &[]);
let tx_context = TransactionContextBuilder::new(account)
.account_seed(Some(seed))
.tx_inputs(tx_inputs)
.build();
let tx = tx_context.execute().context("failed to execute account creating tx")?;
let tx =
ProvenTransaction::from_executed_transaction_mocked(tx, &mock_chain.latest_block_header());

let batch = generate_batch(&mut mock_chain, vec![tx]);
let batches = [batch];

let block_inputs = mock_chain.get_block_inputs(batches.iter());
// Sanity check: The mock chain account tree root should match the previous block header's
// account tree root.
assert_eq!(mock_chain.accounts().root(), block_inputs.prev_block_header().account_root());
assert_eq!(mock_chain.accounts().num_accounts(), 1);

// Sanity check: The block inputs should contain an account witness whose ID matches the
// existing ID.
assert_eq!(block_inputs.account_witnesses().len(), 1);
let witness = block_inputs
.account_witnesses()
.get(&new_id)
.context("block inputs did not contain witness for id")?;

// The witness should be for the **existing** account, because that's the one that exists in
// the tree and is therefore in the same SMT leaf that we would insert the new ID into.
assert_eq!(witness.id(), existing_id);
assert_eq!(witness.state_commitment(), existing_account.commitment());

let block = mock_chain.propose_block(batches).context("failed to propose block")?;

let err = LocalBlockProver::new(0).prove_without_batch_verification(block).unwrap_err();

// This should fail when we try to _insert_ the same two prefixes into the partial tree.
assert_matches!(
err,
ProvenBlockError::AccountIdPrefixDuplicate {
source: AccountTreeError::DuplicateIdPrefix { duplicate_prefix }
} if duplicate_prefix == new_id.prefix()
);

Ok(())
}

/// Tests that creating two accounts in the same block whose ID prefixes match, results in an error.
#[test]
fn proven_block_fails_on_creating_account_with_duplicate_account_id_prefix() -> anyhow::Result<()> {
// Construct a new account.
// --------------------------------------------------------------------------------------------

let mut mock_chain = MockChain::new();
let anchor_block = mock_chain.block_header(0);
let (account, _) = AccountBuilder::new([5; 32])
.anchor(AccountIdAnchor::try_from(&anchor_block)?)
.with_component(
AccountMockComponent::new_with_slots(
TransactionKernel::testing_assembler(),
vec![StorageSlot::Value([5u32.into(); 4])],
)
.unwrap(),
)
.build()
.context("failed to build account")?;

let id0 = account.id();

// Construct a second account whose ID matches the prefix of the first.
// --------------------------------------------------------------------------------------------

// Set some bits on the hash part of the suffix to make the account id distinct from the
// original one, but their prefix is still the same.
let id1 =
AccountId::try_from(u128::from(id0) | 0xffff00).context("failed to convert account ID")?;

assert_eq!(id0.prefix(), id1.prefix(), "test requires that prefixes are the same");
assert_ne!(
id0.suffix(),
id1.suffix(),
"test should work if suffixes are different, so we want to ensure it"
);

// Build two mocked proven transactions, each of which creates a new account and both share the
// same ID prefix but not the suffix.
// --------------------------------------------------------------------------------------------

let [tx0, tx1] =
[(id0, [0, 0, 0, 1u32]), (id1, [0, 0, 0, 2u32])].map(|(id, final_state_comm)| {
ProvenTransactionBuilder::new(
id,
Digest::default(),
Digest::from(final_state_comm),
anchor_block.block_num(),
anchor_block.commitment(),
BlockNumber::from(u32::MAX),
ExecutionProof::new(Proof::new_dummy(), Default::default()),
)
.account_update_details(AccountUpdateDetails::Private)
.build()
.unwrap()
});

// Build a batch from these transactions and attempt to prove a block.
// --------------------------------------------------------------------------------------------

let batch = generate_batch(&mut mock_chain, vec![tx0, tx1]);
let batches = [batch];

// Sanity check: The block inputs should contain two account witnesses that point to the same
// empty entry.
let block_inputs = mock_chain.get_block_inputs(batches.iter());
assert_eq!(block_inputs.account_witnesses().len(), 2);
let witness0 = block_inputs
.account_witnesses()
.get(&id0)
.context("block inputs did not contain witness for id0")?;
let witness1 = block_inputs
.account_witnesses()
.get(&id1)
.context("block inputs did not contain witness for id1")?;
assert_eq!(witness0.id(), id0);
assert_eq!(witness1.id(), id1);

assert_eq!(witness0.state_commitment(), Digest::default());
assert_eq!(witness1.state_commitment(), Digest::default());

let block = mock_chain.propose_block(batches).context("failed to propose block")?;

let err = LocalBlockProver::new(0).prove_without_batch_verification(block).unwrap_err();

// This should fail when we try to _track_ the same two prefixes in the partial tree.
assert_matches!(
err,
ProvenBlockError::AccountWitnessTracking {
source: AccountTreeError::DuplicateIdPrefix { duplicate_prefix }
} if duplicate_prefix == id0.prefix()
);

Ok(())
}
Loading
Loading