Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(bench): generate traffic calling MPC contract's sign method #12658

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
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
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-transfers 500 \
--transactions-per-second 100 \
--receiver-id 'v1.signer-dev.testnet' \
--key-version 0 \
--channel-buffer-size 500 \
--gas 100000000000000 \
--deposit 1
151 changes: 151 additions & 0 deletions benchmarks/synth-bm/src/contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
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_json::json;
use tokio::sync::mpsc;
use tokio::time;

#[derive(Args, Debug)]
pub struct BenchmarkMpcSignArgs {
#[arg(long)]
pub rpc_url: String,
#[arg(long)]
pub user_data_dir: PathBuf,
#[arg(long)]
pub transactions_per_second: u64,
#[arg(long)]
pub num_transfers: u64,
#[arg(long)]
pub receiver_id: AccountId,
/// The `key_version` passed as argument to `sign`.
#[arg(long)]
pub key_version: u32,
#[arg(long)]
pub channel_buffer_size: usize,
#[arg(long)]
pub gas: u64,
#[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::None;
let wait_until_channel = wait_until.clone();
let num_expected_responses = args.num_transfers;
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_transfers {
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_transfers,
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(())
}

fn new_random_mpc_sign_args(rng: &mut ThreadRng, key_version: u32) -> serde_json::Value {
let mut payload: [u8; 32] = [0; 32];
rng.fill(&mut payload);

json!({
"payload": payload,
"path": Alphanumeric.sample_string(rng, 16),
"key_version": 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 that 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
Loading