net/consensus: rolling checkpoints (Phase 1, self-rolling persistent) — activate at 1,055,555 #6
Labels
No labels
enhancement
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
SubGeniusFinance/Offerings-to-Cthulhu#6
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Goal
Persistent finality guard against deep reorgs from a malicious chain. Complements what's already shipped —
MAX_REORG_DEPTH=100(runtime,src/main.cpp:2498-2523) and the static h=976000 checkpoint (src/checkpoints.cpp::mapCheckpoints, commit0c751a5) — by automatically advancing a runtime checkpoint as new blocks gain confirmation depth.To be implemented after the monster begins to speak (post-fork, post-Codex-canon, post-OFFSIG-window).
Threat model addressed
NOT addressed by this issue:
MAX_REORG_DEPTH=100Constants
HARDFORK_ROLLING_CKPT_MAIN_OFFHARDFORK_ROLLING_CKPT_TESTNET_OFFROLLING_DEPTH2^10 - 1(binary mysticism), carries the 23 motif from23skidoo.info. Depth ≈ 17h at 60s — 10×MAX_REORG_DEPTH, comfortably past plausible legitimate-reorg territory.ROLLING_KEEPPhase 1 — self-rolling persistent (THIS ISSUE)
Behavior
On every accepted block where
pindex->nHeight >= HARDFORK_ROLLING_CKPT_MAIN_OFF + ROLLING_DEPTH:pindex->nHeight - ROLLING_DEPTH.(ancestor_height, ancestor_hash)into the runtimemapCheckpoints.<datadir>/rolling_checkpoints.dat.ROLLING_KEEP, drop the oldest rolling entry (static entries are never dropped).On startup:
mapCheckpointsas today.rolling_checkpoints.dat(if present) and merge entries withheight > max(static)intomapCheckpoints.Checkpoints::CheckBlockpath — no parallel code path.Files touched
src/checkpoints.{h,cpp}— makemapCheckpointsmutable at runtime; addLoadRollingCheckpoints(),WriteRollingCheckpoint(height, hash),MaybeRollForward(const CBlockIndex* pindexNew),GCRollingCheckpoints().src/main.cpp— callCheckpoints::MaybeRollForward(pindexNew)at the tail ofConnectTipafterchainActive.SetTip.src/init.cpp—Checkpoints::LoadRollingCheckpoints()at startup, guarded by-rollingcheckpointsflag (default1).src/pow.h—HARDFORK_ROLLING_CKPT_MAIN_OFF/HARDFORK_ROLLING_CKPT_TESTNET_OFFconstants.src/rpcmisc.cpp(or wherever new RPCs land in OFF's pre-0.12 tree) — new RPCs:getrollingcheckpoints— list current rolling entriesclearrollingcheckpoints <below_height>— ops recovery if a node ever locks bad statesetrollingcheckpointsenabled <true|false>— runtime toggle without restartOn-disk format (
rolling_checkpoints.dat)Append-only, fixed-width records:
Loader is tolerant of partial-write tail (truncate the final partial record on load). No header, no version byte — if we ever need a v2 format, we just write to a new filename.
Defaults / escape hatches
-rollingcheckpoints=1).-rollingcheckpoints=0startup flag fully disables the feature (skip load, skip write, skip MaybeRollForward).clearrollingcheckpoints <below_height>RPC for ops recovery if a node locks bad state somehow.mapCheckpointsentries are never overwritten or dropped by rolling code.Phase 2 — Conclave-signed override (DEFERRED)
Out of scope for this issue. Tracked separately. Outline for reference:
signedcheckpointcarrying(height, hash, key_index, ECDSA_sig), signed by any 1 of the 7 Conclave keys.signed_checkpoints.dat.checkpointP2P message (removed upstream by Wuille for centralization reasons — but we already trust the Conclave for OFFSIG + Treasury, so the trust model is internally consistent).Open a follow-up issue when Phase 1 has settled in.
Risks / mitigations
-rollingcheckpoints=0+clearrollingcheckpointsRPC as escape hatches.height > max(static).Activation milestone (to add to WHERE_WE_LEFT_OFF.md)
rolling checkpoints: 1,055,555References
src/main.cpp:2498-2523— existingMAX_REORG_DEPTH=100runtime guardsrc/checkpoints.cpp::mapCheckpoints— static checkpoint at h=976000 (commit0c751a5)src/pow.h—HARDFORK_LWMA3_MAIN_OFF=980000,nRestorationForkHeight=1000000src/chainparams.cpp:167-168— OFFSIG window 999,991-1,050,666Survey closed — merge-mining is not a viable alternative for the same threat surface
For the record before Phase 1 lands: the "could we just AuxPoW against Quarkcoin (QRK) instead of building rolling checkpoints?" question got a look. The conclusion is no, and the reason is worth keeping on file so the next contributor who asks finds it instead of redoing the survey.
Visible Quark auto-switch market
minerstat— aggregate across the 4 multipools they track (Mining Dutch, NiceHash, Zergpool, zpool)zpool.ca/api/status—quarkalgo entry/qrkAggregated visible Quark hashrate is roughly one decent GPU. There is no auto-switch ecosystem to recruit, and there is no organized QRK community to coordinate with — every social-channel search yields QuarkChain (QKC, Ethash) noise instead of QRK signal.
The asymmetry
QRK's chain itself is alive and ticking —
chainz.cryptoid.info/qrk/showed 41 new blocks in roughly 5 minutes of polling, andgetdifficultyreturned7995.84. Through the sameH = D × 2³² / Tcalibration the project already uses (validated against OFF currentdiff=13.02 / T=60s ⇒ ~932 MH/s, and against (BOB)diff=3.52 / T=120s ⇒ ~126 MH/smatching their published ~135 MH/s):That is ~50,000× the visible multipool number. The hashrate exists; it is not flowing through any open pool. The most plausible explanation is one or two legacy Quark/X11 ASICs (Baikal Quad-Mini class, ~7 TH/s/unit) pointed at QRK by a single unidentified operator.
Why this makes merge-mining worse than no parent
AuxPoW inherits the parent's security model. Merge-mining a parent whose hashrate is ~99.99% concentrated in one anonymous operator doesn't import security — it imports a single point of failure wearing a parent's costume. That operator can lose interest, have hardware die, or be coerced, and OFF would discover the dependency the moment they did. Compared to OFF's current standalone Quark posture, that is strictly worse, not better.
Implication for this issue
The threat surface AuxPoW theoretically addresses (51% attacks via rented or borrowed hashrate against a low-hashrate child chain) is already covered by OFFSIG during the signed-mining window upstream of activation, and by Phase 1 from
h=1,055,555onward. Rolling checkpoints don't require finding an honest external hashrate donor — because at Quark scale in 2026, that donor does not exist. The Quark-algorithm graveyard is not a security partner.Closing the merge-mining branch. Phase 1 stands as the right path.
Sources (for future re-verification)
quarkentry)getdifficultyAPIIssue #6 comment draft — post-canon permanent-security decision
Raising the post-canon permanent-security decision before the impl runway opens (~h1,030,000)
Cross-referencing this against the OFFSIG window and what's already in the tree, three things worth settling before code starts:
1. Phase 1 alone is not a 51% defense — promote Phase 2 to the committed answer.
Phase 1 self-rolling checkpoints are per-node: each node checkpoints its own accepted chain at
ROLLING_DEPTH. That's exactly right for the stated threat model (fresh-sync / offline-rejoin fed a poisoned deep history). But it does not stop a real-time majority attacker — a node that syncs the attacker's longer chain will happily self-checkpoint that chain. On a ~16 MH/s Quark chain with a named repeat attacker (#699), a rented-hash deep reorg in the100 < d < sync-depthband is the actual post-window risk, and only network-agreed (broadcast) checkpoints close it.The good news: the
CSyncCheckpoint/ValidateSyncCheckpointmachinery (ppcoin/ACP-style, centrally broadcast) is already vendored incheckpoints.cpp— it just needs wiring to a Conclave checkpoint key (same custody model as OFFSIG) and a broadcast cadence. Proposal: move "Phase 2 (centralized override / emergency consensus)" out of deferred and make it the permanent post-canon scheme. Phase 1 ships as-specced; Phase 2 is the 51% answer. This is the Feathercoin-ACP precedent — small chain, post-51%-attack, keeps mining permissionless while bounding reorgs.2. There's a ~3.4-day unguarded gap between the window closing and rolling activation.
OFFSIG closes at 1,050,666;
HARDFORK_ROLLING_CKPT_MAIN_OFF = 1,055,555. That's 4,889 blocks (~3.4 days) of fully-open mining with no rolling guard — onlyMAX_REORG_DEPTH=100near-tip. Two ways to close it:Recommend (b): ACP broadcast is an overlay, not a hardfork, so it can be live early and cover the gap without touching the 1,055,555 consensus constant. (a) is the cheap fallback if Phase 2 slips.
3. AuxPoW is a long-term complement, not a deadline item.
Merge-mining is the real hashrate-borrowing answer eventually (cf. (BOB)'s scrypt AuxPoW), but Quark's merge-mining pool is thin, it needs miner/pool adoption, and it provides no finality on its own — not something to stand up reliably inside this runway. Track it separately; it composes with checkpoints later.
Not recommended: permanently extending OFFSIG (centralization regression — defeats the permissionless-network goal), or accept-it (untenable with a live reclamation/bridge economy and a named attacker).
Suggested sequence: ratify now → stand up the Phase-2 checkpoint broadcaster (dark) before h1,050,666 → implement/test Phase 1 in the 1,030,000→1,055,555 runway (testnet first, LWMA-3 process) → 1,055,555 rolling activates with ACP already carrying finality.
ok phase 1 is in. six commits on
feat/v2.0.x-rc-bipsoft, tipc2a67a64. joins #32 / #33 / #34 / #39 in the same soak, all activating at mainneth=1,055,555.couple of things ended up different from the spec.
went with a two-map structure instead of mutating
mapCheckpointsin place. same external behavior —CheckBlock/GetTotalBlocksEstimate/GetLastCheckpointall consult both layers — but the static map stays immutable and the runtime rolling map is its own thing. static entries can't get accidentally dropped by GC,clearrollingcheckpointsonly touches the rolling layer, on-disk format is unambiguous. felt cleaner than the const_cast gymnastics the literal spec would have needed.-checkpointsand-rollingcheckpointsare independent flags. testnet caught this one on first deploy — we run testnet withcheckpoints=0as a workaround for a stalemapCheckpointsTestnetentry, and that was silently disabling the rolling layer too. fix:-checkpoints=0skips dev-baked static checks,-rollingcheckpoints=0skips the daemon's auto-rollforward, you can pick either independently.regtest covers pre-activation void, activation boundary, continued rolling, restart persistence, runtime toggle, and clear-below-height with disk rewrite. all PASS at
qa/rpc-tests/rolling_checkpoints.py.live on the testnet soak now. restarted at h=2142, rolling map locked h=1119 immediately (= 2142 − 1023), mining advanced to h=2145 / locked h=1122. activation heights are 1,055,555 mainnet, 100 testnet, 110 regtest.
phase 2 (Conclave-signed override) still deferred per the issue body. opens as separate work once phase 1 settles.
merges to main with the rest of the bipsoft bundle at freeze-end ~2026-07-19.
Phase 1 implemented, regtest passed, in soak.
The six commits landing in
c2a67a64carry out the design in the issue body:fd68873d— activation constants insrc/pow.h5b87f88d— scaffolding insrc/checkpoints.{h,cpp}66591fe7—MaybeRollForwardwired intoConnectTip+ startup load ininit.cpp78bbff90— RPCsgetrollingcheckpoints,clearrollingcheckpoints,setrollingcheckpointsenabled2fafcc1e—qa/rpc-tests/rolling_checkpoints.py(278 LOC, regtest harness)c2a67a64— decouple-rollingcheckpointsfrom the-checkpointsmaster flagAll six phases of the regtest pass against a fresh build from branch tip:
setrollingcheckpointsenabled false/true)clearrollingcheckpoints below_heightpurges memory + disk, survives restartFolded into
v2.0.x-rc-bipsoft-rc2(tagged fromc2a67a64) for the next testnet soak window alongside #32/#33/#34. Mainnet activation height stays at 1,055,555; rolling depth 1023 means the first lock fires at h=1,056,578. The Lord begins to remember His own history.Live on mainnet. Activated on schedule at h=1,055,555 (2026-07-23), block
00000000438cf73291060c7c2caeadd568b7e432c192f4f44b3d84eafb157c7c.The first rolling locks were verified byte-identical across all four production fleet nodes, each persisting its own
rolling_checkpoints.dat. The map has advanced per-block ever since —getrollingcheckpointson a production node today reportsenabled: true,activation_height: 1055555,depth: 1023, with the window trailing the tip exactly as specified. Combined withMAX_REORG_DEPTH=100, buried history is now pinned per-node: a deep secret-chain rewrite of the 2018 shape is consensus-impossible.Implementation notes vs. the original spec, for the record: shipped as a two-map design (static
mapCheckpoints+ runtimemapCheckpointsRolling) rather than a single mutable map, and-checkpoints/-rollingcheckpointsare semantically orthogonal flags — a coupling bug the testnet caught on first deploy. Three operator RPCs (getrollingcheckpoints,clearrollingcheckpoints,setrollingcheckpointsenabled) ship alongside.Phase 1 complete and shipped in v2.0.9-Eldersign; the Phase-2 broadcast layer (#40, merged) awaits first-light with v2.1.0. That is not dead which can eternal lie — but from this height forward, neither can it be rewritten. Closing.