example(exex): tests for OP Bridge (#8658)

This commit is contained in:
Alexey Shekhirin
2024-06-07 16:52:44 +01:00
committed by GitHub
parent ade059235b
commit d0e3504ecc
5 changed files with 208 additions and 1 deletions

5
Cargo.lock generated
View File

@ -2822,14 +2822,19 @@ dependencies = [
"alloy-sol-types",
"eyre",
"futures",
"rand 0.8.5",
"reth",
"reth-exex",
"reth-exex-test-utils",
"reth-node-api",
"reth-node-ethereum",
"reth-primitives",
"reth-provider",
"reth-testing-utils",
"reth-tracing",
"rusqlite",
"tempfile",
"tokio",
]
[[package]]

View File

@ -145,6 +145,26 @@ impl TestExExHandle {
Ok(())
}
/// Send a notification to the Execution Extension that the chain has been reorged
pub async fn send_notification_chain_reorged(
&self,
old: Chain,
new: Chain,
) -> eyre::Result<()> {
self.notifications_tx
.send(ExExNotification::ChainReorged { old: Arc::new(old), new: Arc::new(new) })
.await?;
Ok(())
}
/// Send a notification to the Execution Extension that the chain has been reverted
pub async fn send_notification_chain_reverted(&self, chain: Chain) -> eyre::Result<()> {
self.notifications_tx
.send(ExExNotification::ChainReverted { old: Arc::new(chain) })
.await?;
Ok(())
}
/// Asserts that the Execution Extension did not emit any events.
#[track_caller]
pub fn assert_events_empty(&self) {

View File

@ -18,3 +18,11 @@ eyre.workspace = true
futures.workspace = true
alloy-sol-types = { workspace = true, features = ["json"] }
rusqlite = { version = "0.31.0", features = ["bundled"] }
[dev-dependencies]
reth-exex-test-utils.workspace = true
reth-testing-utils.workspace = true
tokio.workspace = true
rand.workspace = true
tempfile.workspace = true

View File

@ -252,3 +252,172 @@ fn main() -> eyre::Result<()> {
handle.wait_for_node_exit().await
})
}
#[cfg(test)]
mod tests {
use std::pin::pin;
use alloy_sol_types::SolEvent;
use reth::revm::db::BundleState;
use reth_exex_test_utils::{test_exex_context, PollOnce};
use reth_primitives::{
Address, Block, Header, Log, Receipt, Receipts, Transaction, TransactionSigned, TxKind,
TxLegacy, TxType, U256,
};
use reth_provider::{BundleStateWithReceipts, Chain};
use reth_testing_utils::generators::sign_tx_with_random_key_pair;
use rusqlite::Connection;
use crate::{L1StandardBridge, OP_BRIDGES};
/// Given the address of a bridge contract and an event, construct a transaction signed with a
/// random private key and a receipt for that transaction.
fn construct_tx_and_receipt<E: SolEvent>(
to: Address,
event: E,
) -> eyre::Result<(TransactionSigned, Receipt)> {
let tx = Transaction::Legacy(TxLegacy { to: TxKind::Call(to), ..Default::default() });
let log = Log::new(
to,
event.encode_topics().into_iter().map(|topic| topic.0).collect(),
event.encode_data().into(),
)
.ok_or_else(|| eyre::eyre!("failed to encode event"))?;
let receipt = Receipt {
tx_type: TxType::Legacy,
success: true,
cumulative_gas_used: 0,
logs: vec![log],
..Default::default()
};
Ok((sign_tx_with_random_key_pair(&mut rand::thread_rng(), tx), receipt))
}
#[tokio::test]
async fn test_exex() -> eyre::Result<()> {
// Initialize the test Execution Extension context with all dependencies
let (ctx, handle) = test_exex_context().await?;
// Create a temporary database file, so we can access it later for assertions
let db_file = tempfile::NamedTempFile::new()?;
// Initialize the ExEx
let mut exex = pin!(super::init(ctx, Connection::open(&db_file)?).await?);
// Generate random "from" and "to" addresses for deposit and withdrawal events
let from_address = Address::random();
let to_address = Address::random();
// Construct deposit event, transaction and receipt
let deposit_event = L1StandardBridge::ETHBridgeInitiated {
from: from_address,
to: to_address,
amount: U256::from(100),
extraData: Default::default(),
};
let (deposit_tx, deposit_tx_receipt) =
construct_tx_and_receipt(OP_BRIDGES[0], deposit_event.clone())?;
// Construct withdrawal event, transaction and receipt
let withdrawal_event = L1StandardBridge::ETHBridgeFinalized {
from: from_address,
to: to_address,
amount: U256::from(200),
extraData: Default::default(),
};
let (withdrawal_tx, withdrawal_tx_receipt) =
construct_tx_and_receipt(OP_BRIDGES[1], withdrawal_event.clone())?;
// Construct a block
let block = Block {
header: Header::default(),
body: vec![deposit_tx, withdrawal_tx],
..Default::default()
}
.seal_slow()
.seal_with_senders()
.ok_or_else(|| eyre::eyre!("failed to recover senders"))?;
// Construct a chain
let chain = Chain::new(
vec![block.clone()],
BundleStateWithReceipts::new(
BundleState::default(),
Receipts::from_block_receipt(vec![deposit_tx_receipt, withdrawal_tx_receipt]),
block.number,
),
None,
);
// Send a notification that the chain has been committed
handle.send_notification_chain_committed(chain.clone()).await?;
// Poll the ExEx once, it will process the notification that we just sent
exex.poll_once().await?;
let connection = Connection::open(&db_file)?;
// Assert that the deposit event was parsed correctly and inserted into the database
let deposits: Vec<(u64, String, String, String, String, String)> = connection
.prepare(r#"SELECT block_number, contract_address, "from", "to", amount, tx_hash FROM deposits"#)?
.query_map([], |row| {
Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?, row.get(4)?, row.get(5)?))
})?
.collect::<Result<Vec<_>, _>>()?;
assert_eq!(deposits.len(), 1);
assert_eq!(
deposits[0],
(
block.number,
OP_BRIDGES[0].to_string(),
from_address.to_string(),
to_address.to_string(),
deposit_event.amount.to_string(),
block.body[0].hash().to_string()
)
);
// Assert that the withdrawal event was parsed correctly and inserted into the database
let withdrawals: Vec<(u64, String, String, String, String, String)> = connection
.prepare(r#"SELECT block_number, contract_address, "from", "to", amount, tx_hash FROM withdrawals"#)?
.query_map([], |row| {
Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?, row.get(4)?, row.get(5)?))
})?
.collect::<Result<Vec<_>, _>>()?;
assert_eq!(withdrawals.len(), 1);
assert_eq!(
withdrawals[0],
(
block.number,
OP_BRIDGES[1].to_string(),
from_address.to_string(),
to_address.to_string(),
withdrawal_event.amount.to_string(),
block.body[1].hash().to_string()
)
);
// Send a notification that the same chain has been reverted
handle.send_notification_chain_reverted(chain).await?;
// Poll the ExEx once, it will process the notification that we just sent
exex.poll_once().await?;
// Assert that the deposit was removed from the database
let deposits = connection
.prepare(r#"SELECT block_number, contract_address, "from", "to", amount, tx_hash FROM deposits"#)?
.query_map([], |_| {
Ok(())
})?
.count();
assert_eq!(deposits, 0);
// Assert that the withdrawal was removed from the database
let withdrawals = connection
.prepare(r#"SELECT block_number, contract_address, "from", "to", amount, tx_hash FROM withdrawals"#)?
.query_map([], |_| {
Ok(())
})?
.count();
assert_eq!(withdrawals, 0);
Ok(())
}
}

View File

@ -89,9 +89,14 @@ pub fn random_tx<R: Rng>(rng: &mut R) -> Transaction {
///
/// - There is no guarantee that the nonce is not used twice for the same account
pub fn random_signed_tx<R: Rng>(rng: &mut R) -> TransactionSigned {
let tx = random_tx(rng);
sign_tx_with_random_key_pair(rng, tx)
}
/// Signs the [Transaction] with a random key pair.
pub fn sign_tx_with_random_key_pair<R: Rng>(rng: &mut R, tx: Transaction) -> TransactionSigned {
let secp = Secp256k1::new();
let key_pair = Keypair::new(&secp, rng);
let tx = random_tx(rng);
sign_tx_with_key_pair(key_pair, tx)
}