Skip to content
Merged
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
38 changes: 34 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,11 @@ revm = { version = "31.0.2", default-features = false }
revm-bytecode = { version = "7.1.1", default-features = false }

# alloy
alloy-rlp = "0.3.10"
alloy-trie = "0.9.1"
alloy-eips = "1.0.41"
alloy-serde = "1.0.41"
alloy-eip7928 = "0.3.0"
alloy-genesis = "1.0.41"
alloy-signer-local = "1.0.41"
alloy-hardforks = "0.4.4"
Expand All @@ -123,6 +125,9 @@ op-alloy-rpc-jsonrpsee = "0.22.0"
op-alloy-rpc-types-engine = "0.22.0"
alloy-op-evm = { version = "0.23.3", default-features = false }

# op-revm
op-revm = { version = "12.0.2", default-features = false }

# tokio
tokio = "1.48.0"
tokio-stream = "0.1.17"
Expand Down
22 changes: 21 additions & 1 deletion crates/fbal/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[package]
name = "base-reth-fbal"
name = "base-fbal"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
Expand All @@ -12,5 +12,25 @@ description = "FBAL library crate"
workspace = true

[dependencies]
alloy-primitives.workspace = true
alloy-eip7928 = {workspace = true, features = ["serde", "rlp"]}
alloy-rlp = {workspace = true, features = ["derive"]}
tracing.workspace = true
revm.workspace = true
serde.workspace = true

[dev-dependencies]
op-revm.workspace = true
eyre.workspace = true
reth-optimism-chainspec.workspace = true
reth-optimism-evm.workspace = true
alloy-consensus.workspace = true
alloy-contract.workspace = true
alloy-sol-macro = { workspace = true, features = ["json"] }
alloy-sol-types.workspace = true
reth-evm.workspace = true
serde_json.workspace = true

[[test]]
name = "builder"
path = "tests/builder/main.rs"
42 changes: 42 additions & 0 deletions crates/fbal/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# `base-fbal`

A library to build and process Flashblock-level Access Lists (FBALs).

## Overview

This crate provides types and utilities for tracking account and storage changes during EVM transaction execution, producing access lists that can be used by downstream consumers to understand exactly what state was read or modified.

- `FBALBuilderDb<DB>` - A database wrapper that tracks reads and writes during transaction execution.
- `FlashblockAccessListBuilder` - A builder pattern for constructing access lists from tracked changes.
- `FlashblockAccessList` - The final access list containing all account changes, storage changes, and metadata.

## Usage

Wrap your database with `FBALBuilderDb`, execute transactions, then call `finish()` to retrieve the builder:

```rust,ignore
use base_fbal::{FBALBuilderDb, FlashblockAccessList};
use revm::database::InMemoryDB;

// Create a wrapped database
let db = InMemoryDB::default();
let mut fbal_db = FBALBuilderDb::new(db);

// Execute transactions, calling set_index() before each one
for (i, tx) in transactions.into_iter().enumerate() {
fbal_db.set_index(i as u64);
// ... execute transaction with fbal_db ...
fbal_db.commit(state_changes);
}

// Build the access list
let builder = fbal_db.finish()?;
let access_list = builder.build(0, max_tx_index);
```

## Features

- Tracks balance, nonce, and code changes per account
- Tracks storage slot reads and writes
- Associates each change with its transaction index
- Produces RLP-encodable access lists with a commitment hash
118 changes: 118 additions & 0 deletions crates/fbal/src/builder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
use std::u64;

use alloy_eip7928::{
AccountChanges, BalanceChange, CodeChange, NonceChange, SlotChanges, StorageChange,
};
use alloy_primitives::{Address, U256};
use revm::{
primitives::{HashMap, HashSet},
state::Bytecode,
};

use crate::FlashblockAccessList;

/// A builder type for [`FlashblockAccessList`]
#[derive(Default, Debug, Clone, PartialEq, Eq)]
pub struct FlashblockAccessListBuilder {
/// Mapping from Address -> [`AccountChangesBuilder`]
pub changes: HashMap<Address, AccountChangesBuilder>,
}

impl FlashblockAccessListBuilder {
/// Creates a new [`FlashblockAccessListBuilder`]
pub fn new() -> Self {
Self { changes: Default::default() }
}

/// Merges another [`FlashblockAccessListBuilder`] with this one
pub fn merge(&mut self, other: Self) {
for (address, changes) in other.changes.into_iter() {
self.changes
.entry(address)
.and_modify(|prev| prev.merge(changes.clone()))
.or_insert(changes);
}
}

/// Consumes the builder and produces a [`FlashblockAccessList`]
pub fn build(self, min_tx_index: u64, max_tx_index: u64) -> FlashblockAccessList {
let mut changes: Vec<_> = self.changes.into_iter().map(|(k, v)| v.build(k)).collect();
changes.sort_unstable_by_key(|a| a.address);

FlashblockAccessList::build(changes, min_tx_index, max_tx_index)
}
}

/// A builder type for [`AccountChanges`]
#[derive(Default, Debug, Clone, PartialEq, Eq)]
pub struct AccountChangesBuilder {
/// Mapping from Storage Slot -> (Transaction Index -> New Value)
pub storage_changes: HashMap<U256, HashMap<u64, U256>>,
/// Set of storage slots
pub storage_reads: HashSet<U256>,
/// Mapping from Transaction Index -> New Balance
pub balance_changes: HashMap<u64, U256>,
/// Mapping from Transaction Index -> New Nonce
pub nonce_changes: HashMap<u64, u64>,
/// Mapping from Transaction Index -> New Code
pub code_changes: HashMap<u64, Bytecode>,
}

impl AccountChangesBuilder {
/// Merges another [`AccountChangesBuilder`] with this one
pub fn merge(&mut self, other: Self) {
for (slot, sc) in other.storage_changes {
self.storage_changes
.entry(slot)
.and_modify(|prev| prev.extend(sc.clone()))
.or_insert(sc);
}
self.storage_reads.extend(other.storage_reads);
self.balance_changes.extend(other.balance_changes);
self.nonce_changes.extend(other.nonce_changes);
self.code_changes.extend(other.code_changes);
}

/// Consumes the builder and produces [`AccountChanges`]
pub fn build(mut self, address: Address) -> AccountChanges {
AccountChanges {
address,
storage_changes: self
.storage_changes
.drain()
.map(|(slot, sc)| SlotChanges {
slot: slot.into(),
changes: sc
.into_iter()
.map(|(tx_idx, val)| StorageChange {
block_access_index: tx_idx,
new_value: val.into(),
})
.collect(),
})
.collect(),
storage_reads: self.storage_reads.into_iter().collect(),
balance_changes: self
.balance_changes
.into_iter()
.map(|(tx_idx, val)| BalanceChange {
block_access_index: tx_idx,
post_balance: val,
})
.collect(),
nonce_changes: self
.nonce_changes
.into_iter()
.map(|(tx_idx, val)| NonceChange { block_access_index: tx_idx, new_nonce: val })
.collect(),
code_changes: self
.code_changes
.into_iter()
.map(|(tx_idx, bc)| CodeChange {
block_access_index: tx_idx,
new_code: bc.original_bytes(),
})
.collect(),
}
}
}
Loading