diff --git a/Cargo.lock b/Cargo.lock index b283e8eaf..c7eca223b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2881,6 +2881,33 @@ dependencies = [ "tokio", ] +[[package]] +name = "exex-rollup" +version = "0.0.0" +dependencies = [ + "alloy-rlp", + "alloy-sol-types", + "eyre", + "futures", + "once_cell", + "reth", + "reth-cli-runner", + "reth-exex", + "reth-interfaces", + "reth-node-api", + "reth-node-core", + "reth-node-ethereum", + "reth-primitives", + "reth-provider", + "reth-revm", + "reth-tracing", + "reth-trie", + "rusqlite", + "secp256k1", + "serde_json", + "tokio", +] + [[package]] name = "eyre" version = "0.6.12" diff --git a/Cargo.toml b/Cargo.toml index 70e36cd94..e7bf2ec5c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -89,6 +89,7 @@ members = [ "examples/custom-inspector/", "examples/exex/minimal/", "examples/exex/op-bridge/", + "examples/exex/rollup/", "examples/db-access", "testing/ef-tests/", "testing/testing-utils", diff --git a/crates/primitives/src/block.rs b/crates/primitives/src/block.rs index 864e7954f..4b4831b9c 100644 --- a/crates/primitives/src/block.rs +++ b/crates/primitives/src/block.rs @@ -49,7 +49,7 @@ pub struct Block { } impl Block { - /// Create SealedBLock that will create all header hashes. + /// Calculate the header hash and seal the block so that it can't be changed. pub fn seal_slow(self) -> SealedBlock { SealedBlock { header: self.header.seal_slow(), @@ -175,7 +175,7 @@ impl TryFrom for Block { .collect(), reth_rpc_types::BlockTransactions::Hashes(_) | reth_rpc_types::BlockTransactions::Uncle => { - return Err(ConversionError::MissingFullTransactions); + return Err(ConversionError::MissingFullTransactions) } }; transactions? @@ -214,6 +214,12 @@ impl BlockWithSenders { SealedBlockWithSenders { block: block.seal(hash), senders } } + /// Calculate the header hash and seal the block with senders so that it can't be changed. + #[inline] + pub fn seal_slow(self) -> SealedBlockWithSenders { + SealedBlockWithSenders { block: self.block.seal_slow(), senders: self.senders } + } + /// Split Structure to its components #[inline] pub fn into_components(self) -> (Block, Vec
) { @@ -456,7 +462,7 @@ impl std::ops::DerefMut for SealedBlock { } /// Sealed block with senders recovered from transactions. -#[derive(Debug, Clone, PartialEq, Eq, Default)] +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] pub struct SealedBlockWithSenders { /// Sealed block pub block: SealedBlock, diff --git a/crates/storage/provider/src/bundle_state/mod.rs b/crates/storage/provider/src/bundle_state/mod.rs index 3f5da6ec6..5df4a213a 100644 --- a/crates/storage/provider/src/bundle_state/mod.rs +++ b/crates/storage/provider/src/bundle_state/mod.rs @@ -10,4 +10,4 @@ pub use bundle_state_with_receipts::{ }; pub use hashed_state_changes::HashedStateChanges; pub use state_changes::StateChanges; -pub use state_reverts::StateReverts; +pub use state_reverts::{StateReverts, StorageRevertsIter}; diff --git a/crates/storage/provider/src/bundle_state/state_reverts.rs b/crates/storage/provider/src/bundle_state/state_reverts.rs index e61572cf5..006f87b40 100644 --- a/crates/storage/provider/src/bundle_state/state_reverts.rs +++ b/crates/storage/provider/src/bundle_state/state_reverts.rs @@ -108,7 +108,8 @@ impl StateReverts { /// Iterator over storage reverts. /// See [StorageRevertsIter::next] for more details. -struct StorageRevertsIter { +#[allow(missing_debug_implementations)] +pub struct StorageRevertsIter { reverts: Peekable, wiped: Peekable, } @@ -118,7 +119,8 @@ where R: Iterator, W: Iterator, { - fn new( + /// Create a new iterator over storage reverts. + pub fn new( reverts: impl IntoIterator, wiped: impl IntoIterator, ) -> Self { diff --git a/examples/README.md b/examples/README.md index ea2c87c1b..0885aa294 100644 --- a/examples/README.md +++ b/examples/README.md @@ -27,6 +27,7 @@ to make a PR! | ---------------------------------- | --------------------------------------------------------------------------------- | | [Minimal ExEx](./exex/minimal) | Illustrates how to build a simple ExEx | | [OP Bridge ExEx](./exex/op-bridge) | Illustrates an ExEx that decodes Optimism deposit and withdrawal receipts from L1 | +| [Rollup](./exex/rollup) | Illustrates a rollup ExEx that derives the state from L1 | ## RPC diff --git a/examples/exex/rollup/Cargo.toml b/examples/exex/rollup/Cargo.toml new file mode 100644 index 000000000..8d338c241 --- /dev/null +++ b/examples/exex/rollup/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "exex-rollup" +version = "0.0.0" +publish = false +edition.workspace = true +license.workspace = true + +[dependencies] +# reth +reth.workspace = true +reth-cli-runner.workspace = true +reth-exex.workspace = true +reth-interfaces.workspace = true +reth-node-api.workspace = true +reth-node-core.workspace = true +reth-node-ethereum.workspace = true +reth-primitives.workspace = true +reth-provider.workspace = true +reth-revm.workspace = true +reth-tracing.workspace = true +reth-trie.workspace = true + +# async +tokio.workspace = true +futures.workspace = true + +# misc +alloy-sol-types = { workspace = true, features = ["json"] } +alloy-rlp.workspace = true +eyre.workspace = true +rusqlite = { version = "0.31.0", features = ["bundled"] } +serde_json.workspace = true +once_cell.workspace = true + +[dev-dependencies] +reth-interfaces = { workspace = true, features = ["test-utils"] } +secp256k1.workspace = true + diff --git a/examples/exex/rollup/rollup_abi.json b/examples/exex/rollup/rollup_abi.json new file mode 100644 index 000000000..08bc23f0e --- /dev/null +++ b/examples/exex/rollup/rollup_abi.json @@ -0,0 +1,626 @@ +[ + { + "inputs": [ + { "internalType": "address", "name": "admin", "type": "address" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { "inputs": [], "name": "AccessControlBadConfirmation", "type": "error" }, + { + "inputs": [ + { "internalType": "uint48", "name": "schedule", "type": "uint48" } + ], + "name": "AccessControlEnforcedDefaultAdminDelay", + "type": "error" + }, + { + "inputs": [], + "name": "AccessControlEnforcedDefaultAdminRules", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "defaultAdmin", + "type": "address" + } + ], + "name": "AccessControlInvalidDefaultAdmin", + "type": "error" + }, + { + "inputs": [ + { "internalType": "address", "name": "account", "type": "address" }, + { + "internalType": "bytes32", + "name": "neededRole", + "type": "bytes32" + } + ], + "name": "AccessControlUnauthorizedAccount", + "type": "error" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "expected", "type": "uint256" } + ], + "name": "BadSequence", + "type": "error" + }, + { "inputs": [], "name": "BadSignature", "type": "error" }, + { "inputs": [], "name": "BlockExpired", "type": "error" }, + { + "inputs": [ + { + "internalType": "address", + "name": "sequencer", + "type": "address" + } + ], + "name": "NotSequencer", + "type": "error" + }, + { "inputs": [], "name": "OrderExpired", "type": "error" }, + { + "inputs": [ + { "internalType": "uint8", "name": "bits", "type": "uint8" }, + { "internalType": "uint256", "name": "value", "type": "uint256" } + ], + "name": "SafeCastOverflowedUintDowncast", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sequencer", + "type": "address" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "rollupChainId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "sequence", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "confirmBy", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "gasLimit", + "type": "uint256" + }, + { + "internalType": "address", + "name": "rewardAddress", + "type": "address" + } + ], + "indexed": true, + "internalType": "struct CalldataZenith.BlockHeader", + "name": "header", + "type": "tuple" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "blockData", + "type": "bytes" + } + ], + "name": "BlockSubmitted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [], + "name": "DefaultAdminDelayChangeCanceled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint48", + "name": "newDelay", + "type": "uint48" + }, + { + "indexed": false, + "internalType": "uint48", + "name": "effectSchedule", + "type": "uint48" + } + ], + "name": "DefaultAdminDelayChangeScheduled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [], + "name": "DefaultAdminTransferCanceled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "newAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint48", + "name": "acceptSchedule", + "type": "uint48" + } + ], + "name": "DefaultAdminTransferScheduled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "rollupRecipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Enter", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "hostRecipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "ExitFilled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "previousAdminRole", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "newAdminRole", + "type": "bytes32" + } + ], + "name": "RoleAdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "RoleGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "RoleRevoked", + "type": "event" + }, + { "stateMutability": "payable", "type": "fallback" }, + { + "inputs": [], + "name": "DEFAULT_ADMIN_ROLE", + "outputs": [ + { "internalType": "bytes32", "name": "", "type": "bytes32" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "SEQUENCER_ROLE", + "outputs": [ + { "internalType": "bytes32", "name": "", "type": "bytes32" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "acceptDefaultAdminTransfer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "newAdmin", "type": "address" } + ], + "name": "beginDefaultAdminTransfer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "rollupChainId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "sequence", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "confirmBy", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "gasLimit", + "type": "uint256" + }, + { + "internalType": "address", + "name": "rewardAddress", + "type": "address" + } + ], + "internalType": "struct CalldataZenith.BlockHeader", + "name": "header", + "type": "tuple" + }, + { "internalType": "bytes", "name": "blockData", "type": "bytes" } + ], + "name": "blockCommitment", + "outputs": [ + { "internalType": "bytes32", "name": "commit", "type": "bytes32" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "cancelDefaultAdminTransfer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint48", "name": "newDelay", "type": "uint48" } + ], + "name": "changeDefaultAdminDelay", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "defaultAdmin", + "outputs": [ + { "internalType": "address", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "defaultAdminDelay", + "outputs": [{ "internalType": "uint48", "name": "", "type": "uint48" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "defaultAdminDelayIncreaseWait", + "outputs": [{ "internalType": "uint48", "name": "", "type": "uint48" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "rollupRecipient", + "type": "address" + } + ], + "name": "enter", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "internalType": "struct HostPassage.ExitOrder[]", + "name": "orders", + "type": "tuple[]" + } + ], + "name": "fulfillExits", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "role", "type": "bytes32" } + ], + "name": "getRoleAdmin", + "outputs": [ + { "internalType": "bytes32", "name": "", "type": "bytes32" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "role", "type": "bytes32" }, + { "internalType": "address", "name": "account", "type": "address" } + ], + "name": "grantRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "role", "type": "bytes32" }, + { "internalType": "address", "name": "account", "type": "address" } + ], + "name": "hasRole", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "", "type": "uint256" } + ], + "name": "nextSequence", + "outputs": [ + { "internalType": "uint256", "name": "", "type": "uint256" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { "internalType": "address", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pendingDefaultAdmin", + "outputs": [ + { + "internalType": "address", + "name": "newAdmin", + "type": "address" + }, + { "internalType": "uint48", "name": "schedule", "type": "uint48" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pendingDefaultAdminDelay", + "outputs": [ + { "internalType": "uint48", "name": "newDelay", "type": "uint48" }, + { "internalType": "uint48", "name": "schedule", "type": "uint48" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "role", "type": "bytes32" }, + { "internalType": "address", "name": "account", "type": "address" } + ], + "name": "renounceRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "role", "type": "bytes32" }, + { "internalType": "address", "name": "account", "type": "address" } + ], + "name": "revokeRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "rollbackDefaultAdminDelay", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "rollupChainId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "sequence", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "confirmBy", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "gasLimit", + "type": "uint256" + }, + { + "internalType": "address", + "name": "rewardAddress", + "type": "address" + } + ], + "internalType": "struct CalldataZenith.BlockHeader", + "name": "header", + "type": "tuple" + }, + { "internalType": "bytes", "name": "blockData", "type": "bytes" }, + { "internalType": "uint8", "name": "v", "type": "uint8" }, + { "internalType": "bytes32", "name": "r", "type": "bytes32" }, + { "internalType": "bytes32", "name": "s", "type": "bytes32" } + ], + "name": "submitBlock", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { "stateMutability": "payable", "type": "receive" } +] diff --git a/examples/exex/rollup/src/db.rs b/examples/exex/rollup/src/db.rs new file mode 100644 index 000000000..39c2b418b --- /dev/null +++ b/examples/exex/rollup/src/db.rs @@ -0,0 +1,460 @@ +use std::{ + collections::{hash_map::Entry, HashMap}, + str::FromStr, + sync::{Arc, Mutex, MutexGuard}, +}; + +use reth_primitives::{ + revm_primitives::{AccountInfo, Bytecode}, + Address, Bytes, SealedBlockWithSenders, StorageEntry, B256, U256, +}; +use reth_provider::{bundle_state::StorageRevertsIter, OriginalValuesKnown}; +use reth_revm::db::{ + states::{PlainStorageChangeset, PlainStorageRevert}, + BundleState, +}; +use rusqlite::Connection; + +/// Type used to initialize revms bundle state. +type BundleStateInit = + HashMap, Option, HashMap)>; + +/// Types used inside RevertsInit to initialize revms reverts. +pub type AccountRevertInit = (Option>, Vec); + +/// Type used to initialize revms reverts. +pub type RevertsInit = HashMap; + +pub struct Database { + connection: Arc>, +} + +impl Database { + /// Create new database with the provided connection. + pub fn new(connection: Connection) -> eyre::Result { + let database = Self { connection: Arc::new(Mutex::new(connection)) }; + database.create_tables()?; + Ok(database) + } + + fn connection(&self) -> MutexGuard<'_, Connection> { + self.connection.lock().expect("failed to acquire database lock") + } + + fn create_tables(&self) -> eyre::Result<()> { + self.connection().execute_batch( + "CREATE TABLE IF NOT EXISTS block ( + id INTEGER PRIMARY KEY, + number TEXT UNIQUE, + data TEXT + ); + CREATE TABLE IF NOT EXISTS account ( + id INTEGER PRIMARY KEY, + address TEXT UNIQUE, + data TEXT + ); + CREATE TABLE IF NOT EXISTS account_revert ( + id INTEGER PRIMARY KEY, + block_number TEXT, + address TEXT, + data TEXT, + UNIQUE (block_number, address) + ); + CREATE TABLE IF NOT EXISTS storage ( + id INTEGER PRIMARY KEY, + address TEXT, + key TEXT, + data TEXT, + UNIQUE (address, key) + ); + CREATE TABLE IF NOT EXISTS storage_revert ( + id INTEGER PRIMARY KEY, + block_number TEXT, + address TEXT, + key TEXT, + data TEXT, + UNIQUE (block_number, address, key) + ); + CREATE TABLE IF NOT EXISTS bytecode ( + id INTEGER PRIMARY KEY, + hash TEXT UNIQUE, + data TEXT + );", + )?; + Ok(()) + } + + /// Insert block with bundle into the database. + pub fn insert_block_with_bundle( + &self, + block: &SealedBlockWithSenders, + bundle: BundleState, + ) -> eyre::Result<()> { + let mut connection = self.connection(); + let tx = connection.transaction()?; + + tx.execute( + "INSERT INTO block (number, data) VALUES (?, ?)", + (block.header.number.to_string(), serde_json::to_string(block)?), + )?; + + let (changeset, reverts) = bundle.into_plain_state_and_reverts(OriginalValuesKnown::Yes); + + for (address, account) in changeset.accounts { + if let Some(account) = account { + tx.execute( + "INSERT INTO account (address, data) VALUES (?, ?) ON CONFLICT(address) DO UPDATE SET data = excluded.data", + (address.to_string(), serde_json::to_string(&account)?), + )?; + } else { + tx.execute("DELETE FROM account WHERE address = ?", (address.to_string(),))?; + } + } + + if reverts.accounts.len() > 1 { + eyre::bail!("too many blocks in account reverts"); + } + for (address, account) in + reverts.accounts.first().ok_or(eyre::eyre!("no account reverts"))? + { + tx.execute( + "INSERT INTO account_revert (block_number, address, data) VALUES (?, ?, ?) ON CONFLICT(block_number, address) DO UPDATE SET data = excluded.data", + (block.header.number.to_string(), address.to_string(), serde_json::to_string(account)?), + )?; + } + + for PlainStorageChangeset { address, wipe_storage, storage } in changeset.storage { + if wipe_storage { + tx.execute("DELETE FROM storage WHERE address = ?", (address.to_string(),))?; + } + + for (key, data) in storage { + tx.execute( + "INSERT INTO storage (address, key, data) VALUES (?, ?, ?) ON CONFLICT(address, key) DO UPDATE SET data = excluded.data", + (address.to_string(), B256::from(key).to_string(), data.to_string()), + )?; + } + } + + if reverts.storage.len() > 1 { + eyre::bail!("too many blocks in storage reverts"); + } + for PlainStorageRevert { address, wiped, storage_revert } in + reverts.storage.into_iter().next().ok_or(eyre::eyre!("no storage reverts"))? + { + let storage = storage_revert + .into_iter() + .map(|(k, v)| (B256::new(k.to_be_bytes()), v)) + .collect::>(); + let wiped_storage = if wiped { get_storages(&tx, address)? } else { Vec::new() }; + for (key, data) in StorageRevertsIter::new(storage, wiped_storage) { + tx.execute( + "INSERT INTO storage_revert (block_number, address, key, data) VALUES (?, ?, ?, ?) ON CONFLICT(block_number, address, key) DO UPDATE SET data = excluded.data", + (block.header.number.to_string(), address.to_string(), key.to_string(), data.to_string()), + )?; + } + } + + for (hash, bytecode) in changeset.contracts { + tx.execute( + "INSERT INTO bytecode (hash, data) VALUES (?, ?) ON CONFLICT(hash) DO NOTHING", + (hash.to_string(), bytecode.bytes().to_string()), + )?; + } + + tx.commit()?; + + Ok(()) + } + + /// Reverts the tip block from the database, checking it against the provided block number. + /// + /// The code is adapted from + pub fn revert_tip_block(&self, block_number: U256) -> eyre::Result<()> { + let mut connection = self.connection(); + let tx = connection.transaction()?; + + let tip_block_number = tx + .query_row::( + "SELECT number FROM block ORDER BY number DESC LIMIT 1", + [], + |row| row.get(0), + ) + .map(|data| U256::from_str(&data))??; + if block_number != tip_block_number { + eyre::bail!("Reverts can only be done from the tip. Attempted to revert block {} with tip block {}", block_number, tip_block_number); + } + + tx.execute("DELETE FROM block WHERE number = ?", (block_number.to_string(),))?; + + let mut state = BundleStateInit::new(); + let mut reverts = RevertsInit::new(); + + let account_reverts = tx + .prepare("SELECT address, data FROM account_revert WHERE block_number = ?")? + .query((block_number.to_string(),))? + .mapped(|row| { + Ok(( + Address::from_str(row.get_ref(0)?.as_str()?), + serde_json::from_str::>(row.get_ref(1)?.as_str()?), + )) + }) + .map(|result| { + let (address, data) = result?; + Ok((address?, data?)) + }) + .collect::>>()?; + + for (address, old_info) in account_reverts { + // insert old info into reverts + reverts.entry(address).or_default().0 = Some(old_info.clone()); + + match state.entry(address) { + Entry::Vacant(entry) => { + let new_info = get_account(&tx, address)?; + entry.insert((old_info, new_info, HashMap::new())); + } + Entry::Occupied(mut entry) => { + // overwrite old account state + entry.get_mut().0 = old_info; + } + } + } + + let storage_reverts = tx + .prepare("SELECT address, key, data FROM storage_revert WHERE block_number = ?")? + .query((block_number.to_string(),))? + .mapped(|row| { + Ok(( + Address::from_str(row.get_ref(0)?.as_str()?), + B256::from_str(row.get_ref(1)?.as_str()?), + U256::from_str(row.get_ref(2)?.as_str()?), + )) + }) + .map(|result| { + let (address, key, data) = result?; + Ok((address?, key?, data?)) + }) + .collect::>>()?; + + for (address, key, old_data) in storage_reverts.into_iter().rev() { + let old_storage = StorageEntry { key, value: old_data }; + + // insert old info into reverts + reverts.entry(address).or_default().1.push(old_storage); + + // get account state or insert from plain state + let account_state = match state.entry(address) { + Entry::Vacant(entry) => { + let present_info = get_account(&tx, address)?; + entry.insert((present_info.clone(), present_info, HashMap::new())) + } + Entry::Occupied(entry) => entry.into_mut(), + }; + + // match storage + match account_state.2.entry(old_storage.key) { + Entry::Vacant(entry) => { + let new_value = get_storage(&tx, address, old_storage.key)?.unwrap_or_default(); + entry.insert((old_storage.value, new_value)); + } + Entry::Occupied(mut entry) => { + entry.get_mut().0 = old_storage.value; + } + }; + } + + // iterate over local plain state remove all account and all storages + for (address, (old_account, new_account, storage)) in state { + // revert account if needed + if old_account != new_account { + if let Some(account) = old_account { + upsert_account(&tx, address, |_| Ok(account))?; + } else { + delete_account(&tx, address)?; + } + } + + // revert storages + for (storage_key, (old_storage_value, _new_storage_value)) in storage { + // delete previous value + delete_storage(&tx, address, storage_key)?; + + // insert value if needed + if !old_storage_value.is_zero() { + upsert_storage(&tx, address, storage_key, old_storage_value)?; + } + } + } + + tx.commit()?; + + Ok(()) + } + + /// Get block by number. + pub fn get_block(&self, number: U256) -> eyre::Result> { + let block = self.connection().query_row::( + "SELECT data FROM block WHERE number = ?", + (number.to_string(),), + |row| row.get(0), + ); + match block { + Ok(data) => Ok(Some(serde_json::from_str(&data)?)), + Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), + Err(e) => Err(e.into()), + } + } + + /// Insert new account if it does not exist, update otherwise. The provided closure is called + /// with the current account, if it exists. + pub fn upsert_account( + &self, + address: Address, + f: impl FnOnce(Option) -> eyre::Result, + ) -> eyre::Result<()> { + upsert_account(&self.connection(), address, f) + } + + /// Get account by address. + pub fn get_account(&self, address: Address) -> eyre::Result> { + get_account(&self.connection(), address) + } +} + +/// Insert new account if it does not exist, update otherwise. The provided closure is called +/// with the current account, if it exists. Connection can be either +/// [rusqlite::Transaction] or [rusqlite::Connection]. +fn upsert_account( + connection: &Connection, + address: Address, + f: impl FnOnce(Option) -> eyre::Result, +) -> eyre::Result<()> { + let account = get_account(connection, address)?; + let account = f(account)?; + connection.execute( + "INSERT INTO account (address, data) VALUES (?, ?) ON CONFLICT(address) DO UPDATE SET data = excluded.data", + (address.to_string(), serde_json::to_string(&account)?), + )?; + + Ok(()) +} + +/// Delete account by address. Connection can be either [rusqlite::Transaction] or +/// [rusqlite::Connection]. +fn delete_account(connection: &Connection, address: Address) -> eyre::Result<()> { + connection.execute("DELETE FROM account WHERE address = ?", (address.to_string(),))?; + Ok(()) +} + +/// Get account by address using the database connection. Connection can be either +/// [rusqlite::Transaction] or [rusqlite::Connection]. +fn get_account(connection: &Connection, address: Address) -> eyre::Result> { + match connection.query_row::( + "SELECT data FROM account WHERE address = ?", + (address.to_string(),), + |row| row.get(0), + ) { + Ok(account_info) => Ok(Some(serde_json::from_str(&account_info)?)), + Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), + Err(e) => Err(e.into()), + } +} + +/// Insert new storage if it does not exist, update otherwise. Connection can be either +/// [rusqlite::Transaction] or [rusqlite::Connection]. +fn upsert_storage( + connection: &Connection, + address: Address, + key: B256, + data: U256, +) -> eyre::Result<()> { + connection.execute( + "INSERT INTO storage (address, key, data) VALUES (?, ?, ?) ON CONFLICT(address, key) DO UPDATE SET data = excluded.data", + (address.to_string(), key.to_string(), data.to_string()), + )?; + Ok(()) +} + +/// Delete storage by address and key. Connection can be either [rusqlite::Transaction] or +/// [rusqlite::Connection]. +fn delete_storage(connection: &Connection, address: Address, key: B256) -> eyre::Result<()> { + connection.execute( + "DELETE FROM storage WHERE address = ? AND key = ?", + (address.to_string(), key.to_string()), + )?; + Ok(()) +} + +/// Get all storages for the provided address using the database connection. Connection can be +/// either [rusqlite::Transaction] or [rusqlite::Connection]. +fn get_storages(connection: &Connection, address: Address) -> eyre::Result> { + connection + .prepare("SELECT key, data FROM storage WHERE address = ?")? + .query((address.to_string(),))? + .mapped(|row| { + Ok(( + B256::from_str(row.get_ref(0)?.as_str()?), + U256::from_str(row.get_ref(1)?.as_str()?), + )) + }) + .map(|result| { + let (key, data) = result?; + Ok((key?, data?)) + }) + .collect() +} + +/// Get storage for the provided address by key using the database connection. Connection can be +/// either [rusqlite::Transaction] or [rusqlite::Connection]. +fn get_storage(connection: &Connection, address: Address, key: B256) -> eyre::Result> { + match connection.query_row::( + "SELECT data FROM storage WHERE address = ? AND key = ?", + (address.to_string(), key.to_string()), + |row| row.get(0), + ) { + Ok(data) => Ok(Some(U256::from_str(&data)?)), + Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), + Err(e) => Err(e.into()), + } +} + +impl reth_revm::Database for Database { + type Error = eyre::Report; + + fn basic(&mut self, address: Address) -> Result, Self::Error> { + self.get_account(address) + } + + fn code_by_hash(&mut self, code_hash: B256) -> Result { + let bytecode = self.connection().query_row::( + "SELECT data FROM bytecode WHERE hash = ?", + (code_hash.to_string(),), + |row| row.get(0), + ); + match bytecode { + Ok(data) => Ok(Bytecode::new_raw(Bytes::from_str(&data).unwrap())), + Err(rusqlite::Error::QueryReturnedNoRows) => Ok(Bytecode::default()), + Err(err) => Err(err.into()), + } + } + + fn storage(&mut self, address: Address, index: U256) -> Result { + get_storage(&self.connection(), address, index.into()).map(|data| data.unwrap_or_default()) + } + + fn block_hash(&mut self, number: U256) -> Result { + let block_hash = self.connection().query_row::( + "SELECT hash FROM block WHERE number = ?", + (number.to_string(),), + |row| row.get(0), + ); + match block_hash { + Ok(data) => Ok(B256::from_str(&data).unwrap()), + // No special handling for `QueryReturnedNoRows` is needed, because revm does block + // number bound checks on its own. + // See https://github.com/bluealloy/revm/blob/1ca3d39f6a9e9778f8eb0fcb74fe529345a531b4/crates/interpreter/src/instructions/host.rs#L106-L123. + Err(err) => Err(err.into()), + } + } +} diff --git a/examples/exex/rollup/src/main.rs b/examples/exex/rollup/src/main.rs new file mode 100644 index 000000000..cd2b0c94d --- /dev/null +++ b/examples/exex/rollup/src/main.rs @@ -0,0 +1,586 @@ +//! Example of a simple rollup that derives its state from the L1 chain by executing transactions, +//! processing deposits and storing all related data in an SQLite database. +//! +//! The rollup contract accepts blocks of transactions and deposits of ETH and is deployed on +//! Holesky at [ROLLUP_CONTRACT_ADDRESS], see . + +use alloy_rlp::Decodable; +use alloy_sol_types::{sol, SolEventInterface, SolInterface}; +use db::Database; +use eyre::OptionExt; +use once_cell::sync::Lazy; +use reth_exex::{ExExContext, ExExEvent}; +use reth_interfaces::executor::BlockValidationError; +use reth_node_api::{ConfigureEvm, ConfigureEvmEnv, FullNodeComponents}; +use reth_node_ethereum::{EthEvmConfig, EthereumNode}; +use reth_primitives::{ + address, constants, + revm::env::fill_tx_env, + revm_primitives::{CfgEnvWithHandlerCfg, EVMError, ExecutionResult, ResultAndState}, + Address, Block, BlockWithSenders, Bytes, ChainSpec, ChainSpecBuilder, Genesis, Hardfork, + Header, Receipt, SealedBlockWithSenders, TransactionSigned, U256, +}; +use reth_provider::Chain; +use reth_revm::{ + db::{states::bundle_state::BundleRetention, BundleState}, + DatabaseCommit, StateBuilder, +}; +use reth_tracing::tracing::{debug, error, info}; +use rusqlite::Connection; +use std::sync::Arc; + +mod db; + +sol!(RollupContract, "rollup_abi.json"); +use RollupContract::{RollupContractCalls, RollupContractEvents}; + +const DATABASE_PATH: &str = "rollup.db"; +const ROLLUP_CONTRACT_ADDRESS: Address = address!("74ae65DF20cB0e3BF8c022051d0Cdd79cc60890C"); +const ROLLUP_SUBMITTER_ADDRESS: Address = address!("B01042Db06b04d3677564222010DF5Bd09C5A947"); +const CHAIN_ID: u64 = 17001; +static CHAIN_SPEC: Lazy> = Lazy::new(|| { + Arc::new( + ChainSpecBuilder::default() + .chain(CHAIN_ID.into()) + .genesis(Genesis::clique_genesis(CHAIN_ID, ROLLUP_SUBMITTER_ADDRESS)) + .shanghai_activated() + .build(), + ) +}); + +struct Rollup { + ctx: ExExContext, + db: Database, +} + +impl Rollup { + fn new(ctx: ExExContext, connection: Connection) -> eyre::Result { + let db = Database::new(connection)?; + Ok(Self { ctx, db }) + } + + async fn start(mut self) -> eyre::Result<()> { + // Process all new chain state notifications + while let Some(notification) = self.ctx.notifications.recv().await { + if let Some(reverted_chain) = notification.reverted_chain() { + self.revert(&reverted_chain)?; + } + + if let Some(committed_chain) = notification.committed_chain() { + self.commit(&committed_chain)?; + self.ctx.events.send(ExExEvent::FinishedHeight(committed_chain.tip().number))?; + } + } + + Ok(()) + } + + /// Process a new chain commit. + /// + /// This function decodes all transactions to the rollup contract into events, executes the + /// corresponding actions and inserts the results into the database. + fn commit(&mut self, chain: &Chain) -> eyre::Result<()> { + let events = decode_chain_into_rollup_events(chain); + + for (_, tx, event) in events { + match event { + // A new block is submitted to the rollup contract. + // The block is executed on top of existing rollup state and committed into the + // database. + RollupContractEvents::BlockSubmitted(_) => { + let call = RollupContractCalls::abi_decode(tx.input(), true)?; + + if let RollupContractCalls::submitBlock(RollupContract::submitBlockCall { + header, + blockData, + .. + }) = call + { + match execute_block(&mut self.db, &header, blockData) { + Ok((block, bundle, _, _)) => { + let block = block.seal_slow(); + self.db.insert_block_with_bundle(&block, bundle)?; + info!( + tx_hash = %tx.hash, + chain_id = %header.rollupChainId, + sequence = %header.sequence, + transactions = block.body.len(), + "Block submitted, executed and inserted into database" + ); + } + Err(err) => { + error!( + %err, + tx_hash = %tx.hash, + chain_id = %header.rollupChainId, + sequence = %header.sequence, + "Failed to execute block" + ); + } + } + } + } + // A deposit of ETH to the rollup contract. The deposit is added to the recipient's + // balance and committed into the database. + RollupContractEvents::Enter(RollupContract::Enter { + token, + rollupRecipient, + amount, + }) => { + if token != Address::ZERO { + error!(tx_hash = %tx.hash, "Only ETH deposits are supported"); + continue + } + + self.db.upsert_account(rollupRecipient, |account| { + let mut account = account.unwrap_or_default(); + account.balance += amount; + Ok(account) + })?; + + info!( + tx_hash = %tx.hash, + %amount, + recipient = %rollupRecipient, + "Deposit", + ); + } + _ => (), + } + } + + Ok(()) + } + + /// Process a chain revert. + /// + /// This function decodes all transactions to the rollup contract into events, reverts the + /// corresponding actions and updates the database. + fn revert(&mut self, chain: &Chain) -> eyre::Result<()> { + let mut events = decode_chain_into_rollup_events(chain); + // Reverse the order of events to start reverting from the tip + events.reverse(); + + for (_, tx, event) in events { + match event { + // The block is reverted from the database. + RollupContractEvents::BlockSubmitted(_) => { + let call = RollupContractCalls::abi_decode(tx.input(), true)?; + + if let RollupContractCalls::submitBlock(RollupContract::submitBlockCall { + header, + .. + }) = call + { + self.db.revert_tip_block(header.sequence)?; + info!( + tx_hash = %tx.hash, + chain_id = %header.rollupChainId, + sequence = %header.sequence, + "Block reverted" + ); + } + } + // The deposit is subtracted from the recipient's balance. + RollupContractEvents::Enter(RollupContract::Enter { + token, + rollupRecipient, + amount, + }) => { + if token != Address::ZERO { + error!(tx_hash = %tx.hash, "Only ETH deposits are supported"); + continue + } + + self.db.upsert_account(rollupRecipient, |account| { + let mut account = account.ok_or(eyre::eyre!("account not found"))?; + account.balance -= amount; + Ok(account) + })?; + + info!( + tx_hash = %tx.hash, + %amount, + recipient = %rollupRecipient, + "Deposit reverted", + ); + } + _ => (), + } + } + + Ok(()) + } +} + +/// Decode chain of blocks into a flattened list of receipt logs, filter only transactions to the +/// Rollup contract [ROLLUP_CONTRACT_ADDRESS] and extract [RollupContractEvents]. +fn decode_chain_into_rollup_events( + chain: &Chain, +) -> Vec<(&SealedBlockWithSenders, &TransactionSigned, RollupContractEvents)> { + chain + // Get all blocks and receipts + .blocks_and_receipts() + // Get all receipts + .flat_map(|(block, receipts)| { + block + .body + .iter() + .zip(receipts.iter().flatten()) + .map(move |(tx, receipt)| (block, tx, receipt)) + }) + // Filter only transactions to the rollup contract + .filter(|(_, tx, _)| tx.to() == Some(ROLLUP_CONTRACT_ADDRESS)) + // Get all logs + .flat_map(|(block, tx, receipt)| receipt.logs.iter().map(move |log| (block, tx, log))) + // Decode and filter rollup events + .filter_map(|(block, tx, log)| { + RollupContractEvents::decode_raw_log(log.topics(), &log.data.data, true) + .ok() + .map(|event| (block, tx, event)) + }) + .collect() +} + +/// Execute a rollup block and return (block with recovered senders)[BlockWithSenders], (bundle +/// state)[BundleState] and list of (receipts)[Receipt]. +fn execute_block( + db: &mut Database, + header: &RollupContract::BlockHeader, + block_data: Bytes, +) -> eyre::Result<(BlockWithSenders, BundleState, Vec, Vec)> { + if header.rollupChainId != U256::from(CHAIN_ID) { + eyre::bail!("Invalid rollup chain ID") + } + + let block_number = u64::try_from(header.sequence)?; + let parent_block = if !header.sequence.is_zero() { + db.get_block(header.sequence - U256::from(1))? + } else { + None + }; + + // Calculate base fee per gas for EIP-1559 transactions + let base_fee_per_gas = if CHAIN_SPEC.fork(Hardfork::London).transitions_at_block(block_number) { + constants::EIP1559_INITIAL_BASE_FEE + } else { + parent_block + .as_ref() + .ok_or(eyre::eyre!("parent block not found"))? + .header + .next_block_base_fee(CHAIN_SPEC.base_fee_params_at_block(block_number)) + .ok_or(eyre::eyre!("failed to calculate base fee"))? + }; + + // Construct header + let header = Header { + parent_hash: parent_block.map(|block| block.header.hash()).unwrap_or_default(), + number: block_number, + gas_limit: u64::try_from(header.gasLimit)?, + timestamp: u64::try_from(header.confirmBy)?, + base_fee_per_gas: Some(base_fee_per_gas), + ..Default::default() + }; + + // Decode block data, filter only transactions with the correct chain ID and recover senders + let transactions = Vec::::decode(&mut block_data.as_ref())? + .into_iter() + .filter(|tx| tx.chain_id() == Some(CHAIN_ID)) + .map(|tx| { + let sender = tx.recover_signer().ok_or(eyre::eyre!("failed to recover signer"))?; + Ok((tx, sender)) + }) + .collect::>>()?; + + // Execute block + let state = StateBuilder::new_with_database( + Box::new(db) as Box + Send> + ) + .with_bundle_update() + .build(); + let mut evm = EthEvmConfig::default().evm(state); + + // Set state clear flag. + evm.db_mut().set_state_clear_flag( + CHAIN_SPEC.fork(Hardfork::SpuriousDragon).active_at_block(header.number), + ); + + let mut cfg = CfgEnvWithHandlerCfg::new_with_spec_id(evm.cfg().clone(), evm.spec_id()); + EthEvmConfig::fill_cfg_and_block_env( + &mut cfg, + evm.block_mut(), + &CHAIN_SPEC, + &header, + U256::ZERO, + ); + *evm.cfg_mut() = cfg.cfg_env; + + let mut receipts = Vec::with_capacity(transactions.len()); + let mut executed_txs = Vec::with_capacity(transactions.len()); + let mut results = Vec::with_capacity(transactions.len()); + if !transactions.is_empty() { + let mut cumulative_gas_used = 0; + for (transaction, sender) in transactions { + // The sum of the transaction’s gas limit, Tg, and the gas utilized in this block prior, + // must be no greater than the block’s gasLimit. + let block_available_gas = header.gas_limit - cumulative_gas_used; + if transaction.gas_limit() > block_available_gas { + // TODO(alexey): what to do here? + return Err(BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas { + transaction_gas_limit: transaction.gas_limit(), + block_available_gas, + } + .into()) + } + // Execute transaction. + // Fill revm structure. + fill_tx_env(evm.tx_mut(), &transaction, sender); + + let ResultAndState { result, state } = match evm.transact() { + Ok(result) => result, + Err(err) => { + match err { + EVMError::Transaction(err) => { + // if the transaction is invalid, we can skip it + debug!(%err, ?transaction, "Skipping invalid transaction"); + continue + } + err => { + // this is an error that we should treat as fatal for this attempt + eyre::bail!(err) + } + } + } + }; + + debug!(?transaction, ?result, ?state, "Executed transaction"); + + evm.db_mut().commit(state); + + // append gas used + cumulative_gas_used += result.gas_used(); + + // Push transaction changeset and calculate header bloom filter for receipt. + #[allow(clippy::needless_update)] // side-effect of optimism fields + receipts.push(Receipt { + tx_type: transaction.tx_type(), + success: result.is_success(), + cumulative_gas_used, + logs: result.logs().iter().cloned().map(Into::into).collect(), + ..Default::default() + }); + + // append transaction to the list of executed transactions + executed_txs.push(transaction); + results.push(result); + } + + evm.db_mut().merge_transitions(BundleRetention::Reverts); + } + + // Construct block and recover senders + let block = Block { header, body: executed_txs, ..Default::default() } + .with_recovered_senders() + .ok_or_eyre("failed to recover senders")?; + + let bundle = evm.db_mut().take_bundle(); + + Ok((block, bundle, receipts, results)) +} + +fn main() -> eyre::Result<()> { + reth::cli::Cli::parse_args().run(|builder, _| async move { + let handle = builder + .node(EthereumNode::default()) + .install_exex("Rollup", move |ctx| async { + let connection = Connection::open(DATABASE_PATH)?; + + Ok(Rollup::new(ctx, connection)?.start()) + }) + .launch() + .await?; + + handle.wait_for_node_exit().await + }) +} + +#[cfg(test)] +mod tests { + use std::time::{SystemTime, UNIX_EPOCH}; + + use alloy_sol_types::{sol, SolCall}; + use reth_interfaces::test_utils::generators::{self, sign_tx_with_key_pair}; + use reth_primitives::{ + bytes, + constants::ETH_TO_WEI, + public_key_to_address, + revm_primitives::{AccountInfo, ExecutionResult, Output, TransactTo, TxEnv}, + BlockNumber, Receipt, SealedBlockWithSenders, Transaction, TxEip2930, TxKind, U256, + }; + use reth_revm::Evm; + use rusqlite::Connection; + use secp256k1::{Keypair, Secp256k1}; + + use crate::{ + db::Database, execute_block, RollupContract::BlockHeader, CHAIN_ID, + ROLLUP_SUBMITTER_ADDRESS, + }; + + sol!( + WETH, + r#" +[ + { + "constant":true, + "inputs":[ + { + "name":"", + "type":"address" + } + ], + "name":"balanceOf", + "outputs":[ + { + "name":"", + "type":"uint256" + } + ], + "payable":false, + "stateMutability":"view", + "type":"function" + } +] + "# + ); + + #[test] + fn test_execute_block() -> eyre::Result<()> { + reth_tracing::init_test_tracing(); + + let mut database = Database::new(Connection::open_in_memory()?)?; + + // Create key pair + let secp = Secp256k1::new(); + let key_pair = Keypair::new(&secp, &mut generators::rng()); + let sender_address = public_key_to_address(key_pair.public_key()); + + // Deposit some ETH to the sender and insert it into database + database.upsert_account(sender_address, |_| { + Ok(AccountInfo { balance: U256::from(ETH_TO_WEI), nonce: 1, ..Default::default() }) + })?; + + // WETH deployment transaction + let (_, _, results) = execute_transaction( + &mut database, + key_pair, + 0, + Transaction::Eip2930(TxEip2930 { + chain_id: CHAIN_ID, + nonce: 1, + gas_limit: 1_500_000, + gas_price: 1_500_000_000, + to: TxKind::Create, + // WETH9 bytecode + input: bytes!("60606040526040805190810160405280600d81526020017f57726170706564204574686572000000000000000000000000000000000000008152506000908051906020019061004f9291906100c8565b506040805190810160405280600481526020017f57455448000000000000000000000000000000000000000000000000000000008152506001908051906020019061009b9291906100c8565b506012600260006101000a81548160ff021916908360ff16021790555034156100c357600080fd5b61016d565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061010957805160ff1916838001178555610137565b82800160010185558215610137579182015b8281111561013657825182559160200191906001019061011b565b5b5090506101449190610148565b5090565b61016a91905b8082111561016657600081600090555060010161014e565b5090565b90565b610c348061017c6000396000f3006060604052600436106100af576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806306fdde03146100b9578063095ea7b31461014757806318160ddd146101a157806323b872dd146101ca5780632e1a7d4d14610243578063313ce5671461026657806370a082311461029557806395d89b41146102e2578063a9059cbb14610370578063d0e30db0146103ca578063dd62ed3e146103d4575b6100b7610440565b005b34156100c457600080fd5b6100cc6104dd565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561010c5780820151818401526020810190506100f1565b50505050905090810190601f1680156101395780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b341561015257600080fd5b610187600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803590602001909190505061057b565b604051808215151515815260200191505060405180910390f35b34156101ac57600080fd5b6101b461066d565b6040518082815260200191505060405180910390f35b34156101d557600080fd5b610229600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803590602001909190505061068c565b604051808215151515815260200191505060405180910390f35b341561024e57600080fd5b61026460048080359060200190919050506109d9565b005b341561027157600080fd5b610279610b05565b604051808260ff1660ff16815260200191505060405180910390f35b34156102a057600080fd5b6102cc600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610b18565b6040518082815260200191505060405180910390f35b34156102ed57600080fd5b6102f5610b30565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561033557808201518184015260208101905061031a565b50505050905090810190601f1680156103625780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b341561037b57600080fd5b6103b0600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091908035906020019091905050610bce565b604051808215151515815260200191505060405180910390f35b6103d2610440565b005b34156103df57600080fd5b61042a600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610be3565b6040518082815260200191505060405180910390f35b34600360003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055503373ffffffffffffffffffffffffffffffffffffffff167fe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c346040518082815260200191505060405180910390a2565b60008054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156105735780601f1061054857610100808354040283529160200191610573565b820191906000526020600020905b81548152906001019060200180831161055657829003601f168201915b505050505081565b600081600460003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925846040518082815260200191505060405180910390a36001905092915050565b60003073ffffffffffffffffffffffffffffffffffffffff1631905090565b600081600360008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054101515156106dc57600080fd5b3373ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff16141580156107b457507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff600460008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205414155b156108cf5781600460008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020541015151561084457600080fd5b81600460008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825403925050819055505b81600360008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000206000828254039250508190555081600360008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055508273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040518082815260200191505060405180910390a3600190509392505050565b80600360003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205410151515610a2757600080fd5b80600360003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825403925050819055503373ffffffffffffffffffffffffffffffffffffffff166108fc829081150290604051600060405180830381858888f193505050501515610ab457600080fd5b3373ffffffffffffffffffffffffffffffffffffffff167f7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65826040518082815260200191505060405180910390a250565b600260009054906101000a900460ff1681565b60036020528060005260406000206000915090505481565b60018054600181600116156101000203166002900480601f016020809104026020016040519081016040528092919081815260200182805460018160011615610100020316600290048015610bc65780601f10610b9b57610100808354040283529160200191610bc6565b820191906000526020600020905b815481529060010190602001808311610ba957829003601f168201915b505050505081565b6000610bdb33848461068c565b905092915050565b60046020528160005260406000206020528060005260406000206000915091505054815600a165627a7a72305820deb4c2ccab3c2fdca32ab3f46728389c2fe2c165d5fafa07661e4e004f6c344a0029"), + ..Default::default() + }) + )?; + + let weth_address = match results.first() { + Some(ExecutionResult::Success { output: Output::Create(_, Some(address)), .. }) => { + *address + } + _ => eyre::bail!("WETH contract address not found"), + }; + + // WETH deposit transaction + execute_transaction( + &mut database, + key_pair, + 1, + Transaction::Eip2930(TxEip2930 { + chain_id: CHAIN_ID, + nonce: 2, + gas_limit: 50000, + gas_price: 1_500_000_000, + to: TxKind::Call(weth_address), + value: U256::from(0.5 * ETH_TO_WEI as f64), + input: bytes!("d0e30db0"), + ..Default::default() + }), + )?; + + // Verify WETH balance + let mut evm = Evm::builder() + .with_db(&mut database) + .with_tx_env(TxEnv { + caller: sender_address, + gas_limit: 50_000_000, + transact_to: TransactTo::Call(weth_address), + data: WETH::balanceOfCall::new((sender_address,)).abi_encode().into(), + ..Default::default() + }) + .build(); + let result = evm.transact().map_err(|err| eyre::eyre!(err))?.result; + assert_eq!( + result.output(), + Some(&U256::from(0.5 * ETH_TO_WEI as f64).to_be_bytes_vec().into()) + ); + drop(evm); + + // Verify nonce + let account = database.get_account(sender_address)?.unwrap(); + assert_eq!(account.nonce, 3); + + // Revert block with WETH deposit transaction + database.revert_tip_block(U256::from(1))?; + + // Verify WETH balance after revert + let mut evm = Evm::builder() + .with_db(&mut database) + .with_tx_env(TxEnv { + caller: sender_address, + gas_limit: 50_000_000, + transact_to: TransactTo::Call(weth_address), + data: WETH::balanceOfCall::new((sender_address,)).abi_encode().into(), + ..Default::default() + }) + .build(); + let result = evm.transact().map_err(|err| eyre::eyre!(err))?.result; + assert_eq!(result.output(), Some(&U256::ZERO.to_be_bytes_vec().into())); + drop(evm); + + // Verify nonce after revert + let account = database.get_account(sender_address)?.unwrap(); + assert_eq!(account.nonce, 2); + + Ok(()) + } + + fn execute_transaction( + database: &mut Database, + key_pair: Keypair, + sequence: BlockNumber, + tx: Transaction, + ) -> eyre::Result<(SealedBlockWithSenders, Vec, Vec)> { + let signed_tx = sign_tx_with_key_pair(key_pair, tx); + + // Construct block header and data + let block_header = BlockHeader { + rollupChainId: U256::from(CHAIN_ID), + sequence: U256::from(sequence), + confirmBy: U256::from(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs()), + gasLimit: U256::from(30_000_000), + rewardAddress: ROLLUP_SUBMITTER_ADDRESS, + }; + let block_data = alloy_rlp::encode(vec![signed_tx.envelope_encoded()]); + + // Execute block and insert into database + let (block, bundle, receipts, results) = + execute_block(database, &block_header, block_data.into())?; + let block = block.seal_slow(); + database.insert_block_with_bundle(&block, bundle)?; + + Ok((block, receipts, results)) + } +}