diff --git a/Cargo.lock b/Cargo.lock index 8d0aa4ecc..9b3281f3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1563,7 +1563,7 @@ dependencies = [ [[package]] name = "miden-lib" version = "0.4.0" -source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#861837b2f5ce4771af3a627e7e06f8cdcb672e80" +source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#c29d1def0bca6a66115a6011df1171d836563b90" dependencies = [ "miden-assembly", "miden-objects", @@ -1710,7 +1710,7 @@ dependencies = [ [[package]] name = "miden-objects" version = "0.4.0" -source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#861837b2f5ce4771af3a627e7e06f8cdcb672e80" +source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#c29d1def0bca6a66115a6011df1171d836563b90" dependencies = [ "miden-assembly", "miden-core", @@ -1761,7 +1761,7 @@ dependencies = [ [[package]] name = "miden-tx" version = "0.4.0" -source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#861837b2f5ce4771af3a627e7e06f8cdcb672e80" +source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#c29d1def0bca6a66115a6011df1171d836563b90" dependencies = [ "miden-lib", "miden-objects", diff --git a/crates/block-producer/src/batch_builder/batch.rs b/crates/block-producer/src/batch_builder/batch.rs index 14e181f81..010312a0f 100644 --- a/crates/block-producer/src/batch_builder/batch.rs +++ b/crates/block-producer/src/batch_builder/batch.rs @@ -1,12 +1,18 @@ -use std::collections::BTreeSet; +use std::{ + collections::{BTreeMap, BTreeSet}, + mem, +}; use miden_objects::{ accounts::AccountId, batches::BatchNoteTree, block::BlockAccountUpdate, - crypto::hash::blake::{Blake3Digest, Blake3_256}, - notes::{NoteId, Nullifier}, - transaction::{OutputNote, TransactionId, TxAccountUpdate}, + crypto::{ + hash::blake::{Blake3Digest, Blake3_256}, + merkle::MerklePath, + }, + notes::{NoteHeader, NoteId, Nullifier}, + transaction::{InputNoteCommitment, OutputNote, TransactionId, TxAccountUpdate}, Digest, MAX_NOTES_PER_BATCH, }; use tracing::instrument; @@ -26,29 +32,37 @@ pub type BatchId = Blake3Digest<32>; pub struct TransactionBatch { id: BatchId, updated_accounts: Vec<(TransactionId, TxAccountUpdate)>, - unauthenticated_input_notes: BTreeSet, - produced_nullifiers: Vec, + input_notes: Vec, output_notes_smt: BatchNoteTree, output_notes: Vec, } impl TransactionBatch { - // CONSTRUCTOR + // CONSTRUCTORS // -------------------------------------------------------------------------------------------- + /// Returns a new [TransactionBatch] instantiated from the provided vector of proven - /// transactions. + /// transactions. If a map of unauthenticated notes found in the store is provided, it is used + /// for transforming unauthenticated notes into authenticated notes. /// /// # Errors /// Returns an error if: - /// - The number of created notes across all transactions exceeds 4096. + /// - The number of output notes across all transactions exceeds 4096. + /// - 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. /// /// TODO: enforce limit on the number of created nullifiers. #[instrument(target = "miden-block-producer", name = "new_batch", skip_all, err)] - pub fn new(txs: Vec) -> Result { + pub fn new( + txs: Vec, + found_unauthenticated_notes: Option>, + ) -> Result { let id = Self::compute_id(&txs); + // Populate batch output notes and updated accounts. + let mut output_notes = OutputNoteTracker::new(&txs)?; let mut updated_accounts = vec![]; - let mut produced_nullifiers = vec![]; let mut unauthenticated_input_notes = BTreeSet::new(); for tx in &txs { // TODO: we need to handle a possibility that a batch contains multiple transactions against @@ -56,50 +70,62 @@ impl TransactionBatch { // transaction `y` takes account from state `B` to `C`). These will need to be merged // into a single "update" `A` to `C`. updated_accounts.push((tx.id(), tx.account_update().clone())); - - for note in tx.input_notes() { - produced_nullifiers.push(note.nullifier()); - if let Some(header) = note.header() { - if !unauthenticated_input_notes.insert(header.id()) { - return Err(BuildBatchError::DuplicateUnauthenticatedNote( - header.id(), - txs, - )); - } + // 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, txs.clone())); } } } - // Populate batch output notes, filtering out unauthenticated notes consumed in the same batch. - // Consumed notes are also removed from the unauthenticated input notes set in order to avoid - // consumption of notes with the same ID by one single input. + // 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. We also don't add nullifiers for such output notes to the produced + // nullifiers 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 output_notes: Vec<_> = txs - .iter() - .flat_map(|tx| tx.output_notes().iter()) - .filter(|¬e| !unauthenticated_input_notes.remove(¬e.id())) - .cloned() - .collect(); + let mut input_notes = vec![]; + for input_note in txs.iter().flat_map(|tx| 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, &txs)? { + continue; + } + + match found_unauthenticated_notes { + Some(ref found_notes) => match found_notes.get(&input_note_header.id()) { + Some(_path) => input_note.nullifier().into(), + None => input_note.clone(), + }, + None => input_note.clone(), + } + }, + None => input_note.clone(), + }; + input_notes.push(input_note) + } + + let output_notes = output_notes.into_notes(); if output_notes.len() > MAX_NOTES_PER_BATCH { return Err(BuildBatchError::TooManyNotesCreated(output_notes.len(), txs)); } - // TODO: document under what circumstances SMT creating can fail + // Build the output notes SMT. let output_notes_smt = BatchNoteTree::with_contiguous_leaves( output_notes.iter().map(|note| (note.id(), note.metadata())), ) - .map_err(|e| BuildBatchError::NotesSmtError(e, txs))?; + .expect("Unreachable: fails only if the output note list contains duplicates"); Ok(Self { id, updated_accounts, - unauthenticated_input_notes, - produced_nullifiers, + input_notes, output_notes_smt, output_notes, }) @@ -134,14 +160,15 @@ impl TransactionBatch { }) } - /// Returns unauthenticated input notes set consumed by the transactions in this batch. - pub fn unauthenticated_input_notes(&self) -> &BTreeSet { - &self.unauthenticated_input_notes + /// 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.produced_nullifiers.iter().cloned() + self.input_notes.iter().map(InputNoteCommitment::nullifier) } /// Returns the root hash of the output notes SMT. @@ -165,3 +192,55 @@ impl TransactionBatch { Blake3_256::hash(&buf) } } + +struct OutputNoteTracker { + output_notes: Vec>, + output_note_index: BTreeMap, +} + +impl OutputNoteTracker { + fn new(txs: &[ProvenTransaction]) -> 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(), txs.to_vec())); + } + output_notes.push(Some(note.clone())); + } + } + + Ok(Self { output_notes, output_note_index }) + } + + pub fn remove_note( + &mut self, + input_note_header: &NoteHeader, + txs: &[ProvenTransaction], + ) -> 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, + txs: txs.to_vec(), + }); + } + + return Ok(true); + } + } + + Ok(false) + } + + pub fn into_notes(self) -> Vec { + self.output_notes.into_iter().flatten().collect() + } +} diff --git a/crates/block-producer/src/batch_builder/mod.rs b/crates/block-producer/src/batch_builder/mod.rs index 702ee7e6c..ab54615c4 100644 --- a/crates/block-producer/src/batch_builder/mod.rs +++ b/crates/block-producer/src/batch_builder/mod.rs @@ -169,23 +169,28 @@ where // TODO: this can be optimized by first computing dangling notes of the batch itself, // and only then checking against the other ready batches let dangling_notes = self.find_dangling_notes(&txs).await; - if !dangling_notes.is_empty() { - let stored_notes = - match self.store.get_note_authentication_info(dangling_notes.iter()).await { - Ok(stored_notes) => stored_notes, - Err(err) => return Err(BuildBatchError::NotePathsError(err, txs)), - }; - let missing_notes: Vec<_> = dangling_notes - .into_iter() - .filter(|note_id| !stored_notes.contains_key(note_id)) - .collect(); - - if !missing_notes.is_empty() { - return Err(BuildBatchError::UnauthenticatedNotesNotFound(missing_notes, txs)); - } - } + let found_unauthenticated_notes = match dangling_notes.is_empty() { + true => None, + false => { + let stored_notes = + match self.store.get_note_authentication_info(dangling_notes.iter()).await { + Ok(stored_notes) => stored_notes, + Err(err) => return Err(BuildBatchError::NotePathsError(err, txs)), + }; + let missing_notes: Vec<_> = dangling_notes + .into_iter() + .filter(|note_id| !stored_notes.contains_key(note_id)) + .collect(); + + if !missing_notes.is_empty() { + return Err(BuildBatchError::UnauthenticatedNotesNotFound(missing_notes, txs)); + } + + Some(stored_notes) + }, + }; - let batch = TransactionBatch::new(txs)?; + let batch = TransactionBatch::new(txs, found_unauthenticated_notes)?; info!(target: COMPONENT, "Transaction batch built"); Span::current().record("batch_id", format_blake3_digest(batch.id())); diff --git a/crates/block-producer/src/batch_builder/tests/mod.rs b/crates/block-producer/src/batch_builder/tests/mod.rs index 07e7344c4..a20686826 100644 --- a/crates/block-producer/src/batch_builder/tests/mod.rs +++ b/crates/block-producer/src/batch_builder/tests/mod.rs @@ -155,5 +155,5 @@ fn dummy_tx_batch(starting_account_index: u32, num_txs_in_batch: usize) -> Trans MockProvenTxBuilder::with_account_index(starting_account_index + index as u32).build() }) .collect(); - TransactionBatch::new(txs).unwrap() + TransactionBatch::new(txs, None).unwrap() } diff --git a/crates/block-producer/src/block_builder/mod.rs b/crates/block-producer/src/block_builder/mod.rs index 63258f451..0c63ffae4 100644 --- a/crates/block-producer/src/block_builder/mod.rs +++ b/crates/block-producer/src/block_builder/mod.rs @@ -4,7 +4,8 @@ use async_trait::async_trait; use miden_node_utils::formatting::{format_array, format_blake3_digest}; use miden_objects::{ block::{Block, BlockAccountUpdate}, - notes::Nullifier, + notes::{NoteHeader, Nullifier}, + transaction::InputNoteCommitment, }; use tracing::{debug, info, instrument}; @@ -76,24 +77,29 @@ where let updated_accounts: Vec<_> = batches.iter().flat_map(TransactionBatch::updated_accounts).collect(); - let created_notes: Vec<_> = + let output_notes: Vec<_> = batches.iter().map(TransactionBatch::output_notes).cloned().collect(); let produced_nullifiers: Vec = batches.iter().flat_map(TransactionBatch::produced_nullifiers).collect(); - let created_notes_set: BTreeSet<_> = created_notes + // Populate set of output notes from all batches + let output_notes_set: BTreeSet<_> = output_notes .iter() .flat_map(|batch| batch.iter().map(|note| note.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::unauthenticated_input_notes) - .filter(|¬e_id| !created_notes_set.contains(note_id)) - .copied() + .flat_map(TransactionBatch::input_notes) + .filter_map(InputNoteCommitment::header) + .map(NoteHeader::id) + .filter(|note_id| !output_notes_set.contains(note_id)) .collect(); + // Request information needed for block building from the store let block_inputs = self .store .get_block_inputs( @@ -119,7 +125,7 @@ where // TODO: return an error? let block = - Block::new(new_block_header, updated_accounts, created_notes, produced_nullifiers) + Block::new(new_block_header, updated_accounts, output_notes, produced_nullifiers) .expect("invalid block components"); let block_hash = block.hash(); diff --git a/crates/block-producer/src/block_builder/prover/tests.rs b/crates/block-producer/src/block_builder/prover/tests.rs index d65941c16..a0cf4ea23 100644 --- a/crates/block-producer/src/block_builder/prover/tests.rs +++ b/crates/block-producer/src/block_builder/prover/tests.rs @@ -69,7 +69,7 @@ fn test_block_witness_validation_inconsistent_account_ids() { ) .build(); - TransactionBatch::new(vec![tx]).unwrap() + TransactionBatch::new(vec![tx], None).unwrap() }; let batch_2 = { @@ -80,7 +80,7 @@ fn test_block_witness_validation_inconsistent_account_ids() { ) .build(); - TransactionBatch::new(vec![tx]).unwrap() + TransactionBatch::new(vec![tx], None).unwrap() }; vec![batch_1, batch_2] @@ -134,19 +134,25 @@ fn test_block_witness_validation_inconsistent_account_hashes() { }; let batches = { - let batch_1 = TransactionBatch::new(vec![MockProvenTxBuilder::with_account( - account_id_1, - account_1_hash_batches, - Digest::default(), + let batch_1 = TransactionBatch::new( + vec![MockProvenTxBuilder::with_account( + account_id_1, + account_1_hash_batches, + Digest::default(), + ) + .build()], + None, ) - .build()]) .unwrap(); - let batch_2 = TransactionBatch::new(vec![MockProvenTxBuilder::with_account( - account_id_2, - Digest::default(), - Digest::default(), + let batch_2 = TransactionBatch::new( + vec![MockProvenTxBuilder::with_account( + account_id_2, + Digest::default(), + Digest::default(), + ) + .build()], + None, ) - .build()]) .unwrap(); vec![batch_1, batch_2] @@ -229,8 +235,8 @@ async fn test_compute_account_root_success() { }) .collect(); - let batch_1 = TransactionBatch::new(txs[..2].to_vec()).unwrap(); - let batch_2 = TransactionBatch::new(txs[2..].to_vec()).unwrap(); + let batch_1 = TransactionBatch::new(txs[..2].to_vec(), None).unwrap(); + let batch_2 = TransactionBatch::new(txs[2..].to_vec(), None).unwrap(); vec![batch_1, batch_2] }; @@ -373,7 +379,7 @@ async fn test_compute_note_root_empty_notes_success() { .unwrap(); let batches: Vec = { - let batch = TransactionBatch::new(Vec::new()).unwrap(); + let batch = TransactionBatch::new(vec![], None).unwrap(); vec![batch] }; @@ -446,8 +452,8 @@ async fn test_compute_note_root_success() { }) .collect(); - let batch_1 = TransactionBatch::new(txs[..2].to_vec()).unwrap(); - let batch_2 = TransactionBatch::new(txs[2..].to_vec()).unwrap(); + let batch_1 = TransactionBatch::new(txs[..2].to_vec(), None).unwrap(); + let batch_2 = TransactionBatch::new(txs[2..].to_vec(), None).unwrap(); vec![batch_1, batch_2] }; @@ -495,13 +501,13 @@ fn test_block_witness_validation_inconsistent_nullifiers() { let batch_1 = { let tx = MockProvenTxBuilder::with_account_index(0).nullifiers_range(0..1).build(); - TransactionBatch::new(vec![tx]).unwrap() + TransactionBatch::new(vec![tx], None).unwrap() }; let batch_2 = { let tx = MockProvenTxBuilder::with_account_index(1).nullifiers_range(1..2).build(); - TransactionBatch::new(vec![tx]).unwrap() + TransactionBatch::new(vec![tx], None).unwrap() }; vec![batch_1, batch_2] @@ -571,13 +577,13 @@ async fn test_compute_nullifier_root_empty_success() { let batch_1 = { let tx = MockProvenTxBuilder::with_account_index(0).build(); - TransactionBatch::new(vec![tx]).unwrap() + TransactionBatch::new(vec![tx], None).unwrap() }; let batch_2 = { let tx = MockProvenTxBuilder::with_account_index(1).build(); - TransactionBatch::new(vec![tx]).unwrap() + TransactionBatch::new(vec![tx], None).unwrap() }; vec![batch_1, batch_2] @@ -624,13 +630,13 @@ async fn test_compute_nullifier_root_success() { let batch_1 = { let tx = MockProvenTxBuilder::with_account_index(0).nullifiers_range(0..1).build(); - TransactionBatch::new(vec![tx]).unwrap() + TransactionBatch::new(vec![tx], None).unwrap() }; let batch_2 = { let tx = MockProvenTxBuilder::with_account_index(1).nullifiers_range(1..2).build(); - TransactionBatch::new(vec![tx]).unwrap() + TransactionBatch::new(vec![tx], None).unwrap() }; vec![batch_1, batch_2] diff --git a/crates/block-producer/src/block_builder/tests.rs b/crates/block-producer/src/block_builder/tests.rs index 63032f6ef..b146c66d6 100644 --- a/crates/block-producer/src/block_builder/tests.rs +++ b/crates/block-producer/src/block_builder/tests.rs @@ -37,7 +37,7 @@ async fn test_apply_block_called_nonempty_batches() { ) .build(); - TransactionBatch::new(vec![tx]).unwrap() + TransactionBatch::new(vec![tx], None).unwrap() }; vec![batch_1] diff --git a/crates/block-producer/src/errors.rs b/crates/block-producer/src/errors.rs index 6093be290..bec167e09 100644 --- a/crates/block-producer/src/errors.rs +++ b/crates/block-producer/src/errors.rs @@ -80,11 +80,22 @@ pub enum BuildBatchError { #[error("Failed to get note paths: {0}")] NotePathsError(NotePathsError, Vec), - #[error("Duplicated note ID in the batch: {0}")] + #[error("Duplicated unauthenticated transaction input note ID in the batch: {0}")] DuplicateUnauthenticatedNote(NoteId, Vec), + #[error("Duplicated transaction output note ID in the batch: {0}")] + DuplicateOutputNote(NoteId, Vec), + #[error("Unauthenticated transaction notes not found in the store: {0:?}")] UnauthenticatedNotesNotFound(Vec, Vec), + + #[error("Note hashes mismatch for note {id}: (input: {input_hash}, output: {output_hash})")] + NoteHashesMismatch { + id: NoteId, + input_hash: Digest, + output_hash: Digest, + txs: Vec, + }, } impl BuildBatchError { @@ -94,7 +105,9 @@ impl BuildBatchError { BuildBatchError::NotesSmtError(_, txs) => txs, BuildBatchError::NotePathsError(_, txs) => txs, BuildBatchError::DuplicateUnauthenticatedNote(_, txs) => txs, + BuildBatchError::DuplicateOutputNote(_, txs) => txs, BuildBatchError::UnauthenticatedNotesNotFound(_, txs) => txs, + BuildBatchError::NoteHashesMismatch { txs, .. } => txs, } } } diff --git a/crates/block-producer/src/test_utils/batch.rs b/crates/block-producer/src/test_utils/batch.rs index adec43bd1..f340824d9 100644 --- a/crates/block-producer/src/test_utils/batch.rs +++ b/crates/block-producer/src/test_utils/batch.rs @@ -24,7 +24,7 @@ impl TransactionBatchConstructor for TransactionBatch { }) .collect(); - Self::new(txs).unwrap() + Self::new(txs, None).unwrap() } fn from_txs(starting_account_index: u32, num_txs_in_batch: u64) -> Self { @@ -36,6 +36,6 @@ impl TransactionBatchConstructor for TransactionBatch { }) .collect(); - Self::new(txs).unwrap() + Self::new(txs, None).unwrap() } } diff --git a/crates/block-producer/src/txqueue/tests/mod.rs b/crates/block-producer/src/txqueue/tests/mod.rs index 8af60bc96..079290449 100644 --- a/crates/block-producer/src/txqueue/tests/mod.rs +++ b/crates/block-producer/src/txqueue/tests/mod.rs @@ -40,7 +40,8 @@ impl BatchBuilderSuccess { #[async_trait] impl BatchBuilder for BatchBuilderSuccess { async fn build_batch(&self, txs: Vec) -> Result<(), BuildBatchError> { - let batch = TransactionBatch::new(txs).expect("Tx batch building should have succeeded"); + let batch = + TransactionBatch::new(txs, None).expect("Tx batch building should have succeeded"); self.ready_batches .send(batch) .expect("Sending to channel should have succeeded"); @@ -104,7 +105,7 @@ async fn test_build_batch_success() { receiver.try_recv(), "A single transaction produces a single batch" ); - let expected = TransactionBatch::new(vec![tx.clone()]).expect("Valid transactions"); + let expected = TransactionBatch::new(vec![tx.clone()], None).expect("Valid transactions"); assert_eq!(expected, batch, "The batch should have the one transaction added to the queue"); // a batch will include up to `batch_size` transactions @@ -123,7 +124,7 @@ async fn test_build_batch_success() { receiver.try_recv(), "{batch_size} transactions create a single batch" ); - let expected = TransactionBatch::new(txs).expect("Valid transactions"); + let expected = TransactionBatch::new(txs, None).expect("Valid transactions"); assert_eq!(expected, batch, "The batch should the transactions to fill a batch"); // the transaction queue eagerly produces batches @@ -138,7 +139,7 @@ async fn test_build_batch_success() { for expected_batch in txs.chunks(batch_size).map(|txs| txs.to_vec()) { tokio::time::advance(build_batch_frequency).await; let batch = receiver.try_recv().expect("Queue not empty"); - let expected = TransactionBatch::new(expected_batch).expect("Valid transactions"); + let expected = TransactionBatch::new(expected_batch, None).expect("Valid transactions"); assert_eq!(expected, batch, "The batch should the transactions to fill a batch"); } diff --git a/crates/store/src/db/mod.rs b/crates/store/src/db/mod.rs index 934ea33d1..26a192d54 100644 --- a/crates/store/src/db/mod.rs +++ b/crates/store/src/db/mod.rs @@ -171,14 +171,6 @@ impl Db { })? } - /// Loads all the note IDs from the DB. - #[instrument(target = "miden-store", skip_all, ret(level = "debug"), err)] - pub async fn select_note_ids(&self) -> Result> { - self.pool.get().await?.interact(sql::select_note_ids).await.map_err(|err| { - DatabaseError::InteractError(format!("Select note IDs task failed: {err}")) - })? - } - /// Loads all the accounts from the DB. #[instrument(target = "miden-store", skip_all, ret(level = "debug"), err)] pub async fn select_accounts(&self) -> Result> { @@ -288,6 +280,14 @@ impl Db { })? } + /// Loads all note IDs matching a certain NoteId from the database. + #[instrument(target = "miden-store", skip_all, ret(level = "debug"), err)] + pub async fn select_note_ids(&self, note_ids: Vec) -> Result> { + self.select_notes_by_id(note_ids) + .await + .map(|notes| notes.into_iter().map(|note| note.note_id.into()).collect()) + } + /// Inserts the data of a new block into the DB. /// /// `allow_acquire` and `acquire_done` are used to synchronize writes to the DB with writes to diff --git a/crates/store/src/db/sql.rs b/crates/store/src/db/sql.rs index 06090e635..018c2bb31 100644 --- a/crates/store/src/db/sql.rs +++ b/crates/store/src/db/sql.rs @@ -1,6 +1,6 @@ //! Wrapper functions for SQL statements. -use std::{borrow::Cow, collections::BTreeSet, rc::Rc}; +use std::{borrow::Cow, rc::Rc}; use miden_node_proto::domain::accounts::{AccountInfo, AccountSummary}; use miden_objects::{ @@ -369,25 +369,6 @@ pub fn select_notes(conn: &mut Connection) -> Result> { Ok(notes) } -/// Select all note IDs from the DB using the given [Connection]. -/// -/// # Returns -/// -/// A set with note IDs, or an error. -pub fn select_note_ids(conn: &mut Connection) -> Result> { - let mut stmt = conn.prepare("SELECT note_id FROM notes ORDER BY note_id")?; - let mut rows = stmt.query([])?; - - let mut notes = BTreeSet::new(); - while let Some(row) = rows.next()? { - let note_id_data = row.get_ref(0)?.as_blob()?; - let note_id = RpoDigest::read_from_bytes(note_id_data)?.into(); - - notes.insert(note_id); - } - Ok(notes) -} - /// Insert notes to the DB using the given [Transaction]. /// /// # Returns diff --git a/crates/store/src/server/api.rs b/crates/store/src/server/api.rs index 9f4c90b09..dad6408c4 100644 --- a/crates/store/src/server/api.rs +++ b/crates/store/src/server/api.rs @@ -36,11 +36,7 @@ use miden_objects::{ use tonic::{Response, Status}; use tracing::{debug, info, instrument}; -use crate::{ - state::{BlockInputs, State}, - types::AccountId, - COMPONENT, -}; +use crate::{state::State, types::AccountId, COMPONENT}; // STORE API // ================================================================================================ @@ -306,25 +302,12 @@ impl api_server::Api for StoreApi { let account_ids: Vec = request.account_ids.iter().map(|e| e.id).collect(); let unauthenticated_notes = validate_notes(&request.unauthenticated_notes)?; - let BlockInputs { - block_header, - chain_peaks, - account_states, - nullifiers, - found_unauthenticated_notes, - } = self - .state - .get_block_inputs(&account_ids, &nullifiers, &unauthenticated_notes) + self.state + .get_block_inputs(&account_ids, &nullifiers, unauthenticated_notes) .await - .map_err(internal_error)?; - - Ok(Response::new(GetBlockInputsResponse { - block_header: Some(block_header.into()), - mmr_peaks: convert(chain_peaks.peaks()), - account_states: convert(account_states), - nullifiers: convert(nullifiers), - found_unauthenticated_notes: convert(found_unauthenticated_notes), - })) + .map(Into::into) + .map(Response::new) + .map_err(internal_error) } #[instrument( @@ -348,8 +331,9 @@ impl api_server::Api for StoreApi { let tx_inputs = self .state - .get_transaction_inputs(account_id, &nullifiers, &unauthenticated_notes) - .await; + .get_transaction_inputs(account_id, &nullifiers, unauthenticated_notes) + .await + .map_err(internal_error)?; Ok(Response::new(GetTransactionInputsResponse { account_state: Some(AccountTransactionInputRecord { diff --git a/crates/store/src/state.rs b/crates/store/src/state.rs index a25932790..0891d1a01 100644 --- a/crates/store/src/state.rs +++ b/crates/store/src/state.rs @@ -4,7 +4,10 @@ //! data is atomically written, and that reads are consistent. use std::{collections::BTreeSet, sync::Arc}; -use miden_node_proto::{domain::accounts::AccountInfo, AccountInputRecord, NullifierWitness}; +use miden_node_proto::{ + convert, domain::accounts::AccountInfo, generated::responses::GetBlockInputsResponse, + AccountInputRecord, NullifierWitness, +}; use miden_node_utils::formatting::{format_account_id, format_array}; use miden_objects::{ block::Block, @@ -54,7 +57,19 @@ pub struct BlockInputs { pub nullifiers: Vec, /// List of notes found in the store - pub found_unauthenticated_notes: Vec, + pub found_unauthenticated_notes: BTreeSet, +} + +impl From for GetBlockInputsResponse { + fn from(value: BlockInputs) -> Self { + Self { + block_header: Some(value.block_header.into()), + mmr_peaks: convert(value.chain_peaks.peaks()), + account_states: convert(value.account_states), + nullifiers: convert(value.nullifiers), + found_unauthenticated_notes: convert(value.found_unauthenticated_notes), + } + } } #[derive(Debug)] @@ -69,7 +84,6 @@ struct InnerState { nullifier_tree: NullifierTree, chain_mmr: Mmr, account_tree: SimpleSmt, - notes: BTreeSet, } /// The rollup state @@ -100,14 +114,8 @@ impl State { let nullifier_tree = load_nullifier_tree(&mut db).await?; let chain_mmr = load_mmr(&mut db).await?; let account_tree = load_accounts(&mut db).await?; - let notes = load_notes(&mut db).await?; - let inner = RwLock::new(InnerState { - nullifier_tree, - chain_mmr, - account_tree, - notes, - }); + let inner = RwLock::new(InnerState { nullifier_tree, chain_mmr, account_tree }); let writer = Mutex::new(()); let db = Arc::new(db); @@ -181,7 +189,7 @@ impl State { tokio::spawn(async move { store.save_block(block_num, &block_data).await }); // scope to read in-memory data, validate the request, and compute intermediary values - let (account_tree, chain_mmr, nullifier_tree, notes, note_set) = { + let (account_tree, chain_mmr, nullifier_tree, notes) = { let inner = self.inner.read().await; let span = info_span!(target: COMPONENT, "update_in_memory_structs").entered(); @@ -255,7 +263,7 @@ impl State { drop(span); - let (notes, note_set) = block + let notes = block .notes() .map(|(note_index, note)| { let details = match note { @@ -268,23 +276,18 @@ impl State { .get_note_path(note_index) .map_err(ApplyBlockError::UnableToCreateProofForNote)?; - Ok(( - NoteRecord { - block_num, - note_index, - note_id: note.id().into(), - metadata: *note.metadata(), - details, - merkle_path, - }, - note.id(), - )) + Ok(NoteRecord { + block_num, + note_index, + note_id: note.id().into(), + metadata: *note.metadata(), + details, + merkle_path, + }) }) - .collect::, ApplyBlockError>>()? - .into_iter() - .unzip(); + .collect::, ApplyBlockError>>()?; - (account_tree, chain_mmr, nullifier_tree, notes, note_set) + (account_tree, chain_mmr, nullifier_tree, notes) }; // Signals the transaction is ready to be committed, and the write lock can be acquired @@ -331,7 +334,6 @@ impl State { inner.chain_mmr = chain_mmr; inner.nullifier_tree = nullifier_tree; inner.account_tree = account_tree; - inner.notes = note_set; } info!(%block_hash, block_num, COMPONENT, "apply_block successful"); @@ -445,7 +447,7 @@ impl State { &self, account_ids: &[AccountId], nullifiers: &[Nullifier], - unauthenticated_notes: &[NoteId], + unauthenticated_notes: Vec, ) -> Result { let inner = self.inner.read().await; @@ -494,7 +496,7 @@ impl State { }) .collect(); - let found_unauthenticated_notes = filter_found_notes(unauthenticated_notes, &inner.notes); + let found_unauthenticated_notes = self.db.select_note_ids(unauthenticated_notes).await?; Ok(BlockInputs { block_header: latest, @@ -511,8 +513,8 @@ impl State { &self, account_id: AccountId, nullifiers: &[Nullifier], - unauthenticated_notes: &[NoteId], - ) -> TransactionInputs { + unauthenticated_notes: Vec, + ) -> Result { info!(target: COMPONENT, account_id = %format_account_id(account_id), nullifiers = %format_array(nullifiers)); let inner = self.inner.read().await; @@ -527,13 +529,20 @@ impl State { }) .collect(); - let missing_unauthenticated_notes = filter_found_notes(unauthenticated_notes, &inner.notes); + let found_unauthenticated_notes = + self.db.select_note_ids(unauthenticated_notes.clone()).await?; + + let missing_unauthenticated_notes = unauthenticated_notes + .iter() + .filter(|note_id| !found_unauthenticated_notes.contains(note_id)) + .copied() + .collect(); - TransactionInputs { + Ok(TransactionInputs { account_hash, nullifiers, missing_unauthenticated_notes, - } + }) } /// Lists all known nullifiers with their inclusion blocks, intended for testing. @@ -618,13 +627,3 @@ async fn load_accounts( SimpleSmt::with_leaves(account_data) .map_err(StateInitializationError::FailedToCreateAccountsTree) } - -#[instrument(target = "miden-store", skip_all)] -async fn load_notes(db: &mut Db) -> Result, StateInitializationError> { - Ok(db.select_note_ids().await?) -} - -#[instrument(target = "miden-store", skip_all)] -fn filter_found_notes(notes: &[NoteId], in_memory: &BTreeSet) -> Vec { - notes.iter().filter(|¬e_id| in_memory.contains(note_id)).copied().collect() -}