Skip to content

Latest commit

 

History

History
566 lines (458 loc) · 37 KB

File metadata and controls

566 lines (458 loc) · 37 KB

v9.26.4 — Prune 12 Years of History, Keep DigiDollar Working

The one-upgrade release for mining pools · 24-hour execution plan

Mission: ship a v9.26.4 that lets pools run pruned mainnet nodes — deleting the ~23.6 million pre-DigiDollar blocks (12 years, ~32 GB) while keeping the small DigiDollar-era window — so pools upgrade once, enforce the Groestl algolock at block 23,808,000, signal and validate DigiDollar, and never need txindex. One narrowly scoped consensus change (the redeem collateral floor gate, added by the ship audit); every other new behavior is opt-in behind -prune. The whole job is plumbing: make sure a pruned node has every piece of data DigiDollar reads, and that each piece is wired to the right source so the node computes the same answers as a full node.


0. Timing — DigiDollar is NOT active yet (this makes it low-risk)

DigiDollar follows BIP9 signaling and has not activated (v9.26.3 release notes: "DigiDollar consensus, RPC, and P2P all follow the BIP9 deployment automatically"). Activation is weeks away and on a different clock from the ~6-day Groestl algolock. Why that matters:

  • Today the chain has zero DD transactions, zero DD coins, zero oracle bundles. A pruned v9.26.4 node validates plain DigiByte blocks — ordinary pruning, none of the DigiDollar plumbing is even exercised yet.
  • The only thing stopping pruning today is one over-strict check. IsDigiDollarTxIndexRequired triggers when DigiDollar is configured (HasDigiDollarDeployment, always true on mainnet) — not when it's active (IsDigiDollarEnabled). So the node demands txindex now, before any DD tx can exist. The v9.26.3 release notes describe the intent as "refuses to start with DigiDollar active" — the code is stricter than that. v9.26.4 lines the code up with the intent.
  • The DigiDollar-specific plumbing is dormant until activation and fully exercisable on regtest (where DD is always-active) before we ship. The health seed (§4 #5) reads zero today because there are zero DD coins — which is correct — and only starts doing real work at activation. So if any wire were connected wrong, it (a) can't affect mainnet for weeks, and (b) shows up immediately in the regtest tests.
  • Upgrade-once still means shipping the whole thing now — pools install one binary that must carry them through activation without a second upgrade — but doing so is low-risk precisely because the DigiDollar paths don't run on mainnet yet.
  • The flip side — and the real point: because mainnet cannot exercise DigiDollar yet, every guarantee this release makes is about the post-activation future, and the proof must come from the regtest activation rehearsal (TDD plan F0): a pruned node driven through BIP9 signaling → LOCKED_IN → ACTIVE while pruning, then mining the first DD blocks, a user minting/sending/redeeming, a deep redeem thousands of blocks after its mint with pruning in between, and a late restart that rebuilds health and volatility state from retained blocks. "Not active yet" lowers today's shipping risk; it raises the bar on proving tomorrow's behavior — F0 is that bar.

1. What pools do (the whole ask)

# digibyte.conf
prune=2000

Install v9.26.4, keep (or add) prune=N, start. That's the entire migration. Steady-state disk: ~2–3 GB today (vs ~32 GB + txindex). One upgrade, done: algolock ✓, DigiDollar signaling ✓, DigiDollar validation ✓, pruning ✓.


2. How it works — in simple terms

DigiByte's chain is 23.77M blocks, but DigiDollar can only ever reference data created after its activation floor (block 23,627,520) — a DD coin cannot exist before the deployment's minimum activation height, and even a 10-year-locked mint redeemed in 2036 was created inside the DD era. So the chain divides cleanly:

Region Blocks DigiDollar needs it? v9.26.4 pruned node
12 years of history ~23.6M (≈32 GB) Never Deleted
DigiDollar era (floor → tip) ~145K today (≈0.2–0.4 GB) Yes — forever Kept, permanently
UTXO set (chainstate) — Yes (which coins exist) Kept (pruning never touches it)

Everything DigiDollar validation ever reads lives in the kept region — the plan just makes sure each reader is wired to it:

  1. Which DD coins exist → UTXO set (never pruned).
  2. Each coin's dollar amount + lock tier → OP_RETURN of its creating tx → in a DD-era block → on disk. The lookup is "read block at coin.nHeight" — no txindex.
  3. Oracle price / volatility freeze state → coinbase bundles in recent blocks → in the kept window; the startup scan already skips pre-DD blocks before touching disk (bundle_manager.cpp:1825, 1881-1897 — confirmed).
  4. System health totals (DD supply/collateral → DCA/ERR) → rebuilt from #1 + #2 (after the health-reader is reconnected in §4).

The keep-forever rule uses the prune-lock plumbing that already exists in our Bitcoin v26 base: one named lock "digidollar" at the floor, honored by every prune path — automatic pruning and the pruneblockchain RPC both funnel through the same clamp (validation.cpp:3477-3501, confirmed: nothing bypasses it).


3. Making sure a pruned node is always complete — the reindex guard

A v9.26.4 pruned node either has EVERY DigiDollar-era block and computes exactly what a full node computes — or it refuses to start. There is no in-between.

Three pieces make that true:

Piece 1 — the prune lock (no future holes). Registered during chainstate load (CompleteChainstateInitialization, src/node/chainstate.cpp ~:148, cs_main held) — hooked in provably before the first prune event can run (init Step 10; pruning is suspended during reindex until import completes). The lock map is in-memory, so it is re-registered on every startup — same pattern the block indexes already use.

Piece 2 — the reindex guard (no existing holes). At every startup of a pruned node with DigiDollar configured:

if tip->nHeight < floor_height:              # fresh/partial sync — nothing to check yet
    skip (the prune lock keeps the window intact going forward)
else:
    floor_block = ActiveChain()[floor_height]         # guaranteed active-chain ancestor
    if !CheckBlockDataAvailability(*tip, *floor_block):   # blockstorage.cpp:755-759
        InitError("DigiDollar block window incomplete. Restart with -reindex
                   to rebuild it (the node will redownload and re-prune).")

Wiring correction (required): the guard must be conditioned on tip->nHeight >= floor_height and take floor_block from ActiveChain() (a guaranteed ancestor). A bare CheckBlockDataAvailability(tip, floor_block) would crash on a fresh pruned IBD (tip below the floor → ActiveChain()[floor] is null → null-deref; or an assert at blockstorage.cpp:747 if a non-ancestor index at height > tip is passed). This mirrors the way the existing index guard sources its start block via FindFork (init.cpp:2280-2288). For a genuine in-window hole (tip ≥ floor, floor an active ancestor) CheckBlockDataAvailability returns cleanly false and the guard raises InitError — no crash.

This is what keeps a half-hooked-up node from ever running: a datadir pruned under old software, a hand-deleted blk file, any gap — all become a refused boot with clear instructions, never a node that runs and computes different DD health/amounts than the network. It mirrors the existing NeedsRedownload check (src/node/chainstate.cpp:148-155). -reindex on a pruned datadir already does the right thing (CleanupBlockRevFiles → clean redownload, pruning as it goes, ending at the ~2–3 GB steady state, with the lock now keeping the DD era intact).

Piece 3 — a backstop check (make doubly sure). The oracle startup scan currently continues silently if a block read fails (bundle_manager.cpp:1833-1836). For a post-activation block that read can't fail under Pieces 1–2 — so make it a hard startup error. If it ever fires, the data is corrupt and the node must not run.


4. Exact change set (~200 LOC, all node-local, all behind -prune)

# File Change Why
1 src/node/chainstate.cpp Register "digidollar" prune lock, height_first = EarliestDigiDollarActivationHeight = min(nDDActivationHeight, min_activation_height) = 23,627,520 mainnet (matches consensus's own floor, digidollar/validation.cpp:70-78; PRUNE_LOCK_BUFFER=10 → effective 23,627,509). Name must NOT collide with "digidollarstatsindex" (that BaseIndex lock moves upward). Keep the DD era forever
2 src/node/chainstate.cpp Reindex guard (§3 Piece 2) No half-hooked-up nodes
3 src/init.cpp:904 IsDigiDollarTxIndexRequired → false when -prune>0 (main/testnet). The -prune -txindex=1 error stays. Pruned nodes don't need txindex
4 src/init.cpp (param interaction) When -prune>0 and -digidollarstatsindex not explicitly set → SoftSet it to 0 (same pattern as the existing prune→txindex SoftSet at :787-793). Index is default-ON (:1900) and RPC-only (never read by consensus — confirmed); without this, the generic pruned-index check (:2284-2291) would demand a reindex and break "upgrade once"
5 src/digidollar/health.cpp:404 ScanUTXOSet: pass ActiveChain()[coin.nHeight] as block_index to GetTransaction (confirmed fallback path, node/transaction.cpp:268-298) + skip coins with nHeight < floor. The one wiring gap. Today this reads only through the txindex; a pruned node (no txindex) would seed DD supply = 0 and then compute different DCA/ERR health than the rest of the network. Reconnect it to read the same transaction from the retained block — identical result on a full node, correct result on a pruned one.
6 src/oracle/bundle_manager.cpp:1833-1836 Post-activation read failure → startup error (§3 Piece 3). Turn a can't-happen state into a refused boot

Deliberately NOT in this release: an extra floor check on the redeem-collateral input (consensus-adjacent, and only reachable by a class of coins we can show doesn't exist). Instead, a one-time data check before we tag: on an archival node, scan the UTXO set for any pre-floor P2TR coin (nHeight < 23,627,520) that happens to parse as a DD vault output. Expected: 0 → the pruned read path is never exercised on such a coin and the release stays 100 % node-local. If > 0: stop and reconvene (do not ship).


5. Why a pruned node always stays in step with a full node

Every place a pruned node could have computed something differently — and how each is wired to match:

Where they could differ How it's wired to match
DD amount lookups Both read the same creating-tx bytes; pruned reads the retained block, full reads txindex/block. DD coins have nHeight ≥ floor → block always retained (Piece 1)
Startup health seed (DCA/ERR) Reconnected by #5; identical inputs on both node classes
Volatility freeze state on restart Startup scan inputs = post-activation blocks only (header-check confirmed) = fully retained; Piece 3 refuses to boot otherwise
A datadir with a missing DD block Reindex guard refuses boot (Piece 2)
pruneblockchain RPC Cannot cross the floor — it funnels through the same lock clamp (confirmed, no bypass)
Reorg needing pruned undo data rev files prune in lockstep with blk files → retained window has undo; reorgs only lower locks (the safe direction); DD window is ~145K blocks deep
Pre-floor coin that looks like a DD vault Covered by the one-time data check (§4); expected empty; must be 0 before we tag
P2P NODE_NETWORK_LIMITED — existing upstream behavior, accepted by v9.26.2/.3 and the v8 line; deep getdata correctly refused
Mining getblocktemplate/CreateNewBlock read only mempool + tip + the in-memory oracle manager (zero txindex references — confirmed); pruned pool blocks are built under identical rules
DigiDollar activation v9.26.4 signals bit 23 automatically (ComputeBlockVersion) and validates all DD rules — unlike any v8-based alternative, which would both stall activation (no signaling) and stop validating DD (hashpower not checking the rules)
Full-node users Nothing changes for them. Without -prune, v9.26.4 behaves exactly like v9.26.3 (txindex default on, same requirements); change #5 is result-identical on full nodes

Deployed-network note: the live releases are v9.26.2 and v9.26.3 (consensus-identical). "v9.26.1" exists only as pre-release tags with pre-launch chainparams — not a compatibility target.

Assumeutxo note: loadtxoutset snapshots are not applicable on mainnet — no snapshot data is configured (m_assumeutxo_data.clear(), kernel/chainparams.cpp:292) — so snapshot sync introduces no extra case for this plan.


6. Upgrade-once matrix

Coming from What the pool does Resync?
v8.26.x pruned (the common pool case) Install v9.26.4, keep prune=N Maybe none: if the node's retained window already covers the floor (likely — the DD window is only ~0.2–0.4 GB), the guard passes and it just runs. Otherwise the guard asks for one -reindex (hours; prunes as it goes; ends at ~2–3 GB). No txindex hangover (v8 default was off)
v8.26.x / v9.26.2 / v9.26.3 full Install v9.26.4, add prune=N None — node prunes down in place (all blocks present → guard passes trivially). Stale indexes/txindex dir can be deleted manually
Fresh install prune=N from first start One IBD (downloads all blocks once — bandwidth, not disk; prunes during sync)

Either way: one upgrade, and the same binary serves full nodes unchanged.


7. What a pruned pool node can / cannot do

  • Full capability: mining (getblocktemplate/submitblock), complete DD consensus validation, DD wallet ops (mintdigidollar, senddigidollar, redeemdigidollar, positions/balances/UTXO listings), oracle RPCs (all tip-bounded — confirmed), BIP9 signaling for DigiDollar.
  • Limited: getrawtransaction for pre-DD-era txs (no txindex, block deleted); getdigidollarstats historical per-height queries (those need the stats index, off by default under prune) — current stats still work: the RPC has a built-in fallback that scans the UTXO set (rpc/digidollar.cpp:678-705), and that fallback is the same ScanUTXOSet that change #5 rewires, so it reads retained blocks correctly (just slower than the index); serving deep history to peers (NODE_NETWORK_LIMITED, last ~288 blocks) — pruned pools don't seed IBD; the network's full nodes do.
  • Wallet notes (the "user in prune mode" story):
    • Any wallet created after DigiDollar activation restores and rescans fine on a pruned node — its entire relevant history is inside the retained DD-era window.
    • A wallet whose birthday is before the floor hits the standard upstream rule ("last wallet synchronisation goes beyond pruned data… -reindex", wallet.cpp:3593-3602) — restore those on a full node or accept one -reindex.
    • Simplest for pools: create the pool wallet fresh on the pruned node.
    • mintdigidollar / senddigidollar / redeemdigidollar and the Qt DD tab use wallet storage + tip-bounded oracle reads + retained-block amount lookups — all present on a pruned node.

Disk (estimates): today ~2–3 GB total; DD-era window grows ~3–5 GB/yr. prune=N (min 550) bounds pre-DD pruning aggressiveness; steady-state equals the DD window regardless of N. A startup log line states the DigiDollar floor explicitly.


8. 24-hour execution schedule

Hour Work Gate
0–6 Implement #1–#6 + unit tests (TDD; all sites pre-pinned). Tests: txindex-not-required-under-prune; lock registered at load; guard reacts correctly to a synthetic hole; health seed correct with g_txindex == nullptr; stats-index SoftSet Unit suite green
4–10 ∥ test/functional/feature_digidollar_pruning.py (regtest): pruned txindex-less node mints/transfers/redeems; restart → identical health/volatility state vs full peer; GBT with DD txs in mempool; guard → InitError; pruneblockchain cannot cross the floor Functional green
6–10 ∥ One-time data check on an archival mainnet node (pre-floor P2TR coins that parse as DD vaults) Must be 0 before tag
10–16 Full test suite; testnet pruned sync; two mainnet canaries: (a) v9.26.3-full → v9.26.4 prune=2000 in place, (b) v8.26.x-pruned datadir → v9.26.4 (exercises the guard path live) Canaries validate + mine templates
16–20 Tag v9.26.4, deterministic builds, release notes (§6 matrix + §7 table verbatim) Builds reproduce
20–24 Pool announcement + install support Pools installing

Timeline note: the algolock is ~5–6 days out. The 24-hour target is for the release; pools then have days to install one binary + one config line. Even a 24 → 48 h slip keeps the margin.

Rollback story: all new behavior sits behind -prune. Any pool (or we) can revert to full-node behavior at any time by removing prune= — a v9.26.4 node without it is functionally a v9.26.3.


9. Alternatives considered (for the record)

  • v8.26.3 (algolock-only, prunable): set aside. It doesn't signal bit 23, so pools parked on it would stall DigiDollar activation entirely; and once DigiDollar activates those nodes wouldn't validate DD rules at all, so a single invalid DD block would put their hashpower on a different chain than the exchanges and full nodes — the exact split we're trying to avoid, just moved to activation day. It also leaves a second consensus line to maintain forever.
  • blocksdir= / full-node bridge: pools declined; moot.
  • Redeem floor check in consensus now: deferred behind the one-time data check (§4) to keep this release 100 % node-local under the 24 h clock.

10. After this release (not now)

  • Stats index starting at the DD floor (restores getdigidollarstats on pruned nodes).
  • "Phase 2" DD-amount store in the UTXO database (bounds the retained window's growth).
  • Redeem-collateral floor check, added across node types together as an extra correctness check.

Part II — TDD Test Plan

Companion to V9.26.4_PRUNING_PLAN.md. This is the test blueprint that must be 100% green before tag. It follows strict TDD: every test is written RED first (fails on today's v9.26.3), then the implementation makes it GREEN, and the whole suite is the release gate. The job the tests verify is plumbing — that every reader on a pruned node is hooked to the right data and produces the same answer a full node produces.

The three things every test must ultimately prove

  1. Everything is wired right — a pruned node produces byte-identical validation results to a full node, and non-pruned behavior is unchanged.
  2. Pruning is allowed — a DigiDollar-configured mainnet/testnet node starts pruned, deletes pre-DigiDollar history, and never deletes the DigiDollar era.
  3. Mining DigiDollar blocks works — a pruned node builds a valid DD block that a full node accepts.

0. The core property: a pruned node matches a full node (two-node cross-check)

Everything reduces to one property, and we test it directly by running two nodes side by side through the same blocks:

node_A = full (txindex=1, no prune) — the reference. node_P = pruned (prune=…, no txindex, DD era retained) — the node under test.

For every block, reorg, restart, RPC, and malformed/invalid tx: node_P must agree with node_A on best-block hash, accept/reject result, and every DigiDollar quantity (supply, collateral, DCA multiplier, volatility-freeze state).

If they ever disagree, the test fails and we do not ship. This one cross-check is worth more than any number of one-sided assertions — it makes "a pruned node stays in step with the network" a machine-checked fact, not a claim. (Because DigiDollar is not active yet, on mainnet today node_A and node_P are validating plain blocks — the DigiDollar plumbing is exercised entirely on regtest, where we control activation, well before it can matter on mainnet.)


1. TDD workflow (per change)

For each of the 6 changes in V9.26.4_PRUNING_PLAN.md §4:

  1. RED — write the unit/functional test for the intended behavior; run it on the current tree; capture the specific failure (e.g. InitError "DigiDollar requires -txindex=1"). Commit the test (marked expected-fail / skipped with a # RED: v9.26.4 tag).
  2. GREEN — implement the minimal change; the test passes; un-skip; commit test+code together.
  3. REGRESSION — the full pre-existing suite stays green at every step.

Nothing merges without its RED→GREEN pair.


2. Test scaffolding to add

Regtest knobs (already in the tree — no framework changes):

  • -digidollaractivationheight=N → DigiDollar activates ~height N and sets the prune floor to N (retargets BIP9 min + static DD/oracle gates). Pick N=1000.
  • -prune=1 → manual prune mode → pruneblockchain(h) RPC gives deterministic, height-precise pruning (best for exercising the lock clamp). -prune=550 for the auto-prune path.
  • Mock oracle: enablemockoracle / setmockoracleprice → lets a node build price-dependent DD blocks in regtest.
  • Constants: MIN_BLOCKS_TO_KEEP=288, PRUNE_LOCK_BUFFER=10 (effective floor N-11).

Helper (new, test/functional/test_framework/util.py or inline): assert_nodes_agree(node_a, node_p) → asserts equal getbestblockhash, getdigidollarstats, getdcamultiplier, getprotectionstatus. Called at every milestone of the two-node tests.

Note on getdigidollarstats in the helper: node_A answers from the stats index (LookUpStats), node_P answers from the live UTXO-scan fallback (rpc/digidollar.cpp:678-705 — the same ScanUTXOSet change #5 rewires). Their agreement is intended and doubles as an index-vs-scan consistency check. Compare the consensus-derived fields (supply, collateral, health); node_A's index is synced before comparing (BlockUntilSyncedToCurrentChain — the RPC handles this) so the check doesn't race the index.

Test tip geometry: activate at N=1000, mine to tip ≈2000 so the DD era (1000..2000) is deeper than the 288-block keep-window — this forces the prune lock (not MIN_BLOCKS_TO_KEEP) to be what retains blocks 1000..1712, i.e. we actually exercise our plumbing, not upstream's.


3. Unit tests (C++ / boost, src/test/)

3a. digidollar_txindex_tests.cpp (EXTEND the existing file)

Test Asserts RED today
txindex_not_required_under_prune IsDigiDollarTxIndexRequired(Main, args{-prune=550})==false; same for TestNet Returns true today
txindex_still_required_without_prune …(Main, args{})==true (regression guard — full nodes still need it) already true — must stay true
regtest_prune_still_honors_explicit_request regtest -digidollar -prune → false (relaxed); plain regtest → false n/a

3b. digidollar_pruning_tests.cpp (NEW)

As shipped: this file was not created. The floor computation is covered by the dd_prune_activation_floor fuzz target (src/test/fuzz/digidollar_prune_blockdb.cpp), which exercises the shared DigiDollar::EarliestActivationFloor helper directly, and the reindex-guard cases (complete window, hole, truncated block file) are covered by test/functional/feature_digidollar_pruning.py phases F9/F14 rather than unit tests; the tip-below-floor no-op case has no direct unit test.

Test Asserts
earliest_dd_activation_height_matches_consensus plan floor == min(nDDActivationHeight, DEPLOYMENT_DIGIDOLLAR.min_activation_height) for Main (23,627,520) / TestNet / RegTest — pin the exact number so a chainparams edit can't silently move the floor
prune_lock_name_and_height registered lock is named "digidollar" (≠ "digidollarstatsindex") with height_first == floor
prune_lock_effective_floor after PRUNE_LOCK_BUFFER, highest prunable height == floor-11 (pins the off-by-one)
reindex_guard_tip_below_floor_is_noop guard with tip->nHeight < floor returns without throwing / dereferencing (the crash case found while checking the wiring)
reindex_guard_complete_window_ok tip≥floor, contiguous data → guard passes
reindex_guard_hole_signals_reindex tip≥floor, a DD-era block lacks BLOCK_HAVE_DATA → guard returns the "needs reindex" result (not a crash)

3c. digidollar_health_pruned_tests.cpp (NEW — the one wiring gap)

As shipped: this file was not created. ScanUTXOSet pruned-vs-full parity and the pre-floor coin skip are covered by test/functional/feature_digidollar_pruning.py (phases F6 restart parity and F10 full-node-to-pruned migration) rather than unit tests.

Build a tiny regtest chain with one DD mint, then:

Test Asserts
scanutxoset_resolves_amount_without_txindex with g_txindex==nullptr and the reconnected block_index arg, ScanUTXOSet seeds the correct totalDDSupply/totalCollateral
scanutxoset_txindex_vs_blockdb_identical run the seed both ways (txindex on, and off+block_index) → identical s_currentMetrics → proves the reconnection is result-identical on full nodes
scanutxoset_skips_prefloor_coins a coin with nHeight < floor is skipped (never read)
control_unpatched_seeds_zero with block_index==nullptr and no txindex (today's code) → seeds 0 → documents the divergence the reconnection closes (a pruned node would otherwise compute different numbers than a full node)

4. Functional tests (Python, test/functional/)

As shipped: feature_digidollar_pruning.py implements this plan as phases F1 (pruned DD node boots: no txindex, no DD stats index db), F0 (BIP9 activation crossing; the pruned node follows the full node), F4 (the pruned node mines the DD mint block; the full node accepts it), a full mint → send (self and cross-node) → redeem lifecycle on the pruned node, F2 (prune deletes pre-floor blocks; the DD-era window is retained), F7 (the "digidollar" prune lock is proven to be the BINDING constraint: the tip is pushed far enough past the floor that the generic keep-the-last-288 window no longer covers it, and pruneblockchain(tip) is clamped below floor - 10), F6 (restart parity), F8 (a third node, pruned from genesis, cold-syncs the entire DD-era chain over P2P — IBD-side DD validation with no txindex and no wallet knowledge of the transactions), and F9 (a pruned datadir MISSING DD-era blocks refuses to start with the "DigiDollar-era block data is incomplete" -reindex guidance; -prune + explicit -txindex=1 is still rejected).

Default-regtest floor note (review finding, intentional behavior): on default regtest the DigiDollar deployment is ALWAYS_ACTIVE with min_activation_height=0, so the activation floor is 0 and no prune lock or startup guard is registered (the dd_floor > 0 gate in src/node/chainstate.cpp). A pruned default-regtest node can therefore delete DD-era blocks; if it later needs one, validation fails closed (dd-input-amounts-unknown / bad-collateral-release-unknown-dd-amount) rather than accepting anything invalid. This is regtest-only (mainnet floor is 23,627,520; testnet 600). F9 deliberately exploits it to fabricate its damaged datadir, and it is why every regtest test that combines pruning with DigiDollar must pass -digidollaractivationheight=N. Registering a lock at height 0/1 instead would disable pruning entirely on regtest and break the inherited pruning suite (feature_pruning.py, wallet_pruning.py, feature_index_prune.py), so the gate is intentional.

Operator disk note (review finding, reflected in release notes): the prune lock keeps every block from ~10 below the activation floor to the tip forever, so once mainnet activates, a pruned node's retained window grows without bound (~15 s blocks) and the -prune=N MiB target will eventually be exceeded. Pruning still removes the ~12 years of pre-DigiDollar history — the release's promise — but operators should budget for the growing DD-era window.

Original plan, for reference. Two nodes: node_A full, node_P pruned (-prune=1 -digidollaractivationheight=1000), connected.

F0 — the headline: full activation lifecycle on a pruned node

DigiDollar is not active on mainnet yet — so this test rehearses the entire future, start to finish, on a pruned node. -digidollaractivationheight drives the real BIP9 state machine (DEFINED → STARTED → LOCKED_IN → ACTIVE), so the pruned node lives through activation exactly as mainnet pools will:

Phase What happens on node_P (pruned, no txindex) Must hold
1. Before activation Mine to ~700; pruneblockchain deletes early blocks; node validates plain blocks; DD RPCs report not-active Boots, prunes, agrees with node_A
2. Crossing activation Mine through BIP9 signaling → LOCKED_IN → ACTIVE at ~1000, while pruned Both nodes report ACTIVE at the same height (getdigidollardeploymentinfo parity)
3. First DigiDollar use Mock oracle price; node_P mines the first DD mint block; then a user on node_P runs mint → send → redeem node_A accepts every block; balances/positions correct
4. Long-run operation Mine 1500+ more blocks, prune again (pre-floor history gone, DD era intact); redeem a position minted back in phase 3 — the creating block is now thousands of blocks deep Deep redeem works identically on both nodes
5. Late restart Restart node_P well after activation Health / volatility / stats reconstruct from retained blocks == node_A

F0 is the release question in executable form: "once activated, can a pruned node mine and use DigiDollar?" If any phase fails, we don't ship. The subtests below then isolate each mechanism so a failure points at the exact wire:

# Subtest Proves RED today
F1 test_pruned_dd_node_boots node_P starts pruned with DD configured, no -txindex InitError DigiDollar requires -txindex=1
F2 test_prune_deletes_prefloor_keeps_dd_era pruneblockchain(2000) on node_P → returns a height < floor; getblock(500) → pruned data; getblock(1000) and getblock(1500) still succeed (the lock, not the 288-window, keeps them) can't start pruned
F3 test_pruneblockchain_cannot_cross_floor RPC cannot prune at/above floor-10; asserts the returned pruned height ≤ floor-11 —
F4 test_mine_dd_block_on_pruned_node set mock oracle price on node_P; put a DD mint + redeem in its mempool; getblocktemplate includes them; node_P mines the block; node_A accepts it and both agree on tip —
F5 test_mine_graceful_degradation_no_oracle with no valid oracle bundle, node_P's template omits price-dependent DD txs and still produces a block (no hang) — the 6b5ff516c3 path on a pruned node —
F6 test_restart_reconstruction_parity after mints/transfers/redeems, restart node_P; it reboots and getdigidollarstats/DCA/health == node_A (exercises LoadPricesFromChain + the reconnected ScanUTXOSet on pruned data) —
F7 test_volatility_freeze_reconstruction_parity drive mock price to trigger a minting-frozen-volatility condition; restart node_P; a subsequent mint is accepted/rejected identically on node_P and node_A (volatility state reconstructs the same) —
F8 test_invalid_dd_block_rejected_identically build malformed/invalid blocks (DD conservation violation, bad lock tier, and a post-activation retired-algo bad-algo case); submit to both → both reject, same reason —
F9 test_reorg_parity invalidateblock/reconsiderblock across DD txs → node_P and node_A re-converge to the same tip; DD stats match after —
F10 test_reindex_guard_on_hole stop node_P, delete a blk*.dat covering a DD-era block, restart → InitError asking for -reindex (not a crash); then -reindex → boots clean, stats == node_A —
F11 test_upgrade_from_pruned_txindexless_datadir reuse an already-pruned datadir whose window covers the floor → boots, no resync; and one whose window is incomplete → guard asks for -reindex (the v8-pruned migration case) —
F12 test_full_node_adds_prune_in_place full datadir + add -prune=550 on restart → prunes down, no resync, stats unchanged —
F13 test_statsindex_softset_off_under_prune pruned node without explicit -digidollarstatsindex → boots; getindexinfo has no DD stats index; explicit -digidollarstatsindex=1 -prune behaves like the generic pruned-index case pruned-index InitError
F14 test_pruned_node_p2p_network_limited node_P advertises NODE_NETWORK_LIMITED; deep getdata is refused (not mis-served); node_A (full) and node_P stay in sync —

5. Regression suite (existing behavior unchanged)

Non-negotiable gates, all on the v9.26.4 binary:

  • Every existing DigiDollar unit + functional test passes unchanged with no -prune (proves full-node behavior is byte-identical to v9.26.3). Includes digidollar_activation*.py, digidollar_basic.py, digidollar_collateral_spend_guards.py, digidollar_health_restart_consensus.py, digidollar_gbt_optin.py, and the ~150 DD/oracle/MuSig2 unit suites.
  • feature_digibyte_groestl_deactivation.py passes (algolock untouched).
  • Upstream feature_pruning.py / feature_index_prune.py pass (generic pruning intact).
  • Non-DD chains (signet) prune exactly as before.
  • With -prune absent, the v9.26.4 binary must be behaviorally identical to v9.26.3 — spot-checked by running the full suite with and without the v9.26.4 diff and comparing.

6. Pre-release mainnet data check (one-time, must pass before tag)

A dev-side script (archival mainnet node) iterates the pre-floor UTXO set for any P2TR coin with nHeight < 23,627,520 that parses as a DigiDollar mint output.

  • Expected: 0. → the pruned read path (§4 #5 / redeem) is never exercised on such a coin in real history → the change stays 100% node-local.
  • If > 0: STOP the release, reconvene. Do not tag. Recorded as contrib/devtools/scan_prefloor_dd_lookalikes.py with its output archived in the release notes.

7. Coverage matrix (every way a pruned node could differ has a named test)

Where a pruned node could differ Covered by
BIP9 activation crossing while pruned (the mainnet future) F0 phases 1–2
First DD blocks mined / used after activation F0 phase 3, F4
Deep redeem long after mint, with pruning in between F0 phase 4
DD amount lookup differs pruned vs full F4, F6, the two-node cross-check
Health/DCA seed differs (zero-seed) 3c (…identical, control_unpatched), F6
Volatility freeze reconstructs differently F7
A datadir with a missing DD block runs anyway 3b (…hole_signals_reindex), F10, F11
Guard crashes on fresh pruned IBD 3b (…tip_below_floor_is_noop), F1
pruneblockchain crosses the floor F2, F3
Reorg needs pruned undo data F9
Pre-floor coin that looks like a DD vault §6 data check
P2P incompatibility F14
Mining differs / can't build DD blocks F4, F5
Existing (full-node) behavior regressed §5 whole suite
DigiDollar activation affected F1–F9 run through the regtest activation boundary

8. Execution order & release gates (24-hour clock)

Gate Contents Blocks tag if red
G1 (h0–5) Unit RED→GREEN: 3a, 3b, 3c ✅
G2 (h3–11, ∥) Functional RED→GREEN: F1–F14 on regtest ✅
G3 (h6–11, ∥) §6 data check on archival mainnet ✅ (0 required)
G4 (h10–16) §5 full regression (no-prune) 100% green; testnet pruned sync; 2 mainnet canaries (full→prune-in-place; pruned-datadir→guard path) ✅
G5 (h16–20) Deterministic builds reproduce; release notes carry the F-matrix + data-check result ✅
G6 (h20–24) Pool comms + install support —

No tag until G1–G5 are all green. If any single two-node check (assert_nodes_agree) fails at any point, the release stops — that is the "1000%" bar.


9. What each goal maps to (traceability)

  • "Once activated, a pruned node can mine and use DigiDollar" → F0 (the full lifecycle rehearsal: crossing activation while pruned, first DD blocks, mint/send/ redeem, deep redeem after heavy pruning, late restart).
  • "Allows pruning" → F0 phase 1, F1, F2, F3, F12, F13 (+ 3a, 3b).
  • "Allows mining DigiDollar blocks" → F0 phase 3, F4, F5 (+ full-node acceptance in the two-node cross-check).
  • "Everything is wired right" → the core property (§0) enforced by F0 phases 4–5, F6–F11, F14, the full §5 regression, and the §6 data check — i.e. every change is proven equal to a full node or proven inert.