From 9ed9c7a62c803fcd93dc725a31f8ceb60f4f53a6 Mon Sep 17 00:00:00 2001 From: sergerad Date: Thu, 29 Jan 2026 14:10:37 +1300 Subject: [PATCH 01/72] Initial validator db impl --- Cargo.lock | 4 + bin/node/src/commands/bundled.rs | 1 + bin/node/src/commands/validator.rs | 25 +++- crates/store/src/db/manager.rs | 4 +- crates/store/src/db/mod.rs | 9 +- crates/store/src/db/models/conv.rs | 2 +- crates/store/src/lib.rs | 4 + crates/validator/Cargo.toml | 4 + crates/validator/build.rs | 9 ++ crates/validator/diesel.toml | 5 + crates/validator/src/block_validation/mod.rs | 33 ++--- crates/validator/src/db/migrations.rs | 25 ++++ .../migrations/2025062000000_setup/down.sql | 0 .../db/migrations/2025062000000_setup/up.sql | 15 ++ crates/validator/src/db/mod.rs | 128 ++++++++++++++++++ crates/validator/src/db/models.rs | 72 ++++++++++ crates/validator/src/db/schema.rs | 11 ++ crates/validator/src/lib.rs | 1 + crates/validator/src/server/mod.rs | 55 ++++++-- 19 files changed, 361 insertions(+), 46 deletions(-) create mode 100644 crates/validator/build.rs create mode 100644 crates/validator/diesel.toml create mode 100644 crates/validator/src/db/migrations.rs create mode 100644 crates/validator/src/db/migrations/2025062000000_setup/down.sql create mode 100644 crates/validator/src/db/migrations/2025062000000_setup/up.sql create mode 100644 crates/validator/src/db/mod.rs create mode 100644 crates/validator/src/db/models.rs create mode 100644 crates/validator/src/db/schema.rs diff --git a/Cargo.lock b/Cargo.lock index c338dda5fb..3dedfb92ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2999,8 +2999,12 @@ name = "miden-node-validator" version = "0.14.0" dependencies = [ "anyhow", + "deadpool-diesel", + "diesel", + "diesel_migrations", "miden-node-proto", "miden-node-proto-build", + "miden-node-store", "miden-node-utils", "miden-protocol", "miden-tx", diff --git a/bin/node/src/commands/bundled.rs b/bin/node/src/commands/bundled.rs index 22f1199a3f..15abb25060 100644 --- a/bin/node/src/commands/bundled.rs +++ b/bin/node/src/commands/bundled.rs @@ -257,6 +257,7 @@ impl BundledCommand { address: validator_address, grpc_timeout, signer, + data_directory, } .serve() .await diff --git a/bin/node/src/commands/validator.rs b/bin/node/src/commands/validator.rs index f543be3013..ab6a7af651 100644 --- a/bin/node/src/commands/validator.rs +++ b/bin/node/src/commands/validator.rs @@ -1,3 +1,4 @@ +use std::path::PathBuf; use std::time::Duration; use anyhow::Context; @@ -9,6 +10,7 @@ use url::Url; use crate::commands::{ DEFAULT_TIMEOUT, + ENV_DATA_DIRECTORY, ENV_ENABLE_OTEL, ENV_VALIDATOR_INSECURE_SECRET_KEY, ENV_VALIDATOR_URL, @@ -40,6 +42,10 @@ pub enum ValidatorCommand { )] grpc_timeout: Duration, + /// Directory in which to store the database and raw block data. + #[arg(long, env = ENV_DATA_DIRECTORY, value_name = "DIR")] + data_directory: PathBuf, + /// Insecure, hex-encoded validator secret key for development and testing purposes. /// /// If not provided, a predefined key is used. @@ -51,7 +57,11 @@ pub enum ValidatorCommand { impl ValidatorCommand { pub async fn handle(self) -> anyhow::Result<()> { let Self::Start { - url, grpc_timeout, insecure_secret_key, .. + url, + grpc_timeout, + insecure_secret_key, + data_directory, + .. } = self; let address = @@ -59,10 +69,15 @@ impl ValidatorCommand { let signer = SecretKey::read_from_bytes(hex::decode(insecure_secret_key)?.as_ref())?; - Validator { address, grpc_timeout, signer } - .serve() - .await - .context("failed while serving validator component") + Validator { + address, + grpc_timeout, + signer, + data_directory, + } + .serve() + .await + .context("failed while serving validator component") } pub fn is_open_telemetry_enabled(&self) -> bool { diff --git a/crates/store/src/db/manager.rs b/crates/store/src/db/manager.rs index fca9a33db6..f1ac820df3 100644 --- a/crates/store/src/db/manager.rs +++ b/crates/store/src/db/manager.rs @@ -36,12 +36,12 @@ impl ConnectionManagerError { /// Create a connection manager with per-connection setup /// /// Particularly, `foreign_key` checks are enabled and using a write-append-log for journaling. -pub(crate) struct ConnectionManager { +pub struct ConnectionManager { pub(crate) manager: deadpool_diesel::sqlite::Manager, } impl ConnectionManager { - pub(crate) fn new(database_path: &str) -> Self { + pub fn new(database_path: &str) -> Self { let manager = deadpool_diesel::sqlite::Manager::new( database_path.to_owned(), deadpool_diesel::sqlite::Runtime::Tokio1, diff --git a/crates/store/src/db/mod.rs b/crates/store/src/db/mod.rs index 7fc4a5cab4..bb315fac3a 100644 --- a/crates/store/src/db/mod.rs +++ b/crates/store/src/db/mod.rs @@ -219,6 +219,11 @@ impl From for NoteSyncRecord { } impl Db { + /// Creates a new database instance with the provided connection pool. + pub fn new(pool: deadpool_diesel::Pool) -> Self { + Self { pool } + } + /// Creates a new database and inserts the genesis block. #[instrument( target = COMPONENT, @@ -260,7 +265,7 @@ impl Db { } /// Create and commit a transaction with the queries added in the provided closure - pub(crate) async fn transact(&self, msg: M, query: Q) -> std::result::Result + pub async fn transact(&self, msg: M, query: Q) -> std::result::Result where Q: Send + for<'a, 't> FnOnce(&'a mut SqliteConnection) -> std::result::Result @@ -285,7 +290,7 @@ impl Db { } /// Run the query _without_ a transaction - pub(crate) async fn query(&self, msg: M, query: Q) -> std::result::Result + pub async fn query(&self, msg: M, query: Q) -> std::result::Result where Q: Send + FnOnce(&mut SqliteConnection) -> std::result::Result + 'static, R: Send + 'static, diff --git a/crates/store/src/db/models/conv.rs b/crates/store/src/db/models/conv.rs index 2e6313bf61..a52fe194d4 100644 --- a/crates/store/src/db/models/conv.rs +++ b/crates/store/src/db/models/conv.rs @@ -50,7 +50,7 @@ pub struct DatabaseTypeConversionError { /// Convert from and to it's database representation and back /// /// We do not assume sanity of DB types. -pub(crate) trait SqlTypeConvert: Sized { +pub trait SqlTypeConvert: Sized { type Raw: Sized; fn to_raw_sql(self) -> Self::Raw; diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs index 1d345dcf01..91ea420487 100644 --- a/crates/store/src/lib.rs +++ b/crates/store/src/lib.rs @@ -10,6 +10,10 @@ pub mod state; #[cfg(feature = "rocksdb")] pub use accounts::PersistentAccountTree; pub use accounts::{AccountTreeWithHistory, HistoricalError, InMemoryAccountTree}; +pub use db::Db; +pub use db::manager::ConnectionManager; +pub use db::models::conv::SqlTypeConvert; +pub use errors::{DatabaseError, DatabaseSetupError}; pub use genesis::GenesisState; pub use server::{DataDirectory, Store}; diff --git a/crates/validator/Cargo.toml b/crates/validator/Cargo.toml index 6115e7cff3..dea3d48799 100644 --- a/crates/validator/Cargo.toml +++ b/crates/validator/Cargo.toml @@ -18,7 +18,11 @@ workspace = true [dependencies] anyhow = { workspace = true } +deadpool-diesel = { features = ["sqlite"], version = "0.6" } +diesel = { features = ["numeric", "sqlite"], version = "2.2" } +diesel_migrations = { features = ["sqlite"], version = "2.2" } miden-node-proto = { workspace = true } +miden-node-store = { workspace = true } miden-node-proto-build = { features = ["internal"], workspace = true } miden-node-utils = { features = ["testing"], workspace = true } miden-protocol = { workspace = true } diff --git a/crates/validator/build.rs b/crates/validator/build.rs new file mode 100644 index 0000000000..b9f947e177 --- /dev/null +++ b/crates/validator/build.rs @@ -0,0 +1,9 @@ +// This build.rs is required to trigger the `diesel_migrations::embed_migrations!` proc-macro in +// `validator/src/db/migrations.rs` to include the latest version of the migrations into the binary, see . +fn main() { + println!("cargo:rerun-if-changed=./src/db/migrations"); + // If we do one re-write, the default rules are disabled, + // hence we need to trigger explicitly on `Cargo.toml`. + // + println!("cargo:rerun-if-changed=Cargo.toml"); +} diff --git a/crates/validator/diesel.toml b/crates/validator/diesel.toml new file mode 100644 index 0000000000..bdce9175fa --- /dev/null +++ b/crates/validator/diesel.toml @@ -0,0 +1,5 @@ +# For documentation on how to configure this file, +# see https://diesel.rs/guides/configuring-diesel-cli + +[print_schema] +file = "src/db/schema.rs" diff --git a/crates/validator/src/block_validation/mod.rs b/crates/validator/src/block_validation/mod.rs index c1cab190bd..081c65192b 100644 --- a/crates/validator/src/block_validation/mod.rs +++ b/crates/validator/src/block_validation/mod.rs @@ -1,12 +1,11 @@ -use std::sync::Arc; - +use miden_node_store::{DatabaseError, Db}; use miden_protocol::block::{BlockNumber, BlockSigner, ProposedBlock}; use miden_protocol::crypto::dsa::ecdsa_k256_keccak::Signature; use miden_protocol::errors::ProposedBlockError; use miden_protocol::transaction::TransactionId; -use tracing::{Instrument, info_span}; +use tracing::info_span; -use crate::server::ValidatedTransactions; +use crate::db::select_transactions; // BLOCK VALIDATION ERROR // ================================================================================================ @@ -17,6 +16,8 @@ pub enum BlockValidationError { TransactionNotValidated(TransactionId, BlockNumber), #[error("failed to build block")] BlockBuildingFailed(#[from] ProposedBlockError), + #[error("failed to select transactions")] + DatabaseError(#[from] DatabaseError), } // BLOCK VALIDATION @@ -29,25 +30,15 @@ pub enum BlockValidationError { pub async fn validate_block( proposed_block: ProposedBlock, signer: &S, - validated_transactions: Arc, + db: &Db, ) -> Result { - // Check that all transactions in the proposed block have been validated let verify_span = info_span!("verify_transactions"); - for tx_header in proposed_block.transactions() { - let tx_id = tx_header.id(); - // TODO: LruCache is a poor abstraction since it locks many times. - if validated_transactions - .get(&tx_id) - .instrument(verify_span.clone()) - .await - .is_none() - { - return Err(BlockValidationError::TransactionNotValidated( - tx_id, - proposed_block.block_num(), - )); - } - } + + // Retrieve all validated transactions pertaining to the proposed block. + let tx_ids = proposed_block.transactions().map(|tx| tx.id()).collect::>(); + let validated_transactions = db + .transact("select_transactions", move |conn| select_transactions(conn, &tx_ids)) + .await?; // Build the block header. let (header, _) = proposed_block.into_header_and_body()?; diff --git a/crates/validator/src/db/migrations.rs b/crates/validator/src/db/migrations.rs new file mode 100644 index 0000000000..6896082bed --- /dev/null +++ b/crates/validator/src/db/migrations.rs @@ -0,0 +1,25 @@ +use diesel::SqliteConnection; +use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations}; +use miden_node_store::DatabaseError; +use tracing::instrument; + +use crate::COMPONENT; + +// The rebuild is automatically triggered by `build.rs` as described in +// . +pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("src/db/migrations"); + +#[instrument(level = "debug", target = COMPONENT, skip_all, err)] +pub fn apply_migrations(conn: &mut SqliteConnection) -> std::result::Result<(), DatabaseError> { + let migrations = conn.pending_migrations(MIGRATIONS).expect("In memory migrations never fail"); + tracing::info!(target = COMPONENT, "Applying {} migration(s)", migrations.len()); + + let Err(e) = conn.run_pending_migrations(MIGRATIONS) else { + return Ok(()); + }; + tracing::warn!(target = COMPONENT, "Failed to apply migration: {e:?}"); + conn.revert_last_migration(MIGRATIONS) + .expect("Duality is maintained by the developer"); + + Ok(()) +} diff --git a/crates/validator/src/db/migrations/2025062000000_setup/down.sql b/crates/validator/src/db/migrations/2025062000000_setup/down.sql new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/validator/src/db/migrations/2025062000000_setup/up.sql b/crates/validator/src/db/migrations/2025062000000_setup/up.sql new file mode 100644 index 0000000000..0106b74d55 --- /dev/null +++ b/crates/validator/src/db/migrations/2025062000000_setup/up.sql @@ -0,0 +1,15 @@ +CREATE TABLE transactions ( + transaction_id BLOB NOT NULL, + account_id BLOB NOT NULL, + block_num INTEGER NOT NULL, -- Block number in which the transaction was included. + initial_state_commitment BLOB NOT NULL, -- State of the account before applying the transaction. + final_state_commitment BLOB NOT NULL, -- State of the account after applying the transaction. + input_notes BLOB NOT NULL, -- Serialized vector with the Nullifier of the input notes. + output_notes BLOB NOT NULL, -- Serialized vector with the NoteId of the output notes. + size_in_bytes INTEGER NOT NULL, -- Estimated size of the row in bytes, considering the size of the input and output notes. + + PRIMARY KEY (transaction_id) +) WITHOUT ROWID; + +CREATE INDEX idx_transactions_account_id ON transactions(account_id); +CREATE INDEX idx_transactions_block_num ON transactions(block_num); diff --git a/crates/validator/src/db/mod.rs b/crates/validator/src/db/mod.rs new file mode 100644 index 0000000000..7eaed460af --- /dev/null +++ b/crates/validator/src/db/mod.rs @@ -0,0 +1,128 @@ +mod migrations; +mod models; +mod schema; + +use std::collections::{BTreeMap, HashMap}; +use std::path::PathBuf; + +use diesel::SqliteConnection; +use diesel::prelude::*; +use miden_node_store::{ConnectionManager, DatabaseError, DatabaseSetupError}; +use miden_protocol::Word; +use miden_protocol::account::AccountId; +use miden_protocol::asset::FungibleAsset; +use miden_protocol::block::BlockNumber; +use miden_protocol::note::{NoteHeader, NoteMetadata, NoteTag, NoteType}; +use miden_protocol::transaction::{ + InputNotes, + OrderedTransactionHeaders, + OutputNote, + OutputNotes, + TransactionHeader, + TransactionId, +}; +use miden_protocol::utils::{Deserializable, Serializable}; +use tracing::instrument; + +use crate::COMPONENT; +use crate::db::migrations::apply_migrations; +use crate::db::models::{TransactionSummaryRowInsert, TransactionSummaryRowSelect}; + +/// Open a connection to the DB and apply any pending migrations. +#[instrument(target = COMPONENT, skip_all)] +pub async fn load(database_filepath: PathBuf) -> Result { + let manager = ConnectionManager::new(database_filepath.to_str().unwrap()); + let pool = deadpool_diesel::Pool::builder(manager).max_size(16).build()?; + + tracing::info!( + target: COMPONENT, + sqlite= %database_filepath.display(), + "Connected to the database" + ); + + let db = miden_node_store::Db::new(pool); + db.query("migrations", apply_migrations).await?; + Ok(db) +} + +pub(crate) fn insert_transaction( + conn: &mut SqliteConnection, + header: TransactionHeader, +) -> Result { + #[allow(clippy::into_iter_on_ref)] // false positive + let row = TransactionSummaryRowInsert::new(&header); + let count = diesel::insert_into(schema::transactions::table).values(row).execute(conn)?; + Ok(count) +} + +pub(crate) fn select_transactions( + conn: &mut SqliteConnection, + tx_ids: &[TransactionId], +) -> Result, DatabaseError> { + if tx_ids.is_empty() { + return Ok(HashMap::new()); + } + + // Convert TransactionIds to bytes for query + let tx_id_bytes: Vec> = tx_ids.iter().map(TransactionId::to_bytes).collect(); + + // Query the database for matching transactions + let raw_transactions = schema::transactions::table + .filter(schema::transactions::transaction_id.eq_any(tx_id_bytes)) + .order(schema::transactions::transaction_id.asc()) + .load::(conn) + .map_err(DatabaseError::from)?; + + let mut transactions: HashMap = HashMap::new(); + + for raw_tx in raw_transactions { + // Deserialize the stored data + let account_id = AccountId::read_from_bytes(&raw_tx.account_id)?; + + let initial_state_commitment = Word::read_from_bytes(&raw_tx.initial_state_commitment)?; + + let final_state_commitment = Word::read_from_bytes(&raw_tx.final_state_commitment)?; + + let input_notes = InputNotes::read_from_bytes(&raw_tx.input_notes)?; + + let output_notes = OutputNotes::read_from_bytes(&raw_tx.output_notes)?; + + // Create TransactionId from the stored components + let tx_id = TransactionId::new( + initial_state_commitment, + final_state_commitment, + input_notes.commitment(), + output_notes.commitment(), + ); + + // TODO(sergerad): Implement into_iter for OutputNotes to avoid clones. + let output_note_headers = output_notes + .iter() + .map(|note| match note { + OutputNote::Full(full_note) => full_note.header().clone(), + OutputNote::Header(header) => header.clone(), + OutputNote::Partial(partial) => { + // For partial notes, create a minimal header with the note ID. + // Using default values for metadata since we don't have full note data. + let metadata = NoteMetadata::new(account_id, NoteType::Public, NoteTag::new(0)); + NoteHeader::new(partial.id(), metadata) + }, + }) + .collect::>(); + + // Create TransactionHeader + let tx_header = TransactionHeader::new_unchecked( + tx_id, + account_id, + initial_state_commitment, + final_state_commitment, + input_notes, + output_note_headers, + FungibleAsset::new(account_id, 0).expect("todo"), // todo(currentpr): ? + ); + + transactions.insert(tx_id, tx_header); + } + + Ok(transactions) +} diff --git a/crates/validator/src/db/models.rs b/crates/validator/src/db/models.rs new file mode 100644 index 0000000000..586149c565 --- /dev/null +++ b/crates/validator/src/db/models.rs @@ -0,0 +1,72 @@ +use diesel::prelude::*; +use miden_protocol::transaction::TransactionHeader; +use miden_protocol::utils::Serializable; + +use crate::db::schema; + +#[derive(Debug, Clone, PartialEq, Insertable)] +#[diesel(table_name = schema::transactions)] +#[diesel(check_for_backend(diesel::sqlite::Sqlite))] +pub struct TransactionSummaryRowInsert { + transaction_id: Vec, + account_id: Vec, + initial_state_commitment: Vec, + final_state_commitment: Vec, + input_notes: Vec, + output_notes: Vec, + size_in_bytes: i64, +} + +impl TransactionSummaryRowInsert { + #[allow( + clippy::cast_possible_wrap, + reason = "We will not approach the item count where i64 and usize cause issues" + )] + pub fn new(transaction_header: &TransactionHeader) -> Self { + const HEADER_BASE_SIZE: usize = 4 + 32 + 16 + 64; // block_num + tx_id + account_id + commitments + + // Serialize input notes using binary format (store nullifiers) + let input_notes_binary = transaction_header.input_notes().to_bytes(); + + // Serialize output notes using binary format (store note IDs) + let output_notes_binary = transaction_header.output_notes().to_bytes(); + + // Manually calculate the estimated size of the transaction header to avoid + // the cost of serialization. The size estimation includes: + // - 4 bytes for block number + // - 32 bytes for transaction ID + // - 16 bytes for account ID + // - 64 bytes for initial + final state commitments (32 bytes each) + // - 32 bytes per input note (nullifier size) + // - 500 bytes per output note (estimated size when converted to NoteSyncRecord) + // + // Note: 500 bytes per output note is an over-estimate but ensures we don't + // exceed memory limits when these transactions are later converted to proto records. + let input_notes_size = (transaction_header.input_notes().num_notes() * 32) as usize; + let output_notes_size = transaction_header.output_notes().len() * 500; + let size_in_bytes = (HEADER_BASE_SIZE + input_notes_size + output_notes_size) as i64; + + Self { + transaction_id: transaction_header.id().to_bytes(), + account_id: transaction_header.account_id().to_bytes(), + initial_state_commitment: transaction_header.initial_state_commitment().to_bytes(), + final_state_commitment: transaction_header.final_state_commitment().to_bytes(), + input_notes: input_notes_binary, + output_notes: output_notes_binary, + size_in_bytes, + } + } +} + +#[derive(Debug, Clone, PartialEq, Queryable, Selectable)] +#[diesel(table_name = schema::transactions)] +#[diesel(check_for_backend(diesel::sqlite::Sqlite))] +pub struct TransactionSummaryRowSelect { + pub transaction_id: Vec, + pub account_id: Vec, + pub initial_state_commitment: Vec, + pub final_state_commitment: Vec, + pub input_notes: Vec, + pub output_notes: Vec, + pub size_in_bytes: i64, +} diff --git a/crates/validator/src/db/schema.rs b/crates/validator/src/db/schema.rs new file mode 100644 index 0000000000..ba2856f1c3 --- /dev/null +++ b/crates/validator/src/db/schema.rs @@ -0,0 +1,11 @@ +diesel::table! { + transactions (transaction_id) { + transaction_id -> Binary, + account_id -> Binary, + initial_state_commitment -> Binary, + final_state_commitment -> Binary, + input_notes -> Binary, + output_notes -> Binary, + size_in_bytes -> BigInt, + } +} diff --git a/crates/validator/src/lib.rs b/crates/validator/src/lib.rs index a45112d275..a987304c3e 100644 --- a/crates/validator/src/lib.rs +++ b/crates/validator/src/lib.rs @@ -1,4 +1,5 @@ mod block_validation; +mod db; mod server; mod tx_validation; diff --git a/crates/validator/src/server/mod.rs b/crates/validator/src/server/mod.rs index 89d28d25de..bbad5acf6c 100644 --- a/crates/validator/src/server/mod.rs +++ b/crates/validator/src/server/mod.rs @@ -1,5 +1,6 @@ use std::net::SocketAddr; use std::num::NonZeroUsize; +use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; @@ -7,6 +8,7 @@ use anyhow::Context; use miden_node_proto::generated::validator::api_server; use miden_node_proto::generated::{self as proto}; use miden_node_proto_build::validator_api_descriptor; +use miden_node_store::Db; use miden_node_utils::ErrorReport; use miden_node_utils::lru_cache::LruCache; use miden_node_utils::panic::catch_panic_layer_fn; @@ -25,10 +27,11 @@ use tokio_stream::wrappers::TcpListenerStream; use tonic::Status; use tower_http::catch_panic::CatchPanicLayer; use tower_http::trace::TraceLayer; -use tracing::{Instrument, info_span}; +use tracing::info_span; use crate::COMPONENT; use crate::block_validation::validate_block; +use crate::db::{insert_transaction, load}; use crate::tx_validation::validate_transaction; /// Number of transactions to keep in the validated transactions cache. @@ -53,6 +56,9 @@ pub struct Validator { /// The signer used to sign blocks. pub signer: S, + + /// The data directory for the validator component's database files. + pub data_directory: PathBuf, } impl Validator { @@ -63,6 +69,11 @@ impl Validator { pub async fn serve(self) -> anyhow::Result<()> { tracing::info!(target: COMPONENT, endpoint=?self.address, "Initializing server"); + // Initialize database connection. + let db = load(self.data_directory.join("validator.sqlite3")) + .await + .context("failed to initialize validator database")?; + let listener = TcpListener::bind(self.address) .await .context("failed to bind to block producer address")?; @@ -86,7 +97,7 @@ impl Validator { .layer(CatchPanicLayer::custom(catch_panic_layer_fn)) .layer(TraceLayer::new_for_grpc().make_span_with(grpc_trace_fn)) .timeout(self.grpc_timeout) - .add_service(api_server::ApiServer::new(ValidatorServer::new(self.signer))) + .add_service(api_server::ApiServer::new(ValidatorServer::new(self.signer, db))) .add_service(reflection_service) .add_service(reflection_service_alpha) .serve_with_incoming(TcpListenerStream::new(listener)) @@ -103,14 +114,15 @@ impl Validator { /// Implements the gRPC API for the validator. struct ValidatorServer { signer: S, + db: Db, validated_transactions: Arc, } impl ValidatorServer { - fn new(signer: S) -> Self { + fn new(signer: S, db: Db) -> Self { let validated_transactions = Arc::new(ValidatedTransactions::new(NUM_VALIDATED_TRANSACTIONS)); - Self { signer, validated_transactions } + Self { signer, db, validated_transactions } } } @@ -154,14 +166,29 @@ impl api_server::Api for ValidatorServer Status::invalid_argument(err.as_report_context("Invalid transaction")) })?; - // Register the validated transaction. - let tx_id = validated_tx_header.id(); - self.validated_transactions - .put(tx_id, validated_tx_header) - .instrument(info_span!("validated_txs.insert")) + // Store the validated transaction. + let result = self + .db + .transact("insert_transaction", move |conn| { + insert_transaction(conn, validated_tx_header) + }) .await; - Ok(tonic::Response::new(())) + match result { + Ok(rows_affected) => { + if rows_affected == 1 { + Ok(tonic::Response::new(())) + } else { + tracing::error!( + target: COMPONENT, + rows_affected = rows_affected, + "unexpected number of rows affected by insertion of proven transaction" + ); + Err(tonic::Status::internal("failed to submit proven transaction")) + } + }, + Err(err) => Err(err.into()), + } } /// Validates a proposed block and returns the block header and body. @@ -181,11 +208,9 @@ impl api_server::Api for ValidatorServer // Validate the block. let signature = - validate_block(proposed_block, &self.signer, self.validated_transactions.clone()) - .await - .map_err(|err| { - tonic::Status::invalid_argument(format!("Failed to validate block: {err}",)) - })?; + validate_block(proposed_block, &self.signer, &self.db).await.map_err(|err| { + tonic::Status::invalid_argument(format!("Failed to validate block: {err}",)) + })?; // Send the signature. info_span!("serialize").in_scope(|| { From 560c79abbf819f694adc71bdf3f85ed3ca008958 Mon Sep 17 00:00:00 2001 From: sergerad Date: Thu, 29 Jan 2026 15:35:26 +1300 Subject: [PATCH 02/72] Add basic validation logic --- crates/validator/src/block_validation/mod.rs | 43 ++++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/crates/validator/src/block_validation/mod.rs b/crates/validator/src/block_validation/mod.rs index 081c65192b..f483279456 100644 --- a/crates/validator/src/block_validation/mod.rs +++ b/crates/validator/src/block_validation/mod.rs @@ -1,8 +1,10 @@ +use std::collections::HashMap; + use miden_node_store::{DatabaseError, Db}; use miden_protocol::block::{BlockNumber, BlockSigner, ProposedBlock}; use miden_protocol::crypto::dsa::ecdsa_k256_keccak::Signature; use miden_protocol::errors::ProposedBlockError; -use miden_protocol::transaction::TransactionId; +use miden_protocol::transaction::{TransactionHeader, TransactionId}; use tracing::info_span; use crate::db::select_transactions; @@ -14,10 +16,19 @@ use crate::db::select_transactions; pub enum BlockValidationError { #[error("transaction {0} in block {1} has not been validated")] TransactionNotValidated(TransactionId, BlockNumber), + #[error( + "the proposed transaction {proposed_tx} does not match the validated transaction {validated_tx}" + )] + TransactionMismatch { + proposed_tx: TransactionId, + validated_tx: TransactionId, + }, #[error("failed to build block")] BlockBuildingFailed(#[from] ProposedBlockError), #[error("failed to select transactions")] DatabaseError(#[from] DatabaseError), + #[error("internal error: {0}")] + Other(String), } // BLOCK VALIDATION @@ -34,12 +45,38 @@ pub async fn validate_block( ) -> Result { let verify_span = info_span!("verify_transactions"); + // Create a map of transactions from the proposed block. + let proposed_transactions = proposed_block + .transactions() + .map(|header| (header.id(), header.clone())) + .collect::>(); // Retrieve all validated transactions pertaining to the proposed block. - let tx_ids = proposed_block.transactions().map(|tx| tx.id()).collect::>(); + let tx_ids = proposed_block.transactions().map(TransactionHeader::id).collect::>(); + let query_tx_ids = tx_ids.clone(); let validated_transactions = db - .transact("select_transactions", move |conn| select_transactions(conn, &tx_ids)) + .transact("select_transactions", move |conn| select_transactions(conn, &query_tx_ids)) .await?; + // Check that every transaction from the proposed block has been validated. + for tx_id in tx_ids { + let Some(validated_tx) = validated_transactions.get(&tx_id) else { + return Err(BlockValidationError::TransactionNotValidated( + tx_id, + proposed_block.block_num(), + )); + }; + // Check that the proposed and validated transactions are equal. + let proposed_tx = proposed_transactions.get(&tx_id).ok_or(BlockValidationError::Other( + "proposed transactions mapped incorrectly".into(), + ))?; + if validated_tx != proposed_tx { + return Err(BlockValidationError::TransactionMismatch { + proposed_tx: proposed_tx.id(), + validated_tx: validated_tx.id(), + }); + } + } + // Build the block header. let (header, _) = proposed_block.into_header_and_body()?; From 77b1616934b3125e037f87cd0cc5869d6a5f82c7 Mon Sep 17 00:00:00 2001 From: sergerad Date: Fri, 30 Jan 2026 10:47:47 +1300 Subject: [PATCH 03/72] Simplify schema --- crates/validator/src/block_validation/mod.rs | 27 ++++--- .../db/migrations/2025062000000_setup/up.sql | 16 +--- crates/validator/src/db/mod.rs | 76 +++---------------- crates/validator/src/db/models.rs | 52 ++----------- crates/validator/src/db/schema.rs | 7 +- crates/validator/src/server/mod.rs | 23 +----- 6 files changed, 37 insertions(+), 164 deletions(-) diff --git a/crates/validator/src/block_validation/mod.rs b/crates/validator/src/block_validation/mod.rs index f483279456..55047d17d7 100644 --- a/crates/validator/src/block_validation/mod.rs +++ b/crates/validator/src/block_validation/mod.rs @@ -7,7 +7,7 @@ use miden_protocol::errors::ProposedBlockError; use miden_protocol::transaction::{TransactionHeader, TransactionId}; use tracing::info_span; -use crate::db::select_transactions; +use crate::db::select_validated_transactions; // BLOCK VALIDATION ERROR // ================================================================================================ @@ -43,32 +43,35 @@ pub async fn validate_block( signer: &S, db: &Db, ) -> Result { - let verify_span = info_span!("verify_transactions"); - // Create a map of transactions from the proposed block. let proposed_transactions = proposed_block .transactions() .map(|header| (header.id(), header.clone())) .collect::>(); + // Retrieve all validated transactions pertaining to the proposed block. - let tx_ids = proposed_block.transactions().map(TransactionHeader::id).collect::>(); - let query_tx_ids = tx_ids.clone(); + let proposed_tx_ids = + proposed_block.transactions().map(TransactionHeader::id).collect::>(); + let query_tx_ids = proposed_tx_ids.clone(); let validated_transactions = db - .transact("select_transactions", move |conn| select_transactions(conn, &query_tx_ids)) + .transact("select_transactions", move |conn| { + select_validated_transactions(conn, &query_tx_ids) + }) .await?; // Check that every transaction from the proposed block has been validated. - for tx_id in tx_ids { - let Some(validated_tx) = validated_transactions.get(&tx_id) else { + for proposed_tx_id in proposed_tx_ids { + let Some(validated_tx) = validated_transactions.get(&proposed_tx_id) else { return Err(BlockValidationError::TransactionNotValidated( - tx_id, + proposed_tx_id, proposed_block.block_num(), )); }; // Check that the proposed and validated transactions are equal. - let proposed_tx = proposed_transactions.get(&tx_id).ok_or(BlockValidationError::Other( - "proposed transactions mapped incorrectly".into(), - ))?; + let proposed_tx = + proposed_transactions.get(&proposed_tx_id).ok_or(BlockValidationError::Other( + "proposed transactions mapped incorrectly from proposed block".into(), + ))?; if validated_tx != proposed_tx { return Err(BlockValidationError::TransactionMismatch { proposed_tx: proposed_tx.id(), diff --git a/crates/validator/src/db/migrations/2025062000000_setup/up.sql b/crates/validator/src/db/migrations/2025062000000_setup/up.sql index 0106b74d55..a942c27d68 100644 --- a/crates/validator/src/db/migrations/2025062000000_setup/up.sql +++ b/crates/validator/src/db/migrations/2025062000000_setup/up.sql @@ -1,15 +1,5 @@ CREATE TABLE transactions ( - transaction_id BLOB NOT NULL, - account_id BLOB NOT NULL, - block_num INTEGER NOT NULL, -- Block number in which the transaction was included. - initial_state_commitment BLOB NOT NULL, -- State of the account before applying the transaction. - final_state_commitment BLOB NOT NULL, -- State of the account after applying the transaction. - input_notes BLOB NOT NULL, -- Serialized vector with the Nullifier of the input notes. - output_notes BLOB NOT NULL, -- Serialized vector with the NoteId of the output notes. - size_in_bytes INTEGER NOT NULL, -- Estimated size of the row in bytes, considering the size of the input and output notes. - - PRIMARY KEY (transaction_id) + id BLOB NOT NULL, + data BLOB NOT NULL, + PRIMARY KEY (id) ) WITHOUT ROWID; - -CREATE INDEX idx_transactions_account_id ON transactions(account_id); -CREATE INDEX idx_transactions_block_num ON transactions(block_num); diff --git a/crates/validator/src/db/mod.rs b/crates/validator/src/db/mod.rs index 7eaed460af..c987216120 100644 --- a/crates/validator/src/db/mod.rs +++ b/crates/validator/src/db/mod.rs @@ -2,25 +2,13 @@ mod migrations; mod models; mod schema; -use std::collections::{BTreeMap, HashMap}; +use std::collections::HashMap; use std::path::PathBuf; use diesel::SqliteConnection; use diesel::prelude::*; use miden_node_store::{ConnectionManager, DatabaseError, DatabaseSetupError}; -use miden_protocol::Word; -use miden_protocol::account::AccountId; -use miden_protocol::asset::FungibleAsset; -use miden_protocol::block::BlockNumber; -use miden_protocol::note::{NoteHeader, NoteMetadata, NoteTag, NoteType}; -use miden_protocol::transaction::{ - InputNotes, - OrderedTransactionHeaders, - OutputNote, - OutputNotes, - TransactionHeader, - TransactionId, -}; +use miden_protocol::transaction::{TransactionHeader, TransactionId}; use miden_protocol::utils::{Deserializable, Serializable}; use tracing::instrument; @@ -47,15 +35,14 @@ pub async fn load(database_filepath: PathBuf) -> Result Result { - #[allow(clippy::into_iter_on_ref)] // false positive - let row = TransactionSummaryRowInsert::new(&header); + let row = TransactionSummaryRowInsert::new(header); let count = diesel::insert_into(schema::transactions::table).values(row).execute(conn)?; Ok(count) } -pub(crate) fn select_transactions( +pub(crate) fn select_validated_transactions( conn: &mut SqliteConnection, tx_ids: &[TransactionId], ) -> Result, DatabaseError> { @@ -63,64 +50,21 @@ pub(crate) fn select_transactions( return Ok(HashMap::new()); } - // Convert TransactionIds to bytes for query + // Convert TransactionIds to bytes for query. let tx_id_bytes: Vec> = tx_ids.iter().map(TransactionId::to_bytes).collect(); - // Query the database for matching transactions + // Query the database for matching transactions. let raw_transactions = schema::transactions::table .filter(schema::transactions::transaction_id.eq_any(tx_id_bytes)) .order(schema::transactions::transaction_id.asc()) .load::(conn) .map_err(DatabaseError::from)?; + // Deserialize the transaction blobs. let mut transactions: HashMap = HashMap::new(); - for raw_tx in raw_transactions { - // Deserialize the stored data - let account_id = AccountId::read_from_bytes(&raw_tx.account_id)?; - - let initial_state_commitment = Word::read_from_bytes(&raw_tx.initial_state_commitment)?; - - let final_state_commitment = Word::read_from_bytes(&raw_tx.final_state_commitment)?; - - let input_notes = InputNotes::read_from_bytes(&raw_tx.input_notes)?; - - let output_notes = OutputNotes::read_from_bytes(&raw_tx.output_notes)?; - - // Create TransactionId from the stored components - let tx_id = TransactionId::new( - initial_state_commitment, - final_state_commitment, - input_notes.commitment(), - output_notes.commitment(), - ); - - // TODO(sergerad): Implement into_iter for OutputNotes to avoid clones. - let output_note_headers = output_notes - .iter() - .map(|note| match note { - OutputNote::Full(full_note) => full_note.header().clone(), - OutputNote::Header(header) => header.clone(), - OutputNote::Partial(partial) => { - // For partial notes, create a minimal header with the note ID. - // Using default values for metadata since we don't have full note data. - let metadata = NoteMetadata::new(account_id, NoteType::Public, NoteTag::new(0)); - NoteHeader::new(partial.id(), metadata) - }, - }) - .collect::>(); - - // Create TransactionHeader - let tx_header = TransactionHeader::new_unchecked( - tx_id, - account_id, - initial_state_commitment, - final_state_commitment, - input_notes, - output_note_headers, - FungibleAsset::new(account_id, 0).expect("todo"), // todo(currentpr): ? - ); - + let tx_id = TransactionId::read_from_bytes(&raw_tx.transaction_id)?; + let tx_header = TransactionHeader::read_from_bytes(&raw_tx.data)?; transactions.insert(tx_id, tx_header); } diff --git a/crates/validator/src/db/models.rs b/crates/validator/src/db/models.rs index 586149c565..59f4379ac9 100644 --- a/crates/validator/src/db/models.rs +++ b/crates/validator/src/db/models.rs @@ -1,6 +1,6 @@ use diesel::prelude::*; use miden_protocol::transaction::TransactionHeader; -use miden_protocol::utils::Serializable; +use miden_tx::utils::Serializable; use crate::db::schema; @@ -8,52 +8,15 @@ use crate::db::schema; #[diesel(table_name = schema::transactions)] #[diesel(check_for_backend(diesel::sqlite::Sqlite))] pub struct TransactionSummaryRowInsert { - transaction_id: Vec, - account_id: Vec, - initial_state_commitment: Vec, - final_state_commitment: Vec, - input_notes: Vec, - output_notes: Vec, - size_in_bytes: i64, + pub transaction_id: Vec, + pub data: Vec, } impl TransactionSummaryRowInsert { - #[allow( - clippy::cast_possible_wrap, - reason = "We will not approach the item count where i64 and usize cause issues" - )] pub fn new(transaction_header: &TransactionHeader) -> Self { - const HEADER_BASE_SIZE: usize = 4 + 32 + 16 + 64; // block_num + tx_id + account_id + commitments - - // Serialize input notes using binary format (store nullifiers) - let input_notes_binary = transaction_header.input_notes().to_bytes(); - - // Serialize output notes using binary format (store note IDs) - let output_notes_binary = transaction_header.output_notes().to_bytes(); - - // Manually calculate the estimated size of the transaction header to avoid - // the cost of serialization. The size estimation includes: - // - 4 bytes for block number - // - 32 bytes for transaction ID - // - 16 bytes for account ID - // - 64 bytes for initial + final state commitments (32 bytes each) - // - 32 bytes per input note (nullifier size) - // - 500 bytes per output note (estimated size when converted to NoteSyncRecord) - // - // Note: 500 bytes per output note is an over-estimate but ensures we don't - // exceed memory limits when these transactions are later converted to proto records. - let input_notes_size = (transaction_header.input_notes().num_notes() * 32) as usize; - let output_notes_size = transaction_header.output_notes().len() * 500; - let size_in_bytes = (HEADER_BASE_SIZE + input_notes_size + output_notes_size) as i64; - Self { transaction_id: transaction_header.id().to_bytes(), - account_id: transaction_header.account_id().to_bytes(), - initial_state_commitment: transaction_header.initial_state_commitment().to_bytes(), - final_state_commitment: transaction_header.final_state_commitment().to_bytes(), - input_notes: input_notes_binary, - output_notes: output_notes_binary, - size_in_bytes, + data: transaction_header.to_bytes(), } } } @@ -63,10 +26,5 @@ impl TransactionSummaryRowInsert { #[diesel(check_for_backend(diesel::sqlite::Sqlite))] pub struct TransactionSummaryRowSelect { pub transaction_id: Vec, - pub account_id: Vec, - pub initial_state_commitment: Vec, - pub final_state_commitment: Vec, - pub input_notes: Vec, - pub output_notes: Vec, - pub size_in_bytes: i64, + pub data: Vec, } diff --git a/crates/validator/src/db/schema.rs b/crates/validator/src/db/schema.rs index ba2856f1c3..198a559103 100644 --- a/crates/validator/src/db/schema.rs +++ b/crates/validator/src/db/schema.rs @@ -1,11 +1,6 @@ diesel::table! { transactions (transaction_id) { transaction_id -> Binary, - account_id -> Binary, - initial_state_commitment -> Binary, - final_state_commitment -> Binary, - input_notes -> Binary, - output_notes -> Binary, - size_in_bytes -> BigInt, + data -> Binary, } } diff --git a/crates/validator/src/server/mod.rs b/crates/validator/src/server/mod.rs index bbad5acf6c..1c75145b6f 100644 --- a/crates/validator/src/server/mod.rs +++ b/crates/validator/src/server/mod.rs @@ -1,7 +1,5 @@ use std::net::SocketAddr; -use std::num::NonZeroUsize; use std::path::PathBuf; -use std::sync::Arc; use std::time::Duration; use anyhow::Context; @@ -10,17 +8,11 @@ use miden_node_proto::generated::{self as proto}; use miden_node_proto_build::validator_api_descriptor; use miden_node_store::Db; use miden_node_utils::ErrorReport; -use miden_node_utils::lru_cache::LruCache; use miden_node_utils::panic::catch_panic_layer_fn; use miden_node_utils::tracing::OpenTelemetrySpanExt; use miden_node_utils::tracing::grpc::grpc_trace_fn; use miden_protocol::block::{BlockSigner, ProposedBlock}; -use miden_protocol::transaction::{ - ProvenTransaction, - TransactionHeader, - TransactionId, - TransactionInputs, -}; +use miden_protocol::transaction::{ProvenTransaction, TransactionInputs}; use miden_tx::utils::{Deserializable, Serializable}; use tokio::net::TcpListener; use tokio_stream::wrappers::TcpListenerStream; @@ -34,12 +26,6 @@ use crate::block_validation::validate_block; use crate::db::{insert_transaction, load}; use crate::tx_validation::validate_transaction; -/// Number of transactions to keep in the validated transactions cache. -const NUM_VALIDATED_TRANSACTIONS: NonZeroUsize = NonZeroUsize::new(10000).unwrap(); - -/// A type alias for a LRU cache that stores validated transactions. -pub type ValidatedTransactions = LruCache; - // VALIDATOR // ================================================================================ @@ -115,14 +101,11 @@ impl Validator { struct ValidatorServer { signer: S, db: Db, - validated_transactions: Arc, } impl ValidatorServer { fn new(signer: S, db: Db) -> Self { - let validated_transactions = - Arc::new(ValidatedTransactions::new(NUM_VALIDATED_TRANSACTIONS)); - Self { signer, db, validated_transactions } + Self { signer, db } } } @@ -170,7 +153,7 @@ impl api_server::Api for ValidatorServer let result = self .db .transact("insert_transaction", move |conn| { - insert_transaction(conn, validated_tx_header) + insert_transaction(conn, &validated_tx_header) }) .await; From 06369378806b371676ede2ca4d050a0442ead85c Mon Sep 17 00:00:00 2001 From: sergerad Date: Fri, 30 Jan 2026 11:06:43 +1300 Subject: [PATCH 04/72] Changelog, toml --- CHANGELOG.md | 3 ++- crates/validator/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9703ae728..f7eb5a67ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Enhancements - [BREAKING] Updated miden-base dependencies to use `next` branch; renamed `NoteInputs` to `NoteStorage`, `.inputs()` to `.storage()`, and database `inputs` column to `storage` ([#1595](https://github.com/0xMiden/miden-node/pull/1595)). +- Added sqlite database to Validator for transaction persistence ([#1614](https://github.com/0xMiden/miden-node/pull/1614)). ## v0.13.2 (2026-01-27) @@ -119,7 +120,7 @@ - Network transaction builder now marks notes from any error as failed ([#1508](https://github.com/0xMiden/miden-node/pull/1508)). - Network transaction builder now adheres to note limit set by protocol ([#1508](https://github.com/0xMiden/miden-node/pull/1508)). - Race condition resolved in the store's `apply_block` ([#1508](https://github.com/0xMiden/miden-node/pull/1508)). - - This presented as a database locked error and in rare cases a desync between the mempool and store. + - This presented as a database locked error and in rare cases a desync between the mempool and store. ## v0.12.6 (2026-01-12) diff --git a/crates/validator/Cargo.toml b/crates/validator/Cargo.toml index dea3d48799..3b7b36fc46 100644 --- a/crates/validator/Cargo.toml +++ b/crates/validator/Cargo.toml @@ -22,8 +22,8 @@ deadpool-diesel = { features = ["sqlite"], version = "0.6" } diesel = { features = ["numeric", "sqlite"], version = "2.2" } diesel_migrations = { features = ["sqlite"], version = "2.2" } miden-node-proto = { workspace = true } -miden-node-store = { workspace = true } miden-node-proto-build = { features = ["internal"], workspace = true } +miden-node-store = { workspace = true } miden-node-utils = { features = ["testing"], workspace = true } miden-protocol = { workspace = true } miden-tx = { workspace = true } From 71c90b83640c33bdd7dd4543ebcf6515de49f0c5 Mon Sep 17 00:00:00 2001 From: sergerad Date: Fri, 30 Jan 2026 11:10:14 +1300 Subject: [PATCH 05/72] Fix comment --- bin/node/src/commands/validator.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/node/src/commands/validator.rs b/bin/node/src/commands/validator.rs index ab6a7af651..13e3ac0451 100644 --- a/bin/node/src/commands/validator.rs +++ b/bin/node/src/commands/validator.rs @@ -42,7 +42,7 @@ pub enum ValidatorCommand { )] grpc_timeout: Duration, - /// Directory in which to store the database and raw block data. + /// Directory in which to store the database. #[arg(long, env = ENV_DATA_DIRECTORY, value_name = "DIR")] data_directory: PathBuf, From 7bdd61567e53ef4c99e125e545b3cc5317978dd1 Mon Sep 17 00:00:00 2001 From: sergerad Date: Fri, 30 Jan 2026 11:21:47 +1300 Subject: [PATCH 06/72] Add error case --- crates/validator/src/block_validation/mod.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/crates/validator/src/block_validation/mod.rs b/crates/validator/src/block_validation/mod.rs index 55047d17d7..ceda6e6da7 100644 --- a/crates/validator/src/block_validation/mod.rs +++ b/crates/validator/src/block_validation/mod.rs @@ -14,6 +14,14 @@ use crate::db::select_validated_transactions; #[derive(thiserror::Error, Debug)] pub enum BlockValidationError { + #[error( + "proposed transaction count {proposed_tx_count} in block {block_num} does not match validated transaction count {validated_tx_count}" + )] + TransactionCountMismatch { + proposed_tx_count: usize, + validated_tx_count: usize, + block_num: BlockNumber, + }, #[error("transaction {0} in block {1} has not been validated")] TransactionNotValidated(TransactionId, BlockNumber), #[error( @@ -59,6 +67,15 @@ pub async fn validate_block( }) .await?; + // Validated transaction count must match proposed count exactly. + if validated_transactions.len() != proposed_tx_ids.len() { + return Err(BlockValidationError::TransactionCountMismatch { + proposed_tx_count: proposed_tx_ids.len(), + validated_tx_count: validated_transactions.len(), + block_num: proposed_block.block_num(), + }); + } + // Check that every transaction from the proposed block has been validated. for proposed_tx_id in proposed_tx_ids { let Some(validated_tx) = validated_transactions.get(&proposed_tx_id) else { From 4e76c2e0742efcee4dceb5c6a929e27aed3707de Mon Sep 17 00:00:00 2001 From: sergerad Date: Fri, 30 Jan 2026 15:15:02 +1300 Subject: [PATCH 07/72] Fix standalone validator setup alongside bundled components --- bin/node/src/commands/bundled.rs | 192 ++++++++++++++++--------------- bin/node/src/commands/mod.rs | 23 +++- 2 files changed, 122 insertions(+), 93 deletions(-) diff --git a/bin/node/src/commands/bundled.rs b/bin/node/src/commands/bundled.rs index 15abb25060..9848ecbdb9 100644 --- a/bin/node/src/commands/bundled.rs +++ b/bin/node/src/commands/bundled.rs @@ -9,7 +9,6 @@ use miden_node_rpc::Rpc; use miden_node_store::Store; use miden_node_utils::grpc::UrlExt; use miden_node_validator::Validator; -use miden_protocol::block::BlockSigner; use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SecretKey; use miden_protocol::utils::Deserializable; use tokio::net::TcpListener; @@ -25,6 +24,7 @@ use crate::commands::{ ENV_VALIDATOR_INSECURE_SECRET_KEY, INSECURE_VALIDATOR_KEY_HEX, NtxBuilderConfig, + ValidatorConfig, duration_to_human_readable_string, }; @@ -78,6 +78,9 @@ pub enum BundledCommand { #[command(flatten)] ntx_builder: NtxBuilderConfig, + #[command(flatten)] + validator: ValidatorConfig, + /// Enables the exporting of traces for OpenTelemetry. /// /// This can be further configured using environment variables as defined in the official @@ -95,15 +98,6 @@ pub enum BundledCommand { value_name = "DURATION" )] grpc_timeout: Duration, - - /// Insecure, hex-encoded validator secret key for development and testing purposes. - #[arg( - long = "validator.insecure.secret-key", - env = ENV_VALIDATOR_INSECURE_SECRET_KEY, - value_name = "VALIDATOR_INSECURE_SECRET_KEY", - default_value = INSECURE_VALIDATOR_KEY_HEX - )] - validator_insecure_secret_key: String, }, } @@ -132,19 +126,17 @@ impl BundledCommand { data_directory, block_producer, ntx_builder, + validator, enable_otel: _, grpc_timeout, - validator_insecure_secret_key, } => { - let secret_key_bytes = hex::decode(validator_insecure_secret_key)?; - let signer = SecretKey::read_from_bytes(&secret_key_bytes)?; Self::start( rpc_url, data_directory, - ntx_builder, block_producer, + ntx_builder, + validator, grpc_timeout, - signer, ) .await }, @@ -155,10 +147,10 @@ impl BundledCommand { async fn start( rpc_url: Url, data_directory: PathBuf, - ntx_builder: NtxBuilderConfig, block_producer: BlockProducerConfig, + ntx_builder: NtxBuilderConfig, + validator: ValidatorConfig, grpc_timeout: Duration, - signer: impl BlockSigner + Send + Sync + 'static, ) -> anyhow::Result<()> { // Start listening on all gRPC urls so that inter-component connections can be created // before each component is fully started up. @@ -170,17 +162,32 @@ impl BundledCommand { .await .context("Failed to bind to RPC gRPC endpoint")?; - let block_producer_address = TcpListener::bind("127.0.0.1:0") - .await - .context("Failed to bind to block-producer gRPC endpoint")? - .local_addr() - .context("Failed to retrieve the block-producer's gRPC address")?; + let (block_producer_url, block_producer_address) = { + let socket_addr = TcpListener::bind("127.0.0.1:0") + .await + .context("Failed to bind to block-producer gRPC endpoint")? + .local_addr() + .context("Failed to retrieve the block-producer's gRPC address")?; + let url = Url::parse(&format!("http://{socket_addr}")) + .context("Failed to parse Block Producer URL")?; + (url, socket_addr) + }; - let validator_address = TcpListener::bind("127.0.0.1:0") - .await - .context("Failed to bind to validator gRPC endpoint")? - .local_addr() - .context("Failed to retrieve the validator's gRPC address")?; + // Validator URL is either specified remote, or generated local. + let (validator_url, validator_socket_address) = { + if let Some(remote_url) = validator.validator_url { + (remote_url, None) + } else { + let socket_addr = TcpListener::bind("127.0.0.1:0") + .await + .context("Failed to bind to validator gRPC endpoint")? + .local_addr() + .context("Failed to retrieve the validator's gRPC address")?; + let url = Url::parse(&format!("http://{socket_addr}")) + .context("Failed to parse Validator URL")?; + (url, Some(socket_addr)) + } + }; // Store addresses for each exposed API let store_rpc_listener = TcpListener::bind("127.0.0.1:0") @@ -223,76 +230,60 @@ impl BundledCommand { let should_start_ntx_builder = !ntx_builder.disabled; // Start block-producer. The block-producer's endpoint is available after loading completes. - let block_producer_id = join_set - .spawn({ - let store_url = Url::parse(&format!("http://{store_block_producer_address}")) - .context("Failed to parse URL")?; - let validator_url = Url::parse(&format!("http://{validator_address}")) - .context("Failed to parse URL")?; - async move { - BlockProducer { - block_producer_address, - store_url, - validator_url, - batch_prover_url: block_producer.batch_prover_url, - block_prover_url: block_producer.block_prover_url, - batch_interval: block_producer.batch_interval, - block_interval: block_producer.block_interval, - max_batches_per_block: block_producer.max_batches_per_block, - max_txs_per_batch: block_producer.max_txs_per_batch, - grpc_timeout, - mempool_tx_capacity: block_producer.mempool_tx_capacity, + let block_producer_id = { + let validator_url = validator_url.clone(); + join_set + .spawn({ + let store_url = Url::parse(&format!("http://{store_block_producer_address}")) + .context("Failed to parse URL")?; + async move { + BlockProducer { + block_producer_address, + store_url, + validator_url, + batch_prover_url: block_producer.batch_prover_url, + block_prover_url: block_producer.block_prover_url, + batch_interval: block_producer.batch_interval, + block_interval: block_producer.block_interval, + max_batches_per_block: block_producer.max_batches_per_block, + max_txs_per_batch: block_producer.max_txs_per_batch, + grpc_timeout, + mempool_tx_capacity: block_producer.mempool_tx_capacity, + } + .serve() + .await + .context("failed while serving block-producer component") } - .serve() - .await - .context("failed while serving block-producer component") - } - }) - .id(); + }) + .id() + }; - let validator_id = join_set - .spawn({ - async move { - Validator { - address: validator_address, + // Start RPC component. + let rpc_id = { + let block_producer_url = block_producer_url.clone(); + let validator_url = validator_url.clone(); + join_set + .spawn(async move { + let store_url = Url::parse(&format!("http://{store_rpc_address}")) + .context("Failed to parse URL")?; + Rpc { + listener: grpc_rpc, + store_url, + block_producer_url: Some(block_producer_url), + validator_url, grpc_timeout, - signer, - data_directory, } .serve() .await - .context("failed while serving validator component") - } - }) - .id(); - - // Start RPC component. - let rpc_id = join_set - .spawn(async move { - let store_url = Url::parse(&format!("http://{store_rpc_address}")) - .context("Failed to parse URL")?; - let block_producer_url = Url::parse(&format!("http://{block_producer_address}")) - .context("Failed to parse URL")?; - let validator_url = Url::parse(&format!("http://{validator_address}")) - .context("Failed to parse URL")?; - Rpc { - listener: grpc_rpc, - store_url, - block_producer_url: Some(block_producer_url), - validator_url, - grpc_timeout, - } - .serve() - .await - .context("failed while serving RPC component") - }) - .id(); + .context("failed while serving RPC component") + }) + .id() + }; // Lookup table so we can identify the failed component. let mut component_ids = HashMap::from([ (store_id, "store"), (block_producer_id, "block-producer"), - (validator_id, "validator"), (rpc_id, "rpc"), ]); @@ -301,13 +292,10 @@ impl BundledCommand { .context("Failed to parse URL")?; if should_start_ntx_builder { - let validator_url = Url::parse(&format!("http://{validator_address}")) - .context("Failed to parse URL")?; + let block_producer_url = block_producer_url.clone(); + let validator_url = validator_url.clone(); let id = join_set .spawn(async move { - let block_producer_url = - Url::parse(&format!("http://{block_producer_address}")) - .context("Failed to parse URL")?; NetworkTransactionBuilder::new( store_ntx_builder_url, block_producer_url, @@ -323,6 +311,28 @@ impl BundledCommand { component_ids.insert(id, "ntx-builder"); } + // Start the Validator if we have bound a socket. + if let Some(address) = validator_socket_address { + let secret_key_bytes = hex::decode(validator.validator_insecure_secret_key)?; + let signer = SecretKey::read_from_bytes(&secret_key_bytes)?; + let id = join_set + .spawn({ + async move { + Validator { + address, + grpc_timeout, + signer, + data_directory, + } + .serve() + .await + .context("failed while serving validator component") + } + }) + .id(); + component_ids.insert(id, "validator"); + } + // SAFETY: The joinset is definitely not empty. let component_result = join_set.join_next_with_id().await.unwrap(); diff --git a/bin/node/src/commands/mod.rs b/bin/node/src/commands/mod.rs index 7e8fa7e69f..be70cac50e 100644 --- a/bin/node/src/commands/mod.rs +++ b/bin/node/src/commands/mod.rs @@ -47,7 +47,26 @@ fn duration_to_human_readable_string(duration: Duration) -> String { humantime::format_duration(duration).to_string() } -/// Configuration for the Network Transaction Builder component +/// Configuration for the Validator component. +#[derive(clap::Args)] +pub struct ValidatorConfig { + /// Insecure, hex-encoded validator secret key for development and testing purposes. + /// Only used when the Validator URL argument is not set. + #[arg( + long = "validator.insecure.secret-key", + env = ENV_VALIDATOR_INSECURE_SECRET_KEY, + value_name = "VALIDATOR_INSECURE_SECRET_KEY", + default_value = INSECURE_VALIDATOR_KEY_HEX + )] + validator_insecure_secret_key: String, + + /// The remote Validator's gRPC URL. If unset, will default to running a Validator + /// in-process. If set, the insecure key argument is ignored. + #[arg(long = "validator.url", env = ENV_VALIDATOR_URL, value_name = "URL")] + pub validator_url: Option, +} + +/// Configuration for the Network Transaction Builder component. #[derive(clap::Args)] pub struct NtxBuilderConfig { /// Disable spawning the network transaction builder. @@ -77,7 +96,7 @@ pub struct NtxBuilderConfig { pub script_cache_size: NonZeroUsize, } -/// Configuration for the Block Producer component +/// Configuration for the Block Producer component. #[derive(clap::Args)] pub struct BlockProducerConfig { /// Interval at which to produce blocks. From 4fe989013e673a4311d1187e1d45a442ac7c7fd1 Mon Sep 17 00:00:00 2001 From: sergerad Date: Tue, 3 Feb 2026 11:25:57 +1300 Subject: [PATCH 08/72] RM row affected check --- Cargo.toml | 10 +++++----- crates/validator/src/server/mod.rs | 22 +++------------------- 2 files changed, 8 insertions(+), 24 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f2ad1d5a9e..abe16e1fee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,6 +80,11 @@ lru = { default-features = false, version = "0.16" } pretty_assertions = { version = "1.4" } # breaking change `DecodeError::new` is not exposed anymore # but is assumed public by some internal dependency +deadpool = { default-features = false, features = ["managed", "rt_tokio_1"], version = "0.12" } +deadpool-diesel = { features = ["sqlite"], version = "0.6" } +deadpool-sync = { default-features = false, features = ["tracing"], version = "0.1" } +diesel = { features = ["numeric", "sqlite"], version = "2.3" } +diesel_migrations = { features = ["sqlite"], version = "2.3" } prost = { default-features = false, version = "=0.14.1" } protox = { version = "=0.9.0" } rand = { version = "0.9" } @@ -99,11 +104,6 @@ tower-http = { features = ["cors", "trace"], version = "0.6" } tracing = { version = "0.1" } tracing-subscriber = { features = ["env-filter", "fmt", "json"], version = "0.3" } url = { features = ["serde"], version = "2.5" } -deadpool = { default-features = false, features = ["managed", "rt_tokio_1"], version = "0.12" } -deadpool-diesel = { features = ["sqlite"], version = "0.6" } -deadpool-sync = { default-features = false, features = ["tracing"], version = "0.1" } -diesel = { features = ["numeric", "sqlite"], version = "2.3" } -diesel_migrations = { features = ["sqlite"], version = "2.3" } # Lints are set to warn for development, which are promoted to errors in CI. [workspace.lints.clippy] diff --git a/crates/validator/src/server/mod.rs b/crates/validator/src/server/mod.rs index 1c75145b6f..e99fbf593f 100644 --- a/crates/validator/src/server/mod.rs +++ b/crates/validator/src/server/mod.rs @@ -150,28 +150,12 @@ impl api_server::Api for ValidatorServer })?; // Store the validated transaction. - let result = self - .db + self.db .transact("insert_transaction", move |conn| { insert_transaction(conn, &validated_tx_header) }) - .await; - - match result { - Ok(rows_affected) => { - if rows_affected == 1 { - Ok(tonic::Response::new(())) - } else { - tracing::error!( - target: COMPONENT, - rows_affected = rows_affected, - "unexpected number of rows affected by insertion of proven transaction" - ); - Err(tonic::Status::internal("failed to submit proven transaction")) - } - }, - Err(err) => Err(err.into()), - } + .await?; + Ok(tonic::Response::new(())) } /// Validates a proposed block and returns the block header and body. From f3aaebacafcbeb3153270b97b6d90f5835856501 Mon Sep 17 00:00:00 2001 From: sergerad Date: Tue, 3 Feb 2026 11:26:47 +1300 Subject: [PATCH 09/72] Fix comment --- bin/node/src/commands/validator.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/node/src/commands/validator.rs b/bin/node/src/commands/validator.rs index 13e3ac0451..2c103319c5 100644 --- a/bin/node/src/commands/validator.rs +++ b/bin/node/src/commands/validator.rs @@ -42,7 +42,7 @@ pub enum ValidatorCommand { )] grpc_timeout: Duration, - /// Directory in which to store the database. + /// Directory in which to store the validator's data. #[arg(long, env = ENV_DATA_DIRECTORY, value_name = "DIR")] data_directory: PathBuf, From 405737e85f5426a81eea21b8b5b13cccbf600517 Mon Sep 17 00:00:00 2001 From: sergerad Date: Tue, 3 Feb 2026 11:42:57 +1300 Subject: [PATCH 10/72] Add UnvalidatedTransactions err --- crates/validator/src/block_validation/mod.rs | 24 ++++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/crates/validator/src/block_validation/mod.rs b/crates/validator/src/block_validation/mod.rs index ceda6e6da7..ede74b5d52 100644 --- a/crates/validator/src/block_validation/mod.rs +++ b/crates/validator/src/block_validation/mod.rs @@ -14,14 +14,8 @@ use crate::db::select_validated_transactions; #[derive(thiserror::Error, Debug)] pub enum BlockValidationError { - #[error( - "proposed transaction count {proposed_tx_count} in block {block_num} does not match validated transaction count {validated_tx_count}" - )] - TransactionCountMismatch { - proposed_tx_count: usize, - validated_tx_count: usize, - block_num: BlockNumber, - }, + #[error("found unvalidated transactions {0:?}")] + UnvalidatedTransactions(Vec), #[error("transaction {0} in block {1} has not been validated")] TransactionNotValidated(TransactionId, BlockNumber), #[error( @@ -67,13 +61,13 @@ pub async fn validate_block( }) .await?; - // Validated transaction count must match proposed count exactly. - if validated_transactions.len() != proposed_tx_ids.len() { - return Err(BlockValidationError::TransactionCountMismatch { - proposed_tx_count: proposed_tx_ids.len(), - validated_tx_count: validated_transactions.len(), - block_num: proposed_block.block_num(), - }); + // All proposed transactions must have been validated. + if proposed_tx_ids.len() > validated_transactions.len() { + let mut proposed_tx_ids = proposed_tx_ids; + // Return proposed transactions that have not been validated. + proposed_tx_ids + .retain(|proposed_tx_id| !validated_transactions.contains_key(proposed_tx_id)); + return Err(BlockValidationError::UnvalidatedTransactions(proposed_tx_ids)); } // Check that every transaction from the proposed block has been validated. From 2d7a3bacb86f91fbd3ccb50ee382c2e637f970c7 Mon Sep 17 00:00:00 2001 From: sergerad Date: Tue, 3 Feb 2026 11:56:17 +1300 Subject: [PATCH 11/72] Rename insecure key --- bin/node/.env | 2 +- bin/node/src/commands/bundled.rs | 16 ++++++++-------- bin/node/src/commands/mod.rs | 10 +++++----- bin/node/src/commands/store.rs | 16 ++++++++-------- bin/node/src/commands/validator.rs | 10 +++++----- 5 files changed, 27 insertions(+), 27 deletions(-) diff --git a/bin/node/.env b/bin/node/.env index fc4c2793e3..6bdfa9a805 100644 --- a/bin/node/.env +++ b/bin/node/.env @@ -10,7 +10,7 @@ MIDEN_NODE_STORE_RPC_URL= MIDEN_NODE_STORE_NTX_BUILDER_URL= MIDEN_NODE_STORE_BLOCK_PRODUCER_URL= MIDEN_NODE_VALIDATOR_BLOCK_PRODUCER_URL= -MIDEN_NODE_VALIDATOR_INSECURE_SECRET_KEY= +MIDEN_NODE_VALIDATOR_KEY= MIDEN_NODE_RPC_URL=http://0.0.0.0:57291 MIDEN_NODE_DATA_DIRECTORY=./ MIDEN_NODE_ENABLE_OTEL=true diff --git a/bin/node/src/commands/bundled.rs b/bin/node/src/commands/bundled.rs index 362771bbe8..c07339127f 100644 --- a/bin/node/src/commands/bundled.rs +++ b/bin/node/src/commands/bundled.rs @@ -21,7 +21,7 @@ use crate::commands::{ ENV_BLOCK_PROVER_URL, ENV_ENABLE_OTEL, ENV_GENESIS_CONFIG_FILE, - ENV_VALIDATOR_INSECURE_SECRET_KEY, + ENV_VALIDATOR_KEY, INSECURE_VALIDATOR_KEY_HEX, NtxBuilderConfig, ValidatorConfig, @@ -51,12 +51,12 @@ pub enum BundledCommand { /// /// If not provided, a predefined key is used. #[arg( - long = "validator.insecure.secret-key", - env = ENV_VALIDATOR_INSECURE_SECRET_KEY, - value_name = "VALIDATOR_INSECURE_SECRET_KEY", + long = "validator.key", + env = ENV_VALIDATOR_KEY, + value_name = "VALIDATOR_KEY", default_value = INSECURE_VALIDATOR_KEY_HEX )] - validator_insecure_secret_key: String, + validator_key: String, }, /// Runs all three node components in the same process. @@ -112,14 +112,14 @@ impl BundledCommand { data_directory, accounts_directory, genesis_config_file, - validator_insecure_secret_key, + validator_key: validator_insecure_secret_key, } => { // Currently the bundled bootstrap is identical to the store's bootstrap. crate::commands::store::StoreCommand::Bootstrap { data_directory, accounts_directory, genesis_config_file, - validator_insecure_secret_key, + validator_key: validator_insecure_secret_key, } .handle() .await @@ -323,7 +323,7 @@ impl BundledCommand { // Start the Validator if we have bound a socket. if let Some(address) = validator_socket_address { - let secret_key_bytes = hex::decode(validator.validator_insecure_secret_key)?; + let secret_key_bytes = hex::decode(validator.validator_key)?; let signer = SecretKey::read_from_bytes(&secret_key_bytes)?; let id = join_set .spawn({ diff --git a/bin/node/src/commands/mod.rs b/bin/node/src/commands/mod.rs index cae8c618dd..87e673027a 100644 --- a/bin/node/src/commands/mod.rs +++ b/bin/node/src/commands/mod.rs @@ -36,7 +36,7 @@ const ENV_MAX_TXS_PER_BATCH: &str = "MIDEN_MAX_TXS_PER_BATCH"; const ENV_MAX_BATCHES_PER_BLOCK: &str = "MIDEN_MAX_BATCHES_PER_BLOCK"; const ENV_MEMPOOL_TX_CAPACITY: &str = "MIDEN_NODE_MEMPOOL_TX_CAPACITY"; const ENV_NTX_SCRIPT_CACHE_SIZE: &str = "MIDEN_NTX_DATA_STORE_SCRIPT_CACHE_SIZE"; -const ENV_VALIDATOR_INSECURE_SECRET_KEY: &str = "MIDEN_NODE_VALIDATOR_INSECURE_SECRET_KEY"; +const ENV_VALIDATOR_KEY: &str = "MIDEN_NODE_VALIDATOR_KEY"; const DEFAULT_NTX_TICKER_INTERVAL: Duration = Duration::from_millis(200); const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10); @@ -53,12 +53,12 @@ pub struct ValidatorConfig { /// Insecure, hex-encoded validator secret key for development and testing purposes. /// Only used when the Validator URL argument is not set. #[arg( - long = "validator.insecure.secret-key", - env = ENV_VALIDATOR_INSECURE_SECRET_KEY, - value_name = "VALIDATOR_INSECURE_SECRET_KEY", + long = "validator.key", + env = ENV_VALIDATOR_KEY, + value_name = "VALIDATOR_KEY", default_value = INSECURE_VALIDATOR_KEY_HEX )] - validator_insecure_secret_key: String, + validator_key: String, /// The remote Validator's gRPC URL. If unset, will default to running a Validator /// in-process. If set, the insecure key argument is ignored. diff --git a/bin/node/src/commands/store.rs b/bin/node/src/commands/store.rs index a78655cd99..c08348832e 100644 --- a/bin/node/src/commands/store.rs +++ b/bin/node/src/commands/store.rs @@ -20,7 +20,7 @@ use crate::commands::{ ENV_BLOCK_PROVER_URL, ENV_ENABLE_OTEL, ENV_GENESIS_CONFIG_FILE, - ENV_VALIDATOR_INSECURE_SECRET_KEY, + ENV_VALIDATOR_KEY, INSECURE_VALIDATOR_KEY_HEX, duration_to_human_readable_string, }; @@ -48,12 +48,12 @@ pub enum StoreCommand { /// /// If not provided, a predefined key is used. #[arg( - long = "validator.insecure.secret-key", - env = ENV_VALIDATOR_INSECURE_SECRET_KEY, - value_name = "VALIDATOR_INSECURE_SECRET_KEY", + long = "validator.key", + env = ENV_VALIDATOR_KEY, + value_name = "VALIDATOR_KEY", default_value = INSECURE_VALIDATOR_KEY_HEX )] - validator_insecure_secret_key: String, + validator_key: String, }, /// Starts the store component. @@ -109,7 +109,7 @@ impl StoreCommand { data_directory, accounts_directory, genesis_config_file, - validator_insecure_secret_key, + validator_key: validator_insecure_secret_key, } => Self::bootstrap( &data_directory, &accounts_directory, @@ -192,10 +192,10 @@ impl StoreCommand { data_directory: &Path, accounts_directory: &Path, genesis_config: Option<&PathBuf>, - validator_insecure_secret_key: String, + validator_key: String, ) -> anyhow::Result<()> { // Decode the validator key. - let signer = SecretKey::read_from_bytes(&hex::decode(validator_insecure_secret_key)?)?; + let signer = SecretKey::read_from_bytes(&hex::decode(validator_key)?)?; // Parse genesis config (or default if not given). let config = genesis_config diff --git a/bin/node/src/commands/validator.rs b/bin/node/src/commands/validator.rs index 2c103319c5..461e446c1a 100644 --- a/bin/node/src/commands/validator.rs +++ b/bin/node/src/commands/validator.rs @@ -12,7 +12,7 @@ use crate::commands::{ DEFAULT_TIMEOUT, ENV_DATA_DIRECTORY, ENV_ENABLE_OTEL, - ENV_VALIDATOR_INSECURE_SECRET_KEY, + ENV_VALIDATOR_KEY, ENV_VALIDATOR_URL, INSECURE_VALIDATOR_KEY_HEX, duration_to_human_readable_string, @@ -49,8 +49,8 @@ pub enum ValidatorCommand { /// Insecure, hex-encoded validator secret key for development and testing purposes. /// /// If not provided, a predefined key is used. - #[arg(long = "insecure.secret-key", env = ENV_VALIDATOR_INSECURE_SECRET_KEY, value_name = "INSECURE_SECRET_KEY", default_value = INSECURE_VALIDATOR_KEY_HEX)] - insecure_secret_key: String, + #[arg(long = "key", env = ENV_VALIDATOR_KEY, value_name = "VALIDATOR_KEY", default_value = INSECURE_VALIDATOR_KEY_HEX)] + validator_key: String, }, } @@ -59,7 +59,7 @@ impl ValidatorCommand { let Self::Start { url, grpc_timeout, - insecure_secret_key, + validator_key, data_directory, .. } = self; @@ -67,7 +67,7 @@ impl ValidatorCommand { let address = url.to_socket().context("Failed to extract socket address from validator URL")?; - let signer = SecretKey::read_from_bytes(hex::decode(insecure_secret_key)?.as_ref())?; + let signer = SecretKey::read_from_bytes(hex::decode(validator_key)?.as_ref())?; Validator { address, From d3c87703de612346e29788fd3727c521613748aa Mon Sep 17 00:00:00 2001 From: sergerad Date: Tue, 3 Feb 2026 16:05:42 +1300 Subject: [PATCH 12/72] Move to kv store --- Cargo.lock | 217 +++++++++++++++--- Cargo.toml | 2 +- crates/validator/Cargo.toml | 2 +- crates/validator/src/block_validation/mod.rs | 68 +----- crates/validator/src/db/database.rs | 64 ++++++ crates/validator/src/db/kv_conv.rs | 28 +++ crates/validator/src/db/migrations.rs | 25 -- .../migrations/2025062000000_setup/down.sql | 0 .../db/migrations/2025062000000_setup/up.sql | 5 - crates/validator/src/db/mod.rs | 74 +----- crates/validator/src/db/models.rs | 30 --- crates/validator/src/db/schema.rs | 6 - crates/validator/src/server/mod.rs | 17 +- rust-toolchain.toml | 2 +- 14 files changed, 297 insertions(+), 243 deletions(-) create mode 100644 crates/validator/src/db/database.rs create mode 100644 crates/validator/src/db/kv_conv.rs delete mode 100644 crates/validator/src/db/migrations.rs delete mode 100644 crates/validator/src/db/migrations/2025062000000_setup/down.sql delete mode 100644 crates/validator/src/db/migrations/2025062000000_setup/up.sql delete mode 100644 crates/validator/src/db/models.rs delete mode 100644 crates/validator/src/db/schema.rs diff --git a/Cargo.lock b/Cargo.lock index 3c9d3ec2a1..b2c7acda64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,17 +27,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "ahash" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" -dependencies = [ - "getrandom 0.2.17", - "once_cell", - "version_check", -] - [[package]] name = "ahash" version = "0.8.12" @@ -469,12 +458,24 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +[[package]] +name = "byteview" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dda4398f387cc6395a3e93b3867cd9abda914c97a0b344d1eefb2e5c51785fca" + [[package]] name = "bzip2-sys" version = "0.1.13+1.0.8" @@ -726,6 +727,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "compare" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea0095f6103c2a8b44acd6fd15960c801dafebf02e21940360833e0673f48ba7" + [[package]] name = "const-oid" version = "0.9.6" @@ -846,6 +853,16 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-skiplist" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df29de440c58ca2cc6e587ec3d22347551a32435fbde9d2bff64e78a9ffa151b" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -987,6 +1004,20 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "deadpool" version = "0.12.3" @@ -1298,6 +1329,18 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enum_dispatch" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "env_filter" version = "0.1.4" @@ -1334,7 +1377,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1387,6 +1430,23 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" +[[package]] +name = "fjall" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f69637c02d38ad1b0f003101d0195a60368130aa17d9ef78b1557d265a22093" +dependencies = [ + "byteorder-lite", + "byteview", + "dashmap", + "flume 0.12.0", + "log", + "lsm-tree", + "lz4_flex", + "tempfile", + "xxhash-rust", +] + [[package]] name = "flate2" version = "1.1.8" @@ -1410,6 +1470,15 @@ dependencies = [ "spin", ] +[[package]] +name = "flume" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" +dependencies = [ + "spin", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1679,9 +1748,12 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash 0.7.8", -] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hashbrown" @@ -2088,6 +2160,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "interval-heap" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11274e5e8e89b8607cfedc2910b6626e998779b48a019151c7604d0adcb86ac6" +dependencies = [ + "compare", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -2112,7 +2193,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi 0.5.2", "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2433,6 +2514,28 @@ version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +[[package]] +name = "lsm-tree" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b875f1dfe14f557f805b167fb9b0fc54c5560c7a4bd6ae02535b2846f276a8cb" +dependencies = [ + "byteorder-lite", + "byteview", + "crossbeam-skiplist", + "enum_dispatch", + "interval-heap", + "log", + "lz4_flex", + "quick_cache", + "rustc-hash", + "self_cell", + "sfa", + "tempfile", + "varint-rs", + "xxhash-rust", +] + [[package]] name = "lz4-sys" version = "1.11.1+lz4-1.10.0" @@ -2443,6 +2546,15 @@ dependencies = [ "libc", ] +[[package]] +name = "lz4_flex" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a" +dependencies = [ + "twox-hash", +] + [[package]] name = "matchers" version = "0.2.0" @@ -2606,7 +2718,7 @@ dependencies = [ "chacha20poly1305", "curve25519-dalek", "ed25519-dalek", - "flume", + "flume 0.11.1", "glob", "hashbrown 0.16.1", "hkdf", @@ -3017,9 +3129,9 @@ dependencies = [ "deadpool-diesel", "diesel", "diesel_migrations", + "fjall", "miden-node-proto", "miden-node-proto-build", - "miden-node-store", "miden-node-utils", "miden-protocol", "miden-tx", @@ -3481,7 +3593,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3868,7 +3980,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ef622051fbb2cb98a524df3a8112f02d0919ccda600a44d705ec550f1a28fe2" dependencies = [ - "ahash 0.8.12", + "ahash", "async-trait", "blake2", "bytes", @@ -3904,7 +4016,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76f63d3f67d99c95a1f85623fc43242fd644dd12ccbaa18c38a54e1580c6846a" dependencies = [ - "ahash 0.8.12", + "ahash", "async-trait", "brotli", "bytes", @@ -3994,7 +4106,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b93c897e8cc04ff0d077ee2a655142910618222aeefc83f7f99f5b9fc59ccb13" dependencies = [ - "ahash 0.8.12", + "ahash", ] [[package]] @@ -4026,7 +4138,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba89e4400cb978f0d7be1c14bd7ab4168c8e2c00d97ff19f964fc0048780237c" dependencies = [ "arrayvec", - "hashbrown 0.12.3", + "hashbrown 0.16.1", "parking_lot", "rand 0.8.5", ] @@ -4368,7 +4480,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac6c3320f9abac597dcbc668774ef006702672474aad53c6d596b62e487b40b1" dependencies = [ "heck 0.5.0", - "itertools 0.10.5", + "itertools 0.14.0", "log", "multimap", "once_cell", @@ -4390,7 +4502,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools 0.14.0", "proc-macro2", "quote", "syn 2.0.114", @@ -4496,6 +4608,16 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +[[package]] +name = "quick_cache" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ada44a88ef953a3294f6eb55d2007ba44646015e18613d2f213016379203ef3" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", +] + [[package]] name = "quote" version = "1.0.44" @@ -4843,7 +4965,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4856,7 +4978,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -5025,6 +5147,12 @@ dependencies = [ "libc", ] +[[package]] +name = "self_cell" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" + [[package]] name = "semver" version = "0.9.0" @@ -5183,6 +5311,17 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "sfa" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1296838937cab56cd6c4eeeb8718ec777383700c33f060e2869867bd01d1175" +dependencies = [ + "byteorder-lite", + "log", + "xxhash-rust", +] + [[package]] name = "sfv" version = "0.10.4" @@ -5488,7 +5627,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix 1.1.3", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -5497,7 +5636,7 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8c27177b12a6399ffc08b98f76f7c9a1f4fe9fc967c784c5a071fa8d93cf7e1" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6168,6 +6307,12 @@ dependencies = [ "toml 0.9.11+spec-1.1.0", ] +[[package]] +name = "twox-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" + [[package]] name = "typenum" version = "1.19.0" @@ -6283,6 +6428,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "varint-rs" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f54a172d0620933a27a4360d3db3e2ae0dd6cceae9730751a036bbf182c4b23" + [[package]] name = "vcpkg" version = "0.2.15" @@ -6461,7 +6612,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -6915,6 +7066,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "xxhash-rust" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" + [[package]] name = "yaml-rust" version = "0.4.5" diff --git a/Cargo.toml b/Cargo.toml index abe16e1fee..b0d79e411c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ homepage = "https://miden.xyz" license = "MIT" readme = "README.md" repository = "https://github.com/0xMiden/miden-node" -rust-version = "1.90" +rust-version = "1.91" version = "0.14.0" # Optimize the cryptography for faster tests involving account creation. diff --git a/crates/validator/Cargo.toml b/crates/validator/Cargo.toml index 26a76a2b3b..ca6378384c 100644 --- a/crates/validator/Cargo.toml +++ b/crates/validator/Cargo.toml @@ -17,13 +17,13 @@ workspace = true [features] [dependencies] +fjall = { version = "3.0" } anyhow = { workspace = true } deadpool-diesel = { workspace = true } diesel = { workspace = true } diesel_migrations = { workspace = true } miden-node-proto = { workspace = true } miden-node-proto-build = { features = ["internal"], workspace = true } -miden-node-store = { workspace = true } miden-node-utils = { features = ["testing"], workspace = true } miden-protocol = { workspace = true } miden-tx = { workspace = true } diff --git a/crates/validator/src/block_validation/mod.rs b/crates/validator/src/block_validation/mod.rs index ede74b5d52..c333dc7515 100644 --- a/crates/validator/src/block_validation/mod.rs +++ b/crates/validator/src/block_validation/mod.rs @@ -1,36 +1,20 @@ -use std::collections::HashMap; - -use miden_node_store::{DatabaseError, Db}; -use miden_protocol::block::{BlockNumber, BlockSigner, ProposedBlock}; +use miden_protocol::block::{BlockSigner, ProposedBlock}; use miden_protocol::crypto::dsa::ecdsa_k256_keccak::Signature; use miden_protocol::errors::ProposedBlockError; -use miden_protocol::transaction::{TransactionHeader, TransactionId}; +use miden_protocol::transaction::TransactionHeader; use tracing::info_span; -use crate::db::select_validated_transactions; +use crate::db::{Database, DatabaseError}; // BLOCK VALIDATION ERROR // ================================================================================================ #[derive(thiserror::Error, Debug)] pub enum BlockValidationError { - #[error("found unvalidated transactions {0:?}")] - UnvalidatedTransactions(Vec), - #[error("transaction {0} in block {1} has not been validated")] - TransactionNotValidated(TransactionId, BlockNumber), - #[error( - "the proposed transaction {proposed_tx} does not match the validated transaction {validated_tx}" - )] - TransactionMismatch { - proposed_tx: TransactionId, - validated_tx: TransactionId, - }, #[error("failed to build block")] BlockBuildingFailed(#[from] ProposedBlockError), #[error("failed to select transactions")] DatabaseError(#[from] DatabaseError), - #[error("internal error: {0}")] - Other(String), } // BLOCK VALIDATION @@ -43,53 +27,13 @@ pub enum BlockValidationError { pub async fn validate_block( proposed_block: ProposedBlock, signer: &S, - db: &Db, + db: &Database, ) -> Result { - // Create a map of transactions from the proposed block. - let proposed_transactions = proposed_block - .transactions() - .map(|header| (header.id(), header.clone())) - .collect::>(); - // Retrieve all validated transactions pertaining to the proposed block. let proposed_tx_ids = proposed_block.transactions().map(TransactionHeader::id).collect::>(); - let query_tx_ids = proposed_tx_ids.clone(); - let validated_transactions = db - .transact("select_transactions", move |conn| { - select_validated_transactions(conn, &query_tx_ids) - }) - .await?; - - // All proposed transactions must have been validated. - if proposed_tx_ids.len() > validated_transactions.len() { - let mut proposed_tx_ids = proposed_tx_ids; - // Return proposed transactions that have not been validated. - proposed_tx_ids - .retain(|proposed_tx_id| !validated_transactions.contains_key(proposed_tx_id)); - return Err(BlockValidationError::UnvalidatedTransactions(proposed_tx_ids)); - } - - // Check that every transaction from the proposed block has been validated. - for proposed_tx_id in proposed_tx_ids { - let Some(validated_tx) = validated_transactions.get(&proposed_tx_id) else { - return Err(BlockValidationError::TransactionNotValidated( - proposed_tx_id, - proposed_block.block_num(), - )); - }; - // Check that the proposed and validated transactions are equal. - let proposed_tx = - proposed_transactions.get(&proposed_tx_id).ok_or(BlockValidationError::Other( - "proposed transactions mapped incorrectly from proposed block".into(), - ))?; - if validated_tx != proposed_tx { - return Err(BlockValidationError::TransactionMismatch { - proposed_tx: proposed_tx.id(), - validated_tx: validated_tx.id(), - }); - } - } + // TODO(currentpr): If we don't need to retrieve the data at all we can change this. + let _validated_transactions = db.get(&proposed_tx_ids)?; // Build the block header. let (header, _) = proposed_block.into_header_and_body()?; diff --git a/crates/validator/src/db/database.rs b/crates/validator/src/db/database.rs new file mode 100644 index 0000000000..671758b0e7 --- /dev/null +++ b/crates/validator/src/db/database.rs @@ -0,0 +1,64 @@ +use std::path::Path; + +use miden_protocol::transaction::{TransactionHeader, TransactionId}; +use miden_tx::utils::DeserializationError; + +use crate::db::kv_conv::{ToKey, ToValue}; + +#[derive(thiserror::Error, Debug)] +pub enum DatabaseError { + #[error("underlying database error")] + ExecutionError(#[from] fjall::Error), + #[error("failed to deserialize value bytes from the database")] + DeserializationError(#[from] DeserializationError), + #[error("validated transactions not found {0:?}")] + TransactionsNotFound(Vec), +} + +pub struct Database { + _db: fjall::Database, + keyspace: fjall::Keyspace, +} + +impl Database { + /// Creates a new key-value database in the specified directory. + pub fn new(dir: &Path) -> Result { + let db = fjall::Database::builder(dir.join("validator")).open()?; + let keyspace = db.keyspace("transactions", fjall::KeyspaceCreateOptions::default)?; + + Ok(Self { _db: db, keyspace }) + } + + /// Stores a transaction header into the database. + pub fn put(&self, validated_tx_header: &TransactionHeader) -> Result<(), DatabaseError> { + self.keyspace + .insert(validated_tx_header.id().to_key(), validated_tx_header.to_value())?; + Ok(()) + } + + /// Retrieves a transaction headers corresponding to the transaction ids from the database. + /// + /// # Errors + /// + /// Returns an error if: + /// - Any requested transactions are not found. + /// - Retrieved transactions cannot be deserialized. + pub fn get(&self, ids: &[TransactionId]) -> Result, DatabaseError> { + let mut txs = Vec::with_capacity(ids.len()); + let mut txs_not_found = Vec::new(); + for id in ids { + let value = self.keyspace.get(id.as_bytes())?; + if let Some(value) = value { + let tx_header = ::from_value(value)?; + txs.push(tx_header); + } else { + txs_not_found.push(*id); + } + } + if txs_not_found.is_empty() { + Ok(txs) + } else { + Err(DatabaseError::TransactionsNotFound(txs_not_found)) + } + } +} diff --git a/crates/validator/src/db/kv_conv.rs b/crates/validator/src/db/kv_conv.rs new file mode 100644 index 0000000000..aae5154562 --- /dev/null +++ b/crates/validator/src/db/kv_conv.rs @@ -0,0 +1,28 @@ +use miden_protocol::transaction::{TransactionHeader, TransactionId}; +use miden_protocol::utils::Deserializable; +use miden_tx::utils::{DeserializationError, Serializable}; + +pub trait ToKey { + fn to_key(&self) -> fjall::UserKey; +} + +pub trait ToValue: Sized { + fn to_value(&self) -> fjall::UserValue; + fn from_value(slice: fjall::Slice) -> Result; +} + +impl ToKey for TransactionId { + fn to_key(&self) -> fjall::UserKey { + fjall::UserKey::new(&self.as_bytes()) + } +} + +impl ToValue for TransactionHeader { + fn to_value(&self) -> fjall::UserValue { + fjall::UserValue::new(&self.to_bytes()) + } + + fn from_value(slice: fjall::Slice) -> Result { + Self::read_from_bytes(&slice) + } +} diff --git a/crates/validator/src/db/migrations.rs b/crates/validator/src/db/migrations.rs deleted file mode 100644 index 6896082bed..0000000000 --- a/crates/validator/src/db/migrations.rs +++ /dev/null @@ -1,25 +0,0 @@ -use diesel::SqliteConnection; -use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations}; -use miden_node_store::DatabaseError; -use tracing::instrument; - -use crate::COMPONENT; - -// The rebuild is automatically triggered by `build.rs` as described in -// . -pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("src/db/migrations"); - -#[instrument(level = "debug", target = COMPONENT, skip_all, err)] -pub fn apply_migrations(conn: &mut SqliteConnection) -> std::result::Result<(), DatabaseError> { - let migrations = conn.pending_migrations(MIGRATIONS).expect("In memory migrations never fail"); - tracing::info!(target = COMPONENT, "Applying {} migration(s)", migrations.len()); - - let Err(e) = conn.run_pending_migrations(MIGRATIONS) else { - return Ok(()); - }; - tracing::warn!(target = COMPONENT, "Failed to apply migration: {e:?}"); - conn.revert_last_migration(MIGRATIONS) - .expect("Duality is maintained by the developer"); - - Ok(()) -} diff --git a/crates/validator/src/db/migrations/2025062000000_setup/down.sql b/crates/validator/src/db/migrations/2025062000000_setup/down.sql deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/crates/validator/src/db/migrations/2025062000000_setup/up.sql b/crates/validator/src/db/migrations/2025062000000_setup/up.sql deleted file mode 100644 index a942c27d68..0000000000 --- a/crates/validator/src/db/migrations/2025062000000_setup/up.sql +++ /dev/null @@ -1,5 +0,0 @@ -CREATE TABLE transactions ( - id BLOB NOT NULL, - data BLOB NOT NULL, - PRIMARY KEY (id) -) WITHOUT ROWID; diff --git a/crates/validator/src/db/mod.rs b/crates/validator/src/db/mod.rs index c987216120..f7aa2bd5dd 100644 --- a/crates/validator/src/db/mod.rs +++ b/crates/validator/src/db/mod.rs @@ -1,72 +1,4 @@ -mod migrations; -mod models; -mod schema; +mod database; +pub use database::{Database, DatabaseError}; -use std::collections::HashMap; -use std::path::PathBuf; - -use diesel::SqliteConnection; -use diesel::prelude::*; -use miden_node_store::{ConnectionManager, DatabaseError, DatabaseSetupError}; -use miden_protocol::transaction::{TransactionHeader, TransactionId}; -use miden_protocol::utils::{Deserializable, Serializable}; -use tracing::instrument; - -use crate::COMPONENT; -use crate::db::migrations::apply_migrations; -use crate::db::models::{TransactionSummaryRowInsert, TransactionSummaryRowSelect}; - -/// Open a connection to the DB and apply any pending migrations. -#[instrument(target = COMPONENT, skip_all)] -pub async fn load(database_filepath: PathBuf) -> Result { - let manager = ConnectionManager::new(database_filepath.to_str().unwrap()); - let pool = deadpool_diesel::Pool::builder(manager).max_size(16).build()?; - - tracing::info!( - target: COMPONENT, - sqlite= %database_filepath.display(), - "Connected to the database" - ); - - let db = miden_node_store::Db::new(pool); - db.query("migrations", apply_migrations).await?; - Ok(db) -} - -pub(crate) fn insert_transaction( - conn: &mut SqliteConnection, - header: &TransactionHeader, -) -> Result { - let row = TransactionSummaryRowInsert::new(header); - let count = diesel::insert_into(schema::transactions::table).values(row).execute(conn)?; - Ok(count) -} - -pub(crate) fn select_validated_transactions( - conn: &mut SqliteConnection, - tx_ids: &[TransactionId], -) -> Result, DatabaseError> { - if tx_ids.is_empty() { - return Ok(HashMap::new()); - } - - // Convert TransactionIds to bytes for query. - let tx_id_bytes: Vec> = tx_ids.iter().map(TransactionId::to_bytes).collect(); - - // Query the database for matching transactions. - let raw_transactions = schema::transactions::table - .filter(schema::transactions::transaction_id.eq_any(tx_id_bytes)) - .order(schema::transactions::transaction_id.asc()) - .load::(conn) - .map_err(DatabaseError::from)?; - - // Deserialize the transaction blobs. - let mut transactions: HashMap = HashMap::new(); - for raw_tx in raw_transactions { - let tx_id = TransactionId::read_from_bytes(&raw_tx.transaction_id)?; - let tx_header = TransactionHeader::read_from_bytes(&raw_tx.data)?; - transactions.insert(tx_id, tx_header); - } - - Ok(transactions) -} +mod kv_conv; diff --git a/crates/validator/src/db/models.rs b/crates/validator/src/db/models.rs deleted file mode 100644 index 59f4379ac9..0000000000 --- a/crates/validator/src/db/models.rs +++ /dev/null @@ -1,30 +0,0 @@ -use diesel::prelude::*; -use miden_protocol::transaction::TransactionHeader; -use miden_tx::utils::Serializable; - -use crate::db::schema; - -#[derive(Debug, Clone, PartialEq, Insertable)] -#[diesel(table_name = schema::transactions)] -#[diesel(check_for_backend(diesel::sqlite::Sqlite))] -pub struct TransactionSummaryRowInsert { - pub transaction_id: Vec, - pub data: Vec, -} - -impl TransactionSummaryRowInsert { - pub fn new(transaction_header: &TransactionHeader) -> Self { - Self { - transaction_id: transaction_header.id().to_bytes(), - data: transaction_header.to_bytes(), - } - } -} - -#[derive(Debug, Clone, PartialEq, Queryable, Selectable)] -#[diesel(table_name = schema::transactions)] -#[diesel(check_for_backend(diesel::sqlite::Sqlite))] -pub struct TransactionSummaryRowSelect { - pub transaction_id: Vec, - pub data: Vec, -} diff --git a/crates/validator/src/db/schema.rs b/crates/validator/src/db/schema.rs deleted file mode 100644 index 198a559103..0000000000 --- a/crates/validator/src/db/schema.rs +++ /dev/null @@ -1,6 +0,0 @@ -diesel::table! { - transactions (transaction_id) { - transaction_id -> Binary, - data -> Binary, - } -} diff --git a/crates/validator/src/server/mod.rs b/crates/validator/src/server/mod.rs index e99fbf593f..e13cfcba24 100644 --- a/crates/validator/src/server/mod.rs +++ b/crates/validator/src/server/mod.rs @@ -6,7 +6,6 @@ use anyhow::Context; use miden_node_proto::generated::validator::api_server; use miden_node_proto::generated::{self as proto}; use miden_node_proto_build::validator_api_descriptor; -use miden_node_store::Db; use miden_node_utils::ErrorReport; use miden_node_utils::panic::catch_panic_layer_fn; use miden_node_utils::tracing::OpenTelemetrySpanExt; @@ -23,7 +22,7 @@ use tracing::info_span; use crate::COMPONENT; use crate::block_validation::validate_block; -use crate::db::{insert_transaction, load}; +use crate::db::Database; use crate::tx_validation::validate_transaction; // VALIDATOR @@ -56,9 +55,7 @@ impl Validator { tracing::info!(target: COMPONENT, endpoint=?self.address, "Initializing server"); // Initialize database connection. - let db = load(self.data_directory.join("validator.sqlite3")) - .await - .context("failed to initialize validator database")?; + let db = Database::new(&self.data_directory)?; let listener = TcpListener::bind(self.address) .await @@ -100,11 +97,11 @@ impl Validator { /// Implements the gRPC API for the validator. struct ValidatorServer { signer: S, - db: Db, + db: Database, } impl ValidatorServer { - fn new(signer: S, db: Db) -> Self { + fn new(signer: S, db: Database) -> Self { Self { signer, db } } } @@ -151,10 +148,8 @@ impl api_server::Api for ValidatorServer // Store the validated transaction. self.db - .transact("insert_transaction", move |conn| { - insert_transaction(conn, &validated_tx_header) - }) - .await?; + .put(&validated_tx_header) + .map_err(|err| Status::internal(err.as_report_context("Failed insert")))?; Ok(tonic::Response::new(())) } diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 6744e56e15..d9a424cef9 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,5 +1,5 @@ [toolchain] -channel = "1.90" +channel = "1.91" components = ["clippy", "rust-src", "rustfmt"] profile = "minimal" targets = ["wasm32-unknown-unknown"] From 1ec03ef5688c362d41728c2b779900843d77cebc Mon Sep 17 00:00:00 2001 From: sergerad Date: Tue, 3 Feb 2026 16:43:08 +1300 Subject: [PATCH 13/72] Misc cleanup --- crates/validator/src/db/database.rs | 20 +++++++++----------- crates/validator/src/db/kv_conv.rs | 9 +++++++-- crates/validator/src/db/mod.rs | 5 ++++- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/crates/validator/src/db/database.rs b/crates/validator/src/db/database.rs index 671758b0e7..83588c4740 100644 --- a/crates/validator/src/db/database.rs +++ b/crates/validator/src/db/database.rs @@ -1,22 +1,20 @@ use std::path::Path; use miden_protocol::transaction::{TransactionHeader, TransactionId}; -use miden_tx::utils::DeserializationError; +use crate::db::DatabaseError; use crate::db::kv_conv::{ToKey, ToValue}; -#[derive(thiserror::Error, Debug)] -pub enum DatabaseError { - #[error("underlying database error")] - ExecutionError(#[from] fjall::Error), - #[error("failed to deserialize value bytes from the database")] - DeserializationError(#[from] DeserializationError), - #[error("validated transactions not found {0:?}")] - TransactionsNotFound(Vec), -} - +/// Validator database for storing validated transaction data. +/// +/// Transaction data stored in this database is intended to allow for the validation of proposed +/// blocks. All transactions in proposed blocks are expected to have been validated and stored in +/// this database before the proposed block has been received. pub struct Database { + /// The underlying key-value database. _db: fjall::Database, + + /// The "namespace" that all transaction data is stored in. keyspace: fjall::Keyspace, } diff --git a/crates/validator/src/db/kv_conv.rs b/crates/validator/src/db/kv_conv.rs index aae5154562..8b79abf447 100644 --- a/crates/validator/src/db/kv_conv.rs +++ b/crates/validator/src/db/kv_conv.rs @@ -2,13 +2,16 @@ use miden_protocol::transaction::{TransactionHeader, TransactionId}; use miden_protocol::utils::Deserializable; use miden_tx::utils::{DeserializationError, Serializable}; +/// Trait for converting types into keys for the key-value store. pub trait ToKey { fn to_key(&self) -> fjall::UserKey; } +/// Trait for converting types from and into values for the key-value store. pub trait ToValue: Sized { + type Error; fn to_value(&self) -> fjall::UserValue; - fn from_value(slice: fjall::Slice) -> Result; + fn from_value(slice: fjall::Slice) -> Result; } impl ToKey for TransactionId { @@ -18,11 +21,13 @@ impl ToKey for TransactionId { } impl ToValue for TransactionHeader { + type Error = DeserializationError; + fn to_value(&self) -> fjall::UserValue { fjall::UserValue::new(&self.to_bytes()) } - fn from_value(slice: fjall::Slice) -> Result { + fn from_value(slice: fjall::Slice) -> Result { Self::read_from_bytes(&slice) } } diff --git a/crates/validator/src/db/mod.rs b/crates/validator/src/db/mod.rs index f7aa2bd5dd..d3672e6522 100644 --- a/crates/validator/src/db/mod.rs +++ b/crates/validator/src/db/mod.rs @@ -1,4 +1,7 @@ mod database; -pub use database::{Database, DatabaseError}; +pub use database::Database; + +mod errors; +pub use errors::DatabaseError; mod kv_conv; From 4735bd17e3608899f976b5d645d3265a116b1df3 Mon Sep 17 00:00:00 2001 From: sergerad Date: Tue, 3 Feb 2026 16:45:17 +1300 Subject: [PATCH 14/72] Undo pub changes to store crate --- Cargo.lock | 3 --- crates/store/src/db/manager.rs | 4 ++-- crates/store/src/db/mod.rs | 9 ++------- crates/store/src/db/models/conv.rs | 2 +- crates/store/src/lib.rs | 3 --- crates/validator/Cargo.toml | 5 +---- crates/validator/build.rs | 9 --------- crates/validator/diesel.toml | 5 ----- 8 files changed, 6 insertions(+), 34 deletions(-) delete mode 100644 crates/validator/build.rs delete mode 100644 crates/validator/diesel.toml diff --git a/Cargo.lock b/Cargo.lock index b2c7acda64..b5d16de5ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3126,9 +3126,6 @@ name = "miden-node-validator" version = "0.14.0" dependencies = [ "anyhow", - "deadpool-diesel", - "diesel", - "diesel_migrations", "fjall", "miden-node-proto", "miden-node-proto-build", diff --git a/crates/store/src/db/manager.rs b/crates/store/src/db/manager.rs index f1ac820df3..fca9a33db6 100644 --- a/crates/store/src/db/manager.rs +++ b/crates/store/src/db/manager.rs @@ -36,12 +36,12 @@ impl ConnectionManagerError { /// Create a connection manager with per-connection setup /// /// Particularly, `foreign_key` checks are enabled and using a write-append-log for journaling. -pub struct ConnectionManager { +pub(crate) struct ConnectionManager { pub(crate) manager: deadpool_diesel::sqlite::Manager, } impl ConnectionManager { - pub fn new(database_path: &str) -> Self { + pub(crate) fn new(database_path: &str) -> Self { let manager = deadpool_diesel::sqlite::Manager::new( database_path.to_owned(), deadpool_diesel::sqlite::Runtime::Tokio1, diff --git a/crates/store/src/db/mod.rs b/crates/store/src/db/mod.rs index ea461d6174..6b7ecec6a3 100644 --- a/crates/store/src/db/mod.rs +++ b/crates/store/src/db/mod.rs @@ -224,11 +224,6 @@ impl From for NoteSyncRecord { } impl Db { - /// Creates a new database instance with the provided connection pool. - pub fn new(pool: deadpool_diesel::Pool) -> Self { - Self { pool } - } - /// Creates a new database and inserts the genesis block. #[instrument( target = COMPONENT, @@ -271,7 +266,7 @@ impl Db { } /// Create and commit a transaction with the queries added in the provided closure - pub async fn transact(&self, msg: M, query: Q) -> std::result::Result + pub(crate) async fn transact(&self, msg: M, query: Q) -> std::result::Result where Q: Send + for<'a, 't> FnOnce(&'a mut SqliteConnection) -> std::result::Result @@ -296,7 +291,7 @@ impl Db { } /// Run the query _without_ a transaction - pub async fn query(&self, msg: M, query: Q) -> std::result::Result + pub(crate) async fn query(&self, msg: M, query: Q) -> std::result::Result where Q: Send + FnOnce(&mut SqliteConnection) -> std::result::Result + 'static, R: Send + 'static, diff --git a/crates/store/src/db/models/conv.rs b/crates/store/src/db/models/conv.rs index a52fe194d4..2e6313bf61 100644 --- a/crates/store/src/db/models/conv.rs +++ b/crates/store/src/db/models/conv.rs @@ -50,7 +50,7 @@ pub struct DatabaseTypeConversionError { /// Convert from and to it's database representation and back /// /// We do not assume sanity of DB types. -pub trait SqlTypeConvert: Sized { +pub(crate) trait SqlTypeConvert: Sized { type Raw: Sized; fn to_raw_sql(self) -> Self::Raw; diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs index 06bba2fe8a..a437e8a60c 100644 --- a/crates/store/src/lib.rs +++ b/crates/store/src/lib.rs @@ -10,9 +10,6 @@ pub mod state; #[cfg(feature = "rocksdb")] pub use accounts::PersistentAccountTree; pub use accounts::{AccountTreeWithHistory, HistoricalError, InMemoryAccountTree}; -pub use db::Db; -pub use db::manager::ConnectionManager; -pub use db::models::conv::SqlTypeConvert; pub use errors::{DatabaseError, DatabaseSetupError}; pub use genesis::GenesisState; pub use server::block_prover_client::BlockProver; diff --git a/crates/validator/Cargo.toml b/crates/validator/Cargo.toml index ca6378384c..803e9be5e2 100644 --- a/crates/validator/Cargo.toml +++ b/crates/validator/Cargo.toml @@ -17,11 +17,8 @@ workspace = true [features] [dependencies] -fjall = { version = "3.0" } anyhow = { workspace = true } -deadpool-diesel = { workspace = true } -diesel = { workspace = true } -diesel_migrations = { workspace = true } +fjall = { version = "3.0" } miden-node-proto = { workspace = true } miden-node-proto-build = { features = ["internal"], workspace = true } miden-node-utils = { features = ["testing"], workspace = true } diff --git a/crates/validator/build.rs b/crates/validator/build.rs deleted file mode 100644 index b9f947e177..0000000000 --- a/crates/validator/build.rs +++ /dev/null @@ -1,9 +0,0 @@ -// This build.rs is required to trigger the `diesel_migrations::embed_migrations!` proc-macro in -// `validator/src/db/migrations.rs` to include the latest version of the migrations into the binary, see . -fn main() { - println!("cargo:rerun-if-changed=./src/db/migrations"); - // If we do one re-write, the default rules are disabled, - // hence we need to trigger explicitly on `Cargo.toml`. - // - println!("cargo:rerun-if-changed=Cargo.toml"); -} diff --git a/crates/validator/diesel.toml b/crates/validator/diesel.toml deleted file mode 100644 index bdce9175fa..0000000000 --- a/crates/validator/diesel.toml +++ /dev/null @@ -1,5 +0,0 @@ -# For documentation on how to configure this file, -# see https://diesel.rs/guides/configuring-diesel-cli - -[print_schema] -file = "src/db/schema.rs" From 147b0aacf06a37a0e7fda9c5d1d221fe50c39818 Mon Sep 17 00:00:00 2001 From: sergerad Date: Tue, 3 Feb 2026 16:46:06 +1300 Subject: [PATCH 15/72] Changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a27322afda..7546f2aa24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ ### Enhancements - [BREAKING] Updated miden-base dependencies to use `next` branch; renamed `NoteInputs` to `NoteStorage`, `.inputs()` to `.storage()`, and database `inputs` column to `storage` ([#1595](https://github.com/0xMiden/miden-node/pull/1595)). -- Added sqlite database to Validator for transaction persistence ([#1614](https://github.com/0xMiden/miden-node/pull/1614)). +- Added key-value database to Validator for transaction persistence ([#1614](https://github.com/0xMiden/miden-node/pull/1614)). - [BREAKING] Move block proving from Blocker Producer to the Store ([#1579](https://github.com/0xMiden/miden-node/pull/1579)). ### Changes From 1596c50c09791392704b4ad9e559ed573b7bb98d Mon Sep 17 00:00:00 2001 From: sergerad Date: Tue, 3 Feb 2026 16:55:15 +1300 Subject: [PATCH 16/72] Missing file --- crates/validator/src/db/errors.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 crates/validator/src/db/errors.rs diff --git a/crates/validator/src/db/errors.rs b/crates/validator/src/db/errors.rs new file mode 100644 index 0000000000..21eb241045 --- /dev/null +++ b/crates/validator/src/db/errors.rs @@ -0,0 +1,12 @@ +use miden_protocol::transaction::TransactionId; +use miden_tx::utils::DeserializationError; + +#[derive(thiserror::Error, Debug)] +pub enum DatabaseError { + #[error("underlying database error")] + ExecutionError(#[from] fjall::Error), + #[error("failed to deserialize value bytes from the database")] + DeserializationError(#[from] DeserializationError), + #[error("the following transactions were not found to be validated {0:?}")] + TransactionsNotFound(Vec), +} From 92b16f5f1334c1a14ed79b605763d6e2066892b1 Mon Sep 17 00:00:00 2001 From: sergerad Date: Tue, 3 Feb 2026 17:03:04 +1300 Subject: [PATCH 17/72] Update dockerfile --- CHANGELOG.md | 3 ++- bin/node/Dockerfile | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7546f2aa24..ea05abecba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,9 @@ ### Enhancements - [BREAKING] Updated miden-base dependencies to use `next` branch; renamed `NoteInputs` to `NoteStorage`, `.inputs()` to `.storage()`, and database `inputs` column to `storage` ([#1595](https://github.com/0xMiden/miden-node/pull/1595)). -- Added key-value database to Validator for transaction persistence ([#1614](https://github.com/0xMiden/miden-node/pull/1614)). - [BREAKING] Move block proving from Blocker Producer to the Store ([#1579](https://github.com/0xMiden/miden-node/pull/1579)). +- Added key-value database to Validator for transaction persistence ([#1614](https://github.com/0xMiden/miden-node/pull/1614)). +- [BREAKING] Updated MSRV to 1.91 ([#1614](https://github.com/0xMiden/miden-node/pull/1614)). ### Changes diff --git a/bin/node/Dockerfile b/bin/node/Dockerfile index 832b0bb8d2..a8edfa5db6 100644 --- a/bin/node/Dockerfile +++ b/bin/node/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.90-slim-bullseye AS builder +FROM rust:1.91-slim-bookworm AS builder # Install build dependencies. RocksDB is compiled from source by librocksdb-sys. RUN apt-get update && \ From ae5510cafdd625ace1dec375597e9b7d2a6b13af Mon Sep 17 00:00:00 2001 From: sergerad Date: Wed, 4 Feb 2026 10:08:34 +1300 Subject: [PATCH 18/72] Fix test --- crates/block-producer/src/server/tests.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/block-producer/src/server/tests.rs b/crates/block-producer/src/server/tests.rs index c404a2ae90..af23bcf78c 100644 --- a/crates/block-producer/src/server/tests.rs +++ b/crates/block-producer/src/server/tests.rs @@ -48,6 +48,7 @@ async fn block_producer_startup_is_robust_to_network_failures() { address: validator_addr, grpc_timeout, signer: SecretKey::random(), + data_directory: "/tmp/".into(), } .serve() .await From 15b9899041f98a57079200bffb55cd51218cad72 Mon Sep 17 00:00:00 2001 From: sergerad Date: Wed, 4 Feb 2026 10:25:08 +1300 Subject: [PATCH 19/72] Revert "Fix test" This reverts commit ae5510cafdd625ace1dec375597e9b7d2a6b13af. --- crates/block-producer/src/server/tests.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/block-producer/src/server/tests.rs b/crates/block-producer/src/server/tests.rs index af23bcf78c..c404a2ae90 100644 --- a/crates/block-producer/src/server/tests.rs +++ b/crates/block-producer/src/server/tests.rs @@ -48,7 +48,6 @@ async fn block_producer_startup_is_robust_to_network_failures() { address: validator_addr, grpc_timeout, signer: SecretKey::random(), - data_directory: "/tmp/".into(), } .serve() .await From bed51da91e0948dd5cb05131e848ec3015d81795 Mon Sep 17 00:00:00 2001 From: sergerad Date: Wed, 4 Feb 2026 10:25:08 +1300 Subject: [PATCH 20/72] Revert "Merge branch 'next' of github.com:0xMiden/miden-node into sergerad-validator-db" This reverts commit 411c8e8f196d2fd46d3176fd5676990000f2b97e, reversing changes made to 92b16f5f1334c1a14ed79b605763d6e2066892b1. --- .github/workflows/build-docker.yml | 34 +++- Cargo.lock | 2 - bin/node/Dockerfile | 44 ++--- crates/block-producer/Cargo.toml | 2 +- crates/block-producer/src/server/mod.rs | 3 - crates/block-producer/src/server/tests.rs | 215 ++++++++++++---------- crates/rocksdb-cxx-linkage-fix/src/lib.rs | 4 +- crates/rpc/src/tests.rs | 3 - crates/store/Cargo.toml | 3 - crates/store/build.rs | 3 - crates/utils/Cargo.toml | 3 - crates/utils/build.rs | 3 - 12 files changed, 168 insertions(+), 151 deletions(-) delete mode 100644 crates/utils/build.rs diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index b259c23fd9..0e7fe0c073 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -12,16 +12,38 @@ permissions: jobs: docker-build: + strategy: + matrix: + component: [node] runs-on: Linux-ARM64-Runner + name: Build ${{ matrix.component }} steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Configure AWS credentials + if: github.event.pull_request.head.repo.fork == false + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: ${{ secrets.AWS_REGION }} + role-to-assume: ${{ secrets.AWS_ROLE }} + role-session-name: GithubActionsSession + + - name: Set cache parameters + if: github.event.pull_request.head.repo.fork == false + run: | + echo "CACHE_FROM=type=s3,region=${{ secrets.AWS_REGION }},bucket=${{ secrets.AWS_CACHE_BUCKET }},name=miden-${{ matrix.component }}" >> $GITHUB_ENV + echo "CACHE_TO=type=s3,region=${{ secrets.AWS_REGION }},bucket=${{ secrets.AWS_CACHE_BUCKET }},name=miden-${{ matrix.component }}" >> $GITHUB_ENV + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 + with: + cache-binary: true - - name: Build and push - uses: docker/build-push-action@v6 + - name: Build Docker image + uses: docker/build-push-action@v5 with: push: false - file: ./bin/node/Dockerfile - cache-from: type=gha - # Only save cache on push into next - cache-to: ${{ github.event_name == 'push' && github.ref == 'refs/heads/next' && 'type=gha,mode=max' || '' }} + file: ./bin/${{ matrix.component }}/Dockerfile + cache-from: ${{ env.CACHE_FROM || '' }} + cache-to: ${{ env.CACHE_TO || '' }} diff --git a/Cargo.lock b/Cargo.lock index a186c0c1b0..b5d16de5ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2896,7 +2896,6 @@ dependencies = [ "miden-node-store", "miden-node-test-macro", "miden-node-utils", - "miden-node-validator", "miden-protocol", "miden-remote-prover-client", "miden-standards", @@ -3105,7 +3104,6 @@ dependencies = [ "http-body-util", "itertools 0.14.0", "lru 0.16.3", - "miden-node-rocksdb-cxx-linkage-fix", "miden-protocol", "opentelemetry", "opentelemetry-otlp", diff --git a/bin/node/Dockerfile b/bin/node/Dockerfile index ea3adfe171..a8edfa5db6 100644 --- a/bin/node/Dockerfile +++ b/bin/node/Dockerfile @@ -1,48 +1,39 @@ -FROM rust:1.91-slim-bookworm AS chef +FROM rust:1.91-slim-bookworm AS builder # Install build dependencies. RocksDB is compiled from source by librocksdb-sys. RUN apt-get update && \ apt-get -y upgrade && \ - apt-get install -y \ - llvm \ - clang \ - libclang-dev \ - cmake \ - pkg-config \ - libssl-dev \ - libsqlite3-dev \ - ca-certificates && \ + apt-get install -y llvm clang libclang-dev pkg-config libssl-dev libsqlite3-dev ca-certificates && \ rm -rf /var/lib/apt/lists/* -RUN cargo install cargo-chef + WORKDIR /app +COPY ./Cargo.toml . +COPY ./Cargo.lock . +COPY ./bin ./bin +COPY ./crates ./crates +COPY ./proto ./proto -FROM chef AS planner -COPY . . -RUN cargo chef prepare --recipe-path recipe.json +RUN cargo install --path bin/node --locked -FROM chef AS builder -COPY --from=planner /app/recipe.json recipe.json -# Build dependencies - this is the caching Docker layer! -RUN cargo chef cook --release --recipe-path recipe.json -# Build application -COPY . . -RUN cargo build --release --locked --bin miden-node +FROM debian:bullseye-slim -# Base line runtime image with runtime dependencies installed. -FROM debian:bullseye-slim AS runtime-base +# Update machine & install required packages +# The installation of sqlite3 is needed for correct function of the SQLite database RUN apt-get update && \ apt-get -y upgrade && \ - apt-get install -y --no-install-recommends sqlite3 \ + apt-get install -y --no-install-recommends \ + sqlite3 \ && rm -rf /var/lib/apt/lists/* -FROM runtime-base AS runtime -COPY --from=builder /app/target/release/miden-node /usr/local/bin/miden-node +COPY --from=builder /usr/local/cargo/bin/miden-node /usr/local/bin/miden-node + LABEL org.opencontainers.image.authors=devops@miden.team \ org.opencontainers.image.url=https://0xMiden.github.io/ \ org.opencontainers.image.documentation=https://github.com/0xMiden/miden-node \ org.opencontainers.image.source=https://github.com/0xMiden/miden-node \ org.opencontainers.image.vendor=Miden \ org.opencontainers.image.licenses=MIT + ARG CREATED ARG VERSION ARG COMMIT @@ -52,5 +43,6 @@ LABEL org.opencontainers.image.created=$CREATED \ # Expose RPC port EXPOSE 57291 + # Miden node does not spawn sub-processes, so it can be used as the PID1 CMD miden-node diff --git a/crates/block-producer/Cargo.toml b/crates/block-producer/Cargo.toml index 023a7a448c..8437dab3c8 100644 --- a/crates/block-producer/Cargo.toml +++ b/crates/block-producer/Cargo.toml @@ -28,6 +28,7 @@ miden-node-utils = { features = ["testing"], workspace = true } miden-protocol = { default-features = true, workspace = true } miden-remote-prover-client = { features = ["batch-prover", "block-prover"], workspace = true } miden-standards = { workspace = true } +miden-tx = { default-features = true, workspace = true } miden-tx-batch-prover = { workspace = true } rand = { version = "0.9" } thiserror = { workspace = true } @@ -44,7 +45,6 @@ assert_matches = { workspace = true } miden-node-store = { workspace = true } miden-node-test-macro = { workspace = true } miden-node-utils = { features = ["testing"], workspace = true } -miden-node-validator = { workspace = true } miden-protocol = { default-features = true, features = ["testing"], workspace = true } miden-standards = { features = ["testing"], workspace = true } miden-tx = { features = ["testing"], workspace = true } diff --git a/crates/block-producer/src/server/mod.rs b/crates/block-producer/src/server/mod.rs index fb6963efde..d3519eb005 100644 --- a/crates/block-producer/src/server/mod.rs +++ b/crates/block-producer/src/server/mod.rs @@ -40,9 +40,6 @@ use crate::store::StoreClient; use crate::validator::BlockProducerValidatorClient; use crate::{CACHED_MEMPOOL_STATS_UPDATE_INTERVAL, COMPONENT, SERVER_NUM_BATCH_BUILDERS}; -#[cfg(test)] -mod tests; - /// The block producer server. /// /// Specifies how to connect to the store, batch prover, and block prover components. diff --git a/crates/block-producer/src/server/tests.rs b/crates/block-producer/src/server/tests.rs index c404a2ae90..453512597b 100644 --- a/crates/block-producer/src/server/tests.rs +++ b/crates/block-producer/src/server/tests.rs @@ -1,25 +1,27 @@ -use std::num::NonZeroUsize; use std::time::Duration; -use miden_node_proto::generated::block_producer::api_client as block_producer_client; +use miden_air::{ExecutionProof, HashFunction}; +use miden_node_proto::generated::{ + self as proto, block_producer::api_client as block_producer_client, +}; use miden_node_store::{GenesisState, Store}; -use miden_node_utils::fee::test_fee_params; -use miden_node_validator::Validator; -use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SecretKey; -use miden_protocol::testing::random_signer::RandomBlockSigner as _; -use tokio::net::TcpListener; -use tokio::time::sleep; -use tokio::{runtime, task}; +use miden_protocol::{ + Digest, + account::{AccountId, AccountIdVersion, AccountStorageMode, AccountType}, + transaction::ProvenTransactionBuilder, +}; +use miden_tx::utils::Serializable; +use tokio::{net::TcpListener, runtime, task, time::sleep}; use tonic::transport::{Channel, Endpoint}; -use url::Url; +use winterfell::Proof; -use crate::{BlockProducer, DEFAULT_MAX_BATCHES_PER_BLOCK, DEFAULT_MAX_TXS_PER_BATCH}; +use crate::{BlockProducer, SERVER_MAX_BATCHES_PER_BLOCK, SERVER_MAX_TXS_PER_BATCH}; -/// Tests that the block producer starts up correctly even when the store is not initially -/// available. The block producer should retry with exponential backoff until the store becomes -/// available, then start serving requests. #[tokio::test] async fn block_producer_startup_is_robust_to_network_failures() { + // This test starts the block producer and tests that it starts serving only after the store + // is started. + // get the addresses for the store and block producer let store_addr = { let store_listener = @@ -34,103 +36,113 @@ async fn block_producer_startup_is_robust_to_network_failures() { .expect("Failed to get block-producer address") }; - let validator_addr = { - let validator_listener = - TcpListener::bind("127.0.0.1:0").await.expect("failed to bind validator"); - validator_listener.local_addr().expect("failed to get validator address") + let ntx_builder_addr = { + let ntx_builder_address = TcpListener::bind("127.0.0.1:0") + .await + .expect("failed to bind the ntx builder address"); + ntx_builder_address.local_addr().expect("failed to get ntx builder address") }; - let grpc_timeout = Duration::from_secs(30); - - // start the validator - task::spawn(async move { - Validator { - address: validator_addr, - grpc_timeout, - signer: SecretKey::random(), - } - .serve() - .await - .unwrap(); - }); - - // start the block producer BEFORE the store is available - // this tests the exponential backoff behavior - let store_url = Url::parse(&format!("http://{store_addr}")).expect("Failed to parse store URL"); - let validator_url = - Url::parse(&format!("http://{validator_addr}")).expect("Failed to parse validator URL"); + // start the block producer task::spawn(async move { BlockProducer { block_producer_address: block_producer_addr, - store_url, - validator_url, + store_address: store_addr, + ntx_builder_address: Some(ntx_builder_addr), batch_prover_url: None, + block_prover_url: None, batch_interval: Duration::from_millis(500), block_interval: Duration::from_millis(500), - max_txs_per_batch: DEFAULT_MAX_TXS_PER_BATCH, - max_batches_per_block: DEFAULT_MAX_BATCHES_PER_BLOCK, - grpc_timeout, - mempool_tx_capacity: NonZeroUsize::new(100).unwrap(), + max_txs_per_batch: SERVER_MAX_TXS_PER_BATCH, + max_batches_per_block: SERVER_MAX_BATCHES_PER_BLOCK, } .serve() .await .unwrap(); }); - // test: connecting to the block producer should fail because the store is not yet started - // (and therefore the block producer is not yet listening) + // test: connecting to the block producer should fail until the store is started let block_producer_endpoint = Endpoint::try_from(format!("http://{block_producer_addr}")).expect("valid url"); let block_producer_client = block_producer_client::ApiClient::connect(block_producer_endpoint.clone()).await; - assert!( - block_producer_client.is_err(), - "Block producer should not be available before store is started" - ); + assert!(block_producer_client.is_err()); // start the store let data_directory = tempfile::tempdir().expect("tempdir should be created"); - let store_runtime = start_store(store_addr, data_directory.path()).await; - - // wait for the block producer's exponential backoff to connect to the store - // use a retry loop since CI environments may be slower - let block_producer_client = { - let mut attempts = 0; - loop { - attempts += 1; - match block_producer_client::ApiClient::connect(block_producer_endpoint.clone()).await { - Ok(client) => break client, - Err(_) if attempts < 30 => { - sleep(Duration::from_millis(200)).await; - }, - Err(e) => panic!( - "block producer client should connect after store is started (after {attempts} attempts): {e}" - ), + let store_runtime = { + let genesis_state = GenesisState::new(vec![], 1, 1); + Store::bootstrap(genesis_state.clone(), data_directory.path()) + .expect("store should bootstrap"); + let dir = data_directory.path().to_path_buf(); + let rpc_listener = + TcpListener::bind("127.0.0.1:0").await.expect("store should bind the RPC port"); + let ntx_builder_listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("Failed to bind store ntx-builder gRPC endpoint"); + let block_producer_listener = TcpListener::bind(store_addr) + .await + .expect("store should bind the block-producer port"); + // in order to later kill the store, we need to spawn a new runtime and run the store on + // it. That allows us to kill all the tasks spawned by the store when we + // kill the runtime. + let store_runtime = + runtime::Builder::new_multi_thread().enable_time().enable_io().build().unwrap(); + store_runtime.spawn(async move { + Store { + rpc_listener, + ntx_builder_listener, + block_producer_listener, + data_directory: dir, + grpc_timeout: std::time::Duration::from_secs(30), } - } + .serve() + .await + .expect("store should start serving"); + }); + store_runtime }; - // test: status request against block-producer should succeed - let response = send_status_request(block_producer_client).await; - assert!(response.is_ok(), "Status request should succeed, got: {:?}", response.err()); + // we need to wait for the exponential backoff of the block producer to connect to the store + sleep(Duration::from_secs(1)).await; - // verify the response contains expected data - let status = response.unwrap().into_inner(); - assert_eq!(status.status, "connected"); + let block_producer_client = block_producer_client::ApiClient::connect(block_producer_endpoint) + .await + .expect("block producer client should connect"); - // Shutdown the store before data_directory is dropped to allow the database to flush properly + // test: request against block-producer api should succeed + let response = send_request(block_producer_client.clone(), 0).await; + assert!(response.is_ok()); + + // kill the store + shutdown_store(store_runtime).await; + + // test: request against block-producer api should fail immediately + let response = send_request(block_producer_client.clone(), 1).await; + assert!(response.is_err()); + + // test: restart the store and request should succeed + let store_runtime = restart_store(store_addr, data_directory.path()).await; + let response = send_request(block_producer_client.clone(), 2).await; + assert!(response.is_ok()); + + // Shutdown the store before data_directory is dropped to allow RocksDB to flush properly shutdown_store(store_runtime).await; } -/// Starts the store with a fresh genesis state and returns the runtime handle. -async fn start_store( +/// Shuts down the store runtime properly to allow RocksDB to flush before the temp directory is +/// deleted. +async fn shutdown_store(store_runtime: runtime::Runtime) { + task::spawn_blocking(move || store_runtime.shutdown_timeout(Duration::from_millis(500))) + .await + .expect("shutdown should complete"); +} + +/// Restarts a store using an existing data directory. Returns the runtime handle for shutdown. +async fn restart_store( store_addr: std::net::SocketAddr, data_directory: &std::path::Path, ) -> runtime::Runtime { - let genesis_state = GenesisState::new(vec![], test_fee_params(), 1, 1, SecretKey::random()); - Store::bootstrap(genesis_state.clone(), data_directory).expect("store should bootstrap"); - - let dir = data_directory.to_path_buf(); let rpc_listener = TcpListener::bind("127.0.0.1:0").await.expect("store should bind the RPC port"); let ntx_builder_listener = TcpListener::bind("127.0.0.1:0") @@ -139,8 +151,7 @@ async fn start_store( let block_producer_listener = TcpListener::bind(store_addr) .await .expect("store should bind the block-producer port"); - - // Use a separate runtime so we can kill all store tasks later + let dir = data_directory.to_path_buf(); let store_runtime = runtime::Builder::new_multi_thread().enable_time().enable_io().build().unwrap(); store_runtime.spawn(async move { @@ -148,9 +159,8 @@ async fn start_store( rpc_listener, ntx_builder_listener, block_producer_listener, - block_prover_url: None, data_directory: dir, - grpc_timeout: Duration::from_secs(30), + grpc_timeout: std::time::Duration::from_secs(30), } .serve() .await @@ -159,17 +169,32 @@ async fn start_store( store_runtime } -/// Shuts down the store runtime properly to allow the database to flush before the temp directory -/// is deleted. -async fn shutdown_store(store_runtime: runtime::Runtime) { - task::spawn_blocking(move || store_runtime.shutdown_timeout(Duration::from_millis(500))) - .await - .expect("shutdown should complete"); -} - -/// Sends a status request to the block producer to verify connectivity. -async fn send_status_request( +/// Creates a dummy transaction and submits it to the block producer. +async fn send_request( mut client: block_producer_client::ApiClient, -) -> Result, tonic::Status> { - client.status(()).await + i: u8, +) -> Result, tonic::Status> +{ + let tx = ProvenTransactionBuilder::new( + AccountId::dummy( + [0; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ), + Digest::default(), + [i; 32].try_into().unwrap(), + Digest::default(), + 0.into(), + Digest::default(), + u32::MAX.into(), + ExecutionProof::new(Proof::new_dummy(), HashFunction::default()), + ) + .build() + .unwrap(); + let request = proto::transaction::ProvenTransaction { + transaction: tx.to_bytes(), + transaction_replay: None, + }; + client.submit_proven_transaction(request).await } diff --git a/crates/rocksdb-cxx-linkage-fix/src/lib.rs b/crates/rocksdb-cxx-linkage-fix/src/lib.rs index 9eaae82fd3..eeaa456d0c 100644 --- a/crates/rocksdb-cxx-linkage-fix/src/lib.rs +++ b/crates/rocksdb-cxx-linkage-fix/src/lib.rs @@ -6,7 +6,6 @@ use std::env; pub fn configure() { println!("cargo:rerun-if-env-changed=ROCKSDB_COMPILE"); - println!("cargo:rerun-if-env-changed=ROCKSDB_LIB_DIR"); println!("cargo:rerun-if-env-changed=ROCKSDB_STATIC"); println!("cargo:rerun-if-env-changed=CXXSTDLIB"); let target = env::var("TARGET").unwrap_or_default(); @@ -19,9 +18,8 @@ fn should_link_cpp_stdlib() -> bool { let rocksdb_compile = env::var("ROCKSDB_COMPILE").unwrap_or_default(); let rocksdb_compile_disabled = matches!(rocksdb_compile.as_str(), "0" | "false" | "FALSE"); let rocksdb_static = env::var("ROCKSDB_STATIC").is_ok(); - let rocksdb_lib_dir_set = env::var("ROCKSDB_LIB_DIR").is_ok(); - rocksdb_lib_dir_set || (rocksdb_static && rocksdb_compile_disabled) + rocksdb_compile_disabled && rocksdb_static } fn link_cpp_stdlib(target: &str) { diff --git a/crates/rpc/src/tests.rs b/crates/rpc/src/tests.rs index a0b7854e5d..3d87c83289 100644 --- a/crates/rpc/src/tests.rs +++ b/crates/rpc/src/tests.rs @@ -247,9 +247,6 @@ async fn rpc_server_rejects_proven_transactions_with_invalid_commitment() { let (_, rpc_addr, store_addr) = start_rpc().await; let (store_runtime, _data_directory, genesis) = start_store(store_addr).await; - // Wait for the store to be ready before sending requests. - tokio::time::sleep(Duration::from_millis(100)).await; - // Override the client so that the ACCEPT header is not set. let mut rpc_client = miden_node_proto::clients::Builder::new(Url::parse(&format!("http://{rpc_addr}")).unwrap()) diff --git a/crates/store/Cargo.toml b/crates/store/Cargo.toml index bd16c8abcc..c2e36a0085 100644 --- a/crates/store/Cargo.toml +++ b/crates/store/Cargo.toml @@ -49,9 +49,6 @@ tower-http = { features = ["util"], workspace = true } tracing = { workspace = true } url = { workspace = true } -[build-dependencies] -miden-node-rocksdb-cxx-linkage-fix = { workspace = true } - [dev-dependencies] assert_matches = { workspace = true } criterion = { version = "0.5" } diff --git a/crates/store/build.rs b/crates/store/build.rs index a911bea19b..d08f3fd0e6 100644 --- a/crates/store/build.rs +++ b/crates/store/build.rs @@ -1,12 +1,9 @@ // This build.rs is required to trigger the `diesel_migrations::embed_migrations!` proc-macro in // `store/src/db/migrations.rs` to include the latest version of the migrations into the binary, see . - fn main() { println!("cargo:rerun-if-changed=./src/db/migrations"); // If we do one re-write, the default rules are disabled, // hence we need to trigger explicitly on `Cargo.toml`. // println!("cargo:rerun-if-changed=Cargo.toml"); - - miden_node_rocksdb_cxx_linkage_fix::configure(); } diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index 2c5fea6e51..e61930937e 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -42,8 +42,5 @@ tracing-opentelemetry = { version = "0.32" } tracing-subscriber = { workspace = true } url = { workspace = true } -[build-dependencies] -miden-node-rocksdb-cxx-linkage-fix = { workspace = true } - [dev-dependencies] thiserror = { workspace = true } diff --git a/crates/utils/build.rs b/crates/utils/build.rs deleted file mode 100644 index ed4038d06e..0000000000 --- a/crates/utils/build.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - miden_node_rocksdb_cxx_linkage_fix::configure(); -} From 57b1671860c87cab4c5d58a354aa09753f601cf5 Mon Sep 17 00:00:00 2001 From: sergerad Date: Wed, 4 Feb 2026 10:25:34 +1300 Subject: [PATCH 21/72] Revert "ci(docker): use `cargo chef` and cache to github (#1631)" This reverts commit 2965984a37408ea9152adb7b0ab26f31b0675c8f. --- bin/node/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/node/Dockerfile b/bin/node/Dockerfile index a8edfa5db6..832b0bb8d2 100644 --- a/bin/node/Dockerfile +++ b/bin/node/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.91-slim-bookworm AS builder +FROM rust:1.90-slim-bullseye AS builder # Install build dependencies. RocksDB is compiled from source by librocksdb-sys. RUN apt-get update && \ From 4d1baee1753f1b399eb4b95b54f04c89f888add2 Mon Sep 17 00:00:00 2001 From: sergerad Date: Wed, 4 Feb 2026 10:25:38 +1300 Subject: [PATCH 22/72] Revert "Update dockerfile" This reverts commit 92b16f5f1334c1a14ed79b605763d6e2066892b1. --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea05abecba..7546f2aa24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,8 @@ ### Enhancements - [BREAKING] Updated miden-base dependencies to use `next` branch; renamed `NoteInputs` to `NoteStorage`, `.inputs()` to `.storage()`, and database `inputs` column to `storage` ([#1595](https://github.com/0xMiden/miden-node/pull/1595)). -- [BREAKING] Move block proving from Blocker Producer to the Store ([#1579](https://github.com/0xMiden/miden-node/pull/1579)). - Added key-value database to Validator for transaction persistence ([#1614](https://github.com/0xMiden/miden-node/pull/1614)). -- [BREAKING] Updated MSRV to 1.91 ([#1614](https://github.com/0xMiden/miden-node/pull/1614)). +- [BREAKING] Move block proving from Blocker Producer to the Store ([#1579](https://github.com/0xMiden/miden-node/pull/1579)). ### Changes From 6230ce810a1ce5894a857975fb787c1fa11686db Mon Sep 17 00:00:00 2001 From: sergerad Date: Wed, 4 Feb 2026 10:25:38 +1300 Subject: [PATCH 23/72] Revert "Missing file" This reverts commit 1596c50c09791392704b4ad9e559ed573b7bb98d. --- crates/validator/src/db/errors.rs | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 crates/validator/src/db/errors.rs diff --git a/crates/validator/src/db/errors.rs b/crates/validator/src/db/errors.rs deleted file mode 100644 index 21eb241045..0000000000 --- a/crates/validator/src/db/errors.rs +++ /dev/null @@ -1,12 +0,0 @@ -use miden_protocol::transaction::TransactionId; -use miden_tx::utils::DeserializationError; - -#[derive(thiserror::Error, Debug)] -pub enum DatabaseError { - #[error("underlying database error")] - ExecutionError(#[from] fjall::Error), - #[error("failed to deserialize value bytes from the database")] - DeserializationError(#[from] DeserializationError), - #[error("the following transactions were not found to be validated {0:?}")] - TransactionsNotFound(Vec), -} From e75e486f41f50172c72acec9a0cc4e8f0d807721 Mon Sep 17 00:00:00 2001 From: sergerad Date: Wed, 4 Feb 2026 10:25:38 +1300 Subject: [PATCH 24/72] Revert "Changelog" This reverts commit 147b0aacf06a37a0e7fda9c5d1d221fe50c39818. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7546f2aa24..a27322afda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ ### Enhancements - [BREAKING] Updated miden-base dependencies to use `next` branch; renamed `NoteInputs` to `NoteStorage`, `.inputs()` to `.storage()`, and database `inputs` column to `storage` ([#1595](https://github.com/0xMiden/miden-node/pull/1595)). -- Added key-value database to Validator for transaction persistence ([#1614](https://github.com/0xMiden/miden-node/pull/1614)). +- Added sqlite database to Validator for transaction persistence ([#1614](https://github.com/0xMiden/miden-node/pull/1614)). - [BREAKING] Move block proving from Blocker Producer to the Store ([#1579](https://github.com/0xMiden/miden-node/pull/1579)). ### Changes From 2de6f00893259938f1c6042cb35af661e73cbdf4 Mon Sep 17 00:00:00 2001 From: sergerad Date: Wed, 4 Feb 2026 10:25:38 +1300 Subject: [PATCH 25/72] Revert "Undo pub changes to store crate" This reverts commit 4735bd17e3608899f976b5d645d3265a116b1df3. --- Cargo.lock | 3 +++ crates/store/src/db/manager.rs | 4 ++-- crates/store/src/db/mod.rs | 9 +++++++-- crates/store/src/db/models/conv.rs | 2 +- crates/store/src/lib.rs | 3 +++ crates/validator/Cargo.toml | 5 ++++- crates/validator/build.rs | 9 +++++++++ crates/validator/diesel.toml | 5 +++++ 8 files changed, 34 insertions(+), 6 deletions(-) create mode 100644 crates/validator/build.rs create mode 100644 crates/validator/diesel.toml diff --git a/Cargo.lock b/Cargo.lock index b5d16de5ab..b2c7acda64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3126,6 +3126,9 @@ name = "miden-node-validator" version = "0.14.0" dependencies = [ "anyhow", + "deadpool-diesel", + "diesel", + "diesel_migrations", "fjall", "miden-node-proto", "miden-node-proto-build", diff --git a/crates/store/src/db/manager.rs b/crates/store/src/db/manager.rs index fca9a33db6..f1ac820df3 100644 --- a/crates/store/src/db/manager.rs +++ b/crates/store/src/db/manager.rs @@ -36,12 +36,12 @@ impl ConnectionManagerError { /// Create a connection manager with per-connection setup /// /// Particularly, `foreign_key` checks are enabled and using a write-append-log for journaling. -pub(crate) struct ConnectionManager { +pub struct ConnectionManager { pub(crate) manager: deadpool_diesel::sqlite::Manager, } impl ConnectionManager { - pub(crate) fn new(database_path: &str) -> Self { + pub fn new(database_path: &str) -> Self { let manager = deadpool_diesel::sqlite::Manager::new( database_path.to_owned(), deadpool_diesel::sqlite::Runtime::Tokio1, diff --git a/crates/store/src/db/mod.rs b/crates/store/src/db/mod.rs index 6b7ecec6a3..ea461d6174 100644 --- a/crates/store/src/db/mod.rs +++ b/crates/store/src/db/mod.rs @@ -224,6 +224,11 @@ impl From for NoteSyncRecord { } impl Db { + /// Creates a new database instance with the provided connection pool. + pub fn new(pool: deadpool_diesel::Pool) -> Self { + Self { pool } + } + /// Creates a new database and inserts the genesis block. #[instrument( target = COMPONENT, @@ -266,7 +271,7 @@ impl Db { } /// Create and commit a transaction with the queries added in the provided closure - pub(crate) async fn transact(&self, msg: M, query: Q) -> std::result::Result + pub async fn transact(&self, msg: M, query: Q) -> std::result::Result where Q: Send + for<'a, 't> FnOnce(&'a mut SqliteConnection) -> std::result::Result @@ -291,7 +296,7 @@ impl Db { } /// Run the query _without_ a transaction - pub(crate) async fn query(&self, msg: M, query: Q) -> std::result::Result + pub async fn query(&self, msg: M, query: Q) -> std::result::Result where Q: Send + FnOnce(&mut SqliteConnection) -> std::result::Result + 'static, R: Send + 'static, diff --git a/crates/store/src/db/models/conv.rs b/crates/store/src/db/models/conv.rs index 2e6313bf61..a52fe194d4 100644 --- a/crates/store/src/db/models/conv.rs +++ b/crates/store/src/db/models/conv.rs @@ -50,7 +50,7 @@ pub struct DatabaseTypeConversionError { /// Convert from and to it's database representation and back /// /// We do not assume sanity of DB types. -pub(crate) trait SqlTypeConvert: Sized { +pub trait SqlTypeConvert: Sized { type Raw: Sized; fn to_raw_sql(self) -> Self::Raw; diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs index a437e8a60c..06bba2fe8a 100644 --- a/crates/store/src/lib.rs +++ b/crates/store/src/lib.rs @@ -10,6 +10,9 @@ pub mod state; #[cfg(feature = "rocksdb")] pub use accounts::PersistentAccountTree; pub use accounts::{AccountTreeWithHistory, HistoricalError, InMemoryAccountTree}; +pub use db::Db; +pub use db::manager::ConnectionManager; +pub use db::models::conv::SqlTypeConvert; pub use errors::{DatabaseError, DatabaseSetupError}; pub use genesis::GenesisState; pub use server::block_prover_client::BlockProver; diff --git a/crates/validator/Cargo.toml b/crates/validator/Cargo.toml index 803e9be5e2..ca6378384c 100644 --- a/crates/validator/Cargo.toml +++ b/crates/validator/Cargo.toml @@ -17,8 +17,11 @@ workspace = true [features] [dependencies] +fjall = { version = "3.0" } anyhow = { workspace = true } -fjall = { version = "3.0" } +deadpool-diesel = { workspace = true } +diesel = { workspace = true } +diesel_migrations = { workspace = true } miden-node-proto = { workspace = true } miden-node-proto-build = { features = ["internal"], workspace = true } miden-node-utils = { features = ["testing"], workspace = true } diff --git a/crates/validator/build.rs b/crates/validator/build.rs new file mode 100644 index 0000000000..b9f947e177 --- /dev/null +++ b/crates/validator/build.rs @@ -0,0 +1,9 @@ +// This build.rs is required to trigger the `diesel_migrations::embed_migrations!` proc-macro in +// `validator/src/db/migrations.rs` to include the latest version of the migrations into the binary, see . +fn main() { + println!("cargo:rerun-if-changed=./src/db/migrations"); + // If we do one re-write, the default rules are disabled, + // hence we need to trigger explicitly on `Cargo.toml`. + // + println!("cargo:rerun-if-changed=Cargo.toml"); +} diff --git a/crates/validator/diesel.toml b/crates/validator/diesel.toml new file mode 100644 index 0000000000..bdce9175fa --- /dev/null +++ b/crates/validator/diesel.toml @@ -0,0 +1,5 @@ +# For documentation on how to configure this file, +# see https://diesel.rs/guides/configuring-diesel-cli + +[print_schema] +file = "src/db/schema.rs" From 189ad4b5efc922ecfe534a1c18fcb1248cf7d2eb Mon Sep 17 00:00:00 2001 From: sergerad Date: Wed, 4 Feb 2026 10:25:38 +1300 Subject: [PATCH 26/72] Revert "Misc cleanup" This reverts commit 1ec03ef5688c362d41728c2b779900843d77cebc. --- crates/validator/src/db/database.rs | 20 +++++++++++--------- crates/validator/src/db/kv_conv.rs | 9 ++------- crates/validator/src/db/mod.rs | 5 +---- 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/crates/validator/src/db/database.rs b/crates/validator/src/db/database.rs index 83588c4740..671758b0e7 100644 --- a/crates/validator/src/db/database.rs +++ b/crates/validator/src/db/database.rs @@ -1,20 +1,22 @@ use std::path::Path; use miden_protocol::transaction::{TransactionHeader, TransactionId}; +use miden_tx::utils::DeserializationError; -use crate::db::DatabaseError; use crate::db::kv_conv::{ToKey, ToValue}; -/// Validator database for storing validated transaction data. -/// -/// Transaction data stored in this database is intended to allow for the validation of proposed -/// blocks. All transactions in proposed blocks are expected to have been validated and stored in -/// this database before the proposed block has been received. +#[derive(thiserror::Error, Debug)] +pub enum DatabaseError { + #[error("underlying database error")] + ExecutionError(#[from] fjall::Error), + #[error("failed to deserialize value bytes from the database")] + DeserializationError(#[from] DeserializationError), + #[error("validated transactions not found {0:?}")] + TransactionsNotFound(Vec), +} + pub struct Database { - /// The underlying key-value database. _db: fjall::Database, - - /// The "namespace" that all transaction data is stored in. keyspace: fjall::Keyspace, } diff --git a/crates/validator/src/db/kv_conv.rs b/crates/validator/src/db/kv_conv.rs index 8b79abf447..aae5154562 100644 --- a/crates/validator/src/db/kv_conv.rs +++ b/crates/validator/src/db/kv_conv.rs @@ -2,16 +2,13 @@ use miden_protocol::transaction::{TransactionHeader, TransactionId}; use miden_protocol::utils::Deserializable; use miden_tx::utils::{DeserializationError, Serializable}; -/// Trait for converting types into keys for the key-value store. pub trait ToKey { fn to_key(&self) -> fjall::UserKey; } -/// Trait for converting types from and into values for the key-value store. pub trait ToValue: Sized { - type Error; fn to_value(&self) -> fjall::UserValue; - fn from_value(slice: fjall::Slice) -> Result; + fn from_value(slice: fjall::Slice) -> Result; } impl ToKey for TransactionId { @@ -21,13 +18,11 @@ impl ToKey for TransactionId { } impl ToValue for TransactionHeader { - type Error = DeserializationError; - fn to_value(&self) -> fjall::UserValue { fjall::UserValue::new(&self.to_bytes()) } - fn from_value(slice: fjall::Slice) -> Result { + fn from_value(slice: fjall::Slice) -> Result { Self::read_from_bytes(&slice) } } diff --git a/crates/validator/src/db/mod.rs b/crates/validator/src/db/mod.rs index d3672e6522..f7aa2bd5dd 100644 --- a/crates/validator/src/db/mod.rs +++ b/crates/validator/src/db/mod.rs @@ -1,7 +1,4 @@ mod database; -pub use database::Database; - -mod errors; -pub use errors::DatabaseError; +pub use database::{Database, DatabaseError}; mod kv_conv; From 62d0cd221d6a93b56951bd8b4016ab5866f1a2ad Mon Sep 17 00:00:00 2001 From: sergerad Date: Wed, 4 Feb 2026 10:25:39 +1300 Subject: [PATCH 27/72] Revert "Move to kv store" This reverts commit d3c87703de612346e29788fd3727c521613748aa. --- Cargo.lock | 217 +++--------------- Cargo.toml | 2 +- crates/validator/Cargo.toml | 2 +- crates/validator/src/block_validation/mod.rs | 68 +++++- crates/validator/src/db/database.rs | 64 ------ crates/validator/src/db/kv_conv.rs | 28 --- crates/validator/src/db/migrations.rs | 25 ++ .../migrations/2025062000000_setup/down.sql | 0 .../db/migrations/2025062000000_setup/up.sql | 5 + crates/validator/src/db/mod.rs | 74 +++++- crates/validator/src/db/models.rs | 30 +++ crates/validator/src/db/schema.rs | 6 + crates/validator/src/server/mod.rs | 17 +- rust-toolchain.toml | 2 +- 14 files changed, 243 insertions(+), 297 deletions(-) delete mode 100644 crates/validator/src/db/database.rs delete mode 100644 crates/validator/src/db/kv_conv.rs create mode 100644 crates/validator/src/db/migrations.rs create mode 100644 crates/validator/src/db/migrations/2025062000000_setup/down.sql create mode 100644 crates/validator/src/db/migrations/2025062000000_setup/up.sql create mode 100644 crates/validator/src/db/models.rs create mode 100644 crates/validator/src/db/schema.rs diff --git a/Cargo.lock b/Cargo.lock index b2c7acda64..3c9d3ec2a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,6 +27,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "version_check", +] + [[package]] name = "ahash" version = "0.8.12" @@ -458,24 +469,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" -[[package]] -name = "byteorder-lite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" - [[package]] name = "bytes" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" -[[package]] -name = "byteview" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dda4398f387cc6395a3e93b3867cd9abda914c97a0b344d1eefb2e5c51785fca" - [[package]] name = "bzip2-sys" version = "0.1.13+1.0.8" @@ -727,12 +726,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" -[[package]] -name = "compare" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea0095f6103c2a8b44acd6fd15960c801dafebf02e21940360833e0673f48ba7" - [[package]] name = "const-oid" version = "0.9.6" @@ -853,16 +846,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "crossbeam-skiplist" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df29de440c58ca2cc6e587ec3d22347551a32435fbde9d2bff64e78a9ffa151b" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -1004,20 +987,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "dashmap" -version = "6.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" -dependencies = [ - "cfg-if", - "crossbeam-utils", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core", -] - [[package]] name = "deadpool" version = "0.12.3" @@ -1329,18 +1298,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "enum_dispatch" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" -dependencies = [ - "once_cell", - "proc-macro2", - "quote", - "syn 2.0.114", -] - [[package]] name = "env_filter" version = "0.1.4" @@ -1377,7 +1334,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -1430,23 +1387,6 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" -[[package]] -name = "fjall" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f69637c02d38ad1b0f003101d0195a60368130aa17d9ef78b1557d265a22093" -dependencies = [ - "byteorder-lite", - "byteview", - "dashmap", - "flume 0.12.0", - "log", - "lsm-tree", - "lz4_flex", - "tempfile", - "xxhash-rust", -] - [[package]] name = "flate2" version = "1.1.8" @@ -1470,15 +1410,6 @@ dependencies = [ "spin", ] -[[package]] -name = "flume" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" -dependencies = [ - "spin", -] - [[package]] name = "fnv" version = "1.0.7" @@ -1748,12 +1679,9 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash 0.7.8", +] [[package]] name = "hashbrown" @@ -2160,15 +2088,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "interval-heap" -version = "0.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11274e5e8e89b8607cfedc2910b6626e998779b48a019151c7604d0adcb86ac6" -dependencies = [ - "compare", -] - [[package]] name = "ipnet" version = "2.11.0" @@ -2193,7 +2112,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi 0.5.2", "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2514,28 +2433,6 @@ version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" -[[package]] -name = "lsm-tree" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b875f1dfe14f557f805b167fb9b0fc54c5560c7a4bd6ae02535b2846f276a8cb" -dependencies = [ - "byteorder-lite", - "byteview", - "crossbeam-skiplist", - "enum_dispatch", - "interval-heap", - "log", - "lz4_flex", - "quick_cache", - "rustc-hash", - "self_cell", - "sfa", - "tempfile", - "varint-rs", - "xxhash-rust", -] - [[package]] name = "lz4-sys" version = "1.11.1+lz4-1.10.0" @@ -2546,15 +2443,6 @@ dependencies = [ "libc", ] -[[package]] -name = "lz4_flex" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a" -dependencies = [ - "twox-hash", -] - [[package]] name = "matchers" version = "0.2.0" @@ -2718,7 +2606,7 @@ dependencies = [ "chacha20poly1305", "curve25519-dalek", "ed25519-dalek", - "flume 0.11.1", + "flume", "glob", "hashbrown 0.16.1", "hkdf", @@ -3129,9 +3017,9 @@ dependencies = [ "deadpool-diesel", "diesel", "diesel_migrations", - "fjall", "miden-node-proto", "miden-node-proto-build", + "miden-node-store", "miden-node-utils", "miden-protocol", "miden-tx", @@ -3593,7 +3481,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3980,7 +3868,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ef622051fbb2cb98a524df3a8112f02d0919ccda600a44d705ec550f1a28fe2" dependencies = [ - "ahash", + "ahash 0.8.12", "async-trait", "blake2", "bytes", @@ -4016,7 +3904,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76f63d3f67d99c95a1f85623fc43242fd644dd12ccbaa18c38a54e1580c6846a" dependencies = [ - "ahash", + "ahash 0.8.12", "async-trait", "brotli", "bytes", @@ -4106,7 +3994,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b93c897e8cc04ff0d077ee2a655142910618222aeefc83f7f99f5b9fc59ccb13" dependencies = [ - "ahash", + "ahash 0.8.12", ] [[package]] @@ -4138,7 +4026,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba89e4400cb978f0d7be1c14bd7ab4168c8e2c00d97ff19f964fc0048780237c" dependencies = [ "arrayvec", - "hashbrown 0.16.1", + "hashbrown 0.12.3", "parking_lot", "rand 0.8.5", ] @@ -4480,7 +4368,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac6c3320f9abac597dcbc668774ef006702672474aad53c6d596b62e487b40b1" dependencies = [ "heck 0.5.0", - "itertools 0.14.0", + "itertools 0.10.5", "log", "multimap", "once_cell", @@ -4502,7 +4390,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" dependencies = [ "anyhow", - "itertools 0.14.0", + "itertools 0.10.5", "proc-macro2", "quote", "syn 2.0.114", @@ -4608,16 +4496,6 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" -[[package]] -name = "quick_cache" -version = "0.6.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ada44a88ef953a3294f6eb55d2007ba44646015e18613d2f213016379203ef3" -dependencies = [ - "equivalent", - "hashbrown 0.16.1", -] - [[package]] name = "quote" version = "1.0.44" @@ -4965,7 +4843,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -4978,7 +4856,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -5147,12 +5025,6 @@ dependencies = [ "libc", ] -[[package]] -name = "self_cell" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" - [[package]] name = "semver" version = "0.9.0" @@ -5311,17 +5183,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "sfa" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1296838937cab56cd6c4eeeb8718ec777383700c33f060e2869867bd01d1175" -dependencies = [ - "byteorder-lite", - "log", - "xxhash-rust", -] - [[package]] name = "sfv" version = "0.10.4" @@ -5627,7 +5488,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix 1.1.3", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -5636,7 +5497,7 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8c27177b12a6399ffc08b98f76f7c9a1f4fe9fc967c784c5a071fa8d93cf7e1" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -6307,12 +6168,6 @@ dependencies = [ "toml 0.9.11+spec-1.1.0", ] -[[package]] -name = "twox-hash" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" - [[package]] name = "typenum" version = "1.19.0" @@ -6428,12 +6283,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" -[[package]] -name = "varint-rs" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f54a172d0620933a27a4360d3db3e2ae0dd6cceae9730751a036bbf182c4b23" - [[package]] name = "vcpkg" version = "0.2.15" @@ -6612,7 +6461,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -7066,12 +6915,6 @@ dependencies = [ "rand_core 0.6.4", ] -[[package]] -name = "xxhash-rust" -version = "0.8.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" - [[package]] name = "yaml-rust" version = "0.4.5" diff --git a/Cargo.toml b/Cargo.toml index b0d79e411c..abe16e1fee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ homepage = "https://miden.xyz" license = "MIT" readme = "README.md" repository = "https://github.com/0xMiden/miden-node" -rust-version = "1.91" +rust-version = "1.90" version = "0.14.0" # Optimize the cryptography for faster tests involving account creation. diff --git a/crates/validator/Cargo.toml b/crates/validator/Cargo.toml index ca6378384c..26a76a2b3b 100644 --- a/crates/validator/Cargo.toml +++ b/crates/validator/Cargo.toml @@ -17,13 +17,13 @@ workspace = true [features] [dependencies] -fjall = { version = "3.0" } anyhow = { workspace = true } deadpool-diesel = { workspace = true } diesel = { workspace = true } diesel_migrations = { workspace = true } miden-node-proto = { workspace = true } miden-node-proto-build = { features = ["internal"], workspace = true } +miden-node-store = { workspace = true } miden-node-utils = { features = ["testing"], workspace = true } miden-protocol = { workspace = true } miden-tx = { workspace = true } diff --git a/crates/validator/src/block_validation/mod.rs b/crates/validator/src/block_validation/mod.rs index c333dc7515..ede74b5d52 100644 --- a/crates/validator/src/block_validation/mod.rs +++ b/crates/validator/src/block_validation/mod.rs @@ -1,20 +1,36 @@ -use miden_protocol::block::{BlockSigner, ProposedBlock}; +use std::collections::HashMap; + +use miden_node_store::{DatabaseError, Db}; +use miden_protocol::block::{BlockNumber, BlockSigner, ProposedBlock}; use miden_protocol::crypto::dsa::ecdsa_k256_keccak::Signature; use miden_protocol::errors::ProposedBlockError; -use miden_protocol::transaction::TransactionHeader; +use miden_protocol::transaction::{TransactionHeader, TransactionId}; use tracing::info_span; -use crate::db::{Database, DatabaseError}; +use crate::db::select_validated_transactions; // BLOCK VALIDATION ERROR // ================================================================================================ #[derive(thiserror::Error, Debug)] pub enum BlockValidationError { + #[error("found unvalidated transactions {0:?}")] + UnvalidatedTransactions(Vec), + #[error("transaction {0} in block {1} has not been validated")] + TransactionNotValidated(TransactionId, BlockNumber), + #[error( + "the proposed transaction {proposed_tx} does not match the validated transaction {validated_tx}" + )] + TransactionMismatch { + proposed_tx: TransactionId, + validated_tx: TransactionId, + }, #[error("failed to build block")] BlockBuildingFailed(#[from] ProposedBlockError), #[error("failed to select transactions")] DatabaseError(#[from] DatabaseError), + #[error("internal error: {0}")] + Other(String), } // BLOCK VALIDATION @@ -27,13 +43,53 @@ pub enum BlockValidationError { pub async fn validate_block( proposed_block: ProposedBlock, signer: &S, - db: &Database, + db: &Db, ) -> Result { + // Create a map of transactions from the proposed block. + let proposed_transactions = proposed_block + .transactions() + .map(|header| (header.id(), header.clone())) + .collect::>(); + // Retrieve all validated transactions pertaining to the proposed block. let proposed_tx_ids = proposed_block.transactions().map(TransactionHeader::id).collect::>(); - // TODO(currentpr): If we don't need to retrieve the data at all we can change this. - let _validated_transactions = db.get(&proposed_tx_ids)?; + let query_tx_ids = proposed_tx_ids.clone(); + let validated_transactions = db + .transact("select_transactions", move |conn| { + select_validated_transactions(conn, &query_tx_ids) + }) + .await?; + + // All proposed transactions must have been validated. + if proposed_tx_ids.len() > validated_transactions.len() { + let mut proposed_tx_ids = proposed_tx_ids; + // Return proposed transactions that have not been validated. + proposed_tx_ids + .retain(|proposed_tx_id| !validated_transactions.contains_key(proposed_tx_id)); + return Err(BlockValidationError::UnvalidatedTransactions(proposed_tx_ids)); + } + + // Check that every transaction from the proposed block has been validated. + for proposed_tx_id in proposed_tx_ids { + let Some(validated_tx) = validated_transactions.get(&proposed_tx_id) else { + return Err(BlockValidationError::TransactionNotValidated( + proposed_tx_id, + proposed_block.block_num(), + )); + }; + // Check that the proposed and validated transactions are equal. + let proposed_tx = + proposed_transactions.get(&proposed_tx_id).ok_or(BlockValidationError::Other( + "proposed transactions mapped incorrectly from proposed block".into(), + ))?; + if validated_tx != proposed_tx { + return Err(BlockValidationError::TransactionMismatch { + proposed_tx: proposed_tx.id(), + validated_tx: validated_tx.id(), + }); + } + } // Build the block header. let (header, _) = proposed_block.into_header_and_body()?; diff --git a/crates/validator/src/db/database.rs b/crates/validator/src/db/database.rs deleted file mode 100644 index 671758b0e7..0000000000 --- a/crates/validator/src/db/database.rs +++ /dev/null @@ -1,64 +0,0 @@ -use std::path::Path; - -use miden_protocol::transaction::{TransactionHeader, TransactionId}; -use miden_tx::utils::DeserializationError; - -use crate::db::kv_conv::{ToKey, ToValue}; - -#[derive(thiserror::Error, Debug)] -pub enum DatabaseError { - #[error("underlying database error")] - ExecutionError(#[from] fjall::Error), - #[error("failed to deserialize value bytes from the database")] - DeserializationError(#[from] DeserializationError), - #[error("validated transactions not found {0:?}")] - TransactionsNotFound(Vec), -} - -pub struct Database { - _db: fjall::Database, - keyspace: fjall::Keyspace, -} - -impl Database { - /// Creates a new key-value database in the specified directory. - pub fn new(dir: &Path) -> Result { - let db = fjall::Database::builder(dir.join("validator")).open()?; - let keyspace = db.keyspace("transactions", fjall::KeyspaceCreateOptions::default)?; - - Ok(Self { _db: db, keyspace }) - } - - /// Stores a transaction header into the database. - pub fn put(&self, validated_tx_header: &TransactionHeader) -> Result<(), DatabaseError> { - self.keyspace - .insert(validated_tx_header.id().to_key(), validated_tx_header.to_value())?; - Ok(()) - } - - /// Retrieves a transaction headers corresponding to the transaction ids from the database. - /// - /// # Errors - /// - /// Returns an error if: - /// - Any requested transactions are not found. - /// - Retrieved transactions cannot be deserialized. - pub fn get(&self, ids: &[TransactionId]) -> Result, DatabaseError> { - let mut txs = Vec::with_capacity(ids.len()); - let mut txs_not_found = Vec::new(); - for id in ids { - let value = self.keyspace.get(id.as_bytes())?; - if let Some(value) = value { - let tx_header = ::from_value(value)?; - txs.push(tx_header); - } else { - txs_not_found.push(*id); - } - } - if txs_not_found.is_empty() { - Ok(txs) - } else { - Err(DatabaseError::TransactionsNotFound(txs_not_found)) - } - } -} diff --git a/crates/validator/src/db/kv_conv.rs b/crates/validator/src/db/kv_conv.rs deleted file mode 100644 index aae5154562..0000000000 --- a/crates/validator/src/db/kv_conv.rs +++ /dev/null @@ -1,28 +0,0 @@ -use miden_protocol::transaction::{TransactionHeader, TransactionId}; -use miden_protocol::utils::Deserializable; -use miden_tx::utils::{DeserializationError, Serializable}; - -pub trait ToKey { - fn to_key(&self) -> fjall::UserKey; -} - -pub trait ToValue: Sized { - fn to_value(&self) -> fjall::UserValue; - fn from_value(slice: fjall::Slice) -> Result; -} - -impl ToKey for TransactionId { - fn to_key(&self) -> fjall::UserKey { - fjall::UserKey::new(&self.as_bytes()) - } -} - -impl ToValue for TransactionHeader { - fn to_value(&self) -> fjall::UserValue { - fjall::UserValue::new(&self.to_bytes()) - } - - fn from_value(slice: fjall::Slice) -> Result { - Self::read_from_bytes(&slice) - } -} diff --git a/crates/validator/src/db/migrations.rs b/crates/validator/src/db/migrations.rs new file mode 100644 index 0000000000..6896082bed --- /dev/null +++ b/crates/validator/src/db/migrations.rs @@ -0,0 +1,25 @@ +use diesel::SqliteConnection; +use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations}; +use miden_node_store::DatabaseError; +use tracing::instrument; + +use crate::COMPONENT; + +// The rebuild is automatically triggered by `build.rs` as described in +// . +pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("src/db/migrations"); + +#[instrument(level = "debug", target = COMPONENT, skip_all, err)] +pub fn apply_migrations(conn: &mut SqliteConnection) -> std::result::Result<(), DatabaseError> { + let migrations = conn.pending_migrations(MIGRATIONS).expect("In memory migrations never fail"); + tracing::info!(target = COMPONENT, "Applying {} migration(s)", migrations.len()); + + let Err(e) = conn.run_pending_migrations(MIGRATIONS) else { + return Ok(()); + }; + tracing::warn!(target = COMPONENT, "Failed to apply migration: {e:?}"); + conn.revert_last_migration(MIGRATIONS) + .expect("Duality is maintained by the developer"); + + Ok(()) +} diff --git a/crates/validator/src/db/migrations/2025062000000_setup/down.sql b/crates/validator/src/db/migrations/2025062000000_setup/down.sql new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/validator/src/db/migrations/2025062000000_setup/up.sql b/crates/validator/src/db/migrations/2025062000000_setup/up.sql new file mode 100644 index 0000000000..a942c27d68 --- /dev/null +++ b/crates/validator/src/db/migrations/2025062000000_setup/up.sql @@ -0,0 +1,5 @@ +CREATE TABLE transactions ( + id BLOB NOT NULL, + data BLOB NOT NULL, + PRIMARY KEY (id) +) WITHOUT ROWID; diff --git a/crates/validator/src/db/mod.rs b/crates/validator/src/db/mod.rs index f7aa2bd5dd..c987216120 100644 --- a/crates/validator/src/db/mod.rs +++ b/crates/validator/src/db/mod.rs @@ -1,4 +1,72 @@ -mod database; -pub use database::{Database, DatabaseError}; +mod migrations; +mod models; +mod schema; -mod kv_conv; +use std::collections::HashMap; +use std::path::PathBuf; + +use diesel::SqliteConnection; +use diesel::prelude::*; +use miden_node_store::{ConnectionManager, DatabaseError, DatabaseSetupError}; +use miden_protocol::transaction::{TransactionHeader, TransactionId}; +use miden_protocol::utils::{Deserializable, Serializable}; +use tracing::instrument; + +use crate::COMPONENT; +use crate::db::migrations::apply_migrations; +use crate::db::models::{TransactionSummaryRowInsert, TransactionSummaryRowSelect}; + +/// Open a connection to the DB and apply any pending migrations. +#[instrument(target = COMPONENT, skip_all)] +pub async fn load(database_filepath: PathBuf) -> Result { + let manager = ConnectionManager::new(database_filepath.to_str().unwrap()); + let pool = deadpool_diesel::Pool::builder(manager).max_size(16).build()?; + + tracing::info!( + target: COMPONENT, + sqlite= %database_filepath.display(), + "Connected to the database" + ); + + let db = miden_node_store::Db::new(pool); + db.query("migrations", apply_migrations).await?; + Ok(db) +} + +pub(crate) fn insert_transaction( + conn: &mut SqliteConnection, + header: &TransactionHeader, +) -> Result { + let row = TransactionSummaryRowInsert::new(header); + let count = diesel::insert_into(schema::transactions::table).values(row).execute(conn)?; + Ok(count) +} + +pub(crate) fn select_validated_transactions( + conn: &mut SqliteConnection, + tx_ids: &[TransactionId], +) -> Result, DatabaseError> { + if tx_ids.is_empty() { + return Ok(HashMap::new()); + } + + // Convert TransactionIds to bytes for query. + let tx_id_bytes: Vec> = tx_ids.iter().map(TransactionId::to_bytes).collect(); + + // Query the database for matching transactions. + let raw_transactions = schema::transactions::table + .filter(schema::transactions::transaction_id.eq_any(tx_id_bytes)) + .order(schema::transactions::transaction_id.asc()) + .load::(conn) + .map_err(DatabaseError::from)?; + + // Deserialize the transaction blobs. + let mut transactions: HashMap = HashMap::new(); + for raw_tx in raw_transactions { + let tx_id = TransactionId::read_from_bytes(&raw_tx.transaction_id)?; + let tx_header = TransactionHeader::read_from_bytes(&raw_tx.data)?; + transactions.insert(tx_id, tx_header); + } + + Ok(transactions) +} diff --git a/crates/validator/src/db/models.rs b/crates/validator/src/db/models.rs new file mode 100644 index 0000000000..59f4379ac9 --- /dev/null +++ b/crates/validator/src/db/models.rs @@ -0,0 +1,30 @@ +use diesel::prelude::*; +use miden_protocol::transaction::TransactionHeader; +use miden_tx::utils::Serializable; + +use crate::db::schema; + +#[derive(Debug, Clone, PartialEq, Insertable)] +#[diesel(table_name = schema::transactions)] +#[diesel(check_for_backend(diesel::sqlite::Sqlite))] +pub struct TransactionSummaryRowInsert { + pub transaction_id: Vec, + pub data: Vec, +} + +impl TransactionSummaryRowInsert { + pub fn new(transaction_header: &TransactionHeader) -> Self { + Self { + transaction_id: transaction_header.id().to_bytes(), + data: transaction_header.to_bytes(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Queryable, Selectable)] +#[diesel(table_name = schema::transactions)] +#[diesel(check_for_backend(diesel::sqlite::Sqlite))] +pub struct TransactionSummaryRowSelect { + pub transaction_id: Vec, + pub data: Vec, +} diff --git a/crates/validator/src/db/schema.rs b/crates/validator/src/db/schema.rs new file mode 100644 index 0000000000..198a559103 --- /dev/null +++ b/crates/validator/src/db/schema.rs @@ -0,0 +1,6 @@ +diesel::table! { + transactions (transaction_id) { + transaction_id -> Binary, + data -> Binary, + } +} diff --git a/crates/validator/src/server/mod.rs b/crates/validator/src/server/mod.rs index e13cfcba24..e99fbf593f 100644 --- a/crates/validator/src/server/mod.rs +++ b/crates/validator/src/server/mod.rs @@ -6,6 +6,7 @@ use anyhow::Context; use miden_node_proto::generated::validator::api_server; use miden_node_proto::generated::{self as proto}; use miden_node_proto_build::validator_api_descriptor; +use miden_node_store::Db; use miden_node_utils::ErrorReport; use miden_node_utils::panic::catch_panic_layer_fn; use miden_node_utils::tracing::OpenTelemetrySpanExt; @@ -22,7 +23,7 @@ use tracing::info_span; use crate::COMPONENT; use crate::block_validation::validate_block; -use crate::db::Database; +use crate::db::{insert_transaction, load}; use crate::tx_validation::validate_transaction; // VALIDATOR @@ -55,7 +56,9 @@ impl Validator { tracing::info!(target: COMPONENT, endpoint=?self.address, "Initializing server"); // Initialize database connection. - let db = Database::new(&self.data_directory)?; + let db = load(self.data_directory.join("validator.sqlite3")) + .await + .context("failed to initialize validator database")?; let listener = TcpListener::bind(self.address) .await @@ -97,11 +100,11 @@ impl Validator { /// Implements the gRPC API for the validator. struct ValidatorServer { signer: S, - db: Database, + db: Db, } impl ValidatorServer { - fn new(signer: S, db: Database) -> Self { + fn new(signer: S, db: Db) -> Self { Self { signer, db } } } @@ -148,8 +151,10 @@ impl api_server::Api for ValidatorServer // Store the validated transaction. self.db - .put(&validated_tx_header) - .map_err(|err| Status::internal(err.as_report_context("Failed insert")))?; + .transact("insert_transaction", move |conn| { + insert_transaction(conn, &validated_tx_header) + }) + .await?; Ok(tonic::Response::new(())) } diff --git a/rust-toolchain.toml b/rust-toolchain.toml index d9a424cef9..6744e56e15 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,5 +1,5 @@ [toolchain] -channel = "1.91" +channel = "1.90" components = ["clippy", "rust-src", "rustfmt"] profile = "minimal" targets = ["wasm32-unknown-unknown"] From 74aa0749ed76cdefe5b7d16251b155d989afc2b7 Mon Sep 17 00:00:00 2001 From: sergerad Date: Wed, 4 Feb 2026 11:06:58 +1300 Subject: [PATCH 28/72] Add find_unvalidated_ fn --- crates/validator/src/block_validation/mod.rs | 61 +++----------------- crates/validator/src/db/mod.rs | 39 ++++++++++++- 2 files changed, 45 insertions(+), 55 deletions(-) diff --git a/crates/validator/src/block_validation/mod.rs b/crates/validator/src/block_validation/mod.rs index ede74b5d52..137e753000 100644 --- a/crates/validator/src/block_validation/mod.rs +++ b/crates/validator/src/block_validation/mod.rs @@ -1,13 +1,11 @@ -use std::collections::HashMap; - use miden_node_store::{DatabaseError, Db}; -use miden_protocol::block::{BlockNumber, BlockSigner, ProposedBlock}; +use miden_protocol::block::{BlockSigner, ProposedBlock}; use miden_protocol::crypto::dsa::ecdsa_k256_keccak::Signature; use miden_protocol::errors::ProposedBlockError; use miden_protocol::transaction::{TransactionHeader, TransactionId}; use tracing::info_span; -use crate::db::select_validated_transactions; +use crate::db::find_unvalidated_transactions; // BLOCK VALIDATION ERROR // ================================================================================================ @@ -16,21 +14,10 @@ use crate::db::select_validated_transactions; pub enum BlockValidationError { #[error("found unvalidated transactions {0:?}")] UnvalidatedTransactions(Vec), - #[error("transaction {0} in block {1} has not been validated")] - TransactionNotValidated(TransactionId, BlockNumber), - #[error( - "the proposed transaction {proposed_tx} does not match the validated transaction {validated_tx}" - )] - TransactionMismatch { - proposed_tx: TransactionId, - validated_tx: TransactionId, - }, #[error("failed to build block")] BlockBuildingFailed(#[from] ProposedBlockError), #[error("failed to select transactions")] DatabaseError(#[from] DatabaseError), - #[error("internal error: {0}")] - Other(String), } // BLOCK VALIDATION @@ -38,57 +25,23 @@ pub enum BlockValidationError { /// Validates a block by checking that all transactions in the proposed block have been processed by /// the validator in the past. -/// -/// Removes the validated transactions from the cache upon success. pub async fn validate_block( proposed_block: ProposedBlock, signer: &S, db: &Db, ) -> Result { - // Create a map of transactions from the proposed block. - let proposed_transactions = proposed_block - .transactions() - .map(|header| (header.id(), header.clone())) - .collect::>(); - // Retrieve all validated transactions pertaining to the proposed block. let proposed_tx_ids = proposed_block.transactions().map(TransactionHeader::id).collect::>(); - let query_tx_ids = proposed_tx_ids.clone(); - let validated_transactions = db - .transact("select_transactions", move |conn| { - select_validated_transactions(conn, &query_tx_ids) + let unvalidated_transactions = db + .transact("find_unvalidated_transactions", move |conn| { + find_unvalidated_transactions(conn, &proposed_tx_ids) }) .await?; // All proposed transactions must have been validated. - if proposed_tx_ids.len() > validated_transactions.len() { - let mut proposed_tx_ids = proposed_tx_ids; - // Return proposed transactions that have not been validated. - proposed_tx_ids - .retain(|proposed_tx_id| !validated_transactions.contains_key(proposed_tx_id)); - return Err(BlockValidationError::UnvalidatedTransactions(proposed_tx_ids)); - } - - // Check that every transaction from the proposed block has been validated. - for proposed_tx_id in proposed_tx_ids { - let Some(validated_tx) = validated_transactions.get(&proposed_tx_id) else { - return Err(BlockValidationError::TransactionNotValidated( - proposed_tx_id, - proposed_block.block_num(), - )); - }; - // Check that the proposed and validated transactions are equal. - let proposed_tx = - proposed_transactions.get(&proposed_tx_id).ok_or(BlockValidationError::Other( - "proposed transactions mapped incorrectly from proposed block".into(), - ))?; - if validated_tx != proposed_tx { - return Err(BlockValidationError::TransactionMismatch { - proposed_tx: proposed_tx.id(), - validated_tx: validated_tx.id(), - }); - } + if !unvalidated_transactions.is_empty() { + return Err(BlockValidationError::UnvalidatedTransactions(unvalidated_transactions)); } // Build the block header. diff --git a/crates/validator/src/db/mod.rs b/crates/validator/src/db/mod.rs index c987216120..ef10f6c888 100644 --- a/crates/validator/src/db/mod.rs +++ b/crates/validator/src/db/mod.rs @@ -2,7 +2,7 @@ mod migrations; mod models; mod schema; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::path::PathBuf; use diesel::SqliteConnection; @@ -33,6 +33,7 @@ pub async fn load(database_filepath: PathBuf) -> Result Result, DatabaseError> { + if tx_ids.is_empty() { + return Ok(Vec::new()); + } + + // Convert TransactionIds to bytes for query. + let tx_id_bytes: Vec> = tx_ids.iter().map(TransactionId::to_bytes).collect(); + + // Query the database for matching transactions ids. + let raw_transactions_ids = schema::transactions::table + .select(schema::transactions::transaction_id) + .filter(schema::transactions::transaction_id.eq_any(tx_id_bytes)) + .order(schema::transactions::transaction_id.asc()) + .load::>(conn) + .map_err(DatabaseError::from)?; + + // Find any requested ids that the database does not contain. + let expected_tx_ids = tx_ids.iter().copied().collect::>(); + let mut unvalidated_tx_ids = Vec::new(); + for raw_tx_id in raw_transactions_ids { + let tx_id = TransactionId::read_from_bytes(&raw_tx_id)?; + if !expected_tx_ids.contains(&tx_id) { + unvalidated_tx_ids.push(tx_id); + } + } + Ok(unvalidated_tx_ids) +} From 2db6985bc1d68710df295c2041693acf038af707 Mon Sep 17 00:00:00 2001 From: sergerad Date: Wed, 4 Feb 2026 11:25:28 +1300 Subject: [PATCH 29/72] Refactor for tx summary --- .../db/migrations/2025062000000_setup/up.sql | 5 +++- crates/validator/src/db/mod.rs | 27 ++++++++++--------- crates/validator/src/db/models.rs | 19 +++++++------ crates/validator/src/db/schema.rs | 7 ++--- crates/validator/src/server/mod.rs | 7 +++-- crates/validator/src/tx_validation/mod.rs | 16 ++++++++--- 6 files changed, 49 insertions(+), 32 deletions(-) diff --git a/crates/validator/src/db/migrations/2025062000000_setup/up.sql b/crates/validator/src/db/migrations/2025062000000_setup/up.sql index a942c27d68..8088676961 100644 --- a/crates/validator/src/db/migrations/2025062000000_setup/up.sql +++ b/crates/validator/src/db/migrations/2025062000000_setup/up.sql @@ -1,5 +1,8 @@ CREATE TABLE transactions ( id BLOB NOT NULL, - data BLOB NOT NULL, + account_id BLOB NOT NULL, + summary BLOB NOT NULL, PRIMARY KEY (id) ) WITHOUT ROWID; + +CREATE INDEX idx_transactions_account_id ON transactions(account_id); diff --git a/crates/validator/src/db/mod.rs b/crates/validator/src/db/mod.rs index ef10f6c888..f367099515 100644 --- a/crates/validator/src/db/mod.rs +++ b/crates/validator/src/db/mod.rs @@ -8,7 +8,7 @@ use std::path::PathBuf; use diesel::SqliteConnection; use diesel::prelude::*; use miden_node_store::{ConnectionManager, DatabaseError, DatabaseSetupError}; -use miden_protocol::transaction::{TransactionHeader, TransactionId}; +use miden_protocol::transaction::{TransactionId, TransactionSummary}; use miden_protocol::utils::{Deserializable, Serializable}; use tracing::instrument; @@ -36,9 +36,10 @@ pub async fn load(database_filepath: PathBuf) -> Result Result { - let row = TransactionSummaryRowInsert::new(header); + let row = TransactionSummaryRowInsert::new(tx_id, summary); let count = diesel::insert_into(schema::transactions::table).values(row).execute(conn)?; Ok(count) } @@ -48,7 +49,7 @@ pub(crate) fn insert_transaction( pub(crate) fn select_validated_transactions( conn: &mut SqliteConnection, tx_ids: &[TransactionId], -) -> Result, DatabaseError> { +) -> Result, DatabaseError> { if tx_ids.is_empty() { return Ok(HashMap::new()); } @@ -58,17 +59,17 @@ pub(crate) fn select_validated_transactions( // Query the database for matching transactions. let raw_transactions = schema::transactions::table - .filter(schema::transactions::transaction_id.eq_any(tx_id_bytes)) - .order(schema::transactions::transaction_id.asc()) + .filter(schema::transactions::id.eq_any(tx_id_bytes)) + .order(schema::transactions::id.asc()) .load::(conn) .map_err(DatabaseError::from)?; // Deserialize the transaction blobs. - let mut transactions: HashMap = HashMap::new(); + let mut transactions: HashMap = HashMap::new(); for raw_tx in raw_transactions { - let tx_id = TransactionId::read_from_bytes(&raw_tx.transaction_id)?; - let tx_header = TransactionHeader::read_from_bytes(&raw_tx.data)?; - transactions.insert(tx_id, tx_header); + let id = TransactionId::read_from_bytes(&raw_tx.id)?; + let summary = TransactionSummary::read_from_bytes(&raw_tx.summary)?; + transactions.insert(id, summary); } Ok(transactions) @@ -90,9 +91,9 @@ pub(crate) fn find_unvalidated_transactions( // Query the database for matching transactions ids. let raw_transactions_ids = schema::transactions::table - .select(schema::transactions::transaction_id) - .filter(schema::transactions::transaction_id.eq_any(tx_id_bytes)) - .order(schema::transactions::transaction_id.asc()) + .select(schema::transactions::id) + .filter(schema::transactions::id.eq_any(tx_id_bytes)) + .order(schema::transactions::id.asc()) .load::>(conn) .map_err(DatabaseError::from)?; diff --git a/crates/validator/src/db/models.rs b/crates/validator/src/db/models.rs index 59f4379ac9..e7ecafdb39 100644 --- a/crates/validator/src/db/models.rs +++ b/crates/validator/src/db/models.rs @@ -1,5 +1,5 @@ use diesel::prelude::*; -use miden_protocol::transaction::TransactionHeader; +use miden_protocol::transaction::{TransactionId, TransactionSummary}; use miden_tx::utils::Serializable; use crate::db::schema; @@ -8,15 +8,17 @@ use crate::db::schema; #[diesel(table_name = schema::transactions)] #[diesel(check_for_backend(diesel::sqlite::Sqlite))] pub struct TransactionSummaryRowInsert { - pub transaction_id: Vec, - pub data: Vec, + pub id: Vec, + pub account_id: Vec, + pub summary: Vec, } impl TransactionSummaryRowInsert { - pub fn new(transaction_header: &TransactionHeader) -> Self { + pub fn new(id: &TransactionId, summary: &TransactionSummary) -> Self { Self { - transaction_id: transaction_header.id().to_bytes(), - data: transaction_header.to_bytes(), + id: id.to_bytes(), + account_id: summary.account_delta().id().to_bytes(), + summary: summary.to_bytes(), } } } @@ -25,6 +27,7 @@ impl TransactionSummaryRowInsert { #[diesel(table_name = schema::transactions)] #[diesel(check_for_backend(diesel::sqlite::Sqlite))] pub struct TransactionSummaryRowSelect { - pub transaction_id: Vec, - pub data: Vec, + pub id: Vec, + pub account_id: Vec, + pub summary: Vec, } diff --git a/crates/validator/src/db/schema.rs b/crates/validator/src/db/schema.rs index 198a559103..9742403abb 100644 --- a/crates/validator/src/db/schema.rs +++ b/crates/validator/src/db/schema.rs @@ -1,6 +1,7 @@ diesel::table! { - transactions (transaction_id) { - transaction_id -> Binary, - data -> Binary, + transactions (id, account_id, summary) { + id -> Binary, + account_id -> Binary, + summary -> Binary, } } diff --git a/crates/validator/src/server/mod.rs b/crates/validator/src/server/mod.rs index e99fbf593f..44477ecbca 100644 --- a/crates/validator/src/server/mod.rs +++ b/crates/validator/src/server/mod.rs @@ -145,15 +145,14 @@ impl api_server::Api for ValidatorServer tracing::Span::current().set_attribute("transaction.id", tx.id()); // Validate the transaction. - let validated_tx_header = validate_transaction(tx, inputs).await.map_err(|err| { + let id = tx.id(); + let tx_summary = validate_transaction(tx, inputs).await.map_err(|err| { Status::invalid_argument(err.as_report_context("Invalid transaction")) })?; // Store the validated transaction. self.db - .transact("insert_transaction", move |conn| { - insert_transaction(conn, &validated_tx_header) - }) + .transact("insert_transaction", move |conn| insert_transaction(conn, &id, &tx_summary)) .await?; Ok(tonic::Response::new(())) } diff --git a/crates/validator/src/tx_validation/mod.rs b/crates/validator/src/tx_validation/mod.rs index 20d610acaa..c893934a53 100644 --- a/crates/validator/src/tx_validation/mod.rs +++ b/crates/validator/src/tx_validation/mod.rs @@ -2,7 +2,12 @@ mod data_store; pub use data_store::TransactionInputsDataStore; use miden_protocol::MIN_PROOF_SECURITY_LEVEL; -use miden_protocol::transaction::{ProvenTransaction, TransactionHeader, TransactionInputs}; +use miden_protocol::transaction::{ + ProvenTransaction, + TransactionHeader, + TransactionInputs, + TransactionSummary, +}; use miden_tx::auth::UnreachableAuth; use miden_tx::{TransactionExecutor, TransactionExecutorError, TransactionVerifier}; use tracing::{Instrument, info_span}; @@ -33,7 +38,7 @@ pub enum TransactionValidationError { pub async fn validate_transaction( proven_tx: ProvenTransaction, tx_inputs: TransactionInputs, -) -> Result { +) -> Result { // First, verify the transaction proof info_span!("verify").in_scope(|| { let tx_verifier = TransactionVerifier::new(MIN_PROOF_SECURITY_LEVEL); @@ -56,7 +61,12 @@ pub async fn validate_transaction( let executed_tx_header: TransactionHeader = (&executed_tx).into(); let proven_tx_header: TransactionHeader = (&proven_tx).into(); if executed_tx_header == proven_tx_header { - Ok(executed_tx_header) + let account_delta = todo!(); + let input_notes = todo!(); + let output_notes = todo!(); + let salt = todo!(); + let summary = TransactionSummary::new(account_delta, input_notes, output_notes, salt); + Ok(summary) } else { Err(TransactionValidationError::Mismatch { proven_tx_header: proven_tx_header.into(), From 41e27b5d764a88face94381b73a04c3778ed4f4e Mon Sep 17 00:00:00 2001 From: sergerad Date: Wed, 4 Feb 2026 11:50:15 +1300 Subject: [PATCH 30/72] Impl todo for tx summary --- crates/validator/src/tx_validation/mod.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/validator/src/tx_validation/mod.rs b/crates/validator/src/tx_validation/mod.rs index c893934a53..6e1cad49dc 100644 --- a/crates/validator/src/tx_validation/mod.rs +++ b/crates/validator/src/tx_validation/mod.rs @@ -1,13 +1,13 @@ mod data_store; pub use data_store::TransactionInputsDataStore; -use miden_protocol::MIN_PROOF_SECURITY_LEVEL; use miden_protocol::transaction::{ ProvenTransaction, TransactionHeader, TransactionInputs, TransactionSummary, }; +use miden_protocol::{MIN_PROOF_SECURITY_LEVEL, Word}; use miden_tx::auth::UnreachableAuth; use miden_tx::{TransactionExecutor, TransactionExecutorError, TransactionVerifier}; use tracing::{Instrument, info_span}; @@ -61,10 +61,10 @@ pub async fn validate_transaction( let executed_tx_header: TransactionHeader = (&executed_tx).into(); let proven_tx_header: TransactionHeader = (&proven_tx).into(); if executed_tx_header == proven_tx_header { - let account_delta = todo!(); - let input_notes = todo!(); - let output_notes = todo!(); - let salt = todo!(); + let account_delta = executed_tx.account_delta().clone(); + let input_notes = executed_tx.input_notes().clone(); + let output_notes = executed_tx.output_notes().clone(); + let salt = Word::empty(); let summary = TransactionSummary::new(account_delta, input_notes, output_notes, salt); Ok(summary) } else { From 06bb1514ee304c11f0844db4b5f3964a26cc7b17 Mon Sep 17 00:00:00 2001 From: sergerad Date: Wed, 4 Feb 2026 12:53:56 +1300 Subject: [PATCH 31/72] Revert workflow --- .github/workflows/build-docker.yml | 34 ++++++------------------------ 1 file changed, 6 insertions(+), 28 deletions(-) diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index 0e7fe0c073..b259c23fd9 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -12,38 +12,16 @@ permissions: jobs: docker-build: - strategy: - matrix: - component: [node] runs-on: Linux-ARM64-Runner - name: Build ${{ matrix.component }} steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Configure AWS credentials - if: github.event.pull_request.head.repo.fork == false - uses: aws-actions/configure-aws-credentials@v4 - with: - aws-region: ${{ secrets.AWS_REGION }} - role-to-assume: ${{ secrets.AWS_ROLE }} - role-session-name: GithubActionsSession - - - name: Set cache parameters - if: github.event.pull_request.head.repo.fork == false - run: | - echo "CACHE_FROM=type=s3,region=${{ secrets.AWS_REGION }},bucket=${{ secrets.AWS_CACHE_BUCKET }},name=miden-${{ matrix.component }}" >> $GITHUB_ENV - echo "CACHE_TO=type=s3,region=${{ secrets.AWS_REGION }},bucket=${{ secrets.AWS_CACHE_BUCKET }},name=miden-${{ matrix.component }}" >> $GITHUB_ENV - - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - with: - cache-binary: true - - name: Build Docker image - uses: docker/build-push-action@v5 + - name: Build and push + uses: docker/build-push-action@v6 with: push: false - file: ./bin/${{ matrix.component }}/Dockerfile - cache-from: ${{ env.CACHE_FROM || '' }} - cache-to: ${{ env.CACHE_TO || '' }} + file: ./bin/node/Dockerfile + cache-from: type=gha + # Only save cache on push into next + cache-to: ${{ github.event_name == 'push' && github.ref == 'refs/heads/next' && 'type=gha,mode=max' || '' }} From 5d76a80e70a78550744278361819e69daddfdd26 Mon Sep 17 00:00:00 2001 From: sergerad Date: Wed, 4 Feb 2026 12:56:04 +1300 Subject: [PATCH 32/72] Fix var name --- bin/node/src/commands/bundled.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/node/src/commands/bundled.rs b/bin/node/src/commands/bundled.rs index c07339127f..51cbf653a9 100644 --- a/bin/node/src/commands/bundled.rs +++ b/bin/node/src/commands/bundled.rs @@ -112,14 +112,14 @@ impl BundledCommand { data_directory, accounts_directory, genesis_config_file, - validator_key: validator_insecure_secret_key, + validator_key, } => { // Currently the bundled bootstrap is identical to the store's bootstrap. crate::commands::store::StoreCommand::Bootstrap { data_directory, accounts_directory, genesis_config_file, - validator_key: validator_insecure_secret_key, + validator_key, } .handle() .await From a468514a0b904e4a8117c67be61ef139f6568486 Mon Sep 17 00:00:00 2001 From: sergerad Date: Wed, 4 Feb 2026 12:59:02 +1300 Subject: [PATCH 33/72] Revert dockerfile --- bin/node/Dockerfile | 45 ++++++++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/bin/node/Dockerfile b/bin/node/Dockerfile index 832b0bb8d2..a6dab8fde5 100644 --- a/bin/node/Dockerfile +++ b/bin/node/Dockerfile @@ -1,39 +1,47 @@ -FROM rust:1.90-slim-bullseye AS builder - +FROM rust:1.90-slim-bullseye AS chef # Install build dependencies. RocksDB is compiled from source by librocksdb-sys. RUN apt-get update && \ apt-get -y upgrade && \ - apt-get install -y llvm clang libclang-dev pkg-config libssl-dev libsqlite3-dev ca-certificates && \ + apt-get install -y \ + llvm \ + clang \ + libclang-dev \ + cmake \ + pkg-config \ + libssl-dev \ + libsqlite3-dev \ + ca-certificates && \ rm -rf /var/lib/apt/lists/* - +RUN cargo install cargo-chef WORKDIR /app -COPY ./Cargo.toml . -COPY ./Cargo.lock . -COPY ./bin ./bin -COPY ./crates ./crates -COPY ./proto ./proto -RUN cargo install --path bin/node --locked +FROM chef AS planner +COPY . . +RUN cargo chef prepare --recipe-path recipe.json -FROM debian:bullseye-slim +FROM chef AS builder +COPY --from=planner /app/recipe.json recipe.json +# Build dependencies - this is the caching Docker layer! +RUN cargo chef cook --release --recipe-path recipe.json +# Build application +COPY . . +RUN cargo build --release --locked --bin miden-node -# Update machine & install required packages -# The installation of sqlite3 is needed for correct function of the SQLite database +# Base line runtime image with runtime dependencies installed. +FROM debian:bullseye-slim AS runtime-base RUN apt-get update && \ apt-get -y upgrade && \ - apt-get install -y --no-install-recommends \ - sqlite3 \ + apt-get install -y --no-install-recommends sqlite3 \ && rm -rf /var/lib/apt/lists/* -COPY --from=builder /usr/local/cargo/bin/miden-node /usr/local/bin/miden-node - +FROM runtime-base AS runtime +COPY --from=builder /app/target/release/miden-node /usr/local/bin/miden-node LABEL org.opencontainers.image.authors=devops@miden.team \ org.opencontainers.image.url=https://0xMiden.github.io/ \ org.opencontainers.image.documentation=https://github.com/0xMiden/miden-node \ org.opencontainers.image.source=https://github.com/0xMiden/miden-node \ org.opencontainers.image.vendor=Miden \ org.opencontainers.image.licenses=MIT - ARG CREATED ARG VERSION ARG COMMIT @@ -43,6 +51,5 @@ LABEL org.opencontainers.image.created=$CREATED \ # Expose RPC port EXPOSE 57291 - # Miden node does not spawn sub-processes, so it can be used as the PID1 CMD miden-node From c0c692bb268edc1ca69c921f0ad6d08b457eb5e6 Mon Sep 17 00:00:00 2001 From: sergerad Date: Wed, 4 Feb 2026 13:04:28 +1300 Subject: [PATCH 34/72] Fix merge issues --- Cargo.lock | 1 + crates/block-producer/Cargo.toml | 1 + crates/block-producer/src/server/mod.rs | 3 + crates/block-producer/src/server/tests.rs | 216 ++++++++++------------ 4 files changed, 101 insertions(+), 120 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3c9d3ec2a1..396216d1a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2784,6 +2784,7 @@ dependencies = [ "miden-node-store", "miden-node-test-macro", "miden-node-utils", + "miden-node-validator", "miden-protocol", "miden-remote-prover-client", "miden-standards", diff --git a/crates/block-producer/Cargo.toml b/crates/block-producer/Cargo.toml index 8437dab3c8..9b91645299 100644 --- a/crates/block-producer/Cargo.toml +++ b/crates/block-producer/Cargo.toml @@ -45,6 +45,7 @@ assert_matches = { workspace = true } miden-node-store = { workspace = true } miden-node-test-macro = { workspace = true } miden-node-utils = { features = ["testing"], workspace = true } +miden-node-validator = { workspace = true } miden-protocol = { default-features = true, features = ["testing"], workspace = true } miden-standards = { features = ["testing"], workspace = true } miden-tx = { features = ["testing"], workspace = true } diff --git a/crates/block-producer/src/server/mod.rs b/crates/block-producer/src/server/mod.rs index d3519eb005..fb6963efde 100644 --- a/crates/block-producer/src/server/mod.rs +++ b/crates/block-producer/src/server/mod.rs @@ -40,6 +40,9 @@ use crate::store::StoreClient; use crate::validator::BlockProducerValidatorClient; use crate::{CACHED_MEMPOOL_STATS_UPDATE_INTERVAL, COMPONENT, SERVER_NUM_BATCH_BUILDERS}; +#[cfg(test)] +mod tests; + /// The block producer server. /// /// Specifies how to connect to the store, batch prover, and block prover components. diff --git a/crates/block-producer/src/server/tests.rs b/crates/block-producer/src/server/tests.rs index 453512597b..af23bcf78c 100644 --- a/crates/block-producer/src/server/tests.rs +++ b/crates/block-producer/src/server/tests.rs @@ -1,27 +1,25 @@ +use std::num::NonZeroUsize; use std::time::Duration; -use miden_air::{ExecutionProof, HashFunction}; -use miden_node_proto::generated::{ - self as proto, block_producer::api_client as block_producer_client, -}; +use miden_node_proto::generated::block_producer::api_client as block_producer_client; use miden_node_store::{GenesisState, Store}; -use miden_protocol::{ - Digest, - account::{AccountId, AccountIdVersion, AccountStorageMode, AccountType}, - transaction::ProvenTransactionBuilder, -}; -use miden_tx::utils::Serializable; -use tokio::{net::TcpListener, runtime, task, time::sleep}; +use miden_node_utils::fee::test_fee_params; +use miden_node_validator::Validator; +use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SecretKey; +use miden_protocol::testing::random_signer::RandomBlockSigner as _; +use tokio::net::TcpListener; +use tokio::time::sleep; +use tokio::{runtime, task}; use tonic::transport::{Channel, Endpoint}; -use winterfell::Proof; +use url::Url; -use crate::{BlockProducer, SERVER_MAX_BATCHES_PER_BLOCK, SERVER_MAX_TXS_PER_BATCH}; +use crate::{BlockProducer, DEFAULT_MAX_BATCHES_PER_BLOCK, DEFAULT_MAX_TXS_PER_BATCH}; +/// Tests that the block producer starts up correctly even when the store is not initially +/// available. The block producer should retry with exponential backoff until the store becomes +/// available, then start serving requests. #[tokio::test] async fn block_producer_startup_is_robust_to_network_failures() { - // This test starts the block producer and tests that it starts serving only after the store - // is started. - // get the addresses for the store and block producer let store_addr = { let store_listener = @@ -36,113 +34,104 @@ async fn block_producer_startup_is_robust_to_network_failures() { .expect("Failed to get block-producer address") }; - let ntx_builder_addr = { - let ntx_builder_address = TcpListener::bind("127.0.0.1:0") - .await - .expect("failed to bind the ntx builder address"); - ntx_builder_address.local_addr().expect("failed to get ntx builder address") + let validator_addr = { + let validator_listener = + TcpListener::bind("127.0.0.1:0").await.expect("failed to bind validator"); + validator_listener.local_addr().expect("failed to get validator address") }; - // start the block producer + let grpc_timeout = Duration::from_secs(30); + + // start the validator + task::spawn(async move { + Validator { + address: validator_addr, + grpc_timeout, + signer: SecretKey::random(), + data_directory: "/tmp/".into(), + } + .serve() + .await + .unwrap(); + }); + + // start the block producer BEFORE the store is available + // this tests the exponential backoff behavior + let store_url = Url::parse(&format!("http://{store_addr}")).expect("Failed to parse store URL"); + let validator_url = + Url::parse(&format!("http://{validator_addr}")).expect("Failed to parse validator URL"); task::spawn(async move { BlockProducer { block_producer_address: block_producer_addr, - store_address: store_addr, - ntx_builder_address: Some(ntx_builder_addr), + store_url, + validator_url, batch_prover_url: None, - block_prover_url: None, batch_interval: Duration::from_millis(500), block_interval: Duration::from_millis(500), - max_txs_per_batch: SERVER_MAX_TXS_PER_BATCH, - max_batches_per_block: SERVER_MAX_BATCHES_PER_BLOCK, + max_txs_per_batch: DEFAULT_MAX_TXS_PER_BATCH, + max_batches_per_block: DEFAULT_MAX_BATCHES_PER_BLOCK, + grpc_timeout, + mempool_tx_capacity: NonZeroUsize::new(100).unwrap(), } .serve() .await .unwrap(); }); - // test: connecting to the block producer should fail until the store is started + // test: connecting to the block producer should fail because the store is not yet started + // (and therefore the block producer is not yet listening) let block_producer_endpoint = Endpoint::try_from(format!("http://{block_producer_addr}")).expect("valid url"); let block_producer_client = block_producer_client::ApiClient::connect(block_producer_endpoint.clone()).await; - assert!(block_producer_client.is_err()); + assert!( + block_producer_client.is_err(), + "Block producer should not be available before store is started" + ); // start the store let data_directory = tempfile::tempdir().expect("tempdir should be created"); - let store_runtime = { - let genesis_state = GenesisState::new(vec![], 1, 1); - Store::bootstrap(genesis_state.clone(), data_directory.path()) - .expect("store should bootstrap"); - let dir = data_directory.path().to_path_buf(); - let rpc_listener = - TcpListener::bind("127.0.0.1:0").await.expect("store should bind the RPC port"); - let ntx_builder_listener = TcpListener::bind("127.0.0.1:0") - .await - .expect("Failed to bind store ntx-builder gRPC endpoint"); - let block_producer_listener = TcpListener::bind(store_addr) - .await - .expect("store should bind the block-producer port"); - // in order to later kill the store, we need to spawn a new runtime and run the store on - // it. That allows us to kill all the tasks spawned by the store when we - // kill the runtime. - let store_runtime = - runtime::Builder::new_multi_thread().enable_time().enable_io().build().unwrap(); - store_runtime.spawn(async move { - Store { - rpc_listener, - ntx_builder_listener, - block_producer_listener, - data_directory: dir, - grpc_timeout: std::time::Duration::from_secs(30), + let store_runtime = start_store(store_addr, data_directory.path()).await; + + // wait for the block producer's exponential backoff to connect to the store + // use a retry loop since CI environments may be slower + let block_producer_client = { + let mut attempts = 0; + loop { + attempts += 1; + match block_producer_client::ApiClient::connect(block_producer_endpoint.clone()).await { + Ok(client) => break client, + Err(_) if attempts < 30 => { + sleep(Duration::from_millis(200)).await; + }, + Err(e) => panic!( + "block producer client should connect after store is started (after {attempts} attempts): {e}" + ), } - .serve() - .await - .expect("store should start serving"); - }); - store_runtime + } }; - // we need to wait for the exponential backoff of the block producer to connect to the store - sleep(Duration::from_secs(1)).await; + // test: status request against block-producer should succeed + let response = send_status_request(block_producer_client).await; + assert!(response.is_ok(), "Status request should succeed, got: {:?}", response.err()); - let block_producer_client = block_producer_client::ApiClient::connect(block_producer_endpoint) - .await - .expect("block producer client should connect"); + // verify the response contains expected data + let status = response.unwrap().into_inner(); + assert_eq!(status.status, "connected"); - // test: request against block-producer api should succeed - let response = send_request(block_producer_client.clone(), 0).await; - assert!(response.is_ok()); - - // kill the store - shutdown_store(store_runtime).await; - - // test: request against block-producer api should fail immediately - let response = send_request(block_producer_client.clone(), 1).await; - assert!(response.is_err()); - - // test: restart the store and request should succeed - let store_runtime = restart_store(store_addr, data_directory.path()).await; - let response = send_request(block_producer_client.clone(), 2).await; - assert!(response.is_ok()); - - // Shutdown the store before data_directory is dropped to allow RocksDB to flush properly + // Shutdown the store before data_directory is dropped to allow the database to flush properly shutdown_store(store_runtime).await; } -/// Shuts down the store runtime properly to allow RocksDB to flush before the temp directory is -/// deleted. -async fn shutdown_store(store_runtime: runtime::Runtime) { - task::spawn_blocking(move || store_runtime.shutdown_timeout(Duration::from_millis(500))) - .await - .expect("shutdown should complete"); -} - -/// Restarts a store using an existing data directory. Returns the runtime handle for shutdown. -async fn restart_store( +/// Starts the store with a fresh genesis state and returns the runtime handle. +async fn start_store( store_addr: std::net::SocketAddr, data_directory: &std::path::Path, ) -> runtime::Runtime { + let genesis_state = GenesisState::new(vec![], test_fee_params(), 1, 1, SecretKey::random()); + Store::bootstrap(genesis_state.clone(), data_directory).expect("store should bootstrap"); + + let dir = data_directory.to_path_buf(); let rpc_listener = TcpListener::bind("127.0.0.1:0").await.expect("store should bind the RPC port"); let ntx_builder_listener = TcpListener::bind("127.0.0.1:0") @@ -151,7 +140,8 @@ async fn restart_store( let block_producer_listener = TcpListener::bind(store_addr) .await .expect("store should bind the block-producer port"); - let dir = data_directory.to_path_buf(); + + // Use a separate runtime so we can kill all store tasks later let store_runtime = runtime::Builder::new_multi_thread().enable_time().enable_io().build().unwrap(); store_runtime.spawn(async move { @@ -159,8 +149,9 @@ async fn restart_store( rpc_listener, ntx_builder_listener, block_producer_listener, + block_prover_url: None, data_directory: dir, - grpc_timeout: std::time::Duration::from_secs(30), + grpc_timeout: Duration::from_secs(30), } .serve() .await @@ -169,32 +160,17 @@ async fn restart_store( store_runtime } -/// Creates a dummy transaction and submits it to the block producer. -async fn send_request( +/// Shuts down the store runtime properly to allow the database to flush before the temp directory +/// is deleted. +async fn shutdown_store(store_runtime: runtime::Runtime) { + task::spawn_blocking(move || store_runtime.shutdown_timeout(Duration::from_millis(500))) + .await + .expect("shutdown should complete"); +} + +/// Sends a status request to the block producer to verify connectivity. +async fn send_status_request( mut client: block_producer_client::ApiClient, - i: u8, -) -> Result, tonic::Status> -{ - let tx = ProvenTransactionBuilder::new( - AccountId::dummy( - [0; 15], - AccountIdVersion::Version0, - AccountType::RegularAccountImmutableCode, - AccountStorageMode::Private, - ), - Digest::default(), - [i; 32].try_into().unwrap(), - Digest::default(), - 0.into(), - Digest::default(), - u32::MAX.into(), - ExecutionProof::new(Proof::new_dummy(), HashFunction::default()), - ) - .build() - .unwrap(); - let request = proto::transaction::ProvenTransaction { - transaction: tx.to_bytes(), - transaction_replay: None, - }; - client.submit_proven_transaction(request).await +) -> Result, tonic::Status> { + client.status(()).await } From 2e92e72cdb8642ffa0bfa9ccf7f120627bc2ce0d Mon Sep 17 00:00:00 2001 From: sergerad Date: Wed, 4 Feb 2026 14:01:18 +1300 Subject: [PATCH 35/72] Fix more merge issues --- crates/rocksdb-cxx-linkage-fix/src/lib.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/rocksdb-cxx-linkage-fix/src/lib.rs b/crates/rocksdb-cxx-linkage-fix/src/lib.rs index eeaa456d0c..9eaae82fd3 100644 --- a/crates/rocksdb-cxx-linkage-fix/src/lib.rs +++ b/crates/rocksdb-cxx-linkage-fix/src/lib.rs @@ -6,6 +6,7 @@ use std::env; pub fn configure() { println!("cargo:rerun-if-env-changed=ROCKSDB_COMPILE"); + println!("cargo:rerun-if-env-changed=ROCKSDB_LIB_DIR"); println!("cargo:rerun-if-env-changed=ROCKSDB_STATIC"); println!("cargo:rerun-if-env-changed=CXXSTDLIB"); let target = env::var("TARGET").unwrap_or_default(); @@ -18,8 +19,9 @@ fn should_link_cpp_stdlib() -> bool { let rocksdb_compile = env::var("ROCKSDB_COMPILE").unwrap_or_default(); let rocksdb_compile_disabled = matches!(rocksdb_compile.as_str(), "0" | "false" | "FALSE"); let rocksdb_static = env::var("ROCKSDB_STATIC").is_ok(); + let rocksdb_lib_dir_set = env::var("ROCKSDB_LIB_DIR").is_ok(); - rocksdb_compile_disabled && rocksdb_static + rocksdb_lib_dir_set || (rocksdb_static && rocksdb_compile_disabled) } fn link_cpp_stdlib(target: &str) { From 88624e5403b74f9db20cc5fedcc1850cc815d7a1 Mon Sep 17 00:00:00 2001 From: sergerad Date: Wed, 4 Feb 2026 14:04:43 +1300 Subject: [PATCH 36/72] More merge issues --- Cargo.lock | 1 + crates/store/Cargo.toml | 3 +++ crates/store/build.rs | 3 +++ crates/utils/Cargo.toml | 3 +++ 4 files changed, 10 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 396216d1a3..041200867e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2993,6 +2993,7 @@ dependencies = [ "http-body-util", "itertools 0.14.0", "lru 0.16.3", + "miden-node-rocksdb-cxx-linkage-fix", "miden-protocol", "opentelemetry", "opentelemetry-otlp", diff --git a/crates/store/Cargo.toml b/crates/store/Cargo.toml index c2e36a0085..bd16c8abcc 100644 --- a/crates/store/Cargo.toml +++ b/crates/store/Cargo.toml @@ -49,6 +49,9 @@ tower-http = { features = ["util"], workspace = true } tracing = { workspace = true } url = { workspace = true } +[build-dependencies] +miden-node-rocksdb-cxx-linkage-fix = { workspace = true } + [dev-dependencies] assert_matches = { workspace = true } criterion = { version = "0.5" } diff --git a/crates/store/build.rs b/crates/store/build.rs index d08f3fd0e6..a911bea19b 100644 --- a/crates/store/build.rs +++ b/crates/store/build.rs @@ -1,9 +1,12 @@ // This build.rs is required to trigger the `diesel_migrations::embed_migrations!` proc-macro in // `store/src/db/migrations.rs` to include the latest version of the migrations into the binary, see . + fn main() { println!("cargo:rerun-if-changed=./src/db/migrations"); // If we do one re-write, the default rules are disabled, // hence we need to trigger explicitly on `Cargo.toml`. // println!("cargo:rerun-if-changed=Cargo.toml"); + + miden_node_rocksdb_cxx_linkage_fix::configure(); } diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index e61930937e..2c5fea6e51 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -42,5 +42,8 @@ tracing-opentelemetry = { version = "0.32" } tracing-subscriber = { workspace = true } url = { workspace = true } +[build-dependencies] +miden-node-rocksdb-cxx-linkage-fix = { workspace = true } + [dev-dependencies] thiserror = { workspace = true } From 60e1e1f3a94c7c611b58a11a5a360c0f1007ea96 Mon Sep 17 00:00:00 2001 From: sergerad Date: Wed, 4 Feb 2026 14:06:08 +1300 Subject: [PATCH 37/72] Another merge issue --- crates/rpc/src/tests.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/rpc/src/tests.rs b/crates/rpc/src/tests.rs index 3d87c83289..a0b7854e5d 100644 --- a/crates/rpc/src/tests.rs +++ b/crates/rpc/src/tests.rs @@ -247,6 +247,9 @@ async fn rpc_server_rejects_proven_transactions_with_invalid_commitment() { let (_, rpc_addr, store_addr) = start_rpc().await; let (store_runtime, _data_directory, genesis) = start_store(store_addr).await; + // Wait for the store to be ready before sending requests. + tokio::time::sleep(Duration::from_millis(100)).await; + // Override the client so that the ACCEPT header is not set. let mut rpc_client = miden_node_proto::clients::Builder::new(Url::parse(&format!("http://{rpc_addr}")).unwrap()) From 81db1de0d068f9d14b91441feeebf7e3f48525da Mon Sep 17 00:00:00 2001 From: sergerad Date: Wed, 4 Feb 2026 14:07:32 +1300 Subject: [PATCH 38/72] Fix comment --- crates/validator/src/block_validation/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/validator/src/block_validation/mod.rs b/crates/validator/src/block_validation/mod.rs index 137e753000..32b40d6213 100644 --- a/crates/validator/src/block_validation/mod.rs +++ b/crates/validator/src/block_validation/mod.rs @@ -30,7 +30,7 @@ pub async fn validate_block( signer: &S, db: &Db, ) -> Result { - // Retrieve all validated transactions pertaining to the proposed block. + // Search for any proposed transactions that have not previously been validated. let proposed_tx_ids = proposed_block.transactions().map(TransactionHeader::id).collect::>(); let unvalidated_transactions = db From 59858c7115390f4c84ea6dd1ec059cda0184b9ef Mon Sep 17 00:00:00 2001 From: sergerad Date: Wed, 4 Feb 2026 14:16:20 +1300 Subject: [PATCH 39/72] Fix unvalidated logic --- crates/validator/src/db/mod.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/crates/validator/src/db/mod.rs b/crates/validator/src/db/mod.rs index f367099515..69171d0cbc 100644 --- a/crates/validator/src/db/mod.rs +++ b/crates/validator/src/db/mod.rs @@ -90,7 +90,7 @@ pub(crate) fn find_unvalidated_transactions( let tx_id_bytes: Vec> = tx_ids.iter().map(TransactionId::to_bytes).collect(); // Query the database for matching transactions ids. - let raw_transactions_ids = schema::transactions::table + let raw_transaction_ids = schema::transactions::table .select(schema::transactions::id) .filter(schema::transactions::id.eq_any(tx_id_bytes)) .order(schema::transactions::id.asc()) @@ -98,12 +98,14 @@ pub(crate) fn find_unvalidated_transactions( .map_err(DatabaseError::from)?; // Find any requested ids that the database does not contain. - let expected_tx_ids = tx_ids.iter().copied().collect::>(); + let validated_tx_ids = raw_transaction_ids + .into_iter() + .map(|raw_id| TransactionId::read_from_bytes(&raw_id)) + .collect::, _>>()?; let mut unvalidated_tx_ids = Vec::new(); - for raw_tx_id in raw_transactions_ids { - let tx_id = TransactionId::read_from_bytes(&raw_tx_id)?; - if !expected_tx_ids.contains(&tx_id) { - unvalidated_tx_ids.push(tx_id); + for tx_id in tx_ids { + if !validated_tx_ids.contains(tx_id) { + unvalidated_tx_ids.push(*tx_id); } } Ok(unvalidated_tx_ids) From 37eab169898fda1127d35cdf6d04a73ac338a903 Mon Sep 17 00:00:00 2001 From: sergerad Date: Wed, 4 Feb 2026 14:19:22 +1300 Subject: [PATCH 40/72] Add missing build.rs --- crates/utils/build.rs | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 crates/utils/build.rs diff --git a/crates/utils/build.rs b/crates/utils/build.rs new file mode 100644 index 0000000000..ed4038d06e --- /dev/null +++ b/crates/utils/build.rs @@ -0,0 +1,3 @@ +fn main() { + miden_node_rocksdb_cxx_linkage_fix::configure(); +} From 4fcc6c860206cf28ba9bb46564dd6dfb196ec55d Mon Sep 17 00:00:00 2001 From: sergerad Date: Wed, 4 Feb 2026 14:23:03 +1300 Subject: [PATCH 41/72] machete --- crates/block-producer/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/block-producer/Cargo.toml b/crates/block-producer/Cargo.toml index 9b91645299..023a7a448c 100644 --- a/crates/block-producer/Cargo.toml +++ b/crates/block-producer/Cargo.toml @@ -28,7 +28,6 @@ miden-node-utils = { features = ["testing"], workspace = true } miden-protocol = { default-features = true, workspace = true } miden-remote-prover-client = { features = ["batch-prover", "block-prover"], workspace = true } miden-standards = { workspace = true } -miden-tx = { default-features = true, workspace = true } miden-tx-batch-prover = { workspace = true } rand = { version = "0.9" } thiserror = { workspace = true } From 072dbe3c683bb5b96a8a95105f4239e546668d3f Mon Sep 17 00:00:00 2001 From: sergerad Date: Mon, 9 Feb 2026 10:03:30 +1300 Subject: [PATCH 42/72] Use tempdir --- crates/block-producer/src/server/tests.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/block-producer/src/server/tests.rs b/crates/block-producer/src/server/tests.rs index af23bcf78c..8c98e9da4d 100644 --- a/crates/block-producer/src/server/tests.rs +++ b/crates/block-producer/src/server/tests.rs @@ -44,11 +44,13 @@ async fn block_producer_startup_is_robust_to_network_failures() { // start the validator task::spawn(async move { + let temp_dir = tempfile::tempdir().expect("tempdir should be created"); + let data_directory = temp_dir.path().to_path_buf(); Validator { address: validator_addr, grpc_timeout, signer: SecretKey::random(), - data_directory: "/tmp/".into(), + data_directory, } .serve() .await From e639c9794e62da41e2d45517bcac2833f38e2d8b Mon Sep 17 00:00:00 2001 From: sergerad Date: Mon, 9 Feb 2026 10:10:53 +1300 Subject: [PATCH 43/72] UnvalidatedTransactions wording --- crates/validator/src/block_validation/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/validator/src/block_validation/mod.rs b/crates/validator/src/block_validation/mod.rs index 32b40d6213..6cbb02c16a 100644 --- a/crates/validator/src/block_validation/mod.rs +++ b/crates/validator/src/block_validation/mod.rs @@ -12,7 +12,7 @@ use crate::db::find_unvalidated_transactions; #[derive(thiserror::Error, Debug)] pub enum BlockValidationError { - #[error("found unvalidated transactions {0:?}")] + #[error("block contains unvalidated transactions {0:?}")] UnvalidatedTransactions(Vec), #[error("failed to build block")] BlockBuildingFailed(#[from] ProposedBlockError), From cff7f1bdbf91363aa680354a296e5365b1151c37 Mon Sep 17 00:00:00 2001 From: sergerad Date: Mon, 9 Feb 2026 10:12:01 +1300 Subject: [PATCH 44/72] err as report --- crates/validator/src/server/mod.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/validator/src/server/mod.rs b/crates/validator/src/server/mod.rs index 44477ecbca..af840ec8ae 100644 --- a/crates/validator/src/server/mod.rs +++ b/crates/validator/src/server/mod.rs @@ -175,7 +175,10 @@ impl api_server::Api for ValidatorServer // Validate the block. let signature = validate_block(proposed_block, &self.signer, &self.db).await.map_err(|err| { - tonic::Status::invalid_argument(format!("Failed to validate block: {err}",)) + tonic::Status::invalid_argument(format!( + "Failed to validate block: {}", + err.as_report() + )) })?; // Send the signature. From 4cb0459c729e10189018eabc31d1b604b8474a23 Mon Sep 17 00:00:00 2001 From: sergerad Date: Mon, 9 Feb 2026 10:12:45 +1300 Subject: [PATCH 45/72] Rm order query --- crates/validator/src/db/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/validator/src/db/mod.rs b/crates/validator/src/db/mod.rs index 69171d0cbc..f22bb8caea 100644 --- a/crates/validator/src/db/mod.rs +++ b/crates/validator/src/db/mod.rs @@ -60,7 +60,6 @@ pub(crate) fn select_validated_transactions( // Query the database for matching transactions. let raw_transactions = schema::transactions::table .filter(schema::transactions::id.eq_any(tx_id_bytes)) - .order(schema::transactions::id.asc()) .load::(conn) .map_err(DatabaseError::from)?; From 6885616ae38e9b3c21ddf02a6608a7981d0ae35f Mon Sep 17 00:00:00 2001 From: sergerad Date: Mon, 9 Feb 2026 10:13:50 +1300 Subject: [PATCH 46/72] RM deadcode --- crates/validator/src/db/mod.rs | 34 ++----------------------------- crates/validator/src/db/models.rs | 9 -------- 2 files changed, 2 insertions(+), 41 deletions(-) diff --git a/crates/validator/src/db/mod.rs b/crates/validator/src/db/mod.rs index f22bb8caea..c13ee12d5e 100644 --- a/crates/validator/src/db/mod.rs +++ b/crates/validator/src/db/mod.rs @@ -2,7 +2,7 @@ mod migrations; mod models; mod schema; -use std::collections::{HashMap, HashSet}; +use std::collections::HashSet; use std::path::PathBuf; use diesel::SqliteConnection; @@ -14,7 +14,7 @@ use tracing::instrument; use crate::COMPONENT; use crate::db::migrations::apply_migrations; -use crate::db::models::{TransactionSummaryRowInsert, TransactionSummaryRowSelect}; +use crate::db::models::TransactionSummaryRowInsert; /// Open a connection to the DB and apply any pending migrations. #[instrument(target = COMPONENT, skip_all)] @@ -44,36 +44,6 @@ pub(crate) fn insert_transaction( Ok(count) } -/// Retrieves validated transactions from the database. -#[allow(dead_code)] -pub(crate) fn select_validated_transactions( - conn: &mut SqliteConnection, - tx_ids: &[TransactionId], -) -> Result, DatabaseError> { - if tx_ids.is_empty() { - return Ok(HashMap::new()); - } - - // Convert TransactionIds to bytes for query. - let tx_id_bytes: Vec> = tx_ids.iter().map(TransactionId::to_bytes).collect(); - - // Query the database for matching transactions. - let raw_transactions = schema::transactions::table - .filter(schema::transactions::id.eq_any(tx_id_bytes)) - .load::(conn) - .map_err(DatabaseError::from)?; - - // Deserialize the transaction blobs. - let mut transactions: HashMap = HashMap::new(); - for raw_tx in raw_transactions { - let id = TransactionId::read_from_bytes(&raw_tx.id)?; - let summary = TransactionSummary::read_from_bytes(&raw_tx.summary)?; - transactions.insert(id, summary); - } - - Ok(transactions) -} - /// Scans the database for transaction Ids that do not exist. /// /// If the resulting vector is empty, all supplied transaction ids have been validated in the past. diff --git a/crates/validator/src/db/models.rs b/crates/validator/src/db/models.rs index e7ecafdb39..fcfb9dba08 100644 --- a/crates/validator/src/db/models.rs +++ b/crates/validator/src/db/models.rs @@ -22,12 +22,3 @@ impl TransactionSummaryRowInsert { } } } - -#[derive(Debug, Clone, PartialEq, Queryable, Selectable)] -#[diesel(table_name = schema::transactions)] -#[diesel(check_for_backend(diesel::sqlite::Sqlite))] -pub struct TransactionSummaryRowSelect { - pub id: Vec, - pub account_id: Vec, - pub summary: Vec, -} From e612a988e5bfcb6ca47b1a8d86a0ce1bef27f988 Mon Sep 17 00:00:00 2001 From: sergerad Date: Mon, 9 Feb 2026 10:16:56 +1300 Subject: [PATCH 47/72] Align sql add comments --- .../validator/src/db/migrations/2025062000000_setup/up.sql | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/validator/src/db/migrations/2025062000000_setup/up.sql b/crates/validator/src/db/migrations/2025062000000_setup/up.sql index 8088676961..15e3c77238 100644 --- a/crates/validator/src/db/migrations/2025062000000_setup/up.sql +++ b/crates/validator/src/db/migrations/2025062000000_setup/up.sql @@ -1,7 +1,7 @@ CREATE TABLE transactions ( - id BLOB NOT NULL, - account_id BLOB NOT NULL, - summary BLOB NOT NULL, + id BLOB NOT NULL, + account_id BLOB NOT NULL, + summary BLOB NOT NULL, -- Binary encoded TransactionSummary. PRIMARY KEY (id) ) WITHOUT ROWID; From 624c5818734acb887393eec743df65e0dba1b1cb Mon Sep 17 00:00:00 2001 From: sergerad Date: Mon, 9 Feb 2026 10:34:39 +1300 Subject: [PATCH 48/72] Add raw sql comment --- crates/validator/src/db/mod.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crates/validator/src/db/mod.rs b/crates/validator/src/db/mod.rs index c13ee12d5e..4ebcb0f045 100644 --- a/crates/validator/src/db/mod.rs +++ b/crates/validator/src/db/mod.rs @@ -47,6 +47,19 @@ pub(crate) fn insert_transaction( /// Scans the database for transaction Ids that do not exist. /// /// If the resulting vector is empty, all supplied transaction ids have been validated in the past. +/// +/// # Raw SQL +/// +/// ```sql +/// SELECT +/// id +/// FROM +/// transactions +/// WHERE +/// id IN (?, ...) +/// ORDER BY +/// id ASC +/// ``` pub(crate) fn find_unvalidated_transactions( conn: &mut SqliteConnection, tx_ids: &[TransactionId], From 2289a423609f222490b311dead8cbd24c8a86517 Mon Sep 17 00:00:00 2001 From: sergerad Date: Mon, 9 Feb 2026 10:51:01 +1300 Subject: [PATCH 49/72] Fix validator key var name --- bin/node/src/commands/store.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/node/src/commands/store.rs b/bin/node/src/commands/store.rs index c08348832e..e8e9ff56ab 100644 --- a/bin/node/src/commands/store.rs +++ b/bin/node/src/commands/store.rs @@ -109,12 +109,12 @@ impl StoreCommand { data_directory, accounts_directory, genesis_config_file, - validator_key: validator_insecure_secret_key, + validator_key, } => Self::bootstrap( &data_directory, &accounts_directory, genesis_config_file.as_ref(), - validator_insecure_secret_key, + validator_key, ), StoreCommand::Start { rpc_url, From 78030a776578851249240ca1cc3de766d457aef0 Mon Sep 17 00:00:00 2001 From: sergerad Date: Mon, 9 Feb 2026 10:52:53 +1300 Subject: [PATCH 50/72] Added explanatory comment for store validator key arg --- bin/node/src/commands/store.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bin/node/src/commands/store.rs b/bin/node/src/commands/store.rs index e8e9ff56ab..50df572dad 100644 --- a/bin/node/src/commands/store.rs +++ b/bin/node/src/commands/store.rs @@ -46,6 +46,8 @@ pub enum StoreCommand { genesis_config_file: Option, /// Insecure, hex-encoded validator secret key for development and testing purposes. /// + /// Used to sign the genesis block in the bootstrap process. + /// /// If not provided, a predefined key is used. #[arg( long = "validator.key", From 425f6456be74f9b7ea391e07c2f696648e857cee Mon Sep 17 00:00:00 2001 From: sergerad Date: Mon, 9 Feb 2026 11:06:44 +1300 Subject: [PATCH 51/72] ValidatorConfig::to_addresses() --- bin/node/src/commands/bundled.rs | 15 +-------------- bin/node/src/commands/mod.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/bin/node/src/commands/bundled.rs b/bin/node/src/commands/bundled.rs index 51cbf653a9..d6154a5af4 100644 --- a/bin/node/src/commands/bundled.rs +++ b/bin/node/src/commands/bundled.rs @@ -181,20 +181,7 @@ impl BundledCommand { }; // Validator URL is either specified remote, or generated local. - let (validator_url, validator_socket_address) = { - if let Some(remote_url) = validator.validator_url { - (remote_url, None) - } else { - let socket_addr = TcpListener::bind("127.0.0.1:0") - .await - .context("Failed to bind to validator gRPC endpoint")? - .local_addr() - .context("Failed to retrieve the validator's gRPC address")?; - let url = Url::parse(&format!("http://{socket_addr}")) - .context("Failed to parse Validator URL")?; - (url, Some(socket_addr)) - } - }; + let (validator_url, validator_socket_address) = validator.to_addresses().await?; // Store addresses for each exposed API let store_rpc_listener = TcpListener::bind("127.0.0.1:0") diff --git a/bin/node/src/commands/mod.rs b/bin/node/src/commands/mod.rs index 87e673027a..602051a62c 100644 --- a/bin/node/src/commands/mod.rs +++ b/bin/node/src/commands/mod.rs @@ -1,12 +1,15 @@ +use std::net::SocketAddr; use std::num::NonZeroUsize; use std::time::Duration; +use anyhow::Context; use miden_node_block_producer::{ DEFAULT_BATCH_INTERVAL, DEFAULT_BLOCK_INTERVAL, DEFAULT_MAX_BATCHES_PER_BLOCK, DEFAULT_MAX_TXS_PER_BATCH, }; +use tokio::net::TcpListener; use url::Url; pub mod block_producer; @@ -66,6 +69,29 @@ pub struct ValidatorConfig { pub validator_url: Option, } +impl ValidatorConfig { + /// Converts the [`ValidatorConfig`] into a URL and an optional [`SocketAddr`]. + /// + /// If the `validator_url` is set, it returns the URL and `None` for the [`SocketAddr`]. + /// + /// If `validator_url` is not set, it binds to a random port on localhost, creates a URL, + /// and returns the URL and the bound [`SocketAddr`]. + async fn to_addresses(&self) -> anyhow::Result<(Url, Option)> { + if let Some(url) = &self.validator_url { + Ok((url.clone(), None)) + } else { + let socket_addr = TcpListener::bind("127.0.0.1:0") + .await + .context("Failed to bind to validator gRPC endpoint")? + .local_addr() + .context("Failed to retrieve the validator's gRPC address")?; + let url = Url::parse(&format!("http://{socket_addr}")) + .context("Failed to parse Validator URL")?; + Ok((url, Some(socket_addr))) + } + } +} + /// Configuration for the Network Transaction Builder component. #[derive(clap::Args)] pub struct NtxBuilderConfig { From 025cc9395be2ee1ed75db75bd3e426a7e8a7e6ce Mon Sep 17 00:00:00 2001 From: sergerad Date: Mon, 9 Feb 2026 11:33:02 +1300 Subject: [PATCH 52/72] Rm pub --- bin/node/src/commands/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/node/src/commands/mod.rs b/bin/node/src/commands/mod.rs index 602051a62c..a4c9088460 100644 --- a/bin/node/src/commands/mod.rs +++ b/bin/node/src/commands/mod.rs @@ -66,7 +66,7 @@ pub struct ValidatorConfig { /// The remote Validator's gRPC URL. If unset, will default to running a Validator /// in-process. If set, the insecure key argument is ignored. #[arg(long = "validator.url", env = ENV_VALIDATOR_URL, value_name = "URL")] - pub validator_url: Option, + validator_url: Option, } impl ValidatorConfig { From 6fa8f4689dad605dc7175d08c000be3718906b40 Mon Sep 17 00:00:00 2001 From: sergerad Date: Mon, 9 Feb 2026 12:00:31 +1300 Subject: [PATCH 53/72] Add ValidatedTransactionInfo --- .../db/migrations/2025062000000_setup/up.sql | 6 +++--- crates/validator/src/db/mod.rs | 21 +++++++++++-------- crates/validator/src/db/models.rs | 17 ++++++++------- crates/validator/src/db/schema.rs | 4 ++-- crates/validator/src/tx_validation/mod.rs | 20 ++++++------------ 5 files changed, 32 insertions(+), 36 deletions(-) diff --git a/crates/validator/src/db/migrations/2025062000000_setup/up.sql b/crates/validator/src/db/migrations/2025062000000_setup/up.sql index 15e3c77238..4f34a3a635 100644 --- a/crates/validator/src/db/migrations/2025062000000_setup/up.sql +++ b/crates/validator/src/db/migrations/2025062000000_setup/up.sql @@ -1,8 +1,8 @@ -CREATE TABLE transactions ( +CREATE TABLE validated_transactions ( id BLOB NOT NULL, account_id BLOB NOT NULL, - summary BLOB NOT NULL, -- Binary encoded TransactionSummary. + info BLOB NOT NULL, -- Binary encoded ValidatedTransactionInfo. PRIMARY KEY (id) ) WITHOUT ROWID; -CREATE INDEX idx_transactions_account_id ON transactions(account_id); +CREATE INDEX idx__validated_transactions_account_id ON validated_transactions(account_id); diff --git a/crates/validator/src/db/mod.rs b/crates/validator/src/db/mod.rs index 4ebcb0f045..afc2cbb7e8 100644 --- a/crates/validator/src/db/mod.rs +++ b/crates/validator/src/db/mod.rs @@ -8,13 +8,14 @@ use std::path::PathBuf; use diesel::SqliteConnection; use diesel::prelude::*; use miden_node_store::{ConnectionManager, DatabaseError, DatabaseSetupError}; -use miden_protocol::transaction::{TransactionId, TransactionSummary}; +use miden_protocol::transaction::TransactionId; use miden_protocol::utils::{Deserializable, Serializable}; use tracing::instrument; use crate::COMPONENT; use crate::db::migrations::apply_migrations; -use crate::db::models::TransactionSummaryRowInsert; +use crate::db::models::ValidatedTransactionInfoRowInsert; +use crate::tx_validation::ValidatedTransactionInfo; /// Open a connection to the DB and apply any pending migrations. #[instrument(target = COMPONENT, skip_all)] @@ -37,10 +38,12 @@ pub async fn load(database_filepath: PathBuf) -> Result Result { - let row = TransactionSummaryRowInsert::new(tx_id, summary); - let count = diesel::insert_into(schema::transactions::table).values(row).execute(conn)?; + let row = ValidatedTransactionInfoRowInsert::new(tx_id, summary); + let count = diesel::insert_into(schema::validated_transactions::table) + .values(row) + .execute(conn)?; Ok(count) } @@ -72,10 +75,10 @@ pub(crate) fn find_unvalidated_transactions( let tx_id_bytes: Vec> = tx_ids.iter().map(TransactionId::to_bytes).collect(); // Query the database for matching transactions ids. - let raw_transaction_ids = schema::transactions::table - .select(schema::transactions::id) - .filter(schema::transactions::id.eq_any(tx_id_bytes)) - .order(schema::transactions::id.asc()) + let raw_transaction_ids = schema::validated_transactions::table + .select(schema::validated_transactions::id) + .filter(schema::validated_transactions::id.eq_any(tx_id_bytes)) + .order(schema::validated_transactions::id.asc()) .load::>(conn) .map_err(DatabaseError::from)?; diff --git a/crates/validator/src/db/models.rs b/crates/validator/src/db/models.rs index fcfb9dba08..ddac5f4482 100644 --- a/crates/validator/src/db/models.rs +++ b/crates/validator/src/db/models.rs @@ -1,24 +1,25 @@ use diesel::prelude::*; -use miden_protocol::transaction::{TransactionId, TransactionSummary}; +use miden_protocol::transaction::TransactionId; use miden_tx::utils::Serializable; use crate::db::schema; +use crate::tx_validation::ValidatedTransactionInfo; #[derive(Debug, Clone, PartialEq, Insertable)] -#[diesel(table_name = schema::transactions)] +#[diesel(table_name = schema::validated_transactions)] #[diesel(check_for_backend(diesel::sqlite::Sqlite))] -pub struct TransactionSummaryRowInsert { +pub struct ValidatedTransactionInfoRowInsert { pub id: Vec, pub account_id: Vec, - pub summary: Vec, + pub info: Vec, } -impl TransactionSummaryRowInsert { - pub fn new(id: &TransactionId, summary: &TransactionSummary) -> Self { +impl ValidatedTransactionInfoRowInsert { + pub fn new(id: &TransactionId, info: &ValidatedTransactionInfo) -> Self { Self { id: id.to_bytes(), - account_id: summary.account_delta().id().to_bytes(), - summary: summary.to_bytes(), + account_id: info.account_id().to_bytes(), + info: info.to_bytes(), } } } diff --git a/crates/validator/src/db/schema.rs b/crates/validator/src/db/schema.rs index 9742403abb..bc62dc059d 100644 --- a/crates/validator/src/db/schema.rs +++ b/crates/validator/src/db/schema.rs @@ -1,7 +1,7 @@ diesel::table! { - transactions (id, account_id, summary) { + validated_transactions (id, account_id, info) { id -> Binary, account_id -> Binary, - summary -> Binary, + info -> Binary, } } diff --git a/crates/validator/src/tx_validation/mod.rs b/crates/validator/src/tx_validation/mod.rs index 6e1cad49dc..a7ea1d05c8 100644 --- a/crates/validator/src/tx_validation/mod.rs +++ b/crates/validator/src/tx_validation/mod.rs @@ -1,13 +1,10 @@ mod data_store; +mod info; pub use data_store::TransactionInputsDataStore; -use miden_protocol::transaction::{ - ProvenTransaction, - TransactionHeader, - TransactionInputs, - TransactionSummary, -}; -use miden_protocol::{MIN_PROOF_SECURITY_LEVEL, Word}; +pub use info::ValidatedTransactionInfo; +use miden_protocol::MIN_PROOF_SECURITY_LEVEL; +use miden_protocol::transaction::{ProvenTransaction, TransactionHeader, TransactionInputs}; use miden_tx::auth::UnreachableAuth; use miden_tx::{TransactionExecutor, TransactionExecutorError, TransactionVerifier}; use tracing::{Instrument, info_span}; @@ -38,7 +35,7 @@ pub enum TransactionValidationError { pub async fn validate_transaction( proven_tx: ProvenTransaction, tx_inputs: TransactionInputs, -) -> Result { +) -> Result { // First, verify the transaction proof info_span!("verify").in_scope(|| { let tx_verifier = TransactionVerifier::new(MIN_PROOF_SECURITY_LEVEL); @@ -61,12 +58,7 @@ pub async fn validate_transaction( let executed_tx_header: TransactionHeader = (&executed_tx).into(); let proven_tx_header: TransactionHeader = (&proven_tx).into(); if executed_tx_header == proven_tx_header { - let account_delta = executed_tx.account_delta().clone(); - let input_notes = executed_tx.input_notes().clone(); - let output_notes = executed_tx.output_notes().clone(); - let salt = Word::empty(); - let summary = TransactionSummary::new(account_delta, input_notes, output_notes, salt); - Ok(summary) + Ok(executed_tx.into()) } else { Err(TransactionValidationError::Mismatch { proven_tx_header: proven_tx_header.into(), From 3af9c0a2e91e44542f939a6b880b3681a786d232 Mon Sep 17 00:00:00 2001 From: sergerad Date: Mon, 9 Feb 2026 12:05:35 +1300 Subject: [PATCH 54/72] Missing file --- crates/validator/src/tx_validation/info.rs | 68 ++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 crates/validator/src/tx_validation/info.rs diff --git a/crates/validator/src/tx_validation/info.rs b/crates/validator/src/tx_validation/info.rs new file mode 100644 index 0000000000..0eb65094bb --- /dev/null +++ b/crates/validator/src/tx_validation/info.rs @@ -0,0 +1,68 @@ +use miden_protocol::Word; +use miden_protocol::account::{AccountDelta, AccountId}; +use miden_protocol::transaction::{ExecutedTransaction, InputNote, InputNotes, OutputNotes}; +use miden_tx::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}; + +/// Contains the information about a validated transaction that the Validator, or some ad-hoc +/// auditing procedure, might require. +/// +/// Constructed from an [`ExecutedTransaction`] that the Validator would have created while +/// validating a [`miden_protocol::transaction::ProvenTransaction`]. +pub struct ValidatedTransactionInfo { + initial_account_state_commitment: Word, + final_account_state_commitment: Word, + account_delta: AccountDelta, + input_notes: InputNotes, + output_notes: OutputNotes, +} + +impl ValidatedTransactionInfo { + /// Returns a reference to the account Id pertaining to the validated transaction. + pub fn account_id(&self) -> AccountId { + self.account_delta.id() + } +} + +impl From for ValidatedTransactionInfo { + fn from(executed_tx: ExecutedTransaction) -> Self { + let initial_account_state_commitment = executed_tx.initial_account().initial_commitment(); + let final_account_state_commitment = executed_tx.final_account().commitment(); + let (tx_inputs, tx_outputs, account_delta, _) = executed_tx.into_parts(); + let (_, _, _, input_notes, _) = tx_inputs.into_parts(); + let output_notes = tx_outputs.output_notes; + Self { + initial_account_state_commitment, + final_account_state_commitment, + account_delta, + input_notes, + output_notes, + } + } +} + +impl Serializable for ValidatedTransactionInfo { + fn write_into(&self, target: &mut W) { + target.write(self.initial_account_state_commitment); + target.write(self.final_account_state_commitment); + target.write(&self.account_delta); + target.write(&self.input_notes); + target.write(&self.output_notes); + } +} + +impl Deserializable for ValidatedTransactionInfo { + fn read_from(source: &mut R) -> Result { + let initial_account_state_commitment = source.read()?; + let final_account_state_commitment = source.read()?; + let account_delta = source.read()?; + let input_notes = source.read()?; + let output_notes = source.read()?; + Ok(Self { + initial_account_state_commitment, + final_account_state_commitment, + account_delta, + input_notes, + output_notes, + }) + } +} From a6bf55d810e35b1fa97a3844818950e6fff8a9b5 Mon Sep 17 00:00:00 2001 From: sergerad Date: Mon, 9 Feb 2026 12:57:06 +1300 Subject: [PATCH 55/72] Fix comment --- crates/validator/src/db/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/validator/src/db/mod.rs b/crates/validator/src/db/mod.rs index afc2cbb7e8..7086529625 100644 --- a/crates/validator/src/db/mod.rs +++ b/crates/validator/src/db/mod.rs @@ -38,9 +38,9 @@ pub async fn load(database_filepath: PathBuf) -> Result Result { - let row = ValidatedTransactionInfoRowInsert::new(tx_id, summary); + let row = ValidatedTransactionInfoRowInsert::new(tx_id, tx_info); let count = diesel::insert_into(schema::validated_transactions::table) .values(row) .execute(conn)?; @@ -57,7 +57,7 @@ pub(crate) fn insert_transaction( /// SELECT /// id /// FROM -/// transactions +/// validated_transactions /// WHERE /// id IN (?, ...) /// ORDER BY From a7a24552e99d4467742905273ed87da3ab05a303 Mon Sep 17 00:00:00 2001 From: sergerad Date: Mon, 9 Feb 2026 12:58:33 +1300 Subject: [PATCH 56/72] Fix var name --- crates/validator/src/server/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/validator/src/server/mod.rs b/crates/validator/src/server/mod.rs index af840ec8ae..483a420ef0 100644 --- a/crates/validator/src/server/mod.rs +++ b/crates/validator/src/server/mod.rs @@ -146,13 +146,13 @@ impl api_server::Api for ValidatorServer // Validate the transaction. let id = tx.id(); - let tx_summary = validate_transaction(tx, inputs).await.map_err(|err| { + let tx_info = validate_transaction(tx, inputs).await.map_err(|err| { Status::invalid_argument(err.as_report_context("Invalid transaction")) })?; // Store the validated transaction. self.db - .transact("insert_transaction", move |conn| insert_transaction(conn, &id, &tx_summary)) + .transact("insert_transaction", move |conn| insert_transaction(conn, &id, &tx_info)) .await?; Ok(tonic::Response::new(())) } From e60ab9bbb99e5cf8ce34cb04663bdd1a23be14f1 Mon Sep 17 00:00:00 2001 From: sergerad Date: Mon, 9 Feb 2026 13:03:22 +1300 Subject: [PATCH 57/72] Fix changelog --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 226b98ae6f..329248d821 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,9 @@ ### Enhancements -- [BREAKING] Updated miden-base dependencies to use `next` branch; renamed `NoteInputs` to `NoteStorage`, `.inputs()` to `.storage()`, and database `inputs` column to `storage` ([#1595](https://github.com/0xMiden/miden-node/pull/1595)). -- Added sqlite database to Validator for transaction persistence ([#1614](https://github.com/0xMiden/miden-node/pull/1614)). - [BREAKING] Move block proving from Blocker Producer to the Store ([#1579](https://github.com/0xMiden/miden-node/pull/1579)). - [BREAKING] Updated miden-base dependencies to use `next` branch; renamed `NoteInputs` to `NoteStorage`, `.inputs()` to `.storage()`, and database `inputs` column to `storage` ([#1595](https://github.com/0xMiden/miden-node/pull/1595)). +- Added sqlite database to Validator for transaction persistence ([#1614](https://github.com/0xMiden/miden-node/pull/1614)). ### Changes From adb60b3fdb3edb48c76432b90c9a448b77cc02e5 Mon Sep 17 00:00:00 2001 From: sergerad Date: Wed, 11 Feb 2026 09:55:31 +1300 Subject: [PATCH 58/72] Toml --- Cargo.toml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 016fece172..efe275a1a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,6 +69,11 @@ anyhow = { version = "1.0" } assert_matches = { version = "1.5" } async-trait = { version = "0.1" } clap = { features = ["derive"], version = "4.5" } +deadpool = { default-features = false, features = ["managed", "rt_tokio_1"], version = "0.12" } +deadpool-diesel = { features = ["sqlite"], version = "0.6" } +deadpool-sync = { default-features = false, features = ["tracing"], version = "0.1" } +diesel = { features = ["numeric", "sqlite"], version = "2.3" } +diesel_migrations = { features = ["sqlite"], version = "2.3" } fs-err = { version = "3" } futures = { version = "0.3" } hex = { version = "0.4" } @@ -78,13 +83,6 @@ indexmap = { version = "2.12" } itertools = { version = "0.14" } lru = { default-features = false, version = "0.16" } pretty_assertions = { version = "1.4" } -# breaking change `DecodeError::new` is not exposed anymore -# but is assumed public by some internal dependency -deadpool = { default-features = false, features = ["managed", "rt_tokio_1"], version = "0.12" } -deadpool-diesel = { features = ["sqlite"], version = "0.6" } -deadpool-sync = { default-features = false, features = ["tracing"], version = "0.1" } -diesel = { features = ["numeric", "sqlite"], version = "2.3" } -diesel_migrations = { features = ["sqlite"], version = "2.3" } # prost and protox are from different authors and are _not_ released in # lockstep, nor are they adhering to semver semantics. We keep this # to avoid future breakage. From 00e45be457d0cdc86871f8e32f9050db5920d167 Mon Sep 17 00:00:00 2001 From: sergerad Date: Wed, 11 Feb 2026 09:56:33 +1300 Subject: [PATCH 59/72] Revert dockerfile --- bin/node/Dockerfile | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/bin/node/Dockerfile b/bin/node/Dockerfile index a6dab8fde5..9778daec80 100644 --- a/bin/node/Dockerfile +++ b/bin/node/Dockerfile @@ -3,14 +3,14 @@ FROM rust:1.90-slim-bullseye AS chef RUN apt-get update && \ apt-get -y upgrade && \ apt-get install -y \ - llvm \ - clang \ - libclang-dev \ - cmake \ - pkg-config \ - libssl-dev \ - libsqlite3-dev \ - ca-certificates && \ + llvm \ + clang \ + libclang-dev \ + cmake \ + pkg-config \ + libssl-dev \ + libsqlite3-dev \ + ca-certificates && \ rm -rf /var/lib/apt/lists/* RUN cargo install cargo-chef WORKDIR /app From 5ca7001190726ec13b7016ea6e4a3e68ccb0494b Mon Sep 17 00:00:00 2001 From: sergerad Date: Wed, 11 Feb 2026 11:28:46 +1300 Subject: [PATCH 60/72] Add block_num col and index + ValidatedTransactionInfoBlob --- .../db/migrations/2025062000000_setup/up.sql | 4 +- crates/validator/src/db/mod.rs | 3 +- crates/validator/src/db/models.rs | 8 ++-- crates/validator/src/db/schema.rs | 3 +- crates/validator/src/server/mod.rs | 3 +- crates/validator/src/tx_validation/info.rs | 47 ++++++++++++++++--- 6 files changed, 52 insertions(+), 16 deletions(-) diff --git a/crates/validator/src/db/migrations/2025062000000_setup/up.sql b/crates/validator/src/db/migrations/2025062000000_setup/up.sql index 4f34a3a635..9039590d51 100644 --- a/crates/validator/src/db/migrations/2025062000000_setup/up.sql +++ b/crates/validator/src/db/migrations/2025062000000_setup/up.sql @@ -1,8 +1,10 @@ CREATE TABLE validated_transactions ( id BLOB NOT NULL, + block_num INTEGER NOT NULL, account_id BLOB NOT NULL, info BLOB NOT NULL, -- Binary encoded ValidatedTransactionInfo. PRIMARY KEY (id) ) WITHOUT ROWID; -CREATE INDEX idx__validated_transactions_account_id ON validated_transactions(account_id); +CREATE INDEX idx_validated_transactions_account_id ON validated_transactions(account_id); +CREATE INDEX idx_validated_transactions_block_num ON validated_transactions(block_num); diff --git a/crates/validator/src/db/mod.rs b/crates/validator/src/db/mod.rs index 7086529625..d81e85e3f9 100644 --- a/crates/validator/src/db/mod.rs +++ b/crates/validator/src/db/mod.rs @@ -37,10 +37,9 @@ pub async fn load(database_filepath: PathBuf) -> Result Result { - let row = ValidatedTransactionInfoRowInsert::new(tx_id, tx_info); + let row = ValidatedTransactionInfoRowInsert::new(tx_info); let count = diesel::insert_into(schema::validated_transactions::table) .values(row) .execute(conn)?; diff --git a/crates/validator/src/db/models.rs b/crates/validator/src/db/models.rs index ddac5f4482..b021a73a1c 100644 --- a/crates/validator/src/db/models.rs +++ b/crates/validator/src/db/models.rs @@ -1,5 +1,5 @@ use diesel::prelude::*; -use miden_protocol::transaction::TransactionId; +use miden_node_store::SqlTypeConvert; use miden_tx::utils::Serializable; use crate::db::schema; @@ -10,14 +10,16 @@ use crate::tx_validation::ValidatedTransactionInfo; #[diesel(check_for_backend(diesel::sqlite::Sqlite))] pub struct ValidatedTransactionInfoRowInsert { pub id: Vec, + pub block_num: i64, pub account_id: Vec, pub info: Vec, } impl ValidatedTransactionInfoRowInsert { - pub fn new(id: &TransactionId, info: &ValidatedTransactionInfo) -> Self { + pub fn new(info: &ValidatedTransactionInfo) -> Self { Self { - id: id.to_bytes(), + id: info.tx_id().to_bytes(), + block_num: info.block_num().to_raw_sql(), account_id: info.account_id().to_bytes(), info: info.to_bytes(), } diff --git a/crates/validator/src/db/schema.rs b/crates/validator/src/db/schema.rs index bc62dc059d..e1ef640a7d 100644 --- a/crates/validator/src/db/schema.rs +++ b/crates/validator/src/db/schema.rs @@ -1,6 +1,7 @@ diesel::table! { - validated_transactions (id, account_id, info) { + validated_transactions (id, block_num, account_id, info) { id -> Binary, + block_num -> BigInt, account_id -> Binary, info -> Binary, } diff --git a/crates/validator/src/server/mod.rs b/crates/validator/src/server/mod.rs index 483a420ef0..5d4b0727e2 100644 --- a/crates/validator/src/server/mod.rs +++ b/crates/validator/src/server/mod.rs @@ -145,14 +145,13 @@ impl api_server::Api for ValidatorServer tracing::Span::current().set_attribute("transaction.id", tx.id()); // Validate the transaction. - let id = tx.id(); let tx_info = validate_transaction(tx, inputs).await.map_err(|err| { Status::invalid_argument(err.as_report_context("Invalid transaction")) })?; // Store the validated transaction. self.db - .transact("insert_transaction", move |conn| insert_transaction(conn, &id, &tx_info)) + .transact("insert_transaction", move |conn| insert_transaction(conn, &tx_info)) .await?; Ok(tonic::Response::new(())) } diff --git a/crates/validator/src/tx_validation/info.rs b/crates/validator/src/tx_validation/info.rs index 0eb65094bb..fc159b4771 100644 --- a/crates/validator/src/tx_validation/info.rs +++ b/crates/validator/src/tx_validation/info.rs @@ -1,6 +1,13 @@ use miden_protocol::Word; use miden_protocol::account::{AccountDelta, AccountId}; -use miden_protocol::transaction::{ExecutedTransaction, InputNote, InputNotes, OutputNotes}; +use miden_protocol::block::BlockNumber; +use miden_protocol::transaction::{ + ExecutedTransaction, + InputNote, + InputNotes, + OutputNotes, + TransactionId, +}; use miden_tx::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}; /// Contains the information about a validated transaction that the Validator, or some ad-hoc @@ -9,6 +16,14 @@ use miden_tx::utils::{ByteReader, ByteWriter, Deserializable, DeserializationErr /// Constructed from an [`ExecutedTransaction`] that the Validator would have created while /// validating a [`miden_protocol::transaction::ProvenTransaction`]. pub struct ValidatedTransactionInfo { + tx_id: TransactionId, + block_num: BlockNumber, + blob: ValidatedTransactionInfoBlob, +} + +/// The data of [`ValidatedTransactionInfo`] which is serialized into a byte array and stored as a +/// column in the `validated_transactions` table. +struct ValidatedTransactionInfoBlob { initial_account_state_commitment: Word, final_account_state_commitment: Word, account_delta: AccountDelta, @@ -17,30 +32,48 @@ pub struct ValidatedTransactionInfo { } impl ValidatedTransactionInfo { - /// Returns a reference to the account Id pertaining to the validated transaction. + /// Returns ID of the transaction. + pub fn tx_id(&self) -> TransactionId { + self.tx_id + } + + /// Returns the block number in which the transaction was executed. + pub fn block_num(&self) -> BlockNumber { + self.block_num + } + + /// Returns ID of the account against which this transaction was executed. pub fn account_id(&self) -> AccountId { - self.account_delta.id() + self.blob.account_delta.id() + } + + /// Returns the binary representation of the transaction info. + pub fn to_bytes(&self) -> Vec { + self.blob.to_bytes() } } impl From for ValidatedTransactionInfo { fn from(executed_tx: ExecutedTransaction) -> Self { + let tx_id = executed_tx.id(); + let block_num = executed_tx.block_header().block_num(); let initial_account_state_commitment = executed_tx.initial_account().initial_commitment(); let final_account_state_commitment = executed_tx.final_account().commitment(); let (tx_inputs, tx_outputs, account_delta, _) = executed_tx.into_parts(); let (_, _, _, input_notes, _) = tx_inputs.into_parts(); let output_notes = tx_outputs.output_notes; - Self { + let blob = ValidatedTransactionInfoBlob { initial_account_state_commitment, final_account_state_commitment, account_delta, input_notes, output_notes, - } + }; + Self { tx_id, block_num, blob } } } -impl Serializable for ValidatedTransactionInfo { +impl Serializable for ValidatedTransactionInfoBlob { fn write_into(&self, target: &mut W) { target.write(self.initial_account_state_commitment); target.write(self.final_account_state_commitment); @@ -50,7 +83,7 @@ impl Serializable for ValidatedTransactionInfo { } } -impl Deserializable for ValidatedTransactionInfo { +impl Deserializable for ValidatedTransactionInfoBlob { fn read_from(source: &mut R) -> Result { let initial_account_state_commitment = source.read()?; let final_account_state_commitment = source.read()?; From 00816375f42ca519e3de30285a94b15513f9a08d Mon Sep 17 00:00:00 2001 From: sergerad Date: Wed, 11 Feb 2026 13:39:42 +1300 Subject: [PATCH 61/72] Fix comment --- crates/validator/src/db/migrations/2025062000000_setup/up.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/validator/src/db/migrations/2025062000000_setup/up.sql b/crates/validator/src/db/migrations/2025062000000_setup/up.sql index 9039590d51..48bb11bd6c 100644 --- a/crates/validator/src/db/migrations/2025062000000_setup/up.sql +++ b/crates/validator/src/db/migrations/2025062000000_setup/up.sql @@ -2,7 +2,7 @@ CREATE TABLE validated_transactions ( id BLOB NOT NULL, block_num INTEGER NOT NULL, account_id BLOB NOT NULL, - info BLOB NOT NULL, -- Binary encoded ValidatedTransactionInfo. + info BLOB NOT NULL, -- Binary encoded ValidatedTransactionInfoBlob. PRIMARY KEY (id) ) WITHOUT ROWID; From f3d1cfdc59b6db69ea3eef0be13d6b26648be84b Mon Sep 17 00:00:00 2001 From: sergerad Date: Fri, 13 Feb 2026 08:35:26 +1300 Subject: [PATCH 62/72] Update changelog wording --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee3ecd34cd..260cb23373 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ - [BREAKING] Move block proving from Blocker Producer to the Store ([#1579](https://github.com/0xMiden/miden-node/pull/1579)). - [BREAKING] Updated miden-base dependencies to use `next` branch; renamed `NoteInputs` to `NoteStorage`, `.inputs()` to `.storage()`, and database `inputs` column to `storage` ([#1595](https://github.com/0xMiden/miden-node/pull/1595)). -- Added sqlite database to Validator for transaction persistence ([#1614](https://github.com/0xMiden/miden-node/pull/1614)). +- Validator now persists validated transactions ([#1614](https://github.com/0xMiden/miden-node/pull/1614)). ### Changes From 7937a7f076417bc86ec40a69836d55ff13f076a3 Mon Sep 17 00:00:00 2001 From: sergerad Date: Fri, 13 Feb 2026 09:00:17 +1300 Subject: [PATCH 63/72] Refactor validated tx info --- crates/validator/src/block_validation/mod.rs | 6 +- .../db/migrations/2025062000000_setup/up.sql | 8 +- crates/validator/src/db/mod.rs | 8 +- crates/validator/src/db/models.rs | 18 ++-- crates/validator/src/db/schema.rs | 4 +- crates/validator/src/tx_validation/info.rs | 101 ------------------ crates/validator/src/tx_validation/mod.rs | 8 +- 7 files changed, 26 insertions(+), 127 deletions(-) delete mode 100644 crates/validator/src/tx_validation/info.rs diff --git a/crates/validator/src/block_validation/mod.rs b/crates/validator/src/block_validation/mod.rs index 6cbb02c16a..07e8df449d 100644 --- a/crates/validator/src/block_validation/mod.rs +++ b/crates/validator/src/block_validation/mod.rs @@ -33,15 +33,15 @@ pub async fn validate_block( // Search for any proposed transactions that have not previously been validated. let proposed_tx_ids = proposed_block.transactions().map(TransactionHeader::id).collect::>(); - let unvalidated_transactions = db + let unvalidated_txs = db .transact("find_unvalidated_transactions", move |conn| { find_unvalidated_transactions(conn, &proposed_tx_ids) }) .await?; // All proposed transactions must have been validated. - if !unvalidated_transactions.is_empty() { - return Err(BlockValidationError::UnvalidatedTransactions(unvalidated_transactions)); + if !unvalidated_txs.is_empty() { + return Err(BlockValidationError::UnvalidatedTransactions(unvalidated_txs)); } // Build the block header. diff --git a/crates/validator/src/db/migrations/2025062000000_setup/up.sql b/crates/validator/src/db/migrations/2025062000000_setup/up.sql index 48bb11bd6c..06297a9700 100644 --- a/crates/validator/src/db/migrations/2025062000000_setup/up.sql +++ b/crates/validator/src/db/migrations/2025062000000_setup/up.sql @@ -1,8 +1,8 @@ CREATE TABLE validated_transactions ( - id BLOB NOT NULL, - block_num INTEGER NOT NULL, - account_id BLOB NOT NULL, - info BLOB NOT NULL, -- Binary encoded ValidatedTransactionInfoBlob. + id BLOB NOT NULL, + block_num INTEGER NOT NULL, + account_id BLOB NOT NULL, + "transaction" BLOB NOT NULL, -- Binary encoded ExecutedTransaction. PRIMARY KEY (id) ) WITHOUT ROWID; diff --git a/crates/validator/src/db/mod.rs b/crates/validator/src/db/mod.rs index d81e85e3f9..6092193186 100644 --- a/crates/validator/src/db/mod.rs +++ b/crates/validator/src/db/mod.rs @@ -14,8 +14,8 @@ use tracing::instrument; use crate::COMPONENT; use crate::db::migrations::apply_migrations; -use crate::db::models::ValidatedTransactionInfoRowInsert; -use crate::tx_validation::ValidatedTransactionInfo; +use crate::db::models::ValidatedTransactionRowInsert; +use crate::tx_validation::ValidatedTransaction; /// Open a connection to the DB and apply any pending migrations. #[instrument(target = COMPONENT, skip_all)] @@ -37,9 +37,9 @@ pub async fn load(database_filepath: PathBuf) -> Result Result { - let row = ValidatedTransactionInfoRowInsert::new(tx_info); + let row = ValidatedTransactionRowInsert::new(tx_info); let count = diesel::insert_into(schema::validated_transactions::table) .values(row) .execute(conn)?; diff --git a/crates/validator/src/db/models.rs b/crates/validator/src/db/models.rs index b021a73a1c..e1e67086af 100644 --- a/crates/validator/src/db/models.rs +++ b/crates/validator/src/db/models.rs @@ -3,25 +3,25 @@ use miden_node_store::SqlTypeConvert; use miden_tx::utils::Serializable; use crate::db::schema; -use crate::tx_validation::ValidatedTransactionInfo; +use crate::tx_validation::ValidatedTransaction; #[derive(Debug, Clone, PartialEq, Insertable)] #[diesel(table_name = schema::validated_transactions)] #[diesel(check_for_backend(diesel::sqlite::Sqlite))] -pub struct ValidatedTransactionInfoRowInsert { +pub struct ValidatedTransactionRowInsert { pub id: Vec, pub block_num: i64, pub account_id: Vec, - pub info: Vec, + pub transaction: Vec, } -impl ValidatedTransactionInfoRowInsert { - pub fn new(info: &ValidatedTransactionInfo) -> Self { +impl ValidatedTransactionRowInsert { + pub fn new(tx: &ValidatedTransaction) -> Self { Self { - id: info.tx_id().to_bytes(), - block_num: info.block_num().to_raw_sql(), - account_id: info.account_id().to_bytes(), - info: info.to_bytes(), + id: tx.tx_id().to_bytes(), + block_num: tx.block_num().to_raw_sql(), + account_id: tx.account_id().to_bytes(), + transaction: tx.to_bytes(), } } } diff --git a/crates/validator/src/db/schema.rs b/crates/validator/src/db/schema.rs index e1ef640a7d..0d299dbfdb 100644 --- a/crates/validator/src/db/schema.rs +++ b/crates/validator/src/db/schema.rs @@ -1,8 +1,8 @@ diesel::table! { - validated_transactions (id, block_num, account_id, info) { + validated_transactions (id, block_num, account_id, transaction) { id -> Binary, block_num -> BigInt, account_id -> Binary, - info -> Binary, + transaction -> Binary, } } diff --git a/crates/validator/src/tx_validation/info.rs b/crates/validator/src/tx_validation/info.rs deleted file mode 100644 index fc159b4771..0000000000 --- a/crates/validator/src/tx_validation/info.rs +++ /dev/null @@ -1,101 +0,0 @@ -use miden_protocol::Word; -use miden_protocol::account::{AccountDelta, AccountId}; -use miden_protocol::block::BlockNumber; -use miden_protocol::transaction::{ - ExecutedTransaction, - InputNote, - InputNotes, - OutputNotes, - TransactionId, -}; -use miden_tx::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}; - -/// Contains the information about a validated transaction that the Validator, or some ad-hoc -/// auditing procedure, might require. -/// -/// Constructed from an [`ExecutedTransaction`] that the Validator would have created while -/// validating a [`miden_protocol::transaction::ProvenTransaction`]. -pub struct ValidatedTransactionInfo { - tx_id: TransactionId, - block_num: BlockNumber, - blob: ValidatedTransactionInfoBlob, -} - -/// The data of [`ValidatedTransactionInfo`] which is serialized into a byte array and stored as a -/// column in the `validated_transactions` table. -struct ValidatedTransactionInfoBlob { - initial_account_state_commitment: Word, - final_account_state_commitment: Word, - account_delta: AccountDelta, - input_notes: InputNotes, - output_notes: OutputNotes, -} - -impl ValidatedTransactionInfo { - /// Returns ID of the transaction. - pub fn tx_id(&self) -> TransactionId { - self.tx_id - } - - /// Returns the block number in which the transaction was executed. - pub fn block_num(&self) -> BlockNumber { - self.block_num - } - - /// Returns ID of the account against which this transaction was executed. - pub fn account_id(&self) -> AccountId { - self.blob.account_delta.id() - } - - /// Returns the binary representation of the transaction info. - pub fn to_bytes(&self) -> Vec { - self.blob.to_bytes() - } -} - -impl From for ValidatedTransactionInfo { - fn from(executed_tx: ExecutedTransaction) -> Self { - let tx_id = executed_tx.id(); - let block_num = executed_tx.block_header().block_num(); - let initial_account_state_commitment = executed_tx.initial_account().initial_commitment(); - let final_account_state_commitment = executed_tx.final_account().commitment(); - let (tx_inputs, tx_outputs, account_delta, _) = executed_tx.into_parts(); - let (_, _, _, input_notes, _) = tx_inputs.into_parts(); - let output_notes = tx_outputs.output_notes; - let blob = ValidatedTransactionInfoBlob { - initial_account_state_commitment, - final_account_state_commitment, - account_delta, - input_notes, - output_notes, - }; - Self { tx_id, block_num, blob } - } -} - -impl Serializable for ValidatedTransactionInfoBlob { - fn write_into(&self, target: &mut W) { - target.write(self.initial_account_state_commitment); - target.write(self.final_account_state_commitment); - target.write(&self.account_delta); - target.write(&self.input_notes); - target.write(&self.output_notes); - } -} - -impl Deserializable for ValidatedTransactionInfoBlob { - fn read_from(source: &mut R) -> Result { - let initial_account_state_commitment = source.read()?; - let final_account_state_commitment = source.read()?; - let account_delta = source.read()?; - let input_notes = source.read()?; - let output_notes = source.read()?; - Ok(Self { - initial_account_state_commitment, - final_account_state_commitment, - account_delta, - input_notes, - output_notes, - }) - } -} diff --git a/crates/validator/src/tx_validation/mod.rs b/crates/validator/src/tx_validation/mod.rs index a7ea1d05c8..6fa45dde15 100644 --- a/crates/validator/src/tx_validation/mod.rs +++ b/crates/validator/src/tx_validation/mod.rs @@ -1,13 +1,13 @@ mod data_store; -mod info; +mod validated_tx; pub use data_store::TransactionInputsDataStore; -pub use info::ValidatedTransactionInfo; use miden_protocol::MIN_PROOF_SECURITY_LEVEL; use miden_protocol::transaction::{ProvenTransaction, TransactionHeader, TransactionInputs}; use miden_tx::auth::UnreachableAuth; use miden_tx::{TransactionExecutor, TransactionExecutorError, TransactionVerifier}; use tracing::{Instrument, info_span}; +pub use validated_tx::ValidatedTransaction; // TRANSACTION VALIDATION ERROR // ================================================================================================ @@ -35,7 +35,7 @@ pub enum TransactionValidationError { pub async fn validate_transaction( proven_tx: ProvenTransaction, tx_inputs: TransactionInputs, -) -> Result { +) -> Result { // First, verify the transaction proof info_span!("verify").in_scope(|| { let tx_verifier = TransactionVerifier::new(MIN_PROOF_SECURITY_LEVEL); @@ -58,7 +58,7 @@ pub async fn validate_transaction( let executed_tx_header: TransactionHeader = (&executed_tx).into(); let proven_tx_header: TransactionHeader = (&proven_tx).into(); if executed_tx_header == proven_tx_header { - Ok(executed_tx.into()) + Ok(ValidatedTransaction::new(executed_tx)) } else { Err(TransactionValidationError::Mismatch { proven_tx_header: proven_tx_header.into(), From 65b5af0b2859ffebe098ff37b9f75d55778541d3 Mon Sep 17 00:00:00 2001 From: sergerad Date: Fri, 13 Feb 2026 09:02:27 +1300 Subject: [PATCH 64/72] Source errors --- crates/validator/src/block_validation/mod.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/validator/src/block_validation/mod.rs b/crates/validator/src/block_validation/mod.rs index 07e8df449d..9f68d2175d 100644 --- a/crates/validator/src/block_validation/mod.rs +++ b/crates/validator/src/block_validation/mod.rs @@ -15,9 +15,9 @@ pub enum BlockValidationError { #[error("block contains unvalidated transactions {0:?}")] UnvalidatedTransactions(Vec), #[error("failed to build block")] - BlockBuildingFailed(#[from] ProposedBlockError), + BlockBuildingFailed(#[source] ProposedBlockError), #[error("failed to select transactions")] - DatabaseError(#[from] DatabaseError), + DatabaseError(#[source] DatabaseError), } // BLOCK VALIDATION @@ -37,7 +37,8 @@ pub async fn validate_block( .transact("find_unvalidated_transactions", move |conn| { find_unvalidated_transactions(conn, &proposed_tx_ids) }) - .await?; + .await + .map_err(BlockValidationError::DatabaseError)?; // All proposed transactions must have been validated. if !unvalidated_txs.is_empty() { @@ -45,7 +46,9 @@ pub async fn validate_block( } // Build the block header. - let (header, _) = proposed_block.into_header_and_body()?; + let (header, _) = proposed_block + .into_header_and_body() + .map_err(BlockValidationError::BlockBuildingFailed)?; // Sign the header. let signature = info_span!("sign_block").in_scope(|| signer.sign(&header)); From 61a954880c4a010266033eea17420c97c5960686 Mon Sep 17 00:00:00 2001 From: sergerad Date: Fri, 13 Feb 2026 10:32:26 +1300 Subject: [PATCH 65/72] Missing file --- .../src/tx_validation/validated_tx.rs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 crates/validator/src/tx_validation/validated_tx.rs diff --git a/crates/validator/src/tx_validation/validated_tx.rs b/crates/validator/src/tx_validation/validated_tx.rs new file mode 100644 index 0000000000..3ee7dfa458 --- /dev/null +++ b/crates/validator/src/tx_validation/validated_tx.rs @@ -0,0 +1,38 @@ +use miden_protocol::account::AccountId; +use miden_protocol::block::BlockNumber; +use miden_protocol::transaction::{ExecutedTransaction, TransactionId}; +use miden_tx::utils::Serializable; + +/// Re-executed and validated transaction that the Validator, or some ad-hoc +/// auditing procedure, might need to analyze. +/// +/// Constructed from an [`ExecutedTransaction`] that the Validator would have created while +/// re-executing and validating a [`miden_protocol::transaction::ProvenTransaction`]. +pub struct ValidatedTransaction(ExecutedTransaction); + +impl ValidatedTransaction { + /// Creates a new instance of [`ValidatedTransactionInfo`]. + pub fn new(tx: ExecutedTransaction) -> Self { + Self(tx) + } + + /// Returns ID of the transaction. + pub fn tx_id(&self) -> TransactionId { + self.0.id() + } + + /// Returns the block number in which the transaction was executed. + pub fn block_num(&self) -> BlockNumber { + self.0.block_header().block_num() + } + + /// Returns ID of the account against which this transaction was executed. + pub fn account_id(&self) -> AccountId { + self.0.account_delta().id() + } + + /// Returns the binary representation of the transaction info. + pub fn to_bytes(&self) -> Vec { + self.0.to_bytes() + } +} From 806ec0509cd575fc64295ce85e37b7a4a7c6ed0a Mon Sep 17 00:00:00 2001 From: sergerad Date: Fri, 13 Feb 2026 11:36:32 +1300 Subject: [PATCH 66/72] Instrument for errors, on conflict do nothing, WAL and timeout --- crates/validator/src/db/mod.rs | 14 ++++++++++++++ crates/validator/src/server/mod.rs | 3 ++- crates/validator/src/tx_validation/mod.rs | 5 ++++- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/crates/validator/src/db/mod.rs b/crates/validator/src/db/mod.rs index 6092193186..78611b9fa4 100644 --- a/crates/validator/src/db/mod.rs +++ b/crates/validator/src/db/mod.rs @@ -31,6 +31,7 @@ pub async fn load(database_filepath: PathBuf) -> Result Result<(), DatabaseError> { + // Enable the WAL mode. This allows concurrent reads while a write is in progress. + diesel::sql_query("PRAGMA journal_mode=WAL").execute(conn)?; + + // Wait up to 5 seconds for writer locks before erroring. + diesel::sql_query("PRAGMA busy_timeout=5000").execute(conn)?; + + // Enable foreign key checks. + diesel::sql_query("PRAGMA foreign_keys=ON").execute(conn)?; + Ok(()) +} diff --git a/crates/validator/src/server/mod.rs b/crates/validator/src/server/mod.rs index 5d4b0727e2..d930090d2f 100644 --- a/crates/validator/src/server/mod.rs +++ b/crates/validator/src/server/mod.rs @@ -19,7 +19,7 @@ use tokio_stream::wrappers::TcpListenerStream; use tonic::Status; use tower_http::catch_panic::CatchPanicLayer; use tower_http::trace::TraceLayer; -use tracing::info_span; +use tracing::{info_span, instrument}; use crate::COMPONENT; use crate::block_validation::validate_block; @@ -123,6 +123,7 @@ impl api_server::Api for ValidatorServer } /// Receives a proven transaction, then validates and stores it. + #[instrument(target = COMPONENT, skip_all, err)] async fn submit_proven_transaction( &self, request: tonic::Request, diff --git a/crates/validator/src/tx_validation/mod.rs b/crates/validator/src/tx_validation/mod.rs index 6fa45dde15..f2d1250a20 100644 --- a/crates/validator/src/tx_validation/mod.rs +++ b/crates/validator/src/tx_validation/mod.rs @@ -6,9 +6,11 @@ use miden_protocol::MIN_PROOF_SECURITY_LEVEL; use miden_protocol::transaction::{ProvenTransaction, TransactionHeader, TransactionInputs}; use miden_tx::auth::UnreachableAuth; use miden_tx::{TransactionExecutor, TransactionExecutorError, TransactionVerifier}; -use tracing::{Instrument, info_span}; +use tracing::{Instrument, info_span, instrument}; pub use validated_tx::ValidatedTransaction; +use crate::COMPONENT; + // TRANSACTION VALIDATION ERROR // ================================================================================================ @@ -32,6 +34,7 @@ pub enum TransactionValidationError { /// provided proven transaction. /// /// Returns the header of the executed transaction if successful. +#[instrument(target = COMPONENT, skip_all, err)] pub async fn validate_transaction( proven_tx: ProvenTransaction, tx_inputs: TransactionInputs, From 959f7e027a98c0f3d8fb299178c34aa0aa818b76 Mon Sep 17 00:00:00 2001 From: sergerad Date: Fri, 13 Feb 2026 11:56:03 +1300 Subject: [PATCH 67/72] Instrument db fns --- crates/validator/src/block_validation/mod.rs | 4 +++- crates/validator/src/db/mod.rs | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/validator/src/block_validation/mod.rs b/crates/validator/src/block_validation/mod.rs index 9f68d2175d..143d2dee1a 100644 --- a/crates/validator/src/block_validation/mod.rs +++ b/crates/validator/src/block_validation/mod.rs @@ -3,8 +3,9 @@ use miden_protocol::block::{BlockSigner, ProposedBlock}; use miden_protocol::crypto::dsa::ecdsa_k256_keccak::Signature; use miden_protocol::errors::ProposedBlockError; use miden_protocol::transaction::{TransactionHeader, TransactionId}; -use tracing::info_span; +use tracing::{info_span, instrument}; +use crate::COMPONENT; use crate::db::find_unvalidated_transactions; // BLOCK VALIDATION ERROR @@ -25,6 +26,7 @@ pub enum BlockValidationError { /// Validates a block by checking that all transactions in the proposed block have been processed by /// the validator in the past. +#[instrument(target = COMPONENT, skip_all, err)] pub async fn validate_block( proposed_block: ProposedBlock, signer: &S, diff --git a/crates/validator/src/db/mod.rs b/crates/validator/src/db/mod.rs index 78611b9fa4..07704f32f0 100644 --- a/crates/validator/src/db/mod.rs +++ b/crates/validator/src/db/mod.rs @@ -36,6 +36,7 @@ pub async fn load(database_filepath: PathBuf) -> Result Date: Fri, 13 Feb 2026 15:02:58 +1300 Subject: [PATCH 68/72] RM unnecessary src --- crates/validator/src/db/mod.rs | 13 ------------- crates/validator/src/server/mod.rs | 5 +++-- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/crates/validator/src/db/mod.rs b/crates/validator/src/db/mod.rs index 07704f32f0..26fe77c833 100644 --- a/crates/validator/src/db/mod.rs +++ b/crates/validator/src/db/mod.rs @@ -31,7 +31,6 @@ pub async fn load(database_filepath: PathBuf) -> Result Result<(), DatabaseError> { - // Enable the WAL mode. This allows concurrent reads while a write is in progress. - diesel::sql_query("PRAGMA journal_mode=WAL").execute(conn)?; - - // Wait up to 5 seconds for writer locks before erroring. - diesel::sql_query("PRAGMA busy_timeout=5000").execute(conn)?; - - // Enable foreign key checks. - diesel::sql_query("PRAGMA foreign_keys=ON").execute(conn)?; - Ok(()) -} diff --git a/crates/validator/src/server/mod.rs b/crates/validator/src/server/mod.rs index d930090d2f..94bf413152 100644 --- a/crates/validator/src/server/mod.rs +++ b/crates/validator/src/server/mod.rs @@ -1,5 +1,6 @@ use std::net::SocketAddr; use std::path::PathBuf; +use std::sync::Arc; use std::time::Duration; use anyhow::Context; @@ -100,12 +101,12 @@ impl Validator { /// Implements the gRPC API for the validator. struct ValidatorServer { signer: S, - db: Db, + db: Arc, } impl ValidatorServer { fn new(signer: S, db: Db) -> Self { - Self { signer, db } + Self { signer, db: db.into() } } } From 4f464777d9f9cca555f49ee8a233b518c4b26915 Mon Sep 17 00:00:00 2001 From: sergerad Date: Fri, 13 Feb 2026 15:33:32 +1300 Subject: [PATCH 69/72] Add 5s pragma --- crates/store/src/db/manager.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/store/src/db/manager.rs b/crates/store/src/db/manager.rs index f1ac820df3..5ac72e0ad8 100644 --- a/crates/store/src/db/manager.rs +++ b/crates/store/src/db/manager.rs @@ -78,6 +78,11 @@ impl deadpool::managed::Manager for ConnectionManager { pub(crate) fn configure_connection_on_creation( conn: &mut SqliteConnection, ) -> Result<(), ConnectionManagerError> { + // Wait up to 5 seconds for writer locks before erroring. + diesel::sql_query("PRAGMA busy_timeout=5000") + .execute(conn) + .map_err(ConnectionManagerError::ConnectionParamSetup)?; + // Enable the WAL mode. This allows concurrent reads while the transaction is being written, // this is required for proper synchronization of the servers in-memory and on-disk // representations (see [State::apply_block]) From 989e6fcc1696faaecd2214a1e615a79dab877857 Mon Sep 17 00:00:00 2001 From: sergerad Date: Mon, 16 Feb 2026 10:49:00 +1300 Subject: [PATCH 70/72] Refactor query to exists loop --- crates/validator/src/db/mod.rs | 46 +++++++++++----------------------- 1 file changed, 15 insertions(+), 31 deletions(-) diff --git a/crates/validator/src/db/mod.rs b/crates/validator/src/db/mod.rs index 26fe77c833..14d85e34f0 100644 --- a/crates/validator/src/db/mod.rs +++ b/crates/validator/src/db/mod.rs @@ -2,14 +2,14 @@ mod migrations; mod models; mod schema; -use std::collections::HashSet; use std::path::PathBuf; use diesel::SqliteConnection; +use diesel::dsl::exists; use diesel::prelude::*; use miden_node_store::{ConnectionManager, DatabaseError, DatabaseSetupError}; use miden_protocol::transaction::TransactionId; -use miden_protocol::utils::{Deserializable, Serializable}; +use miden_protocol::utils::Serializable; use tracing::instrument; use crate::COMPONENT; @@ -55,43 +55,27 @@ pub(crate) fn insert_transaction( /// # Raw SQL /// /// ```sql -/// SELECT -/// id -/// FROM -/// validated_transactions -/// WHERE -/// id IN (?, ...) -/// ORDER BY -/// id ASC +/// SELECT EXISTS( +/// SELECT 1 +/// FROM validated_transactions +/// WHERE id = ? +/// ); /// ``` #[instrument(target = COMPONENT, skip(conn), err)] pub(crate) fn find_unvalidated_transactions( conn: &mut SqliteConnection, tx_ids: &[TransactionId], ) -> Result, DatabaseError> { - if tx_ids.is_empty() { - return Ok(Vec::new()); - } - - // Convert TransactionIds to bytes for query. - let tx_id_bytes: Vec> = tx_ids.iter().map(TransactionId::to_bytes).collect(); - - // Query the database for matching transactions ids. - let raw_transaction_ids = schema::validated_transactions::table - .select(schema::validated_transactions::id) - .filter(schema::validated_transactions::id.eq_any(tx_id_bytes)) - .order(schema::validated_transactions::id.asc()) - .load::>(conn) - .map_err(DatabaseError::from)?; - - // Find any requested ids that the database does not contain. - let validated_tx_ids = raw_transaction_ids - .into_iter() - .map(|raw_id| TransactionId::read_from_bytes(&raw_id)) - .collect::, _>>()?; let mut unvalidated_tx_ids = Vec::new(); for tx_id in tx_ids { - if !validated_tx_ids.contains(tx_id) { + // Check whether each transaction id exists in the database. + let exists = diesel::select(exists( + schema::validated_transactions::table + .filter(schema::validated_transactions::id.eq(tx_id.to_bytes())), + )) + .get_result::(conn)?; + // Record any transaction ids that do not exist. + if !exists { unvalidated_tx_ids.push(*tx_id); } } From 1ce9530cd75ad81c41115986b589a5a18c90f98a Mon Sep 17 00:00:00 2001 From: sergerad Date: Tue, 17 Feb 2026 13:39:00 +1300 Subject: [PATCH 71/72] Fix merge --- crates/store/Cargo.toml | 35 ++++++++++++++++-------------- crates/store/src/accounts/tests.rs | 1 - 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/crates/store/Cargo.toml b/crates/store/Cargo.toml index d5f50aafa4..77c0d2c1cd 100644 --- a/crates/store/Cargo.toml +++ b/crates/store/Cargo.toml @@ -15,21 +15,24 @@ version.workspace = true workspace = true [dependencies] -anyhow = { workspace = true } -deadpool = { default-features = false, features = ["managed", "rt_tokio_1"], version = "0.12" } -deadpool-diesel = { features = ["sqlite"], version = "0.6" } -deadpool-sync = { default-features = false, features = ["tracing"], version = "0.1" } -diesel = { features = ["numeric", "sqlite"], version = "2.3" } -diesel_migrations = { features = ["sqlite"], version = "2.3" } -fs-err = { workspace = true } -hex = { version = "0.4" } -indexmap = { workspace = true } -libsqlite3-sys = { workspace = true } -miden-crypto = { features = ["concurrent", "hashmaps"], workspace = true } -miden-node-proto = { workspace = true } -miden-node-proto-build = { features = ["internal"], workspace = true } -miden-node-utils = { workspace = true } -miden-standards = { workspace = true } +anyhow = { workspace = true } +deadpool = { default-features = false, features = ["managed", "rt_tokio_1"], version = "0.12" } +deadpool-diesel = { features = ["sqlite"], version = "0.6" } +deadpool-sync = { default-features = false, features = ["tracing"], version = "0.1" } +diesel = { features = ["numeric", "sqlite"], version = "2.3" } +diesel_migrations = { features = ["sqlite"], version = "2.3" } +fs-err = { workspace = true } +futures = { workspace = true } +hex = { version = "0.4" } +indexmap = { workspace = true } +libsqlite3-sys = { workspace = true } +miden-block-prover = { workspace = true } +miden-crypto = { features = ["concurrent", "hashmaps"], workspace = true } +miden-node-proto = { workspace = true } +miden-node-proto-build = { features = ["internal"], workspace = true } +miden-node-utils = { workspace = true } +miden-remote-prover-client = { workspace = true } +miden-standards = { workspace = true } # TODO remove `testing` from `miden-protocol`, required for `BlockProof::new_dummy` miden-protocol = { features = ["std", "testing"], workspace = true } pretty_assertions = { workspace = true } @@ -63,7 +66,7 @@ termtree = { version = "0.5" } [features] default = ["rocksdb"] -rocksdb = ["miden-crypto/rocksdb", "miden-node-rocksdb-cxx-linkage-fix"] +rocksdb = ["miden-crypto/rocksdb"] [[bench]] harness = false diff --git a/crates/store/src/accounts/tests.rs b/crates/store/src/accounts/tests.rs index 4514f23690..0d800696bb 100644 --- a/crates/store/src/accounts/tests.rs +++ b/crates/store/src/accounts/tests.rs @@ -1,7 +1,6 @@ //! Tests for `AccountTreeWithHistory` #[cfg(test)] -#[expect(clippy::similar_names)] #[expect(clippy::needless_range_loop)] #[expect(clippy::uninlined_format_args)] #[expect(clippy::cast_sign_loss)] From 94bed4818a9fa7555c884946f01bf0884c640090 Mon Sep 17 00:00:00 2001 From: sergerad Date: Tue, 17 Feb 2026 14:04:10 +1300 Subject: [PATCH 72/72] Lint --- crates/store/src/accounts/tests.rs | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/crates/store/src/accounts/tests.rs b/crates/store/src/accounts/tests.rs index 0d800696bb..9f7b5dcbd8 100644 --- a/crates/store/src/accounts/tests.rs +++ b/crates/store/src/accounts/tests.rs @@ -151,12 +151,12 @@ mod account_tree_with_history_tests { fn test_many_accounts_sequential_updates() { // Create 50 different account IDs let account_count = 50; - let ids: Vec<_> = (0..account_count) + let account_ids: Vec<_> = (0..account_count) .map(|i| AccountIdBuilder::new().build_with_seed([i as u8; 32])) .collect(); // Create initial state with all accounts having value [i, 0, 0, 0] - let initial_state: Vec<_> = ids + let initial_state: Vec<_> = account_ids .iter() .enumerate() .map(|(i, &id)| (id, Word::from([i as u32, 0, 0, 0]))) @@ -172,7 +172,7 @@ mod account_tree_with_history_tests { .map(|i| { let idx = ((block - 1) * 5 + i) % account_count; let new_value = Word::from([idx as u32 + block as u32 * 100, 0, 0, 0]); - (ids[idx], new_value) + (account_ids[idx], new_value) }) .collect(); hist.compute_and_apply_mutations(updates).unwrap(); @@ -183,7 +183,7 @@ mod account_tree_with_history_tests { // Check genesis state for a few accounts for i in 0..4 { - let witness = hist.open_at(ids[i], BlockNumber::GENESIS).unwrap(); + let witness = hist.open_at(account_ids[i], BlockNumber::GENESIS).unwrap(); assert_eq!( witness.state_commitment(), Word::from([i as u32, 0, 0, 0]), @@ -196,7 +196,8 @@ mod account_tree_with_history_tests { for block in 1..=num_blocks { for i in 0..5 { let idx = ((block - 1) * 5 + i) % account_count; - let witness = hist.open_at(ids[idx], BlockNumber::from(block as u32)).unwrap(); + let witness = + hist.open_at(account_ids[idx], BlockNumber::from(block as u32)).unwrap(); let expected = Word::from([idx as u32 + block as u32 * 100, 0, 0, 0]); assert_eq!( witness.state_commitment(), @@ -301,7 +302,7 @@ mod account_tree_with_history_tests { fn test_sparse_updates_many_accounts() { // Create 200 accounts but only update a few at a time let account_count = 200; - let ids: Vec<_> = (0..account_count) + let account_ids: Vec<_> = (0..account_count) .map(|i| { let mut seed = [0u8; 32]; seed[0] = i as u8; @@ -311,7 +312,7 @@ mod account_tree_with_history_tests { .collect(); // Create initial state with first 50 accounts - let initial_state: Vec<_> = ids + let initial_state: Vec<_> = account_ids .iter() .take(50) .enumerate() @@ -322,7 +323,7 @@ mod account_tree_with_history_tests { let mut hist = AccountTreeWithHistory::new(initial_tree, BlockNumber::GENESIS); // Block 1: Add 50 more accounts - let updates1: Vec<_> = ids + let updates1: Vec<_> = account_ids .iter() .skip(50) .take(50) @@ -332,7 +333,7 @@ mod account_tree_with_history_tests { hist.compute_and_apply_mutations(updates1).unwrap(); // Block 2: Update every 10th account - let updates2: Vec<_> = ids + let updates2: Vec<_> = account_ids .iter() .enumerate() .filter(|(i, _)| i % 10 == 0) @@ -342,7 +343,7 @@ mod account_tree_with_history_tests { hist.compute_and_apply_mutations(updates2).unwrap(); // Block 3: Add remaining accounts - let updates3: Vec<_> = ids + let updates3: Vec<_> = account_ids .iter() .skip(100) .enumerate() @@ -353,13 +354,13 @@ mod account_tree_with_history_tests { // Verify states at different blocks // Check genesis - first 50 accounts exist, others don't for i in 0..50 { - let witness = hist.open_at(ids[i], BlockNumber::GENESIS).unwrap(); + let witness = hist.open_at(account_ids[i], BlockNumber::GENESIS).unwrap(); assert_eq!(witness.state_commitment(), Word::from([i as u32, 0, 0, 0])); } // Check block 1 - first 100 accounts exist for i in 50..100 { - let witness = hist.open_at(ids[i], BlockNumber::from(1)).unwrap(); + let witness = hist.open_at(account_ids[i], BlockNumber::from(1)).unwrap(); assert_eq!(witness.state_commitment(), Word::from([i as u32, 1, 0, 0])); } @@ -367,14 +368,14 @@ mod account_tree_with_history_tests { for i in 0..10 { let idx = i * 10; if idx < 100 { - let witness = hist.open_at(ids[idx], BlockNumber::from(2)).unwrap(); + let witness = hist.open_at(account_ids[idx], BlockNumber::from(2)).unwrap(); assert_eq!(witness.state_commitment(), Word::from([idx as u32, 2, 0, 0])); } } // Check block 3 - all 200 accounts should be accessible for i in [0, 50, 100, 150, 199] { - let witness = hist.open_at(ids[i], BlockNumber::from(3)); + let witness = hist.open_at(account_ids[i], BlockNumber::from(3)); assert!(witness.is_some(), "Account {} should exist at block 3", i); } }