Skip to content

Commit b9c7894

Browse files
authored
bin: txspammer (#236)
1 parent 0bca674 commit b9c7894

File tree

6 files changed

+679
-0
lines changed

6 files changed

+679
-0
lines changed

based/Cargo.lock

Lines changed: 60 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

based/bin/txspammer/Cargo.toml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
[package]
2+
edition.workspace = true
3+
name = "txspammer"
4+
rust-version.workspace = true
5+
version.workspace = true
6+
7+
[dependencies]
8+
alloy-eips.workspace = true
9+
alloy-primitives.workspace = true
10+
alloy-rpc-types.workspace = true
11+
alloy-signer.workspace = true
12+
alloy-signer-local.workspace = true
13+
alloy-provider.workspace = true
14+
alloy-consensus.workspace = true
15+
16+
bop-common.workspace = true
17+
bop-metrics.workspace = true
18+
clap.workspace = true
19+
eyre.workspace = true
20+
futures.workspace = true
21+
jsonrpsee.workspace = true
22+
op-alloy-rpc-types.workspace = true
23+
op-alloy-rpc-types-engine.workspace = true
24+
parking_lot.workspace = true
25+
reqwest.workspace = true
26+
reth-rpc-layer.workspace = true
27+
serde.workspace = true
28+
serde_json.workspace = true
29+
thiserror.workspace = true
30+
tokio.workspace = true
31+
tower.workspace = true
32+
tower-http.workspace = true
33+
tracing.workspace = true
34+
rand.workspace = true
35+
tokio-websockets = { version = "0.12.1", features = ["client", "openssl", "rand", "server"] }
36+
futures-util = "0.3.31"
37+
http.workspace = true

based/bin/txspammer/src/account.rs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
use alloy_consensus::{SignableTransaction, TxEip1559, TxEnvelope};
2+
use alloy_eips::eip2718::Encodable2718;
3+
use alloy_primitives::{B256, Bytes, U256};
4+
use alloy_provider::{Provider, RootProvider};
5+
use alloy_rpc_types::AccessList;
6+
use alloy_signer::SignerSync;
7+
use alloy_signer_local::PrivateKeySigner;
8+
9+
pub struct AccountGenerator {
10+
current: U256,
11+
}
12+
13+
impl AccountGenerator {
14+
pub fn new(start: U256) -> Self {
15+
Self { current: start }
16+
}
17+
18+
pub fn next(&mut self) -> PrivateKeySigner {
19+
let temp = self.current;
20+
self.current += U256::from(1);
21+
PrivateKeySigner::from_slice(temp.as_le_slice()).expect("something wrong with U256 to bytes conversion")
22+
}
23+
}
24+
25+
#[derive(Clone, Debug)]
26+
pub struct TxSpec {
27+
pub chain_id: u64,
28+
pub gas_limit: u64,
29+
pub max_fee_per_gas: u128,
30+
pub max_priority_fee_per_gas: u128,
31+
pub value: U256,
32+
}
33+
34+
#[derive(Clone, Debug)]
35+
pub struct Account {
36+
pub signer: PrivateKeySigner,
37+
pub nonce: u64,
38+
pub balance: U256,
39+
}
40+
41+
impl Account {
42+
pub fn new(signer: PrivateKeySigner) -> Self {
43+
Self { signer, nonce: 0, balance: U256::ZERO }
44+
}
45+
46+
pub async fn refresh_balance(&mut self, provider: &RootProvider) -> eyre::Result<U256> {
47+
self.balance = provider.get_balance(self.signer.address()).await?;
48+
Ok(self.balance)
49+
}
50+
51+
pub async fn refresh_nonce(&mut self, provider: &RootProvider) -> eyre::Result<u64> {
52+
self.nonce = provider.get_transaction_count(self.signer.address()).await?;
53+
Ok(self.nonce)
54+
}
55+
56+
pub async fn refresh(&mut self, provider: &RootProvider) -> eyre::Result<u64> {
57+
self.refresh_balance(provider).await?;
58+
self.refresh_nonce(provider).await?;
59+
Ok(self.nonce)
60+
}
61+
62+
pub async fn transfer(
63+
&mut self,
64+
to: &mut Account,
65+
spec: &TxSpec,
66+
provider: &RootProvider,
67+
sequencer: &Option<RootProvider>,
68+
) -> eyre::Result<B256> {
69+
let tx = TxEip1559 {
70+
chain_id: spec.chain_id,
71+
nonce: self.nonce,
72+
gas_limit: spec.gas_limit,
73+
max_fee_per_gas: spec.max_fee_per_gas,
74+
max_priority_fee_per_gas: spec.max_priority_fee_per_gas,
75+
to: to.signer.address().into(),
76+
value: spec.value,
77+
access_list: AccessList::default(),
78+
input: Bytes::new(),
79+
};
80+
81+
let sig = self.signer.sign_hash_sync(&tx.signature_hash()).unwrap();
82+
let tx: TxEnvelope = tx.into_signed(sig).into();
83+
let encoded = tx.encoded_2718();
84+
let provider_to_use = sequencer.as_ref().unwrap_or(provider);
85+
let _pending_tx = provider_to_use.send_raw_transaction(&encoded).await.unwrap();
86+
self.nonce += 1;
87+
self.balance -= spec.value + U256::from(spec.gas_limit) * U256::from(spec.max_fee_per_gas);
88+
to.balance += spec.value;
89+
90+
Ok(*tx.tx_hash())
91+
}
92+
93+
pub async fn _self_transfer(
94+
&mut self,
95+
spec: &TxSpec,
96+
provider: &RootProvider,
97+
sequencer: &Option<RootProvider>,
98+
) -> eyre::Result<B256> {
99+
let tx = TxEip1559 {
100+
chain_id: spec.chain_id,
101+
nonce: self.nonce,
102+
gas_limit: spec.gas_limit,
103+
max_fee_per_gas: spec.max_fee_per_gas,
104+
max_priority_fee_per_gas: spec.max_priority_fee_per_gas,
105+
to: self.signer.address().into(),
106+
value: spec.value,
107+
access_list: AccessList::default(),
108+
input: Bytes::new(),
109+
};
110+
111+
let sig = self.signer.sign_hash_sync(&tx.signature_hash()).unwrap();
112+
let tx: TxEnvelope = tx.into_signed(sig).into();
113+
let encoded = tx.encoded_2718();
114+
let provider_to_use = sequencer.as_ref().unwrap_or(provider);
115+
let _pending_tx = provider_to_use.send_raw_transaction(&encoded).await.unwrap();
116+
self.nonce += 1;
117+
self.balance -= U256::from(spec.gas_limit) * U256::from(spec.max_fee_per_gas);
118+
119+
Ok(*tx.tx_hash())
120+
}
121+
}

based/bin/txspammer/src/cli.rs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
use std::path::PathBuf;
2+
3+
use bop_common::config::{LoggingConfig, LoggingFlags};
4+
use clap::{Parser, command};
5+
use tracing::level_filters::LevelFilter;
6+
7+
#[derive(Parser, Debug, Clone)]
8+
#[command(version, about, name = "based-txproxy")]
9+
pub struct TxSpammerArgs {
10+
/// Root wallet private key
11+
#[arg(
12+
long = "root.private_key",
13+
default_value = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
14+
)]
15+
pub root_private_key: String,
16+
/// Number of accounts to generate
17+
#[arg(long = "num_accounts", default_value_t = 100)]
18+
pub num_accounts: usize,
19+
/// Throughput (tx/s)
20+
#[arg(long = "throughput", default_value_t = 300)]
21+
pub throughput: usize,
22+
/// Funding amount per account (in ether)
23+
#[arg(long = "funding_amount", default_value_t = 0.01)]
24+
pub funding_amount: f64,
25+
26+
/// Value to transfer per transaction (in ether)
27+
#[arg(long = "tx_value", default_value_t = 0.0000000000000001)]
28+
pub tx_value: f64,
29+
/// Gas limit per transaction
30+
#[arg(long = "gas_limit", default_value_t = 21_000)]
31+
pub gas_limit: u64,
32+
/// Max fee per gas (in wei)
33+
#[arg(long = "max_fee_per_gas", default_value_t = 1_000_000_000)]
34+
pub max_fee_per_gas: u128,
35+
/// Max priority fee per gas (in wei)
36+
#[arg(long = "max_priority_fee_per_gas", default_value_t = 20)]
37+
pub max_priority_fee_per_gas: u128,
38+
39+
/// Eth rpc url (http/ws)
40+
#[arg(long = "eth_rpc.url", default_value = "http://127.0.0.1:8545")]
41+
pub eth_rpc_url: String,
42+
/// Sequencer URL (if specified, eth_sendRawTransaction will be sent to this URL)
43+
#[arg(long = "sequencer.url")]
44+
pub sequencer_url: Option<String>,
45+
/// Frag stream URL (if specified, we can measure the e2e latency via the frag stream). Poll receipt using eth rpc
46+
/// if not specified.
47+
#[arg(long = "fragstream.url")]
48+
pub fragstream_url: Option<String>,
49+
50+
/// Enable debug logging
51+
#[arg(long)]
52+
pub debug: bool,
53+
/// Enable trace logging
54+
#[arg(long)]
55+
pub trace: bool,
56+
/// Enable file logging
57+
#[arg(long = "log.disable_file_logging", action = clap::ArgAction::SetFalse, default_value_t = true)]
58+
pub file_logging: bool,
59+
/// Prefix of log files
60+
#[arg(long = "log.prefix", default_value = "bop-txproxy.log")]
61+
pub log_prefix: String,
62+
/// Path for log files
63+
#[arg(long = "log.dir", default_value = "/tmp")]
64+
pub log_dir: PathBuf,
65+
/// Maximum number of log files
66+
#[arg(long = "log.max_files", default_value_t = 100)]
67+
pub log_max_files: usize,
68+
}
69+
70+
impl From<&TxSpammerArgs> for LoggingConfig {
71+
fn from(args: &TxSpammerArgs) -> Self {
72+
Self {
73+
level: args
74+
.trace
75+
.then_some(LevelFilter::TRACE)
76+
.or(args.debug.then_some(LevelFilter::DEBUG))
77+
.unwrap_or(LevelFilter::INFO),
78+
flags: if args.file_logging { LoggingFlags::all() } else { LoggingFlags::StdOut },
79+
prefix: args.file_logging.then(|| args.log_prefix.clone()),
80+
max_files: args.log_max_files,
81+
path: args.log_dir.clone(),
82+
filters: None,
83+
}
84+
}
85+
}

0 commit comments

Comments
 (0)