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.
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.
IsDigiDollarTxIndexRequiredtriggers 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.
# digibyte.conf
prune=2000Install 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 ✓.
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:
- Which DD coins exist → UTXO set (never pruned).
- 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. - 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). - 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).
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_heightand takefloor_blockfromActiveChain()(a guaranteed ancestor). A bareCheckBlockDataAvailability(tip, floor_block)would crash on a fresh pruned IBD (tip below the floor →ActiveChain()[floor]is null → null-deref; or an assert atblockstorage.cpp:747if a non-ancestor index at height > tip is passed). This mirrors the way the existing index guard sources its start block viaFindFork(init.cpp:2280-2288). For a genuine in-window hole (tip ≥ floor, floor an active ancestor)CheckBlockDataAvailabilityreturns cleanlyfalseand the guard raisesInitError— 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.
| # | 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).
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.
| 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.
- 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:
getrawtransactionfor pre-DD-era txs (no txindex, block deleted);getdigidollarstatshistorical 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 sameScanUTXOSetthat 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/redeemdigidollarand 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.
| 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.
- 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.
- Stats index starting at the DD floor (restores
getdigidollarstatson 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.
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.
- Everything is wired right — a pruned node produces byte-identical validation results to a full node, and non-pruned behavior is unchanged.
- Pruning is allowed — a DigiDollar-configured mainnet/testnet node starts pruned, deletes pre-DigiDollar history, and never deletes the DigiDollar era.
- Mining DigiDollar blocks works — a pruned node builds a valid DD block that a full node accepts.
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.)
For each of the 6 changes in V9.26.4_PRUNING_PLAN.md §4:
- 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.4tag). - GREEN — implement the minimal change; the test passes; un-skip; commit test+code together.
- REGRESSION — the full pre-existing suite stays green at every step.
Nothing merges without its RED→GREEN pair.
Regtest knobs (already in the tree — no framework changes):
-digidollaractivationheight=N→ DigiDollar activates ~heightNand sets the prune floor toN(retargets BIP9 min + static DD/oracle gates). PickN=1000.-prune=1→ manual prune mode →pruneblockchain(h)RPC gives deterministic, height-precise pruning (best for exercising the lock clamp).-prune=550for 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 floorN-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
getdigidollarstatsin 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 sameScanUTXOSetchange #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.
| 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 |
As shipped: this file was not created. The floor computation is covered by the
dd_prune_activation_floorfuzz target (src/test/fuzz/digidollar_prune_blockdb.cpp), which exercises the sharedDigiDollar::EarliestActivationFloorhelper directly, and the reindex-guard cases (complete window, hole, truncated block file) are covered bytest/functional/feature_digidollar_pruning.pyphases 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) |
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) |
As shipped:
feature_digidollar_pruning.pyimplements 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, andpruneblockchain(tip)is clamped belowfloor - 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"-reindexguidance;-prune+ explicit-txindex=1is 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 (thedd_floor > 0gate insrc/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=NMiB 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.
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 |
— |
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). Includesdigidollar_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.pypasses (algolock untouched).- Upstream
feature_pruning.py/feature_index_prune.pypass (generic pruning intact). - Non-DD chains (signet) prune exactly as before.
- With
-pruneabsent, 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.
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.pywith its output archived in the release notes.
| 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 |
| 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.
- "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.