diff --git a/crates/ntx-builder/src/actor/execute.rs b/crates/ntx-builder/src/actor/execute.rs index 59e9cdb4fb..09658cd233 100644 --- a/crates/ntx-builder/src/actor/execute.rs +++ b/crates/ntx-builder/src/actor/execute.rs @@ -55,6 +55,7 @@ use tracing::{Instrument, instrument}; use crate::COMPONENT; use crate::actor::account_state::TransactionCandidate; use crate::block_producer::BlockProducerClient; +use crate::db::Db; use crate::store::StoreClient; #[derive(Debug, thiserror::Error)] @@ -77,6 +78,12 @@ pub enum NtxError { type NtxResult = Result; +/// The result of a successful transaction execution. +/// +/// Contains the transaction ID, any notes that failed during filtering, and note scripts fetched +/// from the remote store that should be persisted to the local DB cache. +pub type NtxExecutionResult = (TransactionId, Vec, Vec<(Word, NoteScript)>); + // NETWORK TRANSACTION CONTEXT // ================================================================================================ @@ -100,6 +107,9 @@ pub struct NtxContext { /// LRU cache for storing retrieved note scripts to avoid repeated store calls. script_cache: LruCache, + + /// Local database for persistent note script caching. + db: Db, } impl NtxContext { @@ -110,6 +120,7 @@ impl NtxContext { prover: Option, store: StoreClient, script_cache: LruCache, + db: Db, ) -> Self { Self { block_producer, @@ -117,6 +128,7 @@ impl NtxContext { prover, store, script_cache, + db, } } @@ -132,8 +144,9 @@ impl NtxContext { /// /// # Returns /// - /// On success, returns the [`TransactionId`] of the executed transaction and a list of - /// [`FailedNote`]s representing notes that were filtered out before execution. + /// On success, returns an [`NtxExecutionResult`] containing the transaction ID, any notes + /// that failed during filtering, and note scripts fetched from the remote store that should + /// be persisted to the local DB cache. /// /// # Errors /// @@ -146,7 +159,7 @@ impl NtxContext { pub fn execute_transaction( self, tx: TransactionCandidate, - ) -> impl FutureMaybeSend)>> { + ) -> impl FutureMaybeSend> { let TransactionCandidate { account, notes, @@ -168,6 +181,7 @@ impl NtxContext { chain_mmr, self.store.clone(), self.script_cache.clone(), + self.db.clone(), ); // Filter notes. @@ -178,6 +192,9 @@ impl NtxContext { // Execute transaction. let executed_tx = Box::pin(self.execute(&data_store, successful_notes)).await?; + // Collect scripts fetched from the remote store during execution. + let scripts_to_cache = data_store.take_fetched_scripts().await; + // Prove transaction. let tx_inputs: TransactionInputs = executed_tx.into(); let proven_tx = Box::pin(self.prove(&tx_inputs)).await?; @@ -188,7 +205,7 @@ impl NtxContext { // Submit transaction to block producer. self.submit(&proven_tx).await?; - Ok((proven_tx.id(), failed_notes)) + Ok((proven_tx.id(), failed_notes, scripts_to_cache)) }) .in_current_span() .await @@ -334,6 +351,11 @@ struct NtxDataStore { store: StoreClient, /// LRU cache for storing retrieved note scripts to avoid repeated store calls. script_cache: LruCache, + /// Local database for persistent note script. + db: Db, + /// Scripts fetched from the remote store during execution, to be persisted by the + /// coordinator. + fetched_scripts: Arc>>, /// Mapping of storage map roots to storage slot names observed during various calls. /// /// The registered slot names are subsequently used to retrieve storage map witnesses from the @@ -366,6 +388,7 @@ impl NtxDataStore { chain_mmr: Arc, store: StoreClient, script_cache: LruCache, + db: Db, ) -> Self { let mast_store = TransactionMastStore::new(); mast_store.load_account_code(account.code()); @@ -377,10 +400,17 @@ impl NtxDataStore { mast_store, store, script_cache, + db, + fetched_scripts: Arc::new(Mutex::new(Vec::new())), storage_slots: Arc::new(Mutex::new(BTreeMap::default())), } } + /// Returns the list of note scripts fetched from the remote store during execution. + async fn take_fetched_scripts(&self) -> Vec<(Word, NoteScript)> { + self.fetched_scripts.lock().await.drain(..).collect() + } + /// Registers storage map slot names for the given account ID and storage header. /// /// These slot names are subsequently used to query for storage map witnesses against the store. @@ -507,28 +537,40 @@ impl DataStore for NtxDataStore { /// Retrieves a note script by its root hash. /// - /// This implementation uses the configured RPC client to call the `GetNoteScriptByRoot` - /// endpoint on the RPC server. + /// Uses a 3-tier lookup strategy: + /// 1. In-memory LRU cache. + /// 2. Local SQLite database. + /// 3. Remote store via gRPC. fn get_note_script( &self, script_root: Word, ) -> impl FutureMaybeSend, DataStoreError>> { async move { - // Attempt to retrieve the script from the cache. + // 1. In-memory LRU cache. if let Some(cached_script) = self.script_cache.get(&script_root).await { return Ok(Some(cached_script)); } - // Retrieve the script from the store. + // 2. Local DB. + if let Some(script) = self.db.lookup_note_script(script_root).await.map_err(|err| { + DataStoreError::other_with_source("failed to look up note script in local DB", err) + })? { + self.script_cache.put(script_root, script.clone()).await; + return Ok(Some(script)); + } + + // 3. Remote store. let maybe_script = self.store.get_note_script_by_root(script_root).await.map_err(|err| { - DataStoreError::Other { - error_msg: "failed to retrieve note script from store".to_string().into(), - source: Some(err.into()), - } + DataStoreError::other_with_source( + "failed to retrieve note script from store", + err, + ) })?; - // Handle response. + if let Some(script) = maybe_script { + // Collect for later persistence by the coordinator. + self.fetched_scripts.lock().await.push((script_root, script.clone())); self.script_cache.put(script_root, script.clone()).await; Ok(Some(script)) } else { diff --git a/crates/ntx-builder/src/actor/mod.rs b/crates/ntx-builder/src/actor/mod.rs index 3b94bd8c3e..ecb72552be 100644 --- a/crates/ntx-builder/src/actor/mod.rs +++ b/crates/ntx-builder/src/actor/mod.rs @@ -17,7 +17,7 @@ use miden_node_utils::lru_cache::LruCache; use miden_protocol::Word; use miden_protocol::account::{Account, AccountDelta}; use miden_protocol::block::BlockNumber; -use miden_protocol::note::{Note, NoteScript}; +use miden_protocol::note::{Note, NoteScript, Nullifier}; use miden_protocol::transaction::TransactionId; use miden_remote_prover_client::RemoteTransactionProver; use tokio::sync::{AcquireError, RwLock, Semaphore, mpsc}; @@ -29,6 +29,21 @@ use crate::builder::ChainState; use crate::db::Db; use crate::store::StoreClient; +// ACTOR NOTIFICATION +// ================================================================================================ + +/// A notification sent from an account actor to the coordinator. +pub enum ActorNotification { + /// One or more notes failed during transaction execution and should have their attempt + /// counters incremented. + NotesFailed { + nullifiers: Vec, + block_num: BlockNumber, + }, + /// A note script was fetched from the remote store and should be persisted to the local DB. + CacheNoteScript { script_root: Word, script: NoteScript }, +} + // ACTOR SHUTDOWN REASON // ================================================================================================ @@ -72,6 +87,8 @@ pub struct AccountActorContext { pub max_note_attempts: usize, /// Database for persistent state. pub db: Db, + /// Channel for sending notifications to the coordinator (via the builder event loop). + pub notification_tx: mpsc::Sender, } // ACCOUNT ORIGIN @@ -173,6 +190,8 @@ pub struct AccountActor { max_notes_per_tx: NonZeroUsize, /// Maximum number of note execution attempts before dropping a note. max_note_attempts: usize, + /// Channel for sending notifications to the coordinator. + notification_tx: mpsc::Sender, } impl AccountActor { @@ -207,6 +226,7 @@ impl AccountActor { script_cache: actor_context.script_cache.clone(), max_notes_per_tx: actor_context.max_notes_per_tx, max_note_attempts: actor_context.max_note_attempts, + notification_tx: actor_context.notification_tx.clone(), } } @@ -272,11 +292,6 @@ impl AccountActor { // Read the chain state. let chain_state = self.chain_state.read().await.clone(); - // Drop notes that have failed too many times. - if let Err(err) = self.db.drop_failing_notes(account_id, self.max_note_attempts).await { - tracing::error!(err = %err, "failed to drop failing notes"); - } - // Query DB for latest account and available notes. let tx_candidate = self.select_candidate_from_db( account_id, @@ -348,17 +363,20 @@ impl AccountActor { self.prover.clone(), self.store.clone(), self.script_cache.clone(), + self.db.clone(), ); let notes = tx_candidate.notes.clone(); let execution_result = context.execute_transaction(tx_candidate).await; match execution_result { // Execution completed without failed notes. - Ok((tx_id, failed)) if failed.is_empty() => { + Ok((tx_id, failed, scripts_to_cache)) if failed.is_empty() => { + self.cache_note_scripts(scripts_to_cache).await; self.mode = ActorMode::TransactionInflight(tx_id); }, // Execution completed with some failed notes. - Ok((tx_id, failed)) => { + Ok((tx_id, failed, scripts_to_cache)) => { + self.cache_note_scripts(scripts_to_cache).await; let nullifiers: Vec<_> = failed.into_iter().map(|note| note.note.nullifier()).collect(); self.mark_notes_failed(&nullifiers, block_num).await; @@ -377,16 +395,26 @@ impl AccountActor { } } - /// Marks notes as failed in the DB. - async fn mark_notes_failed( - &self, - nullifiers: &[miden_protocol::note::Nullifier], - block_num: BlockNumber, - ) { - if let Err(err) = self.db.notes_failed(nullifiers.to_vec(), block_num).await { - tracing::error!(err = %err, "failed to mark notes as failed"); + /// Sends notifications to the coordinator to cache note scripts fetched from the remote store. + async fn cache_note_scripts(&self, scripts: Vec<(Word, NoteScript)>) { + for (script_root, script) in scripts { + let _ = self + .notification_tx + .send(ActorNotification::CacheNoteScript { script_root, script }) + .await; } } + + /// Sends a notification to the coordinator to mark notes as failed. + async fn mark_notes_failed(&self, nullifiers: &[Nullifier], block_num: BlockNumber) { + let _ = self + .notification_tx + .send(ActorNotification::NotesFailed { + nullifiers: nullifiers.to_vec(), + block_num, + }) + .await; + } } // HELPERS diff --git a/crates/ntx-builder/src/builder.rs b/crates/ntx-builder/src/builder.rs index b642d0379c..20090c5b93 100644 --- a/crates/ntx-builder/src/builder.rs +++ b/crates/ntx-builder/src/builder.rs @@ -14,7 +14,7 @@ use tokio_stream::StreamExt; use tonic::Status; use crate::NtxBuilderConfig; -use crate::actor::{AccountActorContext, AccountOrigin}; +use crate::actor::{AccountActorContext, AccountOrigin, ActorNotification}; use crate::coordinator::Coordinator; use crate::db::Db; use crate::store::StoreClient; @@ -98,9 +98,12 @@ pub struct NetworkTransactionBuilder { actor_context: AccountActorContext, /// Stream of mempool events from the block producer. mempool_events: MempoolEventStream, + /// Receiver for notifications from account actors (e.g., note failures). + notification_rx: mpsc::Receiver, } impl NetworkTransactionBuilder { + #[expect(clippy::too_many_arguments)] pub(crate) fn new( config: NtxBuilderConfig, coordinator: Coordinator, @@ -109,6 +112,7 @@ impl NetworkTransactionBuilder { chain_state: Arc>, actor_context: AccountActorContext, mempool_events: MempoolEventStream, + notification_rx: mpsc::Receiver, ) -> Self { Self { config, @@ -118,6 +122,7 @@ impl NetworkTransactionBuilder { chain_state, actor_context, mempool_events, + notification_rx, } } @@ -167,6 +172,10 @@ impl NetworkTransactionBuilder { Some(account_id) = account_rx.recv() => { self.handle_loaded_account(account_id).await?; }, + // Handle actor notifications (DB writes delegated from actors). + Some(notification) = self.notification_rx.recv() => { + self.handle_actor_notification(notification).await; + }, // Handle account loader task completion/failure. // If the task fails, we abort since the builder would be in a degraded state // where existing notes against network accounts won't be processed. @@ -285,6 +294,22 @@ impl NetworkTransactionBuilder { } } + /// Processes a notification from an account actor by performing the corresponding DB write. + async fn handle_actor_notification(&mut self, notification: ActorNotification) { + match notification { + ActorNotification::NotesFailed { nullifiers, block_num } => { + if let Err(err) = self.db.notes_failed(nullifiers, block_num).await { + tracing::error!(err = %err, "failed to mark notes as failed"); + } + }, + ActorNotification::CacheNoteScript { script_root, script } => { + if let Err(err) = self.db.insert_note_script(script_root, &script).await { + tracing::error!(err = %err, "failed to cache note script"); + } + }, + } + } + /// Updates the chain tip and prunes old blocks from the MMR. async fn update_chain_tip(&mut self, tip: BlockHeader) { let mut chain_state = self.chain_state.write().await; diff --git a/crates/ntx-builder/src/db/migrations/2026020900000_setup/up.sql b/crates/ntx-builder/src/db/migrations/2026020900000_setup/up.sql index d8da128a95..68f3793d83 100644 --- a/crates/ntx-builder/src/db/migrations/2026020900000_setup/up.sql +++ b/crates/ntx-builder/src/db/migrations/2026020900000_setup/up.sql @@ -60,3 +60,12 @@ CREATE TABLE notes ( CREATE INDEX idx_notes_account ON notes(account_id); CREATE INDEX idx_notes_created_by ON notes(created_by) WHERE created_by IS NOT NULL; CREATE INDEX idx_notes_consumed_by ON notes(consumed_by) WHERE consumed_by IS NOT NULL; + +-- Persistent cache of note scripts, keyed by script root hash. +-- Survives restarts so scripts don't need to be re-fetched from the store. +CREATE TABLE note_scripts ( + -- Script root hash (Word serialized to 32 bytes). + script_root BLOB PRIMARY KEY, + -- Serialized NoteScript bytes. + script_data BLOB NOT NULL +) WITHOUT ROWID; diff --git a/crates/ntx-builder/src/db/mod.rs b/crates/ntx-builder/src/db/mod.rs index 40709de7c4..47352e29ed 100644 --- a/crates/ntx-builder/src/db/mod.rs +++ b/crates/ntx-builder/src/db/mod.rs @@ -4,10 +4,11 @@ use anyhow::Context; use miden_node_db::DatabaseError; use miden_node_proto::domain::account::NetworkAccountId; use miden_node_proto::domain::note::SingleTargetNetworkNote; +use miden_protocol::Word; use miden_protocol::account::Account; use miden_protocol::account::delta::AccountUpdateDetails; use miden_protocol::block::{BlockHeader, BlockNumber}; -use miden_protocol::note::Nullifier; +use miden_protocol::note::{NoteScript, Nullifier}; use miden_protocol::transaction::TransactionId; use tracing::{info, instrument}; @@ -76,19 +77,6 @@ impl Db { .await } - /// Drops notes for the given account that have exceeded the maximum attempt count. - pub async fn drop_failing_notes( - &self, - account_id: NetworkAccountId, - max_attempts: usize, - ) -> Result<()> { - self.inner - .transact("drop_failing_notes", move |conn| { - queries::drop_failing_notes(conn, account_id, max_attempts) - }) - .await - } - /// Returns the latest account state and available notes for the given account. pub async fn select_candidate( &self, @@ -196,6 +184,25 @@ impl Db { .await } + /// Looks up a cached note script by root hash. + pub async fn lookup_note_script(&self, script_root: Word) -> Result> { + self.inner + .query("lookup_note_script", move |conn| { + queries::lookup_note_script(conn, &script_root) + }) + .await + } + + /// Persists a note script to the local cache. + pub async fn insert_note_script(&self, script_root: Word, script: &NoteScript) -> Result<()> { + let script = script.clone(); + self.inner + .transact("insert_note_script", move |conn| { + queries::insert_note_script(conn, &script_root, &script) + }) + .await + } + /// Creates a file-backed SQLite test connection with migrations applied. #[cfg(test)] pub fn test_conn() -> (diesel::SqliteConnection, tempfile::TempDir) { diff --git a/crates/ntx-builder/src/db/models/conv.rs b/crates/ntx-builder/src/db/models/conv.rs index 2a32994281..26bb99868b 100644 --- a/crates/ntx-builder/src/db/models/conv.rs +++ b/crates/ntx-builder/src/db/models/conv.rs @@ -4,9 +4,10 @@ use miden_node_db::DatabaseError; use miden_node_proto::domain::account::NetworkAccountId; use miden_node_proto::domain::note::SingleTargetNetworkNote; use miden_node_proto::generated as proto; +use miden_protocol::Word; use miden_protocol::account::{Account, AccountId}; use miden_protocol::block::{BlockHeader, BlockNumber}; -use miden_protocol::note::{Note, Nullifier}; +use miden_protocol::note::{Note, NoteScript, Nullifier}; use miden_protocol::transaction::TransactionId; use miden_tx::utils::{Deserializable, Serializable}; use prost::Message; @@ -75,3 +76,15 @@ pub fn single_target_note_from_bytes( SingleTargetNetworkNote::try_from(proto_note) .map_err(|e| DatabaseError::deserialization("network note conversion", e)) } + +pub fn word_to_bytes(word: &Word) -> Vec { + word.to_bytes() +} + +pub fn note_script_to_bytes(script: &NoteScript) -> Vec { + script.to_bytes() +} + +pub fn note_script_from_bytes(bytes: &[u8]) -> Result { + NoteScript::read_from_bytes(bytes).map_err(|e| DatabaseError::deserialization("note script", e)) +} diff --git a/crates/ntx-builder/src/db/models/queries/mod.rs b/crates/ntx-builder/src/db/models/queries/mod.rs index fedcaabe04..2ee11ee287 100644 --- a/crates/ntx-builder/src/db/models/queries/mod.rs +++ b/crates/ntx-builder/src/db/models/queries/mod.rs @@ -19,6 +19,9 @@ pub use accounts::*; mod chain_state; pub use chain_state::*; +mod note_scripts; +pub use note_scripts::*; + mod notes; pub use notes::*; diff --git a/crates/ntx-builder/src/db/models/queries/note_scripts.rs b/crates/ntx-builder/src/db/models/queries/note_scripts.rs new file mode 100644 index 0000000000..09c03e4c1e --- /dev/null +++ b/crates/ntx-builder/src/db/models/queries/note_scripts.rs @@ -0,0 +1,56 @@ +//! Database queries for persisting and retrieving note scripts. + +use diesel::prelude::*; +use miden_node_db::DatabaseError; +use miden_protocol::Word; +use miden_protocol::note::NoteScript; + +use crate::db::models::conv as conversions; +use crate::db::schema; + +#[derive(Insertable)] +#[diesel(table_name = schema::note_scripts)] +struct NoteScriptInsert { + script_root: Vec, + script_data: Vec, +} + +#[derive(Queryable, Selectable)] +#[diesel(table_name = schema::note_scripts)] +struct NoteScriptRow { + script_data: Vec, +} + +/// Looks up a note script by its root hash. +pub fn lookup_note_script( + conn: &mut SqliteConnection, + script_root: &Word, +) -> Result, DatabaseError> { + let root_bytes = conversions::word_to_bytes(script_root); + + let row: Option = schema::note_scripts::table + .find(root_bytes) + .select(NoteScriptRow::as_select()) + .first(conn) + .optional()?; + + row.map(|r| conversions::note_script_from_bytes(&r.script_data)).transpose() +} + +/// Inserts a note script (idempotent via INSERT OR IGNORE). +pub fn insert_note_script( + conn: &mut SqliteConnection, + script_root: &Word, + script: &NoteScript, +) -> Result<(), DatabaseError> { + let insert = NoteScriptInsert { + script_root: conversions::word_to_bytes(script_root), + script_data: conversions::note_script_to_bytes(script), + }; + + diesel::insert_or_ignore_into(schema::note_scripts::table) + .values(&insert) + .execute(conn)?; + + Ok(()) +} diff --git a/crates/ntx-builder/src/db/models/queries/notes.rs b/crates/ntx-builder/src/db/models/queries/notes.rs index c33b847028..1c0145a9b1 100644 --- a/crates/ntx-builder/src/db/models/queries/notes.rs +++ b/crates/ntx-builder/src/db/models/queries/notes.rs @@ -152,33 +152,6 @@ pub fn notes_failed( Ok(()) } -/// Drops notes for the given account that have exceeded the maximum attempt count. -/// -/// # Raw SQL -/// -/// ```sql -/// DELETE FROM notes -/// WHERE account_id = ?1 AND attempt_count >= ?2 -/// ``` -#[expect(clippy::cast_possible_wrap)] -pub fn drop_failing_notes( - conn: &mut SqliteConnection, - account_id: NetworkAccountId, - max_attempts: usize, -) -> Result<(), DatabaseError> { - let account_id_bytes = conversions::network_account_id_to_bytes(account_id); - let max_attempts = max_attempts as i32; - - diesel::delete( - schema::notes::table - .filter(schema::notes::account_id.eq(&account_id_bytes)) - .filter(schema::notes::attempt_count.ge(max_attempts)), - ) - .execute(conn)?; - - Ok(()) -} - // HELPERS // ================================================================================================ diff --git a/crates/ntx-builder/src/db/models/queries/tests.rs b/crates/ntx-builder/src/db/models/queries/tests.rs index 6ef55f9a2a..0db95c018a 100644 --- a/crates/ntx-builder/src/db/models/queries/tests.rs +++ b/crates/ntx-builder/src/db/models/queries/tests.rs @@ -426,39 +426,6 @@ fn available_notes_only_returns_notes_for_specified_account() { assert_eq!(result[0].to_inner().nullifier(), note_acct1.nullifier()); } -// DROP FAILING NOTES TESTS -// ================================================================================================ - -#[test] -fn drop_failing_notes_scoped_to_account() { - let (conn, _dir) = &mut test_conn(); - - let account_id_1 = mock_network_account_id(); - let account_id_2 = mock_network_account_id_seeded(42); - - let note_acct1 = mock_single_target_note(account_id_1, 10); - let note_acct2 = mock_single_target_note(account_id_2, 20); - - // Insert both as committed. - insert_committed_notes(conn, &[note_acct1.clone(), note_acct2.clone()]).unwrap(); - - // Fail both notes enough times to exceed max_attempts=2. - let block_num = BlockNumber::from(100u32); - notes_failed(conn, &[note_acct1.nullifier()], block_num).unwrap(); - notes_failed(conn, &[note_acct1.nullifier()], block_num).unwrap(); - notes_failed(conn, &[note_acct2.nullifier()], block_num).unwrap(); - notes_failed(conn, &[note_acct2.nullifier()], block_num).unwrap(); - - // Drop failing notes for account_id_1 only. - drop_failing_notes(conn, account_id_1, 2).unwrap(); - - // note_acct1 should be deleted, note_acct2 should remain. - assert_eq!(count_notes(conn), 1); - let remaining: Vec> = - schema::notes::table.select(schema::notes::nullifier).load(conn).unwrap(); - assert_eq!(remaining[0], conversions::nullifier_to_bytes(¬e_acct2.nullifier())); -} - // NOTES FAILED TESTS // ================================================================================================ @@ -513,6 +480,55 @@ fn upsert_chain_state_updates_singleton() { assert_eq!(stored_block_num, conversions::block_num_to_i64(block_num_2)); } +// NOTE SCRIPT TESTS +// ================================================================================================ + +#[test] +fn note_script_insert_and_lookup() { + let (conn, _dir) = &mut test_conn(); + + // Extract a NoteScript from a mock note. + let account_id = mock_network_account_id(); + let note: miden_protocol::note::Note = mock_single_target_note(account_id, 10).into(); + let script = note.script().clone(); + let root = script.root(); + + // Insert the script. + insert_note_script(conn, &root, &script).unwrap(); + + // Look it up — should match the original. + let found = lookup_note_script(conn, &root).unwrap(); + assert!(found.is_some()); + assert_eq!(found.unwrap().root(), script.root()); +} + +#[test] +fn note_script_lookup_returns_none_for_missing() { + let (conn, _dir) = &mut test_conn(); + + let missing_root = Word::default(); + let found = lookup_note_script(conn, &missing_root).unwrap(); + assert!(found.is_none()); +} + +#[test] +fn note_script_insert_is_idempotent() { + let (conn, _dir) = &mut test_conn(); + + let account_id = mock_network_account_id(); + let note: miden_protocol::note::Note = mock_single_target_note(account_id, 10).into(); + let script = note.script().clone(); + let root = script.root(); + + // Insert the same script twice — should not error. + insert_note_script(conn, &root, &script).unwrap(); + insert_note_script(conn, &root, &script).unwrap(); + + // Should still be retrievable. + let found = lookup_note_script(conn, &root).unwrap(); + assert!(found.is_some()); +} + // HELPERS (domain type construction) // ================================================================================================ diff --git a/crates/ntx-builder/src/db/schema.rs b/crates/ntx-builder/src/db/schema.rs index 6a70ee1214..93dca8ce5e 100644 --- a/crates/ntx-builder/src/db/schema.rs +++ b/crates/ntx-builder/src/db/schema.rs @@ -17,6 +17,13 @@ diesel::table! { } } +diesel::table! { + note_scripts (script_root) { + script_root -> Binary, + script_data -> Binary, + } +} + diesel::table! { notes (nullifier) { nullifier -> Binary, @@ -29,4 +36,4 @@ diesel::table! { } } -diesel::allow_tables_to_appear_in_same_query!(accounts, chain_state, notes,); +diesel::allow_tables_to_appear_in_same_query!(accounts, chain_state, note_scripts, notes,); diff --git a/crates/ntx-builder/src/lib.rs b/crates/ntx-builder/src/lib.rs index 5732cb43f9..02c9f547ce 100644 --- a/crates/ntx-builder/src/lib.rs +++ b/crates/ntx-builder/src/lib.rs @@ -11,7 +11,7 @@ use db::Db; use futures::TryStreamExt; use miden_node_utils::lru_cache::LruCache; use store::StoreClient; -use tokio::sync::RwLock; +use tokio::sync::{RwLock, mpsc}; use url::Url; mod actor; @@ -249,6 +249,8 @@ impl NtxBuilderConfig { let chain_state = Arc::new(RwLock::new(ChainState::new(chain_tip_header, chain_mmr))); + let (notification_tx, notification_rx) = mpsc::channel(1); + let actor_context = AccountActorContext { block_producer_url: self.block_producer_url.clone(), validator_url: self.validator_url.clone(), @@ -259,6 +261,7 @@ impl NtxBuilderConfig { max_notes_per_tx: self.max_notes_per_tx, max_note_attempts: self.max_note_attempts, db: db.clone(), + notification_tx, }; Ok(NetworkTransactionBuilder::new( @@ -269,6 +272,7 @@ impl NtxBuilderConfig { chain_state, actor_context, mempool_events, + notification_rx, )) } }