Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bin/node/src/commands/bundled.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ impl BundledCommand {
crate::commands::validator::ValidatorCommand::bootstrap_genesis(
&data_directory,
&accounts_directory,
&data_directory,
genesis_config_file.as_ref(),
validator_key,
)
Expand Down
45 changes: 38 additions & 7 deletions bin/node/src/commands/validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,18 @@ pub enum ValidatorCommand {
/// Bootstraps the genesis block.
///
/// Creates accounts from the genesis configuration, builds and signs the genesis block,
/// and writes the signed block and account secret files to disk.
/// and writes the signed block and account secret files to disk. Also initializes the
/// validator's database with the genesis block as the chain tip.
Bootstrap {
/// Directory in which to write the genesis block file.
#[arg(long, value_name = "DIR")]
genesis_block_directory: PathBuf,
/// Directory to write the account secret files (.mac) to.
#[arg(long, value_name = "DIR")]
accounts_directory: PathBuf,
/// Directory in which to store the validator's database.
#[arg(long, env = ENV_DATA_DIRECTORY, value_name = "DIR")]
data_directory: PathBuf,
/// Use the given configuration file to construct the genesis state from.
#[arg(long, env = ENV_GENESIS_CONFIG_FILE, value_name = "GENESIS_CONFIG")]
genesis_config_file: Option<PathBuf>,
Expand Down Expand Up @@ -101,12 +105,14 @@ impl ValidatorCommand {
Self::Bootstrap {
genesis_block_directory,
accounts_directory,
data_directory,
genesis_config_file,
validator_key,
} => {
Self::bootstrap_genesis(
&genesis_block_directory,
&accounts_directory,
&data_directory,
genesis_config_file.as_ref(),
validator_key,
)
Expand Down Expand Up @@ -169,6 +175,7 @@ impl ValidatorCommand {
pub async fn bootstrap_genesis(
genesis_block_directory: &Path,
accounts_directory: &Path,
data_directory: &Path,
genesis_config: Option<&PathBuf>,
validator_key: ValidatorKey,
) -> anyhow::Result<()> {
Expand All @@ -191,24 +198,37 @@ impl ValidatorCommand {
let signer = validator_key.into_signer().await?;
match signer {
ValidatorSigner::Kms(signer) => {
build_and_write_genesis(config, signer, accounts_directory, genesis_block_directory)
.await
build_and_write_genesis(
config,
signer,
accounts_directory,
genesis_block_directory,
data_directory,
)
.await
},
ValidatorSigner::Local(signer) => {
build_and_write_genesis(config, signer, accounts_directory, genesis_block_directory)
.await
build_and_write_genesis(
config,
signer,
accounts_directory,
genesis_block_directory,
data_directory,
)
.await
},
}
}
}

/// Builds the genesis state, writes account secret files, signs the genesis block, and writes it
/// to disk.
/// Builds the genesis state, writes account secret files, signs the genesis block, writes it
/// to disk, and initializes the validator's database with the genesis block as the chain tip.
async fn build_and_write_genesis(
config: GenesisConfig,
signer: impl BlockSigner,
accounts_directory: &Path,
genesis_block_directory: &Path,
data_directory: &Path,
) -> anyhow::Result<()> {
// Build genesis state with the provided signer.
let (genesis_state, secrets) = config.into_state(signer)?;
Expand All @@ -235,5 +255,16 @@ async fn build_and_write_genesis(
let genesis_block_path = genesis_block_directory.join(GENESIS_BLOCK_FILENAME);
fs_err::write(&genesis_block_path, block_bytes).context("failed to write genesis block")?;

// Initialize the validator database and persist the genesis block header as the chain tip.
let (genesis_header, ..) = genesis_block.into_inner().into_parts();
let db = miden_node_validator::db::load(data_directory.join("validator.sqlite3"))
.await
.context("failed to initialize validator database during bootstrap")?;
db.transact("upsert_block_header", move |conn| {
miden_node_validator::db::upsert_block_header(conn, &genesis_header)
})
.await
.context("failed to persist genesis block header as chain tip")?;

Ok(())
}
2 changes: 2 additions & 0 deletions crates/db/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ pub enum DatabaseError {
inner: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
to: &'static str,
},
#[error("conflict: {0}")]
Conflict(String),
#[error(transparent)]
Diesel(#[from] diesel::result::Error),
#[error("schema verification failed")]
Expand Down
80 changes: 66 additions & 14 deletions crates/validator/src/block_validation/mod.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use miden_node_db::{DatabaseError, Db};
use miden_protocol::block::ProposedBlock;
use miden_protocol::block::{BlockHeader, BlockNumber, 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, instrument};
use tracing::instrument;

use crate::db::find_unvalidated_transactions;
use crate::db::{find_unvalidated_transactions, load_block_header};
use crate::{COMPONENT, ValidatorSigner};

// BLOCK VALIDATION ERROR
Expand All @@ -21,19 +21,35 @@ pub enum BlockValidationError {
BlockSigningFailed(String),
#[error("failed to select transactions")]
DatabaseError(#[source] DatabaseError),
#[error("block number mismatch: expected {expected}, got {actual}")]
BlockNumberMismatch {
expected: BlockNumber,
actual: BlockNumber,
},
#[error("previous block commitment does not match chain tip")]
PrevBlockCommitmentMismatch,
#[error("no previous block header available for chain tip overwrite")]
NoPrevBlockHeader,
}

// BLOCK VALIDATION
// ================================================================================================

/// 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)]
/// Validates a proposed block by checking:
/// 1. All transactions have been previously validated by this validator.
/// 2. The block header can be successfully built from the proposed block.
/// 3. The block is either: a. The valid next block in the chain (sequential block number, matching
/// previous block commitment), or b. A replacement block at the same height as the current chain
/// tip, validated against the previous block header.
///
/// On success, returns the signature and the validated block header.
#[instrument(target = COMPONENT, skip_all, err, fields(chain_tip = chain_tip.block_num().as_u32()))]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also log block.number and block.commitment

Copy link
Collaborator Author

@sergerad sergerad Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

those aren't available from ProposedBlock, if thats what you mean. I could add an event in the fn? After

    let (proposed_header, _) = proposed_block

pub async fn validate_block(
proposed_block: ProposedBlock,
signer: &ValidatorSigner,
db: &Db,
) -> Result<Signature, BlockValidationError> {
chain_tip: BlockHeader,
) -> Result<(Signature, BlockHeader), BlockValidationError> {
// Search for any proposed transactions that have not previously been validated.
let proposed_tx_ids =
proposed_block.transactions().map(TransactionHeader::id).collect::<Vec<_>>();
Expand All @@ -50,15 +66,51 @@ pub async fn validate_block(
}

// Build the block header.
let (header, _) = proposed_block
let (proposed_header, _) = proposed_block
.into_header_and_body()
.map_err(BlockValidationError::BlockBuildingFailed)?;

// Sign the header.
let signature = info_span!("sign_block")
.in_scope(async move || signer.sign(&header).await)
.await
.map_err(|err| BlockValidationError::BlockSigningFailed(err.to_string()))?;
// If the proposed block has the same block number as the current chain tip, this is a
// replacement block. Validate it against the previous block header.
let prev = if proposed_header.block_num() == chain_tip.block_num() {
let prev_block_num =
chain_tip.block_num().parent().ok_or(BlockValidationError::NoPrevBlockHeader)?;
db.query("load_block_header", move |conn| load_block_header(conn, prev_block_num))
.await
.map_err(BlockValidationError::DatabaseError)?
.ok_or(BlockValidationError::NoPrevBlockHeader)?
} else {
// Proposed block is a new block.
// Block number must be sequential.
let expected_block_num = chain_tip.block_num().child();
if proposed_header.block_num() != expected_block_num {
return Err(BlockValidationError::BlockNumberMismatch {
expected: expected_block_num,
actual: proposed_header.block_num(),
});
}
// Current chain tip is the parent of the proposed block.
chain_tip
};

Ok(signature)
// The proposed block's parent must match the block that the Validator has determined is its
// parent (either chain tip or parent of chain tip).
if proposed_header.prev_block_commitment() != prev.commitment() {
return Err(BlockValidationError::PrevBlockCommitmentMismatch);
}

let signature = sign_header(signer, &proposed_header).await?;
Ok((signature, proposed_header))
}

/// Signs a block header using the validator's signer.
#[instrument(target = COMPONENT, name = "sign_block", skip_all, err, fields(block.number = header.block_num().as_u32()))]
async fn sign_header(
signer: &ValidatorSigner,
header: &BlockHeader,
) -> Result<Signature, BlockValidationError> {
signer
.sign(header)
.await
.map_err(|err| BlockValidationError::BlockSigningFailed(err.to_string()))
}
5 changes: 5 additions & 0 deletions crates/validator/src/db/migrations/2025062000000_setup/up.sql
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,8 @@ CREATE TABLE validated_transactions (

CREATE INDEX idx_validated_transactions_account_id ON validated_transactions(account_id);
CREATE INDEX idx_validated_transactions_block_num ON validated_transactions(block_num);

CREATE TABLE block_headers (
block_num INTEGER PRIMARY KEY,
block_header BLOB NOT NULL
) WITHOUT ROWID;
63 changes: 60 additions & 3 deletions crates/validator/src/db/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ use std::path::PathBuf;
use diesel::SqliteConnection;
use diesel::dsl::exists;
use diesel::prelude::*;
use miden_node_db::{DatabaseError, Db};
use miden_node_db::{DatabaseError, Db, SqlTypeConvert};
use miden_protocol::block::{BlockHeader, BlockNumber};
use miden_protocol::transaction::TransactionId;
use miden_protocol::utils::Serializable;
use miden_protocol::utils::{Deserializable, Serializable};
use tracing::instrument;

use crate::COMPONENT;
use crate::db::migrations::apply_migrations;
use crate::db::models::ValidatedTransactionRowInsert;
use crate::db::models::{BlockHeaderRowInsert, ValidatedTransactionRowInsert};
use crate::tx_validation::ValidatedTransaction;

/// Open a connection to the DB and apply any pending migrations.
Expand Down Expand Up @@ -78,3 +79,59 @@ pub(crate) fn find_unvalidated_transactions(
}
Ok(unvalidated_tx_ids)
}

/// Upserts a block header into the database.
///
/// Inserts a new row if no block header exists at the given block number, or replaces the
/// existing block header if one already exists.
#[instrument(target = COMPONENT, skip(conn, header), err)]
pub fn upsert_block_header(
conn: &mut SqliteConnection,
header: &BlockHeader,
) -> Result<(), DatabaseError> {
let row = BlockHeaderRowInsert {
block_num: header.block_num().to_raw_sql(),
block_header: header.to_bytes(),
};
diesel::replace_into(schema::block_headers::table).values(row).execute(conn)?;
Ok(())
}

/// Loads the chain tip (block header with the highest block number) from the database.
///
/// Returns `None` if no block headers have been persisted (i.e. bootstrap has not been run).
#[instrument(target = COMPONENT, skip(conn), err)]
pub fn load_chain_tip(conn: &mut SqliteConnection) -> Result<Option<BlockHeader>, DatabaseError> {
let row = schema::block_headers::table
.order(schema::block_headers::block_num.desc())
.select(schema::block_headers::block_header)
.first::<Vec<u8>>(conn)
.optional()?;

row.map(|bytes| {
BlockHeader::read_from_bytes(&bytes)
.map_err(|err| DatabaseError::deserialization("BlockHeader", err))
})
.transpose()
}

/// Loads a block header by its block number.
///
/// Returns `None` if no block header exists at the given block number.
#[instrument(target = COMPONENT, skip(conn), err)]
pub fn load_block_header(
conn: &mut SqliteConnection,
block_num: BlockNumber,
) -> Result<Option<BlockHeader>, DatabaseError> {
let row = schema::block_headers::table
.filter(schema::block_headers::block_num.eq(block_num.to_raw_sql()))
.select(schema::block_headers::block_header)
.first::<Vec<u8>>(conn)
.optional()?;

row.map(|bytes| {
BlockHeader::read_from_bytes(&bytes)
.map_err(|err| DatabaseError::deserialization("BlockHeader", err))
})
.transpose()
}
8 changes: 8 additions & 0 deletions crates/validator/src/db/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ use miden_tx::utils::Serializable;
use crate::db::schema;
use crate::tx_validation::ValidatedTransaction;

#[derive(Debug, Clone, Insertable)]
#[diesel(table_name = schema::block_headers)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct BlockHeaderRowInsert {
pub block_num: i64,
pub block_header: Vec<u8>,
}

#[derive(Debug, Clone, PartialEq, Insertable)]
#[diesel(table_name = schema::validated_transactions)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
Expand Down
7 changes: 7 additions & 0 deletions crates/validator/src/db/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,10 @@ diesel::table! {
fee -> Binary,
}
}

diesel::table! {
block_headers (block_num) {
block_num -> BigInt,
block_header -> Binary,
}
}
2 changes: 1 addition & 1 deletion crates/validator/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
mod block_validation;
mod db;
pub mod db;
mod server;
mod signers;
mod tx_validation;
Expand Down
Loading
Loading