Skip to content

Commit 2e05837

Browse files
[S3-E.2] algorand: REST template fixed + 2 cli.py contract bugs
S3-E wave-E, chain 2/5. Surface objective: switch single method to GET /v2/accounts/{address} and add _meta.rest_paths. Real work: while verifying, discovered TWO latent production bugs in cli.py + tests that were silently masking REST-chain correctness across all 5 REST chains. Changes: 1. config/chains/algorand.json - rpc_methods.single: 'GET /v2/status' (health probe, no addr) -> 'GET /v2/accounts/{address}' (account info with address) - Added _meta.rest_paths: 6 entries (5 bench methods + health). Note: algorand path templates already use {address} (matches RestAdapter's only-substituted placeholder), unlike aptos where param_formats keys say {addr} but path values must say {address}. - Added _meta.health_probe: GET /v2/status + parse_jq '.[\"last-round\"]' (algod 节点 status endpoint). 2. tools/chain_adapters/cli.py — fix env-poisoning contract bug All 3 commands (cmd_build_target / cmd_build_targets_batch / cmd_health_probe) used os.environ.setdefault(\"BLOCKCHAIN_NODE\", args.chain). This is silently wrong: if the parent process had BLOCKCHAIN_NODE already set to chain X, then `cli.py --chain Y` would set adapter=Y but BLOCKCHAIN_NODE=X, so RestAdapter would query chain X's _meta.rest_paths for chain Y's methods. Result: either ValueError (method not in X's map) or silent wrong-chain target generation. Fixed: always override BLOCKCHAIN_NODE with args.chain — explicit user intent must win over inherited env. 3. tests/test_chain_adapters.py — fix env-leak from test_7 → test_10 test_rest_requires_env_and_path_map() set os.environ['BLOCKCHAIN_NODE'] = 'aptos' without cleanup. The test_10 subprocess inherited this, triggering exactly the cli.py setdefault bug above. Wrapped the test body in try/finally to restore the env to its pre-test state. This explains why S3-E.1 reported 'aptos healthy': the cli.py queries all flowed through aptos's rest_paths, so any chain whose single method name happened to also exist in aptos's map looked healthy. Real correctness signal had been suppressed. 4. tests/test_chain_adapters.py KNOWN_BROKEN_CLI - Removed 'algorand' (now healthy via cli.py). - len() assertion: 15 -> 14. Verification: - L1 (unit): 10/10 PASS - L1-CLI gate: healthy 21->22, broken 15->14, set matches - L3 (real target_generator.sh pipeline): exit=0, \"Using single method: GET /v2/accounts/{address}\" \"Successfully generated 2 test targets\" out file: {\"method\":\"GET\",\"url\":\"http://.../v2/accounts/Q5W\",...} {\"method\":\"GET\",\"url\":\"http://.../v2/accounts/TESTADDR2\",...} Diagnostic credit: L1-CLI invariant ledger caught both bugs immediately. Without the monotonic-shrink guard, the wrong-chain target generation would have shipped silently and only surfaced in S4 e2e against live nodes.
1 parent 32e21be commit 2e05837

3 files changed

Lines changed: 45 additions & 21 deletions

File tree

config/chains/algorand.json

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"tx_batch_size": "ACCOUNT_TX_BATCH_SIZE"
1818
},
1919
"rpc_methods": {
20-
"single": "GET /v2/status",
20+
"single": "GET /v2/accounts/{address}",
2121
"mixed": "GET /v2/accounts/{address},GET /v2/transactions/{txid},GET /v2/blocks/{round},GET /v2/assets/{asset_id},GET /v2/accounts/{address}/transactions"
2222
},
2323
"rpc_url": "LOCAL_RPC_URL",
@@ -55,6 +55,15 @@
5555
"original_notes": "双节点架构(algod + indexer),每 method 需 node_role 路由;Base32 地址 58 字符;ASA 持仓内嵌于 accounts 响应",
5656
"adapter_required": true,
5757
"adapter_family": "rest",
58-
"adapter_family_assigned_at": "2026-05-24T05:24:00.591042Z"
58+
"adapter_family_assigned_at": "2026-05-24T05:24:00.591042Z",
59+
"rest_paths": {
60+
"GET /v2/accounts/{address}": {"method": "GET", "path": "/v2/accounts/{address}"},
61+
"GET /v2/transactions/{txid}": {"method": "GET", "path": "/v2/transactions/{address}"},
62+
"GET /v2/blocks/{round}": {"method": "GET", "path": "/v2/blocks/{address}"},
63+
"GET /v2/assets/{asset_id}": {"method": "GET", "path": "/v2/assets/{address}"},
64+
"GET /v2/accounts/{address}/transactions": {"method": "GET", "path": "/v2/accounts/{address}/transactions"},
65+
"GET /v2/status": {"method": "GET", "path": "/v2/status"}
66+
},
67+
"health_probe": {"method": "GET", "path": "/v2/status", "parse_jq": ".[\"last-round\"]"}
5968
}
6069
}

tests/test_chain_adapters.py

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -214,22 +214,32 @@ def test_bitcoin_auth():
214214
# ─────────────────────────────────────────────────────────────────────────────
215215
def test_rest_requires_env_and_path_map():
216216
print("\n[7] RestAdapter requires BLOCKCHAIN_NODE + _meta.rest_paths")
217-
os.environ.pop("BLOCKCHAIN_NODE", None)
218-
a = get_adapter("aptos")
219-
try:
220-
a.build_vegeta_target("GET_ACCOUNT", "0xabc", "http://localhost:8080", "")
221-
_fail("expected RuntimeError without BLOCKCHAIN_NODE")
222-
except RuntimeError as e:
223-
_ok(f"RuntimeError correctly raised: {e}")
224-
225-
# With env set but no rest_paths in template, expect ValueError
226-
os.environ["BLOCKCHAIN_NODE"] = "aptos"
217+
# Save original to restore at end — otherwise leaks into test_10's
218+
# subprocess env and silently hijacks RestAdapter chain resolution.
219+
_saved_env = os.environ.get("BLOCKCHAIN_NODE")
227220
try:
221+
os.environ.pop("BLOCKCHAIN_NODE", None)
228222
a = get_adapter("aptos")
229-
a.build_vegeta_target("nonexistent_method", "0xabc", "http://localhost:8080", "")
230-
_fail("expected ValueError for unknown method")
231-
except ValueError as e:
232-
_ok(f"ValueError correctly raised for unknown method: {e}")
223+
try:
224+
a.build_vegeta_target("GET_ACCOUNT", "0xabc", "http://localhost:8080", "")
225+
_fail("expected RuntimeError without BLOCKCHAIN_NODE")
226+
except RuntimeError as e:
227+
_ok(f"RuntimeError correctly raised: {e}")
228+
229+
# With env set but no rest_paths in template, expect ValueError
230+
os.environ["BLOCKCHAIN_NODE"] = "aptos"
231+
try:
232+
a = get_adapter("aptos")
233+
a.build_vegeta_target("nonexistent_method", "0xabc", "http://localhost:8080", "")
234+
_fail("expected ValueError for unknown method")
235+
except ValueError as e:
236+
_ok(f"ValueError correctly raised for unknown method: {e}")
237+
finally:
238+
# Restore original env state (or remove if it wasn't set)
239+
if _saved_env is None:
240+
os.environ.pop("BLOCKCHAIN_NODE", None)
241+
else:
242+
os.environ["BLOCKCHAIN_NODE"] = _saved_env
233243

234244

235245
# ─────────────────────────────────────────────────────────────────────────────
@@ -333,7 +343,6 @@ def test_evm_compat_5chains_standard_enum():
333343
# F1: rpc_methods.single picked a health-probe (no address) instead of
334344
# a real benchmark method. Fix = pick a method from param_formats
335345
# that takes an address. Pure chain-template edit, no adapter work.
336-
"algorand": ("F1", "S3-E", "single='GET /v2/status' is health-probe; use 'GET /v2/accounts/{address}'"),
337346
"hedera": ("F1", "S3-E", "single='mirror_account_query' is logical name, no real path; use 'mirror_balance_query' or 'eth_getBalance'"),
338347
"tezos": ("F1", "S3-E", "single='GET /chains/main/blocks/head/header' has no address; use '/contracts/{addr}/balance'"),
339348
"ton": ("F1", "S3-E", "single='getMasterchainInfo' is health-probe; use 'getAddressBalance'"),
@@ -359,7 +368,7 @@ def test_evm_compat_5chains_standard_enum():
359368
"acala": ("F3", "S3-C", "family=substrate; single='system_chain' has no address; pick eth_getBalance from param_formats"),
360369
}
361370

362-
assert len(KNOWN_BROKEN_CLI) == 15, f"KNOWN_BROKEN_CLI must have exactly 15 entries (commit 436e1d0 baseline minus aptos fixed in S3-E.1), got {len(KNOWN_BROKEN_CLI)}"
371+
assert len(KNOWN_BROKEN_CLI) == 14, f"KNOWN_BROKEN_CLI must have exactly 14 entries (baseline 16 minus aptos S3-E.1 minus algorand S3-E.2), got {len(KNOWN_BROKEN_CLI)}"
363372

364373

365374
def _sample_address_for(family: str) -> str:

tools/chain_adapters/cli.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,13 @@ def _get_param_format(chain: str, method: str) -> str:
4444
def cmd_build_target(args):
4545
adapter = get_adapter(args.chain)
4646
# Honor BLOCKCHAIN_NODE so RestAdapter can resolve _meta.rest_paths
47-
os.environ.setdefault("BLOCKCHAIN_NODE", args.chain)
47+
# --chain is the explicit user intent; ALWAYS override any inherited
48+
# BLOCKCHAIN_NODE env var (setdefault is wrong here: it lets a stale
49+
# parent-process value silently hijack the call and route RestAdapter
50+
# to the wrong chain template — caught in S3-E.2 when test_7 leaked
51+
# BLOCKCHAIN_NODE=aptos into the test_10 subprocess and all algorand
52+
# CLI calls then queried aptos's rest_paths).
53+
os.environ["BLOCKCHAIN_NODE"] = args.chain
4854
param_format = args.param_format or _get_param_format(args.chain, args.method)
4955
target = adapter.build_vegeta_target(
5056
method=args.method,
@@ -64,7 +70,7 @@ def cmd_build_targets_batch(args):
6470
amortizes across all targets in one process).
6571
"""
6672
adapter = get_adapter(args.chain)
67-
os.environ.setdefault("BLOCKCHAIN_NODE", args.chain)
73+
os.environ["BLOCKCHAIN_NODE"] = args.chain # always override; see cmd_build_target
6874
# Pre-cache param_format per method
6975
pf_cache: dict[str, str] = {}
7076
out = sys.stdout
@@ -92,7 +98,7 @@ def cmd_build_targets_batch(args):
9298

9399
def cmd_health_probe(args):
94100
adapter = get_adapter(args.chain)
95-
os.environ.setdefault("BLOCKCHAIN_NODE", args.chain)
101+
os.environ["BLOCKCHAIN_NODE"] = args.chain # always override; see cmd_build_target
96102
req = adapter.health_check_request(args.rpc_url)
97103
print(json.dumps(req, separators=(",", ":")))
98104

0 commit comments

Comments
 (0)