Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
43 changes: 43 additions & 0 deletions crates/core/src/generator/function_def.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,24 @@ 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<AccessListDefinition>>,
/// 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")]
pub for_all_accounts: bool,
}

/// User-facing EIP-2930 access list entry.
#[derive(Clone, Deserialize, Debug, Eq, PartialEq, Serialize)]
pub struct AccessListDefinition {
/// Account address to warm before execution.
pub address: String,
/// Storage keys to warm for `address`.
pub storage_keys: Vec<String>,
}
Comment thread
karlfloersch marked this conversation as resolved.
Outdated

/// User-facing definition of a function call to be executed.
#[derive(Clone, Deserialize, Debug, Serialize)]
pub struct BundleCallDefinition {
Expand All @@ -69,6 +81,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 +131,10 @@ impl FunctionCallDefinition {
self.authorization_address = Some(auth_addr.as_ref().to_owned());
self
}
pub fn with_access_list(mut self, access_list: Vec<AccessListDefinition>) -> 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 +173,7 @@ pub struct FunctionCallDefinitionStrict {
pub gas_limit: Option<u64>,
pub sidecar: Option<BlobTransactionSidecar>,
pub authorization: Option<Vec<SignedAuthorization>>,
pub access_list: Option<Vec<AccessListDefinition>>,
}

#[derive(Clone, Deserialize, Debug, Serialize)]
Expand Down Expand Up @@ -221,4 +239,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"
storage_keys = [
"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,
"0x4200000000000000000000000000000000000022"
);
assert_eq!(access_list[0].storage_keys.len(), 2);
}
}
177 changes: 174 additions & 3 deletions crates/core/src/generator/templater.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ use crate::{
db::{DbError, DbOps},
generator::{
constants::{SENDER_KEY, SETCODE_KEY},
function_def::{FunctionCallDefinition, FunctionCallDefinitionStrict},
function_def::{
AccessListDefinition, FunctionCallDefinition, FunctionCallDefinitionStrict,
},
util::{encode_calldata, scenario_db_key, UtilError},
CreateDefinition,
},
};
use alloy::{
eips::eip2930::{AccessList, AccessListItem},
hex::{FromHex, ToHexExt},
primitives::{Address, Bytes, FixedBytes, TxKind, U256},
primitives::{Address, Bytes, FixedBytes, TxKind, B256, U256},
rpc::types::TransactionRequest,
};
use std::collections::HashMap;
Expand All @@ -33,6 +36,9 @@ pub enum TemplaterError {
#[error("failed to parse address '{0}'")]
ParseAddressFailed(String),

#[error("failed to parse storage key '{0}'")]
ParseStorageKeyFailed(String),

#[error("templater util error")]
Util(#[from] UtilError),
}
Expand Down Expand Up @@ -105,7 +111,7 @@ where

/// Finds {placeholders} in `fncall` and looks them up in `db`,
/// then inserts the values it finds into `placeholder_map`.
/// NOTE: only finds placeholders in `args`, `authorization_addr`, and `to` fields.
/// NOTE: only finds placeholders in `args`, `authorization_addr`, `access_list`, and `to` fields.
fn find_fncall_placeholders(
&self,
fncall: &FunctionCallDefinition,
Expand Down Expand Up @@ -155,9 +161,63 @@ where
scenario_label,
)?;
}
if let Some(access_list) = &fncall.access_list {
for entry in access_list {
self.find_placeholder_values(
&entry.address,
placeholder_map,
db,
rpc_url,
genesis_hash,
scenario_label,
)?;
for storage_key in &entry.storage_keys {
self.find_placeholder_values(
storage_key,
placeholder_map,
db,
rpc_url,
genesis_hash,
scenario_label,
)?;
}
}
}
Ok(())
}

fn template_access_list(
&self,
access_list: &[AccessListDefinition],
placeholder_map: &HashMap<K, String>,
) -> Result<AccessList> {
access_list
.iter()
.map(|entry| {
let address = self.replace_placeholders(&entry.address, placeholder_map);
let address = address
.parse::<Address>()
.map_err(|_| TemplaterError::ParseAddressFailed(address))?;
let storage_keys = entry
.storage_keys
.iter()
.map(|storage_key| {
let storage_key = self.replace_placeholders(storage_key, placeholder_map);
storage_key
.parse::<B256>()
.map_err(|_| TemplaterError::ParseStorageKeyFailed(storage_key))
})
.collect::<Result<Vec<_>>>()?;

Ok(AccessListItem {
address,
storage_keys,
})
})
.collect::<Result<Vec<_>>>()
.map(AccessList::from)
}

/// Finds {placeholders} in create constructor args and updates the placeholder map.
fn find_create_placeholders(
&self,
Expand Down Expand Up @@ -225,6 +285,11 @@ where
.as_ref()
.map(|x| self.replace_placeholders(x, placeholder_map))
.and_then(|s| s.parse::<U256>().ok());
let access_list = funcdef
.access_list
.as_deref()
.map(|list| self.template_access_list(list, placeholder_map))
.transpose()?;

Ok(TransactionRequest {
to: Some(TxKind::Call(to)),
Expand All @@ -234,6 +299,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 +351,108 @@ where
Ok(tx)
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::generator::{
function_def::{AccessListDefinition, FunctionCallDefinitionStrict},
util::complete_tx_request,
};
use alloy::consensus::TxType;
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 placeholder_storage_key =
"0x0300000000000000000000000000000000000000000000000000000000000000";
let placeholder_map = HashMap::from([(
"lookup_key".to_string(),
placeholder_storage_key.to_string(),
)]);
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![AccessListDefinition {
address: access_list_address.to_string(),
storage_keys: vec![storage_key.to_string(), "{lookup_key}".to_string()],
}]),
};

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],
placeholder_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"
storage_keys = [
"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,
"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"
storage_keys = [
"0x0100000000000000000000000000000000000000000000000000000000000000",
"0x0300000000000000000000000000000000000000000000000000000000000000",
]
```

### sending bundles

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