Skip to content
Open
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
8 changes: 4 additions & 4 deletions crates/cli/src/server/static/openrpc.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"BlobsCliArgs": {"file":"crates/cli/src/default_scenarios/blobs.rs","line":10},
"BuilderParams": {"file":"crates/cli/src/server/rpc_server/types.rs","line":227},
"BuiltinScenarioCli": {"file":"crates/cli/src/default_scenarios/builtin.rs","line":33},
"BundleCallDefinition": {"file":"crates/core/src/generator/function_def.rs","line":53},
"BundleCallDefinition": {"file":"crates/core/src/generator/function_def.rs","line":57},
"BundleTypeCli": {"file":"crates/cli/src/commands/common.rs","line":438},
"CompiledContract": {"file":"crates/core/src/generator/create_def.rs","line":8},
"ContenderSessionInfo": {"file":"crates/cli/src/server/sessions.rs","line":128},
Expand All @@ -27,9 +27,9 @@
"EthereumOpcode": {"file":"crates/cli/src/default_scenarios/eth_functions/opcodes.rs","line":10},
"EthereumPrecompile": {"file":"crates/cli/src/default_scenarios/eth_functions/precompiles.rs","line":11},
"FillBlockCliArgs": {"file":"crates/cli/src/default_scenarios/fill_block.rs","line":17},
"FunctionCallDefinition": {"file":"crates/core/src/generator/function_def.rs","line":12},
"FunctionCallDefinition": {"file":"crates/core/src/generator/function_def.rs","line":13},
"FundAccountsParams": {"file":"crates/cli/src/server/rpc_server/types.rs","line":273},
"FuzzParam": {"file":"crates/core/src/generator/function_def.rs","line":162},
"FuzzParam": {"file":"crates/core/src/generator/function_def.rs","line":172},
"RevertCliArgs": {"file":"crates/cli/src/default_scenarios/revert.rs","line":8},
"ServerStatus": {"file":"crates/cli/src/server/rpc_server/types.rs","line":33},
"SessionOptions": {"file":"crates/cli/src/server/rpc_server/types.rs","line":234},
Expand Down Expand Up @@ -188,7 +188,7 @@
"EthereumOpcode": {"type":"string","enum":["Stop","Add","Mul","Sub","Div","Sdiv","Mod","Smod","Addmod","Mulmod","Exp","Signextend","Lt","Gt","Slt","Sgt","Eq","Iszero","And","Or","Xor","Not","Byte","Shl","Shr","Sar","Sha3","Keccak256","Address","Balance","Origin","Caller","Callvalue","Calldataload","Calldatasize","Calldatacopy","Codesize","Codecopy","Gasprice","Extcodesize","Extcodecopy","Returndatasize","Returndatacopy","Extcodehash","Blockhash","Coinbase","Timestamp","Number","Prevrandao","Gaslimit","Chainid","Selfbalance","Basefee","Pop","Mload","Mstore","Mstore8","Sload","Sstore","Msize","Gas","Log0","Log1","Log2","Log3","Log4","Create","Call","Callcode","Return","Delegatecall","Create2","Staticcall","Revert","Invalid","Selfdestruct"]},
"EthereumPrecompile": {"type":"string","enum":["HashSha256","HashRipemd160","Identity","ModExp","EcAdd","EcMul","EcPairing","Blake2f"]},
"FillBlockCliArgs": {"description":"Taken from the CLI, this is used to fill a block with transactions.","type":"object","properties":{"max_gas_per_block":{"type":"integer"}}},
"FunctionCallDefinition": {"description":"User-facing definition of a function call to be executed.","type":"object","properties":{"to":{"description":"Address of the contract to call.","type":"string"},"from":{"description":"Address of the tx sender.","type":"string"},"from_pool":{"description":"Get a `from` address from the pool of signers specified here.","type":"string"},"signature":{"description":"Name of the function to call.","type":"string"},"args":{"description":"Parameters to pass to the function.","type":"array","items":{"type":"string"}},"value":{"description":"Value in wei to send with the tx.","type":"string"},"fuzz":{"description":"Parameters to fuzz during the test.","type":"array","items":{"$ref":"#/components/schemas/FuzzParam"}},"kind":{"description":"Optional type of the spam transaction for categorization.","type":"string"},"gas_limit":{"description":"Optional gas limit, which will skip gas estimation. This allows reverting txs to be sent.","type":"integer"},"blob_data":{"description":"Optional blob data; tx type must be set to EIP4844 by spammer","type":"string"},"authorization_address":{"description":"Optional setCode data; tx type must be set to EIP7702 by spammer","type":"string"},"for_all_accounts":{"description":"If true and `from_pool` is set, run this setup transaction for all accounts in the pool. Defaults to false (only runs for the first account).","type":"boolean"}},"required":["to","for_all_accounts"]},
"FunctionCallDefinition": {"description":"User-facing definition of a function call to be executed.","type":"object","properties":{"to":{"description":"Address of the contract to call.","type":"string"},"from":{"description":"Address of the tx sender.","type":"string"},"from_pool":{"description":"Get a `from` address from the pool of signers specified here.","type":"string"},"signature":{"description":"Name of the function to call.","type":"string"},"args":{"description":"Parameters to pass to the function.","type":"array","items":{"type":"string"}},"value":{"description":"Value in wei to send with the tx.","type":"string"},"fuzz":{"description":"Parameters to fuzz during the test.","type":"array","items":{"$ref":"#/components/schemas/FuzzParam"}},"kind":{"description":"Optional type of the spam transaction for categorization.","type":"string"},"gas_limit":{"description":"Optional gas limit, which will skip gas estimation. This allows reverting txs to be sent.","type":"integer"},"blob_data":{"description":"Optional blob data; tx type must be set to EIP4844 by spammer","type":"string"},"authorization_address":{"description":"Optional setCode data; tx type must be set to EIP7702 by spammer","type":"string"},"access_list":{"description":"Optional EIP-2930 access list entries to include in the transaction.","type":"array","items":{"$ref":"#/components/schemas/AccessListItem"}},"for_all_accounts":{"description":"If true and `from_pool` is set, run this setup transaction for all accounts in the pool. Defaults to false (only runs for the first account).","type":"boolean"}},"required":["to","for_all_accounts"]},
"FundAccountsParams": {"type":"object","properties":{"sessionId":{"type":"integer"},"agentClass":{"$ref":"#/components/schemas/AgentClass"},"amount":{"type":"string"}},"required":["sessionId","amount"]},
"FuzzParam": {"type":"object","properties":{"param":{"description":"Name of the parameter to fuzz.","type":"string"},"value":{"description":"Fuzz the `value` field of the tx (ETH sent with the tx).","type":"boolean"},"min":{"description":"Minimum value fuzzer will use.","type":"string"},"max":{"description":"Maximum value fuzzer will use.","type":"string"}}},
"RevertCliArgs": {"type":"object","properties":{"gas_use":{"description":"Amount of gas to use before reverting.","type":"integer"}},"required":["gas_use"]},
Expand Down
35 changes: 35 additions & 0 deletions crates/core/src/generator/function_def.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use alloy::{
eips::eip7702::SignedAuthorization,
hex::{FromHex, ToHexExt},
primitives::{Address, Bytes, U256},
rpc::types::AccessListItem,
};
use serde::{Deserialize, Serialize};

Expand Down Expand Up @@ -42,6 +43,9 @@ pub struct FunctionCallDefinition {
/// Optional setCode data; tx type must be set to EIP7702 by spammer
#[serde(skip_serializing_if = "Option::is_none")]
pub authorization_address: Option<String>,
/// Optional EIP-2930 access list entries to include in the transaction.
#[serde(skip_serializing_if = "Option::is_none")]
pub access_list: Option<Vec<AccessListItem>>,
/// If true and `from_pool` is set, run this setup transaction for all accounts in the pool.
/// Defaults to false (only runs for the first account).
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
Expand Down Expand Up @@ -69,6 +73,7 @@ impl FunctionCallDefinition {
gas_limit: None,
blob_data: None,
authorization_address: None,
access_list: None,
for_all_accounts: false,
}
}
Expand Down Expand Up @@ -118,6 +123,10 @@ impl FunctionCallDefinition {
self.authorization_address = Some(auth_addr.as_ref().to_owned());
self
}
pub fn with_access_list(mut self, access_list: Vec<AccessListItem>) -> Self {
self.access_list = Some(access_list);
self
}
pub fn with_for_all_accounts(mut self, for_all_accounts: bool) -> Self {
self.for_all_accounts = for_all_accounts;
self
Expand Down Expand Up @@ -156,6 +165,7 @@ pub struct FunctionCallDefinitionStrict {
pub gas_limit: Option<u64>,
pub sidecar: Option<BlobTransactionSidecar>,
pub authorization: Option<Vec<SignedAuthorization>>,
pub access_list: Option<Vec<AccessListItem>>,
}

#[derive(Clone, Deserialize, Debug, Serialize)]
Expand Down Expand Up @@ -221,4 +231,29 @@ mod tests {
.with_for_all_accounts(false);
assert!(!def.for_all_accounts);
}

#[test]
fn access_list_parses_from_toml() {
let toml = r#"
to = "0x1234567890123456789012345678901234567890"
from_pool = "test_pool"
signature = "test()"

[[access_list]]
address = "0x4200000000000000000000000000000000000022"
storageKeys = [
"0x0100000000000000000000000000000000000000000000000000000000000000",
"0x0300000000000000000000000000000000000000000000000000000000000000",
]
"#;
let def: FunctionCallDefinition = toml::from_str(toml).unwrap();
let access_list = def.access_list.unwrap();

assert_eq!(access_list.len(), 1);
assert_eq!(
access_list[0].address.to_string(),
"0x4200000000000000000000000000000000000022"
);
assert_eq!(access_list[0].storage_keys.len(), 2);
}
}
108 changes: 107 additions & 1 deletion crates/core/src/generator/templater.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::{
use alloy::{
hex::{FromHex, ToHexExt},
primitives::{Address, Bytes, FixedBytes, TxKind, U256},
rpc::types::TransactionRequest,
rpc::types::{AccessList, TransactionRequest},
};
use std::collections::HashMap;
use thiserror::Error;
Expand Down Expand Up @@ -225,6 +225,7 @@ where
.as_ref()
.map(|x| self.replace_placeholders(x, placeholder_map))
.and_then(|s| s.parse::<U256>().ok());
let access_list = funcdef.access_list.to_owned().map(AccessList::from);

Ok(TransactionRequest {
to: Some(TxKind::Call(to)),
Expand All @@ -234,6 +235,7 @@ where
gas: funcdef.gas_limit,
sidecar: funcdef.sidecar.as_ref().map(|sc| sc.to_owned().into()),
authorization_list: funcdef.authorization.to_owned(),
access_list,
..Default::default()
})
}
Expand Down Expand Up @@ -285,3 +287,107 @@ where
Ok(tx)
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::generator::{function_def::FunctionCallDefinitionStrict, util::complete_tx_request};
use alloy::consensus::TxType;
use alloy::primitives::B256;
use alloy::rpc::types::AccessListItem;
use std::collections::HashMap;

struct TestTemplater;

impl Templater<String> for TestTemplater {
fn replace_placeholders(
&self,
input: &str,
placeholder_map: &HashMap<String, String>,
) -> String {
let mut output = input.to_owned();
for (key, value) in placeholder_map {
output = output.replace(&format!("{{{key}}}"), value);
}
output
}

fn terminator_start(&self, input: &str) -> Option<usize> {
input.find('{')
}

fn terminator_end(&self, input: &str) -> Option<usize> {
input.find('}')
}

fn copy_end(&self, input: &str, last_end: usize) -> String {
input[last_end..].to_string()
}

fn num_placeholders(&self, input: &str) -> usize {
input.matches('{').count()
}

fn find_key(&self, input: &str) -> Option<(String, usize)> {
let start = self.terminator_start(input)?;
let end = self.terminator_end(input)?;
Some((input[start + 1..end].to_string(), end))
}
}

#[test]
fn template_function_call_threads_access_list_into_request() {
let templater = TestTemplater;
let access_list_address = "0x4200000000000000000000000000000000000022";
let storage_key = "0x0100000000000000000000000000000000000000000000000000000000000000";
let second_storage_key =
"0x0300000000000000000000000000000000000000000000000000000000000000";
let placeholder_map = HashMap::new();
let funcdef = FunctionCallDefinitionStrict {
to: access_list_address.to_string(),
from: Address::ZERO,
signature: "validate()".to_string(),
args: vec![],
value: None,
fuzz: vec![],
kind: None,
gas_limit: Some(200_000),
sidecar: None,
authorization: None,
access_list: Some(vec![AccessListItem {
address: access_list_address.parse::<Address>().unwrap(),
storage_keys: vec![
storage_key.parse::<B256>().unwrap(),
second_storage_key.parse::<B256>().unwrap(),
],
}]),
};

let mut tx = templater
.template_function_call(&funcdef, &placeholder_map)
.unwrap();
let access_list = tx.access_list.as_ref().unwrap();

assert_eq!(access_list.len(), 1);
assert_eq!(
access_list[0].address,
access_list_address.parse::<Address>().unwrap()
);
assert_eq!(access_list[0].storage_keys.len(), 2);
assert_eq!(
access_list[0].storage_keys[0],
storage_key.parse::<B256>().unwrap()
);
assert_eq!(
access_list[0].storage_keys[1],
second_storage_key.parse::<B256>().unwrap()
);

complete_tx_request(&mut tx, TxType::Eip1559, 10, 1, 200_000, 1, 0);

assert_eq!(tx.access_list.unwrap().len(), 1);
assert_eq!(tx.max_fee_per_gas, Some(10));
assert_eq!(tx.max_priority_fee_per_gas, Some(1));
assert_eq!(tx.chain_id, Some(1));
}
}
1 change: 1 addition & 0 deletions crates/core/src/generator/trait.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ where
gas_limit: funcdef.gas_limit.to_owned(),
sidecar: funcdef.sidecar_data()?,
authorization: signed_auth.map(|a| vec![a]),
access_list: funcdef.access_list.to_owned(),
})
}

Expand Down
36 changes: 36 additions & 0 deletions crates/testfile/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,42 @@ pub mod tests {
}
}

#[test]
fn parses_spam_tx_access_list_toml() {
let test_file = TestConfig::from_str(
r#"
[[spam]]
[spam.tx]
to = "0x4200000000000000000000000000000000000022"
from_pool = "spammers"
signature = "validate()"
gas_limit = 200000

[[spam.tx.access_list]]
address = "0x4200000000000000000000000000000000000022"
storageKeys = [
"0x0100000000000000000000000000000000000000000000000000000000000000",
"0x0300000000000000000000000000000000000000000000000000000000000000",
]
"#,
)
.unwrap();
let spam = test_file.spam.unwrap();

match &spam[0] {
SpamRequest::Tx(fncall) => {
let access_list = fncall.access_list.as_ref().unwrap();
assert_eq!(access_list.len(), 1);
assert_eq!(
access_list[0].address.to_string(),
"0x4200000000000000000000000000000000000022"
);
assert_eq!(access_list[0].storage_keys.len(), 2);
}
SpamRequest::Bundle(_) => panic!("expected SpamRequest::Tx"),
}
}

fn repo_root_path() -> std::path::PathBuf {
let mut dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
dir.pop(); // crates
Expand Down
24 changes: 24 additions & 0 deletions docs/creating_scenarios.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,30 @@ args = ["1350000"]
gas_limit = 1350000
```

### access lists

Spam transactions can include EIP-2930 access-list entries. This is useful for workloads that already know which account and storage keys need to be warm, while still sending EIP-1559 transactions by default.

```toml
[[spam]]

[spam.tx]
to = "0x1111111111111111111111111111111111111111"
from_pool = "bluepool"
signature = "touch(bytes32 lookupKey)"
args = [
"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
]
gas_limit = 200000

[[spam.tx.access_list]]
address = "0x1111111111111111111111111111111111111111"
storageKeys = [
"0x0100000000000000000000000000000000000000000000000000000000000000",
"0x0300000000000000000000000000000000000000000000000000000000000000",
]
```

### sending bundles

The `[spam.tx]` directive sends a mempool transaction using `eth_sendRawTransaction`, but Contender also supports bundles.
Expand Down