Skip to content
Merged
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
3 changes: 3 additions & 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/benchmark/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ workspace = true
anyhow = { workspace = true }
clap = { features = ["env", "string"], workspace = true }
fs-err = { workspace = true }
hex = { workspace = true }
miden-node-proto = { workspace = true }
miden-node-utils = { workspace = true }
miden-protocol = { features = ["std", "testing"], workspace = true }
Expand Down
10 changes: 7 additions & 3 deletions bin/benchmark/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,15 @@ Writes the bundle to `./benchmark-proofs/`:

```sh
miden-benchmark run-benchmark \
--rpc-url http://127.0.0.1:57291 \
--concurrency 32 \
--wait-blocks 3
--rpc-url http://127.0.0.1:57291 \
--validator-signing-public-key <HEX> \
--concurrency 32 \
--wait-blocks 3
```

The signing public key must match the validator key that signs transaction encryption key attestations. The benchmark
will not submit transactions unless it can verify the advertised encryption key.

Mints go in sequentially, then consumes with the requested concurrency, then the run waits `--wait-blocks` blocks before
scanning for inclusion. Per-phase ack rate, RPC latency percentiles, inclusion rate, and inclusion TPS are printed at
the end.
Expand Down
54 changes: 45 additions & 9 deletions bin/benchmark/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,15 @@ use std::time::Duration;
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use miden_node_proto::clients::{Builder, RpcClient};
use miden_node_proto::domain::encryption::{
TransactionInputsSealer,
TrustedTransactionEncryptionState,
verify_transaction_encryption_key,
};
use miden_node_proto::generated::rpc::BlockHeaderByNumberRequest;
use miden_protocol::Word;
use miden_protocol::block::{BlockHeader, BlockNumber};
use miden_protocol::crypto::dsa::ecdsa_k256_keccak::PublicKey as ValidatorPublicKey;
use miden_protocol::utils::serde::{Deserializable, Serializable};
use url::Url;

Expand Down Expand Up @@ -80,6 +87,10 @@ pub enum Command {
/// many blocks to fully include.
#[arg(long, default_value_t = 30)]
wait_blocks: u32,
/// Hex-encoded validator signing public key trusted to attest the transaction encryption
/// key.
#[arg(long)]
validator_signing_public_key: String,
},
}

Expand All @@ -104,8 +115,16 @@ impl Cli {
concurrency,
connections,
wait_blocks,
validator_signing_public_key,
} => {
submit::run(rpc_url, concurrency, connections, wait_blocks).await;
submit::run(
rpc_url,
concurrency,
connections,
wait_blocks,
validator_signing_public_key,
)
.await;
},
}
}
Expand All @@ -119,7 +138,7 @@ impl Cli {
async fn build_rpc_client(
rpc_url: &Url,
timeout: Duration,
genesis: Option<String>,
genesis: Option<Word>,
) -> Result<RpcClient> {
let use_tls = rpc_url.scheme() == "https";

Expand All @@ -141,9 +160,9 @@ async fn build_rpc_client(
.context("Failed to connect to RPC server")
}

/// Discover the genesis commitment (hex) of the node at `rpc_url`. This is the value write RPCs
/// such as `SubmitProvenTransaction` expect echoed back in request metadata.
async fn discover_genesis(rpc_url: &Url, timeout: Duration) -> Result<String> {
/// Discover the genesis commitment of the node at `rpc_url`. This is the value write RPCs such as
/// `SubmitProvenTransaction` expect echoed back in request metadata.
async fn discover_genesis(rpc_url: &Url, timeout: Duration) -> Result<Word> {
let mut rpc = build_rpc_client(rpc_url, timeout, None)
.await
.context("Failed to create RPC client for genesis discovery")?;
Expand All @@ -161,7 +180,7 @@ async fn discover_genesis(rpc_url: &Url, timeout: Duration) -> Result<String> {
let genesis_header: BlockHeader =
genesis_block_header.try_into().context("Failed to convert block header")?;

Ok(genesis_header.commitment().to_hex())
Ok(genesis_header.commitment())
}

/// Create an RPC client configured with the correct genesis metadata in the `Accept` header so that
Expand All @@ -179,18 +198,35 @@ pub(crate) async fn create_genesis_aware_rpc_client(
/// Genesis is discovered once and reused. Because every client owns a distinct channel, concurrent
/// submissions spread across this pool ride separate HTTP/2 sockets (and, behind a load balancer,
/// separate backend replicas) instead of multiplexing over a single connection.
///
/// The discovered genesis commitment is returned alongside the pool because submissions need it to
/// seal their transaction inputs.
pub(crate) async fn create_genesis_aware_rpc_client_pool(
rpc_url: &Url,
timeout: Duration,
size: usize,
) -> Result<Vec<RpcClient>> {
trusted_validator_signing_key: ValidatorPublicKey,
) -> Result<(Vec<RpcClient>, TransactionInputsSealer)> {
let size = size.max(1);
let genesis = discover_genesis(rpc_url, timeout).await?;
let mut pool = Vec::with_capacity(size);
for _ in 0..size {
pool.push(build_rpc_client(rpc_url, timeout, Some(genesis.clone())).await?);
pool.push(build_rpc_client(rpc_url, timeout, Some(genesis)).await?);
}
Ok(pool)
let key = pool[0]
.clone()
.get_transaction_encryption_key(())
.await
.context("Failed to fetch the transaction encryption key")?
.into_inner();
let trusted_keys = [trusted_validator_signing_key];
let verified = verify_transaction_encryption_key(
key,
TrustedTransactionEncryptionState::new(genesis, &trusted_keys),
)
.context("Untrusted transaction encryption key")?;

Ok((pool, TransactionInputsSealer::new(verified)))
}

pub(crate) fn get_genesis_header_request() -> BlockHeaderByNumberRequest {
Expand Down
45 changes: 36 additions & 9 deletions bin/benchmark/src/submit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant, SystemTime};

use miden_node_proto::clients::RpcClient;
use miden_node_proto::domain::encryption::TransactionInputsSealer;
use miden_node_proto::generated as proto;
use miden_protocol::crypto::dsa::ecdsa_k256_keccak::PublicKey as ValidatorPublicKey;
use miden_protocol::transaction::{ProvenTransaction, TransactionId};
use miden_protocol::utils::serde::Serializable;
use miden_protocol::utils::serde::{Deserializable, Serializable};
use tokio::sync::Semaphore;
use url::Url;

Expand All @@ -31,7 +33,13 @@ use crate::{PROOFS_DIR, create_genesis_aware_rpc_client_pool, read_from_file};
// ORCHESTRATOR
// ================================================================================================

pub(crate) async fn run(rpc_url: Url, concurrency: usize, connections: usize, wait_blocks: u32) {
pub(crate) async fn run(
rpc_url: Url,
concurrency: usize,
connections: usize,
wait_blocks: u32,
validator_signing_public_key: String,
) {
let in_dir = PathBuf::from(PROOFS_DIR);

println!("Loading mint txs from {}", in_dir.join("mint_txs.bin").display());
Expand All @@ -49,10 +57,21 @@ pub(crate) async fn run(rpc_url: Url, concurrency: usize, connections: usize, wa
let consume_ids: Vec<TransactionId> = consume_txs.iter().map(ProvenTransaction::id).collect();

println!("Connecting to {rpc_url} ({connections} connection(s))...");
let pool = create_genesis_aware_rpc_client_pool(&rpc_url, Duration::from_secs(30), connections)
.await
.expect("failed to create RPC client pool");
let trusted_validator_signing_key = ValidatorPublicKey::read_from_bytes(
&hex::decode(validator_signing_public_key)
.expect("validator signing public key must be a hex-encoded K256 public key"),
)
.expect("validator signing public key must be a valid K256 public key");
let (pool, sealer) = create_genesis_aware_rpc_client_pool(
&rpc_url,
Duration::from_secs(30),
connections,
trusted_validator_signing_key,
)
.await
.expect("failed to create RPC client pool");
let pool = Arc::new(pool);
let sealer = Arc::new(sealer);

let h_start = current_block_height(pool[0].clone()).await;
println!("Chain height at start: {h_start}");
Expand All @@ -62,15 +81,16 @@ pub(crate) async fn run(rpc_url: Url, concurrency: usize, connections: usize, wa
submits must be serialized for the mempool to chain them)...",
mint_txs.len()
);
let mint_stats = submit_sequential(pool[0].clone(), mint_txs, mint_tx_inputs).await;
let mint_stats = submit_sequential(pool[0].clone(), mint_txs, mint_tx_inputs, &sealer).await;
print_phase_progress("mint", &mint_stats);

println!(
"Submitting {} consume txs with concurrency={concurrency} across {} connection(s)...",
consume_txs.len(),
pool.len(),
);
let consume_stats = submit_all(pool.clone(), consume_txs, consume_tx_inputs, concurrency).await;
let consume_stats =
submit_all(pool.clone(), consume_txs, consume_tx_inputs, concurrency, &sealer).await;
print_phase_progress("consume", &consume_stats);

let ack_by_id = build_ack_map(&consume_ids, &consume_stats);
Expand Down Expand Up @@ -142,6 +162,7 @@ async fn submit_all(
txs: Vec<ProvenTransaction>,
tx_inputs: Vec<Vec<u8>>,
concurrency: usize,
sealer: &Arc<TransactionInputsSealer>,
) -> PhaseStats {
/// How many distinct error messages to surface to the console as they happen. The full failure
/// breakdown still appears in the summary.
Expand All @@ -162,10 +183,13 @@ async fn submit_all(
// sockets instead of multiplexing over one channel.
let mut client = pool[i % pool.len()].clone();
let printed = printed.clone();
let sealer = sealer.clone();
set.spawn(async move {
let sealed_inputs =
sealer.seal(tx.id(), &inputs).expect("failed to seal transaction inputs");
let request = proto::transaction::ProvenTransaction {
transaction: tx.to_bytes(),
transaction_inputs: Some(inputs),
sealed_transaction_inputs: Some(sealed_inputs),
};
let t0 = Instant::now();
let outcome = match client.submit_proven_tx(request).await {
Expand Down Expand Up @@ -213,15 +237,18 @@ async fn submit_sequential(
mut client: RpcClient,
txs: Vec<ProvenTransaction>,
tx_inputs: Vec<Vec<u8>>,
sealer: &Arc<TransactionInputsSealer>,
) -> PhaseStats {
let start = Instant::now();
let total = txs.len();
let mut outcomes = Vec::with_capacity(total);

for (i, (tx, inputs)) in txs.into_iter().zip(tx_inputs).enumerate() {
let sealed_inputs =
sealer.seal(tx.id(), &inputs).expect("failed to seal transaction inputs");
let request = proto::transaction::ProvenTransaction {
transaction: tx.to_bytes(),
transaction_inputs: Some(inputs),
sealed_transaction_inputs: Some(sealed_inputs),
};

let t0 = Instant::now();
Expand Down
4 changes: 4 additions & 0 deletions bin/network-monitor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ configured.
The monitor is an observer and test client, not a node component required for block production. Its network transaction
checks create fresh in-memory accounts on startup and do not persist account state to disk.

Network transaction checks also require `MIDEN_MONITOR_VALIDATOR_SIGNING_PUBLIC_KEY`. It must contain the hex-encoded
validator key that signs transaction encryption key attestations. The monitor will not submit a transaction unless it
can verify the advertised encryption key.
Comment on lines +27 to +29

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.

Question (relevant for some other places too): is this a temporary simplification? The monitor should be able to get validator's signing keys from the blockchains (i.e., the block headers) - right?

@huitseeker huitseeker Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes. MIDEN_MONITOR_VALIDATOR_SIGNING_PUBLIC_KEY is temporary. #2389 uses it to trust encryption-key attestations. The monitor should instead read validator signing keys from authenticated block headers.

#2390 and #2393 keep the fixed setting. After they land, #2373, including #2382, will be rebased on top. That integration should pass chain-derived validator keys to #2373’s schedule verifier and remove the environment variable. The same change should remove fixed validator keys from the other node submitters. Each submitter will then follow validator changes through trusted chain state instead of local config.

/cc @juan518munoz


Use the binary help output for the current command and configuration surface. The help output is the source of truth for
flags and environment variables.

Expand Down
26 changes: 26 additions & 0 deletions bin/network-monitor/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@

use std::time::Duration;

use anyhow::{Context, Result};
use clap::Parser;
use miden_protocol::crypto::dsa::ecdsa_k256_keccak::PublicKey as ValidatorPublicKey;
use miden_protocol::utils::serde::Deserializable;
use url::Url;

// MONITOR CONFIGURATION CONSTANTS
Expand Down Expand Up @@ -108,6 +111,16 @@ pub struct MonitorConfig {
)]
pub disable_ntx_service: bool,

/// Hex-encoded validator signing public key trusted to attest the transaction encryption key.
///
/// Required when network transaction checks are enabled.
#[arg(
long = "validator-signing-public-key",
env = "MIDEN_MONITOR_VALIDATOR_SIGNING_PUBLIC_KEY",
value_name = "HEX"
)]
pub validator_signing_public_key: Option<String>,

/// The interval at which to send the increment counter transaction.
#[arg(
long = "counter-increment-interval",
Expand Down Expand Up @@ -187,3 +200,16 @@ pub struct MonitorConfig {
)]
pub stale_chain_tip_threshold: Duration,
}

impl MonitorConfig {
/// Decodes the validator signing key required by transaction submission checks.
pub fn trusted_validator_signing_key(&self) -> Result<ValidatorPublicKey> {
let encoded = self.validator_signing_public_key.as_deref().context(
"--validator-signing-public-key is required when network transaction checks are enabled",
)?;
let bytes =
hex::decode(encoded).context("validator signing public key must be hex encoded")?;
ValidatorPublicKey::read_from_bytes(&bytes)
.context("validator signing public key must be a valid K256 public key")
}
}
Loading
Loading