public RPC filter: the records-indexing methods refused until the fixed node (ledger N7); testnet go note

One block-tagged request kills a node whose exec follower has no record yet (rpc.rs indexes records[0] on an empty vector; the panic hook exits). Live on seed1's filter from 7 October 2026 06:5xZ.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-07 06:48:31 +00:00
parent 62a7aa43b2
commit 381cdb1c41
3 changed files with 17 additions and 1 deletions

View file

@ -2357,3 +2357,9 @@ The bucket "Decided or closed by rule" counts each entry once: M8, M11 and P16 m
### N6. A fresh node could not join the devnet while a signal window was open
Open 6 October 2026 22:43Z to the 0.3.17 publish (the fix rides 0.3.17; main's ruling (a), 7 October 01:5x UK). Class: upstream's Toccata IBD guard (`protocol/flows/src/ibd/flow.rs`, `sync_and_validate_pruning_proof`) compares the syncer's relay-block version with the plain block version before requesting the pruning proof; from publish 2 (the sixteen-field object, the class v4 window open) the sink carries legal version-1026 signal blocks, so every FRESH node on the headers-proof IBD path refused every peer ("peer relayed block ... header version mismatch: got 1026, expected 2 at DAA score ..."). Found by the 0.3.17 canary (a fresh pod on 02d15a87: 483 refusals in 15 minutes, peers 0, blocks 0); the line is on f1ea7a38, the live node. Who it touched: nobody known. Every node that moved tonight had a datadir (the relay path, no pruning proof) or began its IBD before the hub took the object; the site's download buttons offered 0.3.16 to anyone, and a new install since 23:43 UK would have sat at 0 blocks. Fix: the comparison gated like the header processor's rule (`block_version_of` under `header_signals_active`), known-failed test first (a node with the window object syncs a chain carrying 1026 headers on IBD and relay; a 1026 header with no window still refused). Checklist line from here: a cut's canary is a FRESH node joining the LIVE object's chain, which carries signal headers once a window is open; the pre-cut harness (`infra/fast-time/node-compat.mjs`) carries a chain with signal-version headers under an open window. Confirmation after the 0.3.17 publish: the fleet's fresh-join read on a pod.
## Status updates, 7 October 2026 (06:5x UTC, the exec RPC on an empty state)
### N7. One block-tagged RPC request kills a node whose exec follower has no record yet
Open 7 October 2026 06:4xZ (found by the node lane's Mac headers-proof join gate: the joining node died at 66 percent of the headers stage with "index out of bounds: the len is 0 but the index is 0" at igneum/exec/src/rpc.rs, a local client having polled eth_getBlockByNumber("latest") on 127.0.0.1:26790). Class: `resolve_block` maps "latest" to `tip_number()` = `records.len().saturating_sub(1)` = 0 on an empty vector, and every method that then indexes `state.records[n]` (eth_getBlockByNumber, eth_getBlockTransactionCountByNumber, eth_getBlockReceipts, eth_getTransactionByBlockNumberAndIndex, eth_feeHistory's range, eth_getLogs' range, eth_call / eth_estimateGas / igneum_estimateGas through `simulate`) panics on a tokio worker; the node's panic hook exits the process. Every shipped tree carries it (0.3.17's 5899f603 and the 0.3.18 candidates). Exposure: a fresh or restarting node between the exec RPC binding and the follower's first record; the shipped app calls only eth_blockNumber and igneum_* methods (none index records), so no app-driven node died; a wallet or any third-party client takes the path. The testnet seeds at height 0 hold the genesis record and answered block 0 on 7 October 06:5xZ, so their window is a restart, not steady state. Mitigation on the public testnet RPC (main's order): the whole method class refused at the rpc-filter layer on seed1 until the fixed node is on every seed (BLOCKED_UNTIL_FIXED_NODE; seeds 2 and 3 expose no RPC; no devnet public RPC exists). Fix: every records index bounds-checked and "latest" on an empty state answering null, with a test on an empty state, by the node lane on ca3-v4-0318 (commit to be named), into release-0.3.18-node as its third merge; 0.3.18 carries it. Status: OPEN until 0.3.18 is live and the seeds run it.

View file

@ -117,3 +117,5 @@ The node binary for the seeds: `node tools/build-job.mjs run --node /Users/joshm
then `node tools/build-job.mjs fetch <id>` (lands in `infra/cross/out/`).
- Young-window note (7 October 2026): a chain whose finality depth is under 2,644 blocks (the fast-time profile, not the devnet at 43,200 and not the testnet at the compiled numbers) refuses fresh joins by headers proof for 2,644 DAA after its first pruning; the joiner-side fix rides 0.3.18 (ca3-v4-0318). No action at go; the line stays as the record.
- Public RPC block (7 October 2026, 06:5xZ, main's order): rpc.testnet.igneum.network refuses every method that resolves a block tag and indexes the exec records (eth_getBlockByNumber, eth_getBlockByHash, eth_getBlockTransactionCountByNumber, eth_getBlockReceipts, eth_getTransactionByBlockNumberAndIndex, eth_feeHistory, eth_getLogs, eth_call, eth_estimateGas, igneum_estimateGas) at the rpc-filter layer on seed1 (BLOCKED_UNTIL_FIXED_NODE in infra/seed-nodes/rpc/rpc-filter.py; seeds 2 and 3 expose no RPC). Cause: a node whose exec follower holds no record (a restart or a fresh IBD before the first record; the seeds at height 0 DO hold the genesis record and answered block 0 at 06:5xZ, so the window is the restart) panics on records[0] and the unit restarts it after 10 s. The methods go back once the fixed node (0.3.18, the node lane's bounds-check commit) is on every seed; the go checklist reads that commit back on each seed first.

View file

@ -13,6 +13,13 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
UPSTREAM = os.environ.get("RPC_UPSTREAM", "http://127.0.0.1:26890")
LISTEN = os.environ.get("RPC_LISTEN", "127.0.0.1:8545")
BODY_LIMIT = 256 * 1024
# BLOCKED 7 October 2026 (main's order): on a node whose exec follower holds no record yet (a seed at height 0, any fresh or
# restarting node mid-IBD) every method that resolves a block tag and indexes the records vector panics on records[0] and the
# node's panic hook exits the process (one request, a 10 s outage per unit restart). These go back once the fixed node
# (0.3.18, igneum/exec/src/rpc.rs bounds-checked) is on every seed. Live on seed1 since 06:5xZ.
BLOCKED_UNTIL_FIXED_NODE = {
"eth_call", "eth_estimateGas", "eth_feeHistory", "eth_getBlockByHash", "eth_getBlockByNumber", "eth_getBlockReceipts", "eth_getBlockTransactionCountByNumber", "eth_getLogs", "eth_getTransactionByBlockNumberAndIndex", "igneum_estimateGas",
}
ALLOWED = {
# eth: reads
"eth_chainId", "eth_blockNumber", "eth_getBalance", "eth_getCode", "eth_getStorageAt", "eth_getTransactionCount",
@ -27,7 +34,7 @@ ALLOWED = {
"igneum_getShardPlan", "igneum_getAssignedShards", "igneum_getTransactionStatus",
# identity
"net_version", "net_listening", "net_peerCount", "web3_clientVersion",
}
} - BLOCKED_UNTIL_FIXED_NODE
def err(id_, code, msg):
return {"jsonrpc": "2.0", "id": id_, "error": {"code": code, "message": msg}}
@ -94,6 +101,7 @@ class H(BaseHTTPRequestHandler):
def selftest():
ok = lambda b: filter_body(json.dumps(b).encode())[0]
assert ok({"jsonrpc": "2.0", "id": 1, "method": "eth_chainId", "params": []})
assert not ok({"jsonrpc": "2.0", "id": 1, "method": "eth_getBlockByNumber", "params": ["latest", False]}) # blocked until the fixed node
assert ok({"jsonrpc": "2.0", "id": 1, "method": "eth_sendRawTransaction", "params": ["0x00"]})
assert ok({"jsonrpc": "2.0", "id": 1, "method": "igneum_getBudgets", "params": []})
assert not ok({"jsonrpc": "2.0", "id": 1, "method": "igneum_submitProofRecord", "params": []})