From 2e48193c75d62a7d0b184b788ac9f86d68ea7fa6 Mon Sep 17 00:00:00 2001 From: refcell Date: Wed, 24 Jul 2024 14:45:51 -0400 Subject: [PATCH] fix: cleanup and document op-test-vectors --- book/src/format.md | 25 +++ crates/op-test-vectors/Cargo.toml | 3 +- crates/op-test-vectors/README.md | 7 + crates/op-test-vectors/src/derivation.rs | 7 + crates/op-test-vectors/src/execution.rs | 196 ++++++++++-------- crates/op-test-vectors/src/lib.rs | 6 + .../src/testdata/environment.json | 11 + .../op-test-vectors/src/testdata/result.json | 22 ++ .../src/testdata/tx_receipt.json | 18 ++ crates/opt8n/src/opt8n.rs | 2 +- 10 files changed, 213 insertions(+), 84 deletions(-) create mode 100644 crates/op-test-vectors/README.md create mode 100644 crates/op-test-vectors/src/testdata/environment.json create mode 100644 crates/op-test-vectors/src/testdata/result.json create mode 100644 crates/op-test-vectors/src/testdata/tx_receipt.json diff --git a/book/src/format.md b/book/src/format.md index e69de29..67c2d05 100644 --- a/book/src/format.md +++ b/book/src/format.md @@ -0,0 +1,25 @@ +# Test Fixture Format + +There are two primary types of test fixtures: Derivation and Execution. +Test fixtures are static JSON files that live in the [fixtures][fixtures] directory. + +## Execution Test Fixtures + +Execution test fixtures live inside the `fixtures/execution/` directory. +Each JSON file in this directory contains the JSON-serialized +[`ExecutionFixture`][exec-fixture] object which is defined in Rust +in the [op-test-vectors][op-test-vectors] crate. + +The `ExecutionFixture` holds everything needed to test execution of the OP Stack. +It's composed of the following. +- An `ExecutionEnvironment`, which is used to setup the execution client's environment. +- An initial set of addresses and their states, also called the "pre-state". +- A final set of addresses and their states, also called the "post-state". +- A list of transactions to execute in the environment. +- The result of executing all the transactions. + +## Derivation Test Fixtures + +// TODO + +{{#include ../links.md}} diff --git a/crates/op-test-vectors/Cargo.toml b/crates/op-test-vectors/Cargo.toml index 2391ed9..f268c54 100644 --- a/crates/op-test-vectors/Cargo.toml +++ b/crates/op-test-vectors/Cargo.toml @@ -7,6 +7,7 @@ homepage.workspace = true version.workspace = true authors.workspace = true edition.workspace = true +exclude = ["src/testdata"] [dependencies] op-alloy-rpc-types.workspace = true @@ -14,7 +15,7 @@ op-alloy-consensus.workspace = true alloy.workspace = true serde.workspace = true anvil-core.workspace = true +color-eyre.workspace = true [dev-dependencies] -color-eyre.workspace = true serde_json.workspace = true diff --git a/crates/op-test-vectors/README.md b/crates/op-test-vectors/README.md new file mode 100644 index 0000000..2c77d4b --- /dev/null +++ b/crates/op-test-vectors/README.md @@ -0,0 +1,7 @@ +# OP Test Vectors + +Rust test fixture type definitions for the OP Stack. + +There are two primary test fixture types in this crate: +- execution +- derivation diff --git a/crates/op-test-vectors/src/derivation.rs b/crates/op-test-vectors/src/derivation.rs index c2f0b3b..35a62c3 100644 --- a/crates/op-test-vectors/src/derivation.rs +++ b/crates/op-test-vectors/src/derivation.rs @@ -1 +1,8 @@ +//! Module containing the derivation test fixture. + +use serde::{Deserialize, Serialize}; + +/// The derivation fixture is the top-level object that contains +/// everything needed to run a derivation test. +#[derive(Serialize, Deserialize, Debug, Default)] pub struct DerivationFixture {} diff --git a/crates/op-test-vectors/src/execution.rs b/crates/op-test-vectors/src/execution.rs index 9176718..bdfd66e 100644 --- a/crates/op-test-vectors/src/execution.rs +++ b/crates/op-test-vectors/src/execution.rs @@ -1,157 +1,189 @@ -use std::collections::HashMap; +//! Module containing the execution test fixture. +use std::collections::HashMap; use alloy::primitives::{Address, Bloom, B256, U256}; use alloy::rpc::types::trace::geth::AccountState; use alloy::rpc::types::{Log, TransactionReceipt}; use anvil_core::eth::block::Block; use anvil_core::eth::transaction::{TypedReceipt, TypedTransaction}; use serde::{Deserialize, Serialize}; +use color_eyre::eyre; +/// The execution fixture is the top-level object that contains +/// everything needed to run an execution test. #[derive(Serialize, Deserialize, Debug, Default)] pub struct ExecutionFixture { + /// The execution environment sets up the current block context. pub env: ExecutionEnvironment, + /// The initial state of the accounts before running the transactions, also called the + /// "pre-state". pub alloc: HashMap, + /// The expected state of the accounts after running the transactions, also called the + /// "post-state". pub out_alloc: HashMap, + /// Transactions to execute. #[serde(rename = "txs")] pub transactions: Vec, + /// The expected result after executing transactions. pub result: ExecutionResult, } +/// The execution environment is the initial state of the execution context. +/// It's used to set the execution environment current block information. #[derive(Serialize, Deserialize, Debug, Default)] #[serde(rename_all = "camelCase")] pub struct ExecutionEnvironment { + /// The current block coinbase. pub current_coinbase: Address, + /// The current block difficulty. pub current_difficulty: U256, + /// The current block gas limit. pub current_gas_limit: U256, + /// The previous block hash. pub previous_hash: B256, + /// The current block number. pub current_number: U256, + /// The current block timestamp. pub current_timestamp: U256, + /// The block hashes of the previous blocks. #[serde(skip_serializing_if = "Option::is_none")] pub block_hashes: Option>, } +impl From for ExecutionEnvironment { + fn from(block: Block) -> Self { + Self { + current_coinbase: block.header.beneficiary, + current_difficulty: block.header.difficulty, + current_gas_limit: U256::from(block.header.gas_limit), + previous_hash: block.header.parent_hash, + current_number: U256::from(block.header.number), + current_timestamp: U256::from(block.header.timestamp), + block_hashes: None, + } + } +} + +/// The execution result is the expected result after running the transactions +/// in the execution environment over the pre-state. #[derive(Serialize, Deserialize, Debug, Default)] #[serde(rename_all = "camelCase")] pub struct ExecutionResult { + /// The state root. pub state_root: B256, + /// The transaction root. pub tx_root: B256, + /// The receipt root. pub receipt_root: B256, + /// The logs hash. pub logs_hash: B256, + /// The logs bloom. pub logs_bloom: Bloom, + /// A list of execution receipts for each executed transaction. pub receipts: Vec, } +/// An execution receipt is the result of running a transaction in the execution environment. #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct ExecutionReceipt { + /// The state root. pub root: B256, + /// The hash of the transaction. pub transaction_hash: B256, - pub contract_address: Address, + /// The contract address that the transaction created. + #[serde(skip_serializing_if = "Option::is_none")] + pub contract_address: Option
, + /// The gas used by the transaction. pub gas_used: U256, + /// The block hash. pub block_hash: B256, + /// The transaction index. pub transaction_index: U256, + /// The inner log receipt. #[serde(flatten)] pub inner: TypedReceipt, } -impl From for ExecutionEnvironment { - fn from(block: Block) -> Self { - Self { - current_coinbase: block.header.beneficiary, - current_difficulty: block.header.difficulty, - current_gas_limit: U256::from(block.header.gas_limit), - previous_hash: block.header.parent_hash, - current_number: U256::from(block.header.number), - current_timestamp: U256::from(block.header.timestamp), - block_hashes: None, - } - } -} +impl TryFrom>> for ExecutionReceipt { + type Error = eyre::Error; -impl From>> for ExecutionReceipt { - fn from(receipt: TransactionReceipt>) -> ExecutionReceipt { - ExecutionReceipt { + fn try_from(receipt: TransactionReceipt>) -> eyre::Result { + Ok(Self { transaction_hash: receipt.transaction_hash, - root: receipt.state_root.unwrap_or_default(), - contract_address: receipt.contract_address.unwrap_or_default(), + root: receipt.state_root.ok_or_else(|| eyre::eyre!("missing state root"))?, + contract_address: receipt.contract_address, gas_used: U256::from(receipt.gas_used), - block_hash: receipt.block_hash.unwrap_or_default(), - transaction_index: U256::from(receipt.transaction_index.unwrap_or_default()), + block_hash: receipt.block_hash.ok_or_else(|| eyre::eyre!("missing block hash"))?, + transaction_index: U256::from(receipt.transaction_index.ok_or_else(|| eyre::eyre!("missing transaction index"))?), inner: receipt.inner, - } + }) } } #[cfg(test)] mod tests { - - use super::ExecutionEnvironment; - use crate::execution::ExecutionResult; - use color_eyre::eyre; + use super::*; use serde_json::Value; #[test] - fn test_serialize_execution_environment() -> eyre::Result<()> { - let expected_env = r#" - { - "currentCoinbase" : "0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba", - "currentDifficulty" : "0x20000", - "currentGasLimit" : "0x5f5e100", - "currentNumber" : "0x1", - "currentTimestamp" : "0x3e8", - "previousHash" : "0xe729de3fec21e30bea3d56adb01ed14bc107273c2775f9355afb10f594a10d9e", - "blockHashes" : { - "0x0" : "0xe729de3fec21e30bea3d56adb01ed14bc107273c2775f9355afb10f594a10d9e" - } - } - "#; - - let env = serde_json::from_str::(expected_env)?; - let serialized_env = serde_json::to_string(&env)?; - - assert_eq!( - serde_json::from_str::(expected_env)?, - serde_json::from_str::(&serialized_env)? - ); + fn test_serialize_execution_environment() { + let expected_env = include_str!("./testdata/environment.json"); + let env = serde_json::from_str::(expected_env).expect("failed to parse environment"); + let serialized_env = serde_json::to_string(&env).expect("failed to serialize environment"); + let serialized_value = serde_json::from_str::(&serialized_env).expect("failed to parse serialized environment"); + let expected_value = serde_json::from_str::(expected_env).expect("failed to parse expected environment"); + assert_eq!(serialized_value, expected_value); + } - Ok(()) + #[test] + fn test_serialize_execution_result() { + let expected_result = include_str!("./testdata/result.json"); + let execution_result = serde_json::from_str::(expected_result).expect("failed to parse result"); + let serialized_result = serde_json::to_string(&execution_result).expect("failed to serialize result"); + let serialized_value = serde_json::from_str::(&serialized_result).expect("failed to parse serialized result"); + let expected_value = serde_json::from_str::(expected_result).expect("failed to parse expected result"); + assert_eq!(serialized_value, expected_value); } #[test] - fn test_serialize_execution_result() -> eyre::Result<()> { - let expected_result = r#" - { - "stateRoot": "0x1c99b01120e7a2fa1301b3505f20100e72362e5ac3f96854420e56ba8984d716", - "txRoot": "0xb5eee60b45801179cbde3781b9a5dee9b3111554618c9cda3d6f7e351fd41e0b", - "receiptRoot": "0x86ceb80cb6bef8fe4ac0f1c99409f67cb2554c4432f374e399b94884eb3e6562", - "logsHash": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", - "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "receipts": [ - { - "root": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", - "status": "0x1", - "type": "0x0", - "cumulativeGasUsed": "0xa878", - "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "logs": [], - "transactionHash": "0x4e6549e2276d1bc256b2a56ead2d9705a51a8bf54e3775fbd2e98c91fb0e4494", - "contractAddress": "0x0000000000000000000000000000000000000000", - "gasUsed": "0xa878", - "blockHash": "0x0000000000000000000000000000000000000000000000000000000000000000", - "transactionIndex": "0x0" - } - ] - } - "#; + fn test_exec_receipt_try_from_tx_receipt() { + let tx_receipt_str = include_str!("./testdata/tx_receipt.json"); + let tx_receipt: TransactionReceipt> = serde_json::from_str(tx_receipt_str).expect("failed to parse tx receipt"); + let exec_receipt = ExecutionReceipt::try_from(tx_receipt.clone()).expect("failed to convert tx receipt to exec receipt"); + assert_eq!(exec_receipt.transaction_hash, tx_receipt.transaction_hash); + assert_eq!(exec_receipt.root, tx_receipt.state_root.unwrap()); + assert_eq!(exec_receipt.contract_address, tx_receipt.contract_address); + assert_eq!(exec_receipt.gas_used, U256::from(tx_receipt.gas_used)); + assert_eq!(exec_receipt.block_hash, tx_receipt.block_hash.unwrap()); + assert_eq!(exec_receipt.transaction_index, U256::from(tx_receipt.transaction_index.unwrap())); + assert_eq!(exec_receipt.inner, tx_receipt.inner); + } - let execution_result = serde_json::from_str::(expected_result)?; - let serialized_result = serde_json::to_string(&execution_result)?; + #[test] + fn test_exec_receipt_try_from_missing_root() { + let tx_receipt_str = include_str!("./testdata/tx_receipt.json"); + let mut tx_receipt: TransactionReceipt> = serde_json::from_str(tx_receipt_str).expect("failed to parse tx receipt"); + tx_receipt.state_root = None; + let exec_receipt = ExecutionReceipt::try_from(tx_receipt); + assert!(exec_receipt.is_err()); + } - assert_eq!( - serde_json::from_str::(expected_result)?, - serde_json::from_str::(&serialized_result)? - ); + #[test] + fn test_exec_receipt_try_from_missing_block_hash() { + let tx_receipt_str = include_str!("./testdata/tx_receipt.json"); + let mut tx_receipt: TransactionReceipt> = serde_json::from_str(tx_receipt_str).expect("failed to parse tx receipt"); + tx_receipt.block_hash = None; + let exec_receipt = ExecutionReceipt::try_from(tx_receipt); + assert!(exec_receipt.is_err()); + } - Ok(()) + #[test] + fn test_exec_receipt_try_from_missing_tx_index() { + let tx_receipt_str = include_str!("./testdata/tx_receipt.json"); + let mut tx_receipt: TransactionReceipt> = serde_json::from_str(tx_receipt_str).expect("failed to parse tx receipt"); + tx_receipt.transaction_index = None; + let exec_receipt = ExecutionReceipt::try_from(tx_receipt); + assert!(exec_receipt.is_err()); } } diff --git a/crates/op-test-vectors/src/lib.rs b/crates/op-test-vectors/src/lib.rs index e1745d8..35d85de 100644 --- a/crates/op-test-vectors/src/lib.rs +++ b/crates/op-test-vectors/src/lib.rs @@ -1,2 +1,8 @@ +#![doc = include_str!("../README.md")] +#![warn(missing_debug_implementations, missing_docs, unreachable_pub, rustdoc::all)] +#![deny(unused_must_use, rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + pub mod derivation; + pub mod execution; diff --git a/crates/op-test-vectors/src/testdata/environment.json b/crates/op-test-vectors/src/testdata/environment.json new file mode 100644 index 0000000..5dca82e --- /dev/null +++ b/crates/op-test-vectors/src/testdata/environment.json @@ -0,0 +1,11 @@ +{ + "currentCoinbase" : "0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba", + "currentDifficulty" : "0x20000", + "currentGasLimit" : "0x5f5e100", + "currentNumber" : "0x1", + "currentTimestamp" : "0x3e8", + "previousHash" : "0xe729de3fec21e30bea3d56adb01ed14bc107273c2775f9355afb10f594a10d9e", + "blockHashes" : { + "0x0" : "0xe729de3fec21e30bea3d56adb01ed14bc107273c2775f9355afb10f594a10d9e" + } + } diff --git a/crates/op-test-vectors/src/testdata/result.json b/crates/op-test-vectors/src/testdata/result.json new file mode 100644 index 0000000..7fdd264 --- /dev/null +++ b/crates/op-test-vectors/src/testdata/result.json @@ -0,0 +1,22 @@ +{ + "stateRoot": "0x1c99b01120e7a2fa1301b3505f20100e72362e5ac3f96854420e56ba8984d716", + "txRoot": "0xb5eee60b45801179cbde3781b9a5dee9b3111554618c9cda3d6f7e351fd41e0b", + "receiptRoot": "0x86ceb80cb6bef8fe4ac0f1c99409f67cb2554c4432f374e399b94884eb3e6562", + "logsHash": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "receipts": [ + { + "root": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "status": "0x1", + "type": "0x0", + "cumulativeGasUsed": "0xa878", + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "logs": [], + "transactionHash": "0x4e6549e2276d1bc256b2a56ead2d9705a51a8bf54e3775fbd2e98c91fb0e4494", + "contractAddress": "0x0000000000000000000000000000000000000000", + "gasUsed": "0xa878", + "blockHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "transactionIndex": "0x0" + } + ] +} diff --git a/crates/op-test-vectors/src/testdata/tx_receipt.json b/crates/op-test-vectors/src/testdata/tx_receipt.json new file mode 100644 index 0000000..06d4116 --- /dev/null +++ b/crates/op-test-vectors/src/testdata/tx_receipt.json @@ -0,0 +1,18 @@ +{ + "root": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "type": "0x0", + "status": "0x1", + "cumulativeGasUsed": "0xa878", + "logs": [], + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "transactionHash": "0x4e6549e2276d1bc256b2a56ead2d9705a51a8bf54e3775fbd2e98c91fb0e4494", + "transactionIndex": "0x0", + "blockHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "blockNumber": "0x0", + "gasUsed": "0xa878", + "effectiveGasPrice": "0x0", + "blobGasUsed": "0xa878", + "blobGasPrice": "0x0", + "from": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "to": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +} diff --git a/crates/opt8n/src/opt8n.rs b/crates/opt8n/src/opt8n.rs index 2952df8..b7f99e9 100644 --- a/crates/opt8n/src/opt8n.rs +++ b/crates/opt8n/src/opt8n.rs @@ -201,7 +201,7 @@ impl Opt8n { for tx in &transactions { if let Some(receipt) = self.eth_api.backend.transaction_receipt(tx.hash()).await? { - receipts.push(receipt.into()); + receipts.push(receipt.try_into()?); } self.execution_fixture.transactions.push(tx.to_owned()); }