feat(examples): OP Stack bridge stats ExEx (#7556)

Co-authored-by: Oliver Nordbjerg <hi@notbjerg.me>
Co-authored-by: Oliver Nordbjerg <onbjerg@users.noreply.github.com>
This commit is contained in:
Alexey Shekhirin
2024-04-11 17:38:03 +01:00
committed by GitHub
parent 007e5c2c47
commit 3ffc729833
9 changed files with 1019 additions and 7 deletions

View File

@ -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"] }

View File

@ -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" }
]

View File

@ -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<Node: FullNodeTypes> {
ctx: ExExContext<Node>,
connection: Connection,
}
impl<Node: FullNodeTypes> OPBridgeExEx<Node> {
fn new(ctx: ExExContext<Node>, connection: Connection) -> eyre::Result<Self> {
// 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<Node: FullNodeTypes> Future for OPBridgeExEx<Node> {
type Output = eyre::Result<()>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
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<Item = (&SealedBlockWithSenders, &TransactionSigned, &Log, L1StandardBridgeEvents)>
{
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
})
}