Ask the node only what the whitelist allows, and only when needed - #27
Merged
Merged
Conversation
Two defects, both found by reading the node's configuration rather than by running the suite -- which is the point worth keeping. The follower called `getblockheader`. The node's RPC whitelist for this service grants scantxoutset, getblockchaininfo, getblockcount, getbestblockhash, getblockhash, getblock, getrawmempool and uptime. Not that one. So every test passed against a fake that answers whatever it is asked, and the first real catch-up would have failed on a production node with a whitelist that is doing exactly what it is meant to do. `getblock` at verbosity 1 carries the same two fields -- height and confirmations -- plus a txid list we ignore. A larger response in exchange for not widening a whitelist, which is the right trade: the whitelist is the difference between a compromised service being able to read the chain and being able to act on the node. A restart to add the method was available and was not taken for that reason. The second defect was in the same place and cost more. Every block applied asked the node for its height, including on the contiguous path that runs every ten minutes forever. The child of the block we just applied is one above it by definition, so that was a round trip to be told what we already knew -- and a call that can fail during an outage is a call worth not making. Heights are now counted locally and asked for only on the catch-up path and for the very first block. A test asserted this the wrong way round: it was named "without asking the node for anything" while asserting that the node was asked for a header. It now asserts what its name claims, and a second test asserts `getblockheader` is never called at all, because the forbidden call is the natural one to reach for. Separately, the sweep was leaving mutations on disk. `try/finally` covers exceptions and Ctrl-C. It does not cover SIGTERM, which is what `timeout` sends, what CI sends on cancellation, and what a supervisor sends first. A sweep run under `timeout` that hit its limit left the last mutation applied, silently: the shell reports 124, the tree looks fine, and the source quietly contains broken code. This was not hypothetical -- it left the master secret length check as `if False:` in a working tree today, which is a disabled security check one `git commit -a` away from being real. Fixed in the order the failures matter. A handler restores on SIGTERM and SIGINT. A journal written before each mutation lets the *next* run repair what a SIGKILL, an OOM kill or a power cut left behind, since none of those can be intercepted -- the same reasoning as the scan supervisor's startup abort, which exists for the same reason. Both paths were verified by actually killing a sweep each way and checking the tree. Refs #24 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two defects in the chain follower and one in the mutation sweep. All three were found by reading the node's configuration and by killing a process — not by running the suite, which was green throughout.
The follower called an RPC the node does not allow
The node's RPC whitelist for this service grants
scantxoutset,getblockchaininfo,getblockcount,getbestblockhash,getblockhash,getblock,getrawmempoolanduptime.getblockheaderis not on it. The follower called it for every height lookup and every active-chain check, so the first real catch-up would have failed against a production node whose whitelist is doing exactly what it is meant to do — while passing every test here, because a fake answers whatever it is asked.getblockat verbosity 1 carries the same two fields,heightandconfirmations, plus a txid list we ignore. That is a larger response in exchange for not widening a whitelist, and it is the right way round: the whitelist is the difference between a compromised service being able to read the chain and being able to act on the node. A restart to add the method was available and deliberately not taken.A test now asserts
getblockheaderis never called, because the forbidden call is the natural one to reach for and this will be rewritten by someone eventually.Every block asked the node for its height
Including on the contiguous path — the one that runs every ten minutes forever. The child of the block we just applied is one above it by definition, so that was a round trip to be told what we already knew, and a call that can fail during an outage is a call worth not making. Heights are now counted locally, and asked for only on the catch-up path and for the very first block.
The test covering this asserted the opposite of its own name:
test_a_contiguous_block_is_applied_without_asking_the_node_for_anythingasserted that the node was asked for a header. It now asserts what it claims.The mutation sweep was leaving broken code on disk
This is the one worth reading.
try/finallycovers exceptions and Ctrl-C. It does not cover SIGTERM — which is whattimeoutsends, what CI sends when a job is cancelled, and what most supervisors send first. A sweep run astimeout 900 python tools/mutate.pythat hit its limit left the last mutation applied. Silently: the shell reports 124, the tree looks fine, and the source quietly contains deliberately broken code.Not hypothetical. It left this in a working tree today:
A disabled key-length check, one
git commit -aaway from being real.Fixed in the order the failures matter:
node/supervisor.py's startup abort, which exists because the only complete answer to "did a previous run leave something running?" is to ask on the way in.Both paths were verified by killing a sweep each way and inspecting the tree, not by reasoning about them:
restored … — a previous run was killed mid-mutationand repaired it before its baseline.tools/mutate.pyunder a timeout before this lands, checkgit diff src/on that tree. The failure leaves no trace in the exit status.Checks
176 tests, ruff clean, mutation sweep 67/67 with 3 documented equivalents.
Refs #24
🤖 Generated with Claude Code