Skip to content
Closed
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
2 changes: 2 additions & 0 deletions Cargo.lock

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

17 changes: 11 additions & 6 deletions bin/benchmark/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use miden_node_proto::clients::{Builder, RpcClient};
use miden_node_proto::generated::rpc::BlockHeaderByNumberRequest;
use miden_protocol::Word;
use miden_protocol::block::{BlockHeader, BlockNumber};
use miden_protocol::utils::serde::{Deserializable, Serializable};
use url::Url;
Expand Down Expand Up @@ -143,7 +144,7 @@ async fn build_rpc_client(

/// 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> {
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 +162,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 @@ -171,26 +172,30 @@ pub(crate) async fn create_genesis_aware_rpc_client(
timeout: Duration,
) -> Result<RpcClient> {
let genesis = discover_genesis(rpc_url, timeout).await?;
build_rpc_client(rpc_url, timeout, Some(genesis)).await
build_rpc_client(rpc_url, timeout, Some(genesis.to_hex())).await
}

/// Create a pool of `size` genesis-aware RPC clients, each on its own gRPC connection.
///
/// 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>> {
) -> Result<(Vec<RpcClient>, Word)> {
let size = size.max(1);
let genesis = discover_genesis(rpc_url, timeout).await?;
let genesis_hex = genesis.to_hex();
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_hex.clone())).await?);
}
Ok(pool)
Ok((pool, genesis))
}

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

use miden_node_proto::clients::RpcClient;
use miden_node_proto::domain::encryption::TransactionInputSealer;
use miden_node_proto::generated as proto;
use miden_protocol::transaction::{ProvenTransaction, TransactionId};
use miden_protocol::utils::serde::Serializable;
Expand Down Expand Up @@ -49,11 +50,23 @@ 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 (pool, genesis_commitment) =
create_genesis_aware_rpc_client_pool(&rpc_url, Duration::from_secs(30), connections)
.await
.expect("failed to create RPC client pool");
Comment on lines +53 to +56

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.

Instead of the genesis commitment, I believe this could also just return the TransactionInputsSealer

let pool = Arc::new(pool);

let key = pool[0]
.clone()
.get_transaction_encryption_key(())
.await
.expect("failed to fetch the transaction encryption key")
.into_inner();
let sealer = Arc::new(
TransactionInputSealer::new(key, genesis_commitment)
.expect("unusable transaction encryption key"),
);

let h_start = current_block_height(pool[0].clone()).await;
println!("Chain height at start: {h_start}");

Expand All @@ -62,15 +75,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 +156,7 @@ async fn submit_all(
txs: Vec<ProvenTransaction>,
tx_inputs: Vec<Vec<u8>>,
concurrency: usize,
sealer: &Arc<TransactionInputSealer>,
) -> 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 +177,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 +231,18 @@ async fn submit_sequential(
mut client: RpcClient,
txs: Vec<ProvenTransaction>,
tx_inputs: Vec<Vec<u8>>,
sealer: &Arc<TransactionInputSealer>,
) -> 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
51 changes: 48 additions & 3 deletions bin/network-monitor/src/counter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use std::time::{Duration, Instant};

use anyhow::{Context, Result};
use miden_node_proto::clients::RpcClient;
use miden_node_proto::domain::encryption::TransactionInputSealer;
use miden_node_proto::generated::rpc::BlockHeaderByNumberRequest;
use miden_node_proto::generated::transaction::ProvenTransaction;
use miden_node_utils::spawn::spawn_blocking_in_current_span;
Expand Down Expand Up @@ -54,6 +55,7 @@ use crate::deploy::{
MonitorDataStore,
create_and_deploy_accounts,
create_genesis_aware_rpc_client,
create_genesis_aware_rpc_client_with_commitment,
};
use crate::service::Service;
use crate::status::{
Expand Down Expand Up @@ -164,6 +166,12 @@ pub struct IncrementService {
/// whenever the increment task regenerates accounts after persistent failures, so the tracker
/// can switch to the new account IDs without polling disk.
accounts_sender: watch::Sender<TrackedAccounts>,
/// Genesis commitment of the monitored network, bound into the associated data of sealed
/// transaction inputs.
genesis_commitment: Word,
/// Cached sealer for transaction inputs, populated on first submission. A plain `Option`
/// suffices because submissions run through `&mut self`.
sealer: Option<TransactionInputSealer>,
Comment on lines +169 to +174

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.

I don't see why we can have genesis_commitment but not the sealer?

}

impl IncrementService {
Expand All @@ -180,8 +188,11 @@ impl IncrementService {
accounts_sender: watch::Sender<TrackedAccounts>,
latency_state: Arc<Mutex<LatencyState>>,
) -> Result<Self> {
let mut rpc_client =
create_genesis_aware_rpc_client(&config.rpc_url, config.request_timeout).await?;
let (mut rpc_client, genesis_commitment) = create_genesis_aware_rpc_client_with_commitment(
&config.rpc_url,
config.request_timeout,
)
.await?;
let (tx, details) =
setup_increment_task(wallet_account, secret_key, counter_account, &mut rpc_client)
.await?;
Expand All @@ -194,9 +205,30 @@ impl IncrementService {
details,
latency_state,
accounts_sender,
genesis_commitment,
sealer: None,
})
}

/// Returns the cached sealer, fetching the encryption key on first use.
async fn sealer(&mut self) -> Result<TransactionInputSealer> {
if let Some(sealer) = &self.sealer {
return Ok(sealer.clone());
}

let key = self
.rpc_client
.get_transaction_encryption_key(())
.await
.context("Failed to fetch the transaction encryption key")?
.into_inner();
let sealer = TransactionInputSealer::new(key, self.genesis_commitment)
.context("Unusable transaction encryption key")?;

self.sealer = Some(sealer.clone());
Ok(sealer)
Comment on lines +219 to +229

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.

Personally I would wrap all of this logic into the client itself.

}

/// Applies a successful increment: advances the local wallet by the transaction's account
/// delta, bumps the success count, and returns the value used as the latency-measurement
/// target.
Expand Down Expand Up @@ -368,15 +400,28 @@ impl IncrementService {
.await
.context("counter increment task failed")??;

let sealed = self
.sealer()
.await?
.seal(proven_tx.id(), &tx_inputs)
.context("Failed to seal the transaction inputs")?;

let request = ProvenTransaction {
transaction: proven_tx.to_bytes(),
transaction_inputs: Some(tx_inputs),
sealed_transaction_inputs: Some(sealed),
};

let block_height: BlockNumber = self
.rpc_client
.submit_proven_tx(request)
.await
.inspect_err(|status| {
// A validator restarted with a different encryption key rejects with
// `failed_precondition`. Drop the cached key so the next tick re-fetches it.
if status.code() == tonic::Code::FailedPrecondition {
self.sealer = None;
}
})
.context("Failed to submit proven transaction to RPC")?
.into_inner()
.block_num
Expand Down
31 changes: 28 additions & 3 deletions bin/network-monitor/src/deploy/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use std::time::Duration;
use anyhow::{Context, Result};
use backon::{ExponentialBuilder, Retryable};
use miden_node_proto::clients::{Builder, RpcClient};
use miden_node_proto::domain::encryption::TransactionInputSealer;
use miden_node_proto::generated::rpc::BlockHeaderByNumberRequest;
use miden_node_proto::generated::transaction::ProvenTransaction;
use miden_node_utils::spawn::spawn_blocking_in_current_span;
Expand Down Expand Up @@ -79,6 +80,19 @@ pub async fn create_genesis_aware_rpc_client(
rpc_url: &Url,
timeout: Duration,
) -> Result<RpcClient> {
create_genesis_aware_rpc_client_with_commitment(rpc_url, timeout)
.await
.map(|(client, _)| client)
}

/// As [`create_genesis_aware_rpc_client`], but also returns the discovered genesis commitment.
///
/// Submitting call sites need the commitment to seal transaction inputs, and it is already computed
/// during the handshake.
pub async fn create_genesis_aware_rpc_client_with_commitment(
rpc_url: &Url,
timeout: Duration,
) -> Result<(RpcClient, Word)> {
Comment on lines +92 to +95

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.

Should this not just replace the previous function?

(|| async {
// First, create a temporary client without genesis metadata to discover the genesis block
// header and its commitment.
Expand Down Expand Up @@ -126,7 +140,7 @@ pub async fn create_genesis_aware_rpc_client(
.await
.context("Failed to connect to RPC server with genesis metadata")?;

Ok(rpc_client)
Ok((rpc_client, genesis_commitment))
})
.retry(genesis_discovery_backoff())
.notify(|err: &anyhow::Error, sleep: Duration| {
Expand Down Expand Up @@ -240,7 +254,8 @@ pub async fn deploy_counter_account(
prover: &LocalTransactionProver,
) -> Result<()> {
// Deploy counter account to the network using a genesis-aware RPC client.
let mut rpc_client = create_genesis_aware_rpc_client(rpc_url, Duration::from_secs(10)).await?;
let (mut rpc_client, genesis_commitment) =
create_genesis_aware_rpc_client_with_commitment(rpc_url, Duration::from_secs(10)).await?;

let executed_tx = execute_counter_genesis_tx(counter_account, &mut rpc_client).await?;

Expand All @@ -252,9 +267,19 @@ pub async fn deploy_counter_account(
.context("prover task panicked")?
.context("Failed to prove transaction")?;

let key = rpc_client
.get_transaction_encryption_key(())
.await
.context("Failed to fetch the transaction encryption key")?
.into_inner();
Comment on lines +270 to +274

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.

Should we not cache this and only refresh when a submission fails?

let sealed = TransactionInputSealer::new(key, genesis_commitment)
.context("Unusable transaction encryption key")?
.seal(proven_tx.id(), &transaction_inputs)
.context("Failed to seal the transaction inputs")?;

let request = ProvenTransaction {
transaction: proven_tx.to_bytes(),
transaction_inputs: Some(transaction_inputs),
sealed_transaction_inputs: Some(sealed),
};

rpc_client
Expand Down
Loading
Loading