From 3ffc729833ade7886bd3e1ac2bf75a93637c776f Mon Sep 17 00:00:00 2001 From: Alexey Shekhirin Date: Thu, 11 Apr 2024 17:38:03 +0100 Subject: [PATCH] feat(examples): OP Stack bridge stats ExEx (#7556) Co-authored-by: Oliver Nordbjerg Co-authored-by: Oliver Nordbjerg --- Cargo.lock | 78 +- Cargo.toml | 1 + crates/exex/src/event.rs | 2 +- crates/storage/provider/src/chain.rs | 7 +- examples/Cargo.toml | 5 +- examples/cli-extension-event-hooks/Cargo.toml | 2 +- examples/exex/op-bridge/Cargo.toml | 23 + .../op-bridge/l1_standard_bridge_abi.json | 664 ++++++++++++++++++ examples/exex/op-bridge/src/main.rs | 244 +++++++ 9 files changed, 1019 insertions(+), 7 deletions(-) create mode 100644 examples/exex/op-bridge/Cargo.toml create mode 100644 examples/exex/op-bridge/l1_standard_bridge_abi.json create mode 100644 examples/exex/op-bridge/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 3a1620059109..b0af1abad750 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -452,6 +452,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "452d929748ac948a10481fff4123affead32c553cf362841c5103dd508bdfc16" dependencies = [ + "alloy-json-abi", "alloy-sol-macro-input", "const-hex", "heck 0.4.1", @@ -470,11 +471,13 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df64e094f6d2099339f9e82b5b38440b159757b6920878f28316243f8166c8d1" dependencies = [ + "alloy-json-abi", "const-hex", "dunce", "heck 0.5.0", "proc-macro2", "quote", + "serde_json", "syn 2.0.58", "syn-solidity", ] @@ -494,6 +497,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43bc2d6dfc2a19fd56644494479510f98b1ee929e04cf0d4aa45e98baa3e545b" dependencies = [ + "alloy-json-abi", "alloy-primitives", "alloy-sol-macro", "const-hex", @@ -2396,7 +2400,7 @@ dependencies = [ "enr", "fnv", "futures", - "hashlink", + "hashlink 0.8.4", "hex", "hkdf", "lazy_static", @@ -2751,6 +2755,18 @@ dependencies = [ "once_cell", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fast-float" version = "0.2.0" @@ -3191,6 +3207,15 @@ dependencies = [ "hashbrown 0.14.3", ] +[[package]] +name = "hashlink" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692eaaf7f7607518dd3cef090f1474b61edc5301d8012f09579920df68b725ee" +dependencies = [ + "hashbrown 0.14.3", +] + [[package]] name = "hdrhistogram" version = "7.5.4" @@ -4408,6 +4433,17 @@ dependencies = [ "libsecp256k1-core", ] +[[package]] +name = "libsqlite3-sys" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -5029,6 +5065,26 @@ version = "11.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" +[[package]] +name = "op-bridge" +version = "0.0.0" +dependencies = [ + "alloy-sol-types", + "eyre", + "futures", + "itertools 0.12.1", + "reth", + "reth-exex", + "reth-node-api", + "reth-node-core", + "reth-node-ethereum", + "reth-primitives", + "reth-provider", + "reth-tracing", + "rusqlite", + "tokio", +] + [[package]] name = "opaque-debug" version = "0.3.1" @@ -7654,6 +7710,20 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f86854cf50259291520509879a5c294c3c9a4c334e9ff65071c51e42ef1e2343" +[[package]] +name = "rusqlite" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" +dependencies = [ + "bitflags 2.5.0", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink 0.9.0", + "libsqlite3-sys", + "smallvec", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -9444,6 +9514,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "vergen" version = "8.3.1" diff --git a/Cargo.toml b/Cargo.toml index a9d0fd7ed8e1..34c9740a2ffd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,6 +77,7 @@ members = [ "examples/trace-transaction-cli/", "examples/polygon-p2p/", "examples/custom-inspector/", + "examples/exex/op-bridge/", "testing/ef-tests/", ] default-members = ["bin/reth"] diff --git a/crates/exex/src/event.rs b/crates/exex/src/event.rs index cc6ac4365f87..7929cf0316e5 100644 --- a/crates/exex/src/event.rs +++ b/crates/exex/src/event.rs @@ -1,7 +1,7 @@ use reth_primitives::BlockNumber; /// Events emitted by an ExEx. -#[derive(Debug)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ExExEvent { /// Highest block processed by the ExEx. /// diff --git a/crates/storage/provider/src/chain.rs b/crates/storage/provider/src/chain.rs index 78430748ba27..eb9ef6a4b7ec 100644 --- a/crates/storage/provider/src/chain.rs +++ b/crates/storage/provider/src/chain.rs @@ -8,7 +8,7 @@ use reth_primitives::{ }; use reth_trie::updates::TrieUpdates; use revm::db::BundleState; -use std::{borrow::Cow, collections::BTreeMap, fmt}; +use std::{borrow::Cow, collections::BTreeMap, fmt, ops::RangeInclusive}; /// A chain of blocks and their final state. /// @@ -177,6 +177,11 @@ impl Chain { self.blocks.len() } + /// Returns the range of block numbers in the chain. + pub fn range(&self) -> RangeInclusive { + self.first().number..=self.tip().number + } + /// Get all receipts for the given block. pub fn receipts_by_block_hash(&self, block_hash: BlockHash) -> Option> { let num = self.block_number(block_hash)?; diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 92bb0f1f1ea8..82b6be45ad2e 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -7,14 +7,11 @@ license.workspace = true [dev-dependencies] reth-primitives.workspace = true - reth-db.workspace = true reth-provider.workspace = true - reth-rpc-builder.workspace = true reth-rpc-types.workspace = true reth-rpc-types-compat.workspace = true - reth-revm.workspace = true reth-blockchain-tree.workspace = true reth-beacon-consensus.workspace = true @@ -22,6 +19,7 @@ reth-network-api.workspace = true reth-network.workspace = true reth-transaction-pool.workspace = true reth-tasks.workspace = true + eyre.workspace = true futures.workspace = true async-trait.workspace = true @@ -38,3 +36,4 @@ path = "network.rs" [[example]] name = "network-txpool" path = "network-txpool.rs" + diff --git a/examples/cli-extension-event-hooks/Cargo.toml b/examples/cli-extension-event-hooks/Cargo.toml index 2acac14ee78e..8664057e7d85 100644 --- a/examples/cli-extension-event-hooks/Cargo.toml +++ b/examples/cli-extension-event-hooks/Cargo.toml @@ -7,4 +7,4 @@ license.workspace = true [dependencies] reth.workspace = true -reth-node-ethereum.workspace = true \ No newline at end of file +reth-node-ethereum.workspace = true diff --git a/examples/exex/op-bridge/Cargo.toml b/examples/exex/op-bridge/Cargo.toml new file mode 100644 index 000000000000..3d87b2801765 --- /dev/null +++ b/examples/exex/op-bridge/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "op-bridge" +version = "0.0.0" +publish = false +edition.workspace = true +license.workspace = true + +[dependencies] +reth.workspace = true +reth-exex.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-tracing.workspace = true + +eyre.workspace = true +tokio.workspace = true +futures.workspace = true +alloy-sol-types = { workspace = true, features = ["json"] } +itertools.workspace = true +rusqlite = { version = "0.31.0", features = ["bundled"] } diff --git a/examples/exex/op-bridge/l1_standard_bridge_abi.json b/examples/exex/op-bridge/l1_standard_bridge_abi.json new file mode 100644 index 000000000000..4ae6406f0793 --- /dev/null +++ b/examples/exex/op-bridge/l1_standard_bridge_abi.json @@ -0,0 +1,664 @@ +[ + { + "inputs": [ + { + "internalType": "address payable", + "name": "_messenger", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "localToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "remoteToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "extraData", + "type": "bytes" + } + ], + "name": "ERC20BridgeFinalized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "localToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "remoteToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "extraData", + "type": "bytes" + } + ], + "name": "ERC20BridgeInitiated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "l1Token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "l2Token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "extraData", + "type": "bytes" + } + ], + "name": "ERC20DepositInitiated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "l1Token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "l2Token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "extraData", + "type": "bytes" + } + ], + "name": "ERC20WithdrawalFinalized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "extraData", + "type": "bytes" + } + ], + "name": "ETHBridgeFinalized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "extraData", + "type": "bytes" + } + ], + "name": "ETHBridgeInitiated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "extraData", + "type": "bytes" + } + ], + "name": "ETHDepositInitiated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "extraData", + "type": "bytes" + } + ], + "name": "ETHWithdrawalFinalized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "inputs": [], + "name": "MESSENGER", + "outputs": [ + { + "internalType": "contract CrossDomainMessenger", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "OTHER_BRIDGE", + "outputs": [ + { + "internalType": "contract StandardBridge", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_localToken", + "type": "address" + }, + { + "internalType": "address", + "name": "_remoteToken", + "type": "address" + }, + { "internalType": "uint256", "name": "_amount", "type": "uint256" }, + { + "internalType": "uint32", + "name": "_minGasLimit", + "type": "uint32" + }, + { "internalType": "bytes", "name": "_extraData", "type": "bytes" } + ], + "name": "bridgeERC20", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_localToken", + "type": "address" + }, + { + "internalType": "address", + "name": "_remoteToken", + "type": "address" + }, + { "internalType": "address", "name": "_to", "type": "address" }, + { "internalType": "uint256", "name": "_amount", "type": "uint256" }, + { + "internalType": "uint32", + "name": "_minGasLimit", + "type": "uint32" + }, + { "internalType": "bytes", "name": "_extraData", "type": "bytes" } + ], + "name": "bridgeERC20To", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "_minGasLimit", + "type": "uint32" + }, + { "internalType": "bytes", "name": "_extraData", "type": "bytes" } + ], + "name": "bridgeETH", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "_to", "type": "address" }, + { + "internalType": "uint32", + "name": "_minGasLimit", + "type": "uint32" + }, + { "internalType": "bytes", "name": "_extraData", "type": "bytes" } + ], + "name": "bridgeETHTo", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_l1Token", + "type": "address" + }, + { + "internalType": "address", + "name": "_l2Token", + "type": "address" + }, + { "internalType": "uint256", "name": "_amount", "type": "uint256" }, + { + "internalType": "uint32", + "name": "_minGasLimit", + "type": "uint32" + }, + { "internalType": "bytes", "name": "_extraData", "type": "bytes" } + ], + "name": "depositERC20", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_l1Token", + "type": "address" + }, + { + "internalType": "address", + "name": "_l2Token", + "type": "address" + }, + { "internalType": "address", "name": "_to", "type": "address" }, + { "internalType": "uint256", "name": "_amount", "type": "uint256" }, + { + "internalType": "uint32", + "name": "_minGasLimit", + "type": "uint32" + }, + { "internalType": "bytes", "name": "_extraData", "type": "bytes" } + ], + "name": "depositERC20To", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "_minGasLimit", + "type": "uint32" + }, + { "internalType": "bytes", "name": "_extraData", "type": "bytes" } + ], + "name": "depositETH", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "_to", "type": "address" }, + { + "internalType": "uint32", + "name": "_minGasLimit", + "type": "uint32" + }, + { "internalType": "bytes", "name": "_extraData", "type": "bytes" } + ], + "name": "depositETHTo", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "", "type": "address" }, + { "internalType": "address", "name": "", "type": "address" } + ], + "name": "deposits", + "outputs": [ + { "internalType": "uint256", "name": "", "type": "uint256" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_localToken", + "type": "address" + }, + { + "internalType": "address", + "name": "_remoteToken", + "type": "address" + }, + { "internalType": "address", "name": "_from", "type": "address" }, + { "internalType": "address", "name": "_to", "type": "address" }, + { "internalType": "uint256", "name": "_amount", "type": "uint256" }, + { "internalType": "bytes", "name": "_extraData", "type": "bytes" } + ], + "name": "finalizeBridgeERC20", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "_from", "type": "address" }, + { "internalType": "address", "name": "_to", "type": "address" }, + { "internalType": "uint256", "name": "_amount", "type": "uint256" }, + { "internalType": "bytes", "name": "_extraData", "type": "bytes" } + ], + "name": "finalizeBridgeETH", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_l1Token", + "type": "address" + }, + { + "internalType": "address", + "name": "_l2Token", + "type": "address" + }, + { "internalType": "address", "name": "_from", "type": "address" }, + { "internalType": "address", "name": "_to", "type": "address" }, + { "internalType": "uint256", "name": "_amount", "type": "uint256" }, + { "internalType": "bytes", "name": "_extraData", "type": "bytes" } + ], + "name": "finalizeERC20Withdrawal", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "_from", "type": "address" }, + { "internalType": "address", "name": "_to", "type": "address" }, + { "internalType": "uint256", "name": "_amount", "type": "uint256" }, + { "internalType": "bytes", "name": "_extraData", "type": "bytes" } + ], + "name": "finalizeETHWithdrawal", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract SuperchainConfig", + "name": "_superchainConfig", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "l2TokenBridge", + "outputs": [ + { "internalType": "address", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "messenger", + "outputs": [ + { + "internalType": "contract CrossDomainMessenger", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "otherBridge", + "outputs": [ + { + "internalType": "contract StandardBridge", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "paused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "superchainConfig", + "outputs": [ + { + "internalType": "contract SuperchainConfig", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "stateMutability": "view", + "type": "function" + }, + { "stateMutability": "payable", "type": "receive" } +] diff --git a/examples/exex/op-bridge/src/main.rs b/examples/exex/op-bridge/src/main.rs new file mode 100644 index 000000000000..814ffce689d9 --- /dev/null +++ b/examples/exex/op-bridge/src/main.rs @@ -0,0 +1,244 @@ +use std::{ + pin::Pin, + task::{ready, Context, Poll}, +}; + +use alloy_sol_types::{sol, SolEventInterface}; +use futures::Future; +use reth::builder::FullNodeTypes; +use reth_exex::{ExExContext, ExExEvent}; +use reth_node_ethereum::EthereumNode; +use reth_primitives::{Log, SealedBlockWithSenders, TransactionSigned}; +use reth_provider::Chain; +use reth_tracing::tracing::info; +use rusqlite::Connection; + +sol!(L1StandardBridge, "l1_standard_bridge_abi.json"); +use crate::L1StandardBridge::{ETHBridgeFinalized, ETHBridgeInitiated, L1StandardBridgeEvents}; + +/// An example of ExEx that listens to ETH bridging events from OP Stack chains +/// and stores deposits and withdrawals in a SQLite database. +struct OPBridgeExEx { + ctx: ExExContext, + connection: Connection, +} + +impl OPBridgeExEx { + fn new(ctx: ExExContext, connection: Connection) -> eyre::Result { + // Create deposits and withdrawals tables + connection.execute( + r#" + CREATE TABLE IF NOT EXISTS deposits ( + id INTEGER PRIMARY KEY, + block_number INTEGER NOT NULL, + tx_hash TEXT NOT NULL UNIQUE, + contract_address TEXT NOT NULL, + "from" TEXT NOT NULL, + "to" TEXT NOT NULL, + amount TEXT NOT NULL + ); + "#, + (), + )?; + connection.execute( + r#" + CREATE TABLE IF NOT EXISTS withdrawals ( + id INTEGER PRIMARY KEY, + block_number INTEGER NOT NULL, + tx_hash TEXT NOT NULL UNIQUE, + contract_address TEXT NOT NULL, + "from" TEXT NOT NULL, + "to" TEXT NOT NULL, + amount TEXT NOT NULL + ); + "#, + (), + )?; + + // Create a bridge contract addresses table and insert known ones with their respective + // names + connection.execute( + r#" + CREATE TABLE IF NOT EXISTS contracts ( + id INTEGER PRIMARY KEY, + address TEXT NOT NULL UNIQUE, + name TEXT NOT NULL + ); + "#, + (), + )?; + connection.execute( + r#" + INSERT OR IGNORE INTO contracts (address, name) + VALUES + ('0x3154Cf16ccdb4C6d922629664174b904d80F2C35', 'Base'), + ('0x3a05E5d33d7Ab3864D53aaEc93c8301C1Fa49115', 'Blast'), + ('0x697402166Fbf2F22E970df8a6486Ef171dbfc524', 'Blast'), + ('0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1', 'Optimism'), + ('0x735aDBbE72226BD52e818E7181953f42E3b0FF21', 'Mode'), + ('0x3B95bC951EE0f553ba487327278cAc44f29715E5', 'Manta'); + "#, + (), + )?; + + info!("Initialized database tables"); + + Ok(Self { ctx, connection }) + } +} + +impl Future for OPBridgeExEx { + type Output = eyre::Result<()>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.get_mut(); + + // Process all new chain state notifications until there are no more + while let Some(notification) = ready!(this.ctx.notifications.poll_recv(cx)) { + // If there was a reorg, delete all deposits and withdrawals that were reverted + if let Some(reverted_chain) = notification.reverted() { + let events = decode_chain_into_events(&reverted_chain); + + let mut deposits = 0; + let mut withdrawals = 0; + + for (_, tx, _, event) in events { + match event { + // L1 -> L2 deposit + L1StandardBridgeEvents::ETHBridgeInitiated(ETHBridgeInitiated { + .. + }) => { + let deleted = this.connection.execute( + "DELETE FROM deposits WHERE tx_hash = ?;", + (tx.hash().to_string(),), + )?; + deposits += deleted; + } + // L2 -> L1 withdrawal + L1StandardBridgeEvents::ETHBridgeFinalized(ETHBridgeFinalized { + .. + }) => { + let deleted = this.connection.execute( + "DELETE FROM withdrawals WHERE tx_hash = ?;", + (tx.hash().to_string(),), + )?; + withdrawals += deleted; + } + _ => continue, + }; + } + + info!(block_range = ?reverted_chain.range(), %deposits, %withdrawals, "Reverted chain events"); + } + + // Insert all new deposits and withdrawals + let committed_chain = notification.committed(); + let events = decode_chain_into_events(&committed_chain); + + let mut deposits = 0; + let mut withdrawals = 0; + + for (block, tx, log, event) in events { + match event { + // L1 -> L2 deposit + L1StandardBridgeEvents::ETHBridgeInitiated(ETHBridgeInitiated { + amount, + from, + to, + .. + }) => { + let inserted = this.connection.execute( + r#" + INSERT INTO deposits (block_number, tx_hash, contract_address, "from", "to", amount) + VALUES (?, ?, ?, ?, ?, ?) + "#, + ( + block.number, + tx.hash().to_string(), + log.address.to_string(), + from.to_string(), + to.to_string(), + amount.to_string(), + ), + )?; + deposits += inserted; + } + // L2 -> L1 withdrawal + L1StandardBridgeEvents::ETHBridgeFinalized(ETHBridgeFinalized { + amount, + from, + to, + .. + }) => { + let inserted = this.connection.execute( + r#" + INSERT INTO withdrawals (block_number, tx_hash, contract_address, "from", "to", amount) + VALUES (?, ?, ?, ?, ?, ?) + "#, + ( + block.number, + tx.hash().to_string(), + log.address.to_string(), + from.to_string(), + to.to_string(), + amount.to_string(), + ), + )?; + withdrawals += inserted; + } + _ => continue, + }; + } + + info!(block_range = ?committed_chain.range(), %deposits, %withdrawals, "Committed chain events"); + + // Send a finished height event, signaling the node that we don't need any blocks below + // this height anymore + this.ctx.events.send(ExExEvent::FinishedHeight(notification.tip().number))?; + } + + Poll::Pending + } +} + +/// Decode chain of blocks into a flattened list of receipt logs, and filter only +/// [L1StandardBridgeEvents]. +fn decode_chain_into_events( + chain: &Chain, +) -> impl Iterator +{ + 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)) + }) + // Get all logs + .flat_map(|(block, tx, receipt)| receipt.logs.iter().map(move |log| (block, tx, log))) + // Decode and filter bridge events + .filter_map(|(block, tx, log)| { + L1StandardBridgeEvents::decode_raw_log(&log.topics, &log.data, true) + .ok() + .map(|event| (block, tx, log, event)) + }) +} + +fn main() -> eyre::Result<()> { + reth::cli::Cli::parse_args().run(|builder, _| async move { + let handle = builder + .node(EthereumNode::default()) + .install_exex("OPBridge", move |ctx| async { + let connection = Connection::open("op_bridge.db")?; + OPBridgeExEx::new(ctx, connection) + }) + .launch() + .await?; + + handle.wait_for_node_exit().await + }) +}