Skip to content

Commit c9ca754

Browse files
S3-E.3-followup: hedera_dual 真实证 + adapter 单测 + KNOWN_BROKEN_MIXED 立 ledger
## 背景:用户反向压测发现 S3-E.3 (commit 2d007f2) 验证不足 前一 commit msg 说 "L3 PASS = 双协议路由生效",**但**: - L3 只验 vegeta target schema 生成成功(exit=0) - 从未真打 HTTP 验返码 - 新 adapter 0 个专属单测,只靠 cli e2e 偶然碰 - body 里 `params: ['0.0.801']` 缺 'latest' + 用 3-part ID,我看到了却写成 "已知 defer" 而未承认是当前 commit 的真实生产 blocker ## 真实证(C1 — 真打 endpoint) curl POST https://mainnet.hashio.io/api -d \ '{"jsonrpc":"2.0","id":1,"method":"eth_getBalance","params":["0.0.801"]}' → HTTP 400 {"error":{"code":-32602,"message":"Invalid parameter 0: Expected 0x prefixed string representing the address (20 bytes), value: 0.0.801"}} 正确 payload 真返 balance: curl POST ... -d '{"...","params":["0x0000000000000000000000000000000000000002","latest"]}' → HTTP 200 {"result":"0xdc190f51555e27b8e0800","jsonrpc":"2.0","id":1} Mirror REST 真返 evm_address 字段: curl https://mainnet-public.mirrornode.hedera.com/api/v1/accounts/0.0.2 → {"account":"0.0.2","evm_address":"0x0000000000000000000000000000000000000002", "balance":{"balance":1663012637744658}} eth_blockNumber 纯活性:HTTP 200 result=0x5b10a68 ✓ ## adapter 专属单测(C2/C3 — test_11, test_12) (1) test_hedera_dual_adapter_routing — - REST method 'GET /api/v1/accounts/{addr}' → GET Mirror URL,address 入 path - eth_blockNumber → POST Hashio URL,empty params - eth_getBalance + param_format='address_latest' → POST Hashio, params=[addr, 'latest'](adapter 自身给得对!) - 缺 _meta.json_rpc_url → ValueError - 缺 BLOCKCHAIN_NODE → RuntimeError (2) test_is_jsonrpc_method_regex — - 7 个 JSON-RPC namespace(eth_/net_/web3_/debug_/trace_)正路由 - REST path / camelCase / 前缀干扰("ethReporter") 全否路由 L1 现 12/12 PASS(原 10 + 新 2) ## 关键发现:adapter 本身 OK,生产 mixed 失败在上游 test_11 证明 adapter 给 [addr, 'latest'] 正确 — 当 cli.py 传对 param_format。 但生产链路有两个上游 bug: PARAM cli.py L40 `tpl.get("params", {})` 读 fetcher config,应读 `tpl.get("param_formats", {})`。adapter 收 param_format="" → JsonRpcAdapter `_build_params("")` → 默认 [] → 缺 'latest'。 影响所有 JSON-RPC chain 的 eth_* 调用。Fix wave: cli-param-bug ADDR_FMT fetch_active_accounts.py 不支持 hedera,只能吐 3-part 0.0.N。 Mirror REST 接受(path 用原 ID),JSON-RPC Relay 要 0x 前缀 20-byte hex。Mirror /accounts/{id} 返 evm_address 字段可用, 但 fetcher 没接。Fix wave: S4(per-chain account fetcher) ## 诚实 ledger (1) tests/test_chain_adapters.py 加 KNOWN_BROKEN_MIXED dict: {hedera: ("PARAM+ADDR_FMT", "cli-param-bug + S4", evidence)} 不强制 assert,因为没有 mixed-path 自动测;留位给未来 wave。 (2) config/chains/hedera.json _meta 加 known_broken_mixed 字段: status: ADAPTER_OK_BUT_UPSTREAM_BROKEN evidence_date: 2026-05-24 live_http_test: 上述 curl 命令 + HTTP 400 实证 blockers: 上述 PARAM / ADDR_FMT 两条 ## 校正前一 commit 的过度声明 commit 2d007f2 该说: "hedera_dual adapter 已实现并通过 L1 schema 验证;mixed 真生产链路 阻于 cli.py PARAM bug 和 fetcher ADDR_FMT 缺失,见 KNOWN_BROKEN_MIXED。" 而不是: "L3 PASS → 双协议路由生效"(暗示生产可跑,实际不可) adapter 本身的工作不撤回(test_11 实证 routing 正确),但 hedera 不能 算 "broken set 14→13",该算 "broken_cli 14→13 + broken_mixed 0→1"。 ## 自审 E1-E5(本 commit) E1 调研:回头验 docs/zh/chains/14-hedera.md E1-E15 + 现场 curl E2 无降级:不撤 adapter / 不砍 method;承认 mixed 阻于上游,显式 ledger E3 实证:真打 4 个 HTTP request 各拿返码 / 12/12 单测 PASS E4 parallel-entry-trap:无 E5 ledger 单调:KNOWN_BROKEN_CLI 仍 13(hedera CLI 路径 single 健康); 新建 KNOWN_BROKEN_MIXED = 1(hedera)— 这是新分类, 不算 CLI ledger 增长
1 parent 2d007f2 commit c9ca754

2 files changed

Lines changed: 144 additions & 0 deletions

File tree

config/chains/hedera.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,16 @@
5858
"adapter_family_assigned_at": "2026-05-24T05:24:00.591042Z",
5959
"adapter_family_upgraded_at": "2026-05-24T18:00:00Z",
6060
"adapter_family_upgrade_note": "S3-E.3: was 'rest' (broken — couldn't route eth_*); upgraded to dual-protocol adapter that routes per-method between Mirror REST and JSON-RPC Relay.",
61+
"known_broken_mixed": {
62+
"status": "ADAPTER_OK_BUT_UPSTREAM_BROKEN",
63+
"evidence_date": "2026-05-24",
64+
"live_http_test": "curl POST mainnet.hashio.io/api eth_getBalance params=['0.0.801'] → HTTP 400 'Expected 0x prefixed string representing the address (20 bytes)'",
65+
"blockers": [
66+
"PARAM: cli.py L40 reads tpl['params'] (fetcher config) instead of tpl['param_formats'] (method→format) — eth_getBalance call gets [addr] not [addr, 'latest']. Affects all JSON-RPC chains.",
67+
"ADDR_FMT: fetch_active_accounts.py does not produce EVM-format addresses for hedera (only 3-part 0.0.N). Mirror REST works (path uses raw ID), JSON-RPC Relay requires 0x-prefixed 20-byte hex."
68+
],
69+
"fix_wave": "cli-param-bug (PARAM) + S4 (ADDR_FMT)"
70+
},
6171
"json_rpc_url": "https://mainnet.hashio.io/api",
6272
"rest_paths": {
6373
"GET /api/v1/accounts/{addr}": {"method": "GET", "path": "/api/v1/accounts/{address}"},

tests/test_chain_adapters.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,34 @@ def test_evm_compat_5chains_standard_enum():
370370
assert len(KNOWN_BROKEN_CLI) == 13, f"KNOWN_BROKEN_CLI must have exactly 13 entries (baseline 16 minus aptos S3-E.1 minus algorand S3-E.2 minus hedera S3-E.3), got {len(KNOWN_BROKEN_CLI)}"
371371

372372

373+
# ─────────────────────────────────────────────────────────────────────────────
374+
# KNOWN_BROKEN_MIXED — chains whose `single` benchmark passes L1-CLI test_10
375+
# but whose `mixed` (multi-method) path is broken in production (live HTTP
376+
# would 4xx/5xx). Documented here for honesty; not auto-asserted because we
377+
# don't yet have a `mixed` equivalent of test_10. When a future wave adds a
378+
# mixed-path test, this dict graduates to enforced invariant.
379+
#
380+
# Format: (chain, failure_layer, fix_wave, evidence)
381+
# failure_layer:
382+
# PARAM — cli.py reads tpl['params'] (fetcher config) when it should
383+
# read tpl['param_formats'] (method→shape). Affects all
384+
# JSON-RPC chains' eth_* calls (missing 'latest'). Fix wave: cli-param-bug
385+
# ADDR_FMT — fetch_active_accounts.py doesn't know chain-specific address
386+
# transformations (hedera 3-part 0.0.N → EVM 0x...0N).
387+
# Fix wave: S4 (per-chain account fetcher)
388+
# ─────────────────────────────────────────────────────────────────────────────
389+
KNOWN_BROKEN_MIXED = {
390+
"hedera": (
391+
"PARAM+ADDR_FMT", "cli-param-bug + S4",
392+
"S3-E.3 C1 实证:adapter routing 正确 (test_11 PASS),但 (1) cli.py L40 "
393+
"读 tpl['params'] 应读 tpl['param_formats'] → eth_getBalance 缺 'latest'; "
394+
"(2) fetch_active_accounts.py 不支持 hedera → 喂 3-part ID 0.0.N 给 Hashio "
395+
"返 HTTP 400 'Expected 0x prefixed string representing the address (20 bytes)'. "
396+
"需 fetcher 拿 mirror /accounts/{id} 的 evm_address 字段。"
397+
),
398+
}
399+
400+
373401
def _sample_address_for(family: str) -> str:
374402
"""Return a family-appropriate sample address for the build-target probe.
375403
@@ -477,6 +505,110 @@ def test_cli_build_target_all_36_chains():
477505
_ok(f"{len(healthy)}/36 healthy + {len(actually_broken)} known-broken (set matches expected)")
478506

479507

508+
# ─────────────────────────────────────────────────────────────────────────────
509+
# Test 11: HederaDualAdapter — per-request protocol routing
510+
# ─────────────────────────────────────────────────────────────────────────────
511+
def test_hedera_dual_adapter_routing():
512+
print("\n[11] HederaDualAdapter per-method protocol routing")
513+
import base64 as _b64
514+
import json as _json
515+
from chain_adapters.hedera_dual import HederaDualAdapter, _is_jsonrpc_method
516+
517+
_saved = os.environ.get("BLOCKCHAIN_NODE")
518+
os.environ["BLOCKCHAIN_NODE"] = "hedera"
519+
try:
520+
a = HederaDualAdapter()
521+
mirror_url = "https://mainnet-public.mirrornode.hedera.com"
522+
# REST path-style method → GET against passed rpc_url
523+
tgt = a.build_vegeta_target(
524+
method="GET /api/v1/accounts/{addr}",
525+
address="0.0.2", rpc_url=mirror_url,
526+
)
527+
assert tgt["method"] == "GET", f"REST should be GET, got {tgt['method']}"
528+
assert tgt["url"] == f"{mirror_url}/api/v1/accounts/0.0.2", f"got {tgt['url']}"
529+
assert "body" not in tgt or not tgt.get("body"), "GET should have no body"
530+
_ok("REST method → GET Mirror URL with address in path")
531+
532+
# JSON-RPC method → POST against _meta.json_rpc_url, NOT rpc_url
533+
tgt = a.build_vegeta_target(
534+
method="eth_blockNumber", address="",
535+
rpc_url=mirror_url, param_format="no_params",
536+
)
537+
assert tgt["method"] == "POST", f"JSON-RPC should be POST, got {tgt['method']}"
538+
assert tgt["url"] == "https://mainnet.hashio.io/api", \
539+
f"JSON-RPC URL should come from _meta.json_rpc_url, got {tgt['url']}"
540+
body = _json.loads(_b64.b64decode(tgt["body"]).decode())
541+
assert body["method"] == "eth_blockNumber"
542+
assert body["params"] == []
543+
_ok("eth_blockNumber → POST Hashio with empty params")
544+
545+
# eth_getBalance routing → POST with [addr, "latest"] when param_format=address_latest
546+
tgt = a.build_vegeta_target(
547+
method="eth_getBalance",
548+
address="0x0000000000000000000000000000000000000002",
549+
rpc_url=mirror_url, param_format="address_latest",
550+
)
551+
assert tgt["url"] == "https://mainnet.hashio.io/api"
552+
body = _json.loads(_b64.b64decode(tgt["body"]).decode())
553+
assert body["params"] == ["0x0000000000000000000000000000000000000002", "latest"], \
554+
f"params should be [addr, 'latest'], got {body['params']}"
555+
_ok("eth_getBalance routes to Hashio with [addr, latest] payload")
556+
557+
# Missing _meta.json_rpc_url → ValueError (defensive check)
558+
# We test by temporarily mutating the cached chain dict
559+
a2 = HederaDualAdapter()
560+
a2._load_chain("hedera") # populate cache
561+
original = a2._chain_cache["hedera"]["_meta"].pop("json_rpc_url")
562+
try:
563+
try:
564+
a2.build_vegeta_target(method="eth_blockNumber", address="",
565+
rpc_url=mirror_url, param_format="no_params")
566+
assert False, "should have raised ValueError when json_rpc_url missing"
567+
except ValueError as e:
568+
assert "json_rpc_url" in str(e), f"error msg should mention json_rpc_url: {e}"
569+
_ok("Missing _meta.json_rpc_url raises ValueError")
570+
finally:
571+
a2._chain_cache["hedera"]["_meta"]["json_rpc_url"] = original
572+
573+
# Missing BLOCKCHAIN_NODE → RuntimeError
574+
del os.environ["BLOCKCHAIN_NODE"]
575+
try:
576+
a.build_vegeta_target(method="eth_blockNumber", address="",
577+
rpc_url=mirror_url, param_format="no_params")
578+
assert False, "should have raised RuntimeError when BLOCKCHAIN_NODE missing"
579+
except RuntimeError as e:
580+
assert "BLOCKCHAIN_NODE" in str(e)
581+
_ok("Missing BLOCKCHAIN_NODE raises RuntimeError")
582+
finally:
583+
if _saved is None:
584+
os.environ.pop("BLOCKCHAIN_NODE", None)
585+
else:
586+
os.environ["BLOCKCHAIN_NODE"] = _saved
587+
588+
589+
# ─────────────────────────────────────────────────────────────────────────────
590+
# Test 12: _is_jsonrpc_method regex boundary
591+
# ─────────────────────────────────────────────────────────────────────────────
592+
def test_is_jsonrpc_method_regex():
593+
print("\n[12] _is_jsonrpc_method routing regex boundary")
594+
from chain_adapters.hedera_dual import _is_jsonrpc_method
595+
# Positive cases — JSON-RPC namespaces
596+
for m in ["eth_getBalance", "eth_call", "eth_blockNumber",
597+
"net_version", "web3_clientVersion",
598+
"debug_traceTransaction", "trace_block"]:
599+
assert _is_jsonrpc_method(m), f"{m!r} should route to JSON-RPC"
600+
# Negative cases — REST path keys, never JSON-RPC
601+
for m in ["GET /api/v1/accounts/{addr}",
602+
"GET /api/v1/balances?account.id={addr}",
603+
"POST /api/v1/contracts/call",
604+
"mirror_account_query", # legacy logical name
605+
"getAccount", # camelCase non-eth
606+
"ethReporter", # starts with "eth" but no underscore
607+
"_eth_getBalance"]: # leading underscore
608+
assert not _is_jsonrpc_method(m), f"{m!r} should NOT route to JSON-RPC"
609+
_ok("regex correctly distinguishes 7 JSON-RPC namespaces from REST/other forms")
610+
611+
480612
# ─────────────────────────────────────────────────────────────────────────────
481613
# Main
482614
# ─────────────────────────────────────────────────────────────────────────────
@@ -492,6 +624,8 @@ def main():
492624
test_jsonrpc_s3a_new_formats,
493625
test_evm_compat_5chains_standard_enum,
494626
test_cli_build_target_all_36_chains,
627+
test_hedera_dual_adapter_routing,
628+
test_is_jsonrpc_method_regex,
495629
]
496630
print(f"Running {len(tests)} test groups for chain_adapters")
497631
for t in tests:

0 commit comments

Comments
 (0)