ACP: ResetSyncCheckpoint asserts on fresh-datadir init when latest hardened checkpoint is genesis (mis-ported SetBestChain -> ConnectTip) #48

Closed
opened 2026-07-24 01:48:45 +00:00 by dobbscoin · 2 comments
dobbscoin commented 2026-07-24 01:48:45 +00:00

Symptom

A v2.1.0 (post-#40) daemon started against a fresh testnet datadir aborts during init:

Offeringsd: main.cpp:2421: bool ConnectTip(CValidationState&, CBlockIndex*):
Assertion `pindexNew->pprev == chainActive.Tip()' failed.

Repro: Offeringsd -testnet -datadir=<empty dir> on any binary containing the #40 ACP wiring. 100% reproducible.

Root cause

InitBlockIndex() → CheckCheckpointPubKey() sees no stored pubkey in a fresh leveldb, treats the master key as changed, and calls ResetSyncCheckpoint() (checkpoints.cpp). That function's guard:

if (mapBlockIndex.count(hash) && !mapBlockIndex[hash]->IsInMainChain())

uses the status-flag CBlockIndex::IsInMainChain() (nStatus >= BLOCK_VALID_CHAIN). During fresh init, genesis is already the active tip but has not yet been flagged chain-valid, so the branch misfires with hash = genesis (testnet's latest hardened checkpoint IS genesis) and calls ConnectTip(genesis) — whose precondition pindexNew->pprev == chainActive.Tip() cannot hold for a block with no pprev when the tip is genesis itself.

Underlying mis-port: the ppcoin lineage called SetBestChain(state, pindex) here — a force-reorg-to-arbitrary-block primitive that does not exist in the 0.10 codebase. The port substituted ConnectTip, which can only extend the current tip by one block. The original call is still present, commented out, one line above.

Blast radius

  • Fresh testnet datadirs: hard crash on first start. (Found attempting the v2.1.0-Nodens testnet soak.)
  • Fresh mainnet datadirs: unaffected — the mainnet hardened checkpoint is not in the index yet, so the hashPendingCheckpoint path is taken.
  • Existing synced datadirs (the one-time pubkey-migration fire on first post-upgrade restart): unaffected — checkpoint is chain-valid, guard is false.
  • Regtest: unaffected — no Conclave key ⇒ ACP disabled ⇒ CheckCheckpointPubKey returns early. This is why the #40 regtest suite never exercised the path.

Fix

In ResetSyncCheckpoint():

  1. Test chain membership against chainActive.Contains() instead of the status-flag IsInMainChain().
  2. Only call ConnectTip when the checkpoint block directly extends the current tip (pindexCkpt->pprev == chainActive.Tip()); otherwise log and leave the reorg to the normal ActivateBestChain machinery instead of asserting the daemon down.
  3. Drop the dead ReadBlockFromDisk (the read block was never used).

Fix lands on feat/v2.1.0-nodens-prep; fresh-testnet-datadir start becomes part of the soak checklist.

## Symptom A v2.1.0 (post-#40) daemon started against a **fresh testnet datadir** aborts during init: ``` Offeringsd: main.cpp:2421: bool ConnectTip(CValidationState&, CBlockIndex*): Assertion `pindexNew->pprev == chainActive.Tip()' failed. ``` Repro: `Offeringsd -testnet -datadir=<empty dir>` on any binary containing the #40 ACP wiring. 100% reproducible. ## Root cause `InitBlockIndex()` → `CheckCheckpointPubKey()` sees no stored pubkey in a fresh leveldb, treats the master key as changed, and calls `ResetSyncCheckpoint()` (checkpoints.cpp). That function's guard: ```cpp if (mapBlockIndex.count(hash) && !mapBlockIndex[hash]->IsInMainChain()) ``` uses the **status-flag** `CBlockIndex::IsInMainChain()` (`nStatus >= BLOCK_VALID_CHAIN`). During fresh init, genesis is already the active tip but has not yet been flagged chain-valid, so the branch misfires with `hash` = genesis (testnet's latest hardened checkpoint IS genesis) and calls `ConnectTip(genesis)` — whose precondition `pindexNew->pprev == chainActive.Tip()` cannot hold for a block with no `pprev` when the tip is genesis itself. Underlying mis-port: the ppcoin lineage called `SetBestChain(state, pindex)` here — a force-reorg-to-arbitrary-block primitive that does not exist in the 0.10 codebase. The port substituted `ConnectTip`, which can only extend the current tip by one block. The original call is still present, commented out, one line above. ## Blast radius - **Fresh testnet datadirs: hard crash on first start.** (Found attempting the v2.1.0-Nodens testnet soak.) - Fresh mainnet datadirs: unaffected — the mainnet hardened checkpoint is not in the index yet, so the `hashPendingCheckpoint` path is taken. - Existing synced datadirs (the one-time pubkey-migration fire on first post-upgrade restart): unaffected — checkpoint is chain-valid, guard is false. - Regtest: unaffected — no Conclave key ⇒ ACP disabled ⇒ `CheckCheckpointPubKey` returns early. This is why the #40 regtest suite never exercised the path. ## Fix In `ResetSyncCheckpoint()`: 1. Test chain membership against `chainActive.Contains()` instead of the status-flag `IsInMainChain()`. 2. Only call `ConnectTip` when the checkpoint block *directly extends the current tip* (`pindexCkpt->pprev == chainActive.Tip()`); otherwise log and leave the reorg to the normal `ActivateBestChain` machinery instead of asserting the daemon down. 3. Drop the dead `ReadBlockFromDisk` (the read block was never used). Fix lands on `feat/v2.1.0-nodens-prep`; fresh-testnet-datadir start becomes part of the soak checklist.
dobbscoin commented 2026-07-24 01:50:18 +00:00

Fix landed on feat/v2.1.0-nodens-prep at cabb1162 and verified against the original repro: a fresh testnet datadir now initializes cleanly (ResetSyncCheckpoint: sync-checkpoint reset to <genesis>) and syncs from its peers. Existing datadirs take the unchanged already-on-chain path. Issue closes when the branch merges to main; fresh-datadir first-start on all three networks is now a standing item on the release soak checklist.

Fix landed on `feat/v2.1.0-nodens-prep` at cabb1162 and verified against the original repro: a fresh testnet datadir now initializes cleanly (`ResetSyncCheckpoint: sync-checkpoint reset to <genesis>`) and syncs from its peers. Existing datadirs take the unchanged already-on-chain path. Issue closes when the branch merges to main; fresh-datadir first-start on all three networks is now a standing item on the release soak checklist.
dobbscoin commented 2026-07-24 02:07:27 +00:00

Blast-radius correction — the bug predates #40 and is in the shipped v2.0.9-Eldersign tag.

While standing up a mixed-version peer for the ACP rehearsal, a v2.0.9 (2000900) binary hit the identical assertion on a fresh testnet datadir. The mis-ported ConnectTip and its status-flag guard are part of the dormant ppcoin checkpoint lineage that has been live in the init path since the bipsoft line — #40 did not introduce it; the v2.1.0 branch merely inherited it. Nobody had fresh-initialized a testnet datadir with an Eldersign-lineage binary until now (prior soaks used v2.0.8.7-era builds, which take a different path).

Corrected scope:

  • Fresh testnet datadir, v2.0.9-Eldersign or later: crash on first start.
  • Fresh mainnet datadir, any version: unaffected (hardened checkpoint not yet in index ⇒ pending path).
  • Existing datadirs, all networks: unaffected.

No re-release of v2.0.9 is warranted — testnet-only, workaround is seeding the datadir from any initialized copy — but the fix (cabb1162) ships in v2.1.0.

ACP rehearsal result (testnet, fix in place): broadcaster with the test-fixture key returned "checkpointmaster": true; a second Phase-2-aware node logged ProcessSyncCheckpoint and accepted within seconds; a v2.0.9 peer and two v2.0.8.7 peers each logged the expected CSyncCheckpoint::CheckSignature() : verify signature failed (stale lineage key), assigned banscore 0, stayed connected, and did not crash. Mixed-version rollout risk on mainnet: cosmetic log noise on not-yet-upgraded peers only.

**Blast-radius correction — the bug predates #40 and is in the shipped v2.0.9-Eldersign tag.** While standing up a mixed-version peer for the ACP rehearsal, a v2.0.9 (2000900) binary hit the identical assertion on a fresh testnet datadir. The mis-ported `ConnectTip` and its status-flag guard are part of the dormant ppcoin checkpoint lineage that has been live in the init path since the bipsoft line — #40 did not introduce it; the v2.1.0 branch merely inherited it. Nobody had fresh-initialized a testnet datadir with an Eldersign-lineage binary until now (prior soaks used v2.0.8.7-era builds, which take a different path). Corrected scope: - Fresh **testnet** datadir, v2.0.9-Eldersign or later: crash on first start. - Fresh **mainnet** datadir, any version: unaffected (hardened checkpoint not yet in index ⇒ pending path). - Existing datadirs, all networks: unaffected. No re-release of v2.0.9 is warranted — testnet-only, workaround is seeding the datadir from any initialized copy — but the fix (cabb1162) ships in v2.1.0. **ACP rehearsal result (testnet, fix in place):** broadcaster with the test-fixture key returned `"checkpointmaster": true`; a second Phase-2-aware node logged `ProcessSyncCheckpoint` and accepted within seconds; a v2.0.9 peer and two v2.0.8.7 peers each logged the expected `CSyncCheckpoint::CheckSignature() : verify signature failed` (stale lineage key), assigned **banscore 0**, stayed connected, and did not crash. Mixed-version rollout risk on mainnet: cosmetic log noise on not-yet-upgraded peers only.
dobbscoin closed this issue 2026-07-29 18:16:10 +00:00
Sign in to join this conversation.
No labels
enhancement
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
SubGeniusFinance/Offerings-to-Cthulhu#48
No description provided.