Skip to content

Commit

Permalink
Merge pull request #44 from flashbots/send-bundles
Browse files Browse the repository at this point in the history
Send bundles
  • Loading branch information
zeroXbrock authored Nov 14, 2024
2 parents d543ce5 + 2d0fd13 commit ccbac0d
Show file tree
Hide file tree
Showing 19 changed files with 1,285 additions and 451 deletions.
480 changes: 400 additions & 80 deletions Cargo.lock

Large diffs are not rendered by default.

10 changes: 7 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,22 @@ name = "contender_core"
path = "src/lib.rs"

[dependencies]
alloy = { workspace = true, features = ["full", "node-bindings"] }
eyre = "0.6.12"
alloy = { workspace = true, features = ["full", "node-bindings", "rpc-types-mev"] }
eyre = { workspace = true }
rand = "0.8.5"
serde = { workspace = true }
serde = { workspace = true, features = ["derive"] }
futures = "0.3.30"
async-trait = "0.1.82"
tokio = { workspace = true }
jsonrpsee = { version = "0.24", features = ["http-client", "client-core"] }
alloy-serde = "0.5.4"
serde_json = "1.0.132"

[workspace]
members = ["cli", "sqlite_db", "testfile"]

[workspace.dependencies]
eyre = "0.6.12"
tokio = { version = "1.40.0" }
alloy = { version = "0.3.6" }
serde = "1.0.209"
8 changes: 8 additions & 0 deletions cli/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ pub enum ContenderSubcommand {
/// The HTTP JSON-RPC URL to spam with requests.
rpc_url: String,

/// HTTP JSON-RPC URL to use for bundle spamming (must support `eth_sendBundle`).
#[arg(
short,
long,
long_help = "HTTP JSON-RPC URL to use for bundle spamming (must support `eth_sendBundle`)"
)]
builder_url: Option<String>,

/// The number of txs to send per second using the timed spammer. This is the default spammer.
/// May not be set if `txs_per_block` is set.
#[arg(long, long_help = "Number of txs to send per second. Must not be set if --txs-per-block is set.", visible_aliases = &["tps"])]
Expand Down
51 changes: 41 additions & 10 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use commands::{ContenderCli, ContenderSubcommand};
use contender_core::{
db::{DbOps, RunTx},
generator::{
types::{AnyProvider, FunctionCallDefinition},
types::{AnyProvider, FunctionCallDefinition, SpamRequest},
RandSeed,
},
spammer::{BlockwiseSpammer, LogCallback, NilCallback, TimedSpammer},
Expand Down Expand Up @@ -77,13 +77,17 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.map(|key| PrivateKeySigner::from_str(&key).expect("invalid private key"))
.collect::<Vec<PrivateKeySigner>>();
let signers = get_signers_with_defaults(private_keys);
check_private_keys(&testconfig.setup.to_owned().unwrap_or(vec![]), &signers);
check_private_keys(
&testconfig.setup.to_owned().unwrap_or(vec![]),
signers.as_slice(),
);
check_balances(&user_signers, min_balance, &rpc_client).await;

let scenario = TestScenario::new(
testconfig.to_owned(),
Arc::new(DB.clone()),
url,
None,
RandSeed::new(),
&signers,
);
Expand All @@ -95,6 +99,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
ContenderSubcommand::Spam {
testfile,
rpc_url,
builder_url,
txs_per_block,
txs_per_second,
duration,
Expand All @@ -117,8 +122,22 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.spam
.as_ref()
.expect("No spam function calls found in testfile");
check_private_keys(spam, &signers);
check_balances(&signers, min_balance, &rpc_client).await;

// distill all FunctionCallDefinitions from the spam requests
let mut fn_calls = vec![];
for s in spam {
match s {
SpamRequest::Tx(fn_call) => {
fn_calls.push(fn_call.to_owned());
}
SpamRequest::Bundle(bundle) => {
fn_calls.extend(bundle.txs.iter().map(|s| s.to_owned()));
}
}
}

check_private_keys(&fn_calls, signers.as_slice());
check_balances(signers.as_slice(), min_balance, &rpc_client).await;

if txs_per_block.is_some() && txs_per_second.is_some() {
panic!("Cannot set both --txs-per-block and --txs-per-second");
Expand All @@ -128,8 +147,14 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
}

if let Some(txs_per_block) = txs_per_block {
let scenario =
TestScenario::new(testconfig, DB.clone().into(), url, rand_seed, &signers);
let scenario = TestScenario::new(
testconfig,
DB.clone().into(),
url,
builder_url.map(|url| Url::parse(&url).expect("Invalid builder URL")),
rand_seed,
&signers,
);
println!("Blockwise spamming with {} txs per block", txs_per_block);
match spam_callback_default(!disable_reports, Arc::new(rpc_client).into()).await {
SpamCallbackType::Log(cback) => {
Expand All @@ -138,14 +163,14 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.expect("Time went backwards")
.as_millis();
let run_id = DB.insert_run(timestamp as u64, txs_per_block * duration)?;
let spammer = BlockwiseSpammer::new(scenario, cback);
let mut spammer = BlockwiseSpammer::new(scenario, cback).await;
spammer
.spam_rpc(txs_per_block, duration, Some(run_id.into()))
.await?;
println!("Saved run. run_id = {}", run_id);
}
SpamCallbackType::Nil(cback) => {
let spammer = BlockwiseSpammer::new(scenario, cback);
let mut spammer = BlockwiseSpammer::new(scenario, cback).await;
spammer.spam_rpc(txs_per_block, duration, None).await?;
}
};
Expand All @@ -154,8 +179,14 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {

// NOTE: private keys are not currently used for timed spamming.
// Timed spamming only works with unlocked accounts, because it uses the `eth_sendTransaction` RPC method.
let scenario =
TestScenario::new(testconfig, DB.clone().into(), url, rand_seed, &signers);
let scenario = TestScenario::new(
testconfig,
DB.clone().into(),
url,
None,
rand_seed,
&signers,
);
let tps = txs_per_second.unwrap_or(10);
println!("Timed spamming with {} txs per second", tps);
let spammer = TimedSpammer::new(scenario, NilCallback::new());
Expand Down
46 changes: 46 additions & 0 deletions cli/testfile.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
[[create]]
bytecode = "0x6080604052348015600f57600080fd5b506105668061001f6000396000f3fe60806040526004361061004a5760003560e01c806369f86ec81461004f5780639402c00414610066578063a329e8de14610086578063c5eeaf17146100a6578063fb0e722b146100ae575b600080fd5b34801561005b57600080fd5b506100646100d9565b005b34801561007257600080fd5b50610064610081366004610218565b6100e4565b34801561009257600080fd5b506100646100a13660046102d1565b610119565b610064610145565b3480156100ba57600080fd5b506100c3610174565b6040516100d0919061030e565b60405180910390f35b5b60325a116100da57565b6000816040516020016100f892919061037b565b60405160208183030381529060405260009081610115919061044f565b5050565b600061012660d98361050e565b905060005b8181101561014057600160008190550161012b565b505050565b60405141903480156108fc02916000818181858888f19350505050158015610171573d6000803e3d6000fd5b50565b6000805461018190610341565b80601f01602080910402602001604051908101604052809291908181526020018280546101ad90610341565b80156101fa5780601f106101cf576101008083540402835291602001916101fa565b820191906000526020600020905b8154815290600101906020018083116101dd57829003601f168201915b505050505081565b634e487b7160e01b600052604160045260246000fd5b60006020828403121561022a57600080fd5b813567ffffffffffffffff81111561024157600080fd5b8201601f8101841361025257600080fd5b803567ffffffffffffffff81111561026c5761026c610202565b604051601f8201601f19908116603f0116810167ffffffffffffffff8111828210171561029b5761029b610202565b6040528181528282016020018610156102b357600080fd5b81602084016020830137600091810160200191909152949350505050565b6000602082840312156102e357600080fd5b5035919050565b60005b838110156103055781810151838201526020016102ed565b50506000910152565b602081526000825180602084015261032d8160408501602087016102ea565b601f01601f19169190910160400192915050565b600181811c9082168061035557607f821691505b60208210810361037557634e487b7160e01b600052602260045260246000fd5b50919050565b600080845461038981610341565b6001821680156103a057600181146103b5576103e5565b60ff19831686528115158202860193506103e5565b87600052602060002060005b838110156103dd578154888201526001909101906020016103c1565b505081860193505b50505083516103f88183602088016102ea565b01949350505050565b601f82111561014057806000526020600020601f840160051c810160208510156104285750805b601f840160051c820191505b818110156104485760008155600101610434565b5050505050565b815167ffffffffffffffff81111561046957610469610202565b61047d816104778454610341565b84610401565b6020601f8211600181146104b157600083156104995750848201515b600019600385901b1c1916600184901b178455610448565b600084815260208120601f198516915b828110156104e157878501518255602094850194600190920191016104c1565b50848210156104ff5786840151600019600387901b60f8161c191681555b50505050600190811b01905550565b60008261052b57634e487b7160e01b600052601260045260246000fd5b50049056fea264697066735822122043ea7522db98264cdc5157a0f2d3f9fc75e28c6078f917dfe9a946bf9b21af7f64736f6c634300081b0033"
name = "SpamMe"
from = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"


# spam single tx
[[spam]]

[spam.tx]
to = "0x90F79bf6EB2c4f870365E785982E1f101E93b906"
from = "0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f"
signature = "transfer()"
value = "100000000000000"


# spam bundle
[[spam]]

[[spam.bundle.tx]]
to = "{SpamMe}"
from = "0x90F79bf6EB2c4f870365E785982E1f101E93b906"
signature = "consumeGas(uint256)"
args = ["510000"]


[[spam.bundle.tx]]
to = "{SpamMe}"
from = "0x90F79bf6EB2c4f870365E785982E1f101E93b906"
signature = "tipCoinbase()"
value = "10000000000000000"

[[spam]]

[[spam.bundle.tx]]
to = "{SpamMe}"
from = "0xa0Ee7A142d267C1f36714E4a8F75612F20a79720"
signature = "consumeGas(uint256)"
args = ["510000"]


[[spam.bundle.tx]]
to = "{SpamMe}"
from = "0xa0Ee7A142d267C1f36714E4a8F75612F20a79720"
signature = "tipCoinbase()"
value = "10000000000000000"
91 changes: 91 additions & 0 deletions src/bundle_provider.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
use alloy::primitives::{Bytes, B256};
use jsonrpsee::http_client::HttpClient;
use jsonrpsee::{core::client::ClientT, rpc_params};
use serde::{Deserialize, Serialize};

pub struct BundleClient {
client: HttpClient,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EthSendBundleResponse {
pub bundle_hash: B256,
}

impl BundleClient {
pub fn new(url: String) -> Self {
let client = HttpClient::builder()
.build(url)
.expect("failed to connect to RPC provider");
Self { client }
}

pub async fn send_bundle(&self, bundle: EthSendBundle) -> Result<(), String> {
// Result contents optional because some endpoints don't return this response
let res: Result<Option<EthSendBundleResponse>, _> = self
.client
.request("eth_sendBundle", rpc_params![bundle])
.await;
if let Err(e) = res {
return Err(format!("Failed to send bundle: {:?}", e));
}

Ok(())
}
}

#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EthSendBundle {
/// A list of hex-encoded signed transactions
pub txs: Vec<Bytes>,
/// hex-encoded block number for which this bundle is valid
#[serde(with = "alloy_serde::quantity")]
pub block_number: u64,
/// unix timestamp when this bundle becomes active
#[serde(
default,
with = "alloy_serde::quantity::opt",
skip_serializing_if = "Option::is_none"
)]
pub min_timestamp: Option<u64>,
/// unix timestamp how long this bundle stays valid
#[serde(
default,
with = "alloy_serde::quantity::opt",
skip_serializing_if = "Option::is_none"
)]
pub max_timestamp: Option<u64>,
/// list of hashes of possibly reverting txs
#[serde(
default
// this doesn't work on rbuilder:
// , skip_serializing_if = "Vec::is_empty"
)]
pub reverting_tx_hashes: Vec<B256>,
/// UUID that can be used to cancel/replace this bundle
#[serde(
default,
rename = "replacementUuid",
skip_serializing_if = "Option::is_none"
)]
pub replacement_uuid: Option<String>,
}

impl EthSendBundle {
pub fn new_basic(txs: Vec<Bytes>, block_number: u64) -> Self {
Self {
txs,
block_number,
min_timestamp: None,
max_timestamp: None,
reverting_tx_hashes: Vec::new(),
replacement_uuid: None,
}
}

pub async fn send_to_builder(&self, client: &BundleClient) -> Result<(), String> {
client.send_bundle(self.clone()).await
}
}
File renamed without changes.
Loading

0 comments on commit ccbac0d

Please sign in to comment.