From 581af1e134c7063e2ee8e2be8da9ee5bfe4b1198 Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Wed, 7 Oct 2026 10:16:16 +0000 Subject: [PATCH] ledger N7: the second caller (the prover loop, rpc.rs:808) and the whole-class gate; the macOS listener shape Co-Authored-By: Claude Fable 5.1 --- docs/fud-ledger.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/fud-ledger.md b/docs/fud-ledger.md index 32d57c3af..438cdd678 100644 --- a/docs/fud-ledger.md +++ b/docs/fud-ledger.md @@ -2362,4 +2362,4 @@ Open 6 October 2026 22:43Z to the 0.3.17 publish (the fix rides 0.3.17; main's r ### 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 as c6a62e00 (every chain-block read bounds-checked; an exec state with no record answers an error; test on the empty state), into release-0.3.18-node as its third merge (e69e8a39, 06:53Z); 0.3.18 carries it. The method map (7 October 07:0xZ, settled against a 0.3.17.1 cut): the dead node's site is eth_getBlockByNumber (rpc.rs:689 in 6e4ace3f). The shipped 0.3.17 Igneum Miner app sends twelve exec RPC methods, all from prover.rs: eth_blockNumber and eleven igneum_* reads and submits; no block-tagged eth_ method. Of those, igneum_getAssignedShards (records[from..=tip], from = max(tip-lookback, 1)) and igneum_getProofRecords (records[n] after resolve_block) panic on an empty vector too, but the prover loop sends them only once the node reads synced, and while unsynced sends only igneum_getExecStatus and igneum_getProvingStatus, which index nothing; on the devnet the exec follower holds records from the packaged restart block (27276) early in the block stage, before isSynced flips. The Igneum Wallet app sends eth_getBlockByNumber("latest") (igneum-wallet/src/evm.rs:58) to its own bundled node (26800). Public filter block extended to the five igneum_* allowlist reads that index records (getProofRecords, getAssignedShards, getSegment, getShardPlan, getTransactionStatus). CORRECTION 08:0xZ (the second, and the right one): the shipped engine ITSELF sends eth_getBlockByNumber ["latest", false] to the node's exec port through curl every 9 s as its clock sample (app/igneum-app/src/update.rs:46 `latest_block_time`, called from engine.rs:3752 whenever the node reports blocks > 0 and peers > 0). The first method map missed it because the method sits inside an escaped JSON string literal; the export-pack reading was export-pack failing after the node had died. Consequence on 0.3.17: every node with the app attached whose exec follower has no record when blocks arrive dies within 9 s and the app restarts it: PC 1's loop after the project lead's reopen (the follower does not reach a record in the 2 to 5 s before the sample; the Mac and PC 2 loaded their snapshots first), and a FRESH install's IBD (blocks arrive after the headers stage while the follower waits for consensus to sync), so a new 0.3.17 home miner crash-loops and never syncs; the hotfix canary ran with no app attached. The export-pack and the wallet are not the senders. Every known machine survived the night only by loading its exec snapshot before the first sample. Main's word (08:0xZ): 0.3.18 publishes on the canary's synced line plus the poller summary (the decisive reads for this fault), PC 1's update-now first, then PC 2, the Mac, the fleet one box at a time under the lock rule, the hands and the seed by their owner; the restart reads and the cases follow as confirmation, a FAIL there being a rebuild, not a rollback of this fix. Ruling (main, 07:0xZ): 0.3.18 carries the fix; no 0.3.17.1; PC 1 takes 0.3.18 first on the publish line. Status: OPEN until 0.3.18 is live and the seeds run it. +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 as c6a62e00 (every chain-block read bounds-checked; an exec state with no record answers an error; test on the empty state), into release-0.3.18-node as its third merge (e69e8a39, 06:53Z); 0.3.18 carries it. The method map (7 October 07:0xZ, settled against a 0.3.17.1 cut): the dead node's site is eth_getBlockByNumber (rpc.rs:689 in 6e4ace3f). The shipped 0.3.17 Igneum Miner app sends twelve exec RPC methods, all from prover.rs: eth_blockNumber and eleven igneum_* reads and submits; no block-tagged eth_ method. Of those, igneum_getAssignedShards (records[from..=tip], from = max(tip-lookback, 1)) and igneum_getProofRecords (records[n] after resolve_block) panic on an empty vector too, but the prover loop sends them only once the node reads synced, and while unsynced sends only igneum_getExecStatus and igneum_getProvingStatus, which index nothing; on the devnet the exec follower holds records from the packaged restart block (27276) early in the block stage, before isSynced flips. The Igneum Wallet app sends eth_getBlockByNumber("latest") (igneum-wallet/src/evm.rs:58) to its own bundled node (26800). Public filter block extended to the five igneum_* allowlist reads that index records (getProofRecords, getAssignedShards, getSegment, getShardPlan, getTransactionStatus). CORRECTION 08:0xZ (the second, and the right one): the shipped engine ITSELF sends eth_getBlockByNumber ["latest", false] to the node's exec port through curl every 9 s as its clock sample (app/igneum-app/src/update.rs:46 `latest_block_time`, called from engine.rs:3752 whenever the node reports blocks > 0 and peers > 0). The first method map missed it because the method sits inside an escaped JSON string literal; the export-pack reading was export-pack failing after the node had died. Consequence on 0.3.17: every node with the app attached whose exec follower has no record when blocks arrive dies within 9 s and the app restarts it: PC 1's loop after the project lead's reopen (the follower does not reach a record in the 2 to 5 s before the sample; the Mac and PC 2 loaded their snapshots first), and a FRESH install's IBD (blocks arrive after the headers stage while the follower waits for consensus to sync), so a new 0.3.17 home miner crash-loops and never syncs; the hotfix canary ran with no app attached. The export-pack and the wallet are not the senders. Every known machine survived the night only by loading its exec snapshot before the first sample. Main's word (08:0xZ): 0.3.18 publishes on the canary's synced line plus the poller summary (the decisive reads for this fault), PC 1's update-now first, then PC 2, the Mac, the fleet one box at a time under the lock rule, the hands and the seed by their owner; the restart reads and the cases follow as confirmation, a FAIL there being a rebuild, not a rollback of this fix. Ruling (main, 07:0xZ): 0.3.18 carries the fix; no 0.3.17.1; PC 1 takes 0.3.18 first on the publish line. The second caller, PC 1 on 0.3.18 at 09:1xZ: the prover loop's first igneum_getAssignedShards after the node read synced, "thread 'tokio-rt-worker' panicked at igneum/exec/src/rpc.rs:808:35: range start index 1 out of range for slice of length 0" (records[1..=0] on an empty vector), the node restarting every 40 s. Main's rule: fix the class, not the caller: 0.3.19's app routes every exec RPC call through one gate (execrpc.rs: safe-on-empty methods pass, records-indexing ones wait for igneum_getExecStatus's executed tip, an enumerating test names the callers). On macOS the same panic kills the listener task alone and the process lives (the Mac's 0.3.17 node, 26790 refused with gRPC up); the node lane offers a listener watchdog for 0.3.20. Status: OPEN until 0.3.18 is live and the seeds run it.