Skip to content

Forward-port #914 and #915 (0.6.1 StartOS fixes) to master - #926

Merged
bkeroack merged 2 commits into
masterfrom
fix/fwdport-061-startos
Oct 7, 2026
Merged

bkeroack merged 2 commits into
masterfrom
fix/fwdport-061-startos

Conversation

@bkeroack

@bkeroack bkeroack commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

Brings the two Start9 fixes merged into release/0.6 for 0.6.1 back to master, one commit each, cherry-picked with -x:

Both commits applied cleanly except for the release-notes files. The code patch is identical to the one on release/0.6: the git patch-id of 7ad4d894^..a0b69c6f (all paths except CHANGELOG.md and docs/release-notes/) equals that of this branch's diff. The manual changes (ibd.md, packaging.md) come along.

Release notes

The two CHANGELOG.md bullets and their docs/release-notes/0.6.1-pre.md sections stay on release/0.6. They describe 0.6.1 and will reach master with the 0.6.1 cut, as in #921. Nothing is added to 0.7.0-pre.md.

Merging

"Rebase and merge" keeps the two commits separate, each with its cherry picked from line.

Verified

Ran locally on this branch:

  • cargo clippy --all-targets --all-features --locked -- -D warnings
  • cargo test --test e2e --no-run --locked --features e2e
  • the node lib suite: 2147 passed
  • the satd bin tests: 307 passed
  • the regtest reindex and SIGTERM tests: 16 passed

On release/0.6, each PR passed CI and a Devin review before it merged.

A follow-up, #916 (the reindex scan reads only block headers), is in review against release/0.6 and will need its own forward-port.

🤖 Generated with Claude Code

bkeroack and others added 2 commits October 6, 2026 18:41
…914)

* Stop a startup block replay cleanly on SIGTERM, also as PID 1 (#907)

satd registered its SIGTERM and SIGINT handlers only once startup had
finished, and the block replays of -reindex, -reindex-chainstate and
-upgradechainstate run inside startup. Until then a stop signal had its
default effect: it killed the replay part-way through a batch, or, with
satd as PID 1 of its PID namespace (a container with no init), it was
ignored until the supervisor's SIGKILL. Start9's review of the StartOS
package hit the second: docker stop ran out its 600 s grace period.

Both signals are now registered at the top of run(). Until startup
finishes, a watcher acts on them through node::shutdown::StartupStop:

- During a replay the stop is left to the replay, which polls before
  each block, and between block files while scanning. It stops after
  the block in hand, flushes durably, and returns Stopped (flat-file
  reindex) or stopped (chainstate replay). satd keeps the rebuild marker
  and exits 0. If that takes longer than -maxshutdownsecs, the watcher
  exits without it.
- Anywhere else in startup the process ends at once, as before: the
  default action is restored and the signal raised again. As PID 1 the
  raise is ignored, so satd exits with 128 + the signal number.

The wait loop takes the same registrations once startup has finished.
A shutdown after startup now also logs each wait before it starts (the
block connector, the UTXO cache flush), with its budget, so a stop that
runs long says in its last line what it is waiting on.

Tests: unit tests stop each replay at a chosen poll, and each poll
fails its test when removed. Integration tests send SIGTERM as the
connect phase of -reindex and -reindex-chainstate starts, against a
6000-block fixture; satd as PID 1 under sudo unshare --pid (CI only;
skipped without passwordless sudo, failing under CI); and SIGTERM
during IBD of 1 MB blocks, which must exit 0 within 10 s with the
shutdown waits logged in order.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Abandon the block in hand when a replay is asked to stop (#907)

A stop request used to wait for the block being connected to finish. On a
slow disk that is the slow part: the flat-file replay has no prefetcher,
so every coin lookup that misses the cache is a random read. On a hard
drive a block's lookups alone can take seconds.

connect_block only builds a batch and writes nothing, so a block it
abandons leaves the chainstate at the block before. ConnectParams gains
`interrupt`, which the two startup replays set to their stop request.
connect_block polls it:

- after each chunk of 1024 coin lookups (one batch when `interrupt` is
  None, so the IBD path is unchanged);
- after the lookups;
- every 64 transactions of script verification, in the serial and the
  parallel path.

It returns ConnectError::Interrupted, which is not a verdict on the
block. The replays turn it into the same flushed stop as one between
blocks. A script failure another verification worker reached outranks
the stop.

Tests: connect_block's three polls each stop it with no batch. Both
replays abandon block 6 when the poll inside its connect requests the
stop, and end at block 5. Each poll and each mapping fails a named test
when removed. The parallel-path poll is not covered: the fixture block
has a single transaction to verify, so it takes the serial path.

The SIGTERM-during-IBD test no longer asserts that the node stopped below
the tip. The signal is sent mid-sync, which the test still checks, but on
a fast CI runner the connector had connected the remaining large blocks
before it saw the stop.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Mine the replay test fixture until it is long enough, tolerating timeouts

The shared 6000-block fixture was mined in calls of 100 blocks through
rpc_ok, which panics on a transport error. With other regtest tests
running beside it, a call can outlast the harness's 10 s RPC timeout:
the fixture failed twice that way locally, taking a test with it.

It now mines 25 blocks a call and loops on getblockcount until the chain
is long enough, ignoring a failed call: one that timed out may still
have mined its blocks, so the count, not the call, decides.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 7ad4d89)
) (#915)

* Continue an interrupted -reindex from the block files on any start (#906)

0.6.0 refused every start after an interrupted full -reindex but one with
--reindex, which then wiped the database and replayed from genesis. A
mainnet replay takes days, so every stop, reboot or settings change during
one threw it away, and a package that passes --reindex once (StartOS's
migration from Bitcoin Core) was left with a node that would not start.

Now any start that finds an unfinished full reindex continues it, whatever
the flags. The flat-file replay scans the block files and plans the chain
as before. It then checks every block the interrupted run had stored
against the scan, before connecting the rest:

- the stored tip must be the planned block at its height;
- each entry at or below it must be indexed at its planned height as
  Valid, with the header the files hold, at the position the scan found;
- each entry's chainwork must equal the sum of its ancestors' header work.

What the continued replay reads from those entries (parent links,
chainwork, the timestamps behind median time past) is then backed by the
block files, as in a replay from genesis. Nothing is downloaded and no
block file is written.

A reindex is continued only when its marker says so. The rebuild marker
gains `resumable`, set once the wipe has finished and genesis is indexed
and durable, just before the scan. A marker without it, from satd 0.6.0
or written before its wipe had finished, or one from another chainstate
schema, runs the reindex again from genesis, still on any start. A
stored chain that fails the check exits with ReindexCannotResume, having
changed nothing; satd clears `resumable`, and the next start runs from
genesis.

A fresh reindex now refuses a chainstate past genesis. A continued run
that had already reached -stopatheight connects nothing more, and indexes
no side chain. A stop requested by #907's signal handling is continued
by the next start like any other interruption.

Tests: unit tests continue a stopped reindex onto the uninterrupted UTXO
set, refuse a stored entry the files do not back, refuse a fresh reindex
over a stored chain, handle a continued run past -stopatheight, and read
a 0.6.0 marker as not resumable. Integration tests SIGKILL a reindex after
its durable flush at height 1000, and during its scan, then check that a
plain start continues: it logs so, clears nothing, writes no block file,
and ends on the fixture's UTXO set. A 0.6.0 marker runs again from
genesis on a plain start. #907's SIGTERM tests now finish with a plain
start that continues.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Check rows above the tip and poll for a stop before continuing a reindex (#906)

The check before a continued reindex now also requires every height row
above the stored tip to name the planned block at its height. A connect
that moves no coins bypasses the coin cache's buffer, so a kill can leave
its rows durable above the tip; over the same block files the replay
writes them again, but a row naming another block would outlive it.

The check also polls for a stop every 1000 entries. On mainnet it reads
some 900k index entries, and a stop that waited for it could overrun
-maxshutdownsecs and exit 1.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit a0b69c6)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant