diff --git a/crates/cli/src/server/static/openrpc.json b/crates/cli/src/server/static/openrpc.json index cbb7ef58..7804377a 100644 --- a/crates/cli/src/server/static/openrpc.json +++ b/crates/cli/src/server/static/openrpc.json @@ -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}, @@ -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}, @@ -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"]}, diff --git a/crates/core/src/generator/function_def.rs b/crates/core/src/generator/function_def.rs index 3cc307cd..4ff520f8 100644 --- a/crates/core/src/generator/function_def.rs +++ b/crates/core/src/generator/function_def.rs @@ -4,6 +4,7 @@ use alloy::{ eips::eip7702::SignedAuthorization, hex::{FromHex, ToHexExt}, primitives::{Address, Bytes, U256}, + rpc::types::AccessListItem, }; use serde::{Deserialize, Serialize}; @@ -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, + /// Optional EIP-2930 access list entries to include in the transaction. + #[serde(skip_serializing_if = "Option::is_none")] + pub access_list: Option>, /// 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")] @@ -69,6 +73,7 @@ impl FunctionCallDefinition { gas_limit: None, blob_data: None, authorization_address: None, + access_list: None, for_all_accounts: false, } } @@ -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) -> 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 @@ -156,6 +165,7 @@ pub struct FunctionCallDefinitionStrict { pub gas_limit: Option, pub sidecar: Option, pub authorization: Option>, + pub access_list: Option>, } #[derive(Clone, Deserialize, Debug, Serialize)] @@ -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); + } } diff --git a/crates/core/src/generator/templater.rs b/crates/core/src/generator/templater.rs index 8c58b80e..b1d52f97 100644 --- a/crates/core/src/generator/templater.rs +++ b/crates/core/src/generator/templater.rs @@ -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; @@ -225,6 +225,7 @@ where .as_ref() .map(|x| self.replace_placeholders(x, placeholder_map)) .and_then(|s| s.parse::().ok()); + let access_list = funcdef.access_list.to_owned().map(AccessList::from); Ok(TransactionRequest { to: Some(TxKind::Call(to)), @@ -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() }) } @@ -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 for TestTemplater { + fn replace_placeholders( + &self, + input: &str, + placeholder_map: &HashMap, + ) -> 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 { + input.find('{') + } + + fn terminator_end(&self, input: &str) -> Option { + 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::
().unwrap(), + storage_keys: vec![ + storage_key.parse::().unwrap(), + second_storage_key.parse::().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::
().unwrap() + ); + assert_eq!(access_list[0].storage_keys.len(), 2); + assert_eq!( + access_list[0].storage_keys[0], + storage_key.parse::().unwrap() + ); + assert_eq!( + access_list[0].storage_keys[1], + second_storage_key.parse::().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)); + } +} diff --git a/crates/core/src/generator/trait.rs b/crates/core/src/generator/trait.rs index 0992ae95..2dfeda93 100644 --- a/crates/core/src/generator/trait.rs +++ b/crates/core/src/generator/trait.rs @@ -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(), }) } diff --git a/crates/testfile/src/lib.rs b/crates/testfile/src/lib.rs index 197c7a8a..d1241de6 100644 --- a/crates/testfile/src/lib.rs +++ b/crates/testfile/src/lib.rs @@ -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 diff --git a/docs/creating_scenarios.md b/docs/creating_scenarios.md index 6721d11b..d2b98aa1 100644 --- a/docs/creating_scenarios.md +++ b/docs/creating_scenarios.md @@ -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.