Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
Comment thread
Mirko-von-Leipzig marked this conversation as resolved.
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)).

Comment thread
bobbinth marked this conversation as resolved.
## 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
77 changes: 54 additions & 23 deletions bin/validator/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,26 @@ 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, ValidatorEncryptor, 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 +122,20 @@ pub enum ValidatorCommand {
group = "key"
)]
kms_key_id: Option<String>,
Comment thread
bobbinth marked this conversation as resolved.
Outdated

/// 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,
Comment on lines +143 to +150

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.

Not for this PR, but should we also provide an option to get the key from KMS?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We just need to ensure KMS supports this key type, otherwise we cannot decrypt.

},
}

Expand Down Expand Up @@ -154,34 +174,45 @@ 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 encryptor = ValidatorEncryptor::new_local(encryption_key);
Comment thread
Mirko-von-Leipzig marked this conversation as resolved.
Outdated

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,
encryptor,
data_directory,
sqlite_connection_pool_size,
shutdown,
)
.await
},
}
}
Expand Down
4 changes: 3 additions & 1 deletion bin/validator/src/commands/start.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ use std::path::PathBuf;
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, ValidatorEncryptor, ValidatorServer, ValidatorSigner};

// Starts the validator component.
pub async fn start(
address: SocketAddr,
grpc_options: GrpcOptionsInternal,
signer: ValidatorSigner,
encryptor: ValidatorEncryptor,
data_directory: PathBuf,
sqlite_connection_pool_size: NonZeroUsize,
shutdown: CancellationToken,
Expand All @@ -22,6 +23,7 @@ pub async fn start(
address,
grpc_options,
signer,
encryptor,
data_directory,
sqlite_connection_pool_size,
}
Expand Down
2 changes: 1 addition & 1 deletion bin/validator/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ mod tx_validation;

pub use data_directory::DataDirectory;
pub use server::ValidatorServer;
pub use signers::{KmsSigner, ValidatorSigner};
pub use signers::{KmsSigner, ValidatorEncryptor, ValidatorSigner};

// CONSTANTS
// =================================================================================================
Expand Down
6 changes: 5 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, ValidatorEncryptor, ValidatorSigner};

mod validator_service;

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

/// The shared transaction encryption key used to unseal encrypted transaction inputs.
pub encryptor: ValidatorEncryptor,

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

Expand Down Expand Up @@ -98,6 +101,7 @@ impl ValidatorServer {
.add_service(validator_api::service(
ValidatorService::new(
self.signer,
self.encryptor,
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, ValidatorEncryptor};

#[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: ValidatorEncryptor::scheme_id(),
key_id: self.encryptor.key_id(),
public_key: self.encryptor.public_key().to_bytes(),
signature: self.encryption_key_attestation.to_bytes(),
Comment thread
Mirko-von-Leipzig marked this conversation as resolved.
Outdated
})
}
}
29 changes: 27 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,13 @@ 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::{COMPONENT, ValidatorEncryptor, 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 +62,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 +76,10 @@ pub enum ValidatorError {
/// Implements the gRPC API for the validator.
pub(crate) struct ValidatorService {
signer: ValidatorSigner,
encryptor: ValidatorEncryptor,
Comment thread
Mirko-von-Leipzig marked this conversation as resolved.
Outdated
/// 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 +102,7 @@ pub(crate) struct ValidatorService {
impl ValidatorService {
pub(crate) async fn new(
signer: ValidatorSigner,
encryptor: ValidatorEncryptor,
db: Database,
block_store: BlockStore,
initial_chain_tip: u32,
Expand Down Expand Up @@ -121,8 +131,23 @@ 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_attestation = signer
Comment thread
Mirko-von-Leipzig marked this conversation as resolved.
.sign_commitment(encryptor.attestation_commitment(genesis_commitment))
.await
.map_err(|err| ValidatorError::EncryptionKeyAttestationFailed(err.to_string()))?;

Ok(Self {
signer,
encryptor,
encryption_key_attestation,
serve_lock: Arc::new(tokio::sync::RwLock::new(())),
db: db.into(),
block_store,
Expand Down Expand Up @@ -254,7 +279,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