Repository navigation
Pin the overnight missed mutants in confirm, log, and the mempool - #1016
Conversation
The 5s progress window, the 1 GiB inflight line, parent-check waits, and reject re-arms stay on the block path. Tests pin the boundaries a peer message still reaches, and the empty lookup wave is an early match arm.
Stderr lines, bold terminals, datadir parents, and API-log close and truncation stay operator-visible. Tests read the bytes those paths write.
Package shape, replacement pay, compact, and the recent-reject caps stay on the admit path. A zero-weight pure RBFR guard is the vsize check. Tests cover the admits and the full pinned slot table.
Bucket edges, the descending clip, and the admit EMA stay the rates a template reads. Tests pin the bucket count, the percentile, and two admits.
Cluster rank is rate then fee. Equal rate and fee keep the earlier txid because the walk is txid order. Rebuild still inserts a parent before every spender of its output.
The weight cap and the counters stay what an orphan admission observes. The final overweight check no longer repeats the loop predicate.
Sequence final is a named bit. A record with no outputs makes a short script or witness fail as corrupt packed bytes.
A due persist writes only the new live rows. Abandon and compact still clear or reject every live slot when the table is full.
Confirm progress, parent waits, and reject rewinds go through the functions the confirm thread calls. A disconnect during the parent read is the public reorg note. Compaction uses the dead-slot quarter, equal floored chunk rates keep the earlier chunk, eviction stops when a slot is free, and a package that spends a later txid is not topological. Made-with: Cursor
A disconnect noted while that read is parked is a retry. The sample no longer depends on a second thread winning a race during a coinbase lookup. Made-with: Cursor
The hand-rolled byte walk is the same cut as str::floor_char_boundary. A mid-character prefix no longer has a decrement that can spin. Made-with: Cursor
node_run_p2p_short reserved RPC, Electrum, and Esplora addresses and dropped the sockets before run_p2p bound them. On a busy runner another test took the port, so Electrum read a non-UTF-8 stream or Esplora never accepted, and the 60s wall fired. Configure port 0 and read the address run_p2p publishes after it binds. Made-with: Cursor
A removed tx leaves a DEAD slot, and DEAD is a free slot, so the 10000 eviction cap cannot run. A one-tx self-spend is not a peer message, because the txid commits to the prevout, so the index compare is gone. The tests call accept and check_package_shape and keep those operators out of the functions. Made-with: Cursor
ecc849e to
56a5733
Compare
Deleting the success check on evict_oldest stops after one orphan, so a weight cap that needs two evictions rejects the incoming tx. The free-slot and dead-slot increments each fail slot_stats on their own count. Made-with: Cursor
There was a problem hiding this comment.
Summary
The production edits pin overnight mutants by deleting checks the node cannot reach and by extracting confirm reject paths, without changing what a block, a peer package, or a reopened pool does. Every rewritten predicate checked below is equivalent on the inputs the node can actually see: package topology, free-slot eviction, RBFR zero-weight, orphan weight, chunk tie-break, UTF-8 truncation, and the confirm requeue filters. The one new call, spindle_wait before the missing-parent tx.head read, is the same HDD gate lookup and load already use; a disconnect or tip move during that wait retries the block instead of keeping a verdict. No consensus, denial-of-service, or security failure remains. note_disconnect_height is now pub but its body is unchanged and production still calls it only from disconnect.
Issue counts by severity
- bugs: 0
- suggestions: 0
- nits: 0
Adversarial check of production edits
No remote input can take a path these edits newly open. Each deleted or rewritten predicate was compared with the old expression and with the callers that feed it.
- Confirm requeue.
prepare_offer_backalready drops hasheshas_blockreports. Removing the second filter in the stale-plan path does not put a connected body back on the queue.script_reject_wave,apply_write_reject, andrewind_after_load_rejectare the old conditions moved into functions: a one-block verdict still drops its block; a retry, engine fault, or session fault still keeps the wave; a pin-stage engine fault that is not a uring session fault still rewinds as a cascade. The lookup match split is the same partition: an empty wave with no undecodable item only clears the fault counter; any otherOkstill enters the body path and callson_success. - Parent read.
spindle_waittakes the datadir-hdd lock and drops it. It is a no-op when the flag is off.parent_missing_from_chaindoes not take that lock, so this is not a deadlock. The wait sits after the 10s parent loop, and the followingstop/ready/take_disconnectchecks still discard a verdict if the tip moves during the wait. The parent-check clock is thread-local and unset in production, so the confirm thread still usesInstant::nowand a 2ms sleep. - Mempool eviction. The 10_000-iteration cap is gone. The loop still stops when a FREE or DEAD slot exists, or when a pass removes nothing.
unlink_txidmarks the slot DEAD before it drops the graph entry, and DEAD is a free slot. A missing body fails the unlink and the next pass returns empty, so a stuck tx cannot spin the accept thread. - Package shape. Duplicates are still rejected first. The shrinking
seenset rejects an input that spends this tx or a later package tx, and allows an earlier one. That is the same rule asparent position < i, including a self-spend. - RBFR.
get_virtual_sizeis(weight + 3) / 4, which is 0 only at weight 0. The vsize check replaces the weight-0 check. - Orphan cap.
max.saturating_sub(total) < weightmatchessaturating_add(weight) > maxwhenever the cap is belowu64::MAXor the tx weight is non-zero. Abitcoin::Transactionweight is never 0. At au64::MAXcap the new test is stricter (it rejects an add that saturates), which does not admit extra orphans. - Chunk order.
linearize_deltawalks aBTreeSet, so equal rate and fee already keep the lesser txid. Dropping the third compare does not change the pick. The component-rep||is the same two continues.enforce_monotone_descminmatches the old>assign. - Disk format.
VIN_SEQ_FINALis still 1.u32::try_from(payload.len())matcheslen > u32::MAX. Adjacent slot records are stillprev_end == off.1 << 30is the same 1 GiB inflight line. - Logging.
str::floor_char_boundaryis called atmax.min(len), so it does not panic, and it stops on the same UTF-8 boundary as the old loop. No new attacker-controlled format string. note_disconnect_height. Body still stores the height, bumps the generation, rewinds the lookup watermarks, and drops resolved queue entries from that height. Widening it frompub(crate)topubis what lets the confirm test call it. The node still invokes it only from disconnect. It is not reachable from a peer message.
Tests and the changelog fragment do not change node behavior.
A three-tx submitpackage whose parent and child sit under the relay floor admits only when the grandchild pays, so a walk that stops at the direct parent fails the RPC. Parent-plus-unrelated testmempoolaccept still requires seen.contains before it will allow the parent. The in-crate child-shape twin is gone. Direct parents stay on the function submitpackage calls when the dialect flag is set, because setting that variable races every other package RPC in the process. The scan that the old index compare stays deleted stays with that pin.
api_call writes the operator JSONL. A param that straddles the 384-byte cap keeps the char-safe prefix in that file, and a param with quotes and control characters is escaped there. The helper unit tests are gone.
Fee arithmetic, chunk ties, orphan caps, packed schema, slot coalescing, and the confirm clocks still run on a session. Flipping them does not change a peer message, an RPC answer, or bytes a later open reads. Say that narrow case on the test.
getblocktemplate lists a cluster by rate, then fee, then the lesser txid, and omits a child prioritisetransaction pushed below zero. A full pool's getrawmempool drops the lighter of two chunks that floor to the same sat/kvB. The in-crate pins of those compares are gone.
An empty item list already starts a new stack item, because a missing last length is treated as a full 520-byte item. The second empty check was the same push, and clippy rejects it.
Summary
Pins the 141 mutants the 2026-10-14 overnight run left missed in confirm, logging, and the mempool. Where an operator could not change a block, a peer message, or a reopened pool, the expression is gone. The rest are pinned by an existing journey or by a disk, peer, or operator-visible check.
Confirm progress, parent waits, and reject rewinds run through the functions the confirm thread calls. A disconnect during the parent read is the public reorg note. Mempool compaction uses the dead-slot quarter, equal floored chunk rates keep the earlier chunk, eviction stops when a slot is free, and a package that spends a later txid is not topological.
Test plan
cargo fmt, clippy-D warnings,cargo deny, ast-grep, andci-os-smoke.shqc,test,windows,macos, andcoverage