Skip to content

Commit

Permalink
feat(bench): generate traffic calling MPC contract's sign method (#…
Browse files Browse the repository at this point in the history
…12658)

Requirements have been discussed in [this Zulip
thread](https://near.zulipchat.com/#narrow/channel/295306-contract-runtime/topic/Question.20about.20load-testing.20tools).

### Usage

Assumes the contract has already been deployed and initialized.

1. Create accounts that send transactions, e.g. with `just
create_sub_accounts`.
2. Execute the `benchmark-mpc-sign` command.
  • Loading branch information
mooori authored Jan 17, 2025
1 parent 95b555c commit be90285
Show file tree
Hide file tree
Showing 4 changed files with 200 additions and 0 deletions.
13 changes: 13 additions & 0 deletions benchmarks/synth-bm/justfile
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,16 @@ benchmark_native_transfers:
--channel-buffer-size 30000 \
--interval-duration-micros 550 \
--amount 1

benchmark_mpc_sign:
RUST_LOG=info \
cargo run --release -- benchmark-mpc-sign \
--rpc-url {{rpc_url}} \
--user-data-dir user-data/ \
--num-transactions 500 \
--transactions-per-second 100 \
--receiver-id 'v1.signer-dev.testnet' \
--key-version 0 \
--channel-buffer-size 500 \
--gas 300000000000000 \
--deposit 100000000000000000000000
169 changes: 169 additions & 0 deletions benchmarks/synth-bm/src/contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
use std::path::PathBuf;
use std::sync::Arc;
use std::time::{Duration, Instant};

use crate::account::accounts_from_dir;
use crate::block_service::BlockService;
use crate::rpc::{ResponseCheckSeverity, RpcResponseHandler};
use clap::Args;
use log::info;
use near_jsonrpc_client::methods::send_tx::RpcSendTransactionRequest;
use near_jsonrpc_client::JsonRpcClient;
use near_primitives::transaction::SignedTransaction;
use near_primitives::types::AccountId;
use near_primitives::views::TxExecutionStatus;
use rand::distributions::{Alphanumeric, DistString};
use rand::rngs::ThreadRng;
use rand::{thread_rng, Rng};
use serde::Serialize;
use serde_json::json;
use tokio::sync::mpsc;
use tokio::time;

#[derive(Args, Debug)]
pub struct BenchmarkMpcSignArgs {
/// RPC node to which transactions are sent.
#[arg(long)]
pub rpc_url: String,
/// Directory containing data of the users that sign transactions.
#[arg(long)]
pub user_data_dir: PathBuf,
/// The number of transactions to send per second. May be lower when reaching hardware limits or
/// network congestion.
#[arg(long)]
pub transactions_per_second: u64,
/// The total number of transactions to send.
#[arg(long)]
pub num_transactions: u64,
/// The id of the account to which the MPC contract has been deployed.
#[arg(long)]
pub receiver_id: AccountId,
/// The `key_version` passed as argument to `sign`.
#[arg(long)]
pub key_version: u32,
/// Acts as upper bound on the number of concurrently open RPC requests.
#[arg(long)]
pub channel_buffer_size: usize,
/// The gas (in yoctoNEAR) attached to each `sign` function call transaction.
#[arg(long)]
pub gas: u64,
/// The deposit (in yoctoNEAR) attached to each `sign` function call transaction.
#[arg(long)]
pub deposit: u128,
}

pub async fn benchmark_mpc_sign(args: &BenchmarkMpcSignArgs) -> anyhow::Result<()> {
let mut accounts = accounts_from_dir(&args.user_data_dir)?;
assert!(
accounts.len() > 0,
"at least one account required in {:?} to send transactions",
args.user_data_dir
);

// Pick interval to achieve desired TPS.
let mut interval =
time::interval(Duration::from_micros(1_000_000 / args.transactions_per_second));

let client = JsonRpcClient::connect(&args.rpc_url);
let block_service = Arc::new(BlockService::new(client.clone()).await);
block_service.clone().start().await;
let mut rng = thread_rng();

// Before a request is made, a permit to send into the channel is awaited. Hence buffer size
// limits the number of outstanding requests. This helps to avoid congestion.
let (channel_tx, channel_rx) = mpsc::channel(args.channel_buffer_size);

// Current network capacity for MPC `sign` calls is known to be around 100 TPS. At that
// rate, neither the network nor the RPC should be a bottleneck.
// Hence `wait_until: EXECUTED_OPTIMISTIC` as it provides most insights.
let wait_until = TxExecutionStatus::ExecutedOptimistic;
let wait_until_channel = wait_until.clone();
let num_expected_responses = args.num_transactions;
let response_handler_task = tokio::task::spawn(async move {
let mut rpc_response_handler = RpcResponseHandler::new(
channel_rx,
wait_until_channel,
ResponseCheckSeverity::Log,
num_expected_responses,
);
rpc_response_handler.handle_all_responses().await;
});

info!("Setup complete, starting to send transactions");
let timer = Instant::now();
for i in 0..args.num_transactions {
let sender_idx = usize::try_from(i).unwrap() % accounts.len();
let sender = &accounts[sender_idx];

let transaction = SignedTransaction::call(
sender.nonce,
sender.id.clone(),
args.receiver_id.clone(),
&sender.as_signer(),
args.deposit,
"sign".to_string(),
new_random_mpc_sign_args(&mut rng, args.key_version).to_string().into_bytes(),
args.gas,
block_service.get_block_hash(),
);
let request = RpcSendTransactionRequest {
signed_transaction: transaction,
wait_until: wait_until.clone(),
};

// Let time pass to meet TPS target.
interval.tick().await;

// Await permit before sending the request to make channel buffer size a limit for the
// number of outstanding requests.
let permit = channel_tx.clone().reserve_owned().await.unwrap();
let client = client.clone();
tokio::spawn(async move {
let res = client.call(request).await;
permit.send(res);
});

if i > 0 && i % 200 == 0 {
info!("sent {i} transactions in {:.2} seconds", timer.elapsed().as_secs_f64());
}

let sender = accounts.get_mut(sender_idx).unwrap();
sender.nonce += 1;
}

info!(
"Done sending {} transactions in {:.2} seconds",
args.num_transactions,
timer.elapsed().as_secs_f64()
);

info!("Awaiting RPC responses");
response_handler_task.await.expect("response handler tasks should succeed");
info!("Received all RPC responses after {:.2} seconds", timer.elapsed().as_secs_f64());

info!("Writing updated nonces to {:?}", args.user_data_dir);
for account in accounts.iter() {
account.write_to_dir(&args.user_data_dir)?;
}

Ok(())
}

/// Constructs the parameters according to
/// https://github.com/near/mpc/blob/79ec50759146221e7ad8bb04520f13333b75ca07/chain-signatures/contract/src/lib.rs#L127
fn new_random_mpc_sign_args(rng: &mut ThreadRng, key_version: u32) -> serde_json::Value {
#[derive(Serialize)]
struct SignRequest {
pub payload: [u8; 32],
pub path: String,
pub key_version: u32,
}

let mut payload: [u8; 32] = [0; 32];
rng.fill(&mut payload);
let path = Alphanumeric.sample_string(rng, 16);

json!({
"request": SignRequest { payload, path, key_version },
})
}
6 changes: 6 additions & 0 deletions benchmarks/synth-bm/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ use clap::{Parser, Subcommand};
mod account;
use account::{create_sub_accounts, CreateSubAccountsArgs};
mod block_service;
mod contract;
use contract::BenchmarkMpcSignArgs;
mod native_transfer;
mod rpc;

Expand All @@ -19,6 +21,7 @@ enum Commands {
/// Creates sub accounts for the signer.
CreateSubAccounts(CreateSubAccountsArgs),
BenchmarkNativeTransfers(native_transfer::BenchmarkArgs),
BenchmarkMpcSign(BenchmarkMpcSignArgs),
}

#[tokio::main]
Expand All @@ -35,6 +38,9 @@ async fn main() -> anyhow::Result<()> {
Commands::BenchmarkNativeTransfers(args) => {
native_transfer::benchmark(args).await?;
}
Commands::BenchmarkMpcSign(args) => {
contract::benchmark_mpc_sign(args).await?;
}
}
Ok(())
}
12 changes: 12 additions & 0 deletions docs/practices/workflows/benchmarking_synthetic_workloads.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,18 @@ Automatic calculation of transactions per second (TPS) when RPC requests are sen
http localhost:3030/metrics | grep transaction_processed
```

### Benchmark calls to the `sign` method of an MPC contract

Assumes the accounts that send the transactions invoking `sign` have been created as described above. Transactions can be sent to a RPC of a network on which an instance of the [`mpc/chain-signatures`](https://github.com/near/mpc/tree/79ec50759146221e7ad8bb04520f13333b75ca07/chain-signatures/contract) is deployed.

Transactions are sent to the RPC with `wait_until: EXECUTED_OPTIMISTIC` as the throughput for `sign` is at a level at which neither the network nor the RPC are expected to be a bottleneck.

All options of the command can be shown with:

```command
cargo run -- benchmark-mpc-sign --help
```

## Network setup and `neard` configuration

Details of bringing up and configuring a network are out of scope for this document. Instead we just give a brief overview of the setup regularly used to benchmark TPS of common workloads in a single-node with a single-shard setup.
Expand Down

0 comments on commit be90285

Please sign in to comment.