From 463bc30ffed512c705bf2c1853e4e91c33bdff4b Mon Sep 17 00:00:00 2001 From: keanji-x Date: Wed, 22 Jul 2026 13:33:08 +0800 Subject: [PATCH 1/4] =?UTF-8?q?test(pipe-exec):=20add=20tx=5Ffilter?= =?UTF-8?q?=E2=9F=B7revm=20differential=20oracle=20(difftx)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增机制驱动的差分测试预言机,机械化验证核心不变式 **tx_filter ⊇ revm 交易级校验**:任何被 tx_filter 放行、却被 revm 以 InvalidTransaction 拒绝的交易,都会在 lib.rs:1323 的 executor.execute().unwrap() 处 panic = 停链 DoS。两侧预言机喂同一 (tx, sender, 种子状态),revm 侧走真实 pinned revm 的 evm.transact。 - Tier-1(单 tx 字段级):覆盖 revm 全 32 个 InvalidTransaction 变体,12 个 live crafter 经断言确认能真触发 revm 拒绝;当前 0 gap(证明 filter 是字段级 超集)。matrix_has_no_gap 作为 CI 门;no_gap_gate_is_non_vacuous 防止空过。 - Tier-2(多 tx 顺序执行差分):确定性在进程内复现已知 7702 停链——filter 放行 [tx0,tx1] 两笔,但执行 tx0 的 7702 委托码 CREATE 把账户 A 的 nonce 二次抬升, tx1 执行期 NonceTooLow{tx:1,state:2} → 停链。等价于 gnode attack 7702-halt。 仅新增测试/差分入口(difftx feature 门控的 difftx 模块 + difftx 二进制), 不改任何生产逻辑;tx_filter.rs 保持不变。 跑法:cargo run -p reth-pipe-exec-layer-ext-v2 --features difftx --bin difftx (exit 3 = 发现 gap)。 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../pipe-exec-layer-ext-v2/execute/Cargo.toml | 8 + .../execute/src/bin/difftx.rs | 108 ++ .../execute/src/difftx.rs | 1007 +++++++++++++++++ .../pipe-exec-layer-ext-v2/execute/src/lib.rs | 5 + 4 files changed, 1128 insertions(+) create mode 100644 crates/pipe-exec-layer-ext-v2/execute/src/bin/difftx.rs create mode 100644 crates/pipe-exec-layer-ext-v2/execute/src/difftx.rs diff --git a/crates/pipe-exec-layer-ext-v2/execute/Cargo.toml b/crates/pipe-exec-layer-ext-v2/execute/Cargo.toml index 99c208b7b..485e4e4df 100644 --- a/crates/pipe-exec-layer-ext-v2/execute/Cargo.toml +++ b/crates/pipe-exec-layer-ext-v2/execute/Cargo.toml @@ -88,7 +88,15 @@ ureq = { version = "2", features = ["json"] } [features] pipe_test = ["reth-provider/pipe_test"] config-from-env = ["gravity-primitives/config-from-env"] +# 差分预言机(tx_filter ⊇ revm):编译 difftx 模块与 difftx 工具二进制。 +difftx = [] [[bench]] name = "bls_pop_verify_bench" harness = false + +# 差分预言机命令行工具:跑 revm 变体覆盖矩阵 + 7702 执行诱导 TOCTOU 自检。 +[[bin]] +name = "difftx" +path = "src/bin/difftx.rs" +required-features = ["difftx"] diff --git a/crates/pipe-exec-layer-ext-v2/execute/src/bin/difftx.rs b/crates/pipe-exec-layer-ext-v2/execute/src/bin/difftx.rs new file mode 100644 index 000000000..4ad511a72 --- /dev/null +++ b/crates/pipe-exec-layer-ext-v2/execute/src/bin/difftx.rs @@ -0,0 +1,108 @@ +//! 差分测试预言机命令行入口。 +//! +//! 运行整个 revm 变体矩阵,打印人读表格 + 完整 JSON;对每个停链缺口(gap)在 +//! `difftx-repro/` 目录写一个 gnode `send` 兼容的复现文件。有 gap 退出码 3,否则 0。 +//! +//! 必须带 `--features difftx` 构建/运行: +//! ```text +//! cargo run -p reth-pipe-exec-layer-ext-v2 --features difftx --bin difftx +//! ``` + +use reth_pipe_exec_layer_ext_v2::difftx::{self, DiffOutcome}; +use std::{fs, path::Path, process::ExitCode}; + +fn main() -> ExitCode { + let matrix: Vec = difftx::variant_matrix(); + + // —— 人读表格 —— // + println!("差分预言机矩阵:tx_filter ⊇ revm 交易级校验"); + println!("{:<34} | {:^12} | {:^11} | verdict", "variant", "filter_admit", "revm_reject"); + println!("{}", "-".repeat(90)); + let mut gap_count = 0usize; + for o in &matrix { + let verdict = if o.is_gap { + "CHAIN-HALT GAP" + } else if o.revm_reject { + "OK (both reject)" + } else if o.revm_variant.as_deref().map(|s| s.contains(':')).unwrap_or(false) { + // 说明行:revm_variant 里带分类前缀(xxx: 原因)。 + "note" + } else { + "OK (both admit)" + }; + if o.is_gap { + gap_count += 1; + } + println!("{:<34} | {:^12} | {:^11} | {}", o.name, o.filter_admit, o.revm_reject, verdict); + } + println!("{}", "-".repeat(90)); + println!("总计 {} 行,gap {} 个", matrix.len(), gap_count); + + // —— 完整 JSON —— // + let json_matrix: Vec = matrix + .iter() + .map(|o| { + serde_json::json!({ + "name": o.name, + "filter_admit": o.filter_admit, + "revm_reject": o.revm_reject, + "revm_variant": o.revm_variant, + "is_gap": o.is_gap, + }) + }) + .collect(); + println!("\n完整 JSON:\n{}", serde_json::to_string_pretty(&json_matrix).unwrap()); + + // ==================== Tier-2:多笔交易序列执行差分 ==================== // + println!("\n============ Tier-2:多笔交易序列执行差分(执行诱导 TOCTOU)============"); + let mut seq_gap_count = 0usize; + let repro_dir = Path::new("difftx-repro"); + for case in difftx::seq_cases() { + let out = difftx::run_seq_case(&case); + println!("\n用例 {}: filter_admitted={:?}", out.name, out.filter_admitted); + if out.exec_gaps.is_empty() { + println!(" 执行缺口: 无(该序列被 filter 拦截或执行存活)"); + } else { + for g in &out.exec_gaps { + println!( + " 执行缺口: tx#{} sender=0x{} -> {}", + g.tx_index, + hex::encode(g.sender), + g.revm_variant + ); + seq_gap_count += 1; + } + // 写 gnode 可复现工件。 + fs::create_dir_all(repro_dir).expect("创建 difftx-repro 目录"); + let repro = difftx::emit_seq_repro(&case, &out); + let path = repro_dir.join(format!("seq_{}.json", out.name)); + fs::write(&path, serde_json::to_string_pretty(&repro).unwrap()) + .expect("写序列复现文件"); + eprintln!("写出序列复现文件: {}", path.display()); + } + } + + // —— 对每个 Tier-1 gap 写复现文件 —— // + if gap_count > 0 { + fs::create_dir_all(repro_dir).expect("创建 difftx-repro 目录"); + // 重新按可造用例遍历,找出 gap 并导出(run_case + emit_repro)。 + for case in difftx::craftable_cases_pub() { + let out = difftx::run_case(&case); + if out.is_gap { + let repro = difftx::emit_repro(&case, &out); + let path = repro_dir.join(format!("{}.json", out.name)); + fs::write(&path, serde_json::to_string_pretty(&repro).unwrap()) + .expect("写复现文件"); + eprintln!("写出复现文件: {}", path.display()); + } + } + } + + println!("\n总判定:Tier-1 gap={} 个,Tier-2 exec_gap={} 个", gap_count, seq_gap_count); + // 任一 tier 出现 gap 即退出码 3(Tier-2 的 exec_gap 就是已知真链停机的复现)。 + if gap_count > 0 || seq_gap_count > 0 { + ExitCode::from(3) + } else { + ExitCode::from(0) + } +} diff --git a/crates/pipe-exec-layer-ext-v2/execute/src/difftx.rs b/crates/pipe-exec-layer-ext-v2/execute/src/difftx.rs new file mode 100644 index 000000000..640d2376b --- /dev/null +++ b/crates/pipe-exec-layer-ext-v2/execute/src/difftx.rs @@ -0,0 +1,1007 @@ +//! 差分测试预言机(differential oracle)—— 机械化验证不变式 +//! **`tx_filter` ⊇ revm 交易级校验(tx-level validation)**。 +//! +//! ## 背景 +//! +//! 在 Gravity「先共识排序、后执行」的模型里,执行器无法从单笔交易的 +//! `revm::InvalidTransaction` 错误中恢复(见 `lib.rs:1323` 的 `executor.execute(...).unwrap()` +//! panic 点)。因此 `tx_filter::filter_invalid_txs` 必须是 revm 交易级校验的**超集**: +//! 任何被 filter「放行(ADMIT)」但被 revm 以 `InvalidTransaction` 拒绝的交易, +//! 都是一个能打停真链的 `DoS` 缺口(chain-halt gap)。 +//! +//! 本模块把两侧预言机(filter 侧 / revm 侧)喂**同一笔交易 + 同一份账户状态**, +//! 逐 revm 变体(variant)机械比对: +//! - `filter_admit` —— filter 是否放行(下标不在返回的 invalid 集合里)。 +//! - `revm_reject` —— revm 是否以 `EVMError::Transaction(InvalidTransaction)` 拒绝。 +//! - `is_gap = filter_admit && revm_reject` —— 即「放行了 revm 会拒绝的交易」= 停链缺口。 +//! +//! 这是 D1 Tier-1(字段级、单笔交易)预言机;不做任何 live 集群或多笔执行序列验证。 +//! +//! ## 使用 +//! +//! ```text +//! cargo test -p reth-pipe-exec-layer-ext-v2 --features difftx difftx -- --nocapture +//! cargo run -p reth-pipe-exec-layer-ext-v2 --features difftx --bin difftx +//! ``` + +use alloy_consensus::{ + constants::KECCAK_EMPTY, transaction::Transaction as _, Header, TxEip1559, TxEip7702, TxLegacy, +}; +use alloy_eips::{ + eip7702::{Authorization, SignedAuthorization}, + Typed2718, +}; +use alloy_primitives::{Address, Bytes, Signature, TxKind, U256}; +use reth_chainspec::{ChainSpec, ChainSpecBuilder, MAINNET}; +use reth_ethereum_primitives::{Transaction, TransactionSigned}; +use reth_evm::{ConfigureEvm, Evm, EvmError, IntoTxEnv}; +use reth_evm_ethereum::EthEvmConfig; +use reth_primitives::Recovered; +use revm::{ + database::{CacheDB, EmptyDB, State}, + state::{AccountInfo, Bytecode}, + DatabaseCommit, +}; +use revm_primitives::eip3860::MAX_INITCODE_SIZE; +use std::sync::Arc; + +/// 主网 chain id —— 所有测试 chainspec 都由 `MAINNET` 派生,filter 的 chain-id 门会 +/// 拒绝任何 `chain_id` 不等于此值的 typed 交易。 +const MAINNET_CHAIN_ID: u64 = 1; + +/// 两侧预言机共用的区块时间戳 / 高度。**必须取 0**:`ChainSpecBuilder::from(&*MAINNET)` +/// 会带入主网真实的 Cancun/Prague 时间戳,而 `shanghai_activated()` 只在 ts=0 强制打开 +/// Shanghai、并不移除后续 fork——若用一个很大的时间戳,Shanghai spec 会被主网真实的 +/// Cancun/Prague 激活覆盖成 PRAGUE。ts=0 时:`prague_chain_spec`(Prague@0)解析出 +/// `SpecId::PRAGUE`,`shanghai_chain_spec`(Cancun 未到 ts)解析出 `SpecId::SHANGHAI`。 +/// 两侧必须传入相同的 (ts, number) 才能对齐 `SpecId`。 +const BLOCK_TIMESTAMP: u64 = 0; +const BLOCK_NUMBER: u64 = 1; + +/// 一个充裕余额(1 ETH),用于「余额不是被测因素」的用例。 +const BIG_BALANCE: u128 = 1_000_000_000_000_000_000; + +/// 喂给两侧预言机的种子账户(filter 侧塞进 `CacheDB`,revm 侧塞进同一个 `CacheDB`)。 +#[derive(Debug, Clone)] +pub struct SeedAccount { + /// 账户地址。 + pub addr: Address, + /// 账户信息(余额 / nonce / 代码)。 + pub info: AccountInfo, +} + +/// 一个差分用例:一笔交易 + 一份账户状态 + 区块参数。 +#[derive(Debug, Clone)] +pub struct DiffCase { + /// 用例名(通常等于目标 revm 变体名)。 + pub name: String, + /// 被测交易。 + pub tx: TransactionSigned, + /// 交易发送者(两侧共用,revm 侧用 `Recovered::new_unchecked` 绕过签名恢复)。 + pub sender: Address, + /// 种子账户列表;filter 侧与 revm 侧从同一份列表 seed,保证状态一致。 + pub seed: Vec, + /// 区块 base fee(同时喂给 filter 参数与 revm 的 `Header::base_fee_per_gas`)。 + pub base_fee: u64, + /// 区块 gas limit(同时喂给 filter 参数与 revm 的 `Header::gas_limit`)。 + pub block_gas_limit: u64, + /// `true` => 使用 `prague_chain_spec`;`false` => `shanghai_chain_spec`。 + pub prague: bool, +} + +/// 一个差分结果。 +#[derive(Debug, Clone)] +pub struct DiffOutcome { + /// 用例名。 + pub name: String, + /// filter 是否放行该交易。 + pub filter_admit: bool, + /// revm 是否以 `InvalidTransaction` 拒绝该交易。 + pub revm_reject: bool, + /// 被拒绝时内层 `InvalidTransaction` 的 `Debug` 串。 + pub revm_variant: Option, + /// 停链缺口:`filter_admit && revm_reject`。 + pub is_gap: bool, +} + +/// Prague 从创世激活的 chainspec —— 需要 7702 / initcode / prague 内联 gas 规则的用例用它。 +fn prague_chain_spec() -> Arc { + Arc::new(ChainSpecBuilder::from(&*MAINNET).prague_activated().build()) +} + +/// Shanghai 激活、Prague 未激活的 chainspec —— 用于 pre-Prague 的 7702 边界用例 +/// (revm 会给 `Eip7702NotSupported`)。 +fn shanghai_chain_spec() -> Arc { + Arc::new(ChainSpecBuilder::from(&*MAINNET).shanghai_activated().build()) +} + +/// 运行单个差分用例:分别喂 filter 侧与 revm 侧,计算 `is_gap`。 +pub fn run_case(case: &DiffCase) -> DiffOutcome { + let chain_spec = if case.prague { prague_chain_spec() } else { shanghai_chain_spec() }; + + // 以同一份 seed 构造 CacheDB(同时满足 filter 的 `DatabaseRef` 与 revm 的 `Database`)。 + let mut cache: CacheDB = CacheDB::new(EmptyDB::default()); + for s in &case.seed { + cache.insert_account_info(s.addr, s.info.clone()); + } + + // —— filter 侧 —— // + // `filter_invalid_txs` 返回 INVALID 下标集合;下标 0 不在其中即为「放行」。 + let invalid = crate::tx_filter::filter_invalid_txs( + cache.clone(), + std::slice::from_ref(&case.tx), + std::slice::from_ref(&case.sender), + case.base_fee, + case.block_gas_limit, + chain_spec.as_ref(), + BLOCK_TIMESTAMP, + BLOCK_NUMBER, + ); + let filter_admit = !invalid.contains(&0); + + // —— revm 侧 —— // + // Header 必须让 revm 解析出与 filter 相同的 SpecId,并满足 validate_env 的前置: + // MERGE+ 需 prevrandao(由 mix_hash 默认 ZERO 提供)、CANCUN+ 需 excess_blob_gas。 + let evm_config = EthEvmConfig::new(chain_spec); + let header = Header { + gas_limit: case.block_gas_limit, + base_fee_per_gas: Some(case.base_fee), + excess_blob_gas: Some(0), + timestamp: BLOCK_TIMESTAMP, + number: BLOCK_NUMBER, + ..Default::default() + }; + let evm_env = evm_config.evm_env(&header).expect("evm_env"); + let state = State::builder().with_bundle_update().with_database(cache).build(); + let mut evm = evm_config.evm_with_env(state, evm_env); + + // 与 lib.rs:1067 完全一致的 TxEnv 构造路径。 + let tx_env = Recovered::new_unchecked(case.tx.clone(), case.sender).into_tx_env(); + let result = evm.transact_raw(tx_env); + + // 只把 `EVMError::Transaction(InvalidTransaction)` 记为拒绝信号;Database / Header / + // Custom 错误不算交易级拒绝(不构成本预言机意义上的 gap)。 + let (revm_reject, revm_variant) = match &result { + Err(e) => match e.as_invalid_tx_err() { + Some(itx) => (true, Some(format!("{itx:?}"))), + None => (false, None), + }, + Ok(_) => (false, None), + }; + + let is_gap = filter_admit && revm_reject; + DiffOutcome { name: case.name.clone(), filter_admit, revm_reject, revm_variant, is_gap } +} + +// —————————————————————————————— 交易 / 账户 构造辅助 —————————————————————————————— // + +/// 普通外部账户(EOA)种子:空代码。 +fn eoa(addr: Address, balance: u128, nonce: u64) -> SeedAccount { + SeedAccount { + addr, + info: AccountInfo { + balance: U256::from(balance), + nonce, + code_hash: KECCAK_EMPTY, + code: None, + }, + } +} + +/// 带非委托代码的账户种子(用于 EIP-3607 / `RejectCallerWithCode`)。 +fn coded_account(addr: Address, balance: u128, nonce: u64) -> SeedAccount { + // 一段普通 EVM 字节码(非 0xef0100 委托指示符),触发 EIP-3607。 + let code = Bytecode::new_raw(Bytes::from_static(&[0x60, 0x00, 0x60, 0x00])); + SeedAccount { + addr, + info: AccountInfo { + balance: U256::from(balance), + nonce, + code_hash: code.hash_slow(), + code: Some(code), + }, + } +} + +/// 构造 legacy 交易(`chain_id = None`,合法)。 +fn legacy_tx( + nonce: u64, + gas_limit: u64, + gas_price: u128, + value: u128, + kind: TxKind, +) -> TransactionSigned { + TransactionSigned::new_unhashed( + Transaction::Legacy(TxLegacy { + nonce, + gas_price, + gas_limit, + to: kind, + value: U256::from(value), + ..Default::default() + }), + Signature::test_signature(), + ) +} + +/// 构造 EIP-1559 交易。 +fn eip1559_tx( + chain_id: u64, + nonce: u64, + gas_limit: u64, + max_fee: u128, + max_prio: u128, +) -> TransactionSigned { + TransactionSigned::new_unhashed( + Transaction::Eip1559(TxEip1559 { + chain_id, + nonce, + gas_limit, + max_fee_per_gas: max_fee, + max_priority_fee_per_gas: max_prio, + to: TxKind::Call(Address::ZERO), + ..Default::default() + }), + Signature::test_signature(), + ) +} + +/// 构造 EIP-7702(type-0x04)交易,含 `auths` 个授权项。 +fn eip7702_tx(nonce: u64, gas_limit: u64, max_fee: u128, auths: usize) -> TransactionSigned { + let authorization_list = (0..auths) + .map(|_| { + SignedAuthorization::new_unchecked( + Authorization { chain_id: U256::ZERO, address: Address::ZERO, nonce: 0 }, + 0, + U256::ZERO, + U256::ZERO, + ) + }) + .collect(); + TransactionSigned::new_unhashed( + Transaction::Eip7702(TxEip7702 { + chain_id: MAINNET_CHAIN_ID, + nonce, + gas_limit, + max_fee_per_gas: max_fee, + max_priority_fee_per_gas: 0, + authorization_list, + ..Default::default() + }), + Signature::test_signature(), + ) +} + +/// 固定发送者地址(确定性,便于复现)。 +const fn sender() -> Address { + Address::with_last_byte(0x11) +} + +// —————————————————————————————— 变体矩阵 —————————————————————————————— // + +/// 构造所有「字段级可造(field-level craftable)」的 revm 变体用例。 +/// +/// 每个 case 都是单笔孤立交易,能让 revm 返回目标 `InvalidTransaction` 变体;同时 +/// filter 也应拒绝它(本 pin 下应为零 gap)。 +// 逐个 push 是刻意的:每个用例带独立注释说明其触发的 revm 变体,比一个大 `vec![]` 更可读。 +#[allow(clippy::vec_init_then_push)] +fn craftable_cases() -> Vec { + let s = sender(); + let mut v = Vec::new(); + + // 1) GasPriceLessThanBasefee:legacy gas_price < base_fee。filter 也拒(max_fee < base_fee)。 + v.push(DiffCase { + name: "GasPriceLessThanBasefee".into(), + tx: legacy_tx(0, 21_000, 100, 0, TxKind::Call(Address::ZERO)), + sender: s, + seed: vec![eoa(s, BIG_BALANCE, 0)], + base_fee: 1000, + block_gas_limit: 30_000_000, + prague: true, + }); + + // 2) PriorityFeeGreaterThanMaxFee:1559 max_priority > max_fee。 + v.push(DiffCase { + name: "PriorityFeeGreaterThanMaxFee".into(), + tx: eip1559_tx(MAINNET_CHAIN_ID, 0, 21_000, 1000, 2000), + sender: s, + seed: vec![eoa(s, BIG_BALANCE, 0)], + base_fee: 1000, + block_gas_limit: 30_000_000, + prague: true, + }); + + // 3) NonceTooHigh:账户 nonce=0,交易 nonce=5。 + v.push(DiffCase { + name: "NonceTooHigh".into(), + tx: legacy_tx(5, 21_000, 1000, 0, TxKind::Call(Address::ZERO)), + sender: s, + seed: vec![eoa(s, BIG_BALANCE, 0)], + base_fee: 1000, + block_gas_limit: 30_000_000, + prague: true, + }); + + // 4) NonceTooLow:账户 nonce=5,交易 nonce=0。 + v.push(DiffCase { + name: "NonceTooLow".into(), + tx: legacy_tx(0, 21_000, 1000, 0, TxKind::Call(Address::ZERO)), + sender: s, + seed: vec![eoa(s, BIG_BALANCE, 5)], + base_fee: 1000, + block_gas_limit: 30_000_000, + prague: true, + }); + + // 5) LackOfFundForMaxFee:余额远小于 max_fee*gas_limit。 + v.push(DiffCase { + name: "LackOfFundForMaxFee".into(), + tx: legacy_tx(0, 21_000, 1000, 0, TxKind::Call(Address::ZERO)), + sender: s, + seed: vec![eoa(s, 1000, 0)], // 1000 wei << 21_000_000 wei 上限 + base_fee: 1000, + block_gas_limit: 30_000_000, + prague: true, + }); + + // 6) CreateInitCodeSizeLimit:create 交易 input 长度 > MAX_INITCODE_SIZE。 + let big_init = Bytes::from(vec![0u8; MAX_INITCODE_SIZE + 1]); + v.push(DiffCase { + name: "CreateInitCodeSizeLimit".into(), + tx: TransactionSigned::new_unhashed( + Transaction::Legacy(TxLegacy { + nonce: 0, + gas_price: 1000, + gas_limit: 100_000, + to: TxKind::Create, + input: big_init, + ..Default::default() + }), + Signature::test_signature(), + ), + sender: s, + seed: vec![eoa(s, BIG_BALANCE, 0)], + base_fee: 1000, + block_gas_limit: 30_000_000, + prague: true, + }); + + // 7) InvalidChainId:typed(1559) 交易带错误 chain_id。 + v.push(DiffCase { + name: "InvalidChainId".into(), + tx: eip1559_tx(999, 0, 21_000, 1000, 0), + sender: s, + seed: vec![eoa(s, BIG_BALANCE, 0)], + base_fee: 1000, + block_gas_limit: 30_000_000, + prague: true, + }); + + // 8) CallGasCostMoreThanGasLimit:内联 gas 不足(gas_limit < 21000)。 + v.push(DiffCase { + name: "CallGasCostMoreThanGasLimit".into(), + tx: legacy_tx(0, 20_000, 1000, 0, TxKind::Call(Address::ZERO)), + sender: s, + seed: vec![eoa(s, BIG_BALANCE, 0)], + base_fee: 1000, + block_gas_limit: 30_000_000, + prague: true, + }); + + // 9) CallerGasLimitMoreThanBlock:tx.gas_limit > 区块 gas_limit。 + v.push(DiffCase { + name: "CallerGasLimitMoreThanBlock".into(), + tx: legacy_tx(0, 22_000, 1000, 0, TxKind::Call(Address::ZERO)), + sender: s, + seed: vec![eoa(s, BIG_BALANCE, 0)], + base_fee: 1000, + block_gas_limit: 21_000, // 区块上限低于交易 gas_limit + prague: true, + }); + + // 10) EmptyAuthorizationList:Prague 下 7702 交易空授权列表。 + v.push(DiffCase { + name: "EmptyAuthorizationList".into(), + tx: eip7702_tx(0, 100_000, 1, 0), + sender: s, + seed: vec![eoa(s, BIG_BALANCE, 0)], + base_fee: 1, + block_gas_limit: 30_000_000, + prague: true, + }); + + // 11) Eip7702NotSupported:pre-Prague(Shanghai)下的 7702 交易。 + v.push(DiffCase { + name: "Eip7702NotSupported".into(), + tx: eip7702_tx(0, 100_000, 1, 1), + sender: s, + seed: vec![eoa(s, BIG_BALANCE, 0)], + base_fee: 1, + block_gas_limit: 30_000_000, + prague: false, // Shanghai + }); + + // 12) RejectCallerWithCode:发送者带非委托代码(EIP-3607)。 + v.push(DiffCase { + name: "RejectCallerWithCode".into(), + tx: legacy_tx(0, 21_000, 1000, 0, TxKind::Call(Address::ZERO)), + sender: s, + seed: vec![coded_account(s, BIG_BALANCE, 0)], + base_fee: 1000, + block_gas_limit: 30_000_000, + prague: true, + }); + + v +} + +/// 生成一条「非 live」的说明行(documented unreachable / 不可造 / filter 整型拦截)。 +fn note_outcome(variant: &str, reason: &str) -> DiffOutcome { + DiffOutcome { + name: variant.to_string(), + filter_admit: false, + revm_reject: false, + revm_variant: Some(reason.to_string()), + is_gap: false, + } +} + +/// 说明行:本 pin 下 revm 无构造点、或无法由真实签名交易表达、或被 filter 按交易类型整体拦截。 +fn note_cases() -> Vec { + vec![ + // 无法由真实签名交易表达:alloy 的 typed 交易恒带 chain_id,`tx.chain_id()` 永不为 None, + // 故 revm 的 MissingChainId 从真实 TransactionSigned 不可达(filter + // 的对应门为防御性死代码)。 + note_outcome("MissingChainId", "not-craftable: typed signed txs always carry chain_id"), + // 文档已 pin 为 revm 本版本无构造点(unreachable,见 tx_filter.rs 模块头)。 + note_outcome( + "AccessListNotSupported", + "revm_unreachable: legacy env validation site removed", + ), + note_outcome( + "MaxFeePerBlobGasNotSupported", + "revm_unreachable: removed; 4844 类型被整体拒绝", + ), + note_outcome( + "BlobVersionedHashesNotSupported", + "revm_unreachable: removed; 4844 类型被整体拒绝", + ), + note_outcome( + "AuthorizationListNotSupported", + "revm_unreachable: 走 Eip7702NotSupported 路径", + ), + note_outcome( + "AuthorizationListInvalidFields", + "revm_unreachable: 移至执行期 7702 授权处理", + ), + note_outcome( + "NonceOverflowInTransaction", + "revm_unreachable: revm-handler 18.1.0 才重新引入", + ), + note_outcome( + "TxGasLimitGreaterThanCap", + "revm_unreachable: cap=u64::MAX(OSAKA 前不可触发)", + ), + note_outcome("Eip7873NotSupported", "revm_unreachable: OSAKA 激活"), + note_outcome("Eip7873MissingTarget", "revm_unreachable: OSAKA 激活"), + // 被 filter 按交易类型整体拦截(filter 拒绝所有 EIP-4844 交易,故这些变体 filter_admit + // 恒为假)。 + note_outcome("EmptyBlobs", "filter-wholesale: is_eip4844 门整体拒绝 type-3"), + note_outcome("TooManyBlobs", "filter-wholesale: is_eip4844 门整体拒绝 type-3"), + note_outcome("BlobVersionNotSupported", "filter-wholesale: is_eip4844 门整体拒绝 type-3"), + note_outcome( + "BlobGasPriceGreaterThanMax", + "filter-wholesale: is_eip4844 门整体拒绝 type-3", + ), + note_outcome("BlobCreateTransaction", "filter-wholesale: is_eip4844 门整体拒绝 type-3"), + note_outcome("Eip4844NotSupported", "filter-wholesale: is_eip4844 门整体拒绝 type-3"), + // 内联 gas 下限门:filter 同时校验 initial_gas 与 floor_gas,故与 CallGasCost 同源覆盖。 + note_outcome("GasFloorMoreThanGasLimit", "filter-covered: intrinsic floor_gas 门覆盖"), + // 只在 pre-Berlin / pre-London 才可触发;Gravity 恒在 Prague,全部启用,不可达。 + note_outcome("Eip2930NotSupported", "spec-gated: Gravity 恒 Prague,EIP-2930 已启用"), + note_outcome("Eip1559NotSupported", "spec-gated: Gravity 恒 Prague,EIP-1559 已启用"), + // 需要 balance >= 2^128 才能与 filter 的 saturating 超集产生分歧,物理不可造。 + note_outcome( + "OverflowPaymentInTransaction", + "not-craftable: 需 balance>=2^128;filter saturating 超集覆盖", + ), + ] +} + +/// 对外暴露可造用例列表(供 bin 遍历 gap 并导出复现文件)。 +pub fn craftable_cases_pub() -> Vec { + craftable_cases() +} + +/// 生成完整变体矩阵:所有可造用例(live 运行两侧预言机)+ 说明行。 +pub fn variant_matrix() -> Vec { + let mut out: Vec = craftable_cases().iter().map(run_case).collect(); + out.extend(note_cases()); + out +} + +// —————————————————————————————— 复现导出(gnode send 兼容) —————————————————————————————— // + +/// 为一个 gap(或任意用例)导出一个 gnode `send` 兼容的 tx.json 对象。 +/// +/// 字段从 `case.tx` 推导:`to` / `data` / `value` / `nonce` / `gas` / `type` / +/// `maxFeePerGas` 或 `gasPrice`,type-4 追加 `authorizationList`;`_note` 描述 revm +/// 变体与复现方式。 +pub fn emit_repro(case: &DiffCase, outcome: &DiffOutcome) -> serde_json::Value { + let tx = &case.tx; + let to = match tx.kind() { + TxKind::Call(addr) => serde_json::Value::String(format!("0x{}", hex::encode(addr))), + TxKind::Create => serde_json::Value::Null, + }; + let data = format!("0x{}", hex::encode(tx.input())); + let value = format!("0x{:x}", tx.value()); + let nonce = format!("0x{:x}", tx.nonce()); + let gas = format!("0x{:x}", tx.gas_limit()); + let ty = tx.ty(); + + let mut obj = serde_json::json!({ + "from": format!("0x{}", hex::encode(case.sender)), + "to": to, + "data": data, + "value": value, + "nonce": nonce, + "gas": gas, + "type": format!("0x{:x}", ty), + }); + let map = obj.as_object_mut().unwrap(); + + // 费用字段:typed(1559/7702)用 maxFeePerGas + maxPriorityFeePerGas;legacy 用 gasPrice。 + if ty == 0 { + map.insert( + "gasPrice".into(), + serde_json::Value::String(format!("0x{:x}", tx.max_fee_per_gas())), + ); + } else { + map.insert( + "maxFeePerGas".into(), + serde_json::Value::String(format!("0x{:x}", tx.max_fee_per_gas())), + ); + map.insert( + "maxPriorityFeePerGas".into(), + serde_json::Value::String(format!( + "0x{:x}", + tx.max_priority_fee_per_gas().unwrap_or_default() + )), + ); + } + + // type-4 授权列表。 + if let Some(auth_list) = tx.authorization_list() { + let auths: Vec = auth_list + .iter() + .map(|a| { + serde_json::json!({ + "chainId": format!("0x{:x}", a.chain_id()), + "address": format!("0x{}", hex::encode(a.address())), + "nonce": format!("0x{:x}", a.nonce()), + }) + }) + .collect(); + map.insert("authorizationList".into(), serde_json::Value::Array(auths)); + } + + map.insert( + "_note".into(), + serde_json::Value::String(format!( + "CHAIN-HALT GAP: filter ADMITS but revm REJECTS with {}. \ + filter_admit={}, revm_reject={}. 复现:将本对象存为 tx.json,用 `gnode send tx.json` \ + 提交到本地集群,观察执行器在 lib.rs:1323 panic 停链。", + outcome.revm_variant.as_deref().unwrap_or(""), + outcome.filter_admit, + outcome.revm_reject, + )), + ); + + obj +} + +// ============================================================================ +// Tier-2:多笔交易序列执行差分(execution-induced TOCTOU) +// ============================================================================ +// +// Tier-1(字段级、单笔)证明 filter 是 revm 交易级校验的字段超集,找到 0 gap。 +// 但已知的真链停机是一种**执行诱导的 TOCTOU**:filter 只按「区块起始状态 + 每笔 sender +// nonce +1 + 7702 授权 nonce +1」建模,无法看到执行期由 CREATE / 委托调用等造成的 nonce +// 跃迁。典型场景(tools/gnode/gnodelib/scenarios/halt_7702.py):区块 [tx0, tx1] 中 +// filter 对区块起始状态同时放行两笔,但执行 tx0 会把某账户 nonce 越过 filter 的「+1」模型, +// 使 tx1 在执行期命中 NonceTooLow → 执行器在 lib.rs:1323 panic 停链。 +// +// Tier-2 在同一进程内确定性复现:整块跑 filter 得到放行集合,再用一个**逐笔提交**的 revm +// evm 顺序执行放行的交易;任一放行交易在执行期返回 `EVMError::Transaction(InvalidTransaction)` +// 即为一个执行缺口(SeqGap)—— 这正是停链。仅 `InvalidTransaction` 计入;Revert/Halt(Ok) +// 不算(链能存活)。 + +/// Tier-2 序列用例:一个区块内的多笔交易 + 区块起始账户状态。 +#[derive(Debug, Clone)] +pub struct SeqCase { + /// 用例名。 + pub name: String, + /// 区块内交易(按区块顺序)。 + pub txs: Vec, + /// 各交易发送者(与 `txs` 一一对应)。 + pub senders: Vec
, + /// 区块起始账户状态(两侧共用)。 + pub seed: Vec, + /// 区块 base fee。 + pub base_fee: u64, + /// 区块 gas limit。 + pub block_gas_limit: u64, + /// `true` => Prague spec。 + pub prague: bool, +} + +/// 一个执行缺口:被 filter 放行、却在执行期被 revm 以 `InvalidTransaction` 拒绝的交易。 +#[derive(Debug, Clone)] +pub struct SeqGap { + /// 交易在区块内的下标。 + pub tx_index: usize, + /// 该交易发送者。 + pub sender: Address, + /// 执行期 `InvalidTransaction` 的 Debug 串。 + pub revm_variant: String, +} + +/// Tier-2 序列差分结果。 +#[derive(Debug, Clone)] +pub struct SeqOutcome { + /// 用例名。 + pub name: String, + /// filter 放行的交易下标集合(对整块 `txs` 跑 `filter_invalid_txs` 的补集)。 + pub filter_admitted: Vec, + /// 执行期发现的缺口(放行但执行期 `InvalidTransaction`)。 + pub exec_gaps: Vec, +} + +/// 运行一个 Tier-2 序列用例。 +/// +/// 1. 整块跑 `filter_invalid_txs` 得到 `filter_admitted`(不在 invalid 集合的下标)。 +/// 2. 建单个 revm evm(与 Tier-1 相同的 EthEvmConfig/Header/State 模式),对放行的交易 +/// **按顺序逐笔提交执行**(`transact_commit`,状态随之演进)。 +/// 3. 任一放行交易返回 `EVMError::Transaction(itx)` 即记为 `SeqGap`。 +pub fn run_seq_case(case: &SeqCase) -> SeqOutcome { + let chain_spec = if case.prague { prague_chain_spec() } else { shanghai_chain_spec() }; + + let mut cache: CacheDB = CacheDB::new(EmptyDB::default()); + for s in &case.seed { + cache.insert_account_info(s.addr, s.info.clone()); + } + + // —— filter 侧:整块一次性判定 —— // + let invalid = crate::tx_filter::filter_invalid_txs( + cache.clone(), + &case.txs, + &case.senders, + case.base_fee, + case.block_gas_limit, + chain_spec.as_ref(), + BLOCK_TIMESTAMP, + BLOCK_NUMBER, + ); + let filter_admitted: Vec = + (0..case.txs.len()).filter(|i| !invalid.contains(i)).collect(); + + // —— revm 侧:逐笔提交,状态演进 —— // + let evm_config = EthEvmConfig::new(chain_spec); + let header = Header { + gas_limit: case.block_gas_limit, + base_fee_per_gas: Some(case.base_fee), + excess_blob_gas: Some(0), + timestamp: BLOCK_TIMESTAMP, + number: BLOCK_NUMBER, + ..Default::default() + }; + let evm_env = evm_config.evm_env(&header).expect("evm_env"); + let state = State::builder().with_bundle_update().with_database(cache).build(); + let mut evm = evm_config.evm_with_env(state, evm_env); + + let mut exec_gaps = Vec::new(); + for &i in &filter_admitted { + let tx_env = Recovered::new_unchecked(case.txs[i].clone(), case.senders[i]).into_tx_env(); + // 用 transact_raw(取具体 `Self::Tx`,与 Tier-1 同路径、无 IntoTxEnv 歧义),成功后把 + // ResultAndState.state 提交回 evm 的 State,使区块内状态随每笔演进。 + // Success / Revert / Halt(Ok)都提交(链存活);只有 `EVMError::Transaction` 是执行器 + // 无法恢复的停链信号,记为 gap。 + match evm.transact_raw(tx_env) { + Ok(result_and_state) => { + evm.db_mut().commit(result_and_state.state); + } + Err(e) => { + if let Some(itx) = e.as_invalid_tx_err() { + exec_gaps.push(SeqGap { + tx_index: i, + sender: case.senders[i], + revm_variant: format!("{itx:?}"), + }); + } + // 非交易级错误(Database/Header)不计入 gap。 + } + } + } + + SeqOutcome { name: case.name.clone(), filter_admitted, exec_gaps } +} + +/// 已知的真链停机场景:**委托-CREATE nonce 竞态**(等价 `gnode attack 7702-halt`)。 +/// +/// - 合约 T:运行时字节码 `0x600060006000f05000`(PUSH1 0 ×3, CREATE, POP, STOP)。 +/// - 账户 A(EOA):其代码为 7702 委托指示符 `0xef0100 ++ T`,故调用 A 即在 A 的上下文里 执行 T +/// 的运行时,其中的 CREATE 会 **凸起 A 的 nonce**。 +/// - tx0:faucet F → A 的普通调用(足够 gas)。执行它 => 运行 A 的委托代码 => CREATE => A.nonce += +/// 1。 +/// - tx1:来自 A、nonce = N(A 的区块起始 nonce)的普通调用。filter 按区块起始 nonce=N 放行; 但 +/// tx0 执行后 A.nonce = N+1,故 tx1 执行期校验 => `NonceTooLow { tx: N, state: N+1 }`。 +pub fn seq_7702_create_nonce_race() -> SeqCase { + let f = Address::with_last_byte(0xFF); // faucet + let a = Address::with_last_byte(0xAA); // 被委托的 EOA + let t = Address::with_last_byte(0x77); // 携带 CREATE 运行时的合约 + + // T 运行时:CREATE(0,0,0); POP; STOP。 + let t_code = Bytecode::new_raw(Bytes::from_static(&[ + 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0xf0, 0x50, 0x00, + ])); + // A 代码:7702 委托指示符 0xef0100 || T(Bytecode::new_eip7702 生成规范格式)。 + let a_code = Bytecode::new_eip7702(t); + + let a_nonce: u64 = 1; // A 的区块起始 nonce N + let seed = vec![ + eoa(f, BIG_BALANCE, 0), + SeedAccount { + addr: a, + info: AccountInfo { + balance: U256::from(BIG_BALANCE), + nonce: a_nonce, + code_hash: a_code.hash_slow(), + code: Some(a_code), + }, + }, + SeedAccount { + addr: t, + info: AccountInfo { + balance: U256::ZERO, + nonce: 1, // 已部署合约 + code_hash: t_code.hash_slow(), + code: Some(t_code), + }, + }, + ]; + + // tx0:F(nonce 0) -> A 的普通调用,给足 gas 让委托 CREATE 执行。 + let tx0 = legacy_tx(0, 500_000, 1000, 0, TxKind::Call(a)); + // tx1:A(nonce N=1) -> 任意 sink 的普通调用;filter 按区块起始 nonce 放行。 + let tx1 = legacy_tx(a_nonce, 21_000, 1000, 0, TxKind::Call(Address::ZERO)); + + SeqCase { + name: "seq_7702_create_nonce_race".into(), + txs: vec![tx0, tx1], + senders: vec![f, a], + seed, + base_fee: 1000, + block_gas_limit: 30_000_000, + prague: true, + } +} + +/// 所有 Tier-2 序列用例。 +pub fn seq_cases() -> Vec { + vec![seq_7702_create_nonce_race()] +} + +/// 为一个含执行缺口的序列用例导出 gnode 可复现的说明工件。 +pub fn emit_seq_repro(case: &SeqCase, outcome: &SeqOutcome) -> serde_json::Value { + let gaps: Vec = outcome + .exec_gaps + .iter() + .map(|g| { + serde_json::json!({ + "tx_index": g.tx_index, + "sender": format!("0x{}", hex::encode(g.sender)), + "revm_variant": g.revm_variant, + }) + }) + .collect(); + + let txs: Vec = case + .txs + .iter() + .enumerate() + .map(|(i, tx)| { + let to = match tx.kind() { + TxKind::Call(addr) => serde_json::Value::String(format!("0x{}", hex::encode(addr))), + TxKind::Create => serde_json::Value::Null, + }; + serde_json::json!({ + "index": i, + "from": format!("0x{}", hex::encode(case.senders[i])), + "to": to, + "nonce": format!("0x{:x}", tx.nonce()), + "gas": format!("0x{:x}", tx.gas_limit()), + "type": format!("0x{:x}", tx.ty()), + }) + }) + .collect(); + + serde_json::json!({ + "name": case.name, + "kind": "tier2-sequence-execution-gap", + "filter_admitted": outcome.filter_admitted, + "exec_gaps": gaps, + "block": { + "base_fee": case.base_fee, + "gas_limit": case.block_gas_limit, + "prague": case.prague, + "txs": txs, + }, + "_note": format!( + "CHAIN-HALT (execution-induced TOCTOU): filter 放行交易下标 {:?},但顺序执行时下标 {:?} \ + 在执行期被 revm 以 InvalidTransaction 拒绝(见 exec_gaps)。这就是委托-CREATE nonce 竞态:\ + 执行 tx0(F->A)运行 A 的 7702 委托代码触发 CREATE,把 A 的 nonce 越过 filter 的 +1 模型,\ + tx1(A, 区块起始 nonce)执行期命中 NonceTooLow,执行器在 lib.rs:1323 panic 停链。\ + 复现:在本地集群上 `gnode attack 7702-halt`;或用 `gnode send --no-wait` 在同一区块内先后提交 \ + tx0、tx1(A 预装 0xef0100||T 委托、T 预装 CREATE 运行时)。", + outcome.filter_admitted, + outcome.exec_gaps.iter().map(|g| g.tx_index).collect::>(), + ), + }) +} + +#[cfg(all(test, feature = "difftx"))] +mod tests { + use super::*; + + /// Milestone 1:证明两侧预言机接线正确。 + /// GasPriceLessThanBasefee —— revm 拒绝、filter 也拒绝,非 gap。 + #[test] + fn wiring_gas_price_less_than_basefee() { + let s = sender(); + let case = DiffCase { + name: "GasPriceLessThanBasefee".into(), + tx: legacy_tx(0, 21_000, 100, 0, TxKind::Call(Address::ZERO)), + sender: s, + seed: vec![eoa(s, BIG_BALANCE, 0)], + base_fee: 1000, + block_gas_limit: 30_000_000, + prague: true, + }; + let out = run_case(&case); + println!("wiring outcome: {out:?}"); + assert!(out.revm_reject, "revm 应拒绝 GasPriceLessThanBasefee"); + assert!(!out.filter_admit, "filter 应拒绝(max_fee < base_fee)"); + assert!(!out.is_gap); + assert_eq!(out.revm_variant.as_deref(), Some("GasPriceLessThanBasefee")); + } + + /// Milestone 2:每个可造用例都必须真的让 revm 拒绝(否则 crafter 有问题)。 + #[test] + fn every_craftable_case_makes_revm_reject() { + for case in craftable_cases() { + let out = run_case(&case); + println!( + "{:<32} filter_admit={:<5} revm_reject={:<5} variant={:?}", + out.name, out.filter_admit, out.revm_reject, out.revm_variant + ); + assert!( + out.revm_reject, + "用例 {} 未能让 revm 拒绝(revm_variant={:?})—— crafter 需修正", + case.name, out.revm_variant + ); + } + } + + /// CI 门:矩阵中不允许任何 gap(filter 是 revm 可达变体的证明超集)。 + #[test] + fn matrix_has_no_gap() { + let matrix = variant_matrix(); + let gaps: Vec<&DiffOutcome> = matrix.iter().filter(|o| o.is_gap).collect(); + for o in &matrix { + println!( + "{:<32} admit={:<5} reject={:<5} gap={:<5} {}", + o.name, + o.filter_admit, + o.revm_reject, + o.is_gap, + o.revm_variant.as_deref().unwrap_or("") + ); + } + assert!( + gaps.is_empty(), + "发现停链缺口: {:?}", + gaps.iter().map(|g| &g.name).collect::>() + ); + } + + /// emit_repro 对每个 live 用例都能产出结构化 JSON(含 _note)。 + #[test] + fn emit_repro_shape() { + for case in craftable_cases() { + let out = run_case(&case); + let json = emit_repro(&case, &out); + assert!(json.get("_note").is_some()); + assert!(json.get("type").is_some()); + } + } + + /// 反-vacuity 门:证明 `matrix_has_no_gap` 的「零 gap」不是某一侧被硬编成 false 的假象。 + /// + /// 单笔字段级下 filter 是 revm 校验的完全超集,本 pin 下不存在真实 gap 反例可作 canary, + /// 因此用「两半可达性」证明 `is_gap = filter_admit && revm_reject` 是两个都能独立取到 + /// `true` 的合取——一旦二者在同一 case 同时为真即会被判为 gap: + /// - (a) 合法交易 ⇒ filter 放行(`filter_admit == true`)且 revm 接受(`revm_reject == + /// false`); + /// - (b) 至少一个可造用例 ⇒ revm 拒绝(`revm_reject == true`)。 + /// 若 `run_case` 把 `filter_admit` 恒置 false(例如接线错误),(a) 会立即失败。 + #[test] + fn no_gap_gate_is_non_vacuous() { + let s = sender(); + // (a) 一笔完全合法的 EIP-1559 转账:nonce 对齐、max_fee ≥ base_fee ≥ prio、余额充足、 + // 21000 intrinsic 足够、目标为空码地址。两侧都应放行/接受。 + let happy = DiffCase { + name: "happy_valid_1559".into(), + tx: eip1559_tx(MAINNET_CHAIN_ID, 0, 21_000, 2000, 100), + sender: s, + seed: vec![eoa(s, BIG_BALANCE, 0)], + base_fee: 1000, + block_gas_limit: 30_000_000, + prague: true, + }; + let ho = run_case(&happy); + println!("happy outcome: {ho:?}"); + assert!(ho.filter_admit, "filter 应放行合法交易(证明 filter_admit 可达 true)"); + assert!(!ho.revm_reject, "revm 应接受合法交易,variant={:?}", ho.revm_variant); + assert!(!ho.is_gap); + + // (b) 可造用例证明 revm_reject 可达 true(与 (a) 的 admit=true 构成非平凡合取)。 + let rejected = run_case(&craftable_cases()[0]); + assert!(rejected.revm_reject, "可造用例应让 revm 拒绝,证明 revm_reject 可达 true"); + } + + /// Tier-2:已知真链停机的委托-CREATE nonce 竞态。 + /// + /// 期望 filter 放行 tx1(下标 1),而顺序执行时 tx1 在执行期命中 NonceTooLow —— 即预言机 + /// 捕获到这个执行诱导的停链缺口。若 filter 实际已拒绝 tx1(exec_gaps 为空),则说明该路径 + /// 已被 filter 关闭;此时**不伪造通过**,而是断言经验事实并在断言信息里打印真实结果。 + #[test] + fn seq_7702_create_nonce_race_is_a_gap() { + let case = seq_7702_create_nonce_race(); + let out = run_seq_case(&case); + println!( + "SEQ outcome: name={} filter_admitted={:?} exec_gaps={:?}", + out.name, out.filter_admitted, out.exec_gaps + ); + + // 记录经验事实:ground truth 需两种情形都能被如实报告。 + let tx1_admitted = out.filter_admitted.contains(&1); + let nonce_gap = out + .exec_gaps + .iter() + .find(|g| g.tx_index == 1 && g.revm_variant.contains("NonceTooLow")); + + assert!( + tx1_admitted, + "经验事实:filter 未放行 tx1(filter_admitted={:?})—— 该路径已被 filter 关闭,不是开放 gap", + out.filter_admitted + ); + assert!( + nonce_gap.is_some(), + "经验事实:filter 放行了 tx1 但执行期未产生 NonceTooLow(exec_gaps={:?})—— \ + 需人工确认是否已闭合/或需修正复现", + out.exec_gaps + ); + println!("Tier-2 verdict: 已复现已知停链缺口 -> {:?}", nonce_gap.unwrap()); + } + + /// emit_seq_repro 对含 gap 的序列用例产出结构化工件(含 _note 与 exec_gaps)。 + #[test] + fn emit_seq_repro_shape() { + let case = seq_7702_create_nonce_race(); + let out = run_seq_case(&case); + let json = emit_seq_repro(&case, &out); + assert!(json.get("_note").is_some()); + assert!(json.get("exec_gaps").is_some()); + assert_eq!(json.get("kind").and_then(|v| v.as_str()), Some("tier2-sequence-execution-gap")); + } +} diff --git a/crates/pipe-exec-layer-ext-v2/execute/src/lib.rs b/crates/pipe-exec-layer-ext-v2/execute/src/lib.rs index 260777c69..246903a89 100644 --- a/crates/pipe-exec-layer-ext-v2/execute/src/lib.rs +++ b/crates/pipe-exec-layer-ext-v2/execute/src/lib.rs @@ -8,6 +8,11 @@ pub mod onchain_config; pub mod randomness_precompile; mod system_caller_migration; mod tx_filter; + +/// 差分测试预言机(differential oracle):机械化验证 `tx_filter ⊇ revm 交易级校验`。 +/// 仅在 `difftx` feature 下编译,不进入生产构建面。 +#[cfg(feature = "difftx")] +pub mod difftx; use alloy_sol_types::SolEvent; use channel::Channel; From ddf4f416fe9380715781109655f485c7949fd1a1 Mon Sep 17 00:00:00 2001 From: keanji-x Date: Wed, 22 Jul 2026 14:24:17 +0800 Subject: [PATCH 2/4] =?UTF-8?q?test(pipe-exec):=20widen=20difftx=20?= =?UTF-8?q?=E2=80=94=20serial=E2=9F=B7grevm=20=E6=89=A7=E8=A1=8C=E5=B7=AE?= =?UTF-8?q?=E5=88=86=20+=20=E4=BA=A4=E6=98=93=E5=BA=8F=E5=88=97=20fuzz?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在 difftx 差分预言机上扩两根杠杆,把「攻击面」从单一不变式 + 手搓输入 放大到「多预言机 + 量产生成」: - difftx_exec(新预言机类,feature difftx_exec):serial(revm 顺序执行, WrapExecutor) ⟷ grevm(并行执行,GrevmExecutor) 执行差分。同一批交易、 逐字节相同的种子状态分别过两个后端,diff BundleState(state/contracts/ state_size)+ receipts。任何背离 = 共识 state-fork(节点对状态分歧 → 链 分裂),一类静态分析看不出、只有真跑才现形的致命 bug。 实测:5 baseline + 4 对抗块(RAW/WAW 同槽、同发送者 nonce 链、同收款人 余额 WAW、nonce+storage 混合)全部 serial==grevm,grevm 正确串行化了依赖, 未发现 fork。附带发现并 pin 一处良性非共识差异:raw reverts 的块内顺序 + 尾部空块不同(reverts 不入 state root),故按 Address→AccountRevert 语义 比对(仍抓内容回归)。 - difftx_fuzz(仅测试期,proptest):把 difftx 的 12 个手搓单笔 + 1 条序列 升级成对整个字段空间的 property 搜索——单笔(类型/nonce/gas/fee/value/ initcode 跨 EIP-3860 边界/chain_id/access-list)、多笔序列、7702 授权列表 (alloy_signer_local 真实签名,受控 authority;auth 数/chain_id/nonce match+1+gap+MAX/自赞助/重复 authority)。 实测:各 64 例 + 非空门,filter⊇revm 不变式全程成立,未发现新 gap —— 证明该超集在搜索空间内是完整的(不止 12 个手搓点)。 仅新增测试/差分入口(feature 门控),不改任何生产逻辑。 跑法:cargo test -p reth-pipe-exec-layer-ext-v2 --features difftx_exec --lib difftx (14 tests);cargo run --features difftx_exec --bin difftx_exec(exit 3=有背离)。 Co-Authored-By: Claude Opus 4.8 (1M context) --- Cargo.lock | 1 + .../pipe-exec-layer-ext-v2/execute/Cargo.toml | 10 + .../execute/src/bin/difftx_exec.rs | 145 ++++ .../execute/src/difftx_exec.rs | 820 ++++++++++++++++++ .../execute/src/difftx_fuzz.rs | 626 +++++++++++++ .../pipe-exec-layer-ext-v2/execute/src/lib.rs | 11 + 6 files changed, 1613 insertions(+) create mode 100644 crates/pipe-exec-layer-ext-v2/execute/src/bin/difftx_exec.rs create mode 100644 crates/pipe-exec-layer-ext-v2/execute/src/difftx_exec.rs create mode 100644 crates/pipe-exec-layer-ext-v2/execute/src/difftx_fuzz.rs diff --git a/Cargo.lock b/Cargo.lock index 350f1b9a8..54ab16eaa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9990,6 +9990,7 @@ dependencies = [ "metrics", "once_cell", "parking_lot", + "proptest", "rand 0.9.2", "rayon", "reth-chain-state", diff --git a/crates/pipe-exec-layer-ext-v2/execute/Cargo.toml b/crates/pipe-exec-layer-ext-v2/execute/Cargo.toml index 485e4e4df..f02ea5aa8 100644 --- a/crates/pipe-exec-layer-ext-v2/execute/Cargo.toml +++ b/crates/pipe-exec-layer-ext-v2/execute/Cargo.toml @@ -84,12 +84,16 @@ alloy-genesis.workspace = true alloy-rpc-types-trace.workspace = true reth-trie = { workspace = true, features = ["test-utils"] } ureq = { version = "2", features = ["json"] } +# difftx_fuzz:交易序列 property/fuzz 生成器(仅测试期)。 +proptest = "1" [features] pipe_test = ["reth-provider/pipe_test"] config-from-env = ["gravity-primitives/config-from-env"] # 差分预言机(tx_filter ⊇ revm):编译 difftx 模块与 difftx 工具二进制。 difftx = [] +# serial ⟷ grevm 执行差分:编译 difftx_exec 模块与 difftx_exec 工具二进制。 +difftx_exec = ["difftx"] [[bench]] name = "bls_pop_verify_bench" @@ -100,3 +104,9 @@ harness = false name = "difftx" path = "src/bin/difftx.rs" required-features = ["difftx"] + +# serial(revm) ⟷ grevm 执行差分预言机命令行工具:跑 baseline+adversarial 块,对比双后端。 +[[bin]] +name = "difftx_exec" +path = "src/bin/difftx_exec.rs" +required-features = ["difftx_exec"] diff --git a/crates/pipe-exec-layer-ext-v2/execute/src/bin/difftx_exec.rs b/crates/pipe-exec-layer-ext-v2/execute/src/bin/difftx_exec.rs new file mode 100644 index 000000000..f589f4a0c --- /dev/null +++ b/crates/pipe-exec-layer-ext-v2/execute/src/bin/difftx_exec.rs @@ -0,0 +1,145 @@ +//! serial(revm) ⟷ grevm 执行差分预言机 —— 命令行工具。 +//! +//! 跑所有 baseline + adversarial 用例,逐块过**两个后端**(串行 `WrapExecutor` / +//! 并行 `GrevmExecutor`),打印一张对比表;对**任何背离**写一份可复现 artifact 到 +//! `difftx-repro/exec_.json`(含区块交易 + 如何在 live 集群复现的指引), +//! 并以退出码 3 收场;无背离则退出 0。 +//! +//! ```text +//! cargo run -p reth-pipe-exec-layer-ext-v2 --features difftx_exec --bin difftx_exec +//! ``` + +use reth_pipe_exec_layer_ext_v2::difftx_exec::{ + exec_adversarial_cases, exec_baseline_cases, run_exec_diff, ExecCase, ExecDiff, +}; +use std::io::Write; + +fn main() { + let mut all: Vec<(String, ExecCase, ExecDiff)> = Vec::new(); + + for case in exec_baseline_cases() { + let d = run_exec_diff(&case); + all.push(("baseline".to_string(), case, d)); + } + for case in exec_adversarial_cases() { + let d = run_exec_diff(&case); + all.push(("adversarial".to_string(), case, d)); + } + + // —— 对比表 —— // + println!("\n serial(revm) ⟷ grevm 执行差分预言机\n"); + println!( + "{:<12} {:<40} {:>9} {:>8} {:>9} divergence", + "kind", "case", "serial_ok", "grevm_ok", "diverged" + ); + println!("{}", "-".repeat(110)); + + let mut diverged_count = 0usize; + for (kind, _case, d) in &all { + println!( + "{:<12} {:<40} {:>9} {:>8} {:>9} {}", + kind, + d.name, + d.serial_ok, + d.grevm_ok, + d.diverged, + d.divergence.as_deref().unwrap_or("-") + ); + if d.diverged { + diverged_count += 1; + } + } + + // —— 对背离写可复现 artifact —— // + if diverged_count > 0 { + let dir = std::path::Path::new("difftx-repro"); + if let Err(e) = std::fs::create_dir_all(dir) { + eprintln!("无法创建 difftx-repro 目录:{e}"); + } else { + for (kind, case, d) in &all { + if !d.diverged { + continue; + } + let path = dir.join(format!("exec_{}.json", d.name)); + match write_repro(&path, kind, case, d) { + Ok(()) => println!(" ↳ 已写复现 artifact:{}", path.display()), + Err(e) => eprintln!(" ↳ 写 {} 失败:{e}", path.display()), + } + } + } + } + + println!("\n汇总:{} 个用例,{} 个背离。", all.len(), diverged_count); + if diverged_count > 0 { + println!("判决:发现 serial⟷grevm 背离 —— 疑似共识 state-fork。退出码 3。"); + std::process::exit(3); + } else { + println!("判决:所有用例 serial==grevm,未发现背离。退出码 0。"); + } +} + +/// 写一份 JSON 复现 artifact:用例元信息 + 逐笔交易(RLP hex)+ live 集群复现指引。 +fn write_repro( + path: &std::path::Path, + kind: &str, + case: &ExecCase, + d: &ExecDiff, +) -> std::io::Result<()> { + use alloy_eips::eip2718::Encodable2718; + + let txs: Vec = case + .txs + .iter() + .zip(case.senders.iter()) + .map(|(tx, sender)| { + let mut raw = Vec::new(); + tx.encode_2718(&mut raw); + serde_json::json!({ + "sender": format!("{sender}"), + "type": tx.tx_type() as u8, + "raw_2718_hex": format!("0x{}", hex::encode(&raw)), + }) + }) + .collect(); + + let seed: Vec = case + .seed + .iter() + .map(|s| { + serde_json::json!({ + "addr": format!("{}", s.addr), + "balance": format!("{}", s.info.balance), + "nonce": s.info.nonce, + "has_code": s.info.code.is_some(), + }) + }) + .collect(); + + let doc = serde_json::json!({ + "name": d.name, + "kind": kind, + "serial_ok": d.serial_ok, + "grevm_ok": d.grevm_ok, + "diverged": d.diverged, + "divergence": d.divergence, + "base_fee": case.base_fee, + "block_gas_limit": case.block_gas_limit, + "seed": seed, + "txs": txs, + "reproduce_on_live_cluster": { + "note": "两侧后端从同一份 seed 状态 + 同一区块(Prague@ts=0, number=1)执行;\ + 背离即共识 state-fork。用 gnode 把这批交易灌进一个块复现:", + "steps": [ + "1) 用 seed 预置账户余额/nonce/code(genesis alloc 或直接状态注入)", + "2) 按 txs[].raw_2718_hex 顺序提交:gnode send --no-wait (逐笔,保持块内顺序)", + "3) gnode difftx-exec 对该块跑 serial vs grevm 并比对 state root / BundleState / receipts", + "4) 若 state root 背离 → 确认为链停/分叉级 finding" + ] + } + }); + + let mut f = std::fs::File::create(path)?; + f.write_all(serde_json::to_string_pretty(&doc)?.as_bytes())?; + f.write_all(b"\n")?; + Ok(()) +} diff --git a/crates/pipe-exec-layer-ext-v2/execute/src/difftx_exec.rs b/crates/pipe-exec-layer-ext-v2/execute/src/difftx_exec.rs new file mode 100644 index 000000000..af74b7990 --- /dev/null +++ b/crates/pipe-exec-layer-ext-v2/execute/src/difftx_exec.rs @@ -0,0 +1,820 @@ +//! serial(revm) ⟷ grevm 执行差分预言机(differential oracle)。 +//! +//! ## 不变式(invariant) +//! +//! **串行执行(`WrapExecutor` → `BasicBlockExecutor`,即 revm 顺序执行) +//! == grevm 并行执行(`GrevmExecutor`)**,对任意区块**逐字节**一致。 +//! grevm 是 Galxe 自研的并行执行器;两侧在 state root / `BundleState` / receipts +//! 上的任何背离 = 一次**共识 state-fork**(节点对状态产生分歧 → 链分裂)。 +//! 这类 bug 静态分析看不出——只有把两个后端真跑起来对比才现形。 +//! +//! ## 停链预言机的推广(generalized halt oracle) +//! +//! 不止 `InvalidTransaction`:**任意**执行器级失败(一个后端命中而另一个没命中), +//! 或任一后端 panic,都是一个 finding。这里以「一侧 `Ok`、另一侧 `Err`」记为 +//! `diverged=true`(并记录错误串)。 +//! +//! ## 构造法(参照 `system_caller_migration.rs`) +//! +//! - 串行侧 `WrapExecutor::new(BasicBlockExecutor::new(evm_config, db))` (见 `fresh_executor`)。 +//! - grevm 侧 `GrevmExecutor::new(chain_spec, &evm_config, db)` (见 `fresh_grevm_executor`)。 +//! - 两侧从**同一份** `CacheDB`(clone)seed,保证前置状态逐字节一致。 +//! - `BundleState` 字段选取与迁移测试一致:比对 `state` / `contracts` / `state_size` / +//! `reverts`,**跳过** `reverts_size`(grevm 不更新它,非共识相关)。 +//! +//! ## 使用 +//! +//! ```text +//! cargo test -p reth-pipe-exec-layer-ext-v2 --features difftx_exec --lib difftx_exec -- --nocapture +//! cargo run -p reth-pipe-exec-layer-ext-v2 --features difftx_exec --bin difftx_exec +//! ``` + +use alloy_consensus::{constants::KECCAK_EMPTY, Header, TxEip1559, TxEip7702, TxLegacy}; +use alloy_eips::{ + eip4895::Withdrawals, + eip7685::EMPTY_REQUESTS_HASH, + eip7702::{Authorization, SignedAuthorization}, + merge::BEACON_NONCE, +}; +use alloy_primitives::{Address, Bytes, Signature, TxKind, B256, U256}; +use reth_chainspec::{ChainSpec, ChainSpecBuilder, MAINNET}; +use reth_ethereum_primitives::{Block, BlockBody, Receipt, Transaction, TransactionSigned}; +use reth_evm::{execute::BasicBlockExecutor, parallel_execute::ParallelExecutor}; +use reth_evm_ethereum::{parallel_execute::GrevmExecutor, EthEvmConfig}; +use reth_primitives_traits::RecoveredBlock; +use revm::{ + database::{CacheDB, EmptyDB}, + state::{AccountInfo, Bytecode}, +}; +use std::sync::Arc; + +// 复用 difftx.rs 的 `SeedAccount`(避免两套种子类型)。 +use crate::difftx::SeedAccount; + +// ————————————————————————————————— 常量 ————————————————————————————————— // + +/// 主网 chain id —— 所有 typed 交易的 `chain_id` 都取此值。 +const MAINNET_CHAIN_ID: u64 = 1; + +/// 两侧共用的区块时间戳。**取 0**:`prague_activated()` 让 Prague 从创世(ts=0)激活, +/// `evm_env` 解析出 `SpecId::PRAGUE`(与 difftx.rs 的取值理由一致)。 +const BLOCK_TIMESTAMP: u64 = 0; + +/// 区块高度。**必须 ≥ 1**(非创世):EIP-4788 在创世块要求 `parent_beacon_block_root == 0` +/// 且不发系统交易;取 1 让 4788/2935 走正常 system-call 路径(两侧走同一 `SystemCaller`, +/// 若在此处背离本身就是一个 finding)。 +const BLOCK_NUMBER: u64 = 1; + +/// 充裕余额(100 ETH)——保证「余额不是被测因素」。 +const HUGE_BALANCE: u128 = 100_000_000_000_000_000_000; + +// ————————————————————————————————— 数据结构 ————————————————————————————————— // + +/// 一个执行差分用例:一批交易 + 一份种子状态 + 区块参数。 +#[derive(Debug, Clone)] +pub struct ExecCase { + /// 用例名。 + pub name: String, + /// 区块内的交易序列(顺序即区块内排序)。 + pub txs: Vec, + /// 每笔交易的发送者(与 `txs` 等长、下标对齐;两侧共用)。 + pub senders: Vec
, + /// 种子账户;两侧从同一份列表 seed,保证前置状态逐字节一致。 + pub seed: Vec, + /// 区块 base fee(喂给 `Header::base_fee_per_gas`)。 + pub base_fee: u64, + /// 区块 gas limit(喂给 `Header::gas_limit`)。 + pub block_gas_limit: u64, +} + +/// 一个执行差分结果。 +#[derive(Debug, Clone)] +pub struct ExecDiff { + /// 用例名。 + pub name: String, + /// 串行后端是否无硬错误地执行了整块。 + pub serial_ok: bool, + /// grevm 后端是否无硬错误地执行了整块。 + pub grevm_ok: bool, + /// 是否背离:`state` / `contracts` / `state_size` / `reverts` / `receipts` / state root + /// 任一不一致,或一侧 `Ok` 一侧 `Err`。 + pub diverged: bool, + /// 首个具体差异的人类可读描述(`None` = 完全一致)。 + pub divergence: Option, +} + +// ————————————————————————————————— chainspec ————————————————————————————————— // + +/// Prague 从创世激活的 chainspec(与 difftx.rs 的 `prague_chain_spec` 同构)。 +fn prague_chain_spec() -> Arc { + Arc::new(ChainSpecBuilder::from(&*MAINNET).prague_activated().build()) +} + +// ————————————————————————————————— 后端构造 ————————————————————————————————— // + +/// 由一份 seed 构造 `CacheDB`(两侧 clone 同一份,保证前置状态逐字节一致)。 +fn seeded_cache(seed: &[SeedAccount]) -> CacheDB { + let mut cache: CacheDB = CacheDB::new(EmptyDB::default()); + for s in seed { + cache.insert_account_info(s.addr, s.info.clone()); + } + cache +} + +/// 串行后端类型:镜像 `system_caller_migration.rs::fresh_executor`。 +type SerialExecutor = reth_evm::parallel_execute::WrapExecutor< + CacheDB, + BasicBlockExecutor>, +>; + +fn fresh_serial_executor(chain_spec: Arc, db: CacheDB) -> SerialExecutor { + let evm_config = EthEvmConfig::new(chain_spec); + reth_evm::parallel_execute::WrapExecutor::new(BasicBlockExecutor::new(evm_config, db)) +} + +/// grevm 后端(镜像 `system_caller_migration.rs::fresh_grevm_executor`)。 +fn fresh_grevm_executor( + chain_spec: Arc, + db: CacheDB, +) -> GrevmExecutor, EthEvmConfig, ChainSpec> { + let evm_config = EthEvmConfig::new(chain_spec.clone()); + GrevmExecutor::new(chain_spec, &evm_config, db) +} + +// ————————————————————————————————— 区块构造 ————————————————————————————————— // + +/// 由 `case.txs` + `case.senders` 构造一个 Prague-shape 的 `RecoveredBlock`。 +/// +/// Header 字段镜像 `lib.rs::create_block_for_executor` 的 Prague 分支: +/// `timestamp=0`、`number=1`、`base_fee_per_gas`、`gas_limit`、`mix_hash=ZERO`(prevrandao)、 +/// `excess_blob_gas=Some(0)`、`blob_gas_used=Some(0)`、`parent_beacon_block_root=Some(ZERO)` +/// (EIP-4788)、`requests_hash=Some(EMPTY_REQUESTS_HASH)`(EIP-7685)、 +/// `withdrawals_root=Some(EMPTY_WITHDRAWALS)`(Shanghai)。 +fn build_block(case: &ExecCase) -> RecoveredBlock { + assert_eq!(case.txs.len(), case.senders.len(), "txs 与 senders 必须等长"); + + let header = Header { + parent_hash: B256::ZERO, + beneficiary: Address::ZERO, + number: BLOCK_NUMBER, + timestamp: BLOCK_TIMESTAMP, + gas_limit: case.block_gas_limit, + base_fee_per_gas: Some(case.base_fee), + mix_hash: B256::ZERO, // prevrandao(MERGE+ 需要,默认 ZERO 即可) + nonce: BEACON_NONCE.into(), + ommers_hash: alloy_consensus::constants::EMPTY_OMMER_ROOT_HASH, + // Shanghai:空提款 + withdrawals_root: Some(alloy_consensus::constants::EMPTY_WITHDRAWALS), + // Cancun:blob 字段 + excess_blob_gas: Some(0), + blob_gas_used: Some(0), + parent_beacon_block_root: Some(B256::ZERO), + // Prague:EIP-7685 requests + requests_hash: Some(EMPTY_REQUESTS_HASH), + ..Default::default() + }; + + let body = BlockBody { + transactions: case.txs.clone(), + ommers: Vec::new(), + withdrawals: Some(Withdrawals::default()), + }; + + // 两侧执行器都用调用方提供的 senders(不重新做签名恢复),故 `test_signature` + // 的交易配任意 sender 均可(与 lib.rs 的 `RecoveredBlock::new_unhashed` 路径一致)。 + RecoveredBlock::new_unhashed(Block { header, body }, case.senders.clone()) +} + +// ————————————————————————————————— 差分核心 ————————————————————————————————— // + +/// 比对两侧 `BundleState`(字段选取与 `system_caller_migration.rs` 的 U-6b/U-6c 一致: +/// `state` / `contracts` / `state_size` / `reverts`;**跳过** `reverts_size`)。返回首个差异描述。 +fn diff_bundle( + serial: &revm::database::BundleState, + grevm: &revm::database::BundleState, +) -> Option { + if serial.state != grevm.state { + // 定位首个不一致的账户,给出更精确的定位。 + for (addr, s_acc) in &serial.state { + match grevm.state.get(addr) { + Some(g_acc) if g_acc == s_acc => {} + Some(_) => { + return Some(format!( + "BundleState.state 账户 {addr} 背离(余额/nonce/code/storage 之一不同)" + )) + } + None => return Some(format!("BundleState.state:grevm 缺少账户 {addr}")), + } + } + for addr in grevm.state.keys() { + if !serial.state.contains_key(addr) { + return Some(format!("BundleState.state:serial 缺少账户 {addr}")); + } + } + return Some("BundleState.state 背离(顺序/内容差异)".to_string()); + } + if serial.contracts != grevm.contracts { + return Some("BundleState.contracts 背离(部署的合约字节码集合不同)".to_string()); + } + if serial.state_size != grevm.state_size { + return Some(format!( + "BundleState.state_size 背离:serial={} grevm={}", + serial.state_size, grevm.state_size + )); + } + // reverts 的**语义**比对(order-insensitive,忽略空 revert-block)。 + // + // 经验事实(见模块尾 `notes` 测试):即便 post-state(state/contracts/state_size) + // 与 receipts 逐字节一致——即 state root 必然相同、无共识 fork——两后端的 `reverts` + // 原始结构仍会因两处**纯簿记差异**而不等: + // (a) 串行 `BasicBlockExecutor` 会多出一个**空的**尾部 revert-block(outer len=2), + // grevm 只有一个(outer len=1);空 block 无任何 revert 条目。 + // (b) 同一 block 内账户的**排列顺序**不同(串行按执行/触碰顺序,grevm 按其收集顺序), + // 但账户集合、`previous_status`、storage revert 内容完全一致。 + // reverts 是**回滚/unwind 元数据**,从不参与 state root 的哈希,故 (a)(b) 均非共识相关。 + // 因此这里把所有非空 block 的 `(Address → AccountRevert)` 拍平成有序 map 后比对: + // 既容忍良性的顺序/空块差异,又能抓住**真正**的内容背离(某账户回滚到了不同的 + // previous 值 / 缺失账户 —— 那意味着两后端看到了不同的中间态)。 + if let Some(d) = diff_reverts_semantic(serial, grevm) { + return Some(d); + } + None +} + +/// 把 `BundleState.reverts` 拍平成 `BTreeMap>`(跨所有 outer +/// block 合并、丢弃空 block),返回顺序无关的语义视图。 +fn flatten_reverts( + bs: &revm::database::BundleState, +) -> std::collections::BTreeMap> { + let mut m: std::collections::BTreeMap< + Address, + Vec, + > = std::collections::BTreeMap::new(); + for blk in bs.reverts.iter() { + for (addr, revert) in blk { + m.entry(*addr).or_default().push(revert.clone()); + } + } + m +} + +/// 顺序无关地比对两侧 reverts 的语义内容。返回首个内容差异描述(`None` = 语义一致)。 +fn diff_reverts_semantic( + serial: &revm::database::BundleState, + grevm: &revm::database::BundleState, +) -> Option { + let s = flatten_reverts(serial); + let g = flatten_reverts(grevm); + if s == g { + return None; + } + for (addr, s_rev) in &s { + match g.get(addr) { + Some(g_rev) if g_rev == s_rev => {} + Some(_) => { + return Some(format!( + "reverts 内容背离:账户 {addr} 的 AccountRevert 不同(回滚到了不同的 previous 值)" + )) + } + None => return Some(format!("reverts 内容背离:grevm 缺少账户 {addr} 的回滚项")), + } + } + for addr in g.keys() { + if !s.contains_key(addr) { + return Some(format!("reverts 内容背离:serial 缺少账户 {addr} 的回滚项")); + } + } + Some("reverts 语义背离(内容差异)".to_string()) +} + +/// 比对两侧 receipts(逐笔 `Receipt` 相等)。返回首个差异描述。 +fn diff_receipts(serial: &[Receipt], grevm: &[Receipt]) -> Option { + if serial.len() != grevm.len() { + return Some(format!("receipts 数量背离:serial={} grevm={}", serial.len(), grevm.len())); + } + for (i, (s, g)) in serial.iter().zip(grevm.iter()).enumerate() { + if s != g { + return Some(format!( + "receipt[{i}] 背离:serial={{success={}, cumulative_gas={}, logs={}}} \ + grevm={{success={}, cumulative_gas={}, logs={}}}", + s.success, + s.cumulative_gas_used, + s.logs.len(), + g.success, + g.cumulative_gas_used, + g.logs.len() + )); + } + } + None +} + +/// 运行单个执行差分用例: +/// 1. 由 `case.seed` 构造两份**逐字节相同**的 `CacheDB`(clone)。 +/// 2. 构造 Prague-shape 区块。 +/// 3. 分别过串行 `WrapExecutor` 与 grevm `GrevmExecutor` 的 `.execute(&block)`。 +/// 4. 比对 `Ok/Err`、`BundleState`、receipts;填充 `diverged` + `divergence`。 +/// +/// 注:**state root** 是 post-state 的确定性像(pre-image 即 `BundleState.state`), +/// 故 `BundleState.state` 逐字节相等 ⟹ state root 相等;此处不单独计算 trie root +/// (需 provider/trie 依赖),由 `BundleState.state` 比对**传递性覆盖**。 +pub fn run_exec_diff(case: &ExecCase) -> ExecDiff { + let chain_spec = prague_chain_spec(); + let block = build_block(case); + + // 两份逐字节相同的前置状态。 + let cache = seeded_cache(&case.seed); + let mut serial = fresh_serial_executor(chain_spec.clone(), cache.clone()); + let mut grevm = fresh_grevm_executor(chain_spec, cache); + + let serial_res = serial.execute(&block); + let grevm_res = grevm.execute(&block); + + let serial_ok = serial_res.is_ok(); + let grevm_ok = grevm_res.is_ok(); + + // 组装结果的便捷闭包。 + let mk = |diverged: bool, divergence: Option| ExecDiff { + name: case.name.clone(), + serial_ok, + grevm_ok, + diverged, + divergence, + }; + + // —— 推广的停链预言机:一侧 Ok 一侧 Err(或错误串不同)即背离。 —— + match (&serial_res, &grevm_res) { + (Ok(so), Ok(go)) => { + // 两侧都成功 → 逐字段比对 BundleState + receipts + gas。 + if let Some(d) = diff_bundle(&so.state, &go.state) { + return mk(true, Some(d)); + } + if let Some(d) = diff_receipts(&so.result.receipts, &go.result.receipts) { + return mk(true, Some(d)); + } + if so.result.gas_used != go.result.gas_used { + return mk( + true, + Some(format!( + "block gas_used 背离:serial={} grevm={}", + so.result.gas_used, go.result.gas_used + )), + ); + } + mk(false, None) + } + (Ok(_), Err(e)) => mk( + true, + Some(format!("停链预言机命中:串行成功而 grevm 失败(halt gap)——grevm error: {e:?}")), + ), + (Err(e), Ok(_)) => mk( + true, + Some(format!("停链预言机命中:grevm 成功而串行失败(halt gap)——serial error: {e:?}")), + ), + (Err(se), Err(ge)) => { + // 两侧都失败:若错误串不同也算背离(不同后端对同一块给出不同失败模式)。 + let ss = format!("{se:?}"); + let gs = format!("{ge:?}"); + if ss == gs { + // 同样的错误 —— 非背离(两后端一致地拒绝了这个块)。 + mk(false, None) + } else { + mk(true, Some(format!("两侧均失败但错误模式不同:serial={ss} grevm={gs}"))) + } + } + } +} + +// ————————————————————————————————— 交易/账户 构造辅助 ————————————————————————————————— // + +/// EOA 种子:空代码。 +fn eoa(addr: Address, balance: u128, nonce: u64) -> SeedAccount { + SeedAccount { + addr, + info: AccountInfo { + balance: U256::from(balance), + nonce, + code_hash: KECCAK_EMPTY, + code: None, + }, + } +} + +/// 带真实字节码的合约账户种子。 +fn contract(addr: Address, code_bytes: &'static [u8], balance: u128) -> SeedAccount { + let code = Bytecode::new_raw(Bytes::from_static(code_bytes)); + SeedAccount { + addr, + info: AccountInfo { + balance: U256::from(balance), + nonce: 1, + code_hash: code.hash_slow(), + code: Some(code), + }, + } +} + +/// EIP-7702 委托账户种子:code = `0xef0100 || delegate`(委托指示符)。 +fn delegated_account(addr: Address, delegate: Address, balance: u128, nonce: u64) -> SeedAccount { + let code = Bytecode::new_eip7702(delegate); + SeedAccount { + addr, + info: AccountInfo { + balance: U256::from(balance), + nonce, + code_hash: code.hash_slow(), + code: Some(code), + }, + } +} + +/// legacy 转账/调用交易。 +fn legacy_tx( + nonce: u64, + gas_limit: u64, + gas_price: u128, + value: u128, + to: TxKind, +) -> TransactionSigned { + TransactionSigned::new_unhashed( + Transaction::Legacy(TxLegacy { + nonce, + gas_price, + gas_limit, + to, + value: U256::from(value), + ..Default::default() + }), + Signature::test_signature(), + ) +} + +/// EIP-1559 转账/调用交易。 +fn eip1559_tx( + nonce: u64, + gas_limit: u64, + max_fee: u128, + max_prio: u128, + value: u128, + to: TxKind, +) -> TransactionSigned { + TransactionSigned::new_unhashed( + Transaction::Eip1559(TxEip1559 { + chain_id: MAINNET_CHAIN_ID, + nonce, + gas_limit, + max_fee_per_gas: max_fee, + max_priority_fee_per_gas: max_prio, + to, + value: U256::from(value), + ..Default::default() + }), + Signature::test_signature(), + ) +} + +/// EIP-7702(type-0x04)交易:调用 `to`(走其已存在的委托代码)。 +/// `authorization_list` 塞一个自委托授权(其签名走 dummy,不一定 recover 成功—— +/// 本用例的委托效果由**预置的** `delegated_account` 提供,type-4 执行路径本身即被覆盖)。 +fn eip7702_tx(nonce: u64, gas_limit: u64, max_fee: u128, to: Address) -> TransactionSigned { + let authorization_list = vec![SignedAuthorization::new_unchecked( + Authorization { chain_id: U256::from(MAINNET_CHAIN_ID), address: to, nonce: 0 }, + 0, + U256::from(1u64), + U256::from(1u64), + )]; + TransactionSigned::new_unhashed( + Transaction::Eip7702(TxEip7702 { + chain_id: MAINNET_CHAIN_ID, + nonce, + gas_limit, + max_fee_per_gas: max_fee, + max_priority_fee_per_gas: 0, + to, + value: U256::ZERO, + authorization_list, + ..Default::default() + }), + Signature::test_signature(), + ) +} + +// 确定性地址工具。 +const fn addr(b: u8) -> Address { + Address::with_last_byte(b) +} + +/// increment-slot0 合约字节码:`SLOAD(0); +1; SSTORE(0)`;STOP。 +/// 汇编:PUSH1 1, PUSH1 0, SLOAD, ADD, PUSH1 0, SSTORE, STOP。 +const INC_SLOT0_CODE: &[u8] = &[0x60, 0x01, 0x60, 0x00, 0x54, 0x01, 0x60, 0x00, 0x55, 0x00]; + +// ————————————————————————————————— 用例集 ————————————————————————————————— // + +/// 正常区块基线:这些块**必须** serial==grevm(证明预言机确实两个后端都跑了、且在正常 +/// 输入上一致——所以真背离会被抓住)。 +#[allow(clippy::vec_init_then_push)] +pub fn exec_baseline_cases() -> Vec { + let base_fee = 1u64; + let gas = 30_000_000u64; + let mut v = Vec::new(); + + // 1) 简单 legacy 转账:A -> B。 + { + let a = addr(0xA1); + let b = addr(0xB1); + v.push(ExecCase { + name: "baseline_simple_transfer".into(), + txs: vec![legacy_tx(0, 21_000, 10, 1_000, TxKind::Call(b))], + senders: vec![a], + seed: vec![eoa(a, HUGE_BALANCE, 0), eoa(b, 0, 0)], + base_fee, + block_gas_limit: gas, + }); + } + + // 2) 两笔互不相关的转账(不同 sender、不同收款人)——可并行、必须一致。 + { + let a = addr(0xA2); + let b = addr(0xB2); + let c = addr(0xC2); + let d = addr(0xD2); + v.push(ExecCase { + name: "baseline_two_independent_transfers".into(), + txs: vec![ + legacy_tx(0, 21_000, 10, 111, TxKind::Call(b)), + legacy_tx(0, 21_000, 10, 222, TxKind::Call(d)), + ], + senders: vec![a, c], + seed: vec![ + eoa(a, HUGE_BALANCE, 0), + eoa(b, 0, 0), + eoa(c, HUGE_BALANCE, 0), + eoa(d, 0, 0), + ], + base_fee, + block_gas_limit: gas, + }); + } + + // 3) EIP-1559 转账。 + { + let a = addr(0xA3); + let b = addr(0xB3); + v.push(ExecCase { + name: "baseline_eip1559_transfer".into(), + txs: vec![eip1559_tx(0, 21_000, 100, 2, 500, TxKind::Call(b))], + senders: vec![a], + seed: vec![eoa(a, HUGE_BALANCE, 0), eoa(b, 0, 0)], + base_fee, + block_gas_limit: gas, + }); + } + + // 4) 合约 CREATE:init code `PUSH1 0 PUSH1 0 RETURN`(部署空合约)。 + { + let a = addr(0xA4); + // 6000 6000 f3 = PUSH1 0, PUSH1 0, RETURN → 返回空 runtime code。 + let init: &[u8] = &[0x60, 0x00, 0x60, 0x00, 0xf3]; + v.push(ExecCase { + name: "baseline_create_contract".into(), + txs: vec![TransactionSigned::new_unhashed( + Transaction::Legacy(TxLegacy { + nonce: 0, + gas_price: 10, + gas_limit: 200_000, + to: TxKind::Create, + value: U256::ZERO, + input: Bytes::from_static(init), + ..Default::default() + }), + Signature::test_signature(), + )], + senders: vec![a], + seed: vec![eoa(a, HUGE_BALANCE, 0)], + base_fee, + block_gas_limit: gas, + }); + } + + // 5) EIP-7702:type-4 交易调用一个**预置委托**账户(委托到真实合约 C)。 覆盖 type-4 执行 + + // 委托跳转(delegation follow)路径。 + { + let a = addr(0xA5); + let deleg = addr(0xD5); // 已带 0xef0100||C 委托代码的账户 + let c = addr(0xC5); // 真实 increment-slot0 合约 + v.push(ExecCase { + name: "baseline_eip7702_delegated_call".into(), + txs: vec![eip7702_tx(0, 200_000, 100, deleg)], + senders: vec![a], + seed: vec![ + eoa(a, HUGE_BALANCE, 0), + delegated_account(deleg, c, 0, 0), + contract(c, INC_SLOT0_CODE, 0), + ], + base_fee, + block_gas_limit: gas, + }); + } + + v +} + +/// 对抗性区块:面向并行执行背离的构造(RAW/WAW 冲突、同 sender nonce 依赖、 +/// 同收款人余额写写、CREATE 交互)。**HUNTING** 真 fork——若全都 serial==grevm, +/// 如实报「未发现背离」,绝不编造。 +#[allow(clippy::vec_init_then_push)] +pub fn exec_adversarial_cases() -> Vec { + let base_fee = 1u64; + let gas = 30_000_000u64; + let mut v = Vec::new(); + + // A1) 同一存储槽的 RAW/WAW 冲突:两笔交易都调用同一合约 C(读-改-写 slot0)。 + // 正确的 grevm 必须探测到依赖并串行化;若不然,slot0 结果会是 1 而非 2。 + { + let a = addr(0x1A); + let b = addr(0x1B); + let c = addr(0x1C); + v.push(ExecCase { + name: "adv_raw_waw_same_slot".into(), + txs: vec![ + legacy_tx(0, 100_000, 10, 0, TxKind::Call(c)), + legacy_tx(0, 100_000, 10, 0, TxKind::Call(c)), + ], + senders: vec![a, b], + seed: vec![ + eoa(a, HUGE_BALANCE, 0), + eoa(b, HUGE_BALANCE, 0), + contract(c, INC_SLOT0_CODE, 0), + ], + base_fee, + block_gas_limit: gas, + }); + } + + // A2) 同 sender 的 nonce 依赖:同一账户连发两笔(nonce 0、1),顺序敏感。 + { + let a = addr(0x2A); + let b = addr(0x2B); + v.push(ExecCase { + name: "adv_same_sender_nonce_chain".into(), + txs: vec![ + legacy_tx(0, 21_000, 10, 100, TxKind::Call(b)), + legacy_tx(1, 21_000, 10, 200, TxKind::Call(b)), + ], + senders: vec![a, a], + seed: vec![eoa(a, HUGE_BALANCE, 0), eoa(b, 0, 0)], + base_fee, + block_gas_limit: gas, + }); + } + + // A3) 同收款人的余额写-写:两个不同 sender 向同一账户转账(收款人余额 WAW)。 + { + let a = addr(0x3A); + let b = addr(0x3B); + let r = addr(0x3F); + v.push(ExecCase { + name: "adv_same_recipient_balance_waw".into(), + txs: vec![ + legacy_tx(0, 21_000, 10, 1_000, TxKind::Call(r)), + legacy_tx(0, 21_000, 10, 2_000, TxKind::Call(r)), + ], + senders: vec![a, b], + seed: vec![eoa(a, HUGE_BALANCE, 0), eoa(b, HUGE_BALANCE, 0), eoa(r, 0, 0)], + base_fee, + block_gas_limit: gas, + }); + } + + // A4) 三笔链式:tx0 调用 C 写 slot0;tx1 同 sender 转账;tx2 再调 C。 + // 混合 nonce 依赖 + 存储冲突,压 grevm 的依赖图。 + { + let a = addr(0x4A); + let b = addr(0x4B); + let c = addr(0x4C); + v.push(ExecCase { + name: "adv_mixed_nonce_and_storage".into(), + txs: vec![ + legacy_tx(0, 100_000, 10, 0, TxKind::Call(c)), + legacy_tx(1, 21_000, 10, 50, TxKind::Call(b)), + legacy_tx(2, 100_000, 10, 0, TxKind::Call(c)), + ], + senders: vec![a, a, a], + seed: vec![eoa(a, HUGE_BALANCE, 0), eoa(b, 0, 0), contract(c, INC_SLOT0_CODE, 0)], + base_fee, + block_gas_limit: gas, + }); + } + + v +} + +// ————————————————————————————————— 测试 ————————————————————————————————— // + +#[cfg(all(test, feature = "difftx_exec"))] +mod tests { + use super::*; + + /// **承重的正确性测试**:每个基线块 → 两侧都 Ok 且 `!diverged`。 + /// 证明该差分预言机确实把区块过了**两个后端**、且在正常输入上一致—— + /// 因此一个真实背离**会**被抓住。 + #[test] + fn baseline_serial_equals_grevm() { + for case in exec_baseline_cases() { + let d = run_exec_diff(&case); + assert!(d.serial_ok, "[{}] 串行后端未成功执行区块:{:?}", d.name, d.divergence); + assert!(d.grevm_ok, "[{}] grevm 后端未成功执行区块:{:?}", d.name, d.divergence); + assert!( + !d.diverged, + "[{}] 基线块出现 serial⟷grevm 背离(不应发生):{:?}", + d.name, d.divergence + ); + println!( + "[baseline OK] {:<40} serial_ok={} grevm_ok={} diverged={}", + d.name, d.serial_ok, d.grevm_ok, d.diverged + ); + } + } + + /// 对抗性块:跑并记录经验事实。断言的是「不允许出现 serial⟷grevm 背离」—— + /// 这才是共识安全的要求(我们在 hunting,若一致就报一致,绝不编造)。 + /// 若某天此断言失败,即抓到了真 fork。 + #[test] + fn adversarial_no_divergence_or_report() { + let mut any_diverged = false; + for case in exec_adversarial_cases() { + let d = run_exec_diff(&case); + println!( + "[adversarial] {:<40} serial_ok={} grevm_ok={} diverged={} {}", + d.name, + d.serial_ok, + d.grevm_ok, + d.diverged, + d.divergence.as_deref().unwrap_or("") + ); + if d.diverged { + any_diverged = true; + } + } + assert!( + !any_diverged, + "对抗集出现 serial⟷grevm 背离 —— 疑似共识 state-fork,见上方 [adversarial] 行" + ); + } + + /// **经验事实文档(empirical finding)**:serial 与 grevm 在**原始** `BundleState.reverts` + /// 上确实不等,但这是**良性簿记差异、非共识 fork**——精确断言其形态,防止未来把它 + /// 误判成安全问题,也防止 grevm 在 reverts *内容* 上真回归时无人发现。 + /// + /// 以最简单的 `A -> B` 转账块为证,逐字段固定观察到的事实: + /// 1. `state` / `contracts` / `state_size` **逐字节相等** ⟹ post-state 相同 ⟹ state root 相同。 + /// 2. receipts **逐笔相等**。 + /// 3. **原始 `reverts` 不等**: + /// - outer-block 数量不同:serial=2(含一个空尾块)、grevm=1; + /// - 首块内账户排列顺序不同(串行 `[sender, coinbase, recipient]`、 grevm `[sender, + /// recipient, coinbase]`),但账户集合与 `AccountRevert` 内容一致。 + /// 4. 语义化(顺序无关、忽略空块)的 `flatten_reverts` **相等** —— 证明差异纯属排列/结构。 + #[test] + fn empirical_raw_reverts_differ_but_state_root_equivalent() { + let case = ExecCase { + name: "empirical_simple_transfer".into(), + txs: vec![legacy_tx(0, 21_000, 10, 1_000, TxKind::Call(addr(0xB1)))], + senders: vec![addr(0xA1)], + seed: vec![eoa(addr(0xA1), HUGE_BALANCE, 0), eoa(addr(0xB1), 0, 0)], + base_fee: 1, + block_gas_limit: 30_000_000, + }; + + let chain_spec = prague_chain_spec(); + let block = build_block(&case); + let cache = seeded_cache(&case.seed); + let mut serial = fresh_serial_executor(chain_spec.clone(), cache.clone()); + let mut grevm = fresh_grevm_executor(chain_spec, cache); + + let so = serial.execute(&block).expect("serial 执行成功"); + let go = grevm.execute(&block).expect("grevm 执行成功"); + + // 1) 共识相关字段逐字节相等(⟹ state root 相同)。 + assert_eq!(so.state.state, go.state.state, "post-state 必须相同(决定 state root)"); + assert_eq!(so.state.contracts, go.state.contracts, "contracts 必须相同"); + assert_eq!(so.state.state_size, go.state.state_size, "state_size 必须相同"); + + // 2) receipts 逐笔相等。 + assert_eq!(so.result.receipts, go.result.receipts, "receipts 必须相同"); + assert_eq!(so.result.gas_used, go.result.gas_used, "block gas_used 必须相同"); + + // 3) 原始 reverts **不等**(良性簿记差异:空尾块 + 账户排列顺序)。 + assert_ne!( + so.state.reverts, go.state.reverts, + "已知经验事实:serial/grevm 的原始 reverts 因空尾块+账户排列而不等;\ + 若此断言失败说明 grevm 改了 reverts 结构,需复核本 finding 是否仍成立" + ); + + // 4) 语义化 reverts(顺序无关、忽略空块)**相等** —— 差异纯属排列/结构,非内容。 + assert_eq!( + flatten_reverts(&so.state), + flatten_reverts(&go.state), + "reverts 的语义内容必须一致(顺序无关);若不等则是真正的共识相关回归" + ); + } +} diff --git a/crates/pipe-exec-layer-ext-v2/execute/src/difftx_fuzz.rs b/crates/pipe-exec-layer-ext-v2/execute/src/difftx_fuzz.rs new file mode 100644 index 000000000..3daf91277 --- /dev/null +++ b/crates/pipe-exec-layer-ext-v2/execute/src/difftx_fuzz.rs @@ -0,0 +1,626 @@ +//! 交易 / 交易序列 **proptest fuzz 生成器** —— 把 `crate::difftx` 里手搓的 12 个单笔用例 + +//! 1 条序列,升级成对**整个字段空间**的自动搜索。 +//! +//! ## 被测不变式 +//! +//! `tx_filter::filter_invalid_txs` 必须是 revm 交易级校验的**超集**:任何被 filter 放行 +//! (ADMIT)却被 revm 以 `InvalidTransaction` 拒绝的交易,都是能打停真链的停链缺口 +//! (见 `difftx.rs` / `tx_filter.rs` 模块头,`lib.rs:1323` 的 `.unwrap()` panic 点)。 +//! +//! 本模块用 proptest 随机生成 `TransactionSigned`(tx 类型 / nonce / gas / fee / value / +//! initcode 长度(跨 EIP-3860 边界)/ chain_id / access-list / 7702 授权列表 各字段的对抗 +//! 组合)+ 匹配的 `SeedAccount` 种子,喂给 `run_case` / `run_seq_case` 两侧预言机,断言: +//! +//! 1. **单笔字段级**(`fuzz_single_tx_no_filter_gap`):`prop_assert!(!out.is_gap)` —— filter +//! 永不放行 revm 会拒绝的单笔交易,对**整个生成空间**成立。 +//! 2. **多笔序列**(`fuzz_seq_no_exec_gap`):`prop_assert!(out.exec_gaps.is_empty())` —— 未预置 +//! 委托指示符的普通序列不应产生任何执行期缺口;一旦出现即为 NEW finding。 +//! 3. **7702 授权列表**(`fuzz_7702_auth_list_no_exec_gap`):用 `alloy_signer_local` 真实签名, 让 +//! `recover_authority()` 得到**受控**的 authority 地址,探测 filter 的授权-nonce 模型是否 与 +//! revm 执行一致;断言无新执行缺口(已知的委托-CREATE nonce 竞态需预置 CREATE 运行时的 +//! 委托代码,本策略不构造,故预期零缺口)。 +//! +//! proptest 默认确定性(不调用任何 wall-clock / 随机种子 API),发现反例会自动最小化并打印。 +//! 纯测试期 harness。 + +use alloy_consensus::{constants::KECCAK_EMPTY, TxEip1559, TxEip7702, TxLegacy}; +use alloy_eips::{ + eip2930::{AccessList, AccessListItem}, + eip7702::{Authorization, SignedAuthorization}, +}; +use alloy_primitives::{Address, Bytes, Signature, TxKind, B256, U256}; +use alloy_signer::SignerSync; +use alloy_signer_local::PrivateKeySigner; +use proptest::{collection::vec as prop_vec, prelude::*}; +use reth_ethereum_primitives::{Transaction, TransactionSigned}; +use revm::state::{AccountInfo, Bytecode}; +use revm_primitives::eip3860::MAX_INITCODE_SIZE; + +use crate::difftx::{run_case, run_seq_case, DiffCase, SeedAccount, SeqCase}; + +/// 主网 chain id —— 与 `difftx.rs` 的测试 chainspec 对齐(filter 的 chain-id 门拒绝不等此值的 +/// typed 交易)。 +const MAINNET_CHAIN_ID: u64 = 1; +/// 一个错误 chain id(用于制造 `InvalidChainId`)。 +const WRONG_CHAIN_ID: u64 = 999; +/// 充裕余额(1 ETH),供「余额不是被测因素」的用例。 +const BIG_BALANCE: u128 = 1_000_000_000_000_000_000; +/// 单笔 fuzz 的固定发送者地址(确定性,便于复现)。 +const FUZZ_SENDER: Address = Address::with_last_byte(0x11); +/// 序列 fuzz 的固定发送者集合。 +const SEQ_SENDERS: [Address; 3] = + [Address::with_last_byte(0x11), Address::with_last_byte(0x12), Address::with_last_byte(0x13)]; + +// —————————————————————————————— 账户 / 交易 构造辅助 —————————————————————————————— // + +/// 按 `code_variant` 造账户信息:0=空码 EOA;1=非委托代码(触发 EIP-3607); +/// 2=7702 委托指示符(Pectra 放行)。三者在两侧预言机上应当行为一致。 +fn build_account(balance: u128, nonce: u64, code_variant: u8) -> AccountInfo { + match code_variant { + 1 => { + // 一段普通 EVM 字节码(非 0xef0100 委托指示符)。 + let code = Bytecode::new_raw(Bytes::from_static(&[0x60, 0x00, 0x60, 0x00])); + AccountInfo { + balance: U256::from(balance), + nonce, + code_hash: code.hash_slow(), + code: Some(code), + } + } + 2 => { + // 指向任意目标的 7702 委托指示符 0xef0100||target。 + let code = Bytecode::new_eip7702(Address::with_last_byte(0x77)); + AccountInfo { + balance: U256::from(balance), + nonce, + code_hash: code.hash_slow(), + code: Some(code), + } + } + _ => { + AccountInfo { balance: U256::from(balance), nonce, code_hash: KECCAK_EMPTY, code: None } + } + } +} + +/// 造一个 `len` 项的 access list(每项一个存储槽),用于覆盖 access-list 内联 gas 分支。 +fn build_access_list(len: usize) -> AccessList { + AccessList( + (0..len) + .map(|i| AccessListItem { + address: Address::with_last_byte(0x30u8.wrapping_add(i as u8)), + storage_keys: vec![B256::ZERO], + }) + .collect(), + ) +} + +/// 造 `n` 个「未签名占位」授权项(`r=s=0`,`recover_authority()` 会失败 → 两侧都跳过), +/// 用于在不牵扯真实签名的前提下驱动空/非空授权列表分支。 +fn placeholder_auths(n: usize) -> Vec { + (0..n) + .map(|_| { + SignedAuthorization::new_unchecked( + Authorization { chain_id: U256::ZERO, address: Address::ZERO, nonce: 0 }, + 0, + U256::ZERO, + U256::ZERO, + ) + }) + .collect() +} + +/// 按各字段造一笔 `TransactionSigned`(签名恒为 `test_signature`,与 `difftx.rs` 的 builder +/// 一致——两侧预言机都以显式 sender 绕过签名恢复,故签名内容对单笔/序列不变式无影响)。 +#[allow(clippy::too_many_arguments)] +fn build_tx( + ty: u8, + chain_sel: u8, + nonce: u64, + gas_limit: u64, + max_fee: u128, + max_prio: u128, + value: u128, + to_create: bool, + to_sel: u8, + input_len: usize, + input_byte: u8, + al_len: usize, + auth_len: usize, +) -> TransactionSigned { + let input = Bytes::from(vec![input_byte; input_len]); + let to_addr = Address::with_last_byte(0x20u8.wrapping_add(to_sel)); + let inner = match ty { + // Legacy:chain_id 可为 None(pre-EIP-155,合法)/ 正确 / 错误。 + 0 => { + let chain_id = match chain_sel { + 1 => Some(WRONG_CHAIN_ID), + 2 => None, + _ => Some(MAINNET_CHAIN_ID), + }; + let to = if to_create { TxKind::Create } else { TxKind::Call(to_addr) }; + Transaction::Legacy(TxLegacy { + chain_id, + nonce, + gas_price: max_fee, + gas_limit, + to, + value: U256::from(value), + input, + }) + } + // EIP-1559:typed,chain_id 恒有值(正确 / 错误)。 + 1 => { + let chain_id = if chain_sel == 1 { WRONG_CHAIN_ID } else { MAINNET_CHAIN_ID }; + let to = if to_create { TxKind::Create } else { TxKind::Call(to_addr) }; + Transaction::Eip1559(TxEip1559 { + chain_id, + nonce, + gas_limit, + max_fee_per_gas: max_fee, + max_priority_fee_per_gas: max_prio, + to, + value: U256::from(value), + access_list: build_access_list(al_len), + input, + }) + } + // EIP-7702:typed,`to` 恒为 Call(无 Create);授权列表用占位项。 + _ => { + let chain_id = if chain_sel == 1 { WRONG_CHAIN_ID } else { MAINNET_CHAIN_ID }; + Transaction::Eip7702(TxEip7702 { + chain_id, + nonce, + gas_limit, + max_fee_per_gas: max_fee, + max_priority_fee_per_gas: max_prio, + to: to_addr, + value: U256::from(value), + access_list: build_access_list(al_len), + authorization_list: placeholder_auths(auth_len), + input, + }) + } + }; + TransactionSigned::new_unhashed(inner, Signature::test_signature()) +} + +// —————————————————————————————— 单笔字段级策略 —————————————————————————————— // + +fn nonce_strat() -> impl Strategy { + prop_oneof![8 => 0u64..=8, 1 => 9u64..=1000, 1 => Just(u64::MAX)] +} +fn gas_limit_strat() -> impl Strategy { + prop_oneof![ + 3 => 0u64..=25_000, + 3 => 25_000u64..=100_000, + 2 => 100_000u64..=40_000_000, + 1 => Just(u64::MAX), + ] +} +fn fee_strat() -> impl Strategy { + prop_oneof![ + 5 => 0u128..=5_000, // 覆盖 max_fee < base_fee 边界 + 2 => 900u128..=1_100, // 贴近典型 base_fee + 1 => 1_000_000u128..=1_000_000_000, + 1 => Just(u128::MAX), // 溢出 / 余额门 + ] +} +fn value_strat() -> impl Strategy { + prop_oneof![6 => 0u128..=2_000, 2 => 10_000u128..=1_000_000_000, 1 => Just(u128::MAX)] +} +fn input_len_strat() -> impl Strategy { + prop_oneof![ + 5 => 0usize..=200, + 3 => (MAX_INITCODE_SIZE - 2)..=(MAX_INITCODE_SIZE + 2), // 跨 EIP-3860 initcode 边界 + 1 => 200usize..=2_000, + ] +} +fn base_fee_strat() -> impl Strategy { + prop_oneof![5 => 0u64..=2_000, 2 => Just(1_000u64), 1 => 1_000_000u64..=20_000_000_000] +} +fn block_gas_limit_strat() -> impl Strategy { + prop_oneof![1 => Just(0u64), 3 => 21_000u64..=100_000, 4 => 100_000u64..=30_000_000] +} +fn balance_strat() -> impl Strategy { + prop_oneof![ + 3 => 0u128..=100_000, + 4 => Just(BIG_BALANCE), + 2 => 1_000_000u128..=1_000_000_000_000, + 1 => Just(u128::MAX / 2), + ] +} + +/// 单笔差分用例策略:生成 `TransactionSigned` + 匹配的发送者 `SeedAccount` + 区块参数。 +fn single_case_strategy() -> impl Strategy { + // 拆成两组 ≤10 元 tuple(proptest 对 tuple 的 Strategy 实现上限),再 prop_map 合成。 + let g1 = ( + 0u8..=2u8, // ty: Legacy / 1559 / 7702 + nonce_strat(), // tx nonce + gas_limit_strat(), // tx gas_limit + fee_strat(), // max_fee / gas_price + fee_strat(), // max_priority_fee + value_strat(), // value + any::(), // to = Create?(7702 忽略,恒 Call) + 0u8..8u8, // to 目标选择 + input_len_strat(), // input 长度(跨 initcode 边界) + any::(), // input 填充字节(零/非零 calldata gas) + ); + let g2 = ( + 0u8..=2u8, // chain_sel: 正确 / 错误 / (legacy)None + 0usize..=4, // access-list 项数 + 0usize..=3, // 7702 授权项数(含 0 → EmptyAuthorizationList) + base_fee_strat(), // 区块 base fee + block_gas_limit_strat(), // 区块 gas limit + any::(), // prague / shanghai + balance_strat(), // 发送者余额 + nonce_strat(), // 发送者账户 nonce + 0u8..=2u8, // 发送者代码变体:空 / 非委托 / 7702 委托 + ); + (g1, g2).prop_map(|(a, b)| { + let (ty, nonce, gas_limit, max_fee, max_prio, value, to_create, to_sel, input_len, input_byte) = + a; + let (chain_sel, al_len, auth_len, base_fee, block_gas_limit, prague, balance, seed_nonce, code_variant) = + b; + let tx = build_tx( + ty, chain_sel, nonce, gas_limit, max_fee, max_prio, value, to_create, to_sel, + input_len, input_byte, al_len, auth_len, + ); + DiffCase { + name: format!( + "fuzz1/ty{ty}/n{nonce}/gl{gas_limit}/cs{chain_sel}/cv{code_variant}/al{al_len}/au{auth_len}" + ), + tx, + sender: FUZZ_SENDER, + seed: vec![SeedAccount { + addr: FUZZ_SENDER, + info: build_account(balance, seed_nonce, code_variant), + }], + base_fee, + block_gas_limit, + prague, + } + }) +} + +// —————————————————————————————— 多笔序列策略 —————————————————————————————— // + +/// 单条序列内一笔交易的生成参数。 +#[derive(Debug, Clone)] +struct SeqTxSpec { + sender_idx: u8, + ty: u8, + nonce: u64, + gas_limit: u64, + max_fee: u128, + value: u128, + auth_len: usize, +} + +fn seq_tx_spec_strat() -> impl Strategy { + ( + 0u8..3u8, // 3 个固定 sender 之一 + 0u8..=2u8, // ty + 0u64..=6u64, // nonce:覆盖有效链 / 重用 / 空洞 + prop_oneof![3 => 21_000u64..=120_000, 2 => 0u64..=21_000, 1 => 500_000u64..=5_000_000], + prop_oneof![4 => 0u128..=5_000, 1 => Just(u128::MAX)], + prop_oneof![5 => 0u128..=1_000, 1 => 10_000u128..=1_000_000], + 0usize..=2usize, // 7702 占位授权项数 + ) + .prop_map(|(sender_idx, ty, nonce, gas_limit, max_fee, value, auth_len)| SeqTxSpec { + sender_idx, + ty, + nonce, + gas_limit, + max_fee, + value, + auth_len, + }) +} + +/// 多笔序列差分用例策略:2..=5 笔来自固定 sender 集合的混合类型交易,各 sender 独立播种。 +fn seq_case_strategy() -> impl Strategy { + ( + prop_vec(seq_tx_spec_strat(), 2..=5), + // 3 个 sender 的起始 nonce(小范围,含链与错配)。 + (0u64..=3u64, 0u64..=3u64, 0u64..=3u64), + base_fee_strat(), + block_gas_limit_strat(), + any::(), + ) + .prop_map(|(specs, seed_nonces, base_fee, block_gas_limit, prague)| { + let mut txs = Vec::with_capacity(specs.len()); + let mut senders = Vec::with_capacity(specs.len()); + for sp in &specs { + let tx = build_tx( + sp.ty, + 0, // chain_sel=正确:序列聚焦执行期 TOCTOU,非 chain-id 门 + sp.nonce, + sp.gas_limit, + sp.max_fee, + 0, // max_prio + sp.value, + false, // to = Call + (sp.sender_idx + 1) & 7, + 0, // 无 calldata + 0, + 0, // 无 access list + sp.auth_len, + ); + txs.push(tx); + senders.push(SEQ_SENDERS[sp.sender_idx as usize]); + } + let seed = vec![ + SeedAccount { + addr: SEQ_SENDERS[0], + info: build_account(BIG_BALANCE, seed_nonces.0, 0), + }, + SeedAccount { + addr: SEQ_SENDERS[1], + info: build_account(BIG_BALANCE, seed_nonces.1, 0), + }, + SeedAccount { + addr: SEQ_SENDERS[2], + info: build_account(BIG_BALANCE, seed_nonces.2, 0), + }, + ]; + SeqCase { + name: format!("fuzzSeq/{}tx", specs.len()), + txs, + senders, + seed, + base_fee, + block_gas_limit, + prague, + } + }) +} + +// —————————————————————————————— 7702 授权列表策略(真实签名) —————————————————————————————— // + +/// 从固定私钥派生的确定性签名者(key = 全零 + 末字节 idx+1,是合法非零 secp256k1 标量)。 +/// 不使用 `random()`,保证 proptest 确定性。 +fn auth_signer(idx: u8) -> PrivateKeySigner { + let mut key = [0u8; 32]; + key[31] = idx + 1; + PrivateKeySigner::from_slice(&key).expect("valid secp256k1 key") +} + +/// 该签名者对应的 authority 地址(`recover_authority()` 将确定性地恢复出它)。 +fn authority_addr(idx: u8) -> Address { + auth_signer(idx).address() +} + +/// 用固定私钥真实签名一个授权项,使 `recover_authority()` 得到受控地址。 +fn signed_auth(idx: u8, chain_id: U256, target: Address, nonce: u64) -> SignedAuthorization { + let signer = auth_signer(idx); + let auth = Authorization { chain_id, address: target, nonce }; + let sig = signer.sign_hash_sync(&auth.signature_hash()).expect("sign auth"); + auth.into_signed(sig) +} + +/// 单个授权项的生成参数。 +#[derive(Debug, Clone)] +struct AuthSpec { + signer_idx: u8, + chain_sel: u8, + nonce_kind: u8, + target_sel: u8, +} + +fn auth_spec_strat() -> impl Strategy { + (0u8..3u8, 0u8..3u8, 0u8..4u8, 0u8..4u8).prop_map( + |(signer_idx, chain_sel, nonce_kind, target_sel)| AuthSpec { + signer_idx, + chain_sel, + nonce_kind, + target_sel, + }, + ) +} + +/// 后续(follow-up)普通交易的生成参数:从某 authority 发一笔 legacy,探测 filter 的 +/// 授权-nonce 模型与 revm 执行是否一致。 +#[derive(Debug, Clone)] +struct FollowSpec { + signer_idx: u8, + nonce: u64, +} + +fn follow_spec_strat() -> impl Strategy { + (0u8..3u8, 0u64..=4u64).prop_map(|(signer_idx, nonce)| FollowSpec { signer_idx, nonce }) +} + +/// 7702 授权列表序列策略: +/// - 1..=2 笔来自同一 sender(可能是某 authority 本身 → 自赞助)的 7702 交易,每笔携 1..=2 +/// 条**真实签名**的授权(chain_id = 0/正确/错配,authorization nonce = 账户 nonce/+1/空洞/MAX, +/// 可重复 authority,多授权链); +/// - 0..=2 笔来自某 authority 的后续 legacy 交易。 +/// 所有 authority 账户(3 个签名者地址)与基础 sender E 都被播种(起始 nonce=0,余额充裕)。 +fn seq_7702_auth_strategy() -> impl Strategy { + ( + 0u8..4u8, // sender_sel:0=独立 EOA E;1..3=某 authority(自赞助) + prop_vec(prop_vec(auth_spec_strat(), 1..=2), 1..=2), + prop_vec(follow_spec_strat(), 0..=2), + ) + .prop_map(|(sender_sel, tx_auth_specs, follows)| { + let base_eoa = FUZZ_SENDER; // 0x11 + let sender_addr = + if sender_sel == 0 { base_eoa } else { authority_addr(sender_sel - 1) }; + + let mut txs = Vec::new(); + let mut senders = Vec::new(); + + // 7702 交易:同一 sender,nonce = 交易序号(构成有效链,最大化被放行执行的概率)。 + for (i, auths) in tx_auth_specs.iter().enumerate() { + let authorization_list: Vec = auths + .iter() + .map(|a| { + let chain_id = match a.chain_sel { + 0 => U256::ZERO, // 0 = 通配(对任意链有效) + 1 => U256::from(MAINNET_CHAIN_ID), // 正确 + _ => U256::from(WRONG_CHAIN_ID), // 错配 → 两侧都应跳过 + }; + // authority 账户起始 nonce=0,故:0→0(匹配),1→1(错位),2→5(空洞),3→MAX。 + let auth_nonce = match a.nonce_kind { + 0 => 0, + 1 => 1, + 2 => 5, + _ => u64::MAX, + }; + let target = Address::with_last_byte(0x60u8.wrapping_add(a.target_sel)); + signed_auth(a.signer_idx, chain_id, target, auth_nonce) + }) + .collect(); + let inner = TxEip7702 { + chain_id: MAINNET_CHAIN_ID, + nonce: i as u64, + gas_limit: 500_000, // 足够覆盖 21k + 25k*auths 内联 + 调用 + max_fee_per_gas: 10, + max_priority_fee_per_gas: 0, + to: Address::with_last_byte(0x20), + value: U256::ZERO, + access_list: AccessList::default(), + authorization_list, + input: Bytes::new(), + }; + txs.push(TransactionSigned::new_unhashed( + Transaction::Eip7702(inner), + Signature::test_signature(), + )); + senders.push(sender_addr); + } + + // 后续 legacy 交易:从某 authority 发出,nonce 受 fuzz 控制。 + for f in &follows { + let tx = build_tx( + 0, // legacy + 0, // chain_sel 正确 + f.nonce, 21_000, 10, // max_fee=gas_price ≥ base_fee(1) + 0, 0, false, 0, 0, 0, 0, 0, + ); + txs.push(tx); + senders.push(authority_addr(f.signer_idx)); + } + + // 播种:基础 EOA + 3 个 authority(起始 nonce=0,空码,余额充裕)。 + let mut seed = + vec![SeedAccount { addr: base_eoa, info: build_account(BIG_BALANCE, 0, 0) }]; + for idx in 0u8..3 { + seed.push(SeedAccount { + addr: authority_addr(idx), + info: build_account(BIG_BALANCE, 0, 0), + }); + } + + SeqCase { + name: format!("fuzz7702/sender{sender_sel}/{}tx", txs.len()), + txs, + senders, + seed, + base_fee: 1, + block_gas_limit: 30_000_000, + prague: true, + } + }) +} + +// —————————————————————————————— proptest —————————————————————————————— // + +// 每次 `run_case` / `run_seq_case` 都要重建一份完整主网 +// chainspec(`ChainSpecBuilder::from(&*MAINNET)` ,约 0.3–0.7s/case),故 case +// 数按每个策略单独拨盘,保证整套在一分钟内跑完,同时仍充分搜索。 拆成独立 `proptest!` +// 块以便逐测试配置 case 数。 + +proptest! { + #![proptest_config(ProptestConfig::with_cases(64))] + + /// (1) 单笔字段级 fuzz:filter 永不放行 revm 会拒绝的交易 —— 对整个生成空间成立。 + /// 一旦 `is_gap`,即找到一个 NEW filter⊇revm 停链缺口;proptest 会自动最小化并打印。 + #[test] + fn fuzz_single_tx_no_filter_gap(case in single_case_strategy()) { + let out = run_case(&case); + prop_assert!( + !out.is_gap, + "NEW filter-superset-revm CHAIN-HALT GAP: filter ADMITTED a tx revm REJECTS.\n\ + name={}\n revm_variant={:?}\n filter_admit={} revm_reject={}\n minimized DiffCase={:#?}", + case.name, out.revm_variant, out.filter_admit, out.revm_reject, case + ); + } +} + +proptest! { + #![proptest_config(ProptestConfig::with_cases(64))] + + /// (2) 多笔序列 fuzz:未预置委托指示符的普通序列不应产生任何执行期缺口。 + /// 非空 `exec_gaps` 即为超出已知 7702-CREATE 类的 NEW finding —— 让断言携带细节并打印。 + #[test] + fn fuzz_seq_no_exec_gap(case in seq_case_strategy()) { + let out = run_seq_case(&case); + prop_assert!( + out.exec_gaps.is_empty(), + "NEW seq exec gap (beyond known 7702-CREATE class): filter admitted {:?}, \ + exec gaps {:#?}\n minimized SeqCase={:#?}", + out.filter_admitted, out.exec_gaps, case + ); + } + + /// (3) 7702 授权列表 fuzz(真实签名):探测 filter 的授权-nonce 模型与 revm 执行的一致性。 + /// 本策略不构造 CREATE 运行时的委托代码,故已知的委托-CREATE nonce 竞态不会出现;任何 + /// `exec_gap` 都是 filter 授权模型与 revm 分歧的 NEW finding —— 携细节打印。 + #[test] + fn fuzz_7702_auth_list_no_exec_gap(case in seq_7702_auth_strategy()) { + let out = run_seq_case(&case); + prop_assert!( + out.exec_gaps.is_empty(), + "NEW 7702 auth-list exec gap: filter admitted {:?}, exec gaps {:#?}\n\ + minimized SeqCase={:#?}", + out.filter_admitted, out.exec_gaps, case + ); + } +} + +// —————————————————————————————— 非平凡性(反-vacuity)门 —————————————————————————————— // + +/// 证明 fuzz 空间非退化:至少存在被 filter 放行且 revm 接受的用例(`filter_admit` 可达 true), +/// 且至少存在让 revm 拒绝的用例(`revm_reject` 可达 true)。若 builder 造出的交易全被某一侧硬拒, +/// 上面的 `!is_gap` 断言会平凡通过而失去意义——本测试把这两半可达性钉死。 +#[test] +fn fuzz_generators_are_non_vacuous() { + // (a) 一笔完全合法的 1559 转账:应被两侧放行/接受。 + let happy = DiffCase { + name: "nonvacuous_happy".into(), + tx: build_tx(1, 0, 0, 21_000, 2_000, 100, 0, false, 0, 0, 0, 0, 0), + sender: FUZZ_SENDER, + seed: vec![SeedAccount { addr: FUZZ_SENDER, info: build_account(BIG_BALANCE, 0, 0) }], + base_fee: 1_000, + block_gas_limit: 30_000_000, + prague: true, + }; + let ho = run_case(&happy); + assert!(ho.filter_admit, "builder 造的合法交易应被 filter 放行: {ho:?}"); + assert!(!ho.revm_reject, "builder 造的合法交易应被 revm 接受: {ho:?}"); + assert!(!ho.is_gap); + + // (b) 一笔 gas_price < base_fee 的交易:应让 revm 拒绝(revm_reject 可达 true)。 + let rejy = DiffCase { + name: "nonvacuous_reject".into(), + tx: build_tx(0, 0, 0, 21_000, 100, 0, 0, false, 0, 0, 0, 0, 0), + sender: FUZZ_SENDER, + seed: vec![SeedAccount { addr: FUZZ_SENDER, info: build_account(BIG_BALANCE, 0, 0) }], + base_fee: 1_000, + block_gas_limit: 30_000_000, + prague: true, + }; + let ro = run_case(&rejy); + assert!(ro.revm_reject, "gas_price Date: Wed, 22 Jul 2026 14:48:49 +0800 Subject: [PATCH 3/4] =?UTF-8?q?test(pipe-exec):=20A=C3=97B=20=E8=9E=8D?= =?UTF-8?q?=E5=90=88=20=E2=80=94=20fuzz=20=E5=AF=B9=E6=8A=97=E5=9D=97=20?= =?UTF-8?q?=C3=97=20serial=E2=9F=B7grevm=20=E6=89=A7=E8=A1=8C=E5=B7=AE?= =?UTF-8?q?=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 把 difftx_fuzz 的量产生成能力接到 difftx_exec 的执行差分上:proptest 生成 6 类并行执行 hazard 的对抗块,逐块过串行 revm 与 grevm 双后端,断言 `!diverged`(无共识 state-fork)且 `serial_ok == grevm_ok`(无单侧 panic/Err)。 这是把生成器接到执行差分上的最强 hunt——搜静态看不出的 silent fork。 hazard 类(各自 proptest 策略,共 144 例):存储 RAW/WAW 同/异槽、同发送者 nonce 链交织、同收款人余额 WAW、CREATE + CREATE2 同 salt 地址碰撞、7702 委托 账户被多笔触碰、SELFDESTRUCT 同受益人余额 WAW。 实测:144 例 + 非空门全绿,**未发现 serial↔grevm 背离或非对称停链**—— grevm 的并行调度在这些依赖形态下与串行 revm 逐字节一致。一旦 proptest 最小化 出 diverged 反例即为真实 fork/halt 发现(不 allowlist,让测试大声失败)。 仅测试期 harness(difftx_exec feature 门控),不改任何生产逻辑。 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../execute/src/difftx_exec_fuzz.rs | 647 ++++++++++++++++++ .../pipe-exec-layer-ext-v2/execute/src/lib.rs | 6 + 2 files changed, 653 insertions(+) create mode 100644 crates/pipe-exec-layer-ext-v2/execute/src/difftx_exec_fuzz.rs diff --git a/crates/pipe-exec-layer-ext-v2/execute/src/difftx_exec_fuzz.rs b/crates/pipe-exec-layer-ext-v2/execute/src/difftx_exec_fuzz.rs new file mode 100644 index 000000000..1022ef748 --- /dev/null +++ b/crates/pipe-exec-layer-ext-v2/execute/src/difftx_exec_fuzz.rs @@ -0,0 +1,647 @@ +//! A×B 融合 fuzz:把 proptest 的**对抗块**生成能力接到 `difftx_exec` 的 +//! serial(revm `WrapExecutor`) ⟷ grevm(`GrevmExecutor`) 执行差分预言机上。 +//! +//! ## 被测不变式 +//! +//! 对**任意**区块,串行 revm 顺序执行与 grevm 并行执行必须逐字节一致 +//! (`BundleState.state/contracts/state_size` + 语义化 `reverts` + receipts + gas)。 +//! 任何背离(`ExecDiff::diverged`)= 一次**共识 state-fork**;任一侧 `Ok` 另一侧 `Err` +//! (或两侧 panic)= **非对称停链**。二者都是真 finding。 +//! +//! ## 生成策略(覆盖并行执行 hazard 类) +//! +//! 每个策略镜像并**参数化** `difftx_exec::exec_adversarial_cases` 的手搓用例: +//! +//! 1. `storage_conflict_strategy` —— 同/异存储槽 RAW/WAW:2..4 笔调用同一 +//! 「读-改-写(RMW)」合约,槽下标由 calldata 决定(同槽=依赖,异槽=可并行)。 +//! 2. `nonce_chain_strategy` —— 同发送者 nonce 链与其它发送者交织(顺序敏感)。 +//! 3. `recipient_waw_strategy` —— 多个不同发送者向**同一收款人**转账(余额并行累加 WAW)。 +//! 4. `create_strategy` —— 直接 CREATE(nonce 派生地址)+ 经工厂 CREATE2(同 salt→地址碰撞)混合。 +//! 5. `eip7702_strategy` —— 多笔交易触碰**同一** 7702 委托账户(委托代码在 D 的上下文 SSTORE)。 +//! 6. `selfdestruct_strategy` —— 多个 SELFDESTRUCT 合约向同一受益人转账(账户触碰 + 余额 WAW)。 +//! +//! 每个生成的 `ExecCase` → `run_exec_diff` → `prop_assert!(!diverged)` 且 +//! `prop_assert_eq!(serial_ok, grevm_ok)`。若 proptest 最小化出 `diverged` 反例, +//! **不 allowlist**——让测试大声失败并捕获最小反例(即一次真实的 fork/halt 发现)。 +//! +//! proptest 默认确定性(不触任何 wall-clock / RNG API)。每个 `run_exec_diff` 跑**两**次 +//! 整块执行且重建主网 ChainSpec(~0.5s/case 主导),故每策略 case 数按需拨盘(20..32), +//! 全套控制在 ~90s 内。纯测试期 harness。 + +use alloy_consensus::{TxEip1559, TxEip7702, TxLegacy}; +use alloy_eips::{ + eip2930::AccessList, + eip7702::{Authorization, SignedAuthorization}, +}; +use alloy_primitives::{Address, Bytes, Signature, TxKind, U256}; +use proptest::{ + collection::vec as prop_vec, + prelude::*, + strategy::{BoxedStrategy, Strategy, ValueTree}, + test_runner::TestRunner, +}; +use reth_ethereum_primitives::{Transaction, TransactionSigned}; +use revm::state::{AccountInfo, Bytecode}; +use std::collections::HashMap; + +// 复用 difftx.rs 的种子类型 + difftx_exec.rs 的执行差分预言机。 +use crate::{ + difftx::SeedAccount, + difftx_exec::{run_exec_diff, ExecCase}, +}; + +// ————————————————————————————————— 常量 ————————————————————————————————— // + +/// 主网 chain id(typed 交易的 `chain_id`)。 +const MAINNET_CHAIN_ID: u64 = 1; +/// 充裕余额(100 ETH)——保证「余额不是被测因素」,让块能真执行(非因余额门整块 Err)。 +const HUGE_BALANCE: u128 = 100_000_000_000_000_000_000; +/// SELFDESTRUCT 合约的预置余额(用于制造受益人余额 WAW)。 +const SD_BALANCE: u128 = 1_000_000_000_000; +/// 统一 base fee。 +const BASE_FEE: u64 = 1; +/// 统一 gas price / `max_fee`(> `base_fee`,保证不被 fee 门整块拒绝)。 +const GAS_PRICE: u128 = 10; +/// 统一区块 gas limit。 +const BLOCK_GAS_LIMIT: u64 = 30_000_000; + +// ————————————————————————————————— 地址工具 ————————————————————————————————— // + +/// 确定性地址:`0x00..00 || b`。 +const fn addr(b: u8) -> Address { + Address::with_last_byte(b) +} + +// ————————————————————————————————— 字节码生成 ————————————————————————————————— // + +/// 读-改-写(RMW)合约:槽下标从 **calldata** 取(`CALLDATALOAD(0)`),`slot += 1`。 +/// 汇编:`PUSH1 0; CALLDATALOAD; DUP1; SLOAD; PUSH1 1; ADD; SWAP1; SSTORE; STOP`。 +/// 同槽多笔 = RAW/WAW 依赖(grevm 必须串行化,使 `slot == 命中次数`);异槽则可并行。 +fn rmw_calldata_slot_code() -> Vec { + vec![0x60, 0x00, 0x35, 0x80, 0x54, 0x60, 0x01, 0x01, 0x90, 0x55, 0x00] +} + +/// SELFDESTRUCT 合约:受益人从 **calldata** 取(`CALLDATALOAD(0)` 低 20 字节)。 +/// 汇编:`PUSH1 0; CALLDATALOAD; SELFDESTRUCT`。Cancun 后对**预置**(非同 tx 创建)合约, +/// SELFDESTRUCT 只把余额转给受益人、不删账户 → 多笔向同一受益人 = 余额 WAW + 账户触碰。 +fn selfdestruct_calldata_code() -> Vec { + vec![0x60, 0x00, 0x35, 0xff] +} + +/// CREATE2 工厂运行时:把「部署空 runtime 的 init code」写进内存,再用 **calldata 里的 salt** +/// 做 `CREATE2(value=0, offset=0, size=5, salt)`;`POP; STOP`。 +/// 同 salt 的两笔调用 → 目标地址**碰撞** → 第二笔 CREATE2 失败返回 0,但交易本身成功。 +/// 汇编:`PUSH32 ; PUSH1 0; MSTORE; PUSH1 0; CALLDATALOAD; PUSH1 5; PUSH1 0; PUSH1 0; +/// CREATE2; POP; STOP`。 +fn create2_factory_code() -> Vec { + // 被部署的 init code:`PUSH1 0; PUSH1 0; RETURN` → 返回空 runtime。放在 32 字节字的高 5 字节 + // (MSTORE 大端写入 → 落在 memory[0..5],正好被 offset=0/size=5 读取)。 + let mut word = [0u8; 32]; + word[0..5].copy_from_slice(&[0x60, 0x00, 0x60, 0x00, 0xf3]); + let mut code = vec![0x7f]; // PUSH32 + code.extend_from_slice(&word); + code.extend_from_slice(&[ + 0x60, 0x00, 0x52, // PUSH1 0; MSTORE —— memory[0..32] = word + 0x60, 0x00, 0x35, // PUSH1 0; CALLDATALOAD —— salt + 0x60, 0x05, // PUSH1 5 —— size + 0x60, 0x00, // PUSH1 0 —— offset + 0x60, 0x00, // PUSH1 0 —— value + 0xf5, // CREATE2 + 0x50, // POP + 0x00, // STOP + ]); + code +} + +/// 直接 CREATE 交易的 init code:部署空 runtime(`PUSH1 0; PUSH1 0; RETURN`)。 +fn empty_deploy_init() -> Vec { + vec![0x60, 0x00, 0x60, 0x00, 0xf3] +} + +// ————————————————————————————————— calldata 编码 ————————————————————————————————— // + +/// 32 字节字,末字节 = `slot`(`CALLDATALOAD(0)` 大端解读即 `slot`)。 +fn slot_calldata(slot: u8) -> Bytes { + let mut w = [0u8; 32]; + w[31] = slot; + Bytes::from(w.to_vec()) +} + +/// 32 字节字,低 20 字节 = 地址(供 SELFDESTRUCT 受益人 / 通用地址参数)。 +fn addr_calldata(a: Address) -> Bytes { + let mut w = [0u8; 32]; + w[12..32].copy_from_slice(a.as_slice()); + Bytes::from(w.to_vec()) +} + +// ————————————————————————————————— 账户种子 ————————————————————————————————— // + +/// EOA 种子(空代码)。 +fn eoa(a: Address, balance: u128, nonce: u64) -> SeedAccount { + SeedAccount { + addr: a, + info: AccountInfo { + balance: U256::from(balance), + nonce, + code_hash: alloy_consensus::constants::KECCAK_EMPTY, + code: None, + }, + } +} + +/// 带真实字节码的合约账户种子(nonce=1,已部署)。 +fn contract(a: Address, code_bytes: Vec, balance: u128) -> SeedAccount { + let code = Bytecode::new_raw(Bytes::from(code_bytes)); + SeedAccount { + addr: a, + info: AccountInfo { + balance: U256::from(balance), + nonce: 1, + code_hash: code.hash_slow(), + code: Some(code), + }, + } +} + +/// EIP-7702 委托账户种子:code = `0xef0100 || delegate`。 +fn delegated_account(a: Address, delegate: Address, balance: u128, nonce: u64) -> SeedAccount { + let code = Bytecode::new_eip7702(delegate); + SeedAccount { + addr: a, + info: AccountInfo { + balance: U256::from(balance), + nonce, + code_hash: code.hash_slow(), + code: Some(code), + }, + } +} + +// ————————————————————————————————— 交易构造 ————————————————————————————————— // + +/// legacy CALL 交易(可带 calldata)。`chain_id`=None(pre-155,合法)与 `difftx_exec` 一致。 +fn legacy_call( + nonce: u64, + gas_limit: u64, + value: u128, + to: Address, + input: Bytes, +) -> TransactionSigned { + TransactionSigned::new_unhashed( + Transaction::Legacy(TxLegacy { + nonce, + gas_price: GAS_PRICE, + gas_limit, + to: TxKind::Call(to), + value: U256::from(value), + input, + ..Default::default() + }), + Signature::test_signature(), + ) +} + +/// EIP-1559 CALL 交易(可带 calldata)—— 提供 typed 交易类型覆盖。 +fn eip1559_call( + nonce: u64, + gas_limit: u64, + value: u128, + to: Address, + input: Bytes, +) -> TransactionSigned { + TransactionSigned::new_unhashed( + Transaction::Eip1559(TxEip1559 { + chain_id: MAINNET_CHAIN_ID, + nonce, + gas_limit, + max_fee_per_gas: GAS_PRICE, + max_priority_fee_per_gas: 0, + to: TxKind::Call(to), + value: U256::from(value), + access_list: AccessList::default(), + input, + }), + Signature::test_signature(), + ) +} + +/// legacy CREATE 交易(nonce 派生地址)。 +fn legacy_create(nonce: u64, gas_limit: u64, init: Bytes) -> TransactionSigned { + TransactionSigned::new_unhashed( + Transaction::Legacy(TxLegacy { + nonce, + gas_price: GAS_PRICE, + gas_limit, + to: TxKind::Create, + value: U256::ZERO, + input: init, + ..Default::default() + }), + Signature::test_signature(), + ) +} + +/// EIP-7702(type-0x04)交易,`to` 走其已存在的委托代码(授权项为自委托占位, +/// 委托效果由**预置的** `delegated_account` 提供)。镜像 `difftx_exec::eip7702_tx`。 +fn eip7702_call(nonce: u64, gas_limit: u64, to: Address, input: Bytes) -> TransactionSigned { + let authorization_list = vec![SignedAuthorization::new_unchecked( + Authorization { chain_id: U256::from(MAINNET_CHAIN_ID), address: to, nonce: 0 }, + 0, + U256::from(1u64), + U256::from(1u64), + )]; + TransactionSigned::new_unhashed( + Transaction::Eip7702(TxEip7702 { + chain_id: MAINNET_CHAIN_ID, + nonce, + gas_limit, + max_fee_per_gas: GAS_PRICE, + max_priority_fee_per_gas: 0, + to, + value: U256::ZERO, + access_list: AccessList::default(), + authorization_list, + input, + }), + Signature::test_signature(), + ) +} + +// ————————————————————————————————— nonce 分配 ————————————————————————————————— // + +/// 按区块顺序给每个发送者分配**递增** nonce(种子起始 nonce 均为 0)。 +/// 同发送者的多笔自然构成有效链,且与其它发送者交织后各自仍按 nonce 升序出现。 +fn assign_nonces(senders: &[Address]) -> Vec { + let mut counts: HashMap = HashMap::new(); + senders + .iter() + .map(|s| { + let n = *counts.get(s).unwrap_or(&0); + counts.insert(*s, n + 1); + n + }) + .collect() +} + +// ————————————————————————————————— 策略 1:存储 RAW/WAW ————————————————————————————————— // + +/// RMW 合约地址。 +const STG_CONTRACT: Address = addr(0x1C); +/// 发送者池基址(0x11..=0x14)。 +const STG_SENDER_BASE: u8 = 0x11; + +/// 每笔:(发送者下标 0..4, 槽 0..3, 是否用 1559)。同槽 → 依赖;异槽 → 可并行。 +fn storage_conflict_strategy() -> impl Strategy { + prop_vec((0usize..4usize, 0u8..3u8, any::()), 2..=4).prop_map(|specs| { + let senders: Vec
= + specs.iter().map(|(si, _, _)| addr(STG_SENDER_BASE + *si as u8)).collect(); + let nonces = assign_nonces(&senders); + let txs: Vec = specs + .iter() + .zip(&nonces) + .map(|((_, slot, use_1559), n)| { + let cd = slot_calldata(*slot); + if *use_1559 { + eip1559_call(*n, 200_000, 0, STG_CONTRACT, cd) + } else { + legacy_call(*n, 200_000, 0, STG_CONTRACT, cd) + } + }) + .collect(); + // 播种:4 个 EOA + RMW 合约。 + let mut seed: Vec = + (0u8..4).map(|i| eoa(addr(STG_SENDER_BASE + i), HUGE_BALANCE, 0)).collect(); + seed.push(contract(STG_CONTRACT, rmw_calldata_slot_code(), 0)); + ExecCase { + name: format!("fuzz_storage/{}tx", txs.len()), + txs, + senders, + seed, + base_fee: BASE_FEE, + block_gas_limit: BLOCK_GAS_LIMIT, + } + }) +} + +// ————————————————————————————————— 策略 2:同发送者 nonce 链 ————————————————————————————————— // + +/// nonce 链发送者池基址(0x21..=0x23)。 +const NC_SENDER_BASE: u8 = 0x21; +/// nonce 链的独立收款人基址(每笔一个,隔离掉收款人竞争,只留 nonce 依赖)。 +const NC_SINK_BASE: u8 = 0x71; + +/// 2..5 笔来自 3 个发送者之一;每笔转账到**各自独立**的收款人 → 唯一 hazard 是同发送者 +/// nonce 顺序(与其它发送者交织)。 +fn nonce_chain_strategy() -> impl Strategy { + prop_vec(0usize..3usize, 2..=5).prop_map(|sender_idxs| { + let senders: Vec
= + sender_idxs.iter().map(|si| addr(NC_SENDER_BASE + *si as u8)).collect(); + let nonces = assign_nonces(&senders); + let txs: Vec = nonces + .iter() + .enumerate() + .map(|(i, n)| { + let sink = addr(NC_SINK_BASE + i as u8); + legacy_call(*n, 21_000, 100, sink, Bytes::new()) + }) + .collect(); + // 播种:3 个发送者 + 每笔一个收款人。 + let mut seed: Vec = + (0u8..3).map(|i| eoa(addr(NC_SENDER_BASE + i), HUGE_BALANCE, 0)).collect(); + for i in 0..txs.len() as u8 { + seed.push(eoa(addr(NC_SINK_BASE + i), 0, 0)); + } + ExecCase { + name: format!("fuzz_nonce_chain/{}tx", txs.len()), + txs, + senders, + seed, + base_fee: BASE_FEE, + block_gas_limit: BLOCK_GAS_LIMIT, + } + }) +} + +// ————————————————————————————————— 策略 3:同收款人余额 WAW ————————————————————————————————— // + +/// 收款人地址。 +const RW_RECIPIENT: Address = addr(0x3F); +/// 发送者池基址(0x31..,每笔一个不同发送者,nonce 全 0)。 +const RW_SENDER_BASE: u8 = 0x31; + +/// 2..5 个**不同**发送者各转一笔到同一收款人 R → R 的余额并行累加 WAW。 +fn recipient_waw_strategy() -> impl Strategy { + prop_vec(1u128..=5_000u128, 2..=5).prop_map(|values| { + let n = values.len(); + let senders: Vec
= (0..n).map(|i| addr(RW_SENDER_BASE + i as u8)).collect(); + let txs: Vec = + values.iter().map(|v| legacy_call(0, 21_000, *v, RW_RECIPIENT, Bytes::new())).collect(); + let mut seed: Vec = + (0..n).map(|i| eoa(addr(RW_SENDER_BASE + i as u8), HUGE_BALANCE, 0)).collect(); + seed.push(eoa(RW_RECIPIENT, 0, 0)); + ExecCase { + name: format!("fuzz_recipient_waw/{n}tx"), + txs, + senders, + seed, + base_fee: BASE_FEE, + block_gas_limit: BLOCK_GAS_LIMIT, + } + }) +} + +// ————————————————————————————————— 策略 4:CREATE / CREATE2 ————————————————————————————————— // + +/// CREATE2 工厂地址。 +const CR_FACTORY: Address = addr(0x4C); +/// 发送者池基址(0x41..=0x44)。 +const CR_SENDER_BASE: u8 = 0x41; + +/// 每笔:(kind 0=直接 CREATE / 1=经工厂 CREATE2, 发送者下标 0..4, salt 0..3)。 +/// 直接 CREATE → nonce 派生地址(并行创建账户);同 salt 的多笔工厂调用 → CREATE2 地址碰撞。 +fn create_strategy() -> impl Strategy { + prop_vec((0u8..2u8, 0usize..4usize, 0u8..3u8), 2..=4).prop_map(|specs| { + let senders: Vec
= + specs.iter().map(|(_, si, _)| addr(CR_SENDER_BASE + *si as u8)).collect(); + let nonces = assign_nonces(&senders); + let txs: Vec = specs + .iter() + .zip(&nonces) + .map(|((kind, _, salt), n)| { + if *kind == 0 { + legacy_create(*n, 200_000, Bytes::from(empty_deploy_init())) + } else { + legacy_call(*n, 300_000, 0, CR_FACTORY, slot_calldata(*salt)) + } + }) + .collect(); + let mut seed: Vec = + (0u8..4).map(|i| eoa(addr(CR_SENDER_BASE + i), HUGE_BALANCE, 0)).collect(); + seed.push(contract(CR_FACTORY, create2_factory_code(), 0)); + ExecCase { + name: format!("fuzz_create/{}tx", txs.len()), + txs, + senders, + seed, + base_fee: BASE_FEE, + block_gas_limit: BLOCK_GAS_LIMIT, + } + }) +} + +// ————————————————————————————————— 策略 5:7702 委托账户共享触碰 ————————————————————————————————— +// // + +/// 被委托的账户 D(code = 0xef0100 || C)。 +const D7_DELEGATE: Address = addr(0x5D); +/// D 委托到的真实合约 C(RMW-by-calldata)。 +const D7_TARGET: Address = addr(0x5C); +/// 发送者池基址(0x51..=0x53)。 +const D7_SENDER_BASE: u8 = 0x51; + +/// 2..4 笔(首笔为 type-4,其余 legacy CALL)来自 3 个发送者,**都触碰同一委托账户 D**: +/// 委托代码在 D 的上下文对 calldata 指定槽做 RMW → D 的存储 RAW/WAW(跨多笔)。 +fn eip7702_strategy() -> impl Strategy { + prop_vec((0usize..3usize, 0u8..2u8), 2..=4).prop_map(|specs| { + let senders: Vec
= + specs.iter().map(|(si, _)| addr(D7_SENDER_BASE + *si as u8)).collect(); + let nonces = assign_nonces(&senders); + let txs: Vec = specs + .iter() + .zip(&nonces) + .enumerate() + .map(|(i, ((_, slot), n))| { + let cd = slot_calldata(*slot); + if i == 0 { + // 首笔用 type-0x04 交易走 7702 执行路径(to=D,携自委托占位授权)。 + eip7702_call(*n, 300_000, D7_DELEGATE, cd) + } else { + legacy_call(*n, 200_000, 0, D7_DELEGATE, cd) + } + }) + .collect(); + // 播种:3 个发送者 + 委托账户 D + 真实合约 C。 + let mut seed: Vec = + (0u8..3).map(|i| eoa(addr(D7_SENDER_BASE + i), HUGE_BALANCE, 0)).collect(); + seed.push(delegated_account(D7_DELEGATE, D7_TARGET, 0, 0)); + seed.push(contract(D7_TARGET, rmw_calldata_slot_code(), 0)); + ExecCase { + name: format!("fuzz_7702/{}tx", txs.len()), + txs, + senders, + seed, + base_fee: BASE_FEE, + block_gas_limit: BLOCK_GAS_LIMIT, + } + }) +} + +// ————————————————————————————————— 策略 6:SELFDESTRUCT / 账户触碰 +// ————————————————————————————————— // + +/// 两个 SELFDESTRUCT 合约(各带余额)。 +const SD_A: Address = addr(0x6A); +const SD_B: Address = addr(0x6B); +/// 两个候选受益人。 +const SD_R0: Address = addr(0x6E); +const SD_R1: Address = addr(0x6F); +/// 发送者池基址(0x61..=0x63)。 +const SD_SENDER_BASE: u8 = 0x61; + +/// 2..4 笔:(合约选择 0/1, 受益人选择 0/1, 发送者下标 0..3)。 +/// 多笔向**同一受益人** SELFDESTRUCT → 受益人余额 WAW;重复调用**同一合约** → +/// 账户触碰/余额清零交织。 +fn selfdestruct_strategy() -> impl Strategy { + prop_vec((any::(), any::(), 0usize..3usize), 2..=4).prop_map(|specs| { + let senders: Vec
= + specs.iter().map(|(_, _, si)| addr(SD_SENDER_BASE + *si as u8)).collect(); + let nonces = assign_nonces(&senders); + let txs: Vec = specs + .iter() + .zip(&nonces) + .map(|((use_b, use_r1, _), n)| { + let target = if *use_b { SD_B } else { SD_A }; + let benef = if *use_r1 { SD_R1 } else { SD_R0 }; + legacy_call(*n, 200_000, 0, target, addr_calldata(benef)) + }) + .collect(); + // 播种:3 个发送者 + 2 个 SELFDESTRUCT 合约(带余额)+ 2 个受益人。 + let mut seed: Vec = + (0u8..3).map(|i| eoa(addr(SD_SENDER_BASE + i), HUGE_BALANCE, 0)).collect(); + seed.push(contract(SD_A, selfdestruct_calldata_code(), SD_BALANCE)); + seed.push(contract(SD_B, selfdestruct_calldata_code(), SD_BALANCE)); + seed.push(eoa(SD_R0, 0, 0)); + seed.push(eoa(SD_R1, 0, 0)); + ExecCase { + name: format!("fuzz_selfdestruct/{}tx", txs.len()), + txs, + senders, + seed, + base_fee: BASE_FEE, + block_gas_limit: BLOCK_GAS_LIMIT, + } + }) +} + +// ————————————————————————————————— proptest 断言 ————————————————————————————————— // + +/// 对一个生成的 `ExecCase` 施加共识不变式:无背离 + 两侧执行结果对称(同 Ok/Err)。 +/// 若 proptest 最小化出反例(`diverged`),即为一次真实的 serial⟷grevm 共识 fork / 非对称停链。 +fn assert_no_divergence(case: &ExecCase) -> Result<(), TestCaseError> { + let d = run_exec_diff(case); + prop_assert!( + !d.diverged, + "serial⟷grevm 背离(疑似共识 state-fork / 非对称停链)\n case={}\n divergence={:?}\n \ + serial_ok={} grevm_ok={}\n minimized ExecCase={:#?}", + d.name, + d.divergence, + d.serial_ok, + d.grevm_ok, + case + ); + prop_assert_eq!( + d.serial_ok, + d.grevm_ok, + "单侧执行失败(可能一侧 panic/Err,另一侧成功):{} divergence={:?}", + d.name, + d.divergence + ); + Ok(()) +} + +// 每个策略拆成独立 `proptest!` 块以逐测试拨盘 case 数(重建 ChainSpec ~0.5s/case 主导)。 +// 富 hazard 的类给 32,其余 20..24,全套 ~144 case,控制在 ~90s 内。 + +proptest! { + #![proptest_config(ProptestConfig::with_cases(32))] + + /// 1) 存储 RAW/WAW:同/异槽 RMW 的 serial⟷grevm 一致性。 + #[test] + fn storage_conflict_no_divergence(case in storage_conflict_strategy()) { + assert_no_divergence(&case)?; + } +} + +proptest! { + #![proptest_config(ProptestConfig::with_cases(24))] + + /// 2) 同发送者 nonce 链交织。 + #[test] + fn nonce_chain_no_divergence(case in nonce_chain_strategy()) { + assert_no_divergence(&case)?; + } + + /// 3) 同收款人余额 WAW。 + #[test] + fn recipient_waw_no_divergence(case in recipient_waw_strategy()) { + assert_no_divergence(&case)?; + } +} + +proptest! { + #![proptest_config(ProptestConfig::with_cases(20))] + + /// 4) CREATE / CREATE2(含地址碰撞)。 + #[test] + fn create_no_divergence(case in create_strategy()) { + assert_no_divergence(&case)?; + } + + /// 6) SELFDESTRUCT / 账户触碰交织。 + #[test] + fn selfdestruct_no_divergence(case in selfdestruct_strategy()) { + assert_no_divergence(&case)?; + } +} + +proptest! { + #![proptest_config(ProptestConfig::with_cases(24))] + + /// 5) 7702 委托账户被多笔触碰。 + #[test] + fn eip7702_no_divergence(case in eip7702_strategy()) { + assert_no_divergence(&case)?; + } +} + +// ————————————————————————————————— 非平凡性(反-vacuity)门 ————————————————————————————————— // + +/// **证明生成器确实产出「两侧都能真执行」的块**:从每个策略确定性采样若干用例, +/// 断言其中至少有一个 `serial_ok && grevm_ok`。否则上面所有 `!diverged` 断言可能在 +/// 「所有块都单侧/双侧硬失败」的退化空间里平凡通过,从而观察不到任何真背离。 +#[test] +fn generator_actually_executes() { + let mut runner = TestRunner::deterministic(); + let strategies: Vec<(&str, BoxedStrategy)> = vec![ + ("storage", storage_conflict_strategy().boxed()), + ("nonce_chain", nonce_chain_strategy().boxed()), + ("recipient_waw", recipient_waw_strategy().boxed()), + ("create", create_strategy().boxed()), + ("eip7702", eip7702_strategy().boxed()), + ("selfdestruct", selfdestruct_strategy().boxed()), + ]; + + let mut total = 0usize; + let mut both_ok = 0usize; + for (name, st) in &strategies { + // 每策略采样 2 个,兼顾覆盖与速度。 + for _ in 0..2 { + let case = st.new_tree(&mut runner).expect("strategy 采样成功").current(); + let d = run_exec_diff(&case); + total += 1; + if d.serial_ok && d.grevm_ok { + both_ok += 1; + } + println!( + "[non-vacuity] {name:<14} {:<28} serial_ok={} grevm_ok={} diverged={}", + d.name, d.serial_ok, d.grevm_ok, d.diverged + ); + } + } + + assert!( + both_ok > 0, + "非平凡性失败:{total} 个采样块无一在两侧都成功执行(serial_ok && grevm_ok)——\ + 生成器全在产出硬失败的块,`!diverged` 断言会平凡通过、观察不到真背离" + ); +} diff --git a/crates/pipe-exec-layer-ext-v2/execute/src/lib.rs b/crates/pipe-exec-layer-ext-v2/execute/src/lib.rs index 528d69e5b..0cd0584e3 100644 --- a/crates/pipe-exec-layer-ext-v2/execute/src/lib.rs +++ b/crates/pipe-exec-layer-ext-v2/execute/src/lib.rs @@ -24,6 +24,12 @@ pub mod difftx_exec; /// 纯测试期 harness(用 proptest/rand dev-dep),不进生产/库面。 #[cfg(all(test, feature = "difftx"))] mod difftx_fuzz; + +/// A×B 融合:proptest 生成对抗块 → serial(revm) 与 grevm +/// 双后端执行差分(`difftx_exec::run_exec_diff`) → 搜 state-fork / 单侧 +/// panic。把量产生成器接到执行差分上的最强 hunt。纯测试期 harness。 +#[cfg(all(test, feature = "difftx_exec"))] +mod difftx_exec_fuzz; use alloy_sol_types::SolEvent; use channel::Channel; From 55c1bd598f05a0d56fe4c1260a0040d63a3de931 Mon Sep 17 00:00:00 2001 From: keanji-x Date: Wed, 22 Jul 2026 16:11:24 +0800 Subject: [PATCH 4/4] =?UTF-8?q?test(pipe-exec):=20Gravity=20=E8=87=AA?= =?UTF-8?q?=E5=AE=9A=E4=B9=89=E9=9D=A2=E9=A2=84=E8=A8=80=E6=9C=BA=20?= =?UTF-8?q?=E2=80=94=20=E8=87=AA=E5=AE=9A=E4=B9=89=20precompile=20+=20EIP-?= =?UTF-8?q?2935/BLOCKHASH?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在 difftx 上再加两类 Gravity 特有面的不变式预言机(feature 门控,不改生产逻辑): - difftx_precompile(feature difftx_precompile):mint / randomness 自定义 precompile。三条不变式全绿:①caller 门无绕过(非授权 caller——含差一位地址 ——干净 Err 且不铸币,授权 caller 精确铸 amount);②畸形输入(空/短/超长/错 func-id/gas 不足/amount=MAX)永不 panic(precompile panic=全网停链); ③把 mint precompile 经 apply_custom_precompiles 装进两后端后,serial ⟷ grevm 逐字节一致——含理想路径:授权 caller 字节码 CALL mint 直改 journal,后续 tx 花掉铸出的余额,两后端一致 ⇒ grevm 正确把 precompile 的直写当读写依赖追踪, 未 fork。 - difftx_2935(feature difftx_2935):EIP-2935 / BLOCKHASH。①exec-time BLOCKHASH(探针合约 SSTORE 进 slot 再读,取执行器实际喂给 EVM 的值,非 RPC 视图)在 N-1/N-2/N-128/N-256 上 serial==grevm 且等于规范 hash;②用**生产 结构体** RawBlockViewProvider 造截断窗口(<256 对),确认合法 in-256-window 但落在环外的块号 block_hash_ref 返回 BlockTooOld(block_view_storage/mod.rs :201/272)——**在真实代码上证实了红队记录的截断窗口前置条件**;存储层返回 Err(不 panic),完整 exec→panic 链(真 provider + warm-restart)如实记为需 pipe_test/集群复现,未伪造。 净结论:两类自定义面在当前 main 上均未发现新缺口(gating 稳、无 panic、无 serial≠grevm);2935 截断窗口风险在存储层被证实可达。 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../pipe-exec-layer-ext-v2/execute/Cargo.toml | 4 + .../execute/src/difftx_2935.rs | 493 +++++++++++++ .../execute/src/difftx_precompile.rs | 648 ++++++++++++++++++ .../pipe-exec-layer-ext-v2/execute/src/lib.rs | 9 + 4 files changed, 1154 insertions(+) create mode 100644 crates/pipe-exec-layer-ext-v2/execute/src/difftx_2935.rs create mode 100644 crates/pipe-exec-layer-ext-v2/execute/src/difftx_precompile.rs diff --git a/crates/pipe-exec-layer-ext-v2/execute/Cargo.toml b/crates/pipe-exec-layer-ext-v2/execute/Cargo.toml index f02ea5aa8..9ff1f5177 100644 --- a/crates/pipe-exec-layer-ext-v2/execute/Cargo.toml +++ b/crates/pipe-exec-layer-ext-v2/execute/Cargo.toml @@ -94,6 +94,10 @@ config-from-env = ["gravity-primitives/config-from-env"] difftx = [] # serial ⟷ grevm 执行差分:编译 difftx_exec 模块与 difftx_exec 工具二进制。 difftx_exec = ["difftx"] +# Gravity 自定义 precompile(mint/randomness)预言机(测试期)。 +difftx_precompile = ["difftx_exec"] +# EIP-2935 / BLOCKHASH exec-time + 截断窗口预言机(测试期)。 +difftx_2935 = ["difftx_exec"] [[bench]] name = "bls_pop_verify_bench" diff --git a/crates/pipe-exec-layer-ext-v2/execute/src/difftx_2935.rs b/crates/pipe-exec-layer-ext-v2/execute/src/difftx_2935.rs new file mode 100644 index 000000000..0dce3db24 --- /dev/null +++ b/crates/pipe-exec-layer-ext-v2/execute/src/difftx_2935.rs @@ -0,0 +1,493 @@ +//! EIP-2935 / BLOCKHASH exec-time 预言机(differential oracle)。 +//! +//! ## 目标面 +//! +//! - `eip_2935.rs`:Prague 激活块部署 `HISTORY_STORAGE`,部署失败 `panic!`。 +//! - `crates/gravity-storage/src/block_view_storage/mod.rs`:`BLOCKHASH` 由 `(block_number, +//! block_id)` 环支撑;`block_hash_ref`(191-202 / 262-273 行)在请求块号 落在 `(_, max]` +//! 但**不在环内**时返回 `BlockHashError::BlockTooOld`。 warm-restart / checkpoint-resume 若只供给 +//! <256 对,窗口内最老的合法 `BLOCKHASH` 读取就会命中 `BlockTooOld`。 +//! +//! ## 两条不变式 +//! +//! ① **exec-time 一致性**:`BLOCKHASH` opcode 在 mined-tx 内部读到的值, +//! 串行(revm `WrapExecutor`) ⟷ grevm(`GrevmExecutor`) 必须逐字节一致,且等于喂入的 +//! 规范 block hash。这里用「探针合约把 `BLOCKHASH` 结果 `SSTORE` 进自身存储、再从 +//! `BundleState` 读该 slot」的方式取 **exec-time** 值——不走 RPC / canonical-header 视图 +//! (那是错误的预言机,读的是链上视图而非执行器实际喂给 EVM 的值)。 +//! +//! ② **截断窗口失败行为**:warm-restart 供给 <256 对时,最老 in-window 读取应命中 +//! `BlockTooOld`。存储层直接返回 `Err`(**不 panic**);panic 发生在上游 +//! `lib.rs:1354 / 1411` 的 `executor.execute(&block).unwrap_or_else(|err| panic!(...))`。 +//! +//! ## 可驱动性(feasibility,见模块尾测试) +//! +//! - 不变式①:**完全可驱动**——`CacheDB` 里 seed `block_hashes`,跑真实 串行 / grevm +//! 两个后端,对比探针 slot。 +//! - 不变式②的**存储层** `BlockTooOld`:**完全可驱动**——用**生产结构体** `RawBlockViewProvider` + +//! 截断 `BTreeMap` 直接 `block_hash_ref`,捕获 `Err`。 +//! - 不变式②的**完整 exec→panic 链**(opcode 命中 `BlockTooOld` → `host.block_hash` 返回 `None` → +//! 解释器 `FatalExternalError` halt → `executor.execute` 返回 `Err` → `lib.rs:1354` panic):本 +//! harness 用一个**忠实复刻** `block_hash_ref` 窗口判定的 `DatabaseRef` shim 在**进程内**驱动 +//! opcode→halt→execute-`Err` 这段;但把 shim 换成 **真实** +//! `RawBlockViewProvider`/`BlockViewProvider`(需真实 reth DB `TX` 后端)并在 warm-restart +//! 时用**截断的** `block_number_to_id` 种子驱动,需要 `pipe_test` / gnode 集群 harness——本单元 +//! harness 不具备真实 provider,故那一段**如实记为**需集群复现, 绝不伪造。 + +use alloy_consensus::{ + constants::{EMPTY_OMMER_ROOT_HASH, EMPTY_WITHDRAWALS, KECCAK_EMPTY}, + Header, TxLegacy, +}; +use alloy_eips::{eip4895::Withdrawals, eip7685::EMPTY_REQUESTS_HASH, merge::BEACON_NONCE}; +use alloy_primitives::{Address, Bytes, Signature, TxKind, B256, U256}; +use reth_chainspec::{ChainSpec, ChainSpecBuilder, MAINNET}; +use reth_ethereum_primitives::{Block, BlockBody, Transaction, TransactionSigned}; +use reth_evm::{execute::BasicBlockExecutor, parallel_execute::ParallelExecutor}; +use reth_evm_ethereum::{parallel_execute::GrevmExecutor, EthEvmConfig}; +use reth_primitives_traits::RecoveredBlock; +use revm::{ + database::{BundleState, CacheDB, EmptyDB}, + state::{AccountInfo, Bytecode}, +}; +use std::{collections::BTreeMap, sync::Arc}; + +// 复用 difftx.rs 的 `SeedAccount`(避免两套种子类型)。 +use crate::difftx::SeedAccount; + +// ————————————————————————————————— 常量 ————————————————————————————————— // + +/// 区块时间戳取 0:`prague_activated()` 让 Prague 从创世(ts=0)激活,`evm_env` 解析 +/// 出 `SpecId::PRAGUE`(与 `difftx_exec.rs` 取值理由一致)。 +const BLOCK_TIMESTAMP: u64 = 0; + +/// 充裕余额(100 ETH)——保证「余额不是被测因素」。 +const HUGE_BALANCE: u128 = 100_000_000_000_000_000_000; + +/// 探针合约地址。 +const PROBE_ADDR: Address = Address::with_last_byte(0xBB); + +/// 探针合约字节码:`SSTORE(CALLDATALOAD(0), BLOCKHASH(CALLDATALOAD(0)))`。 +/// 即「读 calldata 前 32 字节当目标块号 `n`,取 `BLOCKHASH(n)`,把结果存到 slot `n`」。 +/// +/// 反汇编: +/// ```text +/// 60 00 PUSH1 0x00 ; calldata offset +/// 35 CALLDATALOAD ; [n] +/// 80 DUP1 ; [n, n] +/// 40 BLOCKHASH ; [n, hash] (消费栈顶 n) +/// 90 SWAP1 ; [hash, n] (栈顶=n=key) +/// 55 SSTORE ; storage[n] = hash +/// 00 STOP +/// ``` +const PROBE_CODE: &[u8] = &[0x60, 0x00, 0x35, 0x80, 0x40, 0x90, 0x55, 0x00]; + +// ————————————————————————————————— chainspec ————————————————————————————————— // + +/// Prague 从创世激活的 chainspec(与 `difftx_exec.rs` 的 `prague_chain_spec` 同构)。 +fn prague_chain_spec() -> Arc { + Arc::new(ChainSpecBuilder::from(&*MAINNET).prague_activated().build()) +} + +// ————————————————————————————————— 种子构造 ————————————————————————————————— // + +/// EOA 种子:空代码。 +fn eoa(addr: Address, balance: u128, nonce: u64) -> SeedAccount { + SeedAccount { + addr, + info: AccountInfo { + balance: U256::from(balance), + nonce, + code_hash: KECCAK_EMPTY, + code: None, + }, + } +} + +/// 带真实字节码的合约账户种子。 +fn contract(addr: Address, code_bytes: &'static [u8], balance: u128) -> SeedAccount { + let code = Bytecode::new_raw(Bytes::from_static(code_bytes)); + SeedAccount { + addr, + info: AccountInfo { + balance: U256::from(balance), + nonce: 1, + code_hash: code.hash_slow(), + code: Some(code), + }, + } +} + +/// 由一份 seed + 一份 `block_hashes`(number→hash)构造 `CacheDB`。 +/// 两侧 clone 同一份,保证前置状态逐字节一致。 +fn seeded_cache(seed: &[SeedAccount], block_hashes: &BTreeMap) -> CacheDB { + let mut cache: CacheDB = CacheDB::new(EmptyDB::default()); + for s in seed { + cache.insert_account_info(s.addr, s.info.clone()); + } + for (&number, &hash) in block_hashes { + cache.cache.block_hashes.insert(U256::from(number), hash); + } + cache +} + +// ————————————————————————————————— 后端构造 ————————————————————————————————— // + +type SerialExecutor = reth_evm::parallel_execute::WrapExecutor< + CacheDB, + BasicBlockExecutor>, +>; + +fn fresh_serial_executor(chain_spec: Arc, db: CacheDB) -> SerialExecutor { + let evm_config = EthEvmConfig::new(chain_spec); + reth_evm::parallel_execute::WrapExecutor::new(BasicBlockExecutor::new(evm_config, db)) +} + +fn fresh_grevm_executor( + chain_spec: Arc, + db: CacheDB, +) -> GrevmExecutor, EthEvmConfig, ChainSpec> { + let evm_config = EthEvmConfig::new(chain_spec.clone()); + GrevmExecutor::new(chain_spec, &evm_config, db) +} + +// ————————————————————————————————— 区块 / 交易构造 ————————————————————————————————— // + +/// legacy「调用探针合约」交易:`input = 32B big-endian 目标块号`。 +fn probe_call_tx(nonce: u64, target: u64) -> TransactionSigned { + let mut input = [0u8; 32]; + input[24..32].copy_from_slice(&target.to_be_bytes()); + TransactionSigned::new_unhashed( + Transaction::Legacy(TxLegacy { + nonce, + gas_price: 10, + gas_limit: 100_000, + to: TxKind::Call(PROBE_ADDR), + value: U256::ZERO, + input: Bytes::from(input.to_vec()), + ..Default::default() + }), + Signature::test_signature(), + ) +} + +/// 构造一个 Prague-shape 区块,块号 = `number`,含 `txs` / `senders`。 +/// Header 字段镜像 `difftx_exec.rs::build_block` 的 Prague 分支。 +fn build_block( + number: u64, + txs: Vec, + senders: Vec
, +) -> RecoveredBlock { + assert_eq!(txs.len(), senders.len(), "txs 与 senders 必须等长"); + let header = Header { + parent_hash: B256::ZERO, + beneficiary: Address::ZERO, + number, + timestamp: BLOCK_TIMESTAMP, + gas_limit: 30_000_000, + base_fee_per_gas: Some(1), + mix_hash: B256::ZERO, + nonce: BEACON_NONCE.into(), + ommers_hash: EMPTY_OMMER_ROOT_HASH, + withdrawals_root: Some(EMPTY_WITHDRAWALS), + excess_blob_gas: Some(0), + blob_gas_used: Some(0), + parent_beacon_block_root: Some(B256::ZERO), + requests_hash: Some(EMPTY_REQUESTS_HASH), + ..Default::default() + }; + let body = BlockBody { + transactions: txs, + ommers: Vec::new(), + withdrawals: Some(Withdrawals::default()), + }; + RecoveredBlock::new_unhashed(Block { header, body }, senders) +} + +// ————————————————————————————————— 读 exec-time slot ————————————————————————————————— // + +/// 从 `BundleState` 读探针合约 slot `n` 的 `present_value`,转成 `B256`。 +/// `None` = 探针未写该 slot(即 `BLOCKHASH` 返回 0,SSTORE 写 0 被 revm 裁剪 / 未写)。 +fn read_probe_slot(bundle: &BundleState, slot: u64) -> Option { + bundle + .state + .get(&PROBE_ADDR) + .and_then(|acc| acc.storage.get(&U256::from(slot))) + .map(|s| B256::from(s.present_value.to_be_bytes::<32>())) +} + +/// 确定性伪 hash(喂给 `block_hashes` 的规范值)。 +fn fake_hash(number: u64) -> B256 { + let mut b = [0u8; 32]; + b[0] = 0x9a; + b[24..32].copy_from_slice(&number.to_be_bytes()); + B256::from(b) +} + +// ————————————————————————————————— exec-time 一致性核心 ————————————————————————————————— // + +/// 一次 exec-time `BLOCKHASH` 一致性观测结果(单个目标高度)。 +#[derive(Debug, Clone)] +struct BlockhashObs { + target: u64, + serial: Option, + grevm: Option, + expected: Option, +} + +/// 在块号 `number` 处,对每个 `targets[i]` 发一笔探针交易读 `BLOCKHASH(target)` 并存进 +/// slot=target。两侧真跑串行 / grevm,返回逐目标的观测。 +/// +/// `expected`:喂入 `CacheDB.block_hashes` 的 number→hash(也是不变式①的期望值); +/// 未出现在 `expected` 里的 target 表示「预期 EVM 返回 0」(如 diff==0 或 >256)。 +fn run_blockhash_case( + number: u64, + targets: &[u64], + expected: &BTreeMap, +) -> Vec { + // 每个 target 一个独立 sender(nonce=0),便于 grevm 并行;探针合约一份。 + let mut seed = vec![contract(PROBE_ADDR, PROBE_CODE, 0)]; + let mut txs = Vec::new(); + let mut senders = Vec::new(); + for (i, &t) in targets.iter().enumerate() { + let sender = Address::with_last_byte(0x40 + i as u8); + seed.push(eoa(sender, HUGE_BALANCE, 0)); + txs.push(probe_call_tx(0, t)); + senders.push(sender); + } + + let chain_spec = prague_chain_spec(); + let block = build_block(number, txs, senders); + let cache = seeded_cache(&seed, expected); + + let mut serial = fresh_serial_executor(chain_spec.clone(), cache.clone()); + let mut grevm = fresh_grevm_executor(chain_spec, cache); + + let so = serial.execute(&block).expect("串行执行整块应成功(BLOCKHASH 命中已 seed 的窗口)"); + let go = grevm.execute(&block).expect("grevm 执行整块应成功"); + + targets + .iter() + .map(|&t| BlockhashObs { + target: t, + serial: read_probe_slot(&so.state, t), + grevm: read_probe_slot(&go.state, t), + expected: expected.get(&t).copied(), + }) + .collect() +} + +// ————————————————————————————————— 测试 ————————————————————————————————— // + +#[cfg(all(test, feature = "difftx_2935"))] +mod tests { + use super::*; + use gravity_storage::block_view_storage::{BlockHashError, RawBlockViewProvider}; + use reth_db::mock::TxMock; + use reth_revm::DatabaseRef as RethDatabaseRef; + + /// 承重的 **exec-time 一致性** 测试(不变式①)。 + /// + /// 在块号 `N=300` 处,对多个 in-window 高度发探针交易读 `BLOCKHASH`,断言: + /// (a) 串行 == grevm(逐字节);(b) 二者都 == 喂入的规范 hash。 + /// 覆盖窗口边界:`N-1`(最新可读)、`N-2`、`N-128`(中段)、`N-256`(最老 in-window)。 + /// + /// 这证明该差分预言机确实把探针块过了**两个后端**、且 exec-time `BLOCKHASH` 一致—— + /// 因此任何 serial⟷grevm 的 `BLOCKHASH` 背离都会被抓住。 + #[test] + fn exec_time_blockhash_serial_equals_grevm_and_expected() { + let n: u64 = 300; + let in_window = [n - 1, n - 2, n - 128, n - 256]; + let mut expected = BTreeMap::new(); + for &t in &in_window { + expected.insert(t, fake_hash(t)); + } + + let obs = run_blockhash_case(n, &in_window, &expected); + assert_eq!(obs.len(), in_window.len()); + for o in &obs { + println!( + "[exec-time BLOCKHASH] N={n} target={} serial={:?} grevm={:?} expected={:?}", + o.target, o.serial, o.grevm, o.expected + ); + // (a) serial == grevm(共识安全的核心要求)。 + assert_eq!( + o.serial, o.grevm, + "target={} 的 exec-time BLOCKHASH 串行⟷grevm 背离(疑似共识 fork)", + o.target + ); + // (b) == 喂入的规范 hash。 + assert_eq!( + o.serial, o.expected, + "target={} 的 exec-time BLOCKHASH 与喂入的规范 hash 不符", + o.target + ); + assert!(o.serial.is_some(), "target={} 应读到非零 BLOCKHASH", o.target); + } + } + + /// 边界 sanity(不变式①的补充): + /// - `BLOCKHASH(N)`(diff==0,当前块)→ revm 返回 0(`host.rs:169`)。 + /// - `BLOCKHASH(N-257)`(diff>256,超窗)→ revm 返回 0(`host.rs:183`)。 + /// + /// 两侧都应「未写 slot / 写 0」,且串行==grevm。用于确认预言机对 revm 的 + /// BLOCKHASH 窗口语义把握正确(否则不变式①的正例也不可信)。 + #[test] + fn exec_time_blockhash_out_of_window_is_zero_both_backends() { + let n: u64 = 300; + let targets = [n, n - 257]; // diff==0 与 diff>256 + let expected = BTreeMap::new(); // 期望都为 0(不 seed) + + let obs = run_blockhash_case(n, &targets, &expected); + for o in &obs { + println!( + "[exec-time BLOCKHASH oob] N={n} target={} serial={:?} grevm={:?}", + o.target, o.serial, o.grevm + ); + assert_eq!(o.serial, o.grevm, "target={} 超窗读取串行⟷grevm 背离", o.target); + // SSTORE 0 会被 revm 裁剪(present==original==0),故 slot 不出现或为 0。 + assert!( + o.serial.is_none() || o.serial == Some(B256::ZERO), + "target={} 超窗应读到 0,实得 {:?}", + o.target, + o.serial + ); + } + } + + /// **截断窗口 `BlockTooOld`(存储层,真实证据)**(不变式②)。 + /// + /// 用**生产结构体** `RawBlockViewProvider`(`block_view_storage/mod.rs`)+ 一份**截断** + /// 的 `block_number_to_id`(只 51 对,<256)复现 warm-restart 场景: + /// - 环 = `{250..=300}`(51 对,模拟 checkpoint-resume 只供给最近 51 块)。 + /// - `BLOCKHASH(44)`(在合法 256 窗内:`300-256=44`,但**不在**截断环内)→ `BlockTooOld`。 + /// + /// 存储层**直接返回 `Err`(不 panic)**;这里用 `Result` 捕获并断言 `BlockTooOld` 变体, + /// **在真实证据上确认**了 red-team 记录的前置条件(<256 对 → 最老 in-window 读 too-old)。 + /// 生产里这条 `Err` 会沿 `host.block_hash → None → FatalExternalError halt → + /// executor.execute 返回 Err → lib.rs:1354 panic!` 上抛(见下一测试的进程内后果链)。 + #[test] + fn truncated_window_blocktooold_storage_level() { + // 截断环:只 51 对(<256),模拟 warm-restart 供给不足。 + let mut window: BTreeMap = BTreeMap::new(); + for number in 250u64..=300 { + window.insert(number, fake_hash(number)); + } + assert!(window.len() < 256, "构造前提:窗口必须 <256 对"); + let map = Arc::new(std::sync::Mutex::new(window)); + + // 生产结构体,真实 block_hash_ref 路径。 + let provider = RawBlockViewProvider::new(TxMock::default(), None, map); + + // (1) 截断环内最老命中 → Ok。 + assert_eq!( + RethDatabaseRef::block_hash_ref(&provider, 250).unwrap(), + fake_hash(250), + "截断环内应命中" + ); + + // (2) 合法 256 窗内、但落在截断环之下(300-256=44 < 250) → BlockTooOld。 + let oldest_in_256 = 300u64 - 256; // = 44 + let err = RethDatabaseRef::block_hash_ref(&provider, oldest_in_256) + .expect_err("截断窗口下最老 in-256 读取必须 Err(BlockTooOld)"); + let downcast = err.downcast_other_ref::().expect("Err 应为 BlockHashError"); + println!("[truncated-window 存储层] BLOCKHASH({oldest_in_256}) => {downcast}"); + assert!( + matches!(downcast, BlockHashError::BlockTooOld(n) if *n == oldest_in_256), + "预期 BlockTooOld({oldest_in_256}),实得 {downcast:?}" + ); + + // (3) 对照:> max 仍是 BlockTooHigh(证明判定分支正确、not 一刀切)。 + let too_high = RethDatabaseRef::block_hash_ref(&provider, 301) + .expect_err("超过 max 应 Err(BlockTooHigh)"); + assert!( + matches!( + too_high.downcast_other_ref::().unwrap(), + BlockHashError::BlockTooHigh(301) + ), + "预期 BlockTooHigh(301)" + ); + } + + /// **exec-time halt 后果链(进程内,复刻件驱动)**(不变式②的后果)。 + /// + /// 用一个 `DatabaseRef`(`TruncatedWindowErrDb`,忠实复刻 `block_hash_ref` 的窗口判定并 + /// 在「截断环内缺失」时返回 `Err`)作为执行器后端,发一笔读 `BLOCKHASH(too_old)` 的探针 + /// 交易,断言 `executor.execute(&block)` **返回 `Err`**(即生产 `lib.rs:1354` + /// `unwrap_or_else(|err| panic!(...))` 会 panic 的那个 `Err`)。 + /// + /// 这证明「截断窗口 → `BlockTooOld` → opcode halt → 整块执行失败」的后果链在真实 + /// 执行器上成立。**边界**:此处用复刻 `DatabaseRef`(真实 `RawBlockViewProvider` 需真实 + /// reth DB `TX`,不在本单元 harness 内可得);把它换成真实 provider 并用截断 + /// `block_number_to_id` 种子驱动,需 `pipe_test` / gnode 集群 warm-restart——见模块头。 + #[test] + fn truncated_window_exec_halt_returns_err() { + use revm::database::DBErrorMarker; + + /// 复刻件:截断环内缺失 → `Err`(可触发 opcode halt)。 + #[derive(Debug)] + struct TruncatedWindowErrDb { + window: BTreeMap, + } + + /// 与 `BlockHashError` 同形的、可进 revm/reth 错误通道的错误。 + #[derive(Debug)] + struct TooOld(u64); + impl std::fmt::Display for TooOld { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "block {} too old (truncated window replica)", self.0) + } + } + impl std::error::Error for TooOld {} + impl DBErrorMarker for TooOld {} + + impl revm::DatabaseRef for TruncatedWindowErrDb { + type Error = TooOld; + fn basic_ref(&self, address: Address) -> Result, Self::Error> { + Ok(EmptyDB::default().basic_ref(address).unwrap()) + } + fn code_by_hash_ref(&self, code_hash: B256) -> Result { + Ok(EmptyDB::default().code_by_hash_ref(code_hash).unwrap()) + } + fn storage_ref(&self, address: Address, index: U256) -> Result { + Ok(EmptyDB::default().storage_ref(address, index).unwrap()) + } + /// 复刻 `block_view_storage.rs`:191-202 判定:环内命中 → Ok;缺失 → `BlockTooOld` → Err。 + fn block_hash_ref(&self, number: u64) -> Result { + match self.window.get(&number) { + Some(h) => Ok(*h), + None => Err(TooOld(number)), + } + } + } + + // 截断环:只 51 对(<256)。 + let mut window: BTreeMap = BTreeMap::new(); + for number in 250u64..=300 { + window.insert(number, fake_hash(number)); + } + + // 用 CacheDB 包裹截断 DB,上层 seed 探针合约 + 一个 sender。 + let ext = TruncatedWindowErrDb { window }; + let mut cache: CacheDB = CacheDB::new(ext); + cache.insert_account_info(PROBE_ADDR, contract(PROBE_ADDR, PROBE_CODE, 0).info); + let sender = Address::with_last_byte(0x77); + cache.insert_account_info(sender, eoa(sender, HUGE_BALANCE, 0).info); + + let chain_spec = prague_chain_spec(); + // 读 BLOCKHASH(44):合法 256 窗内(300-256=44)但不在截断环 → too old → halt。 + let block = build_block(300, vec![probe_call_tx(0, 44)], vec![sender]); + + let evm_config = EthEvmConfig::new(chain_spec); + let executor: BasicBlockExecutor> = + BasicBlockExecutor::new(evm_config, cache); + + // `Executor::execute` 消费 self(见 evm/execute.rs:66)。 + let res = reth_evm::execute::Executor::execute(executor, &block); + println!("[truncated-window exec halt] executor.execute => is_err={}", res.is_err()); + assert!( + res.is_err(), + "截断窗口下读最老 in-256 BLOCKHASH,整块执行应失败(FatalExternalError)——\ + 生产中即 lib.rs:1354 的 panic!" + ); + } +} diff --git a/crates/pipe-exec-layer-ext-v2/execute/src/difftx_precompile.rs b/crates/pipe-exec-layer-ext-v2/execute/src/difftx_precompile.rs new file mode 100644 index 000000000..a89279643 --- /dev/null +++ b/crates/pipe-exec-layer-ext-v2/execute/src/difftx_precompile.rs @@ -0,0 +1,648 @@ +//! Gravity 自定义 precompile 预言机(differential / invariant oracle)。 +//! +//! ## 目标面 +//! +//! - `mint_precompile.rs`(`NATIVE_MINT_PRECOMPILE_ADDR`):只允许 `AUTHORIZED_CALLER` 调用,直接改 +//! EVM journal 余额铸币(`load_account(recipient).balance += amount`)。 +//! - `randomness_precompile.rs` / `gravity_precompiles::randomness_by_height` +//! (`RANDOMNESS_BY_HEIGHT_PRECOMPILE_ADDR`):按高度只读取随机数。 +//! +//! 这些是 Gravity 自研、审计较少、且**直接改状态**的面。 +//! +//! ## 不变式(invariant) +//! +//! 1. **caller 门**——非授权 caller 必须被干净拒绝(`Err`,不 panic、不铸币);授权 caller + +//! 合法输入必须 `Ok`(真铸币)。 +//! 2. **畸形输入永不 panic**——空/短/超长/错 func-id/gas 不足/amount=MAX 全部返回干净 `Err` +//! (或合法时 `Ok`),绝不 panic(precompile panic = 全网停链)。 +//! 3. **应用自定义 precompile 后 serial ⟷ grevm 执行逐字节一致**——mint 直改 journal state, 若 +//! grevm 未把该「precompile 直写」当作读写依赖追踪,后续读该余额的 tx 会看到陈旧状态 → +//! 与串行背离 = 一次共识 state-fork。 +//! +//! 生产接线参照 `lib.rs:955-960`(组 `(NATIVE_MINT_PRECOMPILE_ADDR, +//! create_mint_token_precompile())`) 与 `lib.rs:1257-1260`(`executor.apply_custom_precompiles(... +//! )`);执行 harness 参照 `difftx_exec.rs`。 +//! +//! ```text +//! cargo test -p reth-pipe-exec-layer-ext-v2 --features difftx_precompile --lib difftx_precompile -- --nocapture +//! ``` + +use alloy_consensus::{constants::KECCAK_EMPTY, Header, TxLegacy}; +use alloy_eips::{eip4895::Withdrawals, eip7685::EMPTY_REQUESTS_HASH, merge::BEACON_NONCE}; +use alloy_primitives::{address, Address, Bytes, Signature, TxKind, B256, U256}; +use reth_chainspec::{ChainSpec, ChainSpecBuilder, MAINNET}; +use reth_ethereum_primitives::{Block, BlockBody, Receipt, Transaction, TransactionSigned}; +use reth_evm::{ + execute::BasicBlockExecutor, + parallel_execute::{ParallelExecutor, WrapExecutor}, + precompiles::{DynPrecompile, PrecompileInput}, +}; +use reth_evm_ethereum::{parallel_execute::GrevmExecutor, EthEvmConfig}; +use reth_primitives_traits::RecoveredBlock; +use revm::{ + database::{CacheDB, EmptyDB}, + precompile::{PrecompileError, PrecompileResult}, + state::{AccountInfo, Bytecode}, +}; +use std::sync::Arc; + +use crate::{ + difftx::SeedAccount, + mint_precompile::{create_mint_token_precompile, AUTHORIZED_CALLER}, + onchain_config::NATIVE_MINT_PRECOMPILE_ADDR, +}; + +// mint_precompile.rs 里 `FUNC_MINT` / `MINT_BASE_GAS` +// 是私有常量,这里镜像其取值(与目标面严格对齐)。 +/// mint 函数 id(对应 `mint_precompile::FUNC_MINT`)。 +const FUNC_MINT: u8 = 0x01; +/// mint 基础 gas(对应 `mint_precompile::MINT_BASE_GAS`)。 +const MINT_BASE_GAS: u64 = 21_000; +/// mint 精确输入长度:1(func) + 20(recipient) + 32(amount)。 +const MINT_INPUT_LEN: usize = 1 + 20 + 32; + +// —————————————————————————————— 通用小工具 —————————————————————————————— // + +/// 确定性地址:`0x0000…00{b}`。 +const fn addr(b: u8) -> Address { + Address::with_last_byte(b) +} + +/// 构造 mint precompile 的合法 calldata:`[func_id | recipient(20) | amount(32-be)]`。 +fn mint_calldata(recipient: Address, amount: U256) -> [u8; MINT_INPUT_LEN] { + let mut data = [0u8; MINT_INPUT_LEN]; + data[0] = FUNC_MINT; + data[1..21].copy_from_slice(recipient.as_slice()); + data[21..].copy_from_slice(&amount.to_be_bytes::<32>()); + data +} + +// ============================================================================ +// 第 1 部分:precompile 级不变式测试(直接 .call,caller 门 + 畸形输入不 panic) +// ============================================================================ + +#[cfg(all(test, feature = "difftx_precompile"))] +mod precompile_level { + use super::*; + use gravity_precompiles::randomness_by_height::{ + create_randomness_by_height_precompile, RandomnessByHeightLookup, + RandomnessByHeightProvider, RANDOMNESS_BY_HEIGHT_INPUT_LEN, + RANDOMNESS_BY_HEIGHT_OUTPUT_LEN, + }; + use reth_evm::{eth::EthEvmContext, precompiles::Precompile, EvmInternals}; + use revm::{context::JournalTr, database::EmptyDB}; + + /// 用给定 `caller` / `data` / `gas` 直接调用 mint precompile;`ctx` 由调用方持有, + /// 以便调用后检查 recipient 余额是否真被改。返回结果(result 为 owned,不借用 ctx)。 + fn call_mint( + ctx: &mut EthEvmContext, + caller: Address, + data: &[u8], + gas: u64, + ) -> PrecompileResult { + create_mint_token_precompile().call(PrecompileInput { + data, + gas, + caller, + value: U256::ZERO, + target_address: NATIVE_MINT_PRECOMPILE_ADDR, + bytecode_address: NATIVE_MINT_PRECOMPILE_ADDR, + internals: EvmInternals::new(&mut ctx.journaled_state, &ctx.block), + }) + } + + fn fresh_ctx() -> EthEvmContext { + EthEvmContext::new(EmptyDB::default(), Default::default()) + } + + fn is_unauthorized(res: &PrecompileResult) -> bool { + matches!(res, Err(PrecompileError::Other(msg)) if msg.contains("Unauthorized")) + } + + // ————————————————————————— caller 门 ————————————————————————— // + + /// 非授权 caller → 干净 `Err(Unauthorized)`,不 panic,且**未铸币**(recipient + /// 仍不存在/零余额)。 + #[test] + fn unauthorized_caller_is_rejected_and_mints_nothing() { + let recipient = addr(0xCC); + let data = mint_calldata(recipient, U256::from(1_000u64)); + + // 试遍若干非授权 caller:零地址、随机地址、以及「差一位」的近似地址。 + let impostors = [ + Address::ZERO, + addr(0x01), + address!("0x595475934ed7d9faa7fca28341c2ce583904a44f"), // 末位 e→f + ]; + for caller in impostors { + assert_ne!(caller, AUTHORIZED_CALLER, "构造前提:impostor 必须≠授权地址"); + let mut ctx = fresh_ctx(); + let res = call_mint(&mut ctx, caller, &data, MINT_BASE_GAS); + assert!(is_unauthorized(&res), "非授权 caller {caller} 应被拒:{res:?}"); + + // 未铸币:recipient 账户余额必须为零(load_account 会惰性建账,但余额应为 0)。 + let bal = ctx.journaled_state.load_account(recipient).unwrap().data.info.balance; + assert_eq!(bal, U256::ZERO, "被拒的调用不得改动 recipient 余额"); + } + } + + /// 授权 caller + 合法输入 → `Ok`,且 recipient 余额精确 += amount,账户被 touch。 + #[test] + fn authorized_caller_mints_exactly() { + let recipient = addr(0xCC); + let amount = U256::from(123_456_789u64); + let data = mint_calldata(recipient, amount); + let mut ctx = fresh_ctx(); + + let out = call_mint(&mut ctx, AUTHORIZED_CALLER, &data, MINT_BASE_GAS) + .expect("授权 caller + 合法输入必须 Ok"); + assert_eq!(out.gas_used, MINT_BASE_GAS, "mint 应恰好计 MINT_BASE_GAS"); + + let acct = ctx.journaled_state.load_account(recipient).unwrap().data; + assert_eq!(acct.info.balance, amount, "recipient 余额应精确等于 mint 数额"); + assert!(acct.is_touched(), "mint 后 recipient 必须被 mark_touch"); + } + + // ————————————————————————— 畸形输入永不 panic ————————————————————————— // + + /// 一批畸形/边界输入:**每个都必须返回 `Err` 而非 panic**。用授权 caller 以穿过 caller 门、 + /// 真正压到长度/func-id/amount 解析路径(gas 不足用例除外——它在最前置被拦)。 + #[test] + fn malformed_input_never_panics() { + let recipient = addr(0xCC); + let good = mint_calldata(recipient, U256::from(7u64)); + + // (名称, caller, data, gas) → 期望 Err。 + let mut over_long = good.to_vec(); + over_long.extend_from_slice(&[0u8; 16]); // 69 bytes,超长 + + let max_amount = mint_calldata(recipient, U256::MAX); // amount 溢出 u128 → Err + + let cases: Vec<(&str, Address, Vec, u64)> = vec![ + ("empty", AUTHORIZED_CALLER, vec![], MINT_BASE_GAS), + ( + "wrong_func_id", + AUTHORIZED_CALLER, + { + let mut d = good.to_vec(); + d[0] = 0x02; + d + }, + MINT_BASE_GAS, + ), + ("truncated_1byte", AUTHORIZED_CALLER, vec![FUNC_MINT], MINT_BASE_GAS), + ("truncated_20bytes", AUTHORIZED_CALLER, good[..20].to_vec(), MINT_BASE_GAS), + ("truncated_52bytes", AUTHORIZED_CALLER, good[..52].to_vec(), MINT_BASE_GAS), + ("over_long", AUTHORIZED_CALLER, over_long, MINT_BASE_GAS), + ("gas_below_base", AUTHORIZED_CALLER, good.to_vec(), MINT_BASE_GAS - 1), + ("gas_zero", AUTHORIZED_CALLER, good.to_vec(), 0), + ("amount_u256_max", AUTHORIZED_CALLER, max_amount.to_vec(), MINT_BASE_GAS), + ( + "amount_zero", + AUTHORIZED_CALLER, + mint_calldata(recipient, U256::ZERO).to_vec(), + MINT_BASE_GAS, + ), + // 非授权 + 畸形:应在 caller 门/gas 门被拒,同样不得 panic。 + ("unauth_empty", addr(0x09), vec![], MINT_BASE_GAS), + ("unauth_garbage", addr(0x09), vec![0xff; 200], MINT_BASE_GAS), + ]; + + for (name, caller, data, gas) in cases { + let mut ctx = fresh_ctx(); + // 若 handler panic,此处会直接 panic 使测试失败——即「不 panic」的证明。 + let res = call_mint(&mut ctx, caller, &data, gas); + assert!(res.is_err(), "[{name}] 畸形输入应返回 Err(不 panic、不铸币):{res:?}"); + } + } + + // ————————————————————————— randomness precompile(provider + // 可廉价构造)————————————————————————— + + /// 极简 mock provider:任意高度返回固定值,`Error = Infallible`(构造零成本)。 + struct MockRand { + value: Option, + } + impl RandomnessByHeightProvider for MockRand { + type Error = std::convert::Infallible; + fn randomness_by_height( + &self, + _height: u64, + ) -> Result { + Ok(RandomnessByHeightLookup::recent(self.value)) + } + } + + fn call_rand(precompile: &DynPrecompile, data: &[u8], gas: u64) -> PrecompileResult { + let mut ctx = fresh_ctx(); + precompile.call(PrecompileInput { + data, + gas, + caller: addr(0xAB), + value: U256::ZERO, + target_address: addr(0x02), + bytecode_address: addr(0x02), + internals: EvmInternals::new(&mut ctx.journaled_state, &ctx.block), + }) + } + + /// randomness:合法 32 字节输入 + 充裕 gas → `Ok`,输出恰好 64 字节; + /// 畸形长度 / gas 不足 → 干净 `Err`,全程不 panic。 + #[test] + fn randomness_gating_and_malformed_never_panic() { + let precompile = create_randomness_by_height_precompile(Arc::new(MockRand { + value: Some(B256::repeat_byte(0x7a)), + })); + + // 1) 合法:ABI 编码 uint256 height=1,gas 充裕。 + let mut height = [0u8; RANDOMNESS_BY_HEIGHT_INPUT_LEN]; + height[31] = 1; + let ok = call_rand(&precompile, &height, 1_000_000).expect("合法输入应 Ok"); + assert_eq!(ok.bytes.len(), RANDOMNESS_BY_HEIGHT_OUTPUT_LEN, "输出应为 64 字节"); + + // 2) height > u64::MAX(全 0xff)→ Ok(found=false),仍不 panic。 + let huge = [0xffu8; RANDOMNESS_BY_HEIGHT_INPUT_LEN]; + let res = call_rand(&precompile, &huge, 1_000_000); + assert!(res.is_ok(), "height 溢出 u64 应 Ok(found=false):{res:?}"); + + // 3) 畸形长度 / gas 不足 → Err,不 panic。 + let bad: Vec<(&str, Vec, u64)> = vec![ + ("empty", vec![], 1_000_000), + ("short_31", vec![0u8; 31], 1_000_000), + ("long_33", vec![0u8; 33], 1_000_000), + ("gas_zero", height.to_vec(), 0), + ("gas_below_recent", height.to_vec(), 1), // < RECENT_GAS(4000) + ]; + for (name, data, gas) in bad { + let res = call_rand(&precompile, &data, gas); + assert!(res.is_err(), "[rand:{name}] 应 Err(不 panic):{res:?}"); + } + } +} + +// ============================================================================ +// 第 2 部分:应用自定义 precompile 后的 serial ⟷ grevm 执行差分(fork hunt) +// ============================================================================ +// +// harness 镜像 `difftx_exec.rs`(seeded CacheDB → 两侧同源 clone → 各自 execute → 逐字段比对), +// 但在 execute 之前对**每一侧**调用 `apply_custom_precompiles([(mint_addr, mint())])` +// (镜像 `lib.rs:1257-1260`)。这是自定义 precompile 抵达执行器的**唯一**通道(不在 EthEvmConfig +// 里)。 + +/// Prague-from-genesis `chainspec`(与 `difftx_exec` 同构)。 +fn prague_chain_spec() -> Arc { + Arc::new(ChainSpecBuilder::from(&*MAINNET).prague_activated().build()) +} + +/// 由 seed 构造 `CacheDB`(两侧 clone 同一份,保证前置状态逐字节一致)。 +fn seeded_cache(seed: &[SeedAccount]) -> CacheDB { + let mut cache: CacheDB = CacheDB::new(EmptyDB::default()); + for s in seed { + cache.insert_account_info(s.addr, s.info.clone()); + } + cache +} + +type SerialExecutor = + WrapExecutor, BasicBlockExecutor>>; + +fn fresh_serial_executor(chain_spec: Arc, db: CacheDB) -> SerialExecutor { + let evm_config = EthEvmConfig::new(chain_spec); + WrapExecutor::new(BasicBlockExecutor::new(evm_config, db)) +} + +fn fresh_grevm_executor( + chain_spec: Arc, + db: CacheDB, +) -> GrevmExecutor, EthEvmConfig, ChainSpec> { + let evm_config = EthEvmConfig::new(chain_spec.clone()); + GrevmExecutor::new(chain_spec, &evm_config, db) +} + +/// 每次返回一份**新的** mint 自定义 precompile 注册表(两侧各持一份 Arc,不共享实例)。 +fn mint_custom_precompiles() -> Arc> { + Arc::new(vec![(NATIVE_MINT_PRECOMPILE_ADDR, create_mint_token_precompile())]) +} + +/// 构造 Prague-shape 区块(镜像 `difftx_exec::build_block`)。 +fn build_block(txs: Vec, senders: Vec
) -> RecoveredBlock { + assert_eq!(txs.len(), senders.len(), "txs 与 senders 必须等长"); + let header = Header { + parent_hash: B256::ZERO, + beneficiary: Address::ZERO, + number: 1, + timestamp: 0, + gas_limit: 30_000_000, + base_fee_per_gas: Some(1), + mix_hash: B256::ZERO, + nonce: BEACON_NONCE.into(), + ommers_hash: alloy_consensus::constants::EMPTY_OMMER_ROOT_HASH, + withdrawals_root: Some(alloy_consensus::constants::EMPTY_WITHDRAWALS), + excess_blob_gas: Some(0), + blob_gas_used: Some(0), + parent_beacon_block_root: Some(B256::ZERO), + requests_hash: Some(EMPTY_REQUESTS_HASH), + ..Default::default() + }; + let body = BlockBody { + transactions: txs, + ommers: Vec::new(), + withdrawals: Some(Withdrawals::default()), + }; + RecoveredBlock::new_unhashed(Block { header, body }, senders) +} + +/// legacy 交易(可带 input,用于 CALL 合约传 calldata)。 +fn legacy_tx( + nonce: u64, + gas_limit: u64, + gas_price: u128, + value: u128, + to: TxKind, + input: Bytes, +) -> TransactionSigned { + TransactionSigned::new_unhashed( + Transaction::Legacy(TxLegacy { + nonce, + gas_price, + gas_limit, + to, + value: U256::from(value), + input, + ..Default::default() + }), + Signature::test_signature(), + ) +} + +/// EOA 种子(空代码)。 +fn eoa(addr: Address, balance: u128, nonce: u64) -> SeedAccount { + SeedAccount { + addr, + info: AccountInfo { + balance: U256::from(balance), + nonce, + code_hash: KECCAK_EMPTY, + code: None, + }, + } +} + +/// 合约种子(运行时字节码)。 +fn contract(addr: Address, code_bytes: Vec, balance: u128) -> SeedAccount { + let code = Bytecode::new_raw(Bytes::from(code_bytes)); + SeedAccount { + addr, + info: AccountInfo { + balance: U256::from(balance), + nonce: 1, + code_hash: code.hash_slow(), + code: Some(code), + }, + } +} + +/// 「授权铸币者」运行时字节码:把 tx calldata(53 字节 mint 输入)拷进内存后 `CALL` mint +/// precompile。 因该字节码部署在 `AUTHORIZED_CALLER` 地址,CALL 时 precompile 看到的 `caller == +/// AUTHORIZED_CALLER`。 +/// +/// 汇编: +/// ```text +/// PUSH1 0x35 PUSH1 0x00 PUSH1 0x00 CALLDATACOPY ; mem[0..53] = calldata[0..53] +/// PUSH1 0 PUSH1 0 PUSH1 0x35 PUSH1 0 PUSH1 0 ; retLen retOff argsLen(53) argsOff value(0) +/// PUSH20 GAS CALL STOP ; CALL(gas, mint, 0, 0, 53, 0, 0) +/// ``` +fn mint_caller_bytecode() -> Vec { + let mut code = vec![ + 0x60, 0x35, 0x60, 0x00, 0x60, 0x00, 0x37, // PUSH1 53, PUSH1 0, PUSH1 0, CALLDATACOPY + 0x60, 0x00, // retLength = 0 + 0x60, 0x00, // retOffset = 0 + 0x60, 0x35, // argsLength = 53 + 0x60, 0x00, // argsOffset = 0 + 0x60, 0x00, // value = 0 + 0x73, // PUSH20 + ]; + code.extend_from_slice(NATIVE_MINT_PRECOMPILE_ADDR.as_slice()); + code.extend_from_slice(&[0x5a, 0xf1, 0x00]); // GAS, CALL, STOP + code +} + +/// 单个差分结果。 +struct PrecompileExecDiff { + serial_ok: bool, + grevm_ok: bool, + diverged: bool, + divergence: Option, + /// 便于断言的补充:recipient 在两侧的最终余额(若可取)。 + note: String, +} + +/// 逐字段比对两侧输出(`state`/`contracts`/`state_size` + `receipts` + +/// `gas_used`)。返回首个差异描述。 +fn diff_outputs( + serial: &reth_execution_types::BlockExecutionOutput, + grevm: &reth_execution_types::BlockExecutionOutput, +) -> Option { + if serial.state.state != grevm.state.state { + return Some("BundleState.state 背离(余额/nonce/code/storage 之一不同)".to_string()); + } + if serial.state.contracts != grevm.state.contracts { + return Some("BundleState.contracts 背离".to_string()); + } + if serial.state.state_size != grevm.state.state_size { + return Some(format!( + "BundleState.state_size 背离:serial={} grevm={}", + serial.state.state_size, grevm.state.state_size + )); + } + if serial.result.receipts != grevm.result.receipts { + return Some("receipts 背离".to_string()); + } + if serial.result.gas_used != grevm.result.gas_used { + return Some(format!( + "block gas_used 背离:serial={} grevm={}", + serial.result.gas_used, grevm.result.gas_used + )); + } + None +} + +/// 跑一次「应用 mint precompile → 两侧 execute → 比对」的差分。 +fn run_precompile_exec_diff( + seed: Vec, + txs: Vec, + senders: Vec
, + probe: Option
, +) -> PrecompileExecDiff { + let chain_spec = prague_chain_spec(); + let block = build_block(txs, senders); + let cache = seeded_cache(&seed); + + let mut serial = fresh_serial_executor(chain_spec.clone(), cache.clone()); + let mut grevm = fresh_grevm_executor(chain_spec, cache); + + // 关键步骤:在 execute 之前,对**每一侧**注册自定义 mint precompile(镜像 lib.rs:1257-1260)。 + serial.apply_custom_precompiles(mint_custom_precompiles()); + grevm.apply_custom_precompiles(mint_custom_precompiles()); + + let serial_res = serial.execute(&block); + let grevm_res = grevm.execute(&block); + + let serial_ok = serial_res.is_ok(); + let grevm_ok = grevm_res.is_ok(); + + let (diverged, divergence, note) = match (&serial_res, &grevm_res) { + (Ok(so), Ok(go)) => { + let note = probe + .map(|p| { + let sb = + so.state.state.get(&p).and_then(|a| a.info.as_ref()).map(|i| i.balance); + let gb = + go.state.state.get(&p).and_then(|a| a.info.as_ref()).map(|i| i.balance); + format!("probe {p} 最终余额: serial={sb:?} grevm={gb:?}") + }) + .unwrap_or_default(); + match diff_outputs(so, go) { + Some(d) => (true, Some(d), note), + None => (false, None, note), + } + } + (Ok(_), Err(e)) => { + (true, Some(format!("串行 Ok 而 grevm Err(halt gap):{e:?}")), String::new()) + } + (Err(e), Ok(_)) => { + (true, Some(format!("grevm Ok 而串行 Err(halt gap):{e:?}")), String::new()) + } + (Err(se), Err(ge)) => { + let ss = format!("{se:?}"); + let gs = format!("{ge:?}"); + if ss == gs { + (false, None, "两侧一致失败".to_string()) + } else { + ( + true, + Some(format!("两侧均失败但错误模式不同:serial={ss} grevm={gs}")), + String::new(), + ) + } + } + }; + + PrecompileExecDiff { serial_ok, grevm_ok, diverged, divergence, note } +} + +#[cfg(all(test, feature = "difftx_precompile"))] +mod exec_diff { + use super::*; + + /// **主战场(fork hunt)**:授权铸币者合约 CALL mint precompile 直改 recipient journal 余额, + /// 随后同块内 recipient 花掉这笔被铸的余额。预言机回答的问题: + /// **grevm 是否把 precompile 的「直写 journal」当成 recipient 的读写依赖, + /// 使得 tx1 看到的 recipient 余额与串行逐字节一致?** 否则即真共识 fork。 + #[test] + fn mint_precompile_journal_write_tracked_as_dependency() { + let funder = addr(0xA0); // tx0 sender(付 gas) + let recipient = addr(0xCC); // 被铸币者 = tx1 sender + let dest = addr(0xDD); // tx1 收款人 + let one_eth: u128 = 1_000_000_000_000_000_000; + let huge: u128 = 100 * one_eth; + + // tx0:funder → AUTHORIZED_CALLER(铸币合约),input = mint(recipient, 1 ETH)。 + // 合约 CALL mint precompile → recipient 余额 += 1 ETH(直写 journal)。 + let tx0 = legacy_tx( + 0, + 300_000, + 10, + 0, + TxKind::Call(AUTHORIZED_CALLER), + Bytes::from(mint_calldata(recipient, U256::from(one_eth)).to_vec()), + ); + // tx1:recipient → dest 转 1000 wei。**依赖** tx0 铸出的余额(否则余额不足付 gas)。 + let tx1 = legacy_tx(0, 21_000, 10, 1_000, TxKind::Call(dest), Bytes::new()); + + let seed = vec![ + eoa(funder, huge, 0), + contract(AUTHORIZED_CALLER, mint_caller_bytecode(), 0), + eoa(recipient, 0, 0), // 初始零余额:tx1 完全依赖 tx0 的 precompile 铸币 + eoa(dest, 0, 0), + ]; + + let d = run_precompile_exec_diff( + seed, + vec![tx0, tx1], + vec![funder, recipient], + Some(recipient), + ); + + println!( + "[exec_diff:authorized-mint-then-spend] serial_ok={} grevm_ok={} diverged={} {} {}", + d.serial_ok, + d.grevm_ok, + d.diverged, + d.divergence.as_deref().unwrap_or(""), + d.note, + ); + + assert!(d.serial_ok, "串行后端应成功执行本块:{:?}", d.divergence); + assert!(d.grevm_ok, "grevm 后端应成功执行本块:{:?}", d.divergence); + assert!( + !d.diverged, + "serial⟷grevm 背离 —— 疑似共识 state-fork(precompile 直写未被 grevm 当依赖追踪?):{:?}", + d.divergence + ); + } + + /// 退化路径(path B 兜底):直接把一笔 tx 打到 mint precompile 地址(EOA sender,非授权 → + /// precompile 返回 Err)。仍应用 precompile 并执行整块,断言 serial==grevm。 + /// 覆盖「custom precompile 已注册 + 被 tx 直接命中」的执行路径本身。 + #[test] + fn direct_unauthorized_call_to_precompile_serial_equals_grevm() { + let sender = addr(0xB0); + let huge: u128 = 100_000_000_000_000_000_000; + + // EOA 直接 CALL mint precompile 地址;caller=sender≠AUTHORIZED_CALLER → precompile Err。 + let tx0 = legacy_tx( + 0, + 100_000, + 10, + 0, + TxKind::Call(NATIVE_MINT_PRECOMPILE_ADDR), + Bytes::from(mint_calldata(addr(0xEE), U256::from(1u64)).to_vec()), + ); + + let seed = vec![eoa(sender, huge, 0), eoa(addr(0xEE), 0, 0)]; + let d = run_precompile_exec_diff(seed, vec![tx0], vec![sender], Some(addr(0xEE))); + + println!( + "[exec_diff:direct-unauthorized] serial_ok={} grevm_ok={} diverged={} {} {}", + d.serial_ok, + d.grevm_ok, + d.diverged, + d.divergence.as_deref().unwrap_or(""), + d.note, + ); + + assert!(d.serial_ok, "串行应成功执行整块(tx 内部失败不影响块):{:?}", d.divergence); + assert!(d.grevm_ok, "grevm 应成功执行整块:{:?}", d.divergence); + assert!(!d.diverged, "serial⟷grevm 背离:{:?}", d.divergence); + } + + /// 基线:应用了 mint precompile,但块内不触碰它(普通 A→B 转账)。证明「注册自定义 precompile」 + /// 这一步本身不引入两侧背离——从而真背离可被归因到 precompile 交互而非注册副作用。 + #[test] + fn applying_precompile_without_touching_it_serial_equals_grevm() { + let a = addr(0xA7); + let b = addr(0xB7); + let huge: u128 = 100_000_000_000_000_000_000; + let tx0 = legacy_tx(0, 21_000, 10, 1_000, TxKind::Call(b), Bytes::new()); + let seed = vec![eoa(a, huge, 0), eoa(b, 0, 0)]; + let d = run_precompile_exec_diff(seed, vec![tx0], vec![a], Some(b)); + + println!( + "[exec_diff:baseline-no-touch] serial_ok={} grevm_ok={} diverged={} {}", + d.serial_ok, d.grevm_ok, d.diverged, d.note, + ); + assert!(d.serial_ok && d.grevm_ok, "基线两侧都应成功:{:?}", d.divergence); + assert!(!d.diverged, "基线不应背离:{:?}", d.divergence); + } +} diff --git a/crates/pipe-exec-layer-ext-v2/execute/src/lib.rs b/crates/pipe-exec-layer-ext-v2/execute/src/lib.rs index 0cd0584e3..1bf4f97f4 100644 --- a/crates/pipe-exec-layer-ext-v2/execute/src/lib.rs +++ b/crates/pipe-exec-layer-ext-v2/execute/src/lib.rs @@ -30,6 +30,15 @@ mod difftx_fuzz; /// panic。把量产生成器接到执行差分上的最强 hunt。纯测试期 harness。 #[cfg(all(test, feature = "difftx_exec"))] mod difftx_exec_fuzz; + +/// Gravity 自定义 precompile(mint / randomness)预言机:caller 门 + 畸形输入不 panic + +/// 应用自定义 precompile 后 serial⟷grevm 执行一致(precompile 直改 journal state 的 fork 风险)。 +#[cfg(all(test, feature = "difftx_precompile"))] +mod difftx_precompile; + +/// EIP-2935 HISTORY_STORAGE / BLOCKHASH exec-time 预言机 + warm-restart 截断窗口 BlockTooOld 风险。 +#[cfg(all(test, feature = "difftx_2935"))] +mod difftx_2935; use alloy_sol_types::SolEvent; use channel::Channel;