diff --git a/CHANGELOG.md b/CHANGELOG.md index c9f5ba057..1637ec794 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ - [BREAKING] Updated minimum Rust version to 1.84. - [BREAKING] `Endpoint` configuration simplified to a single string (#654). +### Enhancements + +- Prove transaction batches using Rust batch prover reference implementation (#659). + ## v0.7.2 (2025-01-29) ### Fixes diff --git a/Cargo.lock b/Cargo.lock index 2d8e3495c..5e6116ea9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -160,9 +160,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.85" +version = "0.1.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056" +checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" dependencies = [ "proc-macro2", "quote", @@ -425,9 +425,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" [[package]] name = "camino" @@ -463,9 +463,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.10" +version = "1.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229" +checksum = "e4730490333d58093109dc02c23174c3f4d490998c3fed3cc8e82d57afedb9cf" dependencies = [ "jobserver", "libc", @@ -524,9 +524,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.27" +version = "4.5.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796" +checksum = "3e77c3243bd94243c03672cb5154667347c457ca271254724f9f393aee1c05ff" dependencies = [ "clap_builder", "clap_derive", @@ -546,9 +546,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.24" +version = "4.5.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c" +checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" dependencies = [ "heck", "proc-macro2", @@ -677,9 +677,9 @@ dependencies = [ [[package]] name = "deadpool" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6541a3916932fe57768d4be0b1ffb5ec7cbf74ca8c903fdfd5c0fe8aa958f0ed" +checksum = "5ed5957ff93768adf7a65ab167a17835c3d2c3c50d084fe305174c112f468e2f" dependencies = [ "deadpool-runtime", "num_cpus", @@ -1782,9 +1782,8 @@ dependencies = [ [[package]] name = "miden-lib" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee8babd17ea380c6c5b948761ca63208b633b7130379ee2a57c6d3732d2f8bc" +version = "0.8.0" +source = "git+https://github.com/0xPolygonMiden/miden-base.git?rev=e82dee03de7589ef3fb12b7fd901cef25ae5535d#e82dee03de7589ef3fb12b7fd901cef25ae5535d" dependencies = [ "miden-assembly", "miden-objects", @@ -1874,6 +1873,7 @@ dependencies = [ "miden-processor", "miden-stdlib", "miden-tx", + "miden-tx-batch-prover", "pretty_assertions", "rand", "rand_chacha", @@ -1976,9 +1976,8 @@ dependencies = [ [[package]] name = "miden-objects" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fe3f10d0e3787176f0803be2ecb4646f3a17fe10af45a50736c8d079a3c94d8" +version = "0.8.0" +source = "git+https://github.com/0xPolygonMiden/miden-base.git?rev=e82dee03de7589ef3fb12b7fd901cef25ae5535d#e82dee03de7589ef3fb12b7fd901cef25ae5535d" dependencies = [ "getrandom 0.2.15", "miden-assembly", @@ -2036,9 +2035,8 @@ dependencies = [ [[package]] name = "miden-tx" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4371509f1e4c25dfe26b7ffcffbb34aaa152c6eaad400f2624240a941baed2d0" +version = "0.8.0" +source = "git+https://github.com/0xPolygonMiden/miden-base.git?rev=e82dee03de7589ef3fb12b7fd901cef25ae5535d#e82dee03de7589ef3fb12b7fd901cef25ae5535d" dependencies = [ "async-trait", "miden-lib", @@ -2052,6 +2050,19 @@ dependencies = [ "winter-maybe-async", ] +[[package]] +name = "miden-tx-batch-prover" +version = "0.8.0" +source = "git+https://github.com/0xPolygonMiden/miden-base.git?rev=e82dee03de7589ef3fb12b7fd901cef25ae5535d#e82dee03de7589ef3fb12b7fd901cef25ae5535d" +dependencies = [ + "miden-core", + "miden-crypto", + "miden-objects", + "miden-processor", + "miden-tx", + "thiserror 2.0.11", +] + [[package]] name = "miden-verifier" version = "0.12.0" @@ -2067,9 +2078,9 @@ dependencies = [ [[package]] name = "miette" -version = "7.4.0" +version = "7.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317f146e2eb7021892722af37cf1b971f0a70c8406f487e24952667616192c64" +checksum = "1a955165f87b37fd1862df2a59547ac542c77ef6d17c666f619d1ad22dd89484" dependencies = [ "cfg-if", "miette-derive", @@ -2079,9 +2090,9 @@ dependencies = [ [[package]] name = "miette-derive" -version = "7.4.0" +version = "7.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23c9b935fbe1d6cbd1dac857b54a688145e2d93f48db36010514d0f612d0ad67" +checksum = "bf45bf44ab49be92fd1227a3be6fc6f617f1a337c06af54981048574d8783147" dependencies = [ "proc-macro2", "quote", @@ -2450,27 +2461,27 @@ dependencies = [ [[package]] name = "phf_shared" -version = "0.10.0" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ "siphasher", ] [[package]] name = "pin-project" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e2ec53ad785f4d35dac0adea7f7dc6f1bb277ad84a680c7afefeae05d1f5916" +checksum = "dfe2e71e1471fe07709406bf725f710b02927c9c54b2b5b2ec0e8087d97c327d" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb" +checksum = "f6e859e6e5bd50440ab63c47e3ebabc90f26251f7c73c3d3e837b74a1cc3fa67" dependencies = [ "proc-macro2", "quote", @@ -3143,9 +3154,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "siphasher" -version = "0.3.11" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "slab" @@ -3203,12 +3214,11 @@ dependencies = [ [[package]] name = "string_cache" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +checksum = "938d512196766101d333398efde81bc1f37b00cb42c2f8350e5df639f040bbbe" dependencies = [ "new_debug_unreachable", - "once_cell", "parking_lot", "phf_shared", "precomputed-hash", @@ -3258,9 +3268,9 @@ checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" [[package]] name = "syn" -version = "2.0.96" +version = "2.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" dependencies = [ "proc-macro2", "quote", @@ -3531,9 +3541,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.22" +version = "0.22.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee" dependencies = [ "indexmap 2.7.1", "serde", @@ -3979,9 +3989,9 @@ dependencies = [ [[package]] name = "wait-timeout" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" dependencies = [ "libc", ] @@ -4342,9 +4352,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.25" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad699df48212c6cc6eb4435f35500ac6fd3b9913324f938aea302022ce19d310" +checksum = "86e376c75f4f43f44db463cf729e0d3acbf954d13e22c51e26e4c264b4ab545f" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index bdfa58e34..bbb10141a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,17 +28,18 @@ version = "0.8.0" assert_matches = { version = "1.5" } itertools = { version = "0.14" } miden-air = { version = "0.12" } -miden-lib = { version = "0.7" } +miden-lib = { git = "https://github.com/0xPolygonMiden/miden-base.git", rev = "e82dee03de7589ef3fb12b7fd901cef25ae5535d" } miden-node-block-producer = { path = "crates/block-producer", version = "0.8" } miden-node-proto = { path = "crates/proto", version = "0.8" } miden-node-rpc = { path = "crates/rpc", version = "0.8" } miden-node-store = { path = "crates/store", version = "0.8" } miden-node-test-macro = { path = "crates/test-macro" } miden-node-utils = { path = "crates/utils", version = "0.8" } -miden-objects = { version = "0.7" } +miden-objects = { git = "https://github.com/0xPolygonMiden/miden-base.git", rev = "e82dee03de7589ef3fb12b7fd901cef25ae5535d" } miden-processor = { version = "0.12" } miden-stdlib = { version = "0.12", default-features = false } -miden-tx = { version = "0.7" } +miden-tx = { git = "https://github.com/0xPolygonMiden/miden-base.git", rev = "e82dee03de7589ef3fb12b7fd901cef25ae5535d" } +miden-tx-batch-prover = { git = "https://github.com/0xPolygonMiden/miden-base.git", rev = "e82dee03de7589ef3fb12b7fd901cef25ae5535d" } prost = { version = "0.13" } rand = { version = "0.8" } thiserror = { version = "2.0", default-features = false } diff --git a/bin/faucet/src/client.rs b/bin/faucet/src/client.rs index faf9b17b5..cc3fa2618 100644 --- a/bin/faucet/src/client.rs +++ b/bin/faucet/src/client.rs @@ -9,7 +9,7 @@ use miden_node_proto::generated::{ rpc::api_client::ApiClient, }; use miden_objects::{ - account::{Account, AccountData, AccountId, AuthSecretKey}, + account::{Account, AccountFile, AccountId, AuthSecretKey}, asset::FungibleAsset, block::{BlockHeader, BlockNumber}, crypto::{ @@ -61,7 +61,7 @@ impl FaucetClient { let (mut rpc_api, root_block_header, root_chain_mmr) = initialize_faucet_client(config).await?; - let faucet_account_data = AccountData::read(&config.faucet_account_path) + let faucet_account_data = AccountFile::read(&config.faucet_account_path) .context("Failed to load faucet account from file")?; let id = faucet_account_data.account.id(); diff --git a/bin/faucet/src/main.rs b/bin/faucet/src/main.rs index f7ea8c91f..dc2d53384 100644 --- a/bin/faucet/src/main.rs +++ b/bin/faucet/src/main.rs @@ -19,7 +19,7 @@ use http::HeaderValue; use miden_lib::{account::faucets::create_basic_fungible_faucet, AuthScheme}; use miden_node_utils::{config::load_config, crypto::get_rpo_random_coin, version::LongVersion}; use miden_objects::{ - account::{AccountData, AccountStorageMode, AuthSecretKey}, + account::{AccountFile, AccountStorageMode, AuthSecretKey}, asset::TokenSymbol, crypto::dsa::rpo_falcon512::SecretKey, Felt, @@ -169,7 +169,7 @@ async fn main() -> anyhow::Result<()> { .context("Failed to create basic fungible faucet account")?; let account_data = - AccountData::new(account, Some(account_seed), AuthSecretKey::RpoFalcon512(secret)); + AccountFile::new(account, Some(account_seed), AuthSecretKey::RpoFalcon512(secret)); let output_path = current_dir.join(output_path); account_data diff --git a/bin/node/src/commands/genesis/mod.rs b/bin/node/src/commands/genesis/mod.rs index 08a65898b..a8e95087d 100644 --- a/bin/node/src/commands/genesis/mod.rs +++ b/bin/node/src/commands/genesis/mod.rs @@ -9,7 +9,7 @@ use miden_lib::{account::faucets::create_basic_fungible_faucet, AuthScheme}; use miden_node_store::genesis::GenesisState; use miden_node_utils::{config::load_config, crypto::get_rpo_random_coin}; use miden_objects::{ - account::{Account, AccountData, AccountIdAnchor, AuthSecretKey}, + account::{Account, AccountFile, AccountIdAnchor, AuthSecretKey}, asset::TokenSymbol, crypto::{dsa::rpo_falcon512::SecretKey, utils::Serializable}, Felt, ONE, @@ -134,7 +134,7 @@ fn create_accounts( ); faucet_count += 1; - (AccountData::new(account, Some(account_seed), auth_secret_key), name) + (AccountFile::new(account, Some(account_seed), auth_secret_key), name) }, }; @@ -182,7 +182,7 @@ mod tests { use figment::Jail; use miden_node_store::genesis::GenesisState; - use miden_objects::{account::AccountData, utils::serde::Deserializable}; + use miden_objects::{account::AccountFile, utils::serde::Deserializable}; use crate::DEFAULT_GENESIS_FILE_PATH; @@ -220,7 +220,7 @@ mod tests { assert!(a0_file_path.exists()); // deserialize account and genesis_state - let a0 = AccountData::read(a0_file_path).unwrap(); + let a0 = AccountFile::read(a0_file_path).unwrap(); // assert that the account has the corresponding storage mode assert!(a0.account.is_public()); diff --git a/crates/block-producer/Cargo.toml b/crates/block-producer/Cargo.toml index d01bfebeb..ccecfab44 100644 --- a/crates/block-producer/Cargo.toml +++ b/crates/block-producer/Cargo.toml @@ -18,23 +18,24 @@ workspace = true tracing-forest = ["miden-node-utils/tracing-forest"] [dependencies] -async-trait = { version = "0.1" } -itertools = { workspace = true } -miden-lib = { workspace = true } -miden-node-proto = { workspace = true } -miden-node-utils = { workspace = true } -miden-objects = { workspace = true } -miden-processor = { workspace = true } -miden-stdlib = { workspace = true } -miden-tx = { workspace = true } -rand = { version = "0.8" } -serde = { version = "1.0", features = ["derive"] } -thiserror = { workspace = true } -tokio = { workspace = true, features = ["macros", "net", "rt-multi-thread", "sync", "time"] } -tokio-stream = { workspace = true, features = ["net"] } -tonic = { workspace = true } -tracing = { workspace = true } -url = { workspace = true } +async-trait = { version = "0.1" } +itertools = { workspace = true } +miden-lib = { workspace = true } +miden-node-proto = { workspace = true } +miden-node-utils = { workspace = true } +miden-objects = { workspace = true } +miden-processor = { workspace = true } +miden-stdlib = { workspace = true } +miden-tx = { workspace = true } +miden-tx-batch-prover = { workspace = true } +rand = { version = "0.8" } +serde = { version = "1.0", features = ["derive"] } +thiserror = { workspace = true } +tokio = { workspace = true, features = ["macros", "net", "rt-multi-thread", "sync", "time"] } +tokio-stream = { workspace = true, features = ["net"] } +tonic = { workspace = true } +tracing = { workspace = true } +url = { workspace = true } [dev-dependencies] assert_matches = { workspace = true } diff --git a/crates/block-producer/src/batch_builder/batch.rs b/crates/block-producer/src/batch_builder/batch.rs deleted file mode 100644 index 33b27b058..000000000 --- a/crates/block-producer/src/batch_builder/batch.rs +++ /dev/null @@ -1,453 +0,0 @@ -use std::{ - borrow::Borrow, - collections::{btree_map::Entry, BTreeMap, BTreeSet}, - mem, -}; - -use miden_node_proto::domain::note::NoteAuthenticationInfo; -use miden_node_utils::formatting::format_blake3_digest; -use miden_objects::{ - account::{delta::AccountUpdateDetails, AccountId}, - batch::BatchNoteTree, - crypto::hash::blake::{Blake3Digest, Blake3_256}, - note::{NoteHeader, NoteId, Nullifier}, - transaction::{InputNoteCommitment, OutputNote, ProvenTransaction, TransactionId}, - AccountDeltaError, Digest, -}; -use tracing::instrument; - -use crate::{errors::BuildBatchError, COMPONENT}; - -// BATCH ID -// ================================================================================================ - -/// Uniquely identifies a [`TransactionBatch`]. -#[derive(Debug, Copy, Clone, Eq, Ord, PartialEq, PartialOrd)] -pub struct BatchId(Blake3Digest<32>); - -impl BatchId { - /// Calculates a batch ID from the given set of transactions. - pub fn compute(txs: impl Iterator) -> Self - where - T: Borrow, - { - let mut buf = Vec::with_capacity(32 * txs.size_hint().0); - for tx in txs { - buf.extend_from_slice(&tx.borrow().as_bytes()); - } - Self(Blake3_256::hash(&buf)) - } -} - -impl std::fmt::Display for BatchId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&format_blake3_digest(self.0)) - } -} - -// ACCOUNT UPDATE -// ================================================================================================ - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct AccountUpdate { - pub init_state: Digest, - pub final_state: Digest, - pub transactions: Vec, - pub details: AccountUpdateDetails, -} - -impl AccountUpdate { - fn new(tx: &ProvenTransaction) -> Self { - Self { - init_state: tx.account_update().init_state_hash(), - final_state: tx.account_update().final_state_hash(), - transactions: vec![tx.id()], - details: tx.account_update().details().clone(), - } - } - - /// Merges the transaction's update into this account update. - fn merge_tx(&mut self, tx: &ProvenTransaction) -> Result<(), AccountDeltaError> { - assert!( - self.final_state == tx.account_update().init_state_hash(), - "Transacion's initial state does not match current account state" - ); - - self.final_state = tx.account_update().final_state_hash(); - self.transactions.push(tx.id()); - self.details = self.details.clone().merge(tx.account_update().details().clone())?; - - Ok(()) - } -} - -// TRANSACTION BATCH -// ================================================================================================ - -/// A batch of transactions that share a common proof. -/// -/// Note: Until recursive proofs are available in the Miden VM, we don't include the common proof. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct TransactionBatch { - id: BatchId, - updated_accounts: BTreeMap, - input_notes: Vec, - output_notes_smt: BatchNoteTree, - output_notes: Vec, -} - -impl TransactionBatch { - // CONSTRUCTORS - // -------------------------------------------------------------------------------------------- - - /// Returns a new [TransactionBatch] built from the provided transactions. If a map of - /// unauthenticated notes found in the store is provided, it is used for transforming - /// unauthenticated notes into authenticated notes. - /// - /// The tx input takes an `IntoIterator` of a reference, which effectively allows for cheap - /// cloning of the iterator. Or put differently, we want something similar to `impl - /// Iterator + Clone` which this provides. - /// - /// # Errors - /// - /// Returns an error if: - /// - There are duplicated output notes or unauthenticated notes found across all transactions - /// in the batch. - /// - Hashes for corresponding input notes and output notes don't match. - #[instrument(target = COMPONENT, name = "new_batch", skip_all, err)] - pub fn new<'a, I>( - txs: impl IntoIterator, - found_unauthenticated_notes: NoteAuthenticationInfo, - ) -> Result - where - I: Iterator + Clone, - { - let tx_iter = txs.into_iter(); - let id = BatchId::compute(tx_iter.clone().map(ProvenTransaction::id)); - - // Populate batch output notes and updated accounts. - let mut output_notes = OutputNoteTracker::new(tx_iter.clone())?; - let mut updated_accounts = BTreeMap::::new(); - let mut unauthenticated_input_notes = BTreeSet::new(); - for tx in tx_iter.clone() { - // Merge account updates so that state transitions A->B->C become A->C. - match updated_accounts.entry(tx.account_id()) { - Entry::Vacant(vacant) => { - vacant.insert(AccountUpdate::new(tx)); - }, - Entry::Occupied(occupied) => { - occupied.into_mut().merge_tx(tx).map_err(|source| { - BuildBatchError::AccountUpdateError { account_id: tx.account_id(), source } - })?; - }, - }; - - // Check unauthenticated input notes for duplicates: - for note in tx.get_unauthenticated_notes() { - let id = note.id(); - if !unauthenticated_input_notes.insert(id) { - return Err(BuildBatchError::DuplicateUnauthenticatedNote(id)); - } - } - } - - // Populate batch produced nullifiers and match output notes with corresponding - // unauthenticated input notes in the same batch, which are removed from the unauthenticated - // input notes set. - // - // One thing to note: - // This still allows transaction `A` to consume an unauthenticated note `x` and output note - // `y` and for transaction `B` to consume an unauthenticated note `y` and output - // note `x` (i.e., have a circular dependency between transactions), but this is not - // a problem. - let mut input_notes = vec![]; - for tx in tx_iter { - for input_note in tx.input_notes().iter() { - // Header is presented only for unauthenticated input notes. - let input_note = match input_note.header() { - Some(input_note_header) => { - if output_notes.remove_note(input_note_header)? { - continue; - } - - // If an unauthenticated note was found in the store, transform it to an - // authenticated one (i.e. erase additional note details - // except the nullifier) - if found_unauthenticated_notes.contains_note(&input_note_header.id()) { - InputNoteCommitment::from(input_note.nullifier()) - } else { - input_note.clone() - } - }, - None => input_note.clone(), - }; - input_notes.push(input_note); - } - } - - let output_notes = output_notes.into_notes(); - - // Build the output notes SMT. - let output_notes_smt = BatchNoteTree::with_contiguous_leaves( - output_notes.iter().map(|note| (note.id(), note.metadata())), - ) - .expect("Unreachable: fails only if the output note list contains duplicates"); - - Ok(Self { - id, - updated_accounts, - input_notes, - output_notes_smt, - output_notes, - }) - } - - // PUBLIC ACCESSORS - // -------------------------------------------------------------------------------------------- - - /// Returns the batch ID. - pub fn id(&self) -> BatchId { - self.id - } - - /// Returns an iterator over (`account_id`, `init_state_hash`) tuples for accounts that were - /// modified in this transaction batch. - #[cfg(test)] - pub fn account_initial_states(&self) -> impl Iterator + '_ { - self.updated_accounts - .iter() - .map(|(&account_id, update)| (account_id, update.init_state)) - } - - /// Returns an iterator over (`account_id`, details, `new_state_hash`) tuples for accounts that - /// were modified in this transaction batch. - pub fn updated_accounts(&self) -> impl Iterator + '_ { - self.updated_accounts.iter() - } - - /// Returns input notes list consumed by the transactions in this batch. Any unauthenticated - /// input notes which have matching output notes within this batch are not included in this - /// list. - pub fn input_notes(&self) -> &[InputNoteCommitment] { - &self.input_notes - } - - /// Returns an iterator over produced nullifiers for all consumed notes. - pub fn produced_nullifiers(&self) -> impl Iterator + '_ { - self.input_notes.iter().map(InputNoteCommitment::nullifier) - } - - /// Returns the root hash of the output notes SMT. - pub fn output_notes_root(&self) -> Digest { - self.output_notes_smt.root() - } - - /// Returns output notes list. - pub fn output_notes(&self) -> &Vec { - &self.output_notes - } -} - -#[derive(Debug)] -struct OutputNoteTracker { - output_notes: Vec>, - output_note_index: BTreeMap, -} - -impl OutputNoteTracker { - fn new<'a>(txs: impl Iterator) -> Result { - let mut output_notes = vec![]; - let mut output_note_index = BTreeMap::new(); - for tx in txs { - for note in tx.output_notes().iter() { - if output_note_index.insert(note.id(), output_notes.len()).is_some() { - return Err(BuildBatchError::DuplicateOutputNote(note.id())); - } - output_notes.push(Some(note.clone())); - } - } - - Ok(Self { output_notes, output_note_index }) - } - - pub fn remove_note(&mut self, input_note_header: &NoteHeader) -> Result { - let id = input_note_header.id(); - if let Some(note_index) = self.output_note_index.remove(&id) { - if let Some(output_note) = mem::take(&mut self.output_notes[note_index]) { - let input_hash = input_note_header.hash(); - let output_hash = output_note.hash(); - if output_hash != input_hash { - return Err(BuildBatchError::NoteHashesMismatch { - id, - input_hash, - output_hash, - }); - } - - return Ok(true); - } - } - - Ok(false) - } - - pub fn into_notes(self) -> Vec { - self.output_notes.into_iter().flatten().collect() - } -} - -// TESTS -// ================================================================================================ - -#[cfg(test)] -mod tests { - use miden_objects::note::NoteInclusionProof; - use miden_processor::crypto::MerklePath; - - use super::*; - use crate::test_utils::{ - mock_proven_tx, - note::{mock_note, mock_output_note, mock_unauthenticated_note_commitment}, - }; - - #[test] - fn output_note_tracker_duplicate_output_notes() { - let mut txs = mock_proven_txs(); - - let result = OutputNoteTracker::new(txs.iter()); - assert!( - result.is_ok(), - "Creation of output note tracker was not expected to fail: {result:?}" - ); - - let duplicate_output_note = txs[1].output_notes().get_note(1).clone(); - - txs.push(mock_proven_tx( - 3, - vec![], - vec![duplicate_output_note.clone(), mock_output_note(8), mock_output_note(4)], - )); - - match OutputNoteTracker::new(txs.iter()) { - Err(BuildBatchError::DuplicateOutputNote(note_id)) => { - assert_eq!(note_id, duplicate_output_note.id()); - }, - res => panic!("Unexpected result: {res:?}"), - } - } - - #[test] - fn output_note_tracker_remove_in_place_consumed_note() { - let txs = mock_proven_txs(); - let mut tracker = OutputNoteTracker::new(txs.iter()).unwrap(); - - let note_to_remove = mock_note(4); - - assert!(tracker.remove_note(note_to_remove.header()).unwrap()); - assert!(!tracker.remove_note(note_to_remove.header()).unwrap()); - - // Check that output notes are in the expected order and consumed note was removed - assert_eq!( - tracker.into_notes(), - vec![ - mock_output_note(2), - mock_output_note(3), - mock_output_note(6), - mock_output_note(7), - mock_output_note(8), - ] - ); - } - - #[test] - fn duplicate_unauthenticated_notes() { - let mut txs = mock_proven_txs(); - let duplicate_note = mock_note(5); - txs.push(mock_proven_tx(4, vec![duplicate_note.clone()], vec![mock_output_note(9)])); - match TransactionBatch::new(&txs, NoteAuthenticationInfo::default()) { - Err(BuildBatchError::DuplicateUnauthenticatedNote(note_id)) => { - assert_eq!(note_id, duplicate_note.id()); - }, - res => panic!("Unexpected result: {res:?}"), - } - } - - #[test] - fn consume_notes_in_place() { - let mut txs = mock_proven_txs(); - let note_to_consume = mock_note(3); - txs.push(mock_proven_tx( - 3, - vec![mock_note(11), note_to_consume, mock_note(13)], - vec![mock_output_note(9), mock_output_note(10)], - )); - - let batch = TransactionBatch::new(&txs, NoteAuthenticationInfo::default()).unwrap(); - - // One of the unauthenticated notes must be removed from the batch due to the consumption - // of the corresponding output note - let expected_input_notes = vec![ - mock_unauthenticated_note_commitment(1), - mock_unauthenticated_note_commitment(5), - mock_unauthenticated_note_commitment(11), - mock_unauthenticated_note_commitment(13), - ]; - assert_eq!(batch.input_notes, expected_input_notes); - - // One of the output notes must be removed from the batch due to the consumption - // by the corresponding unauthenticated note - let expected_output_notes = vec![ - mock_output_note(2), - mock_output_note(4), - mock_output_note(6), - mock_output_note(7), - mock_output_note(8), - mock_output_note(9), - mock_output_note(10), - ]; - assert_eq!(batch.output_notes.len(), expected_output_notes.len()); - assert_eq!(batch.output_notes, expected_output_notes); - - // Ensure all nullifiers match the corresponding input notes' nullifiers - let expected_nullifiers: Vec<_> = - batch.input_notes().iter().map(InputNoteCommitment::nullifier).collect(); - let actual_nullifiers: Vec<_> = batch.produced_nullifiers().collect(); - assert_eq!(actual_nullifiers, expected_nullifiers); - } - - #[test] - fn convert_unauthenticated_note_to_authenticated() { - let txs = mock_proven_txs(); - let found_unauthenticated_notes = BTreeMap::from_iter([( - mock_note(5).id(), - NoteInclusionProof::new(0.into(), 0, MerklePath::default()).unwrap(), - )]); - let found_unauthenticated_notes = NoteAuthenticationInfo { - note_proofs: found_unauthenticated_notes, - block_proofs: Vec::default(), - }; - let batch = TransactionBatch::new(&txs, found_unauthenticated_notes).unwrap(); - - let expected_input_notes = - vec![mock_unauthenticated_note_commitment(1), mock_note(5).nullifier().into()]; - assert_eq!(batch.input_notes, expected_input_notes); - } - - // UTILITIES - // ============================================================================================= - - fn mock_proven_txs() -> Vec { - vec![ - mock_proven_tx( - 1, - vec![mock_note(1)], - vec![mock_output_note(2), mock_output_note(3), mock_output_note(4)], - ), - mock_proven_tx( - 2, - vec![mock_note(5)], - vec![mock_output_note(6), mock_output_note(7), mock_output_note(8)], - ), - ] - } -} diff --git a/crates/block-producer/src/batch_builder/mod.rs b/crates/block-producer/src/batch_builder/mod.rs index 68fae021d..550dcb736 100644 --- a/crates/block-producer/src/batch_builder/mod.rs +++ b/crates/block-producer/src/batch_builder/mod.rs @@ -1,22 +1,21 @@ use std::{num::NonZeroUsize, ops::Range, time::Duration}; -use batch::BatchId; -use miden_node_proto::domain::note::NoteAuthenticationInfo; +use miden_node_proto::domain::batch::BatchInputs; +use miden_node_utils::formatting::format_array; +use miden_objects::{ + batch::{BatchId, ProposedBatch, ProvenBatch}, + MIN_PROOF_SECURITY_LEVEL, +}; +use miden_tx_batch_prover::LocalBatchProver; use rand::Rng; use tokio::{task::JoinSet, time}; use tracing::{debug, info, instrument, Span}; use crate::{ - domain::transaction::AuthenticatedTransaction, mempool::SharedMempool, store::StoreClient, - COMPONENT, SERVER_BUILD_BATCH_FREQUENCY, + domain::transaction::AuthenticatedTransaction, errors::BuildBatchError, mempool::SharedMempool, + store::StoreClient, COMPONENT, SERVER_BUILD_BATCH_FREQUENCY, }; -pub mod batch; -pub use batch::TransactionBatch; -use miden_node_utils::formatting::format_array; - -use crate::errors::BuildBatchError; - // BATCH BUILDER // ================================================================================================ @@ -105,7 +104,7 @@ impl BatchBuilder { // BATCH WORKER // ================================================================================================ -type BatchResult = Result; +type BatchResult = Result; /// Represents a pool of batch provers. /// @@ -219,15 +218,19 @@ impl WorkerPool { async move { tracing::debug!("Begin proving batch."); - let inputs = store - .get_batch_inputs( - transactions - .iter() - .flat_map(AuthenticatedTransaction::unauthenticated_notes), - ) + let block_references = + transactions.iter().map(AuthenticatedTransaction::reference_block); + let unauthenticated_notes = transactions + .iter() + .flat_map(AuthenticatedTransaction::unauthenticated_notes); + + let batch_inputs = store + .get_batch_inputs(block_references, unauthenticated_notes) .await .map_err(|err| (id, BuildBatchError::FetchBatchInputsFailed(err)))?; - let batch = Self::build_batch(transactions, inputs).map_err(|err| (id, err))?; + + let batch = + Self::build_batch(transactions, batch_inputs).map_err(|err| (id, err))?; tokio::time::sleep(simulated_proof_time).await; if failed { @@ -250,19 +253,35 @@ impl WorkerPool { #[instrument(target = COMPONENT, skip_all, err, fields(batch_id))] fn build_batch( txs: Vec, - inputs: NoteAuthenticationInfo, - ) -> Result { + batch_inputs: BatchInputs, + ) -> Result { let num_txs = txs.len(); info!(target: COMPONENT, num_txs, "Building a transaction batch"); debug!(target: COMPONENT, txs = %format_array(txs.iter().map(|tx| tx.id().to_hex()))); - let txs = txs.iter().map(AuthenticatedTransaction::raw_proven_transaction); - let batch = TransactionBatch::new(txs, inputs)?; + let BatchInputs { + batch_reference_block_header, + note_proofs, + chain_mmr, + } = batch_inputs; + + let transactions = txs.iter().map(AuthenticatedTransaction::proven_transaction).collect(); + + let proposed_batch = + ProposedBatch::new(transactions, batch_reference_block_header, chain_mmr, note_proofs) + .map_err(BuildBatchError::ProposeBatchError)?; + + Span::current().record("batch_id", proposed_batch.id().to_string()); + info!(target: COMPONENT, "Proposed Batch built"); + + let proven_batch = LocalBatchProver::new(MIN_PROOF_SECURITY_LEVEL) + .prove(proposed_batch) + .map_err(BuildBatchError::ProveBatchError)?; - Span::current().record("batch_id", batch.id().to_string()); - info!(target: COMPONENT, "Transaction batch built"); + Span::current().record("batch_id", proven_batch.id().to_string()); + info!(target: COMPONENT, "Proven Batch built"); - Ok(batch) + Ok(proven_batch) } } diff --git a/crates/block-producer/src/block_builder/mod.rs b/crates/block-producer/src/block_builder/mod.rs index 32023a58f..21790dffc 100644 --- a/crates/block-producer/src/block_builder/mod.rs +++ b/crates/block-producer/src/block_builder/mod.rs @@ -3,6 +3,7 @@ use std::{collections::BTreeSet, ops::Range}; use miden_node_utils::formatting::format_array; use miden_objects::{ account::AccountId, + batch::ProvenBatch, block::Block, note::{NoteHeader, Nullifier}, transaction::{InputNoteCommitment, OutputNote}, @@ -12,8 +13,8 @@ use tokio::time::Duration; use tracing::{debug, info, instrument}; use crate::{ - batch_builder::batch::TransactionBatch, errors::BuildBlockError, mempool::SharedMempool, - store::StoreClient, COMPONENT, SERVER_BLOCK_FREQUENCY, + errors::BuildBlockError, mempool::SharedMempool, store::StoreClient, COMPONENT, + SERVER_BLOCK_FREQUENCY, }; pub(crate) mod prover; @@ -94,34 +95,36 @@ impl BlockBuilder { } #[instrument(target = COMPONENT, skip_all, err)] - async fn build_block(&self, batches: &[TransactionBatch]) -> Result<(), BuildBlockError> { + async fn build_block(&self, batches: &[ProvenBatch]) -> Result<(), BuildBlockError> { info!( target: COMPONENT, num_batches = batches.len(), - batches = %format_array(batches.iter().map(TransactionBatch::id)), + batches = %format_array(batches.iter().map(ProvenBatch::id)), ); let updated_account_set: BTreeSet = batches .iter() - .flat_map(TransactionBatch::updated_accounts) + .flat_map(ProvenBatch::account_updates) .map(|(account_id, _)| *account_id) .collect(); let output_notes: Vec<_> = - batches.iter().map(TransactionBatch::output_notes).cloned().collect(); + batches.iter().map(|batch| batch.output_notes().to_vec()).collect(); let produced_nullifiers: Vec = - batches.iter().flat_map(TransactionBatch::produced_nullifiers).collect(); + batches.iter().flat_map(ProvenBatch::produced_nullifiers).collect(); // Populate set of output notes from all batches - let output_notes_set: BTreeSet<_> = - output_notes.iter().flat_map(|batch| batch.iter().map(OutputNote::id)).collect(); + let output_notes_set: BTreeSet<_> = output_notes + .iter() + .flat_map(|output_notes| output_notes.iter().map(OutputNote::id)) + .collect(); // Build a set of unauthenticated input notes for this block which do not have a matching // output note produced in this block let dangling_notes: BTreeSet<_> = batches .iter() - .flat_map(TransactionBatch::input_notes) + .flat_map(ProvenBatch::input_notes) .filter_map(InputNoteCommitment::header) .map(NoteHeader::id) .filter(|note_id| !output_notes_set.contains(note_id)) diff --git a/crates/block-producer/src/block_builder/prover/block_witness.rs b/crates/block-producer/src/block_builder/prover/block_witness.rs index 9f453b598..a0972e541 100644 --- a/crates/block-producer/src/block_builder/prover/block_witness.rs +++ b/crates/block-producer/src/block_builder/prover/block_witness.rs @@ -2,6 +2,7 @@ use std::collections::{BTreeMap, BTreeSet}; use miden_objects::{ account::{delta::AccountUpdateDetails, AccountId}, + batch::{BatchAccountUpdate, ProvenBatch}, block::{BlockAccountUpdate, BlockHeader}, crypto::merkle::{EmptySubtreeRoots, MerklePath, MerkleStore, MmrPeaks, SmtProof}, note::Nullifier, @@ -11,7 +12,6 @@ use miden_objects::{ }; use crate::{ - batch_builder::batch::{AccountUpdate, TransactionBatch}, block::BlockInputs, errors::{BlockProverError, BuildBlockError}, }; @@ -33,7 +33,7 @@ pub struct BlockWitness { impl BlockWitness { pub fn new( mut block_inputs: BlockInputs, - batches: &[TransactionBatch], + batches: &[ProvenBatch], ) -> Result<(Self, Vec), BuildBlockError> { // This limit should be enforced by the mempool. assert!(batches.len() <= MAX_BATCHES_PER_BLOCK); @@ -44,18 +44,19 @@ impl BlockWitness { .iter() .enumerate() .filter(|(_, batch)| !batch.output_notes().is_empty()) - .map(|(batch_index, batch)| (batch_index, batch.output_notes_root())) + .map(|(batch_index, batch)| (batch_index, batch.output_notes_tree().root())) .collect(); // Order account updates by account ID and each update's initial state hash. // // This let's us chronologically order the updates per account across batches. - let mut updated_accounts = BTreeMap::>::new(); - for (account_id, update) in batches.iter().flat_map(TransactionBatch::updated_accounts) { + let mut updated_accounts = + BTreeMap::>::new(); + for (account_id, update) in batches.iter().flat_map(ProvenBatch::account_updates) { updated_accounts .entry(*account_id) .or_default() - .insert(update.init_state, update.clone()); + .insert(update.initial_state_commitment(), update.clone()); } // Build account witnesses. @@ -84,12 +85,13 @@ impl BlockWitness { ) })?; - transactions.extend(update.transactions); - current_hash = update.final_state; + current_hash = update.final_state_commitment(); + let (update_transactions, update_details) = update.into_parts(); + transactions.extend(update_transactions); details = Some(match details { - None => update.details, - Some(details) => details.merge(update.details).map_err(|source| { + None => update_details, + Some(details) => details.merge(update_details).map_err(|source| { BuildBlockError::AccountUpdateError { account_id, source } })?, }); @@ -156,13 +158,13 @@ impl BlockWitness { /// done in MASM. fn validate_nullifiers( block_inputs: &BlockInputs, - batches: &[TransactionBatch], + batches: &[ProvenBatch], ) -> Result<(), BuildBlockError> { let produced_nullifiers_from_store: BTreeSet = block_inputs.nullifiers.keys().copied().collect(); let produced_nullifiers_from_batches: BTreeSet = - batches.iter().flat_map(TransactionBatch::produced_nullifiers).collect(); + batches.iter().flat_map(ProvenBatch::produced_nullifiers).collect(); if produced_nullifiers_from_store == produced_nullifiers_from_batches { Ok(()) diff --git a/crates/block-producer/src/block_builder/prover/tests.rs b/crates/block-producer/src/block_builder/prover/tests.rs index d688cdcfd..f020faf62 100644 --- a/crates/block-producer/src/block_builder/prover/tests.rs +++ b/crates/block-producer/src/block_builder/prover/tests.rs @@ -6,6 +6,7 @@ use miden_objects::{ account::{ delta::AccountUpdateDetails, AccountId, AccountIdVersion, AccountStorageMode, AccountType, }, + batch::ProvenBatch, block::{BlockAccountUpdate, BlockNoteIndex, BlockNoteTree, BlockNumber}, crypto::merkle::{ EmptySubtreeRoots, LeafIndex, MerklePath, Mmr, MmrPeaks, Smt, SmtLeaf, SmtProof, SMT_DEPTH, @@ -21,9 +22,9 @@ use miden_objects::{ use self::block_witness::AccountUpdateWitness; use super::*; use crate::{ - batch_builder::batch::TransactionBatch, block::{AccountWitness, BlockInputs}, test_utils::{ + batch::TransactionBatchConstructor, block::{build_actual_block_header, build_expected_block_header, MockBlockBuilder}, MockProvenTxBuilder, MockStoreSuccessBuilder, }, @@ -75,7 +76,7 @@ fn block_witness_validation_inconsistent_account_ids() { } }; - let batches: Vec = { + let batches: Vec = { let batch_1 = { let tx = MockProvenTxBuilder::with_account( account_id_2, @@ -84,7 +85,7 @@ fn block_witness_validation_inconsistent_account_ids() { ) .build(); - TransactionBatch::new([&tx], NoteAuthenticationInfo::default()).unwrap() + ProvenBatch::mocked_from_transactions([&tx]) }; let batch_2 = { @@ -95,7 +96,7 @@ fn block_witness_validation_inconsistent_account_ids() { ) .build(); - TransactionBatch::new([&tx], NoteAuthenticationInfo::default()).unwrap() + ProvenBatch::mocked_from_transactions([&tx]) }; vec![batch_1, batch_2] @@ -146,26 +147,19 @@ fn block_witness_validation_inconsistent_account_hashes() { }; let batches = { - let batch_1 = TransactionBatch::new( - [&MockProvenTxBuilder::with_account( - account_id_1, - account_1_hash_batches, - Digest::default(), - ) - .build()], - NoteAuthenticationInfo::default(), + let batch_1 = ProvenBatch::mocked_from_transactions([&MockProvenTxBuilder::with_account( + account_id_1, + account_1_hash_batches, + Digest::default(), ) - .unwrap(); - let batch_2 = TransactionBatch::new( - [&MockProvenTxBuilder::with_account( - account_id_2, - Digest::default(), - Digest::default(), - ) - .build()], - NoteAuthenticationInfo::default(), + .build()]); + + let batch_2 = ProvenBatch::mocked_from_transactions([&MockProvenTxBuilder::with_account( + account_id_2, + Digest::default(), + Digest::default(), ) - .unwrap(); + .build()]); vec![batch_1, batch_2] }; @@ -248,12 +242,8 @@ fn block_witness_multiple_batches_per_account() { }; let batches = { - let batch_1 = - TransactionBatch::new([&x_txs[0], &y_txs[1]], NoteAuthenticationInfo::default()) - .unwrap(); - let batch_2 = - TransactionBatch::new([&y_txs[0], &x_txs[1]], NoteAuthenticationInfo::default()) - .unwrap(); + let batch_1 = ProvenBatch::mocked_from_transactions([&x_txs[0], &y_txs[1]]); + let batch_2 = ProvenBatch::mocked_from_transactions([&y_txs[0], &x_txs[1]]); vec![batch_1, batch_2] }; @@ -360,7 +350,7 @@ async fn compute_account_root_success() { .await .unwrap(); - let batches: Vec = { + let batches: Vec = { let txs: Vec<_> = account_ids .iter() .enumerate() @@ -374,8 +364,8 @@ async fn compute_account_root_success() { }) .collect(); - let batch_1 = TransactionBatch::new(&txs[..2], NoteAuthenticationInfo::default()).unwrap(); - let batch_2 = TransactionBatch::new(&txs[2..], NoteAuthenticationInfo::default()).unwrap(); + let batch_1 = ProvenBatch::mocked_from_transactions(&txs[..2]); + let batch_2 = ProvenBatch::mocked_from_transactions(&txs[2..]); vec![batch_1, batch_2] }; @@ -510,7 +500,7 @@ async fn compute_note_root_empty_batches_success() { .await .unwrap(); - let batches: Vec = Vec::new(); + let batches: Vec = Vec::new(); let (block_witness, _) = BlockWitness::new(block_inputs_from_store, &batches).unwrap(); @@ -542,8 +532,8 @@ async fn compute_note_root_empty_notes_success() { .await .unwrap(); - let batches: Vec = { - let batch = TransactionBatch::new(vec![], NoteAuthenticationInfo::default()).unwrap(); + let batches: Vec = { + let batch = ProvenBatch::mocked_from_transactions(vec![]); vec![batch] }; @@ -620,7 +610,7 @@ async fn compute_note_root_success() { .await .unwrap(); - let batches: Vec = { + let batches: Vec = { let txs: Vec<_> = notes_created .iter() .zip(account_ids.iter()) @@ -632,8 +622,8 @@ async fn compute_note_root_success() { }) .collect(); - let batch_1 = TransactionBatch::new(&txs[..2], NoteAuthenticationInfo::default()).unwrap(); - let batch_2 = TransactionBatch::new(&txs[2..], NoteAuthenticationInfo::default()).unwrap(); + let batch_1 = ProvenBatch::mocked_from_transactions(&txs[..2]); + let batch_2 = ProvenBatch::mocked_from_transactions(&txs[2..]); vec![batch_1, batch_2] }; @@ -686,17 +676,17 @@ async fn compute_note_root_success() { /// The transaction batches will contain nullifiers 1 & 2, while the store will contain 2 & 3. #[test] fn block_witness_validation_inconsistent_nullifiers() { - let batches: Vec = { + let batches: Vec = { let batch_1 = { let tx = MockProvenTxBuilder::with_account_index(0).nullifiers_range(0..1).build(); - TransactionBatch::new([&tx], NoteAuthenticationInfo::default()).unwrap() + ProvenBatch::mocked_from_transactions([&tx]) }; let batch_2 = { let tx = MockProvenTxBuilder::with_account_index(1).nullifiers_range(1..2).build(); - TransactionBatch::new([&tx], NoteAuthenticationInfo::default()).unwrap() + ProvenBatch::mocked_from_transactions([&tx]) }; vec![batch_1, batch_2] @@ -713,7 +703,12 @@ fn block_witness_validation_inconsistent_nullifiers() { let accounts = batches .iter() - .flat_map(TransactionBatch::account_initial_states) + .flat_map(|batch| { + batch + .account_updates() + .iter() + .map(|(account_id, update)| (*account_id, update.initial_state_commitment())) + }) .map(|(account_id, hash)| { (account_id, AccountWitness { hash, proof: MerklePath::default() }) }) @@ -765,17 +760,17 @@ fn block_witness_validation_inconsistent_nullifiers() { /// in the transaction #[tokio::test] async fn compute_nullifier_root_empty_success() { - let batches: Vec = { + let batches: Vec = { let batch_1 = { let tx = MockProvenTxBuilder::with_account_index(0).build(); - TransactionBatch::new([&tx], NoteAuthenticationInfo::default()).unwrap() + ProvenBatch::mocked_from_transactions([&tx]) }; let batch_2 = { let tx = MockProvenTxBuilder::with_account_index(1).build(); - TransactionBatch::new([&tx], NoteAuthenticationInfo::default()).unwrap() + ProvenBatch::mocked_from_transactions([&tx]) }; vec![batch_1, batch_2] @@ -783,7 +778,12 @@ async fn compute_nullifier_root_empty_success() { let account_ids: Vec = batches .iter() - .flat_map(TransactionBatch::account_initial_states) + .flat_map(|batch| { + batch + .account_updates() + .iter() + .map(|(account_id, update)| (*account_id, update.initial_state_commitment())) + }) .map(|(account_id, _)| account_id) .collect(); @@ -819,17 +819,17 @@ async fn compute_nullifier_root_empty_success() { /// present in the transaction #[tokio::test] async fn compute_nullifier_root_success() { - let batches: Vec = { + let batches: Vec = { let batch_1 = { let tx = MockProvenTxBuilder::with_account_index(0).nullifiers_range(0..1).build(); - TransactionBatch::new([&tx], NoteAuthenticationInfo::default()).unwrap() + ProvenBatch::mocked_from_transactions([&tx]) }; let batch_2 = { let tx = MockProvenTxBuilder::with_account_index(1).nullifiers_range(1..2).build(); - TransactionBatch::new([&tx], NoteAuthenticationInfo::default()).unwrap() + ProvenBatch::mocked_from_transactions([&tx]) }; vec![batch_1, batch_2] @@ -837,7 +837,12 @@ async fn compute_nullifier_root_success() { let account_ids: Vec = batches .iter() - .flat_map(TransactionBatch::account_initial_states) + .flat_map(|batch| { + batch + .account_updates() + .iter() + .map(|(account_id, update)| (*account_id, update.initial_state_commitment())) + }) .map(|(account_id, _)| account_id) .collect(); diff --git a/crates/block-producer/src/domain/transaction.rs b/crates/block-producer/src/domain/transaction.rs index 10c2f236d..c249709ed 100644 --- a/crates/block-producer/src/domain/transaction.rs +++ b/crates/block-producer/src/domain/transaction.rs @@ -101,6 +101,10 @@ impl AuthenticatedTransaction { self.inner.input_notes().num_notes() } + pub fn reference_block(&self) -> (BlockNumber, Digest) { + (self.inner.block_num(), self.inner.block_ref()) + } + /// Notes which were unauthenticate in the transaction __and__ which were /// not authenticated by the store inputs. pub fn unauthenticated_notes(&self) -> impl Iterator + '_ { @@ -111,6 +115,11 @@ impl AuthenticatedTransaction { .filter(|note_id| !self.notes_authenticated_by_store.contains(note_id)) } + pub fn proven_transaction(&self) -> Arc { + Arc::clone(&self.inner) + } + + #[cfg(test)] pub fn raw_proven_transaction(&self) -> &ProvenTransaction { &self.inner } diff --git a/crates/block-producer/src/errors.rs b/crates/block-producer/src/errors.rs index 785de16a2..f61cf6dc2 100644 --- a/crates/block-producer/src/errors.rs +++ b/crates/block-producer/src/errors.rs @@ -6,9 +6,10 @@ use miden_objects::{ crypto::merkle::MerkleError, note::{NoteId, Nullifier}, transaction::TransactionId, - AccountDeltaError, Digest, + AccountDeltaError, Digest, ProposedBatchError, }; use miden_processor::ExecutionError; +use miden_tx_batch_prover::errors::BatchProveError; use thiserror::Error; use tokio::task::JoinError; @@ -127,25 +128,6 @@ impl From for tonic::Status { /// Error encountered while building a batch. #[derive(Debug, Error)] pub enum BuildBatchError { - #[error("duplicated unauthenticated transaction input note ID in the batch: {0}")] - DuplicateUnauthenticatedNote(NoteId), - - #[error("duplicated transaction output note ID in the batch: {0}")] - DuplicateOutputNote(NoteId), - - #[error("note hashes mismatch for note {id}: (input: {input_hash}, output: {output_hash})")] - NoteHashesMismatch { - id: NoteId, - input_hash: Digest, - output_hash: Digest, - }, - - #[error("failed to merge transaction delta into account {account_id}")] - AccountUpdateError { - account_id: AccountId, - source: AccountDeltaError, - }, - /// We sometimes randomly inject errors into the batch building process to test our failure /// responses. #[error("nothing actually went wrong, failure was injected on purpose")] @@ -156,6 +138,12 @@ pub enum BuildBatchError { #[error("failed to fetch batch inputs from store")] FetchBatchInputsFailed(#[source] StoreError), + + #[error("failed to build proposed transaction batch")] + ProposeBatchError(#[source] ProposedBatchError), + + #[error("failed to prove proposed transaction batch")] + ProveBatchError(#[source] BatchProveError), } // Block prover errors diff --git a/crates/block-producer/src/mempool/batch_graph.rs b/crates/block-producer/src/mempool/batch_graph.rs index 73948f46e..256408c1f 100644 --- a/crates/block-producer/src/mempool/batch_graph.rs +++ b/crates/block-producer/src/mempool/batch_graph.rs @@ -1,12 +1,15 @@ use std::collections::{BTreeMap, BTreeSet}; -use miden_objects::transaction::TransactionId; +use miden_objects::{ + account::AccountId, + batch::{BatchId, ProvenBatch}, + transaction::TransactionId, +}; use super::{ graph::{DependencyGraph, GraphError}, BlockBudget, BudgetStatus, }; -use crate::batch_builder::batch::{BatchId, TransactionBatch}; // BATCH GRAPH // ================================================================================================ @@ -53,7 +56,7 @@ use crate::batch_builder::batch::{BatchId, TransactionBatch}; #[derive(Default, Debug, Clone, PartialEq)] pub struct BatchGraph { /// Tracks the interdependencies between batches. - inner: DependencyGraph, + inner: DependencyGraph, /// Maps each transaction to its batch, allowing for reverse lookups. /// @@ -97,12 +100,12 @@ impl BatchGraph { /// - any parent transactions are _not_ in the graph pub fn insert( &mut self, - transactions: Vec, + transactions: Vec<(TransactionId, AccountId)>, mut parents: BTreeSet, ) -> Result { let duplicates = transactions .iter() - .filter(|tx| self.transactions.contains_key(tx)) + .filter_map(|(tx, _)| self.transactions.contains_key(tx).then_some(tx)) .copied() .collect::>(); if !duplicates.is_empty() { @@ -111,7 +114,7 @@ impl BatchGraph { // Reverse lookup parent batch IDs. Take care to allow for parent transactions within this // batch i.e. internal dependencies. - for tx in &transactions { + for (tx, _) in &transactions { parents.remove(tx); } let parent_batches = parents @@ -124,13 +127,14 @@ impl BatchGraph { }) .collect::>()?; - let id = BatchId::compute(transactions.iter()); + let id = BatchId::from_ids(transactions.iter().copied()); self.inner.insert_pending(id, parent_batches)?; - for tx in transactions.iter().copied() { + for (tx, _) in transactions.iter().copied() { self.transactions.insert(tx, id); } - self.batches.insert(id, transactions); + + self.batches.insert(id, transactions.into_iter().map(|(tx, _)| tx).collect()); Ok(id) } @@ -231,7 +235,7 @@ impl BatchGraph { /// # Errors /// /// Returns an error if the batch is not in the graph or if it was already previously proven. - pub fn submit_proof(&mut self, batch: TransactionBatch) -> Result<(), GraphError> { + pub fn submit_proof(&mut self, batch: ProvenBatch) -> Result<(), GraphError> { self.inner.promote_pending(batch.id(), batch) } @@ -240,7 +244,7 @@ impl BatchGraph { /// /// Note that batch order should be maintained to allow for inter-batch dependencies to be /// correctly resolved. - pub fn select_block(&mut self, mut budget: BlockBudget) -> Vec { + pub fn select_block(&mut self, mut budget: BlockBudget) -> Vec { let mut batches = Vec::with_capacity(budget.batches); while let Some(batch_id) = self.inner.roots().first().copied() { @@ -289,14 +293,14 @@ mod tests { #[test] fn insert_rejects_duplicate_transactions() { let mut rng = Random::with_random_seed(); - let tx_dup = rng.draw_tx_id(); - let tx_non_dup = rng.draw_tx_id(); + let tx_dup = (rng.draw_tx_id(), rng.draw_account_id()); + let tx_non_dup = (rng.draw_tx_id(), rng.draw_account_id()); let mut uut = BatchGraph::default(); uut.insert(vec![tx_dup], BTreeSet::default()).unwrap(); let err = uut.insert(vec![tx_dup, tx_non_dup], BTreeSet::default()).unwrap_err(); - let expected = BatchInsertError::DuplicateTransactions([tx_dup].into()); + let expected = BatchInsertError::DuplicateTransactions([tx_dup.0].into()); assert_eq!(err, expected); } @@ -304,13 +308,13 @@ mod tests { #[test] fn insert_rejects_missing_parents() { let mut rng = Random::with_random_seed(); - let tx = rng.draw_tx_id(); - let missing = rng.draw_tx_id(); + let tx = (rng.draw_tx_id(), rng.draw_account_id()); + let missing = (rng.draw_tx_id(), rng.draw_account_id()); let mut uut = BatchGraph::default(); - let err = uut.insert(vec![tx], [missing].into()).unwrap_err(); - let expected = BatchInsertError::UnknownParentTransaction(missing); + let err = uut.insert(vec![tx], [missing.0].into()).unwrap_err(); + let expected = BatchInsertError::UnknownParentTransaction(missing.0); assert_eq!(err, expected); } @@ -319,11 +323,11 @@ mod tests { fn insert_with_internal_parent_succeeds() { // Ensure that a batch with internal dependencies can be inserted. let mut rng = Random::with_random_seed(); - let parent = rng.draw_tx_id(); - let child = rng.draw_tx_id(); + let parent = (rng.draw_tx_id(), rng.draw_account_id()); + let child = (rng.draw_tx_id(), rng.draw_account_id()); let mut uut = BatchGraph::default(); - uut.insert(vec![parent, child], [parent].into()).unwrap(); + uut.insert(vec![parent, child], [parent.0].into()).unwrap(); } // PURGE_SUBGRAPHS TESTS @@ -334,19 +338,25 @@ mod tests { // Ensure that purge_subgraphs returns both parent and child batches when the parent is // pruned. Further ensure that a disjoint batch is not pruned. let mut rng = Random::with_random_seed(); - let parent_batch_txs = (0..5).map(|_| rng.draw_tx_id()).collect::>(); - let child_batch_txs = (0..5).map(|_| rng.draw_tx_id()).collect::>(); - let disjoint_batch_txs = (0..5).map(|_| rng.draw_tx_id()).collect(); + let parent_batch_txs = + (0..5).map(|_| (rng.draw_tx_id(), rng.draw_account_id())).collect::>(); + let child_batch_txs = + (0..5).map(|_| (rng.draw_tx_id(), rng.draw_account_id())).collect::>(); + let disjoint_batch_txs = + (0..5).map(|_| (rng.draw_tx_id(), rng.draw_account_id())).collect(); let mut uut = BatchGraph::default(); let parent_batch_id = uut.insert(parent_batch_txs.clone(), BTreeSet::default()).unwrap(); let child_batch_id = - uut.insert(child_batch_txs.clone(), [parent_batch_txs[0]].into()).unwrap(); + uut.insert(child_batch_txs.clone(), [parent_batch_txs[0].0].into()).unwrap(); uut.insert(disjoint_batch_txs, BTreeSet::default()).unwrap(); let result = uut.remove_batches([parent_batch_id].into()).unwrap(); - let expected = - [(parent_batch_id, parent_batch_txs), (child_batch_id, child_batch_txs)].into(); + let expected = [ + (parent_batch_id, parent_batch_txs.into_iter().map(|(tx, _)| tx).collect()), + (child_batch_id, child_batch_txs.into_iter().map(|(tx, _)| tx).collect()), + ] + .into(); assert_eq!(result, expected); } diff --git a/crates/block-producer/src/mempool/mod.rs b/crates/block-producer/src/mempool/mod.rs index 08e332324..3eaa40526 100644 --- a/crates/block-producer/src/mempool/mod.rs +++ b/crates/block-producer/src/mempool/mod.rs @@ -4,8 +4,10 @@ use batch_graph::BatchGraph; use graph::GraphError; use inflight_state::InflightState; use miden_objects::{ - block::BlockNumber, transaction::TransactionId, MAX_ACCOUNTS_PER_BATCH, - MAX_INPUT_NOTES_PER_BATCH, MAX_OUTPUT_NOTES_PER_BATCH, + batch::{BatchId, ProvenBatch}, + block::BlockNumber, + transaction::TransactionId, + MAX_ACCOUNTS_PER_BATCH, MAX_INPUT_NOTES_PER_BATCH, MAX_OUTPUT_NOTES_PER_BATCH, }; use tokio::sync::Mutex; use tracing::instrument; @@ -13,10 +15,8 @@ use transaction_expiration::TransactionExpirations; use transaction_graph::TransactionGraph; use crate::{ - batch_builder::batch::{BatchId, TransactionBatch}, - domain::transaction::AuthenticatedTransaction, - errors::AddTransactionError, - COMPONENT, SERVER_MAX_BATCHES_PER_BLOCK, SERVER_MAX_TXS_PER_BATCH, + domain::transaction::AuthenticatedTransaction, errors::AddTransactionError, COMPONENT, + SERVER_MAX_BATCHES_PER_BLOCK, SERVER_MAX_TXS_PER_BATCH, }; mod batch_graph; @@ -114,7 +114,7 @@ impl BlockBudget { /// Returns [`BudgetStatus::Exceeded`] if the batch would exceed the remaining budget, /// otherwise returns [`BudgetStatus::Ok`]. #[must_use] - fn check_then_subtract(&mut self, _batch: &TransactionBatch) -> BudgetStatus { + fn check_then_subtract(&mut self, _batch: &ProvenBatch) -> BudgetStatus { if self.batches == 0 { BudgetStatus::Exceeded } else { @@ -233,7 +233,7 @@ impl Mempool { if batch.is_empty() { return None; } - let tx_ids = batch.iter().map(AuthenticatedTransaction::id).collect::>(); + let tx_ids = batch.iter().map(|tx| (tx.id(), tx.account_id())).collect::>(); let batch_id = self.batches.insert(tx_ids, parents).expect("Selected batch should insert"); @@ -268,7 +268,7 @@ impl Mempool { /// Marks a batch as proven if it exists. #[instrument(target = COMPONENT, skip_all, fields(batch=%batch.id()))] - pub fn batch_proved(&mut self, batch: TransactionBatch) { + pub fn batch_proved(&mut self, batch: ProvenBatch) { // Batch may have been removed as part of a parent batches failure. if !self.batches.contains(&batch.id()) { return; @@ -287,11 +287,11 @@ impl Mempool { /// /// Panics if there is already a block in flight. #[instrument(target = COMPONENT, skip_all)] - pub fn select_block(&mut self) -> (BlockNumber, Vec) { + pub fn select_block(&mut self) -> (BlockNumber, Vec) { assert!(self.block_in_progress.is_none(), "Cannot have two blocks inflight."); let batches = self.batches.select_block(self.block_budget); - self.block_in_progress = Some(batches.iter().map(TransactionBatch::id).collect()); + self.block_in_progress = Some(batches.iter().map(ProvenBatch::id).collect()); (self.chain_tip.child(), batches) } diff --git a/crates/block-producer/src/mempool/tests.rs b/crates/block-producer/src/mempool/tests.rs index 8e93892fb..e7680736f 100644 --- a/crates/block-producer/src/mempool/tests.rs +++ b/crates/block-producer/src/mempool/tests.rs @@ -1,9 +1,8 @@ -use miden_node_proto::domain::note::NoteAuthenticationInfo; use miden_objects::block::BlockNumber; use pretty_assertions::assert_eq; use super::*; -use crate::test_utils::MockProvenTxBuilder; +use crate::test_utils::{batch::TransactionBatchConstructor, MockProvenTxBuilder}; impl Mempool { fn for_tests() -> Self { @@ -48,10 +47,8 @@ fn children_of_failed_batches_are_ignored() { uut.batch_failed(child_batch_a); assert_eq!(uut, reference); - let proof = - TransactionBatch::new([txs[2].raw_proven_transaction()], NoteAuthenticationInfo::default()) - .unwrap(); - uut.batch_proved(proof); + let proven_batch = ProvenBatch::mocked_from_transactions([txs[2].raw_proven_transaction()]); + uut.batch_proved(proven_batch); assert_eq!(uut, reference); } @@ -95,13 +92,9 @@ fn block_commit_reverts_expired_txns() { // Force the tx into a pending block. uut.add_transaction(tx_to_commit.clone()).unwrap(); uut.select_batch().unwrap(); - uut.batch_proved( - TransactionBatch::new( - [tx_to_commit.raw_proven_transaction()], - NoteAuthenticationInfo::default(), - ) - .unwrap(), - ); + uut.batch_proved(ProvenBatch::mocked_from_transactions( + [tx_to_commit.raw_proven_transaction()], + )); let (block, _) = uut.select_block(); // A reverted transaction behaves as if it never existed, the current state is the expected // outcome, plus an extra committed block at the end. @@ -168,13 +161,9 @@ fn block_failure_reverts_its_transactions() { uut.add_transaction(reverted_txs[0].clone()).unwrap(); uut.select_batch().unwrap(); - uut.batch_proved( - TransactionBatch::new( - [reverted_txs[0].raw_proven_transaction()], - NoteAuthenticationInfo::default(), - ) - .unwrap(), - ); + uut.batch_proved(ProvenBatch::mocked_from_transactions([ + reverted_txs[0].raw_proven_transaction() + ])); // Block 1 will contain just the first batch. let (block_number, _) = uut.select_block(); diff --git a/crates/block-producer/src/store/mod.rs b/crates/block-producer/src/store/mod.rs index 2608975b5..1aa2638fd 100644 --- a/crates/block-producer/src/store/mod.rs +++ b/crates/block-producer/src/store/mod.rs @@ -6,13 +6,13 @@ use std::{ use itertools::Itertools; use miden_node_proto::{ - domain::note::NoteAuthenticationInfo, + domain::batch::BatchInputs, errors::{ConversionError, MissingFieldHelper}, generated::{ digest, requests::{ - ApplyBlockRequest, GetBlockHeaderByNumberRequest, GetBlockInputsRequest, - GetNoteAuthenticationInfoRequest, GetTransactionInputsRequest, + ApplyBlockRequest, GetBatchInputsRequest, GetBlockHeaderByNumberRequest, + GetBlockInputsRequest, GetTransactionInputsRequest, }, responses::{GetTransactionInputsResponse, NullifierTransactionInputRecord}, store::api_client as store_client, @@ -212,21 +212,17 @@ impl StoreClient { #[instrument(target = COMPONENT, skip_all, err)] pub async fn get_batch_inputs( &self, + block_references: impl Iterator + Send, notes: impl Iterator + Send, - ) -> Result { - let request = tonic::Request::new(GetNoteAuthenticationInfoRequest { + ) -> Result { + let request = tonic::Request::new(GetBatchInputsRequest { + reference_blocks: block_references.map(|(block_num, _)| block_num.as_u32()).collect(), note_ids: notes.map(digest::Digest::from).collect(), }); - let store_response = - self.inner.clone().get_note_authentication_info(request).await?.into_inner(); + let store_response = self.inner.clone().get_batch_inputs(request).await?.into_inner(); - let note_authentication_info = store_response - .proofs - .ok_or(GetTransactionInputsResponse::missing_field("proofs"))? - .try_into()?; - - Ok(note_authentication_info) + store_response.try_into().map_err(Into::into) } #[instrument(target = COMPONENT, skip_all, err)] diff --git a/crates/block-producer/src/test_utils/batch.rs b/crates/block-producer/src/test_utils/batch.rs index 45346941a..b4caffd28 100644 --- a/crates/block-producer/src/test_utils/batch.rs +++ b/crates/block-producer/src/test_utils/batch.rs @@ -1,8 +1,25 @@ -use miden_node_proto::domain::note::NoteAuthenticationInfo; +use std::collections::BTreeMap; -use crate::{batch_builder::TransactionBatch, test_utils::MockProvenTxBuilder}; +use miden_objects::{ + batch::{BatchAccountUpdate, BatchId, BatchNoteTree, ProvenBatch}, + block::BlockNumber, + transaction::{InputNotes, ProvenTransaction}, +}; + +use crate::test_utils::MockProvenTxBuilder; pub trait TransactionBatchConstructor { + /// Builds a **mocked** [`ProvenBatch`] from the given transactions, which most likely violates + /// some of the rules of actual transaction batches. + /// + /// This builds a mocked version of a proven batch for testing purposes which can be useful if + /// the batch's details don't need to be correct (e.g. if something else is under test but + /// requires a transaction batch). If you need an actual valid [`ProvenBatch`], build a + /// [`ProposedBatch`](miden_objects::batch::ProposedBatch) first and convert (without proving) + /// or prove it into a [`ProvenBatch`]. + fn mocked_from_transactions<'tx>(txs: impl IntoIterator) + -> Self; + /// Returns a `TransactionBatch` with `notes_per_tx.len()` transactions, where the i'th /// transaction has `notes_per_tx[i]` notes created fn from_notes_created(starting_account_index: u32, notes_per_tx: &[u64]) -> Self; @@ -11,7 +28,46 @@ pub trait TransactionBatchConstructor { fn from_txs(starting_account_index: u32, num_txs_in_batch: u64) -> Self; } -impl TransactionBatchConstructor for TransactionBatch { +impl TransactionBatchConstructor for ProvenBatch { + fn mocked_from_transactions<'tx>( + txs: impl IntoIterator, + ) -> Self { + let mut account_updates = BTreeMap::new(); + + let txs: Vec<_> = txs.into_iter().collect(); + let mut input_notes = Vec::new(); + let mut output_notes = Vec::new(); + + for tx in &txs { + // Aggregate account updates. + account_updates + .entry(tx.account_id()) + .and_modify(|update: &mut BatchAccountUpdate| { + update.merge_proven_tx(tx).unwrap(); + }) + .or_insert_with(|| BatchAccountUpdate::from_transaction(tx)); + + // Consider all input notes of all transactions as inputs of the batch, which may not + // always be correct. + input_notes.extend(tx.input_notes().iter().cloned()); + // Consider all outputs notes of all transactions as outputs of the batch, which may not + // always be correct. + output_notes.extend(tx.output_notes().iter().cloned()); + } + + ProvenBatch::new( + BatchId::from_transactions(txs.into_iter()), + account_updates, + InputNotes::new_unchecked(input_notes), + BatchNoteTree::with_contiguous_leaves( + output_notes.iter().map(|x| (x.id(), x.metadata())), + ) + .unwrap(), + output_notes, + BlockNumber::from(u32::MAX), + ) + } + fn from_notes_created(starting_account_index: u32, notes_per_tx: &[u64]) -> Self { let txs: Vec<_> = notes_per_tx .iter() @@ -26,7 +82,7 @@ impl TransactionBatchConstructor for TransactionBatch { }) .collect(); - Self::new(&txs, NoteAuthenticationInfo::default()).unwrap() + Self::mocked_from_transactions(&txs) } fn from_txs(starting_account_index: u32, num_txs_in_batch: u64) -> Self { @@ -38,6 +94,6 @@ impl TransactionBatchConstructor for TransactionBatch { }) .collect(); - Self::new(&txs, NoteAuthenticationInfo::default()).unwrap() + Self::mocked_from_transactions(&txs) } } diff --git a/crates/block-producer/src/test_utils/block.rs b/crates/block-producer/src/test_utils/block.rs index 2a940b1ae..532e314dd 100644 --- a/crates/block-producer/src/test_utils/block.rs +++ b/crates/block-producer/src/test_utils/block.rs @@ -1,6 +1,7 @@ use std::iter; use miden_objects::{ + batch::ProvenBatch, block::{Block, BlockAccountUpdate, BlockHeader, BlockNoteIndex, BlockNoteTree, NoteBatch}, crypto::merkle::{Mmr, SimpleSmt}, note::Nullifier, @@ -10,7 +11,6 @@ use miden_objects::{ use super::MockStoreSuccess; use crate::{ - batch_builder::TransactionBatch, block::BlockInputs, block_builder::prover::{block_witness::BlockWitness, BlockProver}, }; @@ -19,7 +19,7 @@ use crate::{ /// batches to be applied pub async fn build_expected_block_header( store: &MockStoreSuccess, - batches: &[TransactionBatch], + batches: &[ProvenBatch], ) -> BlockHeader { let last_block_header = *store .block_headers @@ -32,11 +32,11 @@ pub async fn build_expected_block_header( // Compute new account root let updated_accounts: Vec<_> = - batches.iter().flat_map(TransactionBatch::updated_accounts).collect(); + batches.iter().flat_map(|batch| batch.account_updates().iter()).collect(); let new_account_root = { let mut store_accounts = store.accounts.read().await.clone(); for (&account_id, update) in updated_accounts { - store_accounts.insert(account_id.into(), update.final_state.into()); + store_accounts.insert(account_id.into(), update.final_state_commitment().into()); } store_accounts.root() @@ -51,7 +51,8 @@ pub async fn build_expected_block_header( store_chain_mmr.peaks().hash_peaks() }; - let note_created_smt = note_created_smt_from_note_batches(block_output_notes(batches.iter())); + let note_created_smt = + note_created_smt_from_note_batches(block_output_notes(batches.iter()).iter()); // Build header BlockHeader::new( @@ -74,12 +75,12 @@ pub async fn build_expected_block_header( /// node pub async fn build_actual_block_header( store: &MockStoreSuccess, - batches: Vec, + batches: Vec, ) -> BlockHeader { let updated_accounts: Vec<_> = - batches.iter().flat_map(TransactionBatch::updated_accounts).collect(); + batches.iter().flat_map(|batch| batch.account_updates().iter()).collect(); let produced_nullifiers: Vec = - batches.iter().flat_map(TransactionBatch::produced_nullifiers).collect(); + batches.iter().flat_map(ProvenBatch::produced_nullifiers).collect(); let block_inputs_from_store: BlockInputs = store .get_block_inputs( @@ -199,7 +200,7 @@ pub(crate) fn note_created_smt_from_note_batches<'a>( } pub(crate) fn block_output_notes<'a>( - batches: impl Iterator + Clone, -) -> impl Iterator + Clone { - batches.map(TransactionBatch::output_notes) + batches: impl Iterator + Clone, +) -> Vec> { + batches.map(|batch| batch.output_notes().to_vec()).collect() } diff --git a/crates/block-producer/src/test_utils/mod.rs b/crates/block-producer/src/test_utils/mod.rs index 28ba4349f..97dfcc5fb 100644 --- a/crates/block-producer/src/test_utils/mod.rs +++ b/crates/block-producer/src/test_utils/mod.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use miden_objects::{ account::AccountId, crypto::rand::{FeltRng, RpoRandomCoin}, + testing::account_id::AccountIdBuilder, transaction::TransactionId, Digest, }; @@ -48,6 +49,10 @@ impl Random { self.0.draw_word().into() } + pub fn draw_account_id(&mut self) -> AccountId { + AccountIdBuilder::new().build_with_rng(&mut self.0) + } + pub fn draw_digest(&mut self) -> Digest { self.0.draw_word().into() } diff --git a/crates/block-producer/src/test_utils/proven_tx.rs b/crates/block-producer/src/test_utils/proven_tx.rs index 3de0fd2c1..4af9a0e5b 100644 --- a/crates/block-producer/src/test_utils/proven_tx.rs +++ b/crates/block-producer/src/test_utils/proven_tx.rs @@ -135,6 +135,7 @@ impl MockProvenTxBuilder { self.account_id, self.initial_account_hash, self.final_account_hash, + BlockNumber::from(0), Digest::default(), self.expiration_block_num, ExecutionProof::new(Proof::new_dummy(), HashFunction::Blake3_192), diff --git a/crates/block-producer/src/test_utils/store.rs b/crates/block-producer/src/test_utils/store.rs index ac8e694af..ecbe1dc67 100644 --- a/crates/block-producer/src/test_utils/store.rs +++ b/crates/block-producer/src/test_utils/store.rs @@ -5,6 +5,7 @@ use std::{ use miden_node_proto::domain::{block::BlockInclusionProof, note::NoteAuthenticationInfo}; use miden_objects::{ + batch::ProvenBatch, block::{Block, BlockHeader, BlockNumber, NoteBatch}, crypto::merkle::{Mmr, SimpleSmt, Smt, ValuePath}, note::{NoteId, NoteInclusionProof, Nullifier}, @@ -15,7 +16,6 @@ use tokio::sync::RwLock; use super::*; use crate::{ - batch_builder::TransactionBatch, block::{AccountWitness, BlockInputs}, errors::StoreError, store::TransactionInputs, @@ -35,20 +35,23 @@ pub struct MockStoreSuccessBuilder { } impl MockStoreSuccessBuilder { - pub fn from_batches<'a>( - batches_iter: impl Iterator + Clone, - ) -> Self { + pub fn from_batches<'a>(batches_iter: impl Iterator + Clone) -> Self { let accounts_smt = { let accounts = batches_iter .clone() - .flat_map(TransactionBatch::account_initial_states) + .flat_map(|batch| { + batch + .account_updates() + .iter() + .map(|(account_id, update)| (account_id, update.initial_state_commitment())) + }) .map(|(account_id, hash)| (account_id.prefix().into(), hash.into())); SimpleSmt::::with_leaves(accounts).unwrap() }; Self { accounts: Some(accounts_smt), - notes: Some(block_output_notes(batches_iter).cloned().collect()), + notes: Some(block_output_notes(batches_iter)), produced_nullifiers: None, chain_mmr: None, block_num: None, diff --git a/crates/proto/src/domain/batch.rs b/crates/proto/src/domain/batch.rs new file mode 100644 index 000000000..2a29247ea --- /dev/null +++ b/crates/proto/src/domain/batch.rs @@ -0,0 +1,53 @@ +use std::collections::BTreeMap; + +use miden_objects::{ + block::BlockHeader, + note::{NoteId, NoteInclusionProof}, + transaction::ChainMmr, + utils::{Deserializable, Serializable}, +}; + +use crate::{ + errors::{ConversionError, MissingFieldHelper}, + generated::responses as proto, +}; + +/// Data required for a transaction batch. +#[derive(Clone, Debug)] +pub struct BatchInputs { + pub batch_reference_block_header: BlockHeader, + pub note_proofs: BTreeMap, + pub chain_mmr: ChainMmr, +} + +impl From for proto::GetBatchInputsResponse { + fn from(inputs: BatchInputs) -> Self { + 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(), + } + } +} + +impl TryFrom for BatchInputs { + type Error = ConversionError; + + fn try_from(response: proto::GetBatchInputsResponse) -> Result { + let result = Self { + batch_reference_block_header: response + .batch_reference_block_header + .ok_or(proto::GetBatchInputsResponse::missing_field("block_header"))? + .try_into()?, + note_proofs: response + .note_proofs + .iter() + .map(<(NoteId, NoteInclusionProof)>::try_from) + .collect::>()?, + chain_mmr: ChainMmr::read_from_bytes(&response.chain_mmr) + .map_err(|source| ConversionError::deserialization_error("ChainMmr", source))?, + }; + + Ok(result) + } +} diff --git a/crates/proto/src/domain/mod.rs b/crates/proto/src/domain/mod.rs index 83959535e..2f7ee28da 100644 --- a/crates/proto/src/domain/mod.rs +++ b/crates/proto/src/domain/mod.rs @@ -1,4 +1,5 @@ pub mod account; +pub mod batch; pub mod block; pub mod digest; pub mod merkle; diff --git a/crates/proto/src/errors.rs b/crates/proto/src/errors.rs index 8af3c59ea..39f72f3e2 100644 --- a/crates/proto/src/errors.rs +++ b/crates/proto/src/errors.rs @@ -1,6 +1,9 @@ use std::{any::type_name, num::TryFromIntError}; -use miden_objects::crypto::merkle::{SmtLeafError, SmtProofError}; +use miden_objects::{ + crypto::merkle::{SmtLeafError, SmtProofError}, + utils::DeserializationError, +}; use thiserror::Error; #[derive(Debug, Error)] @@ -28,6 +31,17 @@ pub enum ConversionError { }, #[error("MMR error")] MmrError(#[from] miden_objects::crypto::merkle::MmrError), + #[error("failed to deserialize {entity}")] + DeserializationError { + entity: &'static str, + source: DeserializationError, + }, +} + +impl ConversionError { + pub fn deserialization_error(entity: &'static str, source: DeserializationError) -> Self { + Self::DeserializationError { entity, source } + } } pub trait MissingFieldHelper { diff --git a/crates/proto/src/generated/requests.rs b/crates/proto/src/generated/requests.rs index a38b5f656..c8e19bb29 100644 --- a/crates/proto/src/generated/requests.rs +++ b/crates/proto/src/generated/requests.rs @@ -92,6 +92,16 @@ pub struct GetBlockInputsRequest { #[prost(message, repeated, tag = "3")] pub unauthenticated_notes: ::prost::alloc::vec::Vec, } +/// Returns the inputs for a transaction batch. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetBatchInputsRequest { + /// List of unauthenticated notes to be queried from the database. + #[prost(message, repeated, tag = "1")] + pub note_ids: ::prost::alloc::vec::Vec, + /// Set of block numbers referenced by transactions. + #[prost(fixed32, repeated, tag = "2")] + pub reference_blocks: ::prost::alloc::vec::Vec, +} /// Returns data required to validate a new transaction. #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetTransactionInputsRequest { @@ -123,13 +133,6 @@ pub struct GetNotesByIdRequest { #[prost(message, repeated, tag = "1")] pub note_ids: ::prost::alloc::vec::Vec, } -/// Returns a list of Note inclusion proofs for the specified Note IDs. -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GetNoteAuthenticationInfoRequest { - /// List of notes to be queried from the database. - #[prost(message, repeated, tag = "1")] - pub note_ids: ::prost::alloc::vec::Vec, -} /// Returns the latest state of an account with the specified ID. #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetAccountDetailsRequest { diff --git a/crates/proto/src/generated/responses.rs b/crates/proto/src/generated/responses.rs index 5b526ae67..c3a8f5f20 100644 --- a/crates/proto/src/generated/responses.rs +++ b/crates/proto/src/generated/responses.rs @@ -128,6 +128,21 @@ pub struct GetBlockInputsResponse { super::note::NoteAuthenticationInfo, >, } +/// Represents the result of getting batch inputs. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetBatchInputsResponse { + /// The block header that the transaction batch should reference. + #[prost(message, optional, tag = "1")] + pub batch_reference_block_header: ::core::option::Option, + /// Proof of each _found_ unauthenticated note's inclusion in a block. + #[prost(message, repeated, tag = "2")] + pub note_proofs: ::prost::alloc::vec::Vec, + /// 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. + #[prost(bytes = "vec", tag = "3")] + pub chain_mmr: ::prost::alloc::vec::Vec, +} /// An account returned as a response to the `GetTransactionInputs`. #[derive(Clone, PartialEq, ::prost::Message)] pub struct AccountTransactionInputRecord { @@ -178,13 +193,6 @@ pub struct GetNotesByIdResponse { #[prost(message, repeated, tag = "1")] pub notes: ::prost::alloc::vec::Vec, } -/// Represents the result of getting note authentication info. -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GetNoteAuthenticationInfoResponse { - /// Proofs of note inclusions in blocks and block inclusions in chain. - #[prost(message, optional, tag = "1")] - pub proofs: ::core::option::Option, -} /// Represents the result of getting account details. #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetAccountDetailsResponse { diff --git a/crates/proto/src/generated/store.rs b/crates/proto/src/generated/store.rs index 815a65235..0d9b52f78 100644 --- a/crates/proto/src/generated/store.rs +++ b/crates/proto/src/generated/store.rs @@ -328,14 +328,14 @@ pub mod api_client { req.extensions_mut().insert(GrpcMethod::new("store.Api", "GetBlockInputs")); self.inner.unary(req, path, codec).await } - /// Returns a list of Note inclusion proofs for the specified Note IDs. - pub async fn get_note_authentication_info( + /// Returns the inputs for a transaction batch. + pub async fn get_batch_inputs( &mut self, request: impl tonic::IntoRequest< - super::super::requests::GetNoteAuthenticationInfoRequest, + super::super::requests::GetBatchInputsRequest, >, ) -> std::result::Result< - tonic::Response, + tonic::Response, tonic::Status, > { self.inner @@ -347,12 +347,9 @@ pub mod api_client { ) })?; let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/store.Api/GetNoteAuthenticationInfo", - ); + let path = http::uri::PathAndQuery::from_static("/store.Api/GetBatchInputs"); let mut req = request.into_request(); - req.extensions_mut() - .insert(GrpcMethod::new("store.Api", "GetNoteAuthenticationInfo")); + req.extensions_mut().insert(GrpcMethod::new("store.Api", "GetBatchInputs")); self.inner.unary(req, path, codec).await } /// Returns a list of notes matching the provided note IDs. @@ -565,14 +562,12 @@ pub mod api_server { tonic::Response, tonic::Status, >; - /// Returns a list of Note inclusion proofs for the specified Note IDs. - async fn get_note_authentication_info( + /// Returns the inputs for a transaction batch. + async fn get_batch_inputs( &self, - request: tonic::Request< - super::super::requests::GetNoteAuthenticationInfoRequest, - >, + request: tonic::Request, ) -> std::result::Result< - tonic::Response, + tonic::Response, tonic::Status, >; /// Returns a list of notes matching the provided note IDs. @@ -1140,15 +1135,15 @@ pub mod api_server { }; Box::pin(fut) } - "/store.Api/GetNoteAuthenticationInfo" => { + "/store.Api/GetBatchInputs" => { #[allow(non_camel_case_types)] - struct GetNoteAuthenticationInfoSvc(pub Arc); + struct GetBatchInputsSvc(pub Arc); impl< T: Api, > tonic::server::UnaryService< - super::super::requests::GetNoteAuthenticationInfoRequest, - > for GetNoteAuthenticationInfoSvc { - type Response = super::super::responses::GetNoteAuthenticationInfoResponse; + super::super::requests::GetBatchInputsRequest, + > for GetBatchInputsSvc { + type Response = super::super::responses::GetBatchInputsResponse; type Future = BoxFuture< tonic::Response, tonic::Status, @@ -1156,13 +1151,12 @@ pub mod api_server { fn call( &mut self, request: tonic::Request< - super::super::requests::GetNoteAuthenticationInfoRequest, + super::super::requests::GetBatchInputsRequest, >, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::get_note_authentication_info(&inner, request) - .await + ::get_batch_inputs(&inner, request).await }; Box::pin(fut) } @@ -1173,7 +1167,7 @@ pub mod api_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = GetNoteAuthenticationInfoSvc(inner); + let method = GetBatchInputsSvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( diff --git a/crates/rpc-proto/proto/requests.proto b/crates/rpc-proto/proto/requests.proto index 1230eac78..f2323c56c 100644 --- a/crates/rpc-proto/proto/requests.proto +++ b/crates/rpc-proto/proto/requests.proto @@ -86,6 +86,14 @@ message GetBlockInputsRequest { repeated digest.Digest unauthenticated_notes = 3; } +// Returns the inputs for a transaction batch. +message GetBatchInputsRequest { + // List of unauthenticated notes to be queried from the database. + repeated digest.Digest note_ids = 1; + // Set of block numbers referenced by transactions. + repeated fixed32 reference_blocks = 2; +} + // Returns data required to validate a new transaction. message GetTransactionInputsRequest { // ID of the account against which a transaction is executed. @@ -112,12 +120,6 @@ message GetNotesByIdRequest { repeated digest.Digest note_ids = 1; } -// Returns a list of Note inclusion proofs for the specified Note IDs. -message GetNoteAuthenticationInfoRequest { - // List of notes to be queried from the database. - repeated digest.Digest note_ids = 1; -} - // Returns the latest state of an account with the specified ID. message GetAccountDetailsRequest { // Account ID to get details. diff --git a/crates/rpc-proto/proto/responses.proto b/crates/rpc-proto/proto/responses.proto index 36e175d1e..f1dfe5f90 100644 --- a/crates/rpc-proto/proto/responses.proto +++ b/crates/rpc-proto/proto/responses.proto @@ -128,6 +128,20 @@ message GetBlockInputsResponse { note.NoteAuthenticationInfo found_unauthenticated_notes = 5; } +// Represents the result of getting batch inputs. +message GetBatchInputsResponse { + // The block header that the transaction batch should reference. + block.BlockHeader batch_reference_block_header = 1; + + // Proof of each _found_ unauthenticated note's inclusion in a block. + repeated note.NoteInclusionInBlockProof note_proofs = 2; + + // 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; +} + // An account returned as a response to the `GetTransactionInputs`. message AccountTransactionInputRecord { // The account ID. @@ -173,12 +187,6 @@ message GetNotesByIdResponse { repeated note.Note notes = 1; } -// Represents the result of getting note authentication info. -message GetNoteAuthenticationInfoResponse { - // Proofs of note inclusions in blocks and block inclusions in chain. - note.NoteAuthenticationInfo proofs = 1; -} - // Represents the result of getting account details. message GetAccountDetailsResponse { // Account info (with details for public accounts). diff --git a/crates/rpc-proto/proto/store.proto b/crates/rpc-proto/proto/store.proto index 0562b8c54..7137121dc 100644 --- a/crates/rpc-proto/proto/store.proto +++ b/crates/rpc-proto/proto/store.proto @@ -39,8 +39,8 @@ service Api { // Returns data required to prove the next block. rpc GetBlockInputs(requests.GetBlockInputsRequest) returns (responses.GetBlockInputsResponse) {} - // Returns a list of Note inclusion proofs for the specified Note IDs. - rpc GetNoteAuthenticationInfo(requests.GetNoteAuthenticationInfoRequest) returns (responses.GetNoteAuthenticationInfoResponse) {} + // Returns the inputs for a transaction batch. + rpc GetBatchInputs(requests.GetBatchInputsRequest) returns (responses.GetBatchInputsResponse) {} // Returns a list of notes matching the provided note IDs. rpc GetNotesById(requests.GetNotesByIdRequest) returns (responses.GetNotesByIdResponse) {} diff --git a/crates/rpc/src/server/api.rs b/crates/rpc/src/server/api.rs index 976ed4fc2..e9fe29a2a 100644 --- a/crates/rpc/src/server/api.rs +++ b/crates/rpc/src/server/api.rs @@ -182,7 +182,7 @@ impl api_server::Api for RpcApi { let tx_verifier = TransactionVerifier::new(MIN_PROOF_SECURITY_LEVEL); - tx_verifier.verify(tx.clone()).map_err(|err| { + tx_verifier.verify(&tx).map_err(|err| { Status::invalid_argument(format!("Invalid proof for transaction {}: {err}", tx.id())) })?; diff --git a/crates/store/src/errors.rs b/crates/store/src/errors.rs index 462341bf5..2c6fa9e36 100644 --- a/crates/store/src/errors.rs +++ b/crates/store/src/errors.rs @@ -244,11 +244,11 @@ pub enum GetBlockInputsError { NoteInclusionMmr(#[from] MmrError), } -impl From for GetBlockInputsError { - fn from(value: GetNoteInclusionProofError) -> Self { +impl From for GetBlockInputsError { + fn from(value: GetNoteAuthenticationInfoError) -> Self { match value { - GetNoteInclusionProofError::DatabaseError(db_err) => db_err.into(), - GetNoteInclusionProofError::MmrError(mmr_err) => Self::NoteInclusionMmr(mmr_err), + GetNoteAuthenticationInfoError::DatabaseError(db_err) => db_err.into(), + GetNoteAuthenticationInfoError::MmrError(mmr_err) => Self::NoteInclusionMmr(mmr_err), } } } @@ -274,9 +274,24 @@ pub enum NoteSyncError { } #[derive(Error, Debug)] -pub enum GetNoteInclusionProofError { +pub enum GetNoteAuthenticationInfoError { #[error("database error")] DatabaseError(#[from] DatabaseError), #[error("Mmr error")] MmrError(#[from] MmrError), } + +#[derive(Error, Debug)] +pub enum GetBatchInputsError { + #[error("failed to select note inclusion proofs")] + SelectNoteInclusionProofError(#[source] DatabaseError), + #[error("failed to select block headers")] + SelectBlockHeaderError(#[source] DatabaseError), + #[error("set of blocks refernced by transactions is empty")] + TransactionBlockReferencesEmpty, + #[error("highest block number {highest_block_num} referenced by a transaction is newer than the latest block {latest_block_num}")] + TransactionBlockReferenceNewerThanLatestBlock { + highest_block_num: BlockNumber, + latest_block_num: BlockNumber, + }, +} diff --git a/crates/store/src/server/api.rs b/crates/store/src/server/api.rs index 9cb196861..84a0ff33e 100644 --- a/crates/store/src/server/api.rs +++ b/crates/store/src/server/api.rs @@ -1,28 +1,24 @@ -use std::{collections::BTreeSet, sync::Arc}; +use std::{collections::BTreeSet, convert::Infallible, sync::Arc}; use miden_node_proto::{ convert, - domain::{ - account::{AccountInfo, AccountProofRequest}, - note::NoteAuthenticationInfo, - }, + domain::account::{AccountInfo, AccountProofRequest}, errors::ConversionError, generated::{ self, account::AccountSummary, - note::NoteAuthenticationInfo as NoteAuthenticationInfoProto, requests::{ ApplyBlockRequest, CheckNullifiersByPrefixRequest, CheckNullifiersRequest, GetAccountDetailsRequest, GetAccountProofsRequest, GetAccountStateDeltaRequest, - GetBlockByNumberRequest, GetBlockHeaderByNumberRequest, GetBlockInputsRequest, - GetNoteAuthenticationInfoRequest, GetNotesByIdRequest, GetTransactionInputsRequest, + GetBatchInputsRequest, GetBlockByNumberRequest, GetBlockHeaderByNumberRequest, + GetBlockInputsRequest, GetNotesByIdRequest, GetTransactionInputsRequest, SyncNoteRequest, SyncStateRequest, }, responses::{ AccountTransactionInputRecord, ApplyBlockResponse, CheckNullifiersByPrefixResponse, CheckNullifiersResponse, GetAccountDetailsResponse, GetAccountProofsResponse, - GetAccountStateDeltaResponse, GetBlockByNumberResponse, GetBlockHeaderByNumberResponse, - GetBlockInputsResponse, GetNoteAuthenticationInfoResponse, GetNotesByIdResponse, + GetAccountStateDeltaResponse, GetBatchInputsResponse, GetBlockByNumberResponse, + GetBlockHeaderByNumberResponse, GetBlockInputsResponse, GetNotesByIdResponse, GetTransactionInputsResponse, NullifierTransactionInputRecord, NullifierUpdate, SyncNoteResponse, SyncStateResponse, }, @@ -279,42 +275,6 @@ impl api_server::Api for StoreApi { Ok(Response::new(GetNotesByIdResponse { notes })) } - /// Returns the inclusion proofs of the specified notes. - #[instrument( - target = COMPONENT, - name = "store:get_note_inclusion_proofs", - skip_all, - ret(level = "debug"), - err - )] - async fn get_note_authentication_info( - &self, - request: Request, - ) -> Result, Status> { - info!(target: COMPONENT, ?request); - - let note_ids = request.into_inner().note_ids; - - let note_ids: Vec = try_convert(note_ids) - .map_err(|err| Status::invalid_argument(format!("Invalid NoteId: {err}")))?; - - let note_ids = note_ids.into_iter().map(From::from).collect(); - - let NoteAuthenticationInfo { block_proofs, note_proofs } = self - .state - .get_note_authentication_info(note_ids) - .await - .map_err(internal_error)?; - - // Massage into shape required by protobuf - let note_proofs = note_proofs.iter().map(Into::into).collect(); - let block_proofs = block_proofs.into_iter().map(Into::into).collect(); - - Ok(Response::new(GetNoteAuthenticationInfoResponse { - proofs: Some(NoteAuthenticationInfoProto { note_proofs, block_proofs }), - })) - } - /// Returns details for public (public) account by id. #[instrument( target = COMPONENT, @@ -402,6 +362,39 @@ impl api_server::Api for StoreApi { .map_err(internal_error) } + /// Fetches the inputs for a transaction batch from the database. + /// + /// See [`State::get_batch_inputs`] for details. + #[instrument( + target = COMPONENT, + name = "store:get_batch_inputs", + skip_all, + ret(level = "debug"), + err + )] + async fn get_batch_inputs( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + + let note_ids: Vec = try_convert(request.note_ids) + .map_err(|err| Status::invalid_argument(format!("Invalid NoteId: {err}")))?; + let note_ids = note_ids.into_iter().map(NoteId::from).collect(); + + let reference_blocks: Vec = + try_convert::<_, Infallible, _, _, _>(request.reference_blocks) + .expect("operation should be infallible"); + let reference_blocks = reference_blocks.into_iter().map(BlockNumber::from).collect(); + + self.state + .get_batch_inputs(reference_blocks, note_ids) + .await + .map(Into::into) + .map(Response::new) + .map_err(internal_error) + } + #[instrument( target = COMPONENT, name = "store:get_transaction_inputs", diff --git a/crates/store/src/state.rs b/crates/store/src/state.rs index f3ae2dc99..a6d07fc55 100644 --- a/crates/store/src/state.rs +++ b/crates/store/src/state.rs @@ -13,6 +13,7 @@ use miden_node_proto::{ convert, domain::{ account::{AccountInfo, AccountProofRequest, StorageMapKeysProof}, + batch::BatchInputs, block::BlockInclusionProof, note::NoteAuthenticationInfo, }, @@ -28,11 +29,12 @@ use miden_objects::{ crypto::{ hash::rpo::RpoDigest, merkle::{ - LeafIndex, Mmr, MmrDelta, MmrError, MmrPeaks, MmrProof, SimpleSmt, SmtProof, ValuePath, + LeafIndex, Mmr, MmrDelta, MmrError, MmrPeaks, MmrProof, PartialMmr, SimpleSmt, + SmtProof, ValuePath, }, }, note::{NoteId, Nullifier}, - transaction::OutputNote, + transaction::{ChainMmr, OutputNote}, utils::Serializable, AccountError, ACCOUNT_TREE_DEPTH, }; @@ -46,9 +48,9 @@ use crate::{ blocks::BlockStore, db::{Db, NoteRecord, NoteSyncUpdate, NullifierInfo, StateSyncUpdate}, errors::{ - ApplyBlockError, DatabaseError, GetBlockHeaderError, GetBlockInputsError, - GetNoteInclusionProofError, InvalidBlockError, NoteSyncError, StateInitializationError, - StateSyncError, + ApplyBlockError, DatabaseError, GetBatchInputsError, GetBlockHeaderError, + GetBlockInputsError, GetNoteAuthenticationInfoError, InvalidBlockError, NoteSyncError, + StateInitializationError, StateSyncError, }, nullifier_tree::NullifierTree, COMPONENT, @@ -438,8 +440,8 @@ impl State { pub async fn get_note_authentication_info( &self, note_ids: BTreeSet, - ) -> Result { - // First we grab block-inclusion proofs for the known notes. These proofs only + ) -> Result { + // First we grab note inclusion proofs for the known notes. These proofs only // prove that the note was included in a given block. We then also need to prove that // each of those blocks is included in the chain. let note_proofs = self.db.select_note_inclusion_proofs(note_ids).await?; @@ -494,6 +496,145 @@ impl State { Ok(NoteAuthenticationInfo { block_proofs, note_proofs }) } + /// Fetches the inputs for a transaction batch from the database. + /// + /// ## Inputs + /// + /// The function takes as input: + /// - The tx reference blocks are the set of blocks referenced by transactions in the batch. + /// - The unauthenticated note ids are the set of IDs of unauthenticated notes consumed by all + /// transactions in the batch. For these notes, we attempt to find note inclusion proofs. Not + /// all notes will exist in the DB necessarily, as some notes can be created and consumed + /// within the same batch. + /// + /// ## Outputs + /// + /// The function will return: + /// - A block inclusion proof for all tx reference blocks and for all blocks which are + /// referenced by a note inclusion proof. + /// - Note inclusion proofs for all notes that were found in the DB. + /// - The block header that the batch should reference, i.e. the latest known block. + pub async fn get_batch_inputs( + &self, + tx_reference_blocks: BTreeSet, + unauthenticated_note_ids: BTreeSet, + ) -> Result { + if tx_reference_blocks.is_empty() { + return Err(GetBatchInputsError::TransactionBlockReferencesEmpty); + } + + // First we grab note inclusion proofs for the known notes. These proofs only + // prove that the note was included in a given block. We then also need to prove that + // each of those blocks is included in the chain. + let note_proofs = self + .db + .select_note_inclusion_proofs(unauthenticated_note_ids) + .await + .map_err(GetBatchInputsError::SelectNoteInclusionProofError)?; + + // The set of blocks that the notes are included in. + let note_blocks = note_proofs.values().map(|proof| proof.location().block_num()); + + // Collect all blocks we need to query without duplicates, which is: + // - all blocks for which we need to prove note inclusion. + // - all blocks referenced by transactions in the batch. + let mut blocks = tx_reference_blocks; + blocks.extend(note_blocks); + + // Grab the block merkle paths from the inner state. + // + // NOTE: Scoped block to automatically drop the mutex guard asap. + // + // We also avoid accessing the db in the block as this would delay + // dropping the guard. + let (batch_reference_block, partial_mmr) = { + let state = self.inner.read().await; + let latest_block_num = state.latest_block_num(); + + let highest_block_num = + *blocks.last().expect("we should have checked for empty block references"); + if highest_block_num > latest_block_num { + return Err(GetBatchInputsError::TransactionBlockReferenceNewerThanLatestBlock { + highest_block_num, + latest_block_num, + }); + } + + // Remove the latest block from the to-be-tracked blocks as it will be the reference + // block for the batch itself and thus added to the MMR within the batch kernel, so + // there is no need to prove its inclusion. + blocks.remove(&latest_block_num); + + // 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_num.as_usize(); + let peaks = state + .chain_mmr + .peaks_at(target_forest) + .expect("target_forest should be smaller than forest of the chain mmr"); + 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 = state + .chain_mmr + .get(block_num) + .expect("block num less than chain length should exist in chain mmr"); + let path = state + .chain_mmr + .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"); + } + + (latest_block_num, partial_mmr) + }; + + // TODO: Unnecessary conversion. We should change the select_block_headers function to take + // an impl Iterator instead to avoid this allocation. + let mut blocks: Vec<_> = blocks.into_iter().collect(); + // Fetch the reference block of the batch as part of this query, so we can avoid looking it + // up in a separate DB access. + blocks.push(batch_reference_block); + let mut headers = self + .db + .select_block_headers(blocks) + .await + .map_err(GetBatchInputsError::SelectBlockHeaderError)?; + + // Find and remove the batch reference block as we don't want to add it to the chain MMR. + let header_index = headers + .iter() + .enumerate() + .find_map(|(index, header)| { + (header.block_num() == batch_reference_block).then_some(index) + }) + .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. + let batch_reference_block_header = headers.swap_remove(header_index); + + // SAFETY: This should not error because: + // - we're passing exactly the block headers that we've added to the partial MMR, + // - 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) + .expect("partial mmr and block headers should be consistent"); + + Ok(BatchInputs { + batch_reference_block_header, + note_proofs, + chain_mmr, + }) + } + /// Loads data to synchronize a client. /// /// The client's request contains a list of tag prefixes, this method will return the first diff --git a/proto/requests.proto b/proto/requests.proto index 1230eac78..f2323c56c 100644 --- a/proto/requests.proto +++ b/proto/requests.proto @@ -86,6 +86,14 @@ message GetBlockInputsRequest { repeated digest.Digest unauthenticated_notes = 3; } +// Returns the inputs for a transaction batch. +message GetBatchInputsRequest { + // List of unauthenticated notes to be queried from the database. + repeated digest.Digest note_ids = 1; + // Set of block numbers referenced by transactions. + repeated fixed32 reference_blocks = 2; +} + // Returns data required to validate a new transaction. message GetTransactionInputsRequest { // ID of the account against which a transaction is executed. @@ -112,12 +120,6 @@ message GetNotesByIdRequest { repeated digest.Digest note_ids = 1; } -// Returns a list of Note inclusion proofs for the specified Note IDs. -message GetNoteAuthenticationInfoRequest { - // List of notes to be queried from the database. - repeated digest.Digest note_ids = 1; -} - // Returns the latest state of an account with the specified ID. message GetAccountDetailsRequest { // Account ID to get details. diff --git a/proto/responses.proto b/proto/responses.proto index 36e175d1e..f1dfe5f90 100644 --- a/proto/responses.proto +++ b/proto/responses.proto @@ -128,6 +128,20 @@ message GetBlockInputsResponse { note.NoteAuthenticationInfo found_unauthenticated_notes = 5; } +// Represents the result of getting batch inputs. +message GetBatchInputsResponse { + // The block header that the transaction batch should reference. + block.BlockHeader batch_reference_block_header = 1; + + // Proof of each _found_ unauthenticated note's inclusion in a block. + repeated note.NoteInclusionInBlockProof note_proofs = 2; + + // 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; +} + // An account returned as a response to the `GetTransactionInputs`. message AccountTransactionInputRecord { // The account ID. @@ -173,12 +187,6 @@ message GetNotesByIdResponse { repeated note.Note notes = 1; } -// Represents the result of getting note authentication info. -message GetNoteAuthenticationInfoResponse { - // Proofs of note inclusions in blocks and block inclusions in chain. - note.NoteAuthenticationInfo proofs = 1; -} - // Represents the result of getting account details. message GetAccountDetailsResponse { // Account info (with details for public accounts). diff --git a/proto/store.proto b/proto/store.proto index 0562b8c54..7137121dc 100644 --- a/proto/store.proto +++ b/proto/store.proto @@ -39,8 +39,8 @@ service Api { // Returns data required to prove the next block. rpc GetBlockInputs(requests.GetBlockInputsRequest) returns (responses.GetBlockInputsResponse) {} - // Returns a list of Note inclusion proofs for the specified Note IDs. - rpc GetNoteAuthenticationInfo(requests.GetNoteAuthenticationInfoRequest) returns (responses.GetNoteAuthenticationInfoResponse) {} + // Returns the inputs for a transaction batch. + rpc GetBatchInputs(requests.GetBatchInputsRequest) returns (responses.GetBatchInputsResponse) {} // Returns a list of notes matching the provided note IDs. rpc GetNotesById(requests.GetNotesByIdRequest) returns (responses.GetNotesByIdResponse) {}