-
Notifications
You must be signed in to change notification settings - Fork 18
test(pipe-exec): tx_filter⟷revm 差分预言机 (difftx) #399
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
463bc30
ddf4f41
35d7d73
55c1bd5
413648c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<DiffOutcome> = 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<serde_json::Value> = 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) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| //! serial(revm) ⟷ grevm 执行差分预言机 —— 命令行工具。 | ||
| //! | ||
| //! 跑所有 baseline + adversarial 用例,逐块过**两个后端**(串行 `WrapExecutor` / | ||
| //! 并行 `GrevmExecutor`),打印一张对比表;对**任何背离**写一份可复现 artifact 到 | ||
| //! `difftx-repro/exec_<name>.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<serde_json::Value> = 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<serde_json::Value> = 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(), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For any divergence in the code-dependent cases (for example the delegated 7702 baseline or the RAW/WAW storage adversarial case), the artifact only records Useful? React with 👍 / 👎. |
||
| }) | ||
| }) | ||
| .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 <rawtx> (逐笔,保持块内顺序)", | ||
| "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(()) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a
difftx_execcase diverges, this artifact tells operators to submitraw_2718_hex, but those bytes still contain the dummySignature::test_signature()used by the fixtures while the oracle itself bypasses recovery viaRecoveredBlock::new_unhashed(..., case.senders.clone()). On a live node the sender is recovered from the raw signature instead of the JSONsender, so the transaction will run from a different account than the seeded fixture and the repro will either fail nonce/balance checks or exercise a different block; please either sign with the fixture sender keys or emit a replay format that preserves explicit senders.Useful? React with 👍 / 👎.