diff --git a/configure.ac b/configure.ac index 58d06c9d4c..6eb607de94 100644 --- a/configure.ac +++ b/configure.ac @@ -1,7 +1,7 @@ AC_PREREQ([2.69]) define(_CLIENT_VERSION_MAJOR, 9) define(_CLIENT_VERSION_MINOR, 26) -define(_CLIENT_VERSION_BUILD, 2) +define(_CLIENT_VERSION_BUILD, 3) define(_CLIENT_VERSION_RC, 0) define(_CLIENT_VERSION_SUFFIX, []) define(_CLIENT_VERSION_IS_RELEASE, true) diff --git a/doc/HEADERS_SYNC_FIX_PLAN.md b/doc/HEADERS_SYNC_FIX_PLAN.md new file mode 100644 index 0000000000..23baeede34 --- /dev/null +++ b/doc/HEADERS_SYNC_FIX_PLAN.md @@ -0,0 +1,168 @@ +# Fresh-Node Headers Sync — Validated Root Cause & KISS Fix Plan + +*Independent audit (2026-06-30) of `FRESH_NODE_HEADERS_PRESYNC_FIX.md`, validated against +code, the live mainnet node, and the failing fresh node `192.168.1.143`.* + +**Branch note:** this was investigated on `groestl-mining-modded`, but the affected code +(`src/kernel/chainparams.cpp`) is identical on `develop`. **The fix must land on `develop` +(the v9.26.x release line), not on the Groestl mining branch.** This plan file travels with it. + +--- + +## TL;DR + +Fresh v9.26.2 nodes can't sync because the **2026-06-26 release commit `47b9ea3481` +("refresh mainnet chain metadata") set `nMinimumChainWork` from `0x00` to a value equal to +~98.68% of the *real* tip chainwork** — but DigiByte's headers **pre-sync measures +"contextless" work that only ever reaches ~28% of the real chainwork.** So pre-sync tops out +around 28% and can never cross the 98.68% gate → it resets forever (the 99.98% → 76% loop). + +**KISS fix: revert mainnet `nMinimumChainWork` to `0x00`** (one line). That is exactly how +mainnet ran for its entire history; it restores fresh sync immediately, with zero risk and no +regression. The complex "contextual presync cursor" the other team proposed is **not needed** +for this bug and is risky in a consensus-adjacent path. + +--- + +## Evidence + +### Live fresh node (192.168.1.143, v9.26.2, fresh datadir) +``` +blocks=0 headers=0 ibd=True +Pre-synchronizing blockheaders, height: 23740000 (~99.90%) +Pre-synchronizing blockheaders, height: 23760000 (~99.98%) +Pre-synchronizing blockheaders, height: 18220000 (~76.72%) <-- reset +``` +RPC shows **zero headers accepted** — presync never reaches the work threshold, so nothing is +committed to the block index. + +### The numbers (measured against the synced mainnet node) +| Quantity | Value | As % of real tip | +|---|---|---| +| Real tip chainwork | `0x…1d10a64f…` | 100% | +| `nMinimumChainWork` (set 2026-06-26) | `0x…1cae290e…` | **98.68%** | +| What contextless pre-sync actually accumulates | measured | **~28%** | + +Contextless/real work ratio measured over the last 500 / 2000 / 5000 blocks: **0.258 / 0.286 / +0.280**. Fresh sync requires `contextless% > nMinimumChainWork%` → **28% > 98.68% is +impossible.** + +### Git history (the "why now") +``` +47b9ea3481 2026-06-26 "release: refresh mainnet chain metadata" +- consensus.nMinimumChainWork = uint256S("0x00"); ++ consensus.nMinimumChainWork = uint256S("0x…1cae290ed41eb2efd4804c"); +``` +Mainnet `nMinimumChainWork` was **`0x00` for its entire prior history** (no work gate → fresh +sync always worked). v9.26.2 introduced the gate, computed from the **real** chainwork, which +the contextless pre-sync can't reach. + +--- + +## Why the contextless work is only ~28% of real (validated in code) + +Pre-sync sums work from a **dummy index** with no context +(`src/headerssync.cpp:208,244`): +```cpp +m_current_chain_work += GetBlockProof(CBlockIndex(current)); +m_redownload_chain_work += GetBlockProof(CBlockIndex(header)); +``` +`CBlockIndex(const CBlockHeader&)` leaves `nHeight = 0` and `pprev = nullptr` +(`src/chain.cpp:28`). DigiByte's `GetBlockProof()` is **context-sensitive** (`src/chain.cpp`): +- `nHeight >= workComputationChangeTarget` (real headers): geometric mean of + `GetNextWorkRequired(block.pprev, …)` across all active algos, then **`<< 7` (×128)**. +- `nHeight == 0` (the dummy): falls into the legacy branch + `GetBlockProofBase(nBits) * GetAlgoWorkFactor(0, algo)`, and + `GetAlgoWorkFactor(0, …) == 1` for every algo. So the dummy returns only the block's own + single-algo `nBits` work — **no `<<7`, no multi-algo averaging.** + +That structural difference is why the dummy sum is ~28% of the real multi-algo chainwork. This +is inherited from upstream Bitcoin, where `GetBlockProof()` depends **only** on `nBits`, so the +contextless dummy equals the real work. In DigiByte it does not. + +The pre-sync threshold for a fresh node is `nMinimumChainWork` (confirmed: +`GetAntiDoSWorkThreshold()` = `max(near_chaintip_work, MinimumChainWork())`, and for a fresh +node `near_chaintip_work = 0`, `src/net_processing.cpp:2890`). + +--- + +## Audit of `FRESH_NODE_HEADERS_PRESYNC_FIX.md` + +| Their claim | Verdict | +|---|---| +| `GetBlockProof()` is context-sensitive; presync uses a contextless dummy | ✅ **Correct** — validated in code | +| The dummy's `nHeight=0/pprev=null` yields wrong work for modern headers | ✅ **Correct** (it's the legacy branch, ~28% of real) | +| The presync abort at the tip height is the observed failure | ✅ **Correct** — reproduced on 192.168.1.143 | +| "Do **not** lower `nMinimumChainWork` as the primary fix" | ❌ **Wrong** — the over-tight `nMinimumChainWork` set on 2026-06-26 is the proximate root cause; correcting it is the right KISS fix | +| The fix must be a contextual `HeadersSyncWorkCursor` (temp CBlockIndex state) | ❌ **Over-engineered** for this bug — complex, risky, and unnecessary to restore fresh sync | + +They diagnosed the *mechanism* correctly but inverted the *fix*: they treated a latent, +long-tolerated quirk (contextless presync) as the thing to rewrite, and dismissed the actual +trigger (a one-line metadata value set 4 days ago). + +--- + +## The fix + +### Recommended (KISS, zero-risk): revert `nMinimumChainWork` to `0x00` +`src/kernel/chainparams.cpp` (mainnet, ~line 191): +```cpp +// FRESH-SYNC FIX: revert the 2026-06-26 metadata refresh. DigiByte's headers pre-sync +// measures contextless work (~28% of real chainwork), so a real-chainwork-derived +// nMinimumChainWork is unreachable and breaks fresh sync. Mainnet ran with 0x00 for its +// entire history. +consensus.nMinimumChainWork = uint256S("0x00"); +``` +- **Works:** restores the exact behavior mainnet had for years (fresh sync succeeds). +- **No regression:** it is the historical value; nothing that worked breaks. +- **Anti-DoS:** unchanged from mainnet's entire history (which never had a presync work gate). + Header pre-sync still uses commitment-based redownload + peer management. +- **Does not touch consensus, Groestl rules, the algolock, BIP9, Qt, or validation.** + +Also verify the other networks in the same file (testnet `0x…01ad46be4862`, signet, regtest) +and revert/zero any whose `nMinimumChainWork` exceeds ~25% of that network's real tip work by +the same test. + +### Optional — if a real anti-DoS floor is wanted (do NOT ship as the emergency fix) +A non-zero `nMinimumChainWork` is only safe if it is **below what contextless pre-sync can +reach** (~25% of real tip work, with margin). Two ways to get there: +1. **Pick a validated low value** (e.g. ≤15–20% of real tip work) and **prove it on a fresh + node** before release. Risk: the exact whole-chain contextless total isn't trivially + computable, so it must be tested, not guessed. +2. **Make pre-sync contextual (the other team's `HeadersSyncWorkCursor`)** so it measures real + work; then `nMinimumChainWork` can be set from real chainwork the normal way. This is the + architecturally complete fix but is **complex, touches the DoS-protection path, and carries + real regression risk** — schedule it deliberately, not as the emergency patch. + +**Recommendation:** ship the `0x00` revert now to unblock fresh nodes; treat the contextual +pre-sync as a separate, carefully-reviewed improvement only if a presync work gate is actually +desired. + +--- + +## Validation plan for the fix + +1. Build the patched binary; fresh mainnet datadir; `-debug=net`. +2. Connect to current v9.26.2 peers. +3. Confirm the log shows headers being accepted (no more + `Pre-synchronizing … (99.98%) → … (76%)` reset loop). +4. `getblockchaininfo`: `headers` climbs above 0 and tracks `blocks`; `initialblockdownload` + eventually false. +5. Confirm block download proceeds and the node reaches tip. +6. Confirm **no** `bad-algo` / `InvalidChainFound` while syncing the grandfathered Groestl + history (independent of this fix; the algolock only activates at height 23,808,000). +7. Sanity: a synced node still follows the most-work chain (the `0x00` change does not affect + chain selection). + +--- + +## Bottom line + +- **Root cause:** v9.26.2's 2026-06-26 `nMinimumChainWork` refresh (`0x00` → 98.68% of real + tip) made the header pre-sync work gate unreachable, because DigiByte pre-sync measures + contextless work (~28% of real). +- **KISS fix:** revert mainnet `nMinimumChainWork` to `0x00` (one line). Restores years-proven + behavior, zero risk, no consensus impact. +- **The other team's contextual-cursor rewrite is correct in theory but the wrong call for this + bug** — unnecessary, complex, and risky. Keep it on the shelf as an optional future + enhancement if a presync work floor is ever wanted. diff --git a/doc/release-notes/release-notes-9.26.3.md b/doc/release-notes/release-notes-9.26.3.md new file mode 100644 index 0000000000..56d423ab36 --- /dev/null +++ b/doc/release-notes/release-notes-9.26.3.md @@ -0,0 +1,60 @@ +DigiByte Core version 9.26.3 +============================ + +DigiByte Core v9.26.3 is a patch release on top of v9.26.2. It fixes a +fresh-node header synchronization regression and makes the transaction index +on by default so DigiDollar nodes start cleanly. **All v9.26.2 users — and +anyone setting up a new node — should upgrade.** It contains no consensus rule +changes; v9.26.2's Groestl algolock and the DigiDollar BIP9 deployment are +carried forward unchanged. + +How to Upgrade +============== + +Shut down DigiByte Core, replace the binaries, and restart. A reindex is not +required for this release. + +Notable changes +=============== + +Fresh-node header sync fixed (nMinimumChainWork) +------------------------------------------------ + +A fresh v9.26.2 node could pre-synchronize block headers to ~99.98% and then +reset back to a lower height, looping forever with `headers=0` and never leaving +initial block download. + +Root cause: the v9.26.2 release metadata refresh set mainnet +`consensus.nMinimumChainWork` to a value derived from the node's *real* chainwork +(~98.68% of the tip). DigiByte's header pre-synchronization, however, measures +work from a contextless `CBlockIndex` (no height or previous-block context), which +in DigiByte's multi-algorithm work model only accumulates to roughly 28% of the +real chainwork. The pre-sync work total therefore could never cross the threshold, +so the node aborted and restarted pre-sync indefinitely. + +Fix: mainnet `nMinimumChainWork` is reverted to `0x00` — the value mainnet used +for its entire history before the v9.26.2 refresh, and a value the pre-sync always +reaches. This is a chain-metadata fix only; it does not change consensus, chain +selection, or anti-DoS posture relative to mainnet's prior behavior. Testnet, +signet and regtest are unaffected (their values were already pre-sync-reachable). + +Background and a full validation plan are in `HEADERS_SYNC_FIX_PLAN.md`. + +Transaction index on by default +------------------------------- + +`-txindex` now defaults to **on** (`DEFAULT_TXINDEX = true`). DigiDollar requires a +full transaction index (mint/transfer/redeem scanning, collateral lookups, +`getrawtransaction`), and the node already refuses to start with DigiDollar active +and `-txindex` off. Defaulting it on lets DigiDollar nodes start out of the box. +Operators who do not want a transaction index can still set `-txindex=0` on +non-DigiDollar configurations. + +Note: the `-digidollar` startup flag is not required and is not enabled by +default. DigiDollar consensus, RPC, and P2P all follow the BIP9 deployment +automatically (`IsDigiDollarEnabled()`); the flag only affects regtest. + +Credits +======= + +Thanks to everyone who diagnosed the fresh-node sync issue against live nodes. diff --git a/src/index/txindex.h b/src/index/txindex.h index fdefd3f1a8..fba9f7ee30 100644 --- a/src/index/txindex.h +++ b/src/index/txindex.h @@ -6,7 +6,11 @@ #include -static constexpr bool DEFAULT_TXINDEX{false}; +// DigiByte: txindex defaults to ON. DigiDollar (mint/transfer/redeem scanning, +// collateral lookups) and the oracle/getrawtransaction RPC surface require a full +// transaction index, and IsDigiDollarTxIndexRequired() refuses to start without it. +// Defaulting on avoids a hard startup error for normal DigiDollar users. +static constexpr bool DEFAULT_TXINDEX{true}; /** * TxIndex is used to look up transactions included in the blockchain by hash. diff --git a/src/init.cpp b/src/init.cpp index 3b4df27c18..c187d5d451 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -784,6 +784,15 @@ void InitParameterInteraction(ArgsManager& args) LogPrintf("%s: parameter interaction: -whitebind set -> setting -listen=1\n", __func__); } + // DigiByte: -txindex defaults on (DigiDollar needs it), but prune is incompatible with + // txindex. If the node is pruning and the user did not explicitly choose -txindex, leave + // txindex off so the pruned node starts cleanly. An explicit "-prune=N -txindex=1" still + // errors in AppInitParameterInteraction. + if (args.GetIntArg("-prune", 0) > 0) { + if (args.SoftSetBoolArg("-txindex", false)) + LogPrintf("%s: parameter interaction: -prune set -> setting -txindex=0\n", __func__); + } + if (args.IsArgSet("-connect") || args.GetIntArg("-maxconnections", DEFAULT_MAX_PEER_CONNECTIONS) <= 0) { // when only connecting to trusted nodes, do not seed via DNS, or listen by default if (args.SoftSetBoolArg("-dnsseed", false)) diff --git a/src/kernel/chainparams.cpp b/src/kernel/chainparams.cpp index 333fb0520e..8d41851bfe 100644 --- a/src/kernel/chainparams.cpp +++ b/src/kernel/chainparams.cpp @@ -188,7 +188,13 @@ class CMainParams : public CChainParams { consensus.vDeployments[Consensus::DEPLOYMENT_ALGOLOCK].min_activation_height = 0; // may activate as soon as it locks in // The best chain should have at least this much work. - consensus.nMinimumChainWork = uint256S("0x0000000000000000000000000000000000000000001cae290ed41eb2efd4804c"); + // NOTE: must stay reachable by the headers pre-sync, which measures *contextless* + // work (~28% of real chainwork in DigiByte's multi-algo model). The 2026-06-26 + // refresh set this to a real-chainwork value (~98.68% of tip), which pre-sync can + // never reach, breaking fresh-node sync (headers reset loop). Reverted to 0x00 + // (mainnet's historical value) until pre-sync computes contextual work. See + // HEADERS_SYNC_FIX_PLAN.md. + consensus.nMinimumChainWork = uint256S("0x00"); // By default assume that the signatures in ancestors of this block are valid block 23,500,000. consensus.defaultAssumeValid = uint256S("0xade47d5ccbb92cb1d965b97a187bdbf65bf74be6a3709cb6a01339f8c2856deb"); // Block 23,500,000 diff --git a/src/qt/res/icons/digibyte_wallet.png b/src/qt/res/icons/digibyte_wallet.png index 48728a16a6..1aa6313461 100644 Binary files a/src/qt/res/icons/digibyte_wallet.png and b/src/qt/res/icons/digibyte_wallet.png differ diff --git a/test/functional/interface_rest.py b/test/functional/interface_rest.py index f9faabda26..593b45a89e 100755 --- a/test/functional/interface_rest.py +++ b/test/functional/interface_rest.py @@ -51,7 +51,9 @@ def filter_output_indices_by_value(vouts, value): class RESTTest (DigiByteTestFramework): def set_test_params(self): self.num_nodes = 2 - self.extra_args = [["-rest", "-blockfilterindex=1", "-dandelion=0"], ["-dandelion=0"]] + # DigiByte defaults -txindex on; this test asserts getindexinfo() contains only the + # block filter index, so disable txindex explicitly to match the upstream assumption. + self.extra_args = [["-rest", "-blockfilterindex=1", "-txindex=0", "-dandelion=0"], ["-txindex=0", "-dandelion=0"]] # whitelist peers to speed up tx relay / mempool sync for args in self.extra_args: args.append("-whitelist=noban@127.0.0.1") diff --git a/test/functional/rpc_misc.py b/test/functional/rpc_misc.py index a65370968c..cd8e60cfcc 100755 --- a/test/functional/rpc_misc.py +++ b/test/functional/rpc_misc.py @@ -19,6 +19,10 @@ class RpcMiscTest(DigiByteTestFramework): def set_test_params(self): self.num_nodes = 1 + # DigiByte defaults -txindex on; this test first asserts no indexes are running, + # then restarts the node with indexes. Start with -txindex=0 so the initial + # getindexinfo() is empty. + self.extra_args = [["-txindex=0"]] self.supports_cli = False def run_test(self): diff --git a/test/functional/rpc_txoutproof.py b/test/functional/rpc_txoutproof.py index 0de233c3eb..ef64793464 100755 --- a/test/functional/rpc_txoutproof.py +++ b/test/functional/rpc_txoutproof.py @@ -19,8 +19,10 @@ class MerkleBlockTest(DigiByteTestFramework): def set_test_params(self): self.num_nodes = 2 + # DigiByte defaults -txindex on; node 0 is the "no txindex" node this test uses to + # check that a proof can't be fetched without -txindex, so disable it explicitly. self.extra_args = [ - ["-dandelion=0"], + ["-txindex=0", "-dandelion=0"], ["-txindex", "-dandelion=0"], ]