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: 0 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
## Unreleased

- [BREAKING] Updated `miden-protocol` dependencies to use the `next` branch (v0.16). Block and transaction account updates now use the absolute `AccountPatch` representation instead of the relative `AccountDelta`, and the `miden-tx-batch-prover` crate was renamed to `miden-tx-batch` ([#2282](https://github.com/0xMiden/node/pull/2282)).

## v0.15.0 (2026-06-10)

- Fixed the store dropping a fungible asset's callback flag when applying partial account deltas ([#2222](https://github.com/0xMiden/node/pull/2222)).
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions bin/validator/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,6 @@ miden-node-db = { workspace = true }
miden-node-store = { workspace = true }
miden-node-utils = { features = ["testing"], workspace = true }
miden-protocol = { default-features = true, features = ["testing"], workspace = true }
rand = { workspace = true }
tempfile = { workspace = true }
tokio = { features = ["macros", "rt-multi-thread"], workspace = true }
2 changes: 1 addition & 1 deletion bin/validator/src/commands/bootstrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ async fn build_and_write_genesis(
.into_unsigned_block()
.context("failed to build the unsigned genesis block")?;
let signature = signer
.sign(unsigned_genesis_block.header())
.sign_commitment(unsigned_genesis_block.header().commitment())
.await
.context("failed to sign the genesis block")?;
let genesis_block = unsigned_genesis_block
Expand Down
85 changes: 62 additions & 23 deletions bin/validator/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,40 @@ mod start;

use std::num::NonZeroUsize;
use std::path::PathBuf;
use std::sync::Arc;

use anyhow::Context;
use clap::Parser;
use miden_node_utils::clap::GrpcOptionsInternal;
use miden_node_utils::logging::OpenTelemetry;
use miden_node_utils::shutdown::CancellationToken;
use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SigningKey;
use miden_protocol::crypto::dsa::eddsa_25519_sha512::KeyExchangeKey;
use miden_protocol::utils::serde::Deserializable;
use miden_validator::{DataDirectory, ValidatorSigner};
use miden_validator::{
DataDirectory,
LOG_TARGET,
LocalX25519TransactionInputDecryptor,
TransactionInputDecryptor,
ValidatorSigner,
};

const ENV_DATA_DIRECTORY: &str = "MIDEN_VALIDATOR_DATA_DIRECTORY";
const ENV_LISTEN: &str = "MIDEN_VALIDATOR_LISTEN";
const ENV_KEY: &str = "MIDEN_VALIDATOR_KEY";
const ENV_KMS_KEY_ID: &str = "MIDEN_VALIDATOR_KMS_KEY_ID";
const ENV_ENCRYPTION_KEY: &str = "MIDEN_VALIDATOR_ENCRYPTION_KEY";
const ENV_GENESIS_CONFIG_FILE: &str = "MIDEN_VALIDATOR_GENESIS_CONFIG_FILE";
const ENV_SQLITE_CONNECTION_POOL_SIZE: &str = "MIDEN_VALIDATOR_SQLITE_CONNECTION_POOL_SIZE";

/// A predefined, insecure validator key for development purposes.
pub(crate) const INSECURE_KEY_HEX: &str =
"0101010101010101010101010101010101010101010101010101010101010101";

/// A predefined, insecure shared transaction encryption key for development purposes.
pub(crate) const INSECURE_ENCRYPTION_KEY_HEX: &str =
"0202020202020202020202020202020202020202020202020202020202020202";

// VALIDATOR COMMAND
// ================================================================================================

Expand Down Expand Up @@ -116,6 +129,20 @@ pub enum ValidatorCommand {
group = "key"
)]
kms_key_id: Option<String>,

/// Hex-encoded shared secret of the transaction encryption key.
///
/// Unlike the per-validator signing key, this value must be identical across every
/// validator in the set.
///
/// If not provided, a predefined insecure key is used.
Comment thread
SantiagoPittella marked this conversation as resolved.
#[arg(
long = "encryption-key.hex",
env = ENV_ENCRYPTION_KEY,
value_name = "VALIDATOR_ENCRYPTION_KEY",
default_value = INSECURE_ENCRYPTION_KEY_HEX
)]
encryption_key: String,
},
}

Expand Down Expand Up @@ -154,34 +181,46 @@ impl ValidatorCommand {
data_directory,
kms_key_id,
sqlite_connection_pool_size,
encryption_key,
..
} => {
let address = listen;

if let Some(kms_key_id) = kms_key_id {
let signer = ValidatorSigner::new_kms(kms_key_id).await?;
start::start(
address,
grpc_options,
signer,
data_directory,
sqlite_connection_pool_size,
shutdown,
)
.await
// Unlike the signing key, whose insecure default is caught at startup against the
// chain's committed validator key, nothing cross-checks the encryption key. Warn
// loudly so the default never runs in production unnoticed.
if encryption_key == INSECURE_ENCRYPTION_KEY_HEX {
tracing::warn!(
target: LOG_TARGET,
"Using the predefined, insecure transaction encryption key, configure \
--encryption-key.hex for production deployments"
);
}

let encryption_key_bytes = hex::decode(encryption_key)
.context("failed to decode the encryption key hex")?;
let encryption_key = KeyExchangeKey::read_from_bytes(&encryption_key_bytes)
.context("failed to construct the encryption key")?;
let decryptor: Arc<dyn TransactionInputDecryptor> =
Arc::new(LocalX25519TransactionInputDecryptor::new(encryption_key));

let signer = if let Some(kms_key_id) = kms_key_id {
ValidatorSigner::new_kms(kms_key_id).await?
} else {
let signer = SigningKey::read_from_bytes(hex::decode(validator_key)?.as_ref())?;
let signer = ValidatorSigner::new_local(signer);
start::start(
address,
grpc_options,
signer,
data_directory,
sqlite_connection_pool_size,
shutdown,
)
.await
}
ValidatorSigner::new_local(signer)
};

start::start(
address,
grpc_options,
signer,
decryptor,
data_directory,
sqlite_connection_pool_size,
shutdown,
)
.await
},
}
}
Expand Down
5 changes: 4 additions & 1 deletion bin/validator/src/commands/start.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
use std::net::SocketAddr;
use std::num::NonZeroUsize;
use std::path::PathBuf;
use std::sync::Arc;

use anyhow::Context;
use miden_node_utils::clap::GrpcOptionsInternal;
use miden_node_utils::shutdown::CancellationToken;
use miden_validator::{DataDirectory, ValidatorServer, ValidatorSigner};
use miden_validator::{DataDirectory, TransactionInputDecryptor, ValidatorServer, ValidatorSigner};

// Starts the validator component.
pub async fn start(
address: SocketAddr,
grpc_options: GrpcOptionsInternal,
signer: ValidatorSigner,
decryptor: Arc<dyn TransactionInputDecryptor>,
data_directory: PathBuf,
sqlite_connection_pool_size: NonZeroUsize,
shutdown: CancellationToken,
Expand All @@ -22,6 +24,7 @@ pub async fn start(
address,
grpc_options,
signer,
decryptor,
data_directory,
sqlite_connection_pool_size,
}
Expand Down
9 changes: 8 additions & 1 deletion bin/validator/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@ mod tx_validation;

pub use data_directory::DataDirectory;
pub use server::ValidatorServer;
pub use signers::{KmsSigner, ValidatorSigner};
pub use signers::{
KmsSigner,
LocalX25519TransactionInputDecryptor,
TransactionEncryptionKeyInfo,
TransactionInputDecryptor,
ValidatorSigner,
attestation_commitment,
};

// CONSTANTS
// =================================================================================================
Expand Down
7 changes: 6 additions & 1 deletion bin/validator/src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use crate::db::{
load_chain_tip,
load_with_pool_size,
};
use crate::{DataDirectory, LOG_TARGET, ValidatorSigner};
use crate::{DataDirectory, LOG_TARGET, TransactionInputDecryptor, ValidatorSigner};

mod validator_service;

Expand All @@ -43,6 +43,10 @@ pub struct ValidatorServer {
/// The signer used to sign blocks.
pub signer: ValidatorSigner,

/// The decryptor for the shared transaction encryption key, used to unseal encrypted
/// transaction inputs.
pub decryptor: std::sync::Arc<dyn TransactionInputDecryptor>,

/// The data directory for the validator component's database files.
pub data_directory: DataDirectory,

Expand Down Expand Up @@ -98,6 +102,7 @@ impl ValidatorServer {
.add_service(validator_api::service(
ValidatorService::new(
self.signer,
self.decryptor,
db,
block_store,
initial_chain_tip,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use miden_node_proto::generated as grpc;
use miden_node_utils::tracing::miden_instrument;
use miden_tx::utils::serde::Serializable;

use super::ValidatorService;
use crate::COMPONENT;

#[tonic::async_trait]
impl grpc::server::validator_api::GetTransactionEncryptionKey for ValidatorService {
type Input = ();
type Output = grpc::transaction::TransactionEncryptionKey;

fn decode(request: ()) -> tonic::Result<Self::Input> {
Ok(request)
}

fn encode(output: Self::Output) -> tonic::Result<grpc::transaction::TransactionEncryptionKey> {
Ok(output)
}

#[miden_instrument(
target = COMPONENT,
name = "get_transaction_encryption_key",
skip_all,
err,
)]
async fn handle(
&self,
_input: Self::Input,
_metadata: &tonic::metadata::MetadataMap,
_extensions: &tonic::codegen::http::Extensions,
) -> tonic::Result<Self::Output> {
// Built entirely from state fixed at construction, so the endpoint stays available while a
// backup subscription holds the serve lock.
Ok(grpc::transaction::TransactionEncryptionKey {
scheme: self.encryption_key_info.scheme,
key_id: self.encryption_key_info.key_id.clone(),
public_key: self.encryption_key_info.public_key.clone(),
signature: self.encryption_key_attestation.to_bytes(),
Comment on lines +35 to +39

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The endpoint should not call ValidatorEncryptor::scheme_id() or self.encryptor.public_key().to_bytes() directly. It should return the TransactionEncryptionKeyInfo from the decryptor, plus the validator signature over the response fields ( see https://github.com/0xMiden/node/pull/2342/changes#r3604371368 ).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Tackled in cfe6987

})
}
}
39 changes: 37 additions & 2 deletions bin/validator/src/server/validator_service/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ use miden_protocol::transaction::{TransactionHeader, TransactionId};
use tokio::sync::{Semaphore, watch};

use crate::db::{find_unvalidated_transactions, load_block_header, load_chain_tip};
use crate::{COMPONENT, ValidatorSigner};
use crate::signers::TransactionEncryptionKeyInfo;
use crate::{COMPONENT, TransactionInputDecryptor, ValidatorSigner};

#[cfg(test)]
mod tests;

mod block_subscription;
mod get_transaction_encryption_key;
mod sign_block;
mod status;
mod submit_proven_transaction;
Expand Down Expand Up @@ -61,6 +63,10 @@ pub enum ValidatorError {
BlockBackupFailed(#[source] std::io::Error),
#[error("expected a single-key validator set, got {actual} keys")]
UnexpectedValidatorSetSize { actual: usize },
#[error("no genesis block header exists")]
NoGenesisHeader,
#[error("failed to attest the transaction encryption key: {0}")]
EncryptionKeyAttestationFailed(String),
}

// VALIDATOR SERVICE
Expand All @@ -71,6 +77,14 @@ pub enum ValidatorError {
/// Implements the gRPC API for the validator.
pub(crate) struct ValidatorService {
signer: ValidatorSigner,
/// Decryptor for transaction inputs sealed against the shared encryption key.
#[expect(dead_code, reason = "used by the submit path in a follow-up PR")]
decryptor: Arc<dyn TransactionInputDecryptor>,
/// Public metadata of the shared encryption key, fetched once at construction.
encryption_key_info: TransactionEncryptionKeyInfo,
/// Signature by this validator's own signing key over the encryption key attestation
/// commitment, computed once at construction.
encryption_key_attestation: Signature,
db: Arc<Database>,
block_store: BlockStore,
/// Enforces mutual exclusion between backup block subscriptions and all other RPCs. Regular
Expand All @@ -93,6 +107,7 @@ pub(crate) struct ValidatorService {
impl ValidatorService {
pub(crate) async fn new(
signer: ValidatorSigner,
decryptor: Arc<dyn TransactionInputDecryptor>,
db: Database,
block_store: BlockStore,
initial_chain_tip: u32,
Expand Down Expand Up @@ -121,8 +136,28 @@ impl ValidatorService {
});
}

// Both keys are fixed for the process lifetime, so the attestation is computed once. This
// also keeps KMS-backed signers to a single signing call.
let genesis_commitment = db
.read("load_genesis_header", |tx| load_block_header(tx, BlockNumber::GENESIS))
.await
.map_err(ValidatorError::DatabaseError)?
.ok_or(ValidatorError::NoGenesisHeader)?
.commitment();
let encryption_key_info = decryptor
.encryption_key()
.await
.map_err(|err| ValidatorError::EncryptionKeyAttestationFailed(err.to_string()))?;
let encryption_key_attestation = signer

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Similarly I think this can just sign the key metadata returned by the decryptor, rather than enforcing a call to encryptor.attestation_commitment(...) on a concrete X25519 type, which seems a bit too specific and therefore brittle.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done in cfe6987

.sign_commitment(encryption_key_info.attestation_commitment(genesis_commitment))
.await
.map_err(|err| ValidatorError::EncryptionKeyAttestationFailed(err.to_string()))?;

Ok(Self {
signer,
decryptor,
encryption_key_info,
encryption_key_attestation,
serve_lock: Arc::new(tokio::sync::RwLock::new(())),
db: db.into(),
block_store,
Expand Down Expand Up @@ -254,7 +289,7 @@ impl ValidatorService {
)]
async fn sign_header(&self, header: &BlockHeader) -> Result<Signature, ValidatorError> {
self.signer
.sign(header)
.sign_commitment(header.commitment())
.await
.map_err(|err| ValidatorError::BlockSigningFailed(err.to_string()))
}
Expand Down
Loading
Loading