Skip to content

Pin the overnight missed mutants in confirm, log, and the mempool - #1016

Merged
reardencode merged 19 commits into
masterfrom
mutants/overnight-38516921540
Oct 15, 2026
Merged

reardencode merged 19 commits into
masterfrom
mutants/overnight-38516921540

Conversation

@rearden-grok

@rearden-grok rearden-grok Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

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

  • Hand-applied each missed operator (or confirmed the expression is gone) against the targeted crate test
  • cargo fmt, clippy -D warnings, cargo deny, ast-grep, and ci-os-smoke.sh
  • GitHub qc, test, windows, macos, and coverage

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
@rearden-grok
rearden-grok Bot force-pushed the mutants/overnight-38516921540 branch from ecc849e to 56a5733 Compare October 14, 2026 20:09
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

@rearden-grok rearden-grok Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_back already drops hashes has_block reports. 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, and rewind_after_load_reject are 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 other Ok still enters the body path and calls on_success.
  • Parent read. spindle_wait takes the datadir-hdd lock and drops it. It is a no-op when the flag is off. parent_missing_from_chain does not take that lock, so this is not a deadlock. The wait sits after the 10s parent loop, and the following stop / ready / take_disconnect checks 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 uses Instant::now and 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_txid marks 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 seen set rejects an input that spends this tx or a later package tx, and allows an earlier one. That is the same rule as parent position < i, including a self-spend.
  • RBFR. get_virtual_size is (weight + 3) / 4, which is 0 only at weight 0. The vsize check replaces the weight-0 check.
  • Orphan cap. max.saturating_sub(total) < weight matches saturating_add(weight) > max whenever the cap is below u64::MAX or the tx weight is non-zero. A bitcoin::Transaction weight is never 0. At a u64::MAX cap the new test is stricter (it rejects an add that saturates), which does not admit extra orphans.
  • Chunk order. linearize_delta walks a BTreeSet, 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_desc min matches the old > assign.
  • Disk format. VIN_SEQ_FINAL is still 1. u32::try_from(payload.len()) matches len > u32::MAX. Adjacent slot records are still prev_end == off. 1 << 30 is the same 1 GiB inflight line.
  • Logging. str::floor_char_boundary is called at max.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 from pub(crate) to pub is 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.
Comment thread crates/rbitcoin-rpc/src/methods_tests.rs Dismissed
Comment thread crates/rbitcoin-rpc/src/methods_tests.rs Dismissed
Comment thread crates/rbitcoin-rpc/src/methods_tests.rs Dismissed
Comment thread crates/rbitcoin-rpc/src/regtest_chain_ops_journey.rs Dismissed
Comment thread crates/rbitcoin-rpc/src/regtest_chain_ops_journey.rs Dismissed
@reardencode
reardencode merged commit df36845 into master Oct 15, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants