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

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
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);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@saketh-are is 16 a reasonable value for the path length? In case the required path length may change, it can be made an argument (like --key-version above).


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
Loading