net: Phase-2 ACP broadcast checkpoints — Conclave-signed finality overlay #40
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#40
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
Stand up the Phase-2 broadcast-checkpoint overlay that gives OFF a real-time 51%-attack defense, paired with #6 Phase 1 (per-node rolling checkpoints). Conclave-signed
CSyncCheckpointmessages gossiped to peers; recipients reject reorgs past the embedded height.The architectural argument and the recommendation that promoted Phase 2 from deferred to a committed deliverable are in https://github.com/SubGeniusFinance/Offerings-to-Cthulhu/issues/6#issuecomment-4725038454.
Why this is separate from #6
Decoupling lets Phase 2 ship and start carrying finality before h=1,050,666, closing the 4,889-block unguarded gap between OFFSIG-window close and #6 activation — without touching the 1,055,555 consensus constant or being gated on the #6 hardfork timeline.
Plumbing — what already exists
The
CSyncCheckpoint/ValidateSyncCheckpointmachinery is already vendored insrc/checkpoints.cpp(ppcoin/Peercoin lineage). An audit of exactly what's wired vs. dormant is the first sub-task.Work remaining
CSyncCheckpointagainst the current chain tip at depth N and gossips it. Runs on the host that holds Conclave Key #1's privkey.CSyncCheckpointmessages.CSyncCheckpointreceipt, validate signature againstvConclaveKeys[0](already insrc/chainparams.cpp:188), accept if newer-than-stored, reject reorgs past the embedded height.getsynccheckpoint,sendcheckpoint, etc. — peer with the existing checkpoint RPCs.MAX_REORG_DEPTH(= 100) initially, broadcast on every Nth block.Custody — already decided
Reuse Conclave Key #1 (
vConclaveKeys[0], pubkey0238efde05d567979485df6cd6dcf3af2606348a1e260eedf9a6464df57f46b111, P2PKHQcad56y76jCGgjkhGHPyCMY89uC9j9CBXC). Pubkey is already insrc/chainparams.cpp:188— no chainparams change required when the broadcaster ships. No new key generation.Ratified by Conclave 2026-06-17. Tradeoff: faster path, at the cost of conflating canon-mining and ACP-signing duties on the same key. A future revision can separate them if the tradeoff stops feeling acceptable.
Activation goal
Dark-live before h=1,050,666 — broadcaster signing checkpoints, ACP-aware peers honoring them, carrying finality through the OFFSIG → #6 gap. Builds can start immediately; no dependency on #6, on the v2.0.x-rc-bipsoft soak, or on chain height.
Out of scope
Open subquestions
MAX_REORG_DEPTH = 100and tune?Audit phase done — branch
feat/issue-40-phase2-acp(36f319f), full writeup atcontrib/phase2-acp/AUDIT.md.Headline finding
The entire ACP stack is already implemented end-to-end in
src/checkpoints.{cpp,h}— dormant, but architecturally complete and wired:main.cpp:4492(strCommand == "checkpoint"→ProcessSyncCheckpoint)main.cpp:2825-2833(AcceptBlockrejects onCheckSyncCheckpointfail)init.cpp:574-577(-checkpointkey=<WIF>→SetCheckpointPrivKey)sendcheckpoint,enforcecheckpointregistered atrpcserver.cpp:351-352main.cpp:3033The whole machinery has been sitting in the tree since the ppcoin/Peercoin fork era; nobody pointed it at our keys.
The single load-bearing change
src/checkpoints.cpp:525—CSyncCheckpoint::strMainPubKeyis still the stale ppcoin key. Replace with Conclave Key #1's pubkey (0238efde…46b111, already present atchainparams.cpp:188asvConclaveKeys[0]). The corresponding privkey lives where the active canon miner runs.Revised scope vs. the original #40 work list
SendSyncCheckpoint) + auto-fires at tip advanceRelayTo+ProcessMessagehandler)ValidateSyncCheckpoint+AcceptBlockhook)sendcheckpoint,enforcecheckpoint)-checkpointdeptharg exists)Open subquestions (still real, just smaller)
MAX_REORG_DEPTH = 100a sensible starting value for-checkpointdepth?strMainPubKeyin favor of readingParams().ConclaveKeys()[0]directly?Sub-task list for the rest of #40:
-checkpointkey=<WIF>on Conclave Key #1 hostqa/rpc-tests/phase2_acp.py)Params().ConclaveKeys()[0]substitutionFirst-light done. Branch
feat/issue-40-phase2-acpatc4338824.The dormant ACP stack is now wired to the network's actual keys. End-to-end working on regtest: broadcaster signs → peer validates → both nodes accept the checkpoint.
What landed (5 files, +293/-10)
src/checkpoints.cppGetCheckpointMasterPubKeyHex()helper readingParams().ConclaveKeys()[0]; deleted stale ppcoinstrMainPubKey/strTestPubKeyconstantssrc/checkpoints.hsrc/chainparams.cppvConclaveKeys.clear()first —CTestNetParamsinherits fromCMainParamsso the real mainnet keys are already loaded at that point; without the clear,ConclaveKeys()[0]resolves to Slot #1 on every network, including regtest)qa/rpc-tests/phase2_acp.pyProcessSyncCheckpointlog line.gitignoreTest results
test_Offerings: 113 cases / 32 suites — all PASSqa/rpc-tests/phase2_acp.py:sendcheckpoint): PASS — daemon returnscheckpointmaster: trueand broadcastsMainnet operator activation (no further consensus change required)
-checkpointkey=<WIF for Conclave Key #1>.sendcheckpoint <currenttip>for first-light.main.cpp:3033) takes over after.Mainnet pubkey is already in
chainparams.cppatvConclaveKeys[0]— same key the OFFSIG signed-mining gate uses.Subquestions still real
main.cpp:3033)cc1a012testnet genesis update)Sub-task tally:
CheckSignatureto read fromParams().ConclaveKeys()[0]vConclaveKeyswith a deterministic test fixtureNegative path landed. Commit
f44b2369.Added PHASE 4 to
qa/rpc-tests/phase2_acp.py: two fresh unconnected daemons mine independently. Node 2 (broadcaster) mines chain A to h=10, signs a checkpoint at A5. Node 3 mines a longer competing chain B to h=20 with no checkpoint. They peer.When chain B's blocks arrive at node 2 via P2P,
AcceptBlockrunsCheckSyncCheckpointon each. Descendants of B5 don't trace back to checkpoint A5, so they're rejected at validation. Verified by counting the canonical log line:Test asserts:
Both pass. The defense fires exactly as designed.
Updated sub-task status
CheckSignatureto read fromParams().ConclaveKeys()[0]vConclaveKeyswith a deterministic test fixtureOperator runbook drafted. Commit
8485743e,contrib/phase2-acp/RUNBOOK.md.Covers the full mainnet first-light procedure for the operator holding Conclave Key
#1:#1, daemon synced, fresh backup)-checkpointkey(CLI arg only, never config file)sendcheckpointat tip-100 — pastMAX_REORG_DEPTH=100near-tip contention, recent enough to bind somethingProcessSyncCheckpoint)main.cpp:3033) handles re-issue, operator just keeps daemon upenforcecheckpoint falseNo internal hostnames or paths leaked: role labels only, per the project convention.
Updated sub-task status
CheckSignatureto read fromParams().ConclaveKeys()[0]vConclaveKeyswith a deterministic test fixturecc1a012testnet genesis fix)PR open: #41 —
feat/issue-40-phase2-acp→main.CI workflows running on the PR:
linux-build-depends,linux-qt5-build-depends,windows-build-depends. Merge ordering note vsfeat/v2.0.x-rc-bipsoftis called out in the PR body;src/chainparams.cpptestnet block is the only conflict point, small rebase when whichever branch lands second.Merged to main at
da1368d4(PR #41). All three CI workflows green. Branchfeat/issue-40-phase2-acpdeleted. Next stops: testnet rehearsal oncefeat/v2.0.x-rc-bipsoftlands its testnet genesis update on main, then mainnet operator first-light via the runbook.Mainnet first-light: 2026-07-29 18:29 UTC. The ward is lit.
With v2.1.0-Nodens deployed across the fleet, the broadcaster daemon signed its first mainnet sync checkpoint at h=1,064,000 — accepted by every recipient node within a second, enforce mode, banscore clean. Steady-state re-broadcast is verified advancing at tip−100 (first automatic advance landed at h=1,064,019).
Eight years after the counterfeiter's chains were struck at the Restoration, the Conclave's seal now walks the network in real time. A conflicting chain deeper than the seal shall not pass. Iä! The watch is kept.
One operational finding during first-light is tracked separately in #50 (tip-advance auto-broadcast is gated on peer-received blocks; a cadence heartbeat covers it in operation, in-daemon fix targeted v2.1.1).