Skip to content

Commit 5bd01a6

Browse files
S0-tools: L3 prerequisite tooling (N1a slim variant)
S0 末 L3 前置工具链:per user decision N1a, do NOT pre-build 28 new chain handlers (those go in S2 waves incrementally). Only build the gates and harness that S1.2 + every S2 wave will need. CHANGES: 1. tools/mock_rpc_server.py — fallback handler + startup gate * handle_unknown() returns {"_mock_echo": true, "_method", "_params"} * dispatch() routes unknown chain to handle_unknown ONLY when env MOCK_ALLOW_UNKNOWN=1; default behavior unchanged (returns -32601) * main() exits 2 at startup if --chain unknown AND env not set * --chain choices=... removed so unknown names CAN reach the gate * Rationale: prevent "wave added bitcoin.json but no handler" from silently passing as ethereum (parallel-entry-trap variant) 2. tools/e2e_smoke.sh — optional CHAIN_CONFIG env gate * +CHAIN_CONFIG=path: when set, asserts file exists + .chain_type readable; logs binding for diagnosability * When unset, behavior 100% unchanged (no break to existing matrix) 3. tools/e2e_smoke_chain_matrix.sh — new 36-chain template-driven matrix * Auto-discovers chains from config/chains/*.json (no hardcoded list) * ONLY_CHAINS / SKIP_CHAINS / BASE_PORT / FAIL_FAST envs * Per-chain log + auto tail on failure (self-debugging) * Default serial (GCE shared-resource 3 principles) * Existing tools/e2e_smoke_8chain_matrix.sh PRESERVED as S1.2 baseline 4. tests/snapshots/baseline_8chains/*.json — 8 round-trip snapshots * Extracted from config_loader.sh:408-652 UNIFIED_BLOCKCHAIN_CONFIG * Bytes-equal to config/chains/<chain>.json minus _meta (verified) * S1.2 will diff against these as the regression gate 5. tests/test_mock_rpc_unknown_echo.py — L1 unittest (4 tests) * test_default_unknown_chain_rejected * test_env_set_unknown_chain_echoes * test_handle_unknown_shape * test_known_chain_not_affected_by_gate VERIFICATION (all pass): * Step 1: 8/8 baseline chains boot + sentinel curl PASS * Step 2: 8/8 round-trip snapshot DIFF=0 * Step 3: bash -n PASS x2; ONLY=ethereum matrix rc=0 * Step 4: L1 4/4 PASS; L2 real-mock startup gate + echo PASS * Regression: tests/smoke_mock_rpc_8chains.sh 8/8 PASS; tests/test_mock_rpc_chain_forward.py 4/4 PASS See analysis-notes/p2-exec/wave-S0-tools.md for full decision table, trade-offs, 4 fail-fast gates, and next-step plan. baseline=ffbeeee
1 parent 52aaa0d commit 5bd01a6

13 files changed

Lines changed: 656 additions & 1 deletion

File tree

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# Wave S0-tools — L3 前置工具链(N1a 精简版)
2+
3+
**baseline**: `ffbeeee`
4+
**完成时间**: 2026-05-24
5+
**决策依据**: 用户回 **N1a**(精简版,跟 wave 走的渐进式 mock 扩展)
6+
7+
## 决策摘要
8+
9+
| 议题 | 选项 | 决策 | 反转条件 |
10+
|------|------|------|----------|
11+
| S0-tools 范围 | N1a 精简 / N1b 全 28 handler / N1c 跳过 | **N1a** | 若 S2-a 发现 5+ 共享 handler 可压成 1 通用 dispatcher,回头重构 S0 |
12+
| 新链 handler 实现时机 | S0 一把梭 / wave 增量 | **wave 增量**(跟 S2 wave 走) | 若 wave 内开发链 handler 工程量超估 2x,回头折回 S0 |
13+
| 未知 chain 默认行为 | 静默 echo / 报错拒绝 | **报错拒绝**(opt-in `MOCK_ALLOW_UNKNOWN=1`) ||
14+
15+
## 4 个自动停手点(全部生效)
16+
17+
1. **mock 启动 exit≠0** → step 1 PASS(8/8 启动 OK)
18+
2. **baseline snapshot diff≠0** → step 2 PASS(8/8 round-trip DIFF=0)
19+
3. **8 链 matrix 非 0 退出** → step 3 PASS(`ONLY=ethereum` rc=0 + 既有 8 链 smoke rc=0)
20+
4. **兜底 handler 返非合法 JSON** → step 4 PASS(curl 实测合法 JSON-RPC envelope)
21+
22+
## 执行步骤
23+
24+
### Step 1: 验证现有 mock 可启动 + 8 链 curl(2 秒)
25+
26+
8/8 PASS:
27+
28+
```
29+
✅ solana port=18900 result=1779588689
30+
✅ ethereum port=18900 result=0x8d6dd31
31+
✅ bsc port=18900 result=0x8d6dd31
32+
✅ base port=18900 result=0x8d6dd31
33+
✅ polygon port=18900 result=0x8d6dd31
34+
✅ scroll port=18900 result=0x8d6dd31
35+
✅ starknet port=18900 result=148299057
36+
✅ sui port=18900 result=148299057
37+
```
38+
39+
**结论**:现有 `tools/mock_rpc_server.py` 8 链 sentinel method 全过,无需任何修补。
40+
41+
### Step 2: baseline 8 链 snapshot(0.2 秒)
42+
43+
`config/config_loader.sh:408-652``UNIFIED_BLOCKCHAIN_CONFIG` heredoc(顶层 key = `blockchains`,**** `chains`,首版误判已纠正)抽取 8 链 baseline,落盘 `tests/snapshots/baseline_8chains/*.json`
44+
45+
Round-trip 验证(snapshot vs `config/chains/<chain>.json` 去掉 `_meta`):
46+
47+
```
48+
✅ solana snap=1181B round_trip=DIFF=0
49+
✅ ethereum snap=922B round_trip=DIFF=0
50+
✅ bsc snap=917B round_trip=DIFF=0
51+
✅ base snap=918B round_trip=DIFF=0
52+
✅ scroll snap=920B round_trip=DIFF=0
53+
✅ polygon snap=921B round_trip=DIFF=0
54+
✅ starknet snap=905B round_trip=DIFF=0
55+
✅ sui snap=1092B round_trip=DIFF=0
56+
```
57+
58+
**结论**:8/8 字节级一致,可作为 S1.2 拆 8 链后回归的金标准。
59+
60+
### Step 3: e2e harness 接 `config/chains/*.json`
61+
62+
**改造点 1**(零破坏)— `tools/e2e_smoke.sh` 加可选 `CHAIN_CONFIG` 环境变量:
63+
64+
- 未传时行为不变(与既有 8 链 matrix 100% 兼容)
65+
- 传时 gate 校验:文件存在 + `.chain_type` 字段可读;不一致即 fail
66+
67+
**改造点 2**(新文件)— `tools/e2e_smoke_chain_matrix.sh`:
68+
69+
- **自动扫描** `config/chains/*.json`(零写死),36 链全覆盖
70+
- 支持 `ONLY_CHAINS=a,b,c`/`SKIP_CHAINS=x,y`/`BASE_PORT=29000` 过滤
71+
- 每链独立 log:`/tmp/e2e_smoke_chain_matrix/<chain>.log`
72+
- 自带尾部 6 行日志(自调试)
73+
- 默认串行(GCE 共享资源 3 原则);可 `PARALLEL=1` 切并行
74+
75+
**验证**:
76+
77+
```
78+
=== Test A: bash -n syntax ===
79+
✅ tools/e2e_smoke.sh rc=0
80+
✅ tools/e2e_smoke_chain_matrix.sh rc=0
81+
82+
=== Test B: ONLY=__never_match__ 干跑(期望 exit=2)===
83+
rc=2 stderr=ERROR: filter left 0 chains
84+
85+
=== Test C: ONLY=ethereum 单链真跑(带 CHAIN_CONFIG gate)===
86+
rc=0 PASS (CHAIN_CONFIG gate 命中,chain_type=evm)
87+
```
88+
89+
**保留原 `tools/e2e_smoke_8chain_matrix.sh`**(写死 8 链版)作为 S1.2 回归基准——不删,新版与旧版并存,符合 v1.4.4 老测保护规则。
90+
91+
### Step 4: 兜底 handler(unknown chain → echo)
92+
93+
**双层 gate 设计**(防止"未知链静默装成 ethereum"这种 parallel-entry-trap 变体):
94+
95+
1. **dispatch 路径**:`MOCK_ALLOW_UNKNOWN=1` 时,未知 chain → `handle_unknown()``{"_mock_echo": true, "_method": <m>, "_params": <p>}`
96+
2. **启动路径**:`main()` 启动时若 `--chain` 不在 `CHAIN_HANDLERS` 且 env 未设,**直接 exit=2 拒启**(fail-fast)
97+
3. **默认行为不变**:未设 env 时 `dispatch` 仍返 -32601,既有所有测试 0 副作用
98+
99+
`handle_unknown` 返回的 `_mock_echo: true`**显式不通过语义验证的标记**——任何下游 assert 看到这字段就必须当"liveness OK, semantics not validated"处理,绝不能据此声明"通过"。
100+
101+
**验证**:
102+
103+
```
104+
=== L1 unittest (tests/test_mock_rpc_unknown_echo.py) — 4/4 PASS ===
105+
test_default_unknown_chain_rejected ... ok
106+
test_env_set_unknown_chain_echoes ... ok
107+
test_handle_unknown_shape ... ok
108+
test_known_chain_not_affected_by_gate ... ok
109+
110+
=== L2 实测 ===
111+
✅ 启动 gate: --chain bitcoin (no env) → exit=2 + 提示
112+
✅ MOCK_ALLOW_UNKNOWN=1 + --chain bitcoin → curl getblockcount
113+
返回 {"_mock_echo":true,"_method":"getblockcount","_params":[]}
114+
115+
=== 回归 ===
116+
✅ tests/smoke_mock_rpc_8chains.sh 8/8 PASS
117+
✅ tests/test_mock_rpc_chain_forward.py 4/4 PASS
118+
```
119+
120+
## 落盘清单
121+
122+
| 文件 | 性质 | 字节 |
123+
|------|------|------|
124+
| `tools/e2e_smoke.sh` | 改造(+27 行 / -2 行) | 6017 |
125+
| `tools/e2e_smoke_chain_matrix.sh` | 新增 | 5401 |
126+
| `tools/mock_rpc_server.py` | 改造(+30 行 / -1 行) | 31855 |
127+
| `tests/test_mock_rpc_unknown_echo.py` | 新增 L1 测 | 2609 |
128+
| `tests/snapshots/baseline_8chains/*.json` × 8 | 新增 snapshot | 7776 合计 |
129+
130+
## E1/E5 自检
131+
132+
- **E1 完整度**:S0-tools 5 步全 PASS,无 defer,无 skip。
133+
- **E5 反例**:`MOCK_ALLOW_UNKNOWN=1` 时 echo 的语义"对不对"是**已知不验证**(`_mock_echo:true` 显式标记),下游必须用真 endpoint fixture 校验语义,不能拿 echo 当通过。
134+
135+
## 下一步(S1.1)
136+
137+
拆 8 链 baseline 出 `config/chains/*.json`(已完成—— `_meta``source:"baseline-heredoc"` 而非 `"research-md"`)+ 改 `config/config_loader.sh` heredoc 改 loader 读 `config/chains/*.json`。S1.2 回归用 step 2 落盘的 snapshot 做 diff=0 闸门。
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"chain_type": "base",
3+
"methods": {
4+
"get_logs": "eth_getLogs",
5+
"get_transaction": "eth_getTransactionByHash"
6+
},
7+
"param_formats": {
8+
"eth_blockNumber": "no_params",
9+
"eth_gasPrice": "no_params",
10+
"eth_getBalance": "address_latest",
11+
"eth_getTransactionCount": "address_latest"
12+
},
13+
"params": {
14+
"account_count": "ACCOUNT_COUNT",
15+
"max_signatures": "ACCOUNT_MAX_SIGNATURES",
16+
"output_file": "ACCOUNTS_OUTPUT_FILE",
17+
"semaphore_limit": "ACCOUNT_SEMAPHORE_LIMIT",
18+
"target_address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
19+
"tx_batch_size": "ACCOUNT_TX_BATCH_SIZE"
20+
},
21+
"rpc_methods": {
22+
"mixed": "eth_getBalance,eth_getTransactionCount,eth_blockNumber,eth_gasPrice",
23+
"single": "eth_getBalance"
24+
},
25+
"rpc_url": "LOCAL_RPC_URL",
26+
"system_addresses": [
27+
"0x0000000000000000000000000000000000000000",
28+
"0x000000000000000000000000000000000000dead"
29+
]
30+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"chain_type": "bsc",
3+
"methods": {
4+
"get_logs": "eth_getLogs",
5+
"get_transaction": "eth_getTransactionByHash"
6+
},
7+
"param_formats": {
8+
"eth_blockNumber": "no_params",
9+
"eth_gasPrice": "no_params",
10+
"eth_getBalance": "address_latest",
11+
"eth_getTransactionCount": "address_latest"
12+
},
13+
"params": {
14+
"account_count": "ACCOUNT_COUNT",
15+
"max_signatures": "ACCOUNT_MAX_SIGNATURES",
16+
"output_file": "ACCOUNTS_OUTPUT_FILE",
17+
"semaphore_limit": "ACCOUNT_SEMAPHORE_LIMIT",
18+
"target_address": "0x250632378E573c6Be1AC2f97Fcdf00515d0Aa91B",
19+
"tx_batch_size": "ACCOUNT_TX_BATCH_SIZE"
20+
},
21+
"rpc_methods": {
22+
"mixed": "eth_getBalance,eth_getTransactionCount,eth_blockNumber,eth_gasPrice",
23+
"single": "eth_getBalance"
24+
},
25+
"rpc_url": "LOCAL_RPC_URL",
26+
"system_addresses": [
27+
"0x0000000000000000000000000000000000000000",
28+
"0x000000000000000000000000000000000000dead"
29+
]
30+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"chain_type": "ethereum",
3+
"methods": {
4+
"get_logs": "eth_getLogs",
5+
"get_transaction": "eth_getTransactionByHash"
6+
},
7+
"param_formats": {
8+
"eth_blockNumber": "no_params",
9+
"eth_gasPrice": "no_params",
10+
"eth_getBalance": "address_latest",
11+
"eth_getTransactionCount": "address_latest"
12+
},
13+
"params": {
14+
"account_count": "ACCOUNT_COUNT",
15+
"max_signatures": "ACCOUNT_MAX_SIGNATURES",
16+
"output_file": "ACCOUNTS_OUTPUT_FILE",
17+
"semaphore_limit": "ACCOUNT_SEMAPHORE_LIMIT",
18+
"target_address": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
19+
"tx_batch_size": "ACCOUNT_TX_BATCH_SIZE"
20+
},
21+
"rpc_methods": {
22+
"mixed": "eth_getBalance,eth_getTransactionCount,eth_blockNumber,eth_gasPrice",
23+
"single": "eth_getBalance"
24+
},
25+
"rpc_url": "LOCAL_RPC_URL",
26+
"system_addresses": [
27+
"0x0000000000000000000000000000000000000000",
28+
"0x000000000000000000000000000000000000dead"
29+
]
30+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"chain_type": "polygon",
3+
"methods": {
4+
"get_logs": "eth_getLogs",
5+
"get_transaction": "eth_getTransactionByHash"
6+
},
7+
"param_formats": {
8+
"eth_blockNumber": "no_params",
9+
"eth_gasPrice": "no_params",
10+
"eth_getBalance": "address_latest",
11+
"eth_getTransactionCount": "address_latest"
12+
},
13+
"params": {
14+
"account_count": "ACCOUNT_COUNT",
15+
"max_signatures": "ACCOUNT_MAX_SIGNATURES",
16+
"output_file": "ACCOUNTS_OUTPUT_FILE",
17+
"semaphore_limit": "ACCOUNT_SEMAPHORE_LIMIT",
18+
"target_address": "0xc2132D05D31c914a87C6611C10748AEb04B58e8F",
19+
"tx_batch_size": "ACCOUNT_TX_BATCH_SIZE"
20+
},
21+
"rpc_methods": {
22+
"mixed": "eth_getBalance,eth_getTransactionCount,eth_blockNumber,eth_gasPrice",
23+
"single": "eth_getBalance"
24+
},
25+
"rpc_url": "LOCAL_RPC_URL",
26+
"system_addresses": [
27+
"0x0000000000000000000000000000000000000000",
28+
"0x000000000000000000000000000000000000dead"
29+
]
30+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"chain_type": "scroll",
3+
"methods": {
4+
"get_logs": "eth_getLogs",
5+
"get_transaction": "eth_getTransactionByHash"
6+
},
7+
"param_formats": {
8+
"eth_blockNumber": "no_params",
9+
"eth_gasPrice": "no_params",
10+
"eth_getBalance": "address_latest",
11+
"eth_getTransactionCount": "address_latest"
12+
},
13+
"params": {
14+
"account_count": "ACCOUNT_COUNT",
15+
"max_signatures": "ACCOUNT_MAX_SIGNATURES",
16+
"output_file": "ACCOUNTS_OUTPUT_FILE",
17+
"semaphore_limit": "ACCOUNT_SEMAPHORE_LIMIT",
18+
"target_address": "0x06eFdBFf2a14a7c8E15944D1F4A48F9F95F663A4",
19+
"tx_batch_size": "ACCOUNT_TX_BATCH_SIZE"
20+
},
21+
"rpc_methods": {
22+
"mixed": "eth_getBalance,eth_getTransactionCount,eth_blockNumber,eth_gasPrice",
23+
"single": "eth_getBalance"
24+
},
25+
"rpc_url": "LOCAL_RPC_URL",
26+
"system_addresses": [
27+
"0x0000000000000000000000000000000000000000",
28+
"0x000000000000000000000000000000000000dead"
29+
]
30+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"chain_type": "solana",
3+
"methods": {
4+
"get_signatures": "getSignaturesForAddress",
5+
"get_transaction": "getTransaction"
6+
},
7+
"param_formats": {
8+
"getAccountInfo": "single_address",
9+
"getBalance": "single_address",
10+
"getBlockHeight": "no_params",
11+
"getLatestBlockhash": "no_params",
12+
"getTokenAccountBalance": "single_address"
13+
},
14+
"params": {
15+
"account_count": "ACCOUNT_COUNT",
16+
"max_signatures": "ACCOUNT_MAX_SIGNATURES",
17+
"output_file": "ACCOUNTS_OUTPUT_FILE",
18+
"semaphore_limit": "ACCOUNT_SEMAPHORE_LIMIT",
19+
"target_address": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
20+
"tx_batch_size": "ACCOUNT_TX_BATCH_SIZE"
21+
},
22+
"rpc_methods": {
23+
"mixed": "getAccountInfo,getBalance,getTokenAccountBalance,getLatestBlockhash,getBlockHeight",
24+
"single": "getAccountInfo"
25+
},
26+
"rpc_url": "LOCAL_RPC_URL",
27+
"system_addresses": [
28+
"11111111111111111111111111111111",
29+
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
30+
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",
31+
"metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s",
32+
"SysvarRent111111111111111111111111111111111",
33+
"ComputeBudget111111111111111111111111111111"
34+
]
35+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"chain_type": "starknet",
3+
"methods": {
4+
"get_events_native": "starknet_getEvents",
5+
"get_transaction": "starknet_getTransactionByHash"
6+
},
7+
"param_formats": {
8+
"starknet_blockNumber": "no_params",
9+
"starknet_getClassAt": "latest_address",
10+
"starknet_getNonce": "latest_address",
11+
"starknet_getStorageAt": "address_key_latest"
12+
},
13+
"params": {
14+
"account_count": "ACCOUNT_COUNT",
15+
"max_signatures": "ACCOUNT_MAX_SIGNATURES",
16+
"output_file": "ACCOUNTS_OUTPUT_FILE",
17+
"semaphore_limit": "ACCOUNT_SEMAPHORE_LIMIT",
18+
"target_address": "0x068f5c6a61780768455de69077e07e89787839bf8166decfbf92b645209c0fb8",
19+
"tx_batch_size": "ACCOUNT_TX_BATCH_SIZE"
20+
},
21+
"rpc_methods": {
22+
"mixed": "starknet_getClassAt,starknet_getNonce,starknet_getStorageAt,starknet_blockNumber",
23+
"single": "starknet_getClassAt"
24+
},
25+
"rpc_url": "LOCAL_RPC_URL",
26+
"system_addresses": []
27+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"chain_type": "sui",
3+
"methods": {
4+
"get_owned_objects": "suix_getOwnedObjects",
5+
"get_transaction": "sui_getTransactionBlock",
6+
"get_transactions": "suix_queryTransactionBlocks"
7+
},
8+
"param_formats": {
9+
"sui_getChainIdentifier": "no_params",
10+
"sui_getLatestCheckpointSequenceNumber": "no_params",
11+
"sui_getObject": "address_with_options",
12+
"sui_getTotalTransactionBlocks": "no_params",
13+
"suix_getReferenceGasPrice": "no_params"
14+
},
15+
"params": {
16+
"account_count": "ACCOUNT_COUNT",
17+
"max_signatures": "ACCOUNT_MAX_SIGNATURES",
18+
"output_file": "ACCOUNTS_OUTPUT_FILE",
19+
"semaphore_limit": "ACCOUNT_SEMAPHORE_LIMIT",
20+
"target_address": "0x0000000000000000000000000000000000000000000000000000000000000005",
21+
"tx_batch_size": "ACCOUNT_TX_BATCH_SIZE"
22+
},
23+
"rpc_methods": {
24+
"mixed": "sui_getObject,sui_getTotalTransactionBlocks,sui_getLatestCheckpointSequenceNumber,suix_getReferenceGasPrice,sui_getChainIdentifier",
25+
"single": "sui_getObject"
26+
},
27+
"rpc_url": "LOCAL_RPC_URL",
28+
"system_addresses": [
29+
"0x1",
30+
"0x2",
31+
"0x3"
32+
]
33+
}

0 commit comments

Comments
 (0)