diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml index 915da2bd129..eaba6143d06 100644 --- a/.github/codeql/codeql-config.yml +++ b/.github/codeql/codeql-config.yml @@ -6,3 +6,6 @@ paths-ignore: # Archived merge/debug snapshot retained for reference; do not analyze as # live release code or executable tests. - 'test/test_old_v26.2_mergev1/**' + # This retired framework has the same Python package name as the current + # test/functional framework. Scanning both resolves calls to the wrong class. + - 'qa/rpc-tests/**' diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 86d536d457a..626d4794c9e 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -361,16 +361,22 @@ The cipher uses: - **P-boxes**: 2 permutation boxes with 6 subrounds - **Keccak-p[800]** finalization -### 4.5 Algorithm Lookup Cache +### 4.5 Finding the last block mined with a given algorithm -**File:** `src/chain.h` (line 212) +**File:** `src/pow.cpp`, `GetLastBlockIndexForAlgo()` -```cpp -// Fast per-algorithm block tracking -CBlockIndex *lastAlgoBlocks[NUM_ALGOS_IMPL]; -``` +The difficulty rules need the previous block that used the same mining algorithm. There is one way to find it: +walk back through the chain from the current block until a block with that algorithm turns up. + +Every block used to carry an array of pointers, one per algorithm, so the answer could be read without walking. +That array was removed. It cost 64 bytes for every block header the node holds in memory, which is about 1.5 GB +on a fully synced mainnet node, and the walk it saved is short. The difficulty rule that uses this already steps +back fifty blocks on every call before it does anything else, so the walk adds roughly a tenth more work to a +path that was already doing a lot of it. -This cache enables O(1) lookup of the last block for each algorithm, critical for difficulty adjustment. +Removing it changed no difficulty result. The two lookups were run side by side over every algorithm, every +difficulty rule and the special case for minimum-difficulty blocks, and their answers were recorded as plain +numbers that the tests still check. --- @@ -464,7 +470,7 @@ class CBlock : public CBlockHeader { ### 6.2 Block Index Structure -**File:** `src/chain.h` (lines 146-384) +**File:** `src/chain.h` (lines 146-380) ```cpp class CBlockIndex { @@ -487,14 +493,12 @@ class CBlockIndex { unsigned int nTx; // Transactions in block unsigned int nChainTx; // Cumulative transactions - // DigiByte multi-algo - CBlockIndex *lastAlgoBlocks[NUM_ALGOS_IMPL]; // Per-algo tracking }; ``` ### 6.3 Chain State Management -**File:** `src/validation.h` (lines 491-815) +**File:** `src/validation.h` (`Chainstate` at 497, `ChainstateManager` at 884-1308) ``` ChainstateManager @@ -686,7 +690,7 @@ enum class SigVersion { | `SCRIPT_VERIFY_CHECKSEQUENCEVERIFY` | BIP112 CSV | | `SCRIPT_VERIFY_WITNESS` | BIP141 SegWit | | `SCRIPT_VERIFY_TAPROOT` | BIP341/342 Taproot | -| `SCRIPT_VERIFY_DIGIDOLLAR` | DigiDollar opcodes (`OP_DIGIDOLLAR`/`OP_DDVERIFY`/`OP_CHECKPRICE`/`OP_CHECKCOLLATERAL`/`OP_ORACLE`); set in `GetBlockScriptFlags()` (`validation.cpp:2755, 2796-2797`) only when the buried `DEPLOYMENT_DIGIDOLLAR` deployment is active (BIP90 since v9.26.5). | +| `SCRIPT_VERIFY_DIGIDOLLAR` | DigiDollar opcodes (`OP_DIGIDOLLAR`/`OP_DDVERIFY`/`OP_CHECKPRICE`/`OP_CHECKCOLLATERAL`/`OP_ORACLE`); set in `GetBlockScriptFlags()` (`src/validation.cpp:3030, 3071-3072`) only when the buried `DEPLOYMENT_DIGIDOLLAR` deployment is active (BIP90 since v9.26.5). | ### 8.4 DigiDollar Opcodes @@ -704,7 +708,7 @@ OP_ORACLE = 0xbf // Coinbase oracle bundle marker (Tapscript OP_SUCCE ### 8.5 Taproot Support -**Files:** `src/script/interpreter.cpp` (lines 1980-2106) +**Files:** `src/script/interpreter.cpp` (`ExecuteWitnessScript` at 1963, `VerifyWitnessProgram` at 2048-2129) - **Key path spending**: Single Schnorr signature (64 bytes) - **Script path spending**: Control block + script + witness @@ -1206,10 +1210,10 @@ class COraclePriceMessage { ### 13.3 Exchange Integration -**Files:** `src/oracle/exchange.h`, `src/oracle/exchange.cpp` (~1230 lines) +**Files:** `src/oracle/exchange.h` (281 lines), `src/oracle/exchange.cpp` (~1,333 lines) -12 fetcher classes derive from `BaseExchangeFetcher` (`exchange.h:23+`): -Binance, Coinbase, Kraken, CoinGecko, Bittrex, Poloniex, Messari, KuCoin, Crypto.com, Gate.io, HTX, plus the base. Active exchange selection is per oracle operator (see `DIGIDOLLAR_ORACLE_ARCHITECTURE.md` for the configurable subset). +Eleven fetcher classes derive from `BaseExchangeFetcher` (`src/oracle/exchange.h:25-215`): +Binance, Coinbase, Kraken, CoinGecko, Bittrex, Poloniex, Messari, KuCoin, Crypto.com, Gate.io and HTX. Which of them run is not a setting: `MultiExchangeAggregator::InitializeFetchers` creates exactly six of them in the source (`src/oracle/exchange.cpp:1092-1097`) — Binance, CoinGecko, KuCoin, Gate.io, HTX and Crypto.com. The other five compile but are never created. See `DIGIDOLLAR_ORACLE_ARCHITECTURE.md`. ```cpp MultiExchangeAggregator: diff --git a/CLAUDE.md b/CLAUDE.md index 0defb856add..7233d32a886 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,9 +2,9 @@ ## What this repo is -DigiByte Core v9.26 (Bitcoin Core v26.2 lineage) on the `feature/digidollar-v1` branch, augmented with the **DigiDollar** native USD-pegged stablecoin and a multi-oracle DGB/USD price feed network with MuSig2 Schnorr threshold consensus. +DigiByte Core v9.26 (Bitcoin Core v26.2 lineage), augmented with the **DigiDollar** native USD-denominated token and a multi-oracle DGB/USD price feed using MuSig2 Schnorr signatures. -Working directory: `/home/jared/Code/digibyte`. Current DigiDollar v1 campaign branch: `feature/digidollar-v1`. Upstream main branch: `develop`. +The v9.26.6 development line is `release/v9.26.6`. Work may use a separate branch and worktree; verify the actual checkout before editing. Upstream main branch: `develop`. ## Required reading order before any DigiDollar / oracle work @@ -23,18 +23,20 @@ Working directory: `/home/jared/Code/digibyte`. Current DigiDollar v1 campaign b 13. `ORACLE_DISCOVERY_ARCHITECTURE.md` — Oracle endpoint discovery design 14. `DIGIDOLLAR_ORACLE_SETUP.md` — Oracle setup and migration runbook -Also check `docs/ORACLE_OPERATOR_GUIDE.md` for operator-facing steps and the newest relevant `RELEASE_v9.26.0-rc*.md` before public instructions or release claims. +Also read `docs/ORACLE_OPERATOR_GUIDE.md`, `doc/digidollar-operations.md` and `doc/release-notes/release-notes-9.26.6.md` before changing operator instructions or release claims. Historical RC notes do not establish current readiness. ## Repo layout (DigiDollar / oracle surface) ``` -src/digidollar/ # 5 modules: digidollar, health, scripts, txbuilder, validation +src/digidollar/ # 6 modules: amount, digidollar, health, scripts, txbuilder, + # validation src/oracle/ # Oracle daemon + MuSig2: bundle_manager, exchange, mock_oracle, # node, signing_orchestrator, musig2_{aggregator,session, # session_manager,orchestrator,oracle_participation,messages, # session_mining} -src/consensus/ # dca, err, digidollar, digidollar_transaction_validation, - # digidollar_tx, volatility (DigiDollar/oracle consensus rules) +src/consensus/ # dca, err, digidollar, digidollar_state.h, + # digidollar_transaction_validation, digidollar_tx, volatility + # (DigiDollar/oracle consensus rules) src/index/ # digidollarstatsindex (DD supply/health index) src/primitives/ # oracle.h (price message + bundle types) src/rpc/ # digidollar.cpp (18 base RPCs); digidollar_transactions.cpp is @@ -45,12 +47,14 @@ src/qt/ # DD tab + widgets/dialogs: digidollar{tab,mintwidget,sendw # receivewidget,redeemwidget,overviewwidget,positionswidget, # transactionswidget,coincontroldialog,receiverequest}, # ddaddressbookpage, digidollar_qt_translate -src/test/ # ~150 DigiDollar/oracle/MuSig2/Red-Hornet unit tests + fuzz/ -src/wallet/test/ # 6 DD wallet test sources (persistence, security, Wave 16/17, - # rh59 lock-bypass) +src/test/ # ~160 DigiDollar/oracle/MuSig2/Red-Hornet unit tests + fuzz/ +src/wallet/test/ # 7 DD wallet test sources (persistence, security, lock safety, + # Wave 16/17, rh59 lock-bypass) # — linked into src/test/test_digibyte, not a separate binary -src/qt/test/ # digidollarwidgettests, digidollarwave19widgettests -test/functional/ # ~80 DD/oracle Python tests registered in test_runner.py +src/qt/test/ # digidollarwidgettests, digidollarwave19widgettests, + # digidollarmintrecordtests +test/functional/ # 104 DD/oracle Python test entries registered in + # test_runner.py ``` ## DigiDollar opcode soft-fork additions @@ -72,13 +76,55 @@ As of v9.26.5 the Taproot, DigiDollar, and AlgoLock deployments are **buried** ( |---------|-----------------|--------------------|------------------| | Mainnet | 21,168,000 | 23,869,440 | 23,869,440 | | Testnet (testnet26) | 0 | 600 | 0 | -| Signet / Regtest | 0 | 0 | 0 | +| Signet (unsupported) / Regtest | 0 | 0 | 0 | Static gates are unchanged: mainnet `nDDActivationHeight = nOracleActivationHeight = nDigiDollarMuSig2Height = 23,627,520` (the historical BIP9 floor, deliberately below the 23,869,440 burial height); testnet 600 / 600 / 600. Default regtest keeps the DD/oracle height gates at 650 / 650 while `nDigiDollarMuSig2Height = min(650, DigiDollarHeight) = 0`, so v0x03 quotes are valid whenever DigiDollar is active. `EarliestActivationFloor(params) = min(nDDActivationHeight, DigiDollarHeight)` (0 if the deployment is disabled) — value-preserving vs the pre-burial formula on every network. `IsDigiDollarEnabled` is a pure height compare against `DigiDollarHeight` (no `VersionBitsCache` anywhere in the DD path), and startup oracle-price reconstruction uses the same buried predicate as block connection, so DD-active blocks below the regtest 650 gate are still not dropped during restart/reindex cache rebuilds. Regtest knobs: `-digidollaractivationheight=N` sets `DigiDollarHeight` AND `nDDActivationHeight`/`nOracleActivationHeight`/`nDigiDollarMuSig2Height` to N, so DigiDollar activates at exactly height N (pre-burial the knob ran real BIP9 signaling and activated at the first 144-block window boundary >= max(432, N)). `-testactivationheight=taproot@H` / `digidollar@H` / `algolock@H` moves only the buried deployment height — the static DD/oracle gates keep their defaults, but `nDigiDollarMuSig2Height` is derived as `min(nDDActivationHeight, DigiDollarHeight)` and so follows `digidollar@H` below 650; `-digidollaractivationheight` takes precedence for DigiDollar. `-vbparams=digidollar/taproot/algolock` is now a startup error ("Invalid deployment") — only `testdummy` remains. -RPC/GBT surface: `getdeploymentinfo`/`getblockchaininfo` render the three deployments as `{"type":"buried","active":…,"height":…}` with no `bip9` sub-object. `getdigidollardeploymentinfo` now returns `{enabled, type:"buried", status:"active"|"defined", activation_height (omitted if the deployment is disabled), oracle_activation_height, musig2_format_activation_height, oracle_pubkey_count, oracle_consensus_required, oracle_total_slots, oracle_seed_peers, musig2_session{...}}`; the BIP9 fields (`bit`, `start_time`, `timeout`, `min_activation_height`, `blocks_until_timeout`, `signaling_blocks`, `threshold`, `period_blocks`, `progress_percent`) were removed, and `activation_height` is now always the burial height (the old back-scan reported the first-active/last-LOCKED_IN block instead). `getblocktemplate` always lists `taproot`/`digidollar`/`algolock` in `rules` once active (hardcoded like `csv`); `vbavailable` no longer mentions them and the template `version` never sets bits 2/23/0. `MinBIP9WarningHeight` is 23,909,760 on mainnet and 800 on testnet. The variable in code is `nDigiDollarMuSig2Height`, not the older `nDigiDollarPhase3Height`. Once DigiDollar is active, v0x03 MuSig2 is the only on-chain bundle format ever accepted. +RPC/GBT surface: `getdeploymentinfo`/`getblockchaininfo` render the three deployments as `{"type":"buried","active":…,"height":…}` with no `bip9` sub-object. `getdigidollardeploymentinfo` now returns `{enabled, type:"buried", status:"active"|"defined", activation_height (omitted if the deployment is disabled), oracle_activation_height, musig2_format_activation_height, oracle_pubkey_count, oracle_consensus_required, oracle_total_slots, oracle_seed_peers, musig2_session{...}, thaw_day{scheduled, height (only when scheduled), tip_height, next_block_height, active_at_tip, active_next_block}}`; the BIP9 fields (`bit`, `start_time`, `timeout`, `min_activation_height`, `blocks_until_timeout`, `signaling_blocks`, `threshold`, `period_blocks`, `progress_percent`) were removed, and `activation_height` is now always the burial height (the old back-scan reported the first-active/last-LOCKED_IN block instead). `getblocktemplate` always lists `taproot`/`digidollar`/`algolock` in `rules` once active (hardcoded like `csv`); `vbavailable` no longer mentions them and the template `version` never sets bits 2/23/0. `MinBIP9WarningHeight` is 23,909,760 on mainnet and 800 on testnet. The variable in code is `nDigiDollarMuSig2Height`, not the older `nDigiDollarPhase3Height`. Once DigiDollar is active, v0x03 MuSig2 is the only on-chain bundle format ever accepted. + +## Thaw Day and health accounting + +`nDDThawDayHeight` is the shared height for the new mint-only volatility rule, +open-vault health accounting and canonical vault identity. Read it through +`DigiDollar::IsThawDayActive(params, candidate_height)` in +`src/digidollar/digidollar.cpp`. Mainnet is scheduled at **24,490,000**, estimated +for November 1, 2026. Testnet26 is scheduled at **432,100**, estimated for +September 18–19, 2026. Block heights are the triggers; dates are estimates. +Signet is unsupported and remains unscheduled. Regtest is disabled unless +`-ddthawdayheight=N` is set; the option is rejected outside regtest. Publish +the tagged source, binaries and heights at least 14 days before mainnet +activation and at least 3 days before testnet activation. Public activation +and soak testing have not occurred for this candidate. Do not describe +unfinished release work as ready to distribute. + +Block validation uses the candidate's height, including when a node reindexes. +Mining, mempool and wallet construction use the next-block height. At tip H-1, +`thaw_day.active_at_tip` can be false while `active_next_block` is true. See +[DIGIDOLLAR_ACTIVATION_EXPLAINER.md](DIGIDOLLAR_ACTIVATION_EXPLAINER.md). + +`getdigidollarstats.total_dd_supply` is the circulating-supply API field in +cents. Below Thaw Day, its legacy fallback without the stats index still scans +vault amounts; use a synchronized stats index for verified circulation there. +Do not describe that legacy fallback as an exact token count. +`canonical_health.open_vault_principal` separately counts original DD attached +to unspent vaults. At and above the height, health uses the chainstate-owned principal, +collateral and count, tied to the matching block and rules version. Extra burns +reduce circulation without reducing other vaults' principal. This can change +health bands and redemption burn requirements. It does not change balances at +activation. `canonical_health.ready=false` means unavailable, not zero. + +Top-level `health_rule_height` and `selected_health_denominator` describe tip +health; `next_block_health` reports its own candidate height, quote and +readiness. Validate or reconstruct state before using it. The stats index and +wallet must not overwrite consensus state. Sources: `src/consensus/digidollar_state.h`, +`src/validation.cpp`, `src/digidollar/health.cpp`, `src/rpc/digidollar.cpp`. + +Routine DigiDollar and oracle diagnostics use `LogPrint(BCLog::DIGIDOLLAR, ...)`. +Preserve useful failures and startup/recovery progress without requiring that +category. Keep wallet prefixes. Operator debug settings and startup-only log +shrinking are explained in [doc/digidollar-operations.md](doc/digidollar-operations.md). ## Oracle roster @@ -88,6 +134,13 @@ RPC/GBT surface: `getdeploymentinfo`/`getblockchaininfo` render the three deploy | Testnet | 35 | 35 (slots 0-34) | 7 signatures from active keyset | | Regtest | 7 | 7 | 4-of-7 | +Oracle display names come from each network's `OracleNodeInfo.display_name`. +Mainnet ID 0 is `DigiByte.Io Oracle`; ID 11 is `Crypto Corner Shop`. Their +testnet names remain `Jared` and `hallvardo`. Names are local display metadata; +they are not serialized and do not change keys, IDs, ordering or quorum. +Source: `src/primitives/oracle.h`, `src/kernel/chainparams.cpp` and +`src/rpc/digidollar.cpp`. + Slots 0-34 on mainnet and testnet are active. Slot 28 uses the DigiHash Mining Pool key, slot 31 uses the Peer2Peer / DigiRoos key, and all 35 configured slots contain valid compressed secp256k1 oracle keys. ID 35 is outside the @@ -119,6 +172,14 @@ DD_ADDR_PREFIX = 'DD' / 'TD' / 'RD' (mainnet/testnet/regtest) # DigiDollar-specific DD_AMOUNT_UNIT = 1 cent (10000 = $100.00) +DD_RPC_AMOUNT_UNIT = senddigidollar, sendmanydigidollar, redeemdigidollar, + getredemptioninfo, listdigidollarpositions and + listdigidollaraddresses take an optional trailing + amount_unit of "cents" or "dollars". With no unit the + amount must be a whole number of cents; a decimal point + with no unit is refused instead of guessed + (src/digidollar/amount.cpp, + src/rpc/digidollar.cpp ParseDigiDollarRpcAmount) DD_TX_VERSION_MARKER = 0x0770 (low 16 bits of nVersion) DD_TX_TYPE_FIELD = (nVersion >> 24) & 0xFF # 1=MINT, 2=TRANSFER, 3=REDEEM MINT_MIN = 10_000 cents @@ -140,8 +201,8 @@ LOCK_TIER_OPRETURN = stored explicitly in mint OP_RETURN; consensus rejects | Layer | Gate | Reference | |-------|------|-----------| | RPC | `DigiDollar::IsDigiDollarEnabled(tip, chainman)` at the top of each DD/oracle RPC | `src/rpc/digidollar.cpp` (10+ callsites) | -| Mempool | `IsDigiDollarEnabled` + `HasDigiDollarMarker`; DD txs additionally require a recent valid MuSig2 oracle quote via `HasRecentValidMuSig2OracleQuote` | `src/validation.cpp:224-286, 976-989` | -| Block | `IsDigiDollarEnabled` + `HasDigiDollarMarker` during `ConnectBlock`; coinbase oracle bundles require V1 MuSig2 version via `CheckMuSig2OracleBundleVersion` | `src/validation.cpp:185, 2808-3214` | +| Mempool | `IsDigiDollarEnabled` + `HasDigiDollarMarker`; DD mint and redeem txs additionally require a recent valid MuSig2 oracle quote via `HasRecentValidMuSig2OracleQuote`. DD transfers are price-independent and need no quote (`DigiDollarMempoolTxRequiresOracleQuote`) | `src/validation.cpp:422-491, 1234-1249` | +| Block | `IsDigiDollarEnabled` + `HasDigiDollarMarker` during `ConnectBlock`; coinbase oracle bundles require V1 MuSig2 version via `CheckMuSig2OracleBundleVersion` | `src/validation.cpp:268-304, 3091-3768` | | Script | `SCRIPT_VERIFY_DIGIDOLLAR` flag | `src/validation.cpp` | | P2P | `IsOracleP2PActive` at the top of all oracle handlers, including `oraclehb`, `oracleprice`, `oraclebundle` (accepted-and-dropped — V1 puts the bundle on-chain), `oracleconsns`, `oracleattest`, `oramusnonce`, `oramusigctx`, `oramusigpsig`, and `getoracles`. Wire names defined in `src/protocol.cpp:53-62`. | `src/net_processing.cpp` | | Qt | `DigiDollarTab` activation overlay; widgets check `isVisible()` before polling | `src/qt/digidollartab.cpp` | @@ -149,7 +210,7 @@ LOCK_TIER_OPRETURN = stored explicitly in mint OP_RETURN; consensus rejects ## RPC surface (35 commands) -18 commands registered via `RegisterDigiDollarRPCCommands()` in `src/rpc/digidollar.cpp:6266`: +18 commands registered via `RegisterDigiDollarRPCCommands()` in `src/rpc/digidollar.cpp:7046`: `getdigidollarstats`, `getdcamultiplier`, `calculatecollateralrequirement`, `getdigidollardeploymentinfo`, `importdigidollaraddress`, `estimatecollateral`, `getoracleprice`, `getalloracleprices`, `getprotectionstatus`, `getoracles`, `getoraclesigners`, `listoracle`, `stoporacle`, `getoraclepubkey`, `setmockoracleprice` (regtest), `getmockoracleprice` (regtest), `simulatepricevolatility` (regtest), `enablemockoracle` (regtest). 17 wallet-context commands registered in `GetWalletRPCCommands()` at `src/wallet/rpc/wallet.cpp:888`: @@ -192,14 +253,16 @@ rg -n 'digidollar|DigiDollar|oracle|MuSig|musig' src/ test/ \ - **Confirmed-only DigiDollar transfers.** Unconfirmed DD chaining was removed (commit `0b4959f563`). Consensus refuses to resolve DD amounts from `MEMPOOL_HEIGHT` inputs for transfer/redeem; wallets must wait for confirmation between sends. - **OP_CHECKPRICE is deterministically disabled (DD-FINAL-005).** Post-activation the opcode consumes its operand and unconditionally pushes `vchFalse` (`src/script/interpreter.cpp:708-735`); it no longer calls `g_get_oracle_consensus_price` (the hook is left null in production — see `src/init.cpp` Step 13). This removed the AR-0 fork vector (the old live-price consult was node-local/wall-clock dependent). The standalone `libdigibyteconsensus.so` behaviour is unchanged (also fails closed). -- **Mainnet/testnet validator parity.** The mainnet oracle-validation short-circuit was removed (commit `f0d9a7b2c7`). `OracleDataValidator::ValidateBlockOracleData` (`src/oracle/bundle_manager.cpp:2151`) now runs identically on mainnet and testnet, and only v0x03 MuSig2 bundles are accepted (commits `bbb85cf363`, `fa29405adc`, `f2bb0a19a4`). Raw v0x01/v0x02 OP_RETURN payloads short-circuit inside `ExtractOracleBundle`, so the validator emits `bad-oracle-malformed`. The `bad-oracle-legacy` branch only fires when extraction succeeds with a non-MuSig2 version, which is structurally unreachable for current v0x03 wire payloads — it remains as a defense-in-depth gate. +- **Mainnet/testnet validator parity.** The mainnet oracle-validation short-circuit was removed (commit `f0d9a7b2c7`). `OracleDataValidator::ValidateBlockOracleData` in [src/oracle/bundle_manager.cpp](src/oracle/bundle_manager.cpp) now runs identically on mainnet and testnet, and only v0x03 MuSig2 bundles are accepted (commits `bbb85cf363`, `fa29405adc`, `f2bb0a19a4`). Raw v0x01/v0x02 OP_RETURN payloads short-circuit inside `ExtractOracleBundle`, so the validator emits `bad-oracle-malformed`. The `bad-oracle-legacy` branch only fires when extraction succeeds with a non-MuSig2 version, which is structurally unreachable for current v0x03 wire payloads — it remains as a defense-in-depth gate. - **Price-dependent DD blocks must include exactly one v0x03 MuSig2 bundle.** Commit `1e08bd811f`: `ValidateBlockOracleData` returns `bad-oracle-missing` if a DD mint/redeem block has no oracle output, `bad-oracle-multiple-outputs` if it has more than one, and `bad-oracle-malformed` if extraction fails (which is the canonical reason raw v0x01/v0x02 produce). DD transfer-only and non-DD blocks may omit the bundle entirely. -- **Mempool requires an oracle quote for DD txs.** Commit `81bf974f40`: DD txs are not accepted into the mempool unless `HasRecentValidMuSig2OracleQuote` finds a recent valid v0x03 quote (`src/validation.cpp:224-286, 976-989`). +- **Mempool requires an oracle quote for DD mint and redeem.** Commit `81bf974f40`: a DD mint or redeem is not accepted into the mempool unless `HasRecentValidMuSig2OracleQuote` finds a recent valid v0x03 quote. `DigiDollarMempoolTxRequiresOracleQuote` limits the check to those two types, so a DD transfer, which needs no price, is admitted without one (`src/validation.cpp:422-491, 1234-1249`). - **Custom lock durations rejected.** Consensus enforces canonical lock tiers with a 100-block confirmation buffer: `bad-mint-lock-period` for non-canonical periods, `bad-mint-lock-tier` for tier outside 0–9, and `bad-mint-lock-tier-duration` when remaining lock blocks fall outside `[canonical_blocks, canonical_blocks + 100]` for the claimed tier (commits `e1dd69f99b`, `11728a6980`). -- **DD supply alert, not a cap.** `AlertThresholds::ALERT_DD_SUPPLY` (`src/digidollar/health.h:84`, 10000000000 = 100M DD) is a monitoring threshold, not a consensus cap. `MAX_DIGIDOLLAR` is a per-output serialization bound. There is no global circulating-supply cap; total DD is constrained only by available collateral and the per-block minting rate. +- **DD supply alert, not a cap.** `AlertThresholds::ALERT_DD_SUPPLY` (`src/digidollar/health.h:170`, 10000000000 = 100M DD) is a monitoring threshold, not a consensus cap. `MAX_DIGIDOLLAR` is a per-output serialization bound. There is no global circulating-supply cap; total DD is constrained only by available collateral and the per-block minting rate. - **Collateral vault spends require DD burn.** Non-DD transactions that try to spend a registered DigiDollar collateral vault are rejected with `bad-collateral-spend-missing-dd-burn`. Inside DD redemptions, `ValidateCollateralReleaseAmount` rejects partial burns with `bad-collateral-release-partial-burn` (`src/digidollar/validation.cpp`). -- **Qt mint derives HD owner keys.** Commit `1e95478b7e` made the Qt mint flow derive DD owner keys from the wallet's HD chain and persist them via `DigiDollarWallet::StoreOwnerKey` *before* broadcasting the mint tx, instead of generating an ephemeral random key after broadcast. Mint now requires an HD wallet with private keys enabled. RPC mint already used the same path. +- **Mint wallet capability.** `src/wallet/digidollarmintcapability.h` checks descriptor/private-key flags, HD support, an active ranged Taproot receiving descriptor and an active ranged bech32 internal change descriptor with private keys. It does not allocate keys. Qt checks before its confirmation dialogs; RPC checks before unlock/key derivation. A supported encrypted wallet remains eligible for the normal unlock flow. Legacy, watch-only, blank and restricted wallets receive a capability error when required support is missing. Preserve existing backups and never promise that migration recreates missing keys. See `DIGIDOLLAR_WALLET_INTEGRATION.md`. - **Mining graceful degradation.** `CreateNewBlock` strips price-dependent DD mint/redeem txs when no valid oracle bundle is available, keeps ordinary DGB and price-independent DD transfer candidates flowing when valid, and continues block assembly instead of hanging (commit `6b5ff516c3`). +- **A pruned node keeps the whole DigiDollar era.** A DigiDollar spend has to read the block that created its input, and that block is always at or above the DigiDollar floor. So when `-prune` is set, `LoadChainstate` registers a prune lock named `digidollar` at `DigiDollar::EarliestActivationFloor(params)` and the node keeps every block from there to the tip (`src/node/chainstate.cpp:159-190`). On mainnet that floor is 23,627,520, which at the tip of 24,195,289 recorded on 12 September 2026 is 567,769 blocks, about 2.3 per cent of the chain, and grows by roughly 5,760 a day. A small `-prune` target such as 550 MiB is therefore not reachable on mainnet: the node still starts and still prunes everything below the floor, but it cannot shrink past the retained window. If a DigiDollar-era block is already missing when a pruned node starts, startup fails with "DigiDollar state not ready: retained block history is incomplete". +- **Transaction decoding declares its DigiDollar fields.** `TxToUniv` adds a `digidollar` object (`type`, `type_id`, `flags`, `flags_hex`) to any transaction carrying the marker (`src/core_write.cpp:186-193`), and `decoderawtransaction`/`getrawtransaction` now declare that object in their result (`src/rpc/rawtransaction.cpp:107-113`). Before this release the field was returned but not declared, so a node started with `-rpcdoccheck` raised an internal error on the call. - **BIP324 V2 P2P transport** is supported and enabled with `-v2transport=1` (off by default). - **Three-way comparison still applies for non-DigiDollar work.** Compare v9.26 ↔ v8.22.2 ↔ Bitcoin v26.2 in `digibyte-v8.22.2/` and `bitcoin-v26.2-for-digibyte/` when making changes to inherited code. - **Avoid widening doc claims beyond what code shows.** All material claims in the approved docs (16 listed in `Z_PROMPTS.md`) must match `src/`. Treat code as truth. diff --git a/DIGIDOLLAR_ACTIVATION_EXPLAINER.md b/DIGIDOLLAR_ACTIVATION_EXPLAINER.md index bb2f27b7b78..e797a8e6c3a 100644 --- a/DIGIDOLLAR_ACTIVATION_EXPLAINER.md +++ b/DIGIDOLLAR_ACTIVATION_EXPLAINER.md @@ -1,4 +1,69 @@ -# DigiDollar BIP9 Activation — Complete Explainer +# DigiDollar activation and Thaw Day + +## Thaw Day in the v9.26.6 development source + +Thaw Day is a separate, coordinated change to DigiDollar validation. It uses +one height per network, `Consensus::Params::nDDThawDayHeight`. + +| Network | Thaw Day height | Estimated date | +|---------|-----------------|----------------| +| Mainnet | **24,490,000** | November 1, 2026 | +| Testnet26 | **432,100** | September 18–19, 2026 | +| Signet (unsupported) | Not scheduled | None | +| Regtest | Not scheduled unless `-ddthawdayheight=N` is set | Local test setting | + +The block height triggers activation. The date is an estimate and changes with +the block rate. Unscheduled networks use `std::numeric_limits::max()`, +which means disabled. No public-network Thaw Day activation or soak test has +been completed for this candidate. + +The new rules apply only when the height is configured, DigiDollar itself is +active, and the candidate block's height is at least that height. The same +boundary selects the mint-only volatility rule, open-vault health accounting +and vault recognition by the creating transaction and output index. Earlier +blocks retain their legacy validation path, including during a reindex. +Installing this source does not turn those rules on early. + +| Caller | Height used | +|--------|-------------| +| Block validation and reindex | The block being checked | +| Mining, mempool and wallet estimates | The next block on the current branch | +| Disconnect and reorganization | The affected block and replacement branch | +| Status display | Tip and next block reported separately | + +For a scheduled height H, a tip at H-1 has `active_at_tip=false` and +`active_next_block=true`, provided DigiDollar is active at H. This is the +expected boundary. A wallet estimate cannot guarantee the quote, chain state +or preceding transactions of the block that eventually confirms it. + +`getdigidollardeploymentinfo.thaw_day` reports `scheduled`, the `height` only +when scheduled, `tip_height`, `next_block_height`, `active_at_tip` and +`active_next_block`. Do not confuse its `height` with the older +`activation_height` for DigiDollar itself. There is no public-network runtime +switch or miner signal that changes Thaw Day. `-ddthawdayheight=N` is for +regtest only and is a startup error on other networks. + +Publish the exact tagged source, binaries, checksums and activation heights +at least 14 days before mainnet activation and at least 3 days before testnet +activation. The current testnet estimate requires distribution by the evening +of September 15, 2026, in America/Boise. September 21 was the testnet target; +the selected height is expected to arrive earlier, on September 18–19. +Recheck the block rate throughout each upgrade window. Coordinate the rollout +with miners, oracles, exchanges and full-node operators. Scheduling a height +does not prove that operators have upgraded or that release checks have passed. + +If the upgrade window or release checks cannot be met, coordinate a replacement +release before the scheduled height to postpone or disable activation. An +announcement alone cannot change heights in installed binaries. Older nodes +may disagree after activation; burial of a previously activated deployment +does not establish compatibility with this new transition. +See [node operations](doc/digidollar-operations.md) for backup, upgrade and +recovery guidance. Release and network readiness still require separate +verification. + +Source: [shared predicate](src/digidollar/digidollar.cpp), +[network heights](src/kernel/chainparams.cpp), +[status RPC](src/rpc/digidollar.cpp) and [regtest options](src/chainparams.cpp). ## Post-activation status (v9.26.5): BURIED DEPLOYMENT @@ -8,14 +73,14 @@ |---------|-----------------|--------------------|------------------| | Mainnet | 21,168,000 | 23,869,440 | 23,869,440 | | Testnet (testnet26) | 0 | 600 | 0 | -| Signet / Regtest | 0 | 0 | 0 | +| Signet (unsupported) / Regtest | 0 | 0 | 0 | The static gates keep their historical floors: mainnet `nDDActivationHeight = nOracleActivationHeight = nDigiDollarMuSig2Height = 23,627,520` (below the 23,869,440 burial height — `EarliestActivationFloor = min(nDDActivationHeight, DigiDollarHeight)` preserves the 23,627,520 prune/collateral floor exactly); testnet 600; default regtest DD/oracle gates 650 with `nDigiDollarMuSig2Height = min(650, DigiDollarHeight) = 0`. **What changed operationally in v9.26.5:** - **No signaling.** Blocks no longer set bits 2/23/0; the STARTED/LOCKED_IN/FAILED states no longer exist for these three deployments. `getblocktemplate` lists `taproot`/`digidollar`/`algolock` in `rules` when active (hardcoded like `csv`), omits them from `vbavailable`, and never sets their bits in the template `version`. -- **RPC shapes.** `getdeploymentinfo`/`getblockchaininfo` render the three as `{"type":"buried","active":bool,"height":N}` with no `bip9` sub-object. `getdigidollardeploymentinfo` now returns `{enabled, type:"buried", status:"active"|"defined", activation_height (omitted if disabled), oracle_activation_height, musig2_format_activation_height, oracle_pubkey_count, oracle_consensus_required, oracle_total_slots, oracle_seed_peers, musig2_session{...}}` — the BIP9 fields (`bit`, `start_time`, `timeout`, `min_activation_height`, signaling statistics) were removed and `activation_height` is always the burial height. +- **RPC shapes.** `getdeploymentinfo`/`getblockchaininfo` render the three as `{"type":"buried","active":bool,"height":N}` with no `bip9` sub-object. `getdigidollardeploymentinfo` now returns `{enabled, type:"buried", status:"active"|"defined", activation_height (omitted if disabled), oracle_activation_height, musig2_format_activation_height, oracle_pubkey_count, oracle_consensus_required, oracle_total_slots, oracle_seed_peers, musig2_session{...}, thaw_day{scheduled, height (only when scheduled), tip_height, next_block_height, active_at_tip, active_next_block}}` — the BIP9 fields (`bit`, `start_time`, `timeout`, `min_activation_height`, signaling statistics) were removed and `activation_height` is always the burial height. - **Regtest knobs.** `-digidollaractivationheight=N` sets `DigiDollarHeight` and the static DD/oracle/MuSig2 gates to N, so DigiDollar activates at exactly height N (pre-burial it ran real BIP9 signaling and activated at the first 144-block window boundary >= max(432, N)). New `-testactivationheight=taproot@H` / `digidollar@H` / `algolock@H` moves only the buried deployment height; `-digidollaractivationheight` takes precedence for DigiDollar. `-vbparams=digidollar/taproot/algolock` is now a startup error ("Invalid deployment") — only `testdummy` remains a versionbits deployment. - **Predicates.** `IsDigiDollarEnabled` is a pure height compare against `DigiDollarHeight`; no `VersionBitsCache` exists anywhere in the DigiDollar path. `MinBIP9WarningHeight` moved to 23,909,760 (mainnet) / 800 (testnet) so the historical signaling periods do not trigger "unknown new rules" warnings. @@ -25,7 +90,7 @@ The static gates keep their historical floors: mainnet `nDDActivationHeight = nO ## Overview -DigiDollar activated on the DigiByte blockchain through **BIP9 version bit signaling** — the same proven mechanism used by Bitcoin for SegWit and other soft forks. This ensured DigiDollar only activated once a supermajority of miners explicitly signaled support, preventing chain splits and ensuring network consensus. (As of v9.26.5 the completed deployment is buried; see the section above.) +DigiDollar activated on the DigiByte blockchain through **BIP9 version bit signaling** — the same proven mechanism used by Bitcoin for SegWit and other soft forks. The signaling threshold coordinated the original deployment. Signaling did not guarantee that every operator had upgraded or that all implementations agreed. (As of v9.26.5 the completed deployment is buried; see the section above.) **Key principle:** Nothing consensus-critical for DigiDollar works until activation. DD/oracle RPCs, DD transactions, DD opcodes, oracle price relay, oracle consensus relay, MuSig2 relay, `getoracles`, and signed oracle version heartbeats are dormant until the activation predicates below pass. @@ -113,7 +178,7 @@ DEFINED ──→ STARTED ──→ LOCKED_IN ──→ ACTIVE The DigiDollar/oracle RPC surface is split between the node-context registration in `src/rpc/digidollar.cpp` (registered via `RegisterDigiDollarRPCCommands`) and the wallet-context registration in `src/wallet/rpc/wallet.cpp` (added inside `GetWalletRPCCommands`). -**Node-context (18, registered in `src/rpc/digidollar.cpp:6266`):** +**Node-context (18, registered in `src/rpc/digidollar.cpp:7046`):** - `getdigidollarstats`, `getdcamultiplier`, `calculatecollateralrequirement`, `getdigidollardeploymentinfo`, `importdigidollaraddress`, `estimatecollateral` - `getoracleprice`, `getalloracleprices`, `getprotectionstatus`, `getoracles`, `getoraclesigners`, `listoracle`, `stoporacle`, `getoraclepubkey` - Regtest helpers: `setmockoracleprice`, `getmockoracleprice`, `simulatepricevolatility`, `enablemockoracle` @@ -151,8 +216,8 @@ The DigiDollar/oracle RPC surface is split between the node-context registration ### Consensus Validation (all activation-gated) -1. **Mempool acceptance** (`src/validation.cpp:976-989`): `DigiDollar::HasDigiDollarMarker(tx)` + `IsDigiDollarEnabled()` → rejects DD TXs with `TX_CONSENSUS "digidollar-not-active"`. After activation, mempool acceptance also requires that an oracle quote is available for any DD transaction (commit `81bf974f40`). -2. **Block validation** (`src/validation.cpp:2816-2854`): `DeploymentActiveAt(DEPLOYMENT_DIGIDOLLAR)` during `ConnectBlock()` delegates to `DeploymentActiveAfter(index.pprev, ...)`, so the candidate block is judged by comparing the previous block's height + 1 against the buried activation height (v9.26.5). Blocks containing DD TXs before activation are rejected. After activation, `ValidateBlockOracleData` (`src/oracle/bundle_manager.cpp:2151`) requires DD mint/redeem blocks to carry exactly one v0x03 MuSig2 oracle bundle in the coinbase. DD transfer-only and non-DD blocks may omit oracle data; if any block includes one it must still be a valid v0x03 bundle (commit `1e08bd811f`). +1. **Mempool acceptance** (`src/validation.cpp:1234-1249`): `DigiDollar::HasDigiDollarMarker(tx)` + `IsDigiDollarEnabled()` → rejects DD TXs with `TX_CONSENSUS "digidollar-not-active"`. After activation, mempool acceptance also requires that an oracle quote is available for any DD transaction (commit `81bf974f40`). +2. **Block validation** (`src/validation.cpp:3144-3151, 3407-3500`): `DeploymentActiveAt(DEPLOYMENT_DIGIDOLLAR)` during `ConnectBlock()` delegates to `DeploymentActiveAfter(index.pprev, ...)`, so the candidate block is judged by comparing the previous block's height + 1 against the buried activation height (v9.26.5). Blocks containing DD TXs before activation are rejected. After activation, `OracleDataValidator::ValidateBlockOracleData` in [src/oracle/bundle_manager.cpp](src/oracle/bundle_manager.cpp) requires DD mint/redeem blocks to carry exactly one v0x03 MuSig2 oracle bundle in the coinbase. DD transfer-only and non-DD blocks may omit oracle data; if any block includes one it must still be a valid v0x03 bundle (commit `1e08bd811f`). 3. **Script verification** (`validation.cpp` script-flag setup): `SCRIPT_VERIFY_DIGIDOLLAR` flag only set when `DeploymentActiveAt()` returns true, so the Tapscript OP_SUCCESSx-class DD opcodes are not interpreted as DigiDollar operations before activation. Once active, `OP_CHECKPRICE` is reserved and deterministically disabled (`src/script/interpreter.cpp:708-735`): it consumes one stack item and pushes false rather than reading node-local oracle state. 4. **Mining graceful degradation** (`src/node/miner.cpp`, commit `6b5ff516c3`): `CreateNewBlock` strips price-dependent DD mint/redeem txs when no valid oracle bundle is available rather than aborting block assembly. Transfer-only DD txs are validated with oracle-price validation skipped because they do not need a block oracle price. The block is still produced; rejected DD txs remain in the mempool until either they confirm in a later attempt or are evicted. @@ -283,12 +348,12 @@ A practical implication: there is no period in which the oracle P2P surface is l | Deployment names | `src/deploymentinfo.cpp` | `DeploymentName(BuriedDeployment)` + `GetBuriedDeployment()` (resolves `-testactivationheight` names); `VersionBitsDeploymentInfo[]` retains only testdummy | | RPC activation gate | `src/rpc/digidollar.cpp` | `IsDigiDollarEnabled()` check in each RPC | | P2P activation gate | `src/net_processing.cpp` | `IsOracleP2PActive()` in `ORACLEPRICE`/`ORACLEBUNDLE`/`ORACLECONSENSUS`/`ORACLEATTESTATION`/`ORACLEMUSIGNONCE`/`ORACLEMUSIGCONTEXT`/`ORACLEMUSIGPARTIALSIG`/`GETORACLES`/`ORACLEHEARTBEAT` | -| Mempool gate | `src/validation.cpp:976-989` | `IsDigiDollarEnabled()` in `AcceptToMemoryPool`; recent MuSig2 quote required for DD txs | -| Block validation gate | `src/validation.cpp:2816-2854` | `DeploymentActiveAt(DEPLOYMENT_DIGIDOLLAR)` in `ConnectBlock` | -| Script flags | `src/validation.cpp:2755-2798` | `SCRIPT_VERIFY_DIGIDOLLAR` flag (set in `GetBlockScriptFlags`) | +| Mempool gate | `src/validation.cpp:1234-1249` | `IsDigiDollarEnabled()` in `AcceptToMemoryPool`; recent MuSig2 quote required for DD txs | +| Block validation gate | `src/validation.cpp:3407-3500` | `DeploymentActiveAt(DEPLOYMENT_DIGIDOLLAR)` in `ConnectBlock` | +| Script flags | `src/validation.cpp:3030-3073` | `SCRIPT_VERIFY_DIGIDOLLAR` flag (set in `GetBlockScriptFlags`) | | Qt activation overlay | `src/qt/digidollartab.cpp` | `checkActivationStatus()` timer | | Qt widget polling guard | `src/qt/digidollar*widget.cpp` | `if (!isVisible()) return;` | -| Oracle height gate | `src/consensus/params.h:243-245` | `IsOracleActive()` | +| Oracle height gate | `src/consensus/params.h:279-281` | `IsOracleActive()` | | DD enabled check | `src/digidollar/digidollar.cpp` | `IsDigiDollarEnabled()` | | Oracle bundle V1 enforcement | `src/oracle/bundle_manager.cpp` (`ValidateBlockOracleData`, `ExtractOracleBundle`, `CreateOracleScript`) | Raw v0x01/v0x02 OP_RETURN payloads short-circuit in `ExtractOracleBundle` (returns false), surfaced by the validator as `bad-oracle-malformed`. Only v0x03 MuSig2 bundles are accepted; the `bad-oracle-legacy` branch is defense-in-depth | | Reserved `OP_CHECKPRICE` behavior | `src/script/interpreter.cpp:708-735` | `OP_CHECKPRICE` is reserved and deterministically disabled; the old `g_get_oracle_consensus_price` hook remains only for tests | diff --git a/DIGIDOLLAR_ARCHITECTURE.md b/DIGIDOLLAR_ARCHITECTURE.md index 797f8fe53d4..d760e363493 100644 --- a/DIGIDOLLAR_ARCHITECTURE.md +++ b/DIGIDOLLAR_ARCHITECTURE.md @@ -1,9 +1,8 @@ # DigiDollar Implementation Architecture -**DigiByte v9.26.2 - Current Implementation Status** -*Updated: 2026-05-20* -*Implementation Status: V1 / `feature/digidollar-v1`* -*Document Version: 6.8 - Validated against codebase* -*Validation Status: Validated Against Codebase (2026-05-20)* +**DigiByte v9.26.6 development source** + +The activation and accounting sections below describe the current source. +They do not establish release readiness or completed public-network testing. ## Executive Summary @@ -15,17 +14,24 @@ DigiDollar is a decentralized USD-denominated token design built natively on Dig ### Current Implementation Status -The DigiDollar V1 stack on `feature/digidollar-v1` is feature-complete; remaining work is operational hardening and mainnet rollout. Highlights: +The source contains the DigiDollar V1 stack and a height-gated validation +transition. Mainnet Thaw Day is scheduled at 24,490,000, estimated for +November 1, 2026. Testnet26 is scheduled at 432,100, estimated for +September 18–19, 2026. The heights trigger the changes. Signet is unsupported +and remains unscheduled. Release verification and coordinated activation are +still required. +See the [activation guide](DIGIDOLLAR_ACTIVATION_EXPLAINER.md) for the upgrade +window and postponement process. Components include: - **Address System** — DD/TD/RD addresses live on mainnet/testnet/regtest (`src/base58.{h,cpp}`). - **Minting / Transferring / Redeeming** — Full P2TR pipeline (`src/digidollar/txbuilder.cpp`, `src/digidollar/validation.cpp`). - **Confirmed-only DD chaining** — Consensus refuses `MEMPOOL_HEIGHT` DD inputs for transfers and redeems (`src/digidollar/validation.cpp:1811, 1955`). -- **Network-Wide Tracking** — `SystemHealthMonitor::ScanUTXOSet` plus incremental `OnMintConnected/OnRedeemConnected` hooks (`src/digidollar/health.cpp`); `DigiDollarStatsIndex` provides per-block aggregates (`src/index/digidollarstatsindex.{cpp,h}`). +- **Network-wide tracking** — Circulating tokens remain separate from open-vault principal. After Thaw Day, validation uses the chainstate health record described below. The stats index reports aggregates; it does not own consensus state. - **Protection Systems** — DCA (`src/consensus/dca.cpp`), ERR (`src/consensus/err.cpp`) and Volatility (`src/consensus/volatility.cpp`) all production-wired. ERR enforces extra DD burn (no DGB haircut) via integer `__int128` math. - **Canonical Lock Tiers** — Mint OP_RETURNs must declare tier 0-9. Validation accepts only the claimed canonical tier window `[tier_blocks, tier_blocks + 100]`, so under-locked/custom terms are rejected while delayed mining remains valid. - **OP_CHECKPRICE reserved opcode** — `OP_CHECKPRICE` is reserved and deterministically disabled; the interpreter consumes its operand and pushes false instead of reading node-local oracle state (`src/script/interpreter.cpp:708-735`). -- **MuSig2 V1 oracle path only** — Block validation rejects pre-V1 (legacy) oracle bundles in coinbase once DigiDollar is active (`src/validation.cpp:185-217`); mempool acceptance requires a recent valid MuSig2 oracle quote (`src/validation.cpp:224-283`). -- **Mining graceful degradation** — DD txs that fail validation are stripped from `mapModifiedTx` rather than blocking block assembly (`src/node/miner.cpp:707-744`). +- **MuSig2 V1 oracle path only** — Block validation rejects pre-V1 (legacy) oracle bundles in coinbase once DigiDollar is active (`src/validation.cpp:268-304`); mempool acceptance requires a recent valid MuSig2 oracle quote (`src/validation.cpp:422-491`). +- **Mining graceful degradation** — DD txs that fail validation are stripped from `mapModifiedTx` rather than blocking block assembly (`src/node/miner.cpp:898-913`). - **Comprehensive Testing** — Unit tests under `src/test/digidollar_*` and `src/test/rh*`/`src/test/oracle_*`/`src/test/musig2_*`; functional tests under `test/functional/digidollar_*.py` and `test/functional/wallet_digidollar_*.py`. See `REPO_MAP_DIGIDOLLAR.md` for the full inventory. This document explains how each piece works, where the code lives, and how the runtime gates are wired together. @@ -198,8 +204,8 @@ Phase 1 (data structures, P2TR scripts, DD addresses, transaction versioning) an │ active. │ ├─────────────────────────────────────────────┤ │ Volatility (src/consensus/volatility.cpp): │ -│ • 1h vol > 20% -> freeze new mints │ -│ • 24h vol > 30% -> freeze ALL DD ops │ +│ • Legacy volatility checks before Thaw Day │ +│ • Mint-only chain reference after Thaw Day │ │ • Cooldown: 8640 blocks (~36h) │ │ • Pre-mutation gate: WouldCandidateFreeze │ │ Minting() rejects bad mints before they │ @@ -574,12 +580,12 @@ DigiDollar consensus consumes consensus prices from the oracle subsystem; the fu **Where DigiDollar reads the price.** - `src/script/interpreter.cpp` — `OP_CHECKPRICE` is reserved and deterministically disabled. It consumes its operand and pushes false; it does not consult any node-local oracle cache. - `src/digidollar/validation.cpp` — `ValidateMintTransaction()` requires `ctx.oraclePriceMicroUSD > 0` (rejects with `bad-oracle-price` otherwise) and `ShouldBlockMintingDuringERR()` calls `EmergencyRedemptionRatio::ShouldBlockMinting()` which pulls the oracle price from `MockOracleManager` on regtest and `OracleBundleManager::GetLatestPrice()` elsewhere, failing closed when the price is unavailable. -- `src/validation.cpp:185-283` — Block validation rejects coinbase oracle bundles that fail extraction with `bad-oracle-malformed` once `IsDigiDollarEnabled` is true (the canonical reason for raw v0x01/v0x02 wire payloads, since `ExtractOracleBundle` short-circuits at `src/oracle/bundle_manager.cpp:1065-1069`). The `bad-oracle-legacy` reason is kept as a defense-in-depth gate for hypothetical bundles that parse successfully but report a non-MuSig2 `version`. Mempool acceptance requires a recent valid MuSig2 oracle quote (`HasRecentValidMuSig2OracleQuote`). +- `src/validation.cpp:268-491` — Block validation rejects coinbase oracle bundles that fail extraction with `bad-oracle-malformed` once `IsDigiDollarEnabled` is true (the canonical reason for raw v0x01/v0x02 wire payloads, since `ExtractOracleBundle` short-circuits at `src/oracle/bundle_manager.cpp:1017-1021`). The `bad-oracle-legacy` reason is kept as a defense-in-depth gate for hypothetical bundles that parse successfully but report a non-MuSig2 `version`. Mempool acceptance requires a recent valid MuSig2 oracle quote (`HasRecentValidMuSig2OracleQuote`). - `src/digidollar/health.cpp` — `SystemHealthMonitor` caches the last oracle price (`SystemMetrics::lastOraclePrice`) and feeds DCA/ERR. **Mock oracle (regtest only).** `MockOracleManager` (`src/oracle/mock_oracle.cpp`) is a regtest helper used by `setmockoracleprice`/`enablemockoracle`/`simulatepricevolatility` RPCs and by `EmergencyRedemptionRatio::ShouldBlockMinting()` when running on regtest. It is *not* a production fallback for any consensus path; mainnet/testnet `ShouldBlockMinting` consults the real `OracleBundleManager`, while `OP_CHECKPRICE` is reserved and deterministically disabled. -**Oracle quote requirement at mempool admission.** Mainnet/testnet nodes refuse to admit DigiDollar transactions when no recent valid MuSig2 oracle quote is available (`HasRecentValidMuSig2OracleQuote`, commit `81bf974f40`); reorg-resurrected DD transactions are likewise removed if no quote is available (`src/validation.cpp:601-602`). +**Oracle quote requirement at mempool admission.** Mainnet/testnet nodes refuse to admit DigiDollar transactions when no recent valid MuSig2 oracle quote is available (`HasRecentValidMuSig2OracleQuote`, commit `81bf974f40`); reorg-resurrected DD transactions are likewise removed if no quote is available (`src/validation.cpp:815-820`). --- @@ -596,20 +602,60 @@ DigiDollar consensus consumes consensus prices from the oracle subsystem; the fu #### **Layer 2: Dynamic Collateral Adjustment (DCA)** Implemented in `/src/consensus/dca.cpp`. -System health is computed once per snapshot and cached on `SystemHealthMonitor`; DCA always reads the cached value (single source of truth, commit `2de69c94d9`): - -```cpp -// Cached metrics drive DCA. Inputs: -// totalCollateral — DGB satoshis from ScanUTXOSet + OnMint/OnRedeemConnected -// totalDDSupply — DD cents from the same incremental hooks -// oraclePrice — micro-USD per DGB from the consensus price (live MuSig2) -// -// CalculateSystemHealth() returns an int percentage in [0, 30000]; the math -// uses signed __int128 (commit 9cca6970ae) to avoid overflowing on large -// collateral × price products. -int health = DCA::DynamicCollateralAdjustment::CalculateSystemHealth( - totalCollateral, totalDDSupply, oraclePriceMillicents); -``` +The candidate height selects the health inputs. Below Thaw Day, validation +keeps the legacy health path. At and above Thaw Day, each candidate uses a +`ChainstateHealth` record tied to its parent UTXO state and its own valid oracle +quote. The record belongs to chainstate, not a wallet, RPC call or stats index. + +| Quantity | Meaning | +|----------|---------| +| Circulating DD | Accepted issuance minus actual DD burns; the circulating-supply API field is `total_dd_supply` in cents, subject to the legacy fallback limit below | +| Open-vault principal | Original DD amounts attached to vault outputs that remain unspent | +| Active collateral | DGB locked in those same vault outputs | +| Active vault count | Number of those unspent vault outputs | + +Before Thaw Day, the legacy fallback used when `digidollarstatsindex` is +disabled scans vault amounts. Do not treat that fallback's `total_dd_supply` +as a verified circulating token count. Use a synchronized stats index for +circulation reporting below the transition. At and above Thaw Day, the +fallback reconstructs tokens separately from vault principal. + +A redemption removes the closed vault's original principal from the health +record. Extra DD burned during redemption reduces circulating supply too, but +does not close anyone else's vault. Transfers do not change vault principal. +For example, starting with 1,000 DD in both totals, closing a 100-DD vault and +burning 125 DD leaves 875 circulating tokens and 900 DD of open-vault principal. +After Thaw Day, health uses 900. It does not recreate the extra 25 burned tokens. + +Health is the collateral's quoted dollar value divided by the selected +liability amount. `DCA::DynamicCollateralAdjustment::CalculateSystemHealth` +keeps integer rounding and the existing health bands. Using open-vault +principal after extra burns can produce lower health, prolong emergency mint +restrictions or require more DD to redeem. This is a validation change, not +just a display change. It does not alter a wallet balance at activation. + +The record stores format/rules versions, network identity, activation heights, +block hash, principal, collateral, vault count and whether activated history +was checked. Each coins view stages its own record. Accepted transactions +update it in block order; rejected candidates do not publish it. Disconnect +restores the prior state. Crossing the activation boundary on a replacement +branch requires the appropriate state for that branch. Missing data is a +readiness error, not a verified empty system. Startup may need retained blocks +and undo data to verify previously unchecked history. + +`getdigidollarstats` exposes `canonical_health` separately from +`total_dd_supply`. Use its `ready` flag before consuming canonical amounts. +Top-level `selected_health_denominator`, `health_denominator_cents`, +`health_rule_height` and `health_oracle_price_source` describe tip-rule display +health. `next_block_health` has its own candidate height, readiness, quote and +denominator. At tip H-1, display health can still use legacy rules while the +next block uses the new rules. See the [activation guide](DIGIDOLLAR_ACTIVATION_EXPLAINER.md). + +Source: [record](src/consensus/digidollar_state.h), +[validation and recovery](src/validation.cpp), +[health reconstruction](src/digidollar/health.cpp), +[health arithmetic](src/consensus/dca.cpp), +[database writes](src/txdb.cpp) and [reporting](src/rpc/digidollar.cpp). **DCA tiers (HEALTH_TIERS in src/consensus/dca.cpp:53-59):** @@ -620,7 +666,10 @@ int health = DCA::DynamicCollateralAdjustment::CalculateSystemHealth( | 110–119% | 1.50x | 15000 | critical | | <110% | 2.00x | 20000 | emergency | -`ConsensusParams::dcaLevels` (in `src/consensus/digidollar.h`) carries the same tier values. `ApplyDCA(baseRatio, health)` applies `ceil(baseRatio * multiplierBps / 10000)` and fails closed (returns INT_MAX) when the caller-supplied health is stale relative to the canonical cached value. +`ApplyDCAForHealth(baseRatio, health)` applies +`ceil(baseRatio * multiplierBps / 10000)` to the explicit health input. The +legacy `ApplyDCA` wrapper retains its cached-health consistency guard; it is +not the post-Thaw Day source of consensus health. #### **Layer 3: Emergency Redemption Ratio (ERR)** Implemented in `/src/consensus/err.cpp`. @@ -651,26 +700,35 @@ CAmount EmergencyRedemptionRatio::GetRequiredDDBurn(CAmount originalDDMinted, in | Health | ratioBps | DD Burn (= 10000/ratioBps) | |--------|----------|----------------------------| -| 95–100% | 9500 | 1.053× | -| 90–95% | 9000 | 1.111× | -| 85–90% | 8500 | 1.176× | +| 95–99% | 9500 | 1.053× | +| 90–94% | 9000 | 1.111× | +| 85–89% | 8500 | 1.176× | | <85% | 8000 | 1.250× | -**Mint blocking.** `ShouldBlockMinting(oraclePriceOverride)` blocks new mints whenever ERR is active OR no oracle price is available; it fails closed during oracle outages so health uncertainty cannot allow new DD issuance (commit `8bbbfedf70`). Block validation enforces this independently of the local mempool sync flag (`src/digidollar/validation.cpp:2683-2685`, `ShouldBlockMintingDuringERR` at `3004-3007`). +**Mint blocking.** Below Thaw Day, validation retains the legacy +`ShouldBlockMinting` path. At and above the height, +`ShouldBlockMintingDuringERR` uses the candidate's canonical health context. +Unavailable health cannot authorize minting, and health below 100% blocks new +mints. Oracle-quote checks still apply independently. See +[src/digidollar/validation.cpp](src/digidollar/validation.cpp). -**Mint-time burn enforcement.** `ValidateCollateralReleaseAmount()` (`src/digidollar/validation.cpp:2316+`) requires the redeemer to burn at least `requiredDDBurn` and to release the FULL locked collateral; partial-burn releases are rejected as `bad-collateral-release-partial-burn`. Non-DD transactions that try to spend a registered collateral vault are rejected with `bad-collateral-spend-missing-dd-burn` (commit `46cf97e804`). +**Mint-time burn enforcement.** `ValidateCollateralReleaseAmount()` (`src/digidollar/validation.cpp:2462+`) requires the redeemer to burn at least `requiredDDBurn` and to release the FULL locked collateral; partial-burn releases are rejected as `bad-collateral-release-partial-burn`. Non-DD transactions that try to spend a registered collateral vault are rejected with `bad-collateral-spend-missing-dd-burn` (commit `46cf97e804`). #### **Layer 4: Volatility Protection** -**Status: ✅ FULLY IMPLEMENTED AND PRODUCTION-READY** (`/src/consensus/volatility.cpp`) -```cpp -// DigiDollar::Volatility::VolatilityMonitor (src/consensus/volatility.h) -class VolatilityMonitor { - static bool ShouldFreezeMinting(); // True if 1-hour volatility > 20% - static bool ShouldFreezeAll(); // True if 24-hour volatility > 30% - static bool InCooldownPeriod(); // Post-freeze cooldown (8640 blocks) -}; -``` +Below Thaw Day, the legacy `VolatilityMonitor` checks remain in force. At and +above the shared height, new mints compare their valid block quote with a +reference derived from ancestor blocks on the candidate's branch. The old +volatility checks no longer block transfers or redemptions there. Oracle, +health, timelock, burn and fee requirements still apply. A large price move +can still pause new mints; there is no promised wall-clock recovery time. + +`getprotectionstatus.volatility` reports the selected next-block rule, quote, +reference readiness and restriction reason. Missing history differs from a +complete reference window with no samples. See +[src/consensus/volatility.cpp](src/consensus/volatility.cpp), +[src/validation.cpp](src/validation.cpp) and +[src/digidollar/validation.cpp](src/digidollar/validation.cpp). ### 7.2 System Health Monitoring @@ -686,130 +744,36 @@ class VolatilityMonitor { **Health Metrics Tracked:** - Total DGB locked across all positions -- Total DD supply in circulation -- Overall system collateral ratio +- Circulating DD and separately reported open-vault principal +- System health with its selected denominator and rule height - Per-tier health ratios - Protection system status (DCA/ERR/Volatility) -### 7.3 Network-Wide Tracking System - -#### **CRITICAL FEATURE: Blockchain-Wide UTXO Scanning** -**Status: ✅ FULLY IMPLEMENTED AND TESTED** (`/src/digidollar/health.cpp:307`) - -This is a **major implementation** that was completely missing from the architecture document. - -**What It Does:** -DigiDollar implements true network-wide tracking by scanning the **entire blockchain UTXO set**, not just individual wallets. This means: -- Every node sees **identical** total DD supply and collateral -- System health is calculated **network-wide**, not per-wallet -- New nodes immediately see full network state -- No wallet needs to be loaded to see system statistics - -**Implementation Details:** - -```cpp -void SystemHealthMonitor::ScanUTXOSet(CCoinsView* view, - CCoinsView* validation_view, - const node::BlockManager* blockman, - const CTxMemPool* mempool, - const CChain* chain, - const Consensus::Params* consensus) -{ - // Create cursor to iterate ALL UTXOs (similar to gettxoutsetinfo) - std::unique_ptr pcursor(view->Cursor()); - - // Iterate through entire blockchain UTXO set - while (pcursor->Valid()) { - COutPoint key; - Coin coin; - - // Find DigiDollar vault outputs (P2TR with value > 0 at output 0) - if (key.n == 0 && coin.out.scriptPubKey[0] == OP_1 && coin.out.nValue > 0) { - - // v9.26.4: coins created below the DigiDollar activation floor can - // never be DD vaults — skip them (their blocks may be pruned away) - - // Fetch FULL transaction: txindex when available, otherwise read it - // straight from the retained block at the coin's height (works on - // pruned nodes, which keep every block at/above the DD floor) - const CBlockIndex* creating_block = chain ? (*chain)[coin.nHeight] : nullptr; - CTransactionRef tx = node::GetTransaction(creating_block, mempool, txid, - hashBlock, *blockman); - - // Validate DD mint structure: - // - Output 0: P2TR collateral vault (has DGB value) - // - Output 1: P2TR DD token (value = 0) - // - Output 2: OP_RETURN with exact DD metadata - - // Extract exact DD amount from OP_RETURN - if (DigiDollar::ExtractDDAmount(tx->vout[2].scriptPubKey, ddAmount)) { - s_currentMetrics.totalDDSupply += ddAmount; - s_currentMetrics.totalCollateral += collateral; - } - } - pcursor->Next(); - } -} -``` - -**Key Innovations:** - -1. **Full Transaction Access**: Unlike simple UTXO scans, this implementation fetches **full transaction data** from BlockManager to access OP_RETURN metadata - -2. **Exact Amount Extraction**: Reads precise DD amounts from OP_RETURN (output 2), not estimated from collateral - -3. **Network Consensus**: All nodes scan the same UTXO set → identical results everywhere - -4. **Performance**: Efficient streaming cursor, ~100ms for 1000 vaults, read-only - -**Integration Points:** - -1. **RPC Command**: `getdigidollarstats` calls `ScanUTXOSet()` with chainstate access (full re-scan; expensive). -2. **Incremental hooks**: `OnMintConnected`/`OnRedeemConnected` (`src/digidollar/health.h:222-248`) update `s_currentMetrics` in O(1) under `cs_main` whenever a DD tx is connected during normal block processing; the symmetric `OnMintDisconnected`/`OnRedeemDisconnected` reverse the update on reorg. -3. **Block-level index**: `DigiDollarStatsIndex` (`src/index/digidollarstatsindex.{h,cpp}`) persists per-height totals (`total_dd_supply`, `total_collateral`, `vault_count`) so historical lookups don't require a re-scan. -4. **Qt GUI**: Overview widget displays network totals via RPC. -5. **Protection Systems**: DCA/ERR read the cached `SystemMetrics` (commit `2de69c94d9`) so health comes from one canonical source. - -**Verification:** - -✅ **Functional Test**: `test/functional/digidollar_network_tracking.py` - PASSING -- Creates 2 nodes (Bob and Alice) -- Bob mints DD on node 0 -- **Verifies both nodes see identical network statistics** -- Proves UTXO scanning works across network - -✅ **Documented Proof**: `NETWORK_TRACKING_PROOF.md` -- Complete test output showing identical stats -- Technical implementation details -- Performance characteristics - -**Example Output:** -``` -Bob (node 0) sees: - Total DD Supply: 20043 cents ($200.43) - Total Collateral: 633.00000000 DGB - -Alice (node 1) sees: - Total DD Supply: 20043 cents ($200.43) ← IDENTICAL! - Total Collateral: 633.00000000 DGB ← IDENTICAL! -``` +### 7.3 Reconstruction and reporting -**Impact on Architecture:** +Legacy monitoring still uses `SystemHealthMonitor::ScanUTXOSet` and its +incremental hooks below Thaw Day. A scan of open vaults recovers original +principal; it cannot stand in for circulating tokens after extra burns. +Do not use a legacy monitor value as the post-Thaw Day consensus denominator. -This is a **critical differentiator** from account-based stablecoin systems. Rather than relying on contract state, DigiDollar tracks protocol state through: -- Native UTXO set integration -- Blockchain-wide visibility -- No reliance on external indexers or APIs -- Consensus-compatible read-only queries +The chainstate record described above supplies post-Thaw Day health inputs. +Reconstruction checks a consistent UTXO view and verified creating mint +transactions. A full transaction lookup is needed because a collateral output's +DGB value alone does not identify its original DD principal. A missing creating +block produces a readiness error. A scan alone does not prove that all +post-activation history has passed the new validation rules. -**Files Implementing This Feature:** -- `src/digidollar/health.h` - `ScanUTXOSet()` declaration with validation_view + BlockManager parameters -- `src/digidollar/health.cpp` - Full UTXO scanning implementation with tx data extraction -- `src/rpc/digidollar.cpp` - RPC integration with chainstate access -- `src/qt/digidollaroverviewwidget.cpp` - Qt GUI network statistics display -- `test/functional/digidollar_network_tracking.py` - Comprehensive verification test +The stats index stores circulating-supply aggregates. At and above Thaw Day, +RPC fallback reporting can reconstruct token amounts without replacing the +chainstate health record. A stats query cannot turn unavailable validation +state into a verified total. Compare nodes at the same block hash, and compare +principal with principal and circulation with circulation. -**Status**: ✅ **Production-Ready** - Fully implemented, tested, and documented +Relevant functions are `ReconstructChainstateHealth`, `GetCirculatingSupplyChange` +in [src/digidollar/health.cpp](src/digidollar/health.cpp), +`Chainstate::InitializeDigiDollarState` in [src/validation.cpp](src/validation.cpp), and the +reporting helpers in [src/rpc/digidollar.cpp](src/rpc/digidollar.cpp). +The [stats index](src/index/digidollarstatsindex.cpp) is optional for consensus. --- @@ -908,7 +872,7 @@ class DigiDollarTab : public QWidget { DigiDollar uses HD (Hierarchical Deterministic) key derivation from the wallet's seed for all DigiDollar operations. This enables wallet restore via descriptors. -**Implementation Location**: `src/wallet/wallet.cpp:2741-2836` (`CWallet::GetHDKeyForDigiDollar`). Callers: `src/rpc/digidollar.cpp:1439` (`mintdigidollar` RPC), `src/rpc/digidollar.cpp:2845` (`getdigidollaraddress` RPC), `src/qt/walletmodel.cpp:892` (Qt mint flow). +**Implementation Location**: `src/wallet/wallet.cpp:2769-2864` (`CWallet::GetHDKeyForDigiDollar`). Callers: `src/rpc/digidollar.cpp:1925` (`mintdigidollar` RPC), `src/rpc/digidollar.cpp:3540` (`getdigidollaraddress` RPC), `src/qt/walletmodel.cpp:924` (Qt mint flow). ### 8.5.2 GetHDKeyForDigiDollar() Function @@ -928,15 +892,15 @@ CKey CWallet::GetHDKeyForDigiDollar(const std::string& label) **Key Design Decisions:** - **Uses `GetSigningProviderWithKeys()`**: critical for Taproot — `GetSolvingProvider()` returns keys without private-key access, causing signing failures. -- **No random-key or legacy-wallet fallback in V1.** Earlier revisions of this section described a `MakeNewKey(true)` fallback for legacy wallets; current code rejects non-descriptor wallets before any legacy key extraction (`src/wallet/wallet.cpp:2757-2759`). On any failure path the helper returns an invalid `CKey` and the calling RPC throws `RPC_WALLET_ERROR "DigiDollar mint requires a descriptor/bech32m HD wallet with private keys enabled"` (`src/rpc/digidollar.cpp:1441`). The Qt mint flow surfaces the same error to the user (`src/qt/walletmodel.cpp:894-896`). DigiDollar mint therefore requires a descriptor/bech32m HD wallet with private keys; non-HD/legacy/private-keys-disabled wallets fail clearly without minting. -- **Database persistence**: keys stored via `StoreOwnerKey()` and `StoreAddressKey()` *before* the mint transaction is broadcast (commit `1e95478b7e`), so a crash between broadcast and persistence cannot lose the owner key. +- **Mint wallet capabilities**: `GetDigiDollarMintWalletError` checks descriptor/private-key flags, HD support, Taproot receiving and bech32 change descriptors before confirmation or key derivation. The later `GetHDKeyForDigiDollar` call must still produce a valid owner key; stored keys in a complex imported descriptor do not prove that it can. See `src/wallet/digidollarmintcapability.h`, `src/wallet/wallet.cpp`, `src/rpc/digidollar.cpp` and `src/qt/walletmodel.cpp`. +- **Owner-key storage and recovery**: Qt requests `StoreOwnerKey` before `CWallet::CommitTransaction`. A call sequence alone does not establish durable recovery. Persistence and restore behavior still require verification on the final integrated wallet build. Keep tested wallet backups; no crash-recovery guarantee is made for this development candidate. ### 8.5.3 Usage in DigiDollar Operations | Operation | Label | Called From | |-----------|-------|-------------| -| Mint DigiDollars | `"dd-owner"` | `mintdigidollar` RPC (`src/rpc/digidollar.cpp:1439`) and Qt mint (`src/qt/walletmodel.cpp:892`) | -| Generate DD Address | `"dd-address"` | `getdigidollaraddress` RPC (`src/rpc/digidollar.cpp:2845`) | +| Mint DigiDollars | `"dd-owner"` | `mintdigidollar` RPC (`src/rpc/digidollar.cpp:1925`) and Qt mint (`src/qt/walletmodel.cpp:924`) | +| Generate DD Address | `"dd-address"` | `getdigidollaraddress` RPC (`src/rpc/digidollar.cpp:3540`) | **Note**: `redeemdigidollar` RPC does not call `GetHDKeyForDigiDollar()` — it uses stored owner keys from the position database. The label `"dd-redeem"` is NOT used anywhere in the codebase; only `"dd-owner"` and `"dd-address"` are used. @@ -1516,7 +1480,7 @@ The V1 branch (`feature/digidollar-v1`) closed a series of consensus and policy | Area | Behavior | Reference | |------|----------|-----------| | OP_CHECKPRICE | Reserved and deterministically disabled; consumes one operand and pushes false. It does not read live oracle state or production mock state. | `b9ddae031e` / `3668971ab2` (`src/script/interpreter.cpp:708-735`) | -| MuSig2 V1 only | Coinbase oracle bundles in DigiDollar-active blocks must carry the V1 MuSig2 v0x03 format. Raw v0x01/v0x02 OP_RETURN payloads fail extraction (`src/oracle/bundle_manager.cpp:1065-1069`) and surface as `bad-oracle-malformed`; the `bad-oracle-legacy` branch is kept as defense-in-depth for bundles that parse but report a non-MuSig2 version. Mempool acceptance requires a recent valid MuSig2 quote. | `f2bb0a19a4`, `bbb85cf363` (`src/validation.cpp:185-283`) | +| MuSig2 V1 only | Coinbase oracle bundles in DigiDollar-active blocks must carry the V1 MuSig2 v0x03 format. Raw v0x01/v0x02 OP_RETURN payloads fail extraction (`src/oracle/bundle_manager.cpp:1017-1021`) and surface as `bad-oracle-malformed`; the `bad-oracle-legacy` branch is kept as defense-in-depth for bundles that parse but report a non-MuSig2 version. Mempool acceptance requires a recent valid MuSig2 quote. | `f2bb0a19a4`, `bbb85cf363` (`src/validation.cpp:268-491`) | | Mainnet/testnet validator parity | The mainnet oracle-validation short-circuit was removed; both networks honor the same V1 oracle-bundle gates. | `f0d9a7b2c7` | | DCA / health overflow | DCA and health math use signed `__int128` throughout to prevent collateral × price overflow. | `9cca6970ae` | | ERR burn math | Required burn = `ceil(originalDD * 10000 / ratioBps)` in `__int128`; `GetAdjustedRedemption` is a deprecated identity passthrough. | `55926c372a` | @@ -1525,10 +1489,10 @@ The V1 branch (`feature/digidollar-v1`) closed a series of consensus and policy | Pre-mutation volatility check | `WouldCandidateFreezeMinting()` runs before any mutation so a rejected mint cannot poison volatility history. | `03f4af47a7` | | Missing system health fail-closed | Mint validation requires deterministic system health and rejects with `bad-system-health` otherwise. | `8bbbfedf70` (`src/digidollar/validation.cpp:1581-1584`) | | Mempool oracle quote | DD mempool acceptance requires a recent valid MuSig2 oracle quote; without one, DD txs are rejected and reorg-resurrected DD txs are removed. | `81bf974f40` | -| Mining graceful degradation | DD txs failing validation are skipped during package selection and price-dependent DD txs are removed before retrying block validity when no valid oracle bundle is ready. | `6b5ff516c3` (`src/node/miner.cpp:523-538, 561-584, 849-864`) | +| Mining graceful degradation | DD txs failing validation are skipped during package selection and price-dependent DD txs are removed before retrying block validity when no valid oracle bundle is ready. | `6b5ff516c3` (`src/node/miner.cpp:234-401, 403-494, 614-635`) | | Lock tier canonical window | Custom/under-locked durations are rejected; mint OP_RETURN must record the canonical tier and the remaining lock blocks must be in `[tier_blocks, tier_blocks + 100]`. | `e1dd69f99b`, `11728a6980` (`src/digidollar/validation.cpp:1342-1397, 1603-1611`) | -| DD supply alert (not a cap) | `AlertThresholds::ALERT_DD_SUPPLY = 100M DD` is a monitoring threshold, not a hard cap; `MAX_DIGIDOLLAR` is a per-output serialization bound. | `99b1f79480` (`src/digidollar/health.h:83`) | -| Coinbase price-cache poisoning | Coinbase oracle-price extraction is gated on `DEPLOYMENT_DIGIDOLLAR`, and global `UpdatePriceCache` mutation is deferred until after block checks succeed, so a coinbase OP_ORACLE cannot poison the price cache pre-activation or from an invalid block. | rh61 (`src/validation.cpp:3077-3099, 3373-3380`) | +| DD supply alert (not a cap) | `AlertThresholds::ALERT_DD_SUPPLY = 100M DD` is a monitoring threshold, not a hard cap; `MAX_DIGIDOLLAR` is a per-output serialization bound. | `99b1f79480` (`src/digidollar/health.h:170`) | +| Coinbase price-cache poisoning | Coinbase oracle-price extraction is gated on `DEPLOYMENT_DIGIDOLLAR`, and global `UpdatePriceCache` mutation is deferred until after block checks succeed, so a coinbase OP_ORACLE cannot poison the price cache pre-activation or from an invalid block. | rh61 (`src/validation.cpp:3402-3410, 3742-3749`) | | Single source of system health | DCA/ERR consume `SystemHealthMonitor::GetCachedMetrics()`; stale callers fail closed in `ApplyDCA`. | `2de69c94d9` | ## 15. Known Open Items @@ -1715,12 +1679,12 @@ test/functional/digidollar_basic.py | Lock tier canonical window on mint | `bad-mint-lock-tier` for tier outside 0–9; `bad-mint-lock-tier-duration` if remaining lock blocks are outside `[canonical_blocks, canonical_blocks + 100]`; `bad-mint-lock-period` if not in `collateralRatios` | `src/digidollar/validation.cpp:1342-1397, 1603-1611` | | Oracle price required | `bad-oracle-price` rejection in mint validation when `ctx.oraclePriceMicroUSD <= 0` | `src/digidollar/validation.cpp:1147-1153` | | Pre-mutation volatility freeze | `WouldCandidateFreezeMinting` checked before `RecordPrice`; rejected mints can't poison history | `src/digidollar/validation.cpp:2688-2689, 2753` | -| Mainnet/testnet validator parity | Mainnet short-circuit removed; both networks honor identical V1 oracle-bundle gates | `src/validation.cpp:185-283` | +| Mainnet/testnet validator parity | Mainnet short-circuit removed; both networks honor identical V1 oracle-bundle gates | `src/validation.cpp:268-491` | | `OP_CHECKPRICE` reserved/disabled | Interpreter consumes one operand and pushes false; no live or mock price lookup | `src/script/interpreter.cpp:708-735` | -| DD amount in cents (100 = $1) | Stored amounts in cents; oracle prices in micro-USD; conversion `priceMillicents = priceMicroUSD / 10` for health math | `src/consensus/digidollar.h:70-73`, `src/consensus/dca.cpp:239-243` | +| DD amount in cents (100 = $1) | Stored amounts in cents; oracle prices in micro-USD; conversion `priceMillicents = priceMicroUSD / 10` for health math | `src/consensus/digidollar.h:85-88`, `src/consensus/dca.cpp:239-243` | | Transaction types fixed at 4 | `DD_TX_NONE=0, DD_TX_MINT=1, DD_TX_TRANSFER=2, DD_TX_REDEEM=3, DD_TX_MAX=4` | `src/primitives/transaction.h:38-44` | | Redemption paths fixed at 2 | `RedemptionPath { PATH_NORMAL=0, PATH_ERR=1 }` | `src/digidollar/digidollar.h:70-73` | -| DD supply alert (not cap) | `AlertThresholds::ALERT_DD_SUPPLY = 100M DD` is monitoring threshold; `MAX_DIGIDOLLAR` is per-output bound | `src/digidollar/health.h:83`, `src/digidollar/digidollar.h:19` | +| DD supply alert (not cap) | `AlertThresholds::ALERT_DD_SUPPLY = 100M DD` is monitoring threshold; `MAX_DIGIDOLLAR` is per-output bound | `src/digidollar/health.h:170`, `src/digidollar/digidollar.h:19` | --- diff --git a/DIGIDOLLAR_EXCHANGE_INTEGRATION.md b/DIGIDOLLAR_EXCHANGE_INTEGRATION.md index 29ea4027b96..a67fcc21080 100644 --- a/DIGIDOLLAR_EXCHANGE_INTEGRATION.md +++ b/DIGIDOLLAR_EXCHANGE_INTEGRATION.md @@ -25,14 +25,14 @@ If you already run a DigiByte node, you're most of the way there. | Fee unit | DGB/kB (DigiByte uses kB, not vB) | | Block time | 15 seconds (same as DGB) | | Confirmations | Same security model as DGB | -| Backend required | DigiByte Core v9.26.2+ with DigiDollar built in; features remain BIP9-gated until activation | +| Backend required | The reviewed release for the target network and coordinated activation height | | Wallet | Spend-capable descriptor wallet required for generated deposit addresses and withdrawals | --- ## 1. Node Setup -Upgrade your existing DigiByte node to v9.26.2+ and enable DigiDollar: +Use the reviewed build for your network. Follow the [backup and upgrade instructions](doc/digidollar-operations.md) before replacing an existing backend. DigiDollar configuration: ```ini # digibyte.conf @@ -52,7 +52,9 @@ rpcpassword=yourpassword That's it. Your existing DGB infrastructure stays the same — DD runs alongside it. -`txindex=1` is not optional for DD chains because validation and wallet recovery need creating-transaction metadata. Before activation, `getdigidollardeploymentinfo` remains available, but DD address, balance, history, send, mint, redeem, and running-oracle RPCs are gated. +`txindex=1` is not optional on an unpruned mainnet or testnet node, because validation and wallet recovery need creating-transaction metadata. Startup refuses to run without it. Before activation, `getdigidollardeploymentinfo` remains available, but DD address, balance, history, send, mint, redeem, and running-oracle RPCs are gated. + +**If you prune instead.** `-prune` and `-txindex` cannot both be set, so a pruned node is not asked for the index: it reads a spent DigiDollar output's creating transaction straight out of the retained block at that coin's height. What it must have is the block. So a pruned node keeps every block from the DigiDollar activation floor to the tip, which on mainnet is height 23,627,520, about 870,000 blocks and growing by about 5,760 a day. A small prune target such as `prune=550` therefore cannot be reached on mainnet: the node starts and removes everything below the floor, but it will not shrink past the blocks it has to keep. If a block above the floor is already missing, startup stops with "DigiDollar state not ready: retained block history is incomplete". See [node operations](doc/digidollar-operations.md). --- @@ -177,10 +179,10 @@ digibyte-cli getbalance ### Withdrawal Limits -- Per-output dust floor: $1 (100 cents) — see `src/consensus/digidollar.h:73` -- Maximum single transfer: **$100,000** (10,000,000 cents) per `maxMintAmount`-aligned policy in `src/consensus/digidollar.h:72` +- Per-output dust floor: $1 (100 cents) — see `src/consensus/digidollar.h:88` +- Maximum single transfer: **$100,000** (10,000,000 cents) per `maxMintAmount`-aligned policy in `src/consensus/digidollar.h:87` - DD inputs must be **confirmed** (≥1 confirmation) before they can be re-spent. The wallet does not chain unconfirmed DigiDollar UTXOs, and consensus rejects DD transfer/redeem inputs that resolve from `MEMPOOL_HEIGHT` (commit `0b4959f563`). Plan withdrawal cadence around the 15-second block time, or batch with `sendmanydigidollar`. -- Integration code should pass integer cents with no decimal point. The send/redeem RPCs accept decimal-dollar input for CLI compatibility, so `25000` means $250.00 but `25000.00` means $25,000.00. +- Integration code can keep passing integer cents without an `amount_unit`: `25000` means $250.00. A decimal amount without a unit is rejected. To send dollar amounts, explicitly set `amount_unit` to `dollars`; then `250.00` means $250.00. The send, batch-send, and redeem RPCs share this rule. Explicit `cents` values must be integers; `dollars` values allow at most two decimal places. - DD transfer withdrawals do not need a fresh oracle quote for mempool admission. Mint and redeem paths require recent valid MuSig2 oracle data; transfer-only exchange withdrawals are price-independent, but still require confirmed DD and DGB fee inputs. ### Batch withdrawals @@ -211,7 +213,7 @@ digibyte-cli getdigidollarbalance "" 0 # DGB balance (for fees) digibyte-cli getbalance -# BIP9 activation status (verifies DD is live) +# Buried activation status (verifies DD is live) digibyte-cli getdigidollardeploymentinfo ``` @@ -247,9 +249,28 @@ digibyte-cli getdigidollarstats ``` Returns system-wide metrics: -- **Total DD supply** — all DigiDollars in circulation -- **Total collateral** — all DGB locked as backing -- **System health ratio** — collateral value / DD supply (should be >100%) +- **`total_dd_supply`** — circulating-supply field in cents: accepted issuance minus actual burns, subject to the legacy fallback limit below. +- **`canonical_health.open_vault_principal`** — original DD attached to unspent vaults; use only when `canonical_health.ready` is true. +- **`canonical_health.collateral`** — DGB satoshis locked in those vaults, with `active_vaults` and the matching `block_hash`. +- **`selected_health_denominator` / `health_rule_height`** — the liability definition and height used for tip-rule health. +- **`next_block_health`** — separate candidate height, readiness, health denominator and quote for next-block construction. + +Before Thaw Day, the legacy fallback used when `digidollarstatsindex` is +disabled scans vault amounts. Do not treat that fallback's `total_dd_supply` +as a verified circulating token count. Use a synchronized stats index for +circulation reporting below the transition. At and above Thaw Day, the +fallback reconstructs tokens separately from vault principal. + +At and above Thaw Day, health divides collateral value by open-vault principal. +It keeps circulating tokens separate. Closing a 100-DD vault while burning +125 DD reduces principal by 100 and circulation by 125. Their difference is +expected and does not itself show corruption. This health change can prolong +emergency mint restrictions or increase required redemption burn. It does not +alter customer balances at activation. + +At tip H-1, tip health can still use legacy rules while the next candidate +uses the new rules. Unknown or unready values are not zero. A later quote or +chain change can change a construction estimate. This is useful for risk monitoring. It is not a customer-deposit index. For custody, use wallet-local `listdigidollartxs`, `getdigidollarbalance`, and `listdigidollarunspent`, or build a raw indexer using the parsing rules below. If system health drops significantly, new minting gets more expensive (Dynamic Collateral Adjustment / DCA) and the Emergency Redemption Ratio (ERR) may activate (`src/consensus/err.cpp`). @@ -357,6 +378,16 @@ Exchanges typically handle deposits and withdrawals — not minting or redeeming --- +### Address encoding in external systems + +DGB witness version 0 addresses use Bech32. Witness version 1 Taproot addresses +use Bech32m. Decode the output script and witness version before labeling its +address; a display label alone is not evidence of its encoding. Core enforces +this distinction in [src/key_io.cpp](src/key_io.cpp). DD/TD/RD addresses use a +separate Base58Check encoding of the Taproot key, as described above. Record +the network, transaction ID, output index and exact displayed address when +investigating an explorer disagreement. + ## 12. Common Pitfalls | Pitfall | Solution | @@ -364,7 +395,7 @@ Exchanges typically handle deposits and withdrawals — not minting or redeeming | Sending to a DGB address instead of DD | Use `validateddaddress`; prefix-only checks are not enough | | Running out of DGB for fees | Monitor DGB balance, auto-top-up from exchange reserves | | Filtering out 0-sat outputs as dust | DD token outputs are 0-sat by design — don't discard them | -| Using wrong amount units | Backend code should send integer cents; any decimal point is interpreted as dollars | +| Using wrong amount units | Keep integer cents, or explicitly set `amount_unit=dollars`; decimal amounts without a unit are rejected | | Not checking activation status | Call `getdigidollardeploymentinfo` — DD RPCs error before activation | | Crediting deposits without reorg checks | Track `blockhash`, confirmations, `in_mempool`, and `wallet_state`; reverse or hold credits on conflicts | | Ignoring system health | Monitor `getdigidollarstats` — ERR state affects the broader ecosystem | @@ -373,11 +404,16 @@ Exchanges typically handle deposits and withdrawals — not minting or redeeming ## 13. Test on Testnet Now! -The current public testnet in this source tree is **testnet26**. DigiDollar activation is BIP9-gated at/after block 600 once 140 of 200 blocks signal; verify status with `getdigidollardeploymentinfo`. +The public testnet configured in this source is **testnet26**, with buried +DigiDollar activation at 600. Thaw Day is scheduled at **432,100**, estimated +for September 18–19, 2026. The block height triggers the change. Check +`getdigidollardeploymentinfo` on the installed build and follow the +[upgrade window](DIGIDOLLAR_ACTIVATION_EXPLAINER.md). No public Thaw Day +activation or soak test is claimed. ### Testnet Quick Start -1. **Download** the latest DigiByte Core v9.26.2 release build from this branch +1. **Obtain** the reviewed test build and network instructions from the release owner 2. **Configure:** ```ini testnet=1 @@ -402,14 +438,25 @@ The current public testnet in this source tree is **testnet26**. DigiDollar acti | Testnet name | testnet26 | | P2P Port | 12033 | | DD Address Prefix | `TD` | -| Status | BIP9 bit 23, min activation height 600; check `getdigidollardeploymentinfo` for current status | +| Status | DigiDollar buried at 600; Thaw Day scheduled at 432,100 and reported separately by `getdigidollardeploymentinfo` | | Oracle | 35 active slots, 7 MuSig2 signatures required, 6 exchange sources | --- ## 14. Mainnet Activation -Activation parameters are branch/network-specific. On mainnet, DigiDollar is gated by BIP9 bit 23 with a start time of 2026-06-01, a 40,320-block signaling window, a 70% threshold (28,224 of 40,320), and a minimum activation height of 23,627,520 (`src/kernel/chainparams.cpp:177-180,307`). Always confirm the live state by calling `getdigidollardeploymentinfo` on the target release and network as the source of truth for status, window size, threshold, timeout, and minimum activation height. +DigiDollar's buried mainnet activation height is 23,869,440. Thaw Day is a +separate consensus transition scheduled at 24,490,000, estimated for +November 1, 2026. The block height triggers the change. +Use `getdigidollardeploymentinfo.thaw_day` to distinguish scheduled, active at +tip and active next block. There are no current DigiDollar BIP9 signaling +statistics in that RPC. + +Upgrade custody nodes before that height. Older nodes may +disagree under the new rules. Agree on how deposits and withdrawals will be +held if chain agreement becomes uncertain. Installation alone is not proof of +activation or readiness. See the [activation guide](DIGIDOLLAR_ACTIVATION_EXPLAINER.md) +and [node operations](doc/digidollar-operations.md). --- diff --git a/DIGIDOLLAR_EXPLAINER.md b/DIGIDOLLAR_EXPLAINER.md index 889f7549e5d..03e004a04e7 100644 --- a/DIGIDOLLAR_EXPLAINER.md +++ b/DIGIDOLLAR_EXPLAINER.md @@ -9,7 +9,7 @@ DigiDollar is a decentralized USD-denominated token design native to DigiByte's ### Key Points - **DGB becomes the strategic reserve asset** (21B max supply, ~1.94 per person on Earth at 8.1B population) - **Everything happens inside DigiByte Core wallet** — you never give up control of your private keys -- **Status (V1, `feature/digidollar-v1`)**: Testnet26 activates at height 600 after BIP9 signaling; mainnet activation gate is configured for height 23,627,520 (start time 2026-06-01) — see `DIGIDOLLAR_ARCHITECTURE.md` for details +- **Status (V1)**: DigiDollar is a buried deployment since v9.26.5 and is active on mainnet from height 23,869,440 and on testnet26 from height 600. There is no miner vote left to run — see `DIGIDOLLAR_ACTIVATION_EXPLAINER.md` for details --- @@ -162,7 +162,14 @@ Supply chain, gaming, and other wallet-native payment flows can be explored on t DigiDollar is built natively on a UTXO (Unspent Transaction Output) blockchain. All operations occur directly in DigiByte Core wallet — users maintain complete control of their private keys throughout the entire process. -**Implementation Status (V1, `feature/digidollar-v1`)**: Core transaction system, MAST collateral, DCA/ERR/Volatility protections, network-wide UTXO scanning, MuSig2 oracle bundles, Qt GUI, and RPC surface are feature-complete. The June 1, 2026 BIP9 start time has passed, but mainnet activation remains gated by the configured minimum height, threshold, continued testnet26 validation, and mainnet oracle-operator deployment. See `DIGIDOLLAR_ARCHITECTURE.md` for the complete code-to-spec mapping. +**Implementation status:** This is v9.26.6 development documentation. +DigiDollar's existing mainnet deployment is buried at height 23,869,440. +The separate Thaw Day transition is scheduled on mainnet at 24,490,000, +estimated for November 1, 2026, and on testnet26 at 432,100, estimated for +September 18–19, 2026. The heights trigger activation. Signet is unsupported +and remains unscheduled. Completed code does not establish release readiness +or a successful network activation. See the [architecture guide](DIGIDOLLAR_ARCHITECTURE.md) +and [activation guide](DIGIDOLLAR_ACTIVATION_EXPLAINER.md). ### Core Technologies @@ -189,7 +196,7 @@ Efficient script execution with Merkleized Alternative Script Trees. The collate Both paths **require the timelock to expire first** - there is no early redemption, no forced liquidation, and no exceptions. -**Implementation Note**: Partial redemption is rejected at consensus. `ValidateCollateralReleaseAmount` (`src/digidollar/validation.cpp:2299+`) requires the redeemer to burn at least `requiredDDBurn` (= `originalDDMinted` for healthy systems, or the ERR-adjusted amount when health < 100%) AND release the full locked collateral; otherwise the transaction is rejected with `bad-collateral-release-partial-burn`. Non-DD transactions cannot spend a registered collateral vault at all (`bad-collateral-spend-missing-dd-burn`). +**Implementation Note**: Partial redemption is rejected at consensus. `ValidateCollateralReleaseAmount` (`src/digidollar/validation.cpp:2462+`) requires the redeemer to burn at least `requiredDDBurn` (= `originalDDMinted` for healthy systems, or the ERR-adjusted amount when health < 100%) AND release the full locked collateral; otherwise the transaction is rejected with `bad-collateral-release-partial-burn`. Non-DD transactions cannot spend a registered collateral vault at all (`bad-collateral-spend-missing-dd-burn`). ### Key Features @@ -289,16 +296,25 @@ ERR activates automatically when system health drops below 100%. New minting is ### 4️⃣ Volatility Protection (Fourth Defense) -Automatic freezes during extreme market volatility: - -| Timeframe | Threshold | Action | -|-----------|-----------|--------| -| 1-hour | 10% | Warning logged | -| 1-hour | 20% | Freeze new minting | -| 24-hour | 30% | Freeze all DD operations | -| 7-day | 50% | Emergency mode | - -Cooldown period: 8640 blocks (~36 hours at 15s blocks) after volatility subsides. There is NO oracle override of volatility freeze - the system must wait for the full cooldown period. +The candidate block height selects the volatility rules. Before Thaw Day, +the legacy freezes remain in effect. At and above Thaw Day, new mints use a +price reference from ancestor blocks. Transfers and redemptions no longer use +the old volatility checks there, but still need to satisfy their other rules. +A large valid price move can still pause minting. There is no exact wall-clock +thaw time or oracle override. + +Thaw Day also changes health to use the original DD amounts attached to open +vaults. Circulating tokens are counted separately. If closing a 100-DD vault +burns 125 DD, circulating supply falls by 125 and open-vault principal falls +by 100. With the same remaining collateral, the larger principal denominator +can keep emergency restrictions active longer or require more DD to redeem. +It does not create tokens or change balances at activation. + +Mainnet Thaw Day is scheduled at 24,490,000, estimated for November 1, 2026. +Testnet26 uses height 432,100, estimated for September 18–19, 2026. Block heights +decide when the rules change. Signet is unsupported and remains unscheduled. +See the [activation guide](DIGIDOLLAR_ACTIVATION_EXPLAINER.md) +and [accounting explanation](DIGIDOLLAR_ARCHITECTURE.md#layer-2-dynamic-collateral-adjustment-dca). ### 5️⃣ Supply & Demand Dynamics (Natural Defense) @@ -394,15 +410,15 @@ digibyte-cli -rpcwallet=restored rescanblockchain | ERR semantics | 100% collateral, MORE DD burned | `src/consensus/err.cpp:100-149` (`__int128` ceiling math) | | Minting blocked during ERR | Yes; also blocked when oracle absent | `src/consensus/err.cpp:417-469` | | Both MAST paths require CLTV | Both leaves prefix-match ` OP_CLTV OP_DROP` | `src/digidollar/scripts.cpp:73-99` | -| Lock tiers | 10 tiers (1h, 30d, 90d, 180d, 1y, 2y, 3y, 5y, 7y, 10y) | `src/consensus/digidollar.h:57-68` | +| Lock tiers | 10 tiers (1h, 30d, 90d, 180d, 1y, 2y, 3y, 5y, 7y, 10y) | `src/consensus/digidollar.h:72-83` | | Custom durations rejected | Mint validation enforces canonical tier windows: `[tier_blocks, tier_blocks + 100]` | `src/digidollar/validation.cpp:1354-1384` (`bad-mint-lock-tier-duration`) | | DCA tiers | 1.00 / 1.25 / 1.50 / 2.00 (≥150 / 120-149 / 110-119 / <110) | `src/consensus/dca.cpp:53-59` (HEALTH_TIERS) and `src/consensus/digidollar.h:94-99` (dcaLevels) | | ERR ratios | 0.95 / 0.90 / 0.85 / 0.80 | `src/consensus/err.cpp:53-58` (ERR_TIERS) | | Oracle config | 35 active slots, 7 signatures required (mainnet/testnet); 4-of-7 regtest | `src/kernel/chainparams.cpp` (`nOracleTotalOracles`, `nOracleRequiredMessages`, `nOracleConsensusRequired`) | -| Cooldown period | 8640 blocks (~36h) | `src/consensus/volatility.h:74` (`COOLDOWN_BLOCKS`) | -| DD amount unit | Cents (100 = $1.00) | `src/consensus/digidollar.h:70-73`, `src/digidollar/digidollar.h` | +| Cooldown period | 8640 blocks (~36h) | `src/consensus/volatility.h:113` (`COOLDOWN_BLOCKS`) | +| DD amount unit | Cents (100 = $1.00) | `src/consensus/digidollar.h:85-88`, `src/digidollar/digidollar.h` | | Oracle price unit | Micro-USD (1,000,000 = $1.00) | `src/oracle/bundle_manager.*`, `src/script/interpreter.cpp` | -| DD supply alert | Monitoring only — no hard cap | `src/digidollar/health.h:83` (`ALERT_DD_SUPPLY`) | +| DD supply alert | Monitoring only — no hard cap | `src/digidollar/health.h:170` (`ALERT_DD_SUPPLY`) | The full code-to-spec verification table lives in `DIGIDOLLAR_ARCHITECTURE.md` Section 18. @@ -415,16 +431,16 @@ The V1 branch closes the consensus and policy gaps that the previous draft of th | Subsystem | Source | Status | |-----------|--------|--------| | OP_CHECKPRICE reserved opcode | `src/script/interpreter.cpp:436-746` | `OP_CHECKPRICE` is reserved and deterministically disabled; it does not read `g_get_oracle_consensus_price` | -| MuSig2-only oracle bundles | `src/validation.cpp:185-283` | Pre-V1 (legacy) bundles rejected; mempool requires recent valid MuSig2 quote | +| MuSig2-only oracle bundles | `src/validation.cpp:268-491` | Pre-V1 (legacy) bundles rejected; mempool requires recent valid MuSig2 quote | | Mainnet/testnet validator parity | `src/validation.cpp` | Mainnet short-circuit removed (commit `f0d9a7b2c7`) | | DCA/ERR integer math | `src/consensus/dca.cpp`, `src/consensus/err.cpp` | `__int128` ceiling arithmetic; `ApplyDCA` fails closed on stale health | | Confirmed-only DD chaining | `src/digidollar/validation.cpp:1810, 1954` | `MEMPOOL_HEIGHT` DD inputs rejected (commit `0b4959f563`) | -| Mining graceful degradation | `src/node/miner.cpp:857-859` | Failing DD txs (`ValidateDDForBlockInclusion`) are added to `failedTx` and skipped; assembler continues | -| DD supply alert (not a cap) | `src/digidollar/health.h:83` | Monitoring threshold only | +| Mining graceful degradation | `src/node/miner.cpp:898-901` | Failing DD txs (`ValidateDDForBlockInclusion`) are added to `failedTx` and skipped; assembler continues | +| DD supply alert (not a cap) | `src/digidollar/health.h:170` | Monitoring threshold only | **Where this leaves operators**: -- **Regtest / testnet**: Fully exercisable today; testnet26 is configured with `min_activation_height = 600`. -- **Mainnet**: Configuration is in place (BIP9 bit 23, start time 2026-06-01, timeout 2027-06-01, `min_activation_height = 23627520`). Outstanding work is operational — mainnet oracle operator deployment and continued testnet validation. +- **Regtest / testnet**: Fully exercisable today; testnet26 buries DigiDollar at height 600, regtest at height 0. +- **Mainnet**: DigiDollar is buried and active from height 23,869,440. The static DigiDollar and oracle height gates stay at the historical 23,627,520 floor, which is what the pruning floor and the pre-floor coin gate use. Outstanding work is operational: mainnet oracle operator deployment and continued testnet validation. --- diff --git a/DIGIDOLLAR_ORACLE_ARCHITECTURE.md b/DIGIDOLLAR_ORACLE_ARCHITECTURE.md index ca13f7dca63..d9aa24fd6f4 100644 --- a/DIGIDOLLAR_ORACLE_ARCHITECTURE.md +++ b/DIGIDOLLAR_ORACLE_ARCHITECTURE.md @@ -16,30 +16,15 @@ ## Table of Contents -### Part I: Executive Summary & Overview -1. [Executive Summary](#1-executive-summary) -2. [Quick Start Guide](#2-quick-start-guide) -3. [System Architecture Overview](#3-system-architecture-overview) - -### Part II: Core Components (Deep Dive) -4. [Data Structures & Serialization](#4-data-structures--serialization) -5. [Block Validation & Consensus Rules](#5-block-validation--consensus-rules) -6. [P2P Networking & Message Broadcasting](#6-p2p-networking--message-broadcasting) -7. [Exchange API Integration](#7-exchange-api-integration) - -### Part III: Testing & Quality Assurance -8. [Test Suite Documentation](#8-test-suite-documentation) -9. [Validation Flows](#9-validation-flows) - -### Part IV: Operational Guide -10. [Configuration & Deployment](#10-configuration--deployment) -11. [Monitoring & Troubleshooting](#11-monitoring--troubleshooting) -12. [Performance & Security](#12-performance--security) - -### Part V: Reference -13. [API Reference](#13-api-reference) -14. [Phase Two Roadmap](#14-phase-two-roadmap) -15. [Glossary](#15-glossary) +- [Executive Summary](#1-executive-summary) +- [Quick Start Guide](#2-quick-start-guide) +- [System Architecture Overview](#3-system-architecture-overview) +- [Data Structures & Serialization](#4-data-structures--serialization) +- [Block Validation & Consensus Rules](#5-block-validation--consensus-rules) +- [Current Oracle Configuration](#14-current-oracle-configuration) + +For deployment and diagnostics, see the [oracle setup guide](DIGIDOLLAR_ORACLE_SETUP.md) +and [node operations](doc/digidollar-operations.md). --- @@ -76,13 +61,13 @@ V1 ships with: **V1 surface — code as shipped on `feature/digidollar-v1`:** - ✅ OP_ORACLE opcode (0xbf) wired through script flag `SCRIPT_VERIFY_DIGIDOLLAR` -- ✅ MuSig2 v0x03 on-chain format only — `OracleBundleManager::CreateOracleScript` produces v0x03 (`src/oracle/bundle_manager.cpp:889-925`); `ExtractOracleBundle` rejects v0x01/v0x02 (`src/oracle/bundle_manager.cpp:1065-1068`) +- ✅ MuSig2 v0x03 on-chain format only — `OracleBundleManager::CreateOracleScript` produces v0x03 (`src/oracle/bundle_manager.cpp:841-893`); `ExtractOracleBundle` rejects v0x01/v0x02 (`src/oracle/bundle_manager.cpp:1017-1021`) - ✅ 35 active slots (35 active oracle slots) with a 7-signature mainnet/testnet quorum -- ✅ Single validator path on mainnet and testnet (`OracleDataValidator::ValidateBlockOracleData`, `src/oracle/bundle_manager.cpp:2151`) — the prior mainnet short-circuit is gone +- ✅ Single validator path on mainnet and testnet (`OracleDataValidator::ValidateBlockOracleData`, `src/oracle/bundle_manager.cpp:2187`) — the prior mainnet short-circuit is gone - ✅ P2P message surface: `oracleprice`, `oraclebundle` (received-and-dropped), `oracleconsns`, `oracleattest`, `oramusnonce`, `oramusigctx`, `oramusigpsig`, `oraclehb`, `getoracles` (`src/protocol.cpp:53-62`, handlers in `src/net_processing.cpp` 5440–6340). All oracle P2P handlers, including `oraclehb`, share the `IsOracleP2PActive` gate. - ✅ Six initialized exchange fetchers (`src/oracle/exchange.cpp:1092-1097`) -- ✅ Block-validated price cache, gated by the buried `DEPLOYMENT_DIGIDOLLAR` deployment (BIP90 since v9.26.5) (`src/validation.cpp:3064-3094, 3365-3372`) -- ✅ BIP-340 Schnorr verification at every relay hop, with bound-from-chainparams pubkey replacement before verification (`src/net_processing.cpp:5462-5491`) so an attacker cannot ship their own pubkey alongside a forged signature +- ✅ Block-validated price cache, gated by the buried `DEPLOYMENT_DIGIDOLLAR` deployment (BIP90 since v9.26.5) (`src/validation.cpp:3402-3410, 3742-3749`) +- ✅ BIP-340 Schnorr verification at every relay hop, with bound-from-chainparams pubkey replacement before verification (`src/net_processing.cpp:5555-5585`) so an attacker cannot ship their own pubkey alongside a forged signature **Removed / never-shipped:** - `sendoracleprice` RPC — removed for fake-price-injection (commit history); replaced by signed P2P attestations sourced from the local exchange aggregator @@ -112,7 +97,7 @@ V1 ships with: **If you're running a node:** - Your node validates the MuSig2 aggregate signature in every DD mint/redeem block once `DEPLOYMENT_DIGIDOLLAR` is active (buried height since v9.26.5) and `nHeight >= nOracleActivationHeight`; DD transfer-only and ordinary DGB blocks can omit a coinbase oracle bundle - No setup needed — validation happens automatically; the trust anchor is `consensus.vOraclePublicKeys` in chainparams -- Enable `-debug=digidollar` to see oracle activity +- Enable `-debug=digidollar` temporarily for routine oracle diagnostics. Useful errors and startup/recovery progress remain visible without it. See [node operations](doc/digidollar-operations.md) for log retention and compact filter service. ### 2.2 For Developers: Integration Points @@ -521,7 +506,7 @@ $10.00 → 10,000,000 → 0x8096980000000000 $100.00 → 100,000,000 → 0x00E1F50500000000 ``` -**Validation Constraints** (`IsValid()` implementation at oracle.cpp:35-36): +**Validation Constraints** (`IsValid()` implementation at `src/primitives/oracle.cpp:30-49`): ```cpp static constexpr uint64_t MIN_PRICE_MICRO_USD = 100; // $0.0001 per DGB (minimum) static constexpr uint64_t MAX_PRICE_MICRO_USD = 100000000; // $100.00 per DGB (maximum) @@ -539,7 +524,7 @@ if (price_micro_usd > MAX_PRICE_MICRO_USD) return false; #### 4.1.3 XOnlyPubKey (BIP-340 Schnorr) -**Implementation**: `/home/jared/Code/digibyte/src/pubkey.h` (lines 230-300) +**Implementation**: `src/pubkey.h` (lines 230-300) ```cpp class XOnlyPubKey @@ -704,7 +689,7 @@ bool COraclePriceMessage::IsValid(int64_t reference_time) const ### 4.2 COracleBundle - Complete Specification -**Location**: `/home/jared/Code/digibyte/src/primitives/oracle.h` (lines 113-168) +**Location**: `src/primitives/oracle.h` (lines 113-168) #### 4.2.1 Bundle Structure @@ -834,7 +819,7 @@ Epoch 15: Blocks 600 - 639 (testnet DD/oracle activation height) ### 4.3 Historical Compact Format Encoding (Not V1 Acceptance) -**Location**: `/home/jared/Code/digibyte/src/oracle/bundle_manager.cpp` (lines 896-1048) +**Location**: `src/oracle/bundle_manager.cpp` (`AddOracleBundleToBlock` at lines 712-839) This subsection is retained as historical context for the old v0x01 compact layout. Current V1 block acceptance is MuSig2-only v0x03: @@ -901,7 +886,7 @@ PHASE 2: P2P VALIDATION (All Nodes - net_processing.cpp) └───────────────────────────────────────────────────────────────┘ │ ▼ -PHASE 3: BLOCK INCLUSION (Miner - bundle_manager.cpp:896-1048) +PHASE 3: BLOCK INCLUSION (Miner - bundle_manager.cpp:712-839) ════════════════════════════════════════════════════════════════════ ┌───────────────────────────────────────────────────────────────┐ │ Miner calls CreateOracleScript() │ @@ -957,7 +942,7 @@ PHASE 4: BLOCK VALIDATION (All Nodes - V1 validator path) │ │ found = true; │ │ │ └───────────────────────────────────────────────┘ │ │ │ -│ 4. Extract Compact Data (bundle_manager.cpp:1050-1200): │ +│ 4. Extract Compact Data (bundle_manager.cpp:895-1033): │ │ ┌───────────────────────────────────────────────┐ │ │ │ Parse 22-byte scriptPubKey: │ │ │ │ - Byte 3: version (must be 0x01) │ │ @@ -1413,7 +1398,7 @@ Chainstate::ConnectBlock() Validation Sequence: ### 5.2 ValidateBlockOracleData() — V1 Flow -**Location:** `src/oracle/bundle_manager.cpp:2151` (`OracleDataValidator::ValidateBlockOracleData`). +**Location:** `src/oracle/bundle_manager.cpp:2187` (`OracleDataValidator::ValidateBlockOracleData`). The validator runs identically on mainnet, testnet, and regtest. The pseudocode below is updated to match V1 — the prior "Phase One" / "Phase Two" multi-branch description and the mainnet short-circuit have been removed from the code (commits `f0d9a7b2c7`, `bbb85cf363`, `fa29405adc`, `f2bb0a19a4`). @@ -1736,27 +1721,41 @@ bool Chainstate::ConnectBlock(const CBlock& block, BlockValidationState& state, **Price Cache Implementation**: ```cpp -// src/oracle/bundle_manager.cpp:2067-2095 -void OracleBundleManager::UpdatePriceCache(int height, uint64_t price_micro_usd) +// src/oracle/bundle_manager.cpp:2115-2145 +void OracleBundleManager::UpdatePriceCache(int height, uint64_t price_micro_usd, int64_t source_time) { - std::lock_guard lock(mtx_price_cache); - height_to_price[height] = price_micro_usd; - - // Keep cache size limited (last 1000 blocks) - if (height_to_price.size() > 1000) { - height_to_price.erase(height_to_price.begin()); + const int64_t effective_update_time = source_time > 0 ? source_time : GetTime(); + + // Update the height-to-price map + { + std::lock_guard lock(mtx_price_cache); + height_to_price[height] = price_micro_usd; + height_to_price_time[height] = effective_update_time; + + // Keep cache size limited (last 1000 blocks) + if (height_to_price.size() > 1000) { + const int erase_height = height_to_price.begin()->first; + height_to_price.erase(height_to_price.begin()); + height_to_price_time.erase(erase_height); + } } - LogPrint(BCLog::DIGIDOLLAR, "Oracle: Price cache updated for height %d: %llu micro-USD\n", - height, price_micro_usd); + // Also update cached_price so GetLatestPrice() returns the block price. + { + std::lock_guard lock(mtx_bundles); + cached_price = static_cast(price_micro_usd); + last_update_time = effective_update_time; + } + // ... logging } ``` **Cache Properties**: -- **Thread-safe**: Protected by `mtx_price_cache` mutex +- **Thread-safe**: `height_to_price` and `height_to_price_time` under `mtx_price_cache`; `cached_price` and `last_update_time` under `mtx_bundles` - **Maximum size**: 1000 entries (last 1000 blocks) -- **Eviction**: FIFO (oldest entries removed first) -- **Data structure**: `std::map height_to_price` +- **Eviction**: FIFO (oldest entries removed first, from both maps) +- **Data structures**: `std::map height_to_price` and `std::map height_to_price_time` +- **Time source**: the block's own oracle timestamp when it has one, otherwise the wall clock ### 5.5 DisconnectBlock() Integration @@ -1804,21 +1803,40 @@ bool Chainstate::DisconnectBlock(const CBlock& block, const CBlockIndex* pindex, **Cache Removal Implementation**: ```cpp -// src/oracle/bundle_manager.cpp:2110-2133 +// src/oracle/bundle_manager.cpp:2158-2181 void OracleBundleManager::RemovePriceCache(int height) { - std::lock_guard lock(mtx_price_cache); + // Both locks are needed: height_to_price is under one, cached_price under + // the other, and GetLatestPrice() reads cached_price. Lock order is + // mtx_bundles before mtx_price_cache. + std::lock_guard bundles_lock(mtx_bundles); + std::lock_guard price_lock(mtx_price_cache); auto it = height_to_price.find(height); if (it != height_to_price.end()) { height_to_price.erase(it); - LogPrint(BCLog::DIGIDOLLAR, "Oracle: Removed price cache for height %d\n", height); + height_to_price_time.erase(height); + // Fall back to the highest remaining height's price, or nothing. + if (!height_to_price.empty()) { + const int restored_height = height_to_price.rbegin()->first; + cached_price = height_to_price.rbegin()->second; + auto time_it = height_to_price_time.find(restored_height); + last_update_time = time_it != height_to_price_time.end() ? time_it->second : 0; + } else { + cached_price = 0; + last_update_time = 0; + } + // ... logging } } ``` +Removing a height does not leave `GetLatestPrice()` reporting a price from a +block that was disconnected. It steps back to the highest height still in the +cache, and reports nothing when the cache is empty. + --- -## 14. Phase Two Roadmap - Multi-Oracle Consensus +## 14. Current Oracle Configuration ### 14.1 V1 Activation & Quorum Reality (replaces the old Phase Two roadmap) @@ -1849,7 +1867,7 @@ consensus.nOracleTotalOracles = 35; consensus.nOraclePubkeyCount = 35; consensus.nOracleConsensusRequired = 7; -// Regtest override (chainparams.cpp:1112-1119): +// Regtest override (chainparams.cpp:1266-1300): consensus.nDDActivationHeight = 650; consensus.nOracleActivationHeight = 650; consensus.nDigiDollarMuSig2Height = 0; // min(650, buried DigiDollarHeight=0) — v9.26.5 burial @@ -1867,58 +1885,44 @@ consensus.nOracleConsensusRequired = 4; There is no longer a "mainnet validation bypass" — mainnet runs the same validator and the same MuSig2 verification path as testnet and regtest. -### 14.3 Mainnet/Testnet Oracle Keys (35 Active - slot order 0-34) - -**Location:** `src/kernel/chainparams.cpp` mainnet/testnet `consensus.vOraclePublicKeys.push_back(...)` blocks. The mainnet and testnet rosters share the same active operators/placeholders and slot order for slots 0-34. - -| Slot | Operator | -|------|----------| -| 0 | Jared | -| 1 | Green Candle | -| 2 | Bastian | -| 3 | DanGB | -| 4 | Shenger | -| 5 | Ycagel | -| 6 | Aussie | -| 7 | LookInto | -| 8 | JohnnyLawDGB | -| 9 | Ogilvie | -| 10 | ChopperBrian | -| 11 | hallvardo (RC31 rotated key) | -| 12 | DaPunzy (RC31 rotated key) | -| 13 | DigiByteForce (RC31 rotated key) | -| 14 | Neel | -| 15 | DigiSwarm | -| 16 | GTO90 | -| 17 | digibyte-maxi | -| 18 | Anthony | -| 19 | mbah_jambon | -| 20 | Camden | - -Mainnet and testnet26 `vOracleNodes` slots 21-34 are active consensus -metadata and are aligned with `consensus.vOraclePublicKeys`. Slot 28 uses the -DigiHash Mining Pool key. Slot 31 is assigned to Peer2Peer / DigiRoos but -remains a placeholder until a valid compressed secp256k1 oracle key is supplied; -slots 32-34 use the submitted 3DogsKanab, LiberatedLark, and Manu_DGB_oracle -keys. The local mini-testnet mode still keeps a 24-key local-only harness -because only slots 0-23 have deterministic local private keys there. +### 14.3 Network-specific oracle roster + +The roster is in [src/kernel/chainparams.cpp](src/kernel/chainparams.cpp). +Mainnet and testnet each configure 35 active slots, IDs 0–34, but their names +and keys are network-specific. `getoracles` uses the selected roster's +`OracleNodeInfo.display_name`. + +| Slot | Mainnet display name | Testnet display name | +|------|----------------------|----------------------| +| 0 | DigiByte.Io Oracle | Jared | +| 11 | Crypto Corner Shop | hallvardo | + +Slot 28 uses the DigiHash Mining Pool key and slot 31 uses the Peer2Peer / +DigiRoos key. Slot 31 is not an empty placeholder in this source. All configured +mainnet/testnet slots have compressed public keys; use the selected network's +actual roster when checking an operator. + +Display names are local metadata. They are excluded from `OracleNodeInfo` +serialization and do not change IDs, keys, slot order, quorum or signatures. +See [src/primitives/oracle.h](src/primitives/oracle.h) and the name lookup in +[src/rpc/digidollar.cpp](src/rpc/digidollar.cpp). ### 14.4 V1 Validator Helpers (`src/oracle/bundle_manager.cpp`) ```cpp // Live MuSig2 validator (single path for all networks). -// src/oracle/bundle_manager.cpp:ValidateMuSig2Bundle (definition at line 2430) +// src/oracle/bundle_manager.cpp:ValidateMuSig2Bundle (definition at line 2468) bool OracleBundleManager::ValidateMuSig2Bundle( const COracleBundle& bundle, int32_t block_height, const Consensus::Params& params, std::string& error); // Off-chain consensus price (deterministic, time-independent). -// src/oracle/bundle_manager.cpp:CalculateConsensusPrice (definition at line 2553) +// src/oracle/bundle_manager.cpp:CalculateConsensusPrice (definition at line 2593) CAmount OracleBundleManager::CalculateConsensusPrice( const COracleBundle& bundle, const Consensus::Params& params); // Required quorum for a v0x03 bundle (always nOracleConsensusRequired). -// src/oracle/bundle_manager.cpp:GetRequiredConsensus (definition at line 2371) +// src/oracle/bundle_manager.cpp:GetRequiredConsensus (definition at line 2409) int OracleBundleManager::GetRequiredConsensus( int block_height, const Consensus::Params& params); ``` @@ -1949,7 +1953,7 @@ There is no longer a separate "activate Phase Two on testnet" step — testnet/r **Verified against code:** - Price format: micro-USD (`1,000,000 = $1.00 USD`), constants `ORACLE_MIN_PRICE_MICRO_USD=100`, `ORACLE_MAX_PRICE_MICRO_USD=100000000` (`src/primitives/oracle.h:23-24`) - v0x03 on-chain payload: `version + bitmap_len + bitmap + epoch + price + timestamp + 64-byte aggregate sig` (`COracleBundle::SerializeV03Data`, `OracleBundleManager::CreateOracleScript`) -- Validator: single code path for mainnet/testnet/regtest in `OracleDataValidator::ValidateBlockOracleData` (`src/oracle/bundle_manager.cpp:2151`) +- Validator: single code path for mainnet/testnet/regtest in `OracleDataValidator::ValidateBlockOracleData` in [src/oracle/bundle_manager.cpp](src/oracle/bundle_manager.cpp) - P2P handlers: 9 message types in `src/protocol.cpp:53-62`; price/consensus/MuSig2/getoracles handlers, including `oraclehb`, share the `IsOracleP2PActive` gate in `src/net_processing.cpp` ~5440–6340, and `oraclebundle` is accepted-and-dropped. - Historical BIP9 (deployment buried in v9.26.5 — mainnet activated at 23,869,440, testnet26 at 600, regtest buried height 0): bit 23, mainnet start `2026-06-01`, mainnet timeout `2027-06-01`, mainnet `min_activation_height=23627520`, mainnet window 40320 / threshold 28224 (70%); testnet26 start at genesis, `min_activation_height=600`, window 200, threshold 140 (70%); regtest `ALWAYS_ACTIVE` - `OP_CHECKPRICE` is reserved and deterministically disabled (`src/script/interpreter.cpp:708-735`); it consumes one operand and pushes false without reading oracle state @@ -1984,9 +1988,9 @@ These items appeared in earlier revisions of this document. They are recorded he | Earlier claim | V1 reality | |---------------|-----------| -| Mainnet validation returns true (bundle_manager.cpp:2229) | Removed (commit `f0d9a7b2c7`); validator runs identically on mainnet and testnet | +| Mainnet validation returns true early in `ValidateBlockOracleData` | Removed (commit `f0d9a7b2c7`); validator runs identically on mainnet and testnet | | v0x01 / v0x02 oracle bundle accepted on-chain | Rejected at extraction, surfaced by the validator as `bad-oracle-malformed`. The `bad-oracle-legacy` branch only fires when extraction returns true with a non-MuSig2 version and remains as defense-in-depth; commits `bbb85cf363`, `fa29405adc`, `f2bb0a19a4` | -| Empty `schnorr_sig` bypasses verification in P2P | Bound to chainparams pubkey then verified in `src/net_processing.cpp:5462-5491`; v0x03 on-chain bundle uses an aggregate signature that is always required | +| Empty `schnorr_sig` bypasses verification in P2P | Bound to chainparams pubkey then verified in `src/net_processing.cpp:5555-5585`; v0x03 on-chain bundle uses an aggregate signature that is always required | | Phase One single oracle on testnet/regtest | Replaced by 7-signature mainnet/testnet MuSig2 / 4-of-7 regtest MuSig2 | | `sendoracleprice` RPC | Removed | | Mock prices reachable from `OP_CHECKPRICE` | Removed; `OP_CHECKPRICE` is reserved and deterministically disabled, so neither mock nor live node-local prices are read | diff --git a/DIGIDOLLAR_ORACLE_EXPLAINER.md b/DIGIDOLLAR_ORACLE_EXPLAINER.md index ad353e14de3..427e57cb380 100644 --- a/DIGIDOLLAR_ORACLE_EXPLAINER.md +++ b/DIGIDOLLAR_ORACLE_EXPLAINER.md @@ -33,6 +33,10 @@ Mainnet and testnet slots 0-34 are in `consensus.vOraclePublicKeys` and Peer2Peer / DigiRoos key, and slots 32-34 use final submitted operator keys. Slot 35 is outside the configured roster. +Names are display metadata from each network's roster. Mainnet slot 0 is +`DigiByte.Io Oracle` and slot 11 is `Crypto Corner Shop`; their testnet names +are `Jared` and `hallvardo`. Names do not change operator IDs, keys or quorum. + ## Operator Flow 1. The oracle node fetches DGB/USD from the active exchange fetchers in @@ -143,17 +147,28 @@ disconnect, reorg, reindex, and IBD flows. ## Activation -DigiDollar and the oracle validator activate together: - -| Network | DigiDollar activation | Oracle activation | MuSig2 height | -|---------|-----------------------|-------------------|---------------| -| Mainnet | BIP9 bit 23, min height 23,627,520 | same trigger | 23,627,520 | -| Testnet26 | height 600 / BIP9 active | same trigger | 600 | -| Regtest | BIP9 `ALWAYS_ACTIVE`; DD/oracle P2P height gates 650 by default, or the direct `-digidollaractivationheight=N` override | same height trigger | 0 | - -Before activation, DD-looking data does not trigger V1 consensus rules. After -activation, DD mint/redeem blocks must satisfy the V1 oracle rules above; -DD transfer-only and ordinary DGB blocks may omit the coinbase oracle bundle. +DigiDollar uses buried activation heights. Block oracle validation follows +the candidate's DigiDollar activation status. The static oracle P2P height +also applies to network oracle messages. + +| Network | Buried DigiDollar height | Static oracle P2P height | MuSig2 height | +|---------|--------------------------|-------------------------|---------------| +| Mainnet | 23,869,440 | 23,627,520 | 23,627,520 | +| Testnet26 | 600 | 600 | 600 | +| Default regtest | 0 | 650 | 0 | + +On regtest, `-digidollaractivationheight=N` sets the buried DigiDollar and +static DD/oracle/MuSig2 heights together. Once DigiDollar is active, mint/redeem +blocks must satisfy the V1 oracle rules above; transfer-only and ordinary DGB +blocks may omit the coinbase oracle bundle. + +Thaw Day is a separate shared height for the new DigiDollar validation rules. +Mainnet is scheduled at 24,490,000, estimated for November 1, 2026. Testnet26 +is scheduled at 432,100, estimated for September 18–19, 2026. The heights trigger +activation. Signet is unsupported and remains unscheduled. Thaw Day does not +change the oracle signing quorum or the v0x03 bundle format. See +the [activation guide](DIGIDOLLAR_ACTIVATION_EXPLAINER.md) for tip and next-block +status and the limits of this development candidate. ## Regtest Mocking diff --git a/DIGIDOLLAR_ORACLE_SETUP.md b/DIGIDOLLAR_ORACLE_SETUP.md index f1c06d504f8..3e4a3d97790 100644 --- a/DIGIDOLLAR_ORACLE_SETUP.md +++ b/DIGIDOLLAR_ORACLE_SETUP.md @@ -1,6 +1,6 @@ # DigiDollar Oracle Setup Guide -*The single source of truth for oracle operator setup — multi-oracle MuSig2 (V1 activates alongside DigiDollar; `nDigiDollarMuSig2Height = nDDActivationHeight` on mainnet and testnet26. On default regtest `nDigiDollarMuSig2Height = std::min(nDDActivationHeight, min_activation_height) = 0`.)* +*The single source of truth for oracle operator setup — multi-oracle MuSig2 (V1 activates alongside DigiDollar; `nDigiDollarMuSig2Height = nDDActivationHeight` on mainnet and testnet26. On default regtest `nDigiDollarMuSig2Height = std::min(nDDActivationHeight, DigiDollarHeight) = 0`.)* --- @@ -67,8 +67,6 @@ txindex=1 server=1 listen=1 addnode=oracle1.digibyte.io -debug=digidollar -debug=net ``` > **`testnet=1` goes at the top** (not under any section). Everything else under `[test]`. @@ -90,6 +88,10 @@ algo=sha256d --- +For two nodes behind one router, compact filter service and temporary debug +logging, see [node operations](doc/digidollar-operations.md). Routine operation +does not need `debug=digidollar` or `debug=net`. + ## New Oracle Setup For first-time oracle operators. You need an assigned active oracle ID (slot **0–34**) from the maintainer; mainnet and testnet26 both use 35 consensus-active MuSig2 public keys with a 7-signature quorum. Slot ID 35 is outside the configured 35-slot roster. @@ -130,11 +132,19 @@ Your oracle key persists in your wallet across upgrades. You do **not** need to ### Current testnet26 restart / upgrade +Follow the [upgrade and recovery guide](doc/digidollar-operations.md). Keep +existing wallet backups and retained block/undo data. Mainnet Thaw Day is +scheduled at 24,490,000, estimated for November 1, 2026. Testnet26 is scheduled +at 432,100, estimated for September 18–19, 2026. The heights trigger activation. +Signet is unsupported and remains unscheduled. Public activation, soak testing +and release verification still need to be completed. + ```bash -# 1. Stop your node +# 1. Back up the loaded wallet, then stop cleanly +digibyte-cli -testnet -rpcwallet=oracle backupwallet "/secure/backup/oracle.dat" digibyte-cli -testnet stop -# 2. Replace binaries (download new release or rebuild from source) +# 2. Wait for shutdown, then install the reviewed replacement binaries # 3. Start your node digibyted -testnet -daemon @@ -151,7 +161,7 @@ digibyte-cli -testnet getoracles true **Qt wallet users:** Start Qt → **File → Open Wallet → oracle**. If the oracle does not come up automatically, open **Console** and run `startoracle `. -> **Auto-start behavior:** since RC25, unencrypted oracle wallets auto-start when the wallet loads (`CWallet::TryAutoStartOracles` in `src/wallet/wallet.cpp:4870`), and encrypted wallets auto-start after `walletpassphrase` unlock. Keep the manual `startoracle` command handy anyway, because it remains the safest fallback if the oracle is not already running. +> **Auto-start behavior:** since RC25, unencrypted oracle wallets auto-start when the wallet loads (`CWallet::TryAutoStartOracles` in `src/wallet/wallet.cpp:4904`), and encrypted wallets auto-start after `walletpassphrase` unlock. Keep the manual `startoracle` command handy anyway, because it remains the safest fallback if the oracle is not already running. ### Decommissioning retired testnets @@ -243,7 +253,9 @@ removed / paid API key required). | Active Oracles (`nOraclePubkeyCount`) | 35 | 7 | 35 | | Reserved slots (`nOracleTotalOracles`) | 35 | 7 | 35 | | Consensus Required (`nOracleConsensusRequired`) | 7 | 4-of-7 | 7 | -| Activation Height (`nDDActivationHeight`) | 600 | 650 | BIP9 (23,627,520) | +| Static DD height (`nDDActivationHeight`) | 600 | 650 | 23,627,520 | +| Buried DigiDollar height (`DigiDollarHeight`) | 600 | 0 | 23,869,440 | +| Thaw Day (`nDDThawDayHeight`) | 432,100 | Disabled unless configured | 24,490,000 | | Rotation Interval (`nDDOracleEpochBlocks`) | 40 blocks | 40 blocks | 40 blocks | | Price Update Interval (`nDDOracleUpdateInterval`) | 2 blocks | 1 block | 4 blocks | | Bundle/MuSig2 Epoch (`nOracleEpochLength`) | 40 blocks | 40 blocks | 40 blocks | @@ -363,7 +375,14 @@ List all oracles from chainparams with their status. ``` digibyte-cli -testnet getoracles [active_only] [blocks] ``` -Returns array with: `oracle_id`, `name`, `pubkey`, `endpoint`, `is_active`, `last_price_micro_usd`, `last_price_usd`, `last_update`, `price_source`, `status`, `selected_for_epoch`, `is_running_locally`. +Returns an array, one object per oracle slot, with: `oracle_id`, `name`, +`pubkey`, `endpoint`, `is_active`, `active_oracle_count`, `total_oracle_slots`, +`consensus_threshold`, `in_consensus`, `last_price_micro_usd`, `last_price_usd`, +`last_update`, `price_source`, `status`, `selected_for_epoch`, +`is_running_locally`, and the signed heartbeat fields `heartbeat_status`, +`software_version`, `client_version`, `p2p_protocol_version`, +`oracle_protocol_version`, `musig2_context_version`, `heartbeat_timestamp`, +`heartbeat_age_seconds` and `heartbeat_signature_valid`. #### `listoracle` Show the status of the oracle running on this local node (no parameters). @@ -384,6 +403,15 @@ digibyte-cli -testnet getalloracleprices ### DigiDollar RPCs +> **Amounts.** Every DigiDollar amount below is a whole number of cents: `10000` +> is $100.00. `senddigidollar`, `sendmanydigidollar`, `redeemdigidollar`, +> `getredemptioninfo`, `listdigidollarpositions` and `listdigidollaraddresses` +> also take an optional trailing `amount_unit` of `"cents"` or `"dollars"`. With +> `dollars` the amount may have up to two decimals, so `100.00` is $100.00. An +> amount written with a decimal point and no unit is refused, not guessed, +> because `10000.00` could mean $100.00 or $10,000.00. A single typed amount may +> not exceed 10,000,000 cents ($100,000.00). + #### `getoracleprice` Get the current consensus DGB/USD oracle price. @@ -486,7 +514,7 @@ digibyte-cli -testnet getdigidollarstats ``` #### `getdigidollardeploymentinfo` -Get DigiDollar BIP9 deployment status. +Get buried DigiDollar activation status and separate Thaw Day tip/next-block status. ``` digibyte-cli -testnet getdigidollardeploymentinfo diff --git a/DIGIDOLLAR_WALLET_INTEGRATION.md b/DIGIDOLLAR_WALLET_INTEGRATION.md index ced12dde5ee..3e33ffb57af 100644 --- a/DIGIDOLLAR_WALLET_INTEGRATION.md +++ b/DIGIDOLLAR_WALLET_INTEGRATION.md @@ -22,19 +22,19 @@ There are only 4 operations: **Mint**, **Transfer**, **Redeem**, and regular DGB | Fees | Always paid in **DGB** (not DD) | | Minimum fee | 0.1 DGB per DD transaction for transfer builders; mint and redeem builders enforce their own DGB fee floors in `src/digidollar/txbuilder.cpp` | | Signing | Schnorr (BIP-340) for DD inputs, ECDSA/Schnorr for DGB fee inputs | -| Wallet type | Descriptor/bech32m HD wallet required for DigiDollar V1 mint/address creation; legacy wallets are unsupported | +| Wallet type | Mint needs private keys, HD support, Taproot receiving and bech32 change descriptors; see wallet support below | | Confirmations | Same as DGB — 15-second blocks | -| Backend | Requires DigiByte Core v9.26.2+ with DigiDollar built in; features remain BIP9-gated until activation | +| Backend | Use the coordinated release for the target network; see the activation and upgrade guidance below | --- ## 1. Prerequisites Your wallet must: -- Run DigiByte Core v9.26.2 or later (with DigiDollar consensus rules) +- Use the reviewed release selected for the target network and its activation height - Set `digidollar=1` in `digibyte.conf` -- Set `txindex=1`; startup enforces this on mainnet/testnet DigiDollar chains and on regtest when DD testing is enabled -- Wait for BIP9 activation (DigiDollar features are disabled until activation; status is exposed via `getdigidollardeploymentinfo`) +- Set `txindex=1`; startup enforces this on unpruned mainnet/testnet DigiDollar chains and on regtest when DD testing is enabled. A pruned node is exempt, because `-prune` and `-txindex` cannot both be set; it reads the creating transaction out of the retained block instead, and keeps every block from the DigiDollar activation floor (mainnet 23,627,520) to the tip +- Check `getdigidollardeploymentinfo`: DigiDollar uses a buried activation height; Thaw Day has its own separately reported height - Use a descriptor/bech32m HD wallet with private keys enabled. DD mint requires deriving an HD owner key for the time-lock; encryption is supported, in which case the wallet must be unlocked with @@ -69,6 +69,15 @@ digibyte-cli getdigidollaraddress # Returns a Base58Check DD/TD/RD address for the current network ``` +**Validate a customer's DD address:** +```bash +digibyte-cli -rpcwallet="your-wallet" validateddaddress "" +``` + +Check `isvalid` before accepting the address. This wallet RPC checks the current +network and checksum. The ordinary `validateaddress` RPC handles DGB addresses; +use `validateddaddress` for DigiDollar. + **List DD addresses in wallet:** ```bash digibyte-cli listdigidollaraddresses @@ -179,6 +188,32 @@ digibyte-cli mintdigidollar 10000 3 } ``` +### When coins must be combined first + +If the wallet needs too many small DGB coins, `mintdigidollar` combines only +enough coins for the requested mint and its fees. It then returns a different +result: + +```json +{ + "status": "consolidation_pending", + "message": "Coins were merged. No DigiDollar was minted. Wait for confirmation, then retry the mint.", + "consolidation_txids": ["first-merge-transaction-id", "second-merge-transaction-id"] +} +``` + +This result is **not a mint**. Do not credit DigiDollars or record a new vault. +Wait for the listed transactions to confirm, then request the mint again. +There is no fixed confirmation time. Repeated requests, including after a +restart, report pending merges instead of paying for another set of merges. +The normal successful mint response above is unchanged. + +An `error` field can accompany the transaction IDs if a later merge fails. +Keep those IDs: earlier merges may already have been sent and paid fees. +A wallet-file error must be resolved before retrying. The wallet releases +failed local attempts through its existing transaction-abandonment path; it +does not discard an accepted merge simply because it leaves the mempool. + ### What Happens Under the Hood The mint transaction creates: @@ -193,11 +228,43 @@ The mint transaction creates: Sending DD is straightforward — it works like sending any UTXO. -For automated integrations, pass integer cents without a decimal point. `senddigidollar`, `sendmanydigidollar`, and `redeemdigidollar` also accept decimal-dollar input for CLI compatibility: `5000` means $50.00, but `5000.00` means $5,000.00. +### Amount units: cents by default, `amount_unit` to say otherwise + +DigiDollar amounts are integer **cents** inside the node. `senddigidollar`, +`sendmanydigidollar`, `redeemdigidollar` and `getredemptioninfo` take an +optional trailing `amount_unit` argument, `"cents"` or `"dollars"`, and follow +one contract (`src/digidollar/amount.cpp`): + +| Request | Result | +|---------|--------| +| `5000` or `"5000"`, no unit | 5,000 cents = $50.00 (unchanged for existing callers) | +| `"50.00"` or `50.5`, no unit | **rejected**, error `-8`: `ambiguous amount: pass amount_unit=cents or amount_unit=dollars ...`; nothing is sent | +| `"50.00"`, `amount_unit="dollars"` | 5,000 cents; at most two decimals (`"12.345"` is rejected) | +| `5000`, `amount_unit="cents"` | 5,000 cents; must be an integer (`"50.00"` with `cents` is rejected) | +| `"1e3"`, `"+5"`, `" 5"`, `"5,000"`, `-5` | rejected: only a plain decimal number (digits, optional `.digits`) is accepted | +| more than 10,000,000 cents ($100,000) | rejected at the RPC boundary, under either unit | + +Before v9.26.6 a decimal point silently meant dollars, so `5000.00` sent +$5,000.00 when the caller meant 5,000 cents. That guess is gone: pass integer +cents, or say `amount_unit="dollars"` explicitly. Named arguments work with +`digibyte-cli -named` and JSON-RPC named parameters. + +The `min_amount` filter of `listdigidollarpositions` and the `min_balance` +filter of `listdigidollaraddresses` read their amounts the same way and take +the same `amount_unit`. One other change there: a negative `min_amount` or +`min_balance` used to be accepted and then ignored, so the call returned +everything and the caller never learned its filter had been dropped. It is now +rejected like any other negative amount. ```bash digibyte-cli senddigidollar "DDrecipientAddress..." 5000 -# Sends $50.00 worth of DD +# Sends $50.00 worth of DD (integer cents, no unit needed) + +digibyte-cli -named senddigidollar address="DDrecipientAddress..." amount="50.00" amount_unit="dollars" +# Also $50.00; the unit makes the decimal unambiguous + +digibyte-cli senddigidollar "DDrecipientAddress..." 50.00 +# error code -8: ambiguous amount: pass amount_unit=cents or amount_unit=dollars ... ``` **With optional comment:** @@ -227,6 +294,44 @@ digibyte-cli senddigidollar "DDrecipientAddress..." 5000 "Payment for services" - Transfers are **confirmed-only**: a DD UTXO must have at least one confirmation before it can be spent in a subsequent transfer or redeem. Consensus refuses to resolve DD amounts from `MEMPOOL_HEIGHT` inputs for transfer/redeem, and the wallet no longer chains unconfirmed DigiDollar outputs (commit `0b4959f563`). Plan throughput around the 15-second block time, or batch with `sendmanydigidollar`. - Advanced wallet coin control can pass `selected_inputs` matching `listdigidollarunspent` rows. The deprecated `fee_rate` argument on send/redeem RPCs is ignored by the fixed DD fee policy. +### Which fee settings DigiDollar transactions actually use + +DigiDollar mint, send and redeem do **not** use the wallet's general fee +settings. None of `-mintxfee`, `-paytxfee`, `-fallbackfee`, `-minrelaytxfee` +or `estimatesmartfee` is read anywhere in `src/rpc/digidollar.cpp`, +`src/wallet/digidollarwallet.cpp`, `src/digidollar/txbuilder.cpp` or the Qt +DigiDollar widgets. Changing them cannot fix or cause a DigiDollar fee +failure. What the code does use: + +| Path | Fee rate | Where | +|------|----------|-------| +| `mintdigidollar` | 0.35 DGB/kB (`MIN_DD_FEE_RATE` = 35,000,000 sat/kB); the optional `fee_rate` argument is floored to it | `src/rpc/digidollar.cpp` (`MIN_DD_FEE_RATE`, the `fee_rate` floor) | +| `senddigidollar`, `sendmanydigidollar`, Qt Send $DD | 0.35 DGB/kB, fixed (`MIN_DD_TRANSFER_FEE_RATE`); the `fee_rate` argument is ignored | `src/wallet/digidollarwallet.cpp` (`TransferDigiDollarMany`, `PreflightDDTransferCapacity`) | +| `redeemdigidollar`, Qt Redeem (which calls the RPC) | 0.35 DGB/kB, fixed (`MIN_DD_FEE_RATE`); the `fee_rate` argument is ignored | `src/rpc/digidollar.cpp` (`redeemdigidollar`), `src/qt/walletmodel.cpp` (`executeRpc("redeemdigidollar")`) | +| Qt Mint | requests 500,000 sat/kB, but the builder's floor below applies | `src/qt/walletmodel.cpp` (`params.feeRate = 500000`) | + +On top of the rate, every DigiDollar transaction pays at least the absolute +floor `MIN_DD_TX_FEE` = 0.1 DGB (`src/digidollar/txbuilder.cpp`, applied in +the mint, transfer and redeem builders and in the redemption fee estimate). +The size the rate is applied to comes from `EstimateTransactionVSize` in the +same file, which counts 110 witness bytes per input and adds a 35% safety +margin, so a one-fee-input redemption is about 400 vB and pays about 0.14 DGB; +each additional DGB fee coin adds roughly 90 vB, about 0.03 DGB. + +The one DigiDollar-related transaction that *does* follow the wallet's +general fee settings is the automatic DGB consolidation sweep a mint performs +first when the collateral would need more inputs than one transaction may +carry: that is an ordinary DGB transaction built by `wallet::CreateTransaction` +(`src/rpc/digidollar.cpp` and `src/qt/walletmodel.cpp` mint paths), so +`-paytxfee`, fee estimation, `-fallbackfee` and `-mintxfee` apply to it as to +any DGB send. + +The 0.1 DGB DigiDollar floor happens to equal the wallet's default +`-mintxfee` of 0.1 DGB/kB, which is why the two were confused in reports of +failed redemptions; those failures came from the old fixed-size fee-coin +guess described under "Fee coins for a redemption" in section 9, not from any +fee setting. + ### Sending to many recipients in one transaction Use `sendmanydigidollar` to fan out DD to many addresses with a single fee: @@ -234,9 +339,12 @@ Use `sendmanydigidollar` to fan out DD to many addresses with a single fee: ```bash digibyte-cli -rpcwallet=hot sendmanydigidollar "" '{"DDaddr1...":1500,"DDaddr2...":2500}' # amounts in cents + +digibyte-cli -rpcwallet=hot -named sendmanydigidollar dummy="" amounts='{"DDaddr1...":"15.00","DDaddr2...":"25.00"}' amount_unit="dollars" +# one amount_unit applies to every recipient ``` -This is the DigiDollar analogue of `sendmany`; the first argument must be the compatibility dummy string `""`. Like `senddigidollar`, it requires confirmed DD inputs and pays the fee in DGB. Large batches are limited by standard OP_RETURN relay size because one amount is committed for every DD output plus possible change; split large withdrawal batches and handle the RPC's "Too many DigiDollar recipients" error. +This is the DigiDollar analogue of `sendmany`; the first argument must be the compatibility dummy string `""`. Every recipient amount follows the unit contract above, and the $100,000 cap applies per recipient. Like `senddigidollar`, it requires confirmed DD inputs and pays the fee in DGB. Large batches are limited by standard OP_RETURN relay size because one amount is committed for every DD output plus possible change; split large withdrawal batches and handle the RPC's "Too many DigiDollar recipients" error. --- @@ -329,7 +437,7 @@ command works with DigiDollar wallets: | `loadwallet "name"` | Positions, balances, DD owner keys, dd_transactions | After load, `listdigidollarpositions` and `getdigidollarbalance` reflect the same on-chain state. | | `digibyted` stop / start | Same as above | `postInitProcess` re-runs `ScanForDDUTXOs()` to validate vault UTXO state against the active chain. | | `rescanblockchain` | Idempotent — no double counting | Triggers a post-rescan call to `ScanForDDUTXOs()` -> `ValidatePositionStates()` so any vault that was redeemed off-wallet is correctly marked inactive. | -| `-reindex=1` | Same as restart | Wallet replays the chain; confirmed mints remain active until a real redeem/transfer spends the collateral on the active chain. | +| `-reindex=1` | Same as restart | The node rebuilds its indexes from the block files and the wallet rescans; confirmed mints remain active until a real redeem/transfer spends the collateral on the active chain. | | `backupwallet path` / `restorewallet new_name path` | Full DD state including owner keys | Restored wallet is loaded under `new_name`; existing wallet is untouched. | | `importdescriptors` into a fresh wallet + `rescanblockchain` | Reconstructs DD positions from on-chain OP_RETURN metadata after proving ownership of the zero-value DD P2TR output | If the imported descriptors can provide the Taproot spending key, the wallet recovers and indexes that key for redemption. | @@ -351,11 +459,65 @@ digibyte-cli -rpcwallet=restored listdigidollarpositions digibyte-cli -rpcwallet=restored getdigidollarbalance ``` -After step 4, the restored wallet may immediately `redeemdigidollar` any -position whose `unlock_height` has passed. There is no separate "DD owner -key import" step. The rescan reconstructs positions from mint metadata after -wallet ownership of the zero-value DD token output is proven, then recovers the -Taproot spending key from the imported descriptors when available. +After step 4, check the restored positions and whether the wallet has their +matching spending keys. A visible position and an expired timelock alone do +not establish that redemption can succeed. It also needs the required DD burn, +DGB fees and a valid quote. Rescan can recover the Taproot spending key only +when the imported private descriptors provide it. Keep the original backups; +final wallet recovery integration and restore verification remain pending for +this development candidate. + +### If redeeming reports a missing owner key + +The owner key of a mint comes from the wallet's own chain of keys. The wallet +also writes that key down against the position, so redeeming can find it +straight away. That row can go missing: a database write failed, a restore +skipped it, or the node stopped at the wrong moment. + +When it is missing, `redeemdigidollar` now searches the wallet's own keys for +the one that matches the DigiDollar token output of the mint. It includes the +unused addresses the wallet keeps ready beyond the last one it handed out. When +it finds the key it writes it down again and carries on with the redemption. It +never makes up a new key: only the key the collateral was locked with can +release it. The message you get tells you what is needed: + +| Error | Meaning | What to do | +|-------|---------|------------| +| `DigiDollar redemption requires the wallet to be unlocked ... walletpassphrase` | The wallet is encrypted and locked. | `walletpassphrase "" 600`, then redeem again. | +| `Private keys are disabled for this wallet` | This is a watch-only wallet. It can show positions but cannot sign. | Redeem from the wallet that holds the private keys. | +| `This wallet does not hold the mint transaction of this position` | The position record exists but the mint transaction was never scanned into this wallet. | `rescanblockchain `, then redeem again. | +| `No key in this wallet matches the owner of this DigiDollar vault` | None of this wallet's keys locked this collateral. A different wallet or a different seed minted it. | Restore the wallet that minted it **with its private keys** (the wallet file itself, or `importdescriptors` with the private descriptors from `listdescriptors true`), run `rescanblockchain` from a height before the first mint, then redeem from that wallet. | +| `... could not be written to the wallet database` | The key was found but the wallet file refused the write. | Check free disk space and the wallet file, then try again. | + +Restoring with public descriptors alone gives you a wallet that can watch but +cannot sign. Moving a wallet to a new machine needs the wallet file, or the +private descriptors, plus a rescan that starts before the first mint. + +### Mint attempts that can no longer confirm + +A mint names the exact height at which its collateral unlocks. A block can only +include that mint while the lock still left to run is at least the full length +of the tier the mint claims. That leaves about 100 blocks after the wallet built +it. Once the chain is past that, a mint that never confirmed and is in no +mempool can never be mined. + +The wallet now gives such an attempt up by itself. It marks the transaction +abandoned, so the DGB it spent can be spent again. It removes the coin locks on +the collateral and token outputs. `listdigidollarpositions false` then shows the +position as `expired_mint`, and `listdigidollartxs` shows `wallet_state` as +`expired_mint`. A mint the network refused when it was sent shows as +`abandoned_mint`. One pushed out by a conflicting transaction shows as +`conflicted_mint`. + +The owner key, the transaction and the position record are all kept. If a reorg +or a late block does confirm the mint after all, the position becomes active +again and its outputs are locked again as usual. A mint still sitting in the +mempool is never treated as expired. + +From v9.26.6 `mintdigidollar` writes the owner key and the position record +**before** it sends the transaction, and every wallet write reports whether it +worked. If a write fails, the mint is not sent and the RPC returns +`Mint not broadcast: ...`. ### Recovering from a lost wallet file @@ -378,13 +540,48 @@ positions remain visible to read-only RPCs while locked, but `listdigidollarpositions` reports them as not spendable/not redeemable until the wallet is unlocked. -### Legacy (BDB) wallets are unsupported for V1 - -DigiDollar V1 mint requires HD-derived owner keys persisted via the -descriptor wallet path. Loading or restoring a BDB legacy wallet that has -DD records is not supported and is not exercised by the V1 test suite. -Operators with legacy wallets must migrate to a descriptor wallet -(`migratewallet` or fresh export/import) before minting on mainnet. +### Wallet support for minting + +Minting requires a descriptor wallet with private keys and HD key support. +A descriptor describes the keys and script for a wallet's addresses. +It needs an active ranged Taproot (`bech32m`) receiving descriptor and an +active ranged SegWit (`bech32`) internal change descriptor, both with private +keys. A ranged descriptor can derive a sequence of wallet addresses. Merely +having `descriptors=true` in `getwalletinfo` does not prove those capabilities. + +| Wallet | Mint support | +|--------|--------------| +| Ordinary descriptor wallet with private keys | Supported if the required receiving/change descriptors are available | +| Encrypted supported wallet | Supported through the normal unlock flow | +| Legacy/BDB wallet | Refused; use a supported descriptor wallet for new mints | +| Watch-only or private keys disabled | Refused; public keys alone cannot sign | +| Blank or restricted descriptor wallet | Refused if the required key/address capability is absent | + +Qt checks this before either mint confirmation dialog. RPC mint checks it +before requesting an unlock or deriving a key. The check itself does not +reserve an address or consume a key. Construction and signing can still fail +later, and confirmation remains subject to the applicable chain rules. +For complex imported descriptors, finding stored private keys does not prove +that the wallet can derive the required Taproot owner key. That later step +must succeed too. + +Back up the existing wallet before changing its setup. For new mints, create +a normal descriptor wallet and transfer spendable DGB to it, or restore a +backup containing the required descriptors and private keys. Keep the original +wallet and its backups for existing funds and vaults. Creating a replacement +wallet does not recover a missing vault key. + +`migratewallet` is marked experimental by its RPC help. It creates a legacy +backup, requires the passphrase for an encrypted wallet and requires a new +backup after migration. Do not run it automatically or assume every migrated +wallet supplies the mint capabilities above. Check the result before funding +new mints. A migration or rescan cannot recreate private keys absent from the +recovery material. + +Source: [mint capability check](src/wallet/digidollarmintcapability.h), +[Qt mint flow](src/qt/digidollarmintwidget.cpp), +[RPC mint flow](src/rpc/digidollar.cpp) and +[migration help](src/wallet/rpc/wallet.cpp). --- @@ -397,8 +594,39 @@ Redeeming burns DD tokens and unlocks your DGB collateral. The timelock must hav digibyte-cli redeemdigidollar "position_id" 10000 # position_id = the mint transaction hash # amount = DD cents to redeem (must match full vault amount) + +# The same, stated in dollars +digibyte-cli -named redeemdigidollar position_id="position_id" dd_amount="100.00" amount_unit="dollars" +# "100.00" without amount_unit is rejected as ambiguous (see section 5) ``` +The amount names the vault principal and must equal it exactly; it is capped at +$100,000 like a send. During an emergency (ERR) the wallet computes the extra +DD it must burn from the vault itself, and that computed burn is not limited by +the cap. + +### Fee coins for a redemption + +A redemption pays its DGB fee from separate DGB coins in the wallet, never from +the collateral. The wallet chooses those coins from the projected size of the +transaction they produce and re-selects, a bounded number of times, when adding +a coin raises the fee past what the coins cover (`RedeemTxBuilder::EstimateRedemptionFee` +and `SelectRedemptionFeeInputs`, `src/digidollar/txbuilder.cpp`). A wallet +whose DGB is split into many small coins therefore redeems normally, using more +of them. If every coin is so small that it adds more fee than value, the RPC +fails with `Insufficient DGB fee inputs ... Consolidate small DGB coins into one +larger coin and retry`; send yourself one larger DGB payment first. + +### Where the returned collateral and the leftover DGB go + +The returned collateral goes to `redemption_address` when you pass one, and to +a new address of the redeeming wallet when you do not. The DGB left over after +the fee always goes to a separate change address of the redeeming wallet. If +the wallet cannot produce a change address, a redemption that names a +`redemption_address` now fails with a clear error instead of sending the +leftover to that address: the address you supply may be an exchange deposit +address or otherwise not yours, and money sent there does not come back. + ### Two Redemption Paths - **Normal** (system health ≥ 100%): Burn your original DD amount → get 100% of your collateral back @@ -421,11 +649,36 @@ digibyte-cli getoracleprice # Check activation status digibyte-cli getdigidollardeploymentinfo -# Returns: BIP9 status, signaling progress +# Returns: buried DigiDollar activation and separate tip/next-block Thaw Day status ``` --- +### Circulating tokens, vault principal and estimates + +`getdigidollarstats.total_dd_supply` is the circulating-supply field in cents: +issuance minus actual burns, subject to the legacy fallback limit below. `canonical_health.open_vault_principal` is the original DD +attached to vaults that remain open. It is not a wallet balance or a count of +all historical mints. Read canonical values only when `ready` is true. + +Before Thaw Day, the legacy fallback used when `digidollarstatsindex` is +disabled scans vault amounts. Do not treat that fallback's `total_dd_supply` +as a verified circulating token count. Use a synchronized stats index for +circulation reporting below the transition. At and above Thaw Day, the +fallback reconstructs tokens separately from vault principal. + +After Thaw Day, health uses open-vault principal. Closing a 100-DD vault with +a 125-DD burn reduces these totals by different amounts. The resulting health +can keep emergency mint restrictions active longer or require extra DD to +redeem another vault. The switch does not create tokens or change balances. + +Top-level `health_rule_height` and `selected_health_denominator` describe tip +health. `next_block_health` describes the next candidate, with its own quote, +readiness and denominator. At tip H-1 these may legitimately use different +rules. Use current construction/estimate RPCs and handle a retry if chain state +or the quote changes; a displayed estimate does not bind the confirming block. +The [architecture guide](DIGIDOLLAR_ARCHITECTURE.md) explains the accounting. + ## 11. Identifying DD Transactions (Raw Parsing) If your wallet parses raw transactions, here's how to identify DD transactions: @@ -460,7 +713,7 @@ If your wallet parses raw transactions, here's how to identify DD transactions: | `OP_CHECKCOLLATERAL` | `0xbe` | Collateral ratio check | | `OP_ORACLE` | `0xbf` | Coinbase oracle bundle marker (Tapscript OP_SUCCESSx slot pre-activation) | -Non-DD-aware wallets can safely ignore these — they behave as Tapscript OP_SUCCESSx (BIP-342) until `SCRIPT_VERIFY_DIGIDOLLAR` is set, which only happens after BIP9 `DEPLOYMENT_DIGIDOLLAR` is ACTIVE. +Non-DD-aware wallets can safely ignore these — they behave as Tapscript OP_SUCCESSx (BIP-342) until `SCRIPT_VERIFY_DIGIDOLLAR` is set, which only happens at and above the buried `DigiDollarHeight` (mainnet 23,869,440, testnet26 600, signet and regtest 0). `OP_CHECKPRICE` is reserved and deterministically disabled; mint/redeem validation reads authenticated coinbase oracle bundles and the oracle price cache instead of a script-local price opcode. Oracle P2P messages, including `ORACLEHEARTBEAT` use `IsOracleP2PActive`. @@ -475,17 +728,17 @@ Registered in `GetWalletRPCCommands()` at `src/wallet/rpc/wallet.cpp`: | Command | Description | |---------|-------------| | `getdigidollaraddress [label]` | Generate new DD deposit address | -| `listdigidollaraddresses [include_watchonly] [min_balance] [include_empty]` | List DD addresses; empty generated addresses are hidden unless `include_empty=true` | +| `listdigidollaraddresses [include_watchonly] [min_balance] [include_empty] [amount_unit]` | List DD addresses; empty generated addresses are hidden unless `include_empty=true`; `min_balance` follows the amount-unit contract | | `getdigidollarbalance [addr] [minconf] [include_watchonly]` | Get DD balance (`confirmed`, `unconfirmed`, `total`) | | `mintdigidollar [fee_rate]` | Mint DD by locking DGB collateral; amount is integer cents | -| `senddigidollar [comment] [fee_rate_ignored] [selected_inputs]` | Send DD to a DD address; integer means cents, decimal means dollars | -| `sendmanydigidollar "" [comment] [selected_inputs]` | Send DD to multiple DD addresses in one tx | +| `senddigidollar [comment] [fee_rate_ignored] [selected_inputs] [amount_unit]` | Send DD to a DD address; integer cents by default, `amount_unit` = `cents` or `dollars`, a decimal without a unit is rejected | +| `sendmanydigidollar "" [comment] [selected_inputs] [amount_unit]` | Send DD to multiple DD addresses in one tx; one `amount_unit` for all recipients | | `listdigidollartxs [count] [skip] [addr] [category]` | List DD transaction history; categories include `mint`, `send`, `receive`, `redeem`, `redeem_change` | | `listdigidollarunspent [minconf] [maxconf] [addresses] [include_unsafe]` | List DD UTXOs with `spendable` and `safe` flags | | `listdigidollarutxos [minconf] [maxconf] [addresses] [include_unsafe]` | Alias for DD UTXO listing | -| `listdigidollarpositions [active_only] [tier_filter] [min_amount] [count] [skip]` | List collateral positions | -| `getredemptioninfo [amount]` | Check redemption status; optional amount must equal the full vault amount | -| `redeemdigidollar [redemption_address] [fee_rate_ignored]` | Redeem DD -> unlock DGB collateral; integer means cents, decimal means dollars | +| `listdigidollarpositions [active_only] [tier_filter] [min_amount] [count] [skip] [amount_unit]` | List collateral positions; `min_amount` follows the amount-unit contract | +| `getredemptioninfo [amount] [amount_unit]` | Check redemption status; optional amount must equal the full vault amount | +| `redeemdigidollar [redemption_address] [fee_rate_ignored] [amount_unit]` | Redeem DD -> unlock DGB collateral; integer cents by default, `amount_unit` = `cents` or `dollars`, a decimal without a unit is rejected | | `validateddaddress
` | Validate a DD address | | `createoraclekey ` | Wallet-scoped oracle key generation | | `exportoracleprivkey ` | Export a wallet-stored oracle private key for backup/migration | @@ -498,7 +751,7 @@ Registered in `RegisterDigiDollarRPCCommands()` at `src/rpc/digidollar.cpp`: | Command | Description | |---------|-------------| -| `getdigidollardeploymentinfo` | BIP9 activation status, signaling progress | +| `getdigidollardeploymentinfo` | Buried activation height, oracle roster, MuSig2 session and Thaw Day status | | `getdigidollarstats` | Network-wide DD supply and health | | `getdcamultiplier` | Current Dynamic Collateral Adjustment multiplier | | `getoracleprice` | Current DGB/USD oracle price (from MuSig2 consensus) | @@ -519,11 +772,18 @@ Registered in `RegisterDigiDollarRPCCommands()` at `src/rpc/digidollar.cpp`: ### Qt GUI integration -DigiByte Core ships a DD lifecycle tab plus Qt widgets/dialogs/helpers: `digidollartab`, `digidollaroverviewwidget`, `digidollarsendwidget`, `digidollarreceivewidget`, `digidollarmintwidget`, `digidollarredeemwidget`, `digidollarpositionswidget`, `digidollartransactionswidget`, `digidollarcoincontroldialog`, `digidollarreceiverequest`, `ddaddressbookpage`, and `digidollar_qt_translate`. `DigiDollarTab` exposes the tabs `$DD Overview`, `Send $DD`, `Receive $DD`, `Mint $DD`, `Redeem $DD`, `$DD Vault`, and `$DD Transactions`, with an activation overlay until BIP9 activates. The Qt mint flow derives an HD owner key and persists it before broadcasting (commit `1e95478b7e`), so an HD wallet with private keys enabled is required. See `REPO_MAP_DIGIDOLLAR.md` (Qt GUI section) for individual widget responsibilities. +DigiByte Core ships a DD lifecycle tab plus Qt widgets/dialogs/helpers: `digidollartab`, `digidollaroverviewwidget`, `digidollarsendwidget`, `digidollarreceivewidget`, `digidollarmintwidget`, `digidollarredeemwidget`, `digidollarpositionswidget`, `digidollartransactionswidget`, `digidollarcoincontroldialog`, `digidollarreceiverequest`, `ddaddressbookpage`, and `digidollar_qt_translate`. `DigiDollarTab` exposes the tabs `$DD Overview`, `Send $DD`, `Receive $DD`, `Mint $DD`, `Redeem $DD`, `$DD Vault`, and `$DD Transactions`, with an activation overlay until the buried DigiDollar height is reached. The Qt mint flow uses an HD-derived owner key and the wallet capability checks described above. Key storage is requested before wallet commit; that sequence alone does not establish durable recovery. See `REPO_MAP_DIGIDOLLAR.md` (Qt GUI section) for individual widget responsibilities. #### Qt mint reject-reason translation (`DD-FA-DOC-010`) -The Qt mint widget broadcasts the assembled mint transaction directly through `node().broadcastTransaction` rather than through the `mintdigidollar` RPC. As a result the Wave 6 RPC pre-check that translates Emergency Redemption Ratio (ERR) state into a friendly RPC error does **not** run on the Qt code path: the widget's owner-key derivation, two-step confirmation dialogs, and HD wallet flow are all driven before broadcast, and the consensus reject reason (`minting-blocked-during-err`, `bad-tx-no-musig2-quote`, `volatility-freeze`, `bad-mint-lock-tier-duration`, `bad-oracle-price`, `bad-mint-collateral`) reaches the user only when mempool refuses the transaction. +The Qt mint widget calls `WalletModel::mintDigiDollar`. The wallet model +constructs and signs the transaction, then calls `CWallet::CommitTransaction` +through the wallet relay path. The widget checks wallet capabilities before +either confirmation dialog, retains the normal unlock flow and refreshes +estimates before construction. Commit or relay can still fail +if a quote or the chain changes, or if a remaining validation requirement is +not met. The wallet displays the returned reason through the translation +helper below. In v9.26.2 (`DD-FA-FUNC-032`), the mint widget routes the broadcast reason through `qt/digidollar_qt_translate.h::TranslateMintRejectReasonForUser` before display. Known DD/oracle reject tokens are rewritten with a plain-English explanation and a remediation hint (e.g. `minting-blocked-during-err` is shown as "DigiDollar minting is paused because the system is in Emergency Redemption Ratio (ERR) recovery mode."). Unknown reasons pass through unchanged so operators retain forensic detail. The translator is unit-tested by `src/test/digidollar_qt_translate_tests.cpp` and is buildable without enabling Qt. @@ -533,11 +793,16 @@ For wallet integrators that bypass the Qt widget and submit raw mint transaction ## 13. Test on Testnet Now! -The current public testnet in this source tree is **testnet26**. DigiDollar activation is BIP9-gated at/after block 600 once 140 of 200 blocks signal; verify live status with `getdigidollardeploymentinfo`. +The public testnet configured in this source is **testnet26**. DigiDollar uses +buried activation height 600. Thaw Day is scheduled at **432,100**, estimated +for September 18–19, 2026. The block height triggers the change. No public +Thaw Day activation or soak result is claimed. Check +`getdigidollardeploymentinfo` on the installed build and follow the +[upgrade window](DIGIDOLLAR_ACTIVATION_EXPLAINER.md). ### Quick Setup -1. Download the latest DigiByte Core v9.26.2 build from this branch +1. Obtain the reviewed test build and network instructions from the release owner 2. Configure for testnet: ```ini testnet=1 @@ -563,11 +828,16 @@ The current public testnet in this source tree is **testnet26**. DigiDollar acti | Oracle Consensus | 35 active slots, 7 signatures required | | Exchange Sources | Binance, CoinGecko, KuCoin, Gate.io, HTX, Crypto.com (6 active feeders, see `src/oracle/exchange.cpp:1092-1097`) | | Outlier filter | Median-distance: a price is dropped when its distance from the median exceeds `outlier_threshold × median` (`MultiExchangeAggregator::FilterOutliers` at `src/oracle/exchange.cpp:1225`) | -| Activation | BIP9 bit 23, min activation height 600; check `getdigidollardeploymentinfo` for current status | +| Activation | DigiDollar buried at 600; Thaw Day scheduled at 432,100 and reported separately by `getdigidollardeploymentinfo` | ### Mainnet Activation -Mainnet activation is BIP9-gated on bit 23 (`src/kernel/chainparams.cpp:177-180`): signaling starts 2026-06-01 (`nStartTime=1780272000`), times out 2027-06-01 (`nTimeout=1811808000`), uses a 40,320-block confirmation window with a 70% threshold (28,224 blocks), and the minimum activation height is 23,627,520. Use `getdigidollardeploymentinfo` on the target release and network as the runtime source of truth for status, window size, threshold, timeout, and minimum activation height. +DigiDollar's buried mainnet activation height is 23,869,440. The separate +Thaw Day height is 24,490,000, estimated for November 1, 2026. The block height +triggers the change. Upgrade the backend before that height using the +[node operations guide](doc/digidollar-operations.md). +See the [activation guide](DIGIDOLLAR_ACTIVATION_EXPLAINER.md) for the candidate +height boundary and old-node compatibility limits. --- diff --git a/HOUSEKEEPING_TO-DOs.md b/HOUSEKEEPING_TO-DOs.md deleted file mode 100644 index e9986465a9e..00000000000 --- a/HOUSEKEEPING_TO-DOs.md +++ /dev/null @@ -1,493 +0,0 @@ -# HOUSEKEEPING TO-DOs - -Audit date: 2026-06-25 - -Scope: formal v9.26.2 DigiDollar mainnet release readiness. This document now -tracks both the original audit findings and the housekeeping execution status. - -## Confirmed In Code - -- Version source is set to v9.26.2: - `configure.ac` has major `9`, minor `26`, build `2`, release `true`, and no RC - suffix. -- Normal public mainnet parameters are restored: - `src/kernel/chainparams.cpp` uses magic `fa c3 b6 da` and P2P port `12024`; - `src/chainparamsbase.cpp` uses mainnet data directory suffix `""`, RPC port - `14022`, and onion target port `14122`. -- The PRE/mainnet rehearsal network settings are not active in source: - the active source no longer uses `mainnet-pre`, `12046`, `14046`, or `14146` - for mainnet. -- Historical DigiByte fork heights are still present in mainnet chainparams: - DigiShield `67200`, MultiAlgo `145000`, MultiShield `400000`, DigiSpeed - `1430000`, reserve algo bits `8547840`, algo swap `9100000`, Odocrypt - `9112320`, and BIP34/BIP65/BIP66/CSV/SegWit at `4394880`. -- Mainnet DigiDollar activation is BIP9-gated: - bit `23`, threshold `28224`, window `40320`, start time `1780272000`, - timeout `1811808000`, and minimum activation height `23627520`. -- Mainnet DigiDollar, oracle validation, and MuSig2 are aligned: - `nDDActivationHeight`, `nOracleActivationHeight`, and - `nDigiDollarMuSig2Height` are all `23627520`. -- Mainnet oracle quorum is configured as 7-of-35: - `nOraclePubkeyCount = 35`, `nOracleConsensusRequired = 7`, with 35 configured - oracle pubkeys and 35 configured oracle node entries. -- The current oracle bundle validation path is shared by mainnet and testnet: - a DigiDollar price-sensitive mint or redeem must have a valid v0x03 MuSig2 - oracle bundle after activation. -- Lock-tier policy is intentionally not a housekeeping item: - the current build still contains the 1-hour and 10-year lock tiers, and this - audit does not propose changing them. - -Targeted validation already run during this audit: - -- `./src/test/test_digibyte --run_test=musig2_activation_tests` -- `./src/test/test_digibyte --run_test=rh50_oracle_keyset_alignment_tests` -- `./src/test/test_digibyte --run_test=digidollar_activation_wave12_tests` -- `./src/test/test_digibyte --run_test=miner_dd_validation_tests` -- `./src/test/test_digibyte --run_test=rh65_mainnet_testnet_validator_parity_tests` - -All five targeted test groups passed. - -## 2026-06-26 Execution Update - -Completed in this housekeeping pass: - -- Commit `88b7ad81cd` (`release: prune stale mainnet seeders`) - removed the four mainnet DNS seed domains that did not resolve from local and - Cloudflare DNS checks: - `seed.digibyteblockchain.org`, `eu.digibyteseed.com`, - `seed.quakeguy.com`, and `seed.digibyte.services`. -- The retained mainnet DNS seed domains were rechecked and returned reachable - sampled peers on TCP `12024`: - `seed.digibyte.io`, `seed.diginode.tools`, `seed.digibyte.link`, and - `seed.aroundtheblock.app`. -- `contrib/seeds/makeseeds.py` now accepts the v9.26.x user-agent family so a - future fixed-seed refresh does not filter current v9 release nodes out. -- `src/chainparamsseeds.h` was regenerated from the current - `contrib/seeds/nodes_main.txt` and `contrib/seeds/nodes_test.txt`; the - generated output matched the checked-in file, so no fixed-seed byte change was - needed. -- Commit `47b9ea3481` (`release: refresh mainnet chain metadata`) added a fresh - block `23,500,000` mainnet checkpoint and updated release safety metadata. - -Mainnet checkpoint data used from the local synced v9.26.2 Qt node: - -- Height: `23,500,000` -- Block hash: - `ade47d5ccbb92cb1d965b97a187bdbf65bf74be6a3709cb6a01339f8c2856deb` -- Chainwork: - `0000000000000000000000000000000000000000001cae290ed41eb2efd4804c` -- Block time: `1778906996` -- Chain transaction count from `getchaintxstats` at that block: `53,488,713` -- Transaction rate from the 8192-block stats window: - `0.09148870167189133` - -The placeholder mainnet AssumeUTXO entry was removed because it had a zero -serialized UTXO hash and zero transaction count. v9.26.2 now ships no mainnet -AssumeUTXO snapshot until a real snapshot is generated. - -Taproot live state was also checked with the running mainnet node: - -- Taproot status: `active` -- Taproot since height: `21,168,000` -- DigiDollar status during the check: `started`, minimum activation height - `23,627,520` - -Validation completed after the code changes: - -- `make -j$(nproc)` -- `python3 contrib/seeds/generate-seeds.py contrib/seeds` produced no diff - against `src/chainparamsseeds.h` -- Focused unit tests: - `oracle_config_tests`, `rh50_oracle_keyset_alignment_tests`, - `digidollar_oracle_tests/chainparams_oracle_endpoint_uniqueness`, and - `validation_tests` -- Full unit test suite: - `./src/test/test_digibyte --show_progress` passed `3401` test cases -- Full functional suite: - `test/functional/test_runner.py --jobs=8` passed all `378` listed entries - with expected skips; runtime `398s` -- Fuzz smoke with an empty input file passed for: - `dd_txbuilder_validate_mint_params`, `dd_txbuilder_mint`, - `dd_txbuilder_redeem`, `oracle_bundle_v03_roundtrip`, - `oracle_bundle_validation`, `oracle_musig2_bundle`, - `oracle_p2p_wire_messages`, `oracle_validate_block_data`, and - `fuzz_block_algo_routing_phase2a` -- Commit `07a63a9586` (`test: refresh regtest assumeutxo vector`) refreshed the - regtest height-299 AssumeUTXO vector and updated `feature_assumeutxo.py` to - match the deterministic DigiByte regtest snapshot it builds. -- Commit `1fec3a51f9` (`test: restore assumeutxo functional coverage`) restored - `feature_assumeutxo.py` to the default functional runner. -- `test/functional/feature_assumeutxo.py` passed directly. -- `test/functional/test_runner.py feature_assumeutxo.py --jobs=1` passed. -- `feature_assumevalid.py` remains intentionally disabled. The current test - buries the invalid block by 2100 blocks, which is Bitcoin's two-week-work - assumption, but only hours of DigiByte-equivalent work at 15-second spacing. - Porting it correctly is a separate test-maintenance task. - -Oracle peer-seeding decision: - -- The 35 oracle public keys, 35 active oracle slots, and 7-of-35 MuSig2 quorum - must stay intact. -- The release does not need 35 live oracle DNS endpoints for consensus. The - 35-slot endpoint list is roster/status metadata; oracle security comes from - the hardcoded public keys and MuSig2 bundle validation. -- For launch operations, oracle operators should use the two public v9.26.2 - mainnet oracle seed peers we have now: - `oracle1.digibyte.io:12024` and `digihash.digibyte.io:12024`. -- A third, fourth, and fifth stable public seed peer would be useful for - redundancy, but this is operational bootstrap redundancy, not consensus. - -## Release Housekeeping Status - -### 1. Remaining: Verify Mainnet Oracle Seed Peers - -Code currently defines 35 active mainnet oracle slots and endpoint strings, all -using port `12024`. Those endpoint strings are not consensus trust anchors. - -Local DNS verification found only these oracle hostnames resolving: - -- `oracle1.digibyte.io` -- `digihash.digibyte.io` - -Local TCP reachability on port `12024` during the re-check: - -- `digihash.digibyte.io:12024` accepted a TCP connection. -- `oracle1.digibyte.io:12024` did not accept a TCP connection from this host. - -The `oracle2.digidollar.org` through `oracle35.digidollar.org` style hostnames -did not resolve locally during this audit. - -Important: these configured oracle endpoints are not the same thing as mainnet -DNS seeders. They are oracle roster metadata used by RPC/status display and key -alignment checks. Oracle message validity is controlled by the configured oracle -public keys and MuSig2 signature validation, not by trusting DNS hostnames. - -Tasks: - -- Confirm `oracle1.digibyte.io` and `digihash.digibyte.io` resolve from outside - the local network. -- Confirm both public oracle seed peers accept inbound DigiByte P2P traffic on - port `12024`. -- Add one to three more stable public v9.26.2 mainnet peers if community hosts - volunteer them. -- Confirm the running oracle node for each slot uses the pubkey assigned to that - slot. -- Run a mainnet smoke check after DNS is fixed: - `listoracle`, `getoraclepubkey`, `getdigidollardeploymentinfo`, and one - controlled oracle start/stop flow. - -Why this matters: - -The code-side 35-slot roster is internally consistent. Oracle operators still -need a few reliable public peers so their nodes can find each other and relay -oracle traffic cleanly through activation. - -### 2. Completed: DNS Seed Cleanup And Fixed-Seed Check - -Mainnet previously listed eight DNS seeds in `src/kernel/chainparams.cpp`. -Commit `88b7ad81cd` removed the four domains that did not resolve locally. -The formal v9.26.2 mainnet source now retains these four DNS seeds: - -- `seed.digibyte.io` -- `seed.diginode.tools` -- `seed.digibyte.link` -- `seed.aroundtheblock.app` - -Local TCP reachability on port `12024` during the re-check: - -- Accepted `12024` in the sampled returned peers: `seed.digibyte.io`, - `seed.diginode.tools`, `seed.digibyte.link`, `seed.aroundtheblock.app` - -Current removal candidates because they did not resolve locally: - -- `seed.digibyteblockchain.org` -- `eu.digibyteseed.com` -- `seed.quakeguy.com` -- `seed.digibyte.services` - -Completed: - -- The four non-resolving domains were removed from mainnet chainparams. -- The retained seed domains returned reachable sampled mainnet peers on TCP - `12024` during the local audit. -- `contrib/seeds/makeseeds.py` now accepts v9.26.x user agents. -- Regenerating `src/chainparamsseeds.h` from the current seed files produced no - diff, so the fixed-seed byte list did not need a source change. - -Why this matters: - -The release should not depend on stale seed infrastructure. New mainnet users -need reliable initial peers after installing v9.26.2. - -### 3. Completed: Add Fresh Mainnet Checkpoint And Refresh Chain Safety Metadata - -Commit `47b9ea3481` refreshed mainnet chain safety metadata from the synced -local v9.26.2 Qt node at block `23,500,000`. - -Completed: - -- Added checkpoint height `23,500,000`. -- Updated `defaultAssumeValid` to block `23,500,000`. -- Set `nMinimumChainWork` from that block's chainwork. -- Refreshed `chainTxData` from `getchaintxstats`. -- Removed the placeholder mainnet AssumeUTXO entry because it had zero values - and was not a real release snapshot. - -Why this matters: - -This is normal release housekeeping, but it matters more for DigiDollar because -activation and oracle validation depend on nodes being cleanly synced on the -same mainnet chain. - -### 4. Completed: Confirm Taproot Is Active On Live Mainnet - -DigiDollar V1 uses P2TR vault scripts. The code has a separate Taproot BIP9 -deployment and DigiDollar activation assumes Taproot script support is available -before DigiDollar activates. - -Completed: - -- The synced local mainnet v9.26.2 Qt node reported Taproot `active`. -- Taproot was active since height `21,168,000`. - -Why this matters: - -The code sets DigiDollar activation separately from Taproot. The release process -must confirm the live network state before asking wallets, exchanges, pools, and -oracles to prepare for DigiDollar activation. - -### 5. Prepare Formal v9.26.2 Release Notes And User-Facing Docs - -The source version is v9.26.2, but several public docs still read like RC, -testnet, or PRE rehearsal material. - -Tasks: - -- Add formal `digidollar/RELEASE_v9.26.2.md` release notes. -- Update root `README.md` to explain that v9.26.2 is the formal DigiDollar - mainnet activation release. -- Update `doc/release-notes.md`; it still contains stale DigiDollar language - from older phases, including old oracle counts and old activation descriptions. -- Update `DIGIDOLLAR_ARCHITECTURE.md` and `DIGIDOLLAR_EXPLAINER.md` where they - still describe validation as RC/testnet-focused instead of formal mainnet - release-focused. -- Update oracle setup and integration guides so exchanges, wallets, pools, and - oracle operators see v9.26.2 as the formal baseline. -- Clearly mark `digidollar/MAINNET_TEST.md` and PRE release notes as historical - rehearsal documentation, not formal mainnet configuration. - -Why this matters: - -The code may be correct, but stale docs can cause operators to use the wrong -version, wrong network assumptions, wrong oracle count, or wrong activation -expectations. - -### 6. Add A Release Version Regression Test - -The build source says v9.26.2, but the current tests only check that version -output exists. They do not assert the exact formal release version. - -Tasks: - -- Add or update a focused test that verifies the exact v9.26.2 release string. -- Include the GUI splash/version text if the release image is part of the - expected user-facing version surface. - -Why this matters: - -The formal release should not ship with mismatched binary version, GUI version, -release notes, or tag name. - -### 7. Clean Up Or Register Stray Test Sources - -Tracked test files exist that are not wired into the normal test build lists. -These should be intentionally registered or intentionally removed before the -formal release. - -Unit-test files found tracked but not registered in the current test list: - -- `src/test/bip324_tests.cpp` -- `src/test/limitedmap_tests.cpp` -- `src/test/rbf_tests.cpp` -- `src/wallet/test/accounting_tests.cpp` - -Additional cleanup finding: - -- `src/test/crypto_tests_backup.cpp` defines a duplicate `crypto_tests` suite - name and appears to be a backup artifact, not an intended release test. - -Fuzz files found tracked but not wired into the normal fuzz source list: - -- `src/test/fuzz/crypto_chacha20_poly1305_aead.cpp` -- `src/test/fuzz/data_stream.cpp` -- `src/test/fuzz/odocrypt.cpp` - -Tasks: - -- Decide whether each file should be active release coverage or removed as - stale source. -- Register intended tests in the appropriate makefile lists. -- Remove true backup/stale artifacts in a focused cleanup commit. -- Re-run unit, functional, and fuzz build/test gates after cleanup. - -Why this matters: - -Unregistered tests create false confidence. Backup test artifacts can also break -future test discovery or duplicate-suite cleanup. - -### 8. Run Full Final Release Gates After Remaining Release Decisions - -After the remaining oracle endpoint and release-document decisions are fixed, -run one final release validation pass. - -Required gates: - -- Full unit test suite. -- Full functional test suite: - `test/functional/test_runner.py --jobs=8` -- DigiDollar/oracle-focused functional subset if the full suite fails for an - unrelated environment reason. -- Fuzz target build and at least a short smoke run for DigiDollar/oracle/mining - related targets. -- Clean build from a fresh checkout or clean build directory. -- GUI launch smoke check showing the v9.26.2 DigiDollar release branding. -- Mainnet startup smoke check with no PRE network parameters active. - -Why this matters: - -The formal DigiDollar release needs one clean final proof pass after metadata, -seed, docs, and version housekeeping are complete. - -## Should Do Before Or Shortly After Release - -### 9. Review Headerssync Parameters - -`doc/release-process.md` calls out headerssync review as part of release -maintenance. `contrib/devtools/headerssync-params.py` should be checked against -current DigiByte mainnet assumptions before the formal release. - -Tasks: - -- Confirm the script is still used by this release process. -- If used, update parameters from current mainnet data. -- If not used, document that clearly so future release work does not treat stale - values as active release policy. - -### 10. Fix Stale Comments Around DigiDollar Format Gates - -One RPC comment still describes V1 as setting the format gate to zero, while -current mainnet code sets MuSig2/DigiDollar activation at height `23627520`. - -Tasks: - -- Update stale comments in `src/rpc/digidollar.cpp` and nearby docs so they - match the current v0x03 MuSig2-only release model. - -### 11. Review No-libcurl Oracle Build Wording - -The exchange-price code fails closed when libcurl support is unavailable. -Release documentation and build messaging should make that clear for oracle -operators. - -Tasks: - -- Confirm configure/build output describes oracle price fetching accurately. -- Document that production oracle operators need libcurl-enabled builds. -- Make clear that no-libcurl builds are not valid production oracle-signing - builds. - -### 12. Refresh Repo Maps After Final Housekeeping - -The repo maps are useful for future agents and release audits, but they should -match the final v9.26.2 tree after housekeeping changes. - -Tasks: - -- Update `REPO_MAP.md`. -- Update `REPO_MAP_DIGIDOLLAR.md`. -- Remove or clearly mark obsolete PRE/testnet-only references. - -## Not Current To-DOs - -- Do not change the mainnet magic bytes. -- Do not change the normal mainnet ports. -- Do not change the normal mainnet data directory. -- Do not change historical DigiByte fork heights. -- Do not move DigiDollar, oracle validation, and MuSig2 to different mainnet - heights. -- Do not remove the current 1-hour or 10-year lock tiers as part of this - housekeeping pass. -- Do not keep PRE/mainnet rehearsal settings in the formal release. - -## Summary - -The formal v9.26.2 source tree now has the main PRE rehearsal reversion done, -normal mainnet network identity restored, stale DNS seeds pruned, fresh -mainnet chain safety metadata added, Taproot verified active, and DigiDollar, -oracle validation, and MuSig2 activation aligned. - -The highest-risk remaining release item is operational oracle/bootstrap -connectivity: the 35 oracle public keys and 7-of-35 quorum are correct in code, -and the launch plan is to use DigiByte.io and DigiHash as public v9.26.2 -mainnet oracle seed peers while asking for one to three more for redundancy. -Formal public docs are being handled by the separate docs team. - -## Dev Chat Post Draft - -Quick v9.26.2 DigiDollar housekeeping update before we do the formal mainnet -release. - -The main PRE rehearsal changes have been reverted. The current source tree is -back on normal DigiByte mainnet identity: same mainnet magic bytes, same normal -mainnet ports, same normal mainnet data directory, and the historical DigiByte -fork heights are still intact. DigiDollar, oracle validation, and MuSig2 are -all aligned at the same mainnet activation height in chainparams. - -Taproot is already active, and the release/docs team is handling the formal -public release notes and integration docs separately. The core housekeeping -work completed in this pass: - -1. Fresh mainnet checkpoint and chain metadata are updated. - We added block `23,500,000` as the current checkpoint, updated - `defaultAssumeValid`, set real `nMinimumChainWork`, refreshed - `chainTxData`, and removed the placeholder mainnet AssumeUTXO entry. - -2. Mainnet DNS seeds are cleaned up. - The seeds that resolved and returned reachable sampled mainnet peers were: - `seed.digibyte.io`, `seed.diginode.tools`, `seed.digibyte.link`, and - `seed.aroundtheblock.app`. - - The seed domains that did not resolve locally were removed: - `seed.digibyteblockchain.org`, `eu.digibyteseed.com`, - `seed.quakeguy.com`, and `seed.digibyte.services`. - - `src/chainparamsseeds.h` was regenerated from the current seed files and had - no diff. The seed-generation user-agent filter now accepts v9.26.x nodes. - -3. Regtest AssumeUTXO coverage is restored. - The stale regtest AssumeUTXO vector was updated and `feature_assumeutxo.py` - is back in the default functional runner. - -Remaining item: - -1. Verify oracle/bootstrap connectivity. - Oracle hostnames are not consensus trust anchors. The actual oracle security - comes from the hardcoded oracle public keys and 7-of-35 MuSig2 validation. - The hostnames are only bootstrap/status/operator metadata. - - For the formal release we only need stable public bootstrap/connectivity for - DigiHash and DigiByte.io style nodes, with one to three more useful but not - consensus-required. Those seed peers should accept normal mainnet P2P on TCP - `12024` before we put them in operator instructions. - -2. Run final release gates after the remaining release decisions. - Once oracle endpoint/docs decisions are finalized, we need the full unit - suite, full functional suite with `test/functional/test_runner.py --jobs=8`, - relevant fuzz build/smoke coverage, Qt launch/version verification, and a - final mainnet startup smoke check. - -The important part: nothing in this housekeeping changes DigiDollar economics, -mainnet identity, historical fork heights, or the DigiDollar/oracle/MuSig2 -activation alignment. This is the final cleanup pass to make sure the formal -v9.26.2 DigiDollar release starts from clean mainnet settings and reliable -network bootstrap data. diff --git a/ORACLE_DISCOVERY_ARCHITECTURE.md b/ORACLE_DISCOVERY_ARCHITECTURE.md index f0bb285a4dc..349be859d4a 100644 --- a/ORACLE_DISCOVERY_ARCHITECTURE.md +++ b/ORACLE_DISCOVERY_ARCHITECTURE.md @@ -14,7 +14,7 @@ declares 7. The `endpoint` field is informational metadata for operator coordination (it shows up in `getoracles` and `listoracle` RPC output). DigiByte Core does **not** make outbound connections to those endpoints — oracle data flows over the standard P2P graph. Wallet/light nodes therefore do not need to discover oracle endpoints to use DigiDollar; they only need a working P2P link to any peer that has the latest MuSig2 bundle. -The `getoracles` P2P message (`src/protocol.cpp:56`, handler in `src/net_processing.cpp:6262`) lets a node pull missing oracle price telemetry from a peer rather than waiting for it to be re-gossiped. It carries a request descriptor; the peer responds by re-pushing matching fresh `oracleprice` messages and recent signed `oraclehb` version heartbeats. It does not return on-chain bundles, MuSig2 nonces, context proposals, partial signatures, or endpoint records. There is no Layer-3 `oracleaddr` style endpoint announcement on the wire today. +The `getoracles` P2P message (`src/protocol.cpp:56`, handler in `src/net_processing.cpp:6343`) lets a node pull missing oracle price telemetry from a peer rather than waiting for it to be re-gossiped. It carries a request descriptor; the peer responds by re-pushing matching fresh `oracleprice` messages and recent signed `oraclehb` version heartbeats. It does not return on-chain bundles, MuSig2 nonces, context proposals, partial signatures, or endpoint records. There is no Layer-3 `oracleaddr` style endpoint announcement on the wire today. Everything below this section is design intent for adding decentralized endpoint discovery on top of that baseline. @@ -217,7 +217,7 @@ Payload: 5. No duplicate from same oracle_id in last epoch 6. Endpoint type and length are valid; no automatic outbound connection is made during validation -**Rate limiting (proposed):** Max 1 message per oracle per epoch. Reject duplicates. (For comparison, the *implemented* `oracleprice` rate limiter is 3,600 novel messages per peer per hour — silent-drop, no misbehavior penalty — see `src/net_processing.cpp:5494-5511`.) +**Rate limiting (proposed):** Max 1 message per oracle per epoch. Reject duplicates. (For comparison, the *implemented* `oracleprice` rate limiter is 3,600 novel messages per peer per hour — silent-drop, no misbehavior penalty — see `src/net_processing.cpp:5604-5632`.) ## Summary diff --git a/RAM_IMPROVE.md b/RAM_IMPROVE.md new file mode 100644 index 00000000000..78157b96cf9 --- /dev/null +++ b/RAM_IMPROVE.md @@ -0,0 +1,236 @@ +# Feather: how DigiByte uses less RAM + +Feather is the name of DigiByte's memory and startup improvements in v9.26.6. +The node keeps smaller records in RAM, reads less-used details from disk when +needed, and does less repeated work when it starts. + +## Summary + +- **Keep less in RAM.** Details already saved on disk no longer need a permanent + copy in every block's memory record. +- **Waste less space.** Smaller records and shared memory allocations reduce the + cost of keeping a directory of millions of blocks. +- **Do less repeated work.** Startup avoids unnecessary counting, large temporary + lists and repeated processing of oracle prices. +- **Keep the checks.** These changes preserve mining rules, exact work scores + and checks for missing or damaged data. + +One RC1 desktop test used about **3.65–3.67 GiB of RAM** after startup. Starting +that wallet took about **1 minute 48 seconds**. Results depend on the computer, +chain data and workload; they are not a fixed RAM limit or startup time. + +**These improvements work as soon as the upgraded program runs.** They do not +wait for Thaw Day. The separate DigiDollar rule changes wait for that height. + +## Why a full node needs so much memory + +A block is a group of transactions. DigiByte keeps a directory entry for every +known block so it can find the block, check its place in the chain and compare +competing chains. The code calls this directory the **block index**. + +There are more than 24 million mainnet blocks. A saving of just 64 bytes per +entry adds up to about 1.43 GiB across 24 million entries. + +Feather keeps this directory in RAM. It makes each entry smaller and removes +other unnecessary memory use around it. + +## What moved to disk? + +The data was already saved on disk. The change is that the node no longer keeps +some of those details in RAM for every block, all the time. + +A block's short summary is called its **header**. Two header fields now come +from disk when needed: the summary of its transactions and a number used in +mining. Their code names are `hashMerkleRoot` and `nNonce`. + +A **cache** is temporary memory that saves repeated reads. Feather keeps a +limited cache of saved headers. Older entries can be replaced because they +can be read from disk again. + +```mermaid +flowchart TD + A[Keep a small directory of blocks in RAM] + B[Need more details about a saved block] + C{Already in the cache?} + D[Use the RAM copy] + E[Read from the existing disk database] + A --> B --> C + C -->|Yes| D + C -->|No| E +``` + +**Unsaved data is different.** New header details stay in RAM until their save +succeeds. A full cache is never permission to discard unsaved data. + +## The other memory changes + +### 1. Remove eight shortcuts from every block record + +Each record used to hold eight pointers to earlier blocks, grouped by mining +algorithm. These were shortcuts for finding a previous block of the same kind. + +The node now follows the chain's existing links to find that block. Removing +these pointers saves 64 bytes per record on the measured 64-bit build. The +mining difficulty rules stay the same, including rules for historical Groestl +blocks. + +The tradeoff is a little extra lookup work. This particular change saves RAM; +it does not make every lookup faster. + +### 2. Store work scores in less space + +The node compares valid chains by their total mining work. This number is +called **chainwork**, and its exact value matters. + +Feather uses a smaller holder for this number. If a value is too large to fit, +it keeps the full-size value separately in RAM. No digits are rounded away, +and the math still uses the full number. + +Together with reading the two header fields from disk, this cuts another +48 bytes from the main block record on the measured build. + +| Change | Size of the main record | +| --- | ---: | +| Before the memory work | 208 bytes | +| After removing the eight pointers | 144 bytes | +| After the header and work-score changes | 96 bytes | + +This table covers the main record only. The surrounding lookup tables, caches +and other memory still add to the total. Sizes can differ on other builds. + +### 3. Give many records space in one larger memory allocation + +Asking the system for millions of separate pieces of memory adds bookkeeping +and wasted space. Feather asks for larger chunks and gives each record a piece. +This is called a **memory pool**. + +Each record stays in the same place while in use, so links between records keep +working. The lookup table also removes some bookkeeping where the system's +C++ library allows it. The full block identifier is still kept. + +### 4. Read the part of a database file that is needed + +The block directory and coin database now read requested parts of files into +buffers. Previously, their reader could map whole files into the program's +memory space. The database still checks that the data is complete and intact. + +This also changes how the task manager counts memory. The operating system can +still keep recently read file data in its own cache. A lower RAM figure for the +program does not mean every byte of that decrease became free system RAM. + +The actual savings from smaller records and fewer temporary allocations are +separate from this change in how file memory is counted. The database cache +budgets were not simply turned down. + +### 5. Use a smaller temporary list when checking coins + +Some coin records are on disk. Recent changes may still be in RAM. Accounting +checks need both, with the newer information taking priority. + +The scan now reads the saved records alongside a sorted copy of the changed +coins. This replaces the extra lookup table used in an earlier version of the +new accounting code. Coins already spent are left out of the result. + +The copy is intentional. The working coin cache can change during the scan. +Keeping a stable copy of its changes avoids reading data that moves beneath +the scan. This is a smaller temporary structure, not the removal of a check. + +## What makes startup do less work? + +| Before | Now | +| --- | --- | +| Walk the block database just to count its entries, then load them | Load them straight away and show how many have been read | +| Build a large list of possible chain ends, then remove most of it | Read the saved chain's last block first, then build the list that is needed | +| Repeat warning calculations for old periods that cannot trigger a warning | Skip those periods; still check the boundary and later periods | +| Repeat parsing and setup for historical oracle price records | Reuse work already done for those records | +| Rebuild completed oracle price history again during the same startup | Use the completed reconstruction | + +Oracle services provide signed DGB prices for DigiDollar. Their required price +history is still loaded before startup finishes. Valid competing chains are +still considered when the node builds its list of possible chain ends. + +Long scans now show progress and respond to cancellation. Incomplete results +are not marked ready. Required accounting checks still run, including the +check of saved vault totals. + +Mining also avoids repeated work. The initial Thaw Day accounting record is +prepared when the preceding block is actually added to the chain. It is not +rebuilt every time a miner asks for a proposed block to mine. + +## How saves and reads stay safe + +- **Save before forgetting.** Unsaved header details and pending-write records + stay available if a database write fails. +- **Save in the right order.** Block files and the data needed to reverse blocks + are saved before the directory records their disk locations. +- **Check what was read.** A stored header must match the block requested. + Missing or damaged data produces a storage error. +- **Keep errors separate.** A local disk failure is not treated as proof that + another node sent an invalid block. + +Feather keeps the existing block-index file format and network header format. +Its storage changes alone do not require downloading or rechecking the whole +chain. Thaw Day's separate accounting changes have their own recovery checks. + +## Why the node still uses several gigabytes + +The block directory still has an entry for every known block. The node also +needs memory for coin caches, wallets, pending transactions, network connections +and the work of checking new blocks. + +The `dbcache` setting controls only some of those caches. It is not a limit on +the whole program. Feather therefore reduces RAM use without turning a full +DigiByte node into a 200 MB program. + +Some operations now do more disk reads or follow more chain links. Startup +removes other work. The total effect on speed depends on the workload and disk. +First sync, recovery and accounting checks after Thaw Day can take longer than +an ordinary restart before it. + +## Details for developers and operators + +These are the limits and checks behind the design above. + +| Item | Detail | +| --- | --- | +| Saved-header cache | Holds at most 65,536 full headers, plus bookkeeping; callers receive their own copy | +| Unsaved headers | Reaching 65,536 entries requests a save; entries remain until that save succeeds | +| Header fields read from disk | A 32-byte transaction summary and a 4-byte mining nonce | +| Work-score storage | A 16-byte holder keeps values up to 112 bits directly; larger values use a separate exact 256-bit number | +| Database file cache | Targets 64 table files per affected database; an active scan can hold more | +| Open-file budget | Startup reserves room for database files before assigning room to peer connections | +| Lookup bookkeeping | Avoids an extra cached hash where supported; full block identifiers remain | +| Unix and Windows reads | Unix uses reads at a given file offset; Windows locks the seek-and-read operation | +| Cache changes and chain snapshots | Keep the same buffered-read settings; block and reversal data are saved before index references | +| Other databases | Keep their existing defaults | + +On Unix, a low open-file limit can reduce connections or stop startup. If the +node reports that limit, check the operating system's file allowance. + +The recorded desktop run and test results used source commit +`d2097819f260f4d82409643fe9f7263cdd7e3eaa`. That run used no process swap in the +recorded samples. The startup time was 108 seconds; it was not a measurement +of the future Thaw Day accounting scan. + +Recorded checks included 3,739 passing unit tests and 394 passing functional +tests, with 17 functional tests skipped and none failed. Separate desktop, +memory-error and fuzz checks are recorded with the release tests. A skipped +test is not a pass. Full mainnet replay and public activation checks remain +separate release requirements. + +Future changes need tests for exact work scores, historical mining rules, +cache replacement, failed reads and writes, simultaneous reads, restarts, +pruning and chain changes. Compare the accepted block and work score as well +as RAM use. Measure the same workload on both builds. + +| Code area | Files | +| --- | --- | +| Block records and saved headers | [chain.h](src/chain.h), [chain.cpp](src/chain.cpp), [blockstorage.cpp](src/node/blockstorage.cpp) | +| Mining lookups | [pow.cpp](src/pow.cpp), [lookup tests](src/test/pow_algo_lookup_tests.cpp) | +| Work scores | [compact_work.h](src/util/compact_work.h), [work-score tests](src/test/compact_work_tests.cpp) | +| Shared memory allocations | [blockstorage.h](src/node/blockstorage.h), [pool.h](src/support/allocators/pool.h), [hasher.h](src/util/hasher.h) | +| Header cache | [blockheadercache.cpp](src/node/blockheadercache.cpp), [cache tests](src/test/blockheadercache_tests.cpp) | +| File reads | [dbwrapper.h](src/dbwrapper.h), [dbwrapper_env.cpp](src/dbwrapper_env.cpp), [reader tests](src/test/dbwrapper_env_tests.cpp) | +| Coin scans | [coins.cpp](src/coins.cpp) | +| Startup and saving | [chainstate.cpp](src/node/chainstate.cpp), [validation.cpp](src/validation.cpp), [init.cpp](src/init.cpp) | +| Oracle prices and health | [bundle_manager.cpp](src/oracle/bundle_manager.cpp), [health.cpp](src/digidollar/health.cpp) | diff --git a/RELEASE_v9.26.6.md b/RELEASE_v9.26.6.md new file mode 100644 index 00000000000..c231a346421 --- /dev/null +++ b/RELEASE_v9.26.6.md @@ -0,0 +1,808 @@ +# DigiByte Core v9.26.6 release notes + +## Final v9.26.6 release + +v9.26.6 includes RC1, RC2 and the final wallet improvements listed below. +The final changes improve minting from wallets with many small coins, explain +send errors, and make the wallet easier to read. **They do not change RC2's +consensus rules, oracle requirements or Thaw Day heights.** Consensus means +the rules nodes use to agree on valid blocks. + +**All full-node and mining operators must upgrade before mainnet block +24,490,000, including operators who do not use DigiDollar.** Older software +can disagree about valid blocks after that height. Testnet26 activated Thaw +Day at block **432,100**. Mainnet activation is controlled by its block +height, not a calendar date. + +### What to do + +1. Back up your wallets and configuration. Keep the backups private. +2. Download the final release for your system and verify its signed checksums. +3. Stop the old wallet or daemon normally, replace the program, and restart it. +4. Check that the version is v9.26.6 and that the node finishes synchronizing. + +A normal upgrade from a working RC2 node does not require a reindex. Keep the +required DigiDollar block history. If the old node stopped on a rejected block, +use the reviewed recovery instructions for that failure. + +Installing this release enables its wallet and display improvements immediately. +The scheduled mainnet rule changes begin at block 24,490,000. Minting still +requires an eligible price, sufficient collateral and the other existing checks. + +### Changes added after RC2 + +Each entry is one change group. Supporting tests count with their fix. +The version number, wallet version image and release-note edits are not extra fixes. + +| Release stage | Change groups added | +| --- | ---: | +| RC1 | 83 | +| RC2 | 24 | +| Final release | 18 | +| **Total for v9.26.6** | **125** | + +#### Minting and sending + +1. **Change:** Large mints wait for coin-merge transactions to confirm before building the mint, and show the merge transaction IDs. Repeated requests reuse that pending status, including after restart.\ + **Why:** Spending several large, unconfirmed merges at once can exceed the existing transaction-chain limit. The wallet must explain the wait without recording a failed mint or starting unnecessary extra merges. + +2. **Change:** The Qt mint form respects manually locked DGB coins and uses the wallet's normal spendable-coin checks.\ + **Why:** Coins the user has locked must stay out of automatic collateral selection. + +3. **Change:** DigiDollar sends report the actual coin-selection error and check the selected inputs before asking for confirmation.\ + **Why:** Change below $1 needs a different amount or input selection. Waiting for confirmations will not fix that error. The $1 output minimum is unchanged. + +4. **Change:** Oversized DGB sends explain that too many coins are needed and suggest a smaller send or combining small coins first.\ + **Why:** The existing transaction-size limit remains necessary, but users need a useful next step. + +5. **Change:** A DigiDollar send that lacks fee funds shows the estimated fee in DGB.\ + **Why:** Users should not have to convert an internal unit or assume every send costs the same amount. + +#### Wallet screens and CSV exports + +6. **Change:** The Send DigiDollar address field says "Enter a DigiDollar address."\ + **Why:** Example text should not look like an address already entered by the user. + +7. **Change:** DigiDollar paste and address-book icons have readable contrast, and Send and Clear buttons visibly respond to hovering and clicking.\ + **Why:** The controls should be recognizable and show when they are being used in both themes. + +8. **Change:** DigiDollar transaction rows, filters and dropdowns use the DigiDollar green theme consistently.\ + **Why:** Mixed background and text colours made the page harder to read. + +9. **Change:** Main Transactions CSV dates include hours, minutes and seconds without an unnecessary ".000" ending.\ + **Why:** Exports should contain clear, consistent dates. Dates shown inside the wallet still follow the computer's regional settings. + +10. **Change:** The main Transactions page exports DigiDollar amounts as plain decimal numbers without a currency suffix.\ + **Why:** Spreadsheets need numeric cells for calculations. This completes the CSV amount repair started in RC2. + +11. **Change:** Transaction exports suggest a dated filename and make the disabled Save button readable.\ + **Why:** Users should have a useful starting filename and understand when saving is unavailable. + +12. **Change:** New folder names remain readable while editing them in the dark-theme export dialog.\ + **Why:** The old styling could put white text on a white background. + +13. **Change:** The About window gives its text enough room, keeps links readable, and points to the current DigiByte website and source repository.\ + **Why:** Users need to find their version and the project's current information without a cramped text column. + +#### RPC, logging and test tools + +14. **Change:** `listdigidollaraddresses` returns saved labels, transaction counts and last-used times from the wallet.\ + **Why:** These fields previously returned empty or zero values. An unknown address creation date remains empty rather than being guessed. + +15. **Change:** `validateaddress` help and wallet integration instructions direct DigiDollar users to `validateddaddress`.\ + **Why:** DigiByte and DigiDollar use different address formats. The existing DigiDollar command checks DD, TD and RD addresses without changing ordinary DigiByte address validation. + +16. **Change:** Expected nonmatching DigiDollar key checks appear only when DigiDollar debug logging is enabled.\ + **Why:** Routine wallet scanning should not fill the normal log with misleading messages. + +17. **Change:** The isolated Thaw Day test builds the current release candidate by default and also accepts an exact commit or tag.\ + **Why:** Each rehearsal must exercise the intended candidate rather than an older, fixed commit. + +18. **Change:** The Thaw Day test leaves its last permitted block available while oracle signatures finish.\ + **Why:** A block mined without a price bundle cannot gain one later. The test must wait before mining that block and still check the actual signed bundle afterward. + +### Integration notes and limits + +When a mint needs coin merging, `mintdigidollar` can return +`status: "consolidation_pending"` and `consolidation_txids`. This means that +**no DigiDollar has been minted yet**. Wait for those transactions to confirm, +then retry. Check any accompanying `error` if a later merge failed. The normal +successful mint response is unchanged. Coin merges are ordinary DGB +transactions and pay the normal network fees. + +`listdigidollaraddresses` counts distinct DigiDollar transactions known to the +wallet. `last_used` is the latest wallet transaction time, in UTC. These fields +describe this wallet's records, not a complete public address history. + +This release does not add a general "combine coins" button or DigiDollar +watch-only import support. An oracle pause remains a restriction, not permission +to bypass missing signatures or price checks. A reported Windows-only blank +progress popup was not reproduced in the Linux checks; no speculative change +was made for it. + +### Final verification + +The Linux test runs checked the production source at `a96e6b10b6`. The later +rehearsal correction changes only the test script and its regression tests; +the production source is identical. + +| Check | Result | +| --- | --- | +| Core unit tests | 3,759 cases passed; two existing cases have partial-fixture warnings | +| Extended functional tests | 405 passed, 17 skipped, none failed | +| Qt tests | All 11 suites passed: 178 reported rows, including setup and cleanup | +| Fuzz tests | All 256 targets completed with memory and undefined-behavior checks | +| Cryptography and supporting library tests | All six test programs passed | +| Utility and RPC authentication tests | Passed | +| Rehearsal script regression tests | All seven passed | +| Full private Thaw Day rehearsal | Passed before and after activation, including price limits, recovery, reindex and fresh sync | + +The functional skips require older binaries, tracing support, special network +interfaces, or unsupported Signet features. Fuzz checks replayed the saved +test inputs; the two targets without saved inputs each generated new inputs +for ten seconds. This was a bounded test run, not an exhaustive search. + +Linux visual checks covered all seven DigiDollar tabs, the changed controls +and export dialogs, and the About window in light and dark themes. Native +Windows and macOS checks are still needed on their release packages. + +The private rehearsal used commit `86c8176902` with the isolated lab patch. +All nine nodes finished on the same block at height 5,686. The lab changes +its own network settings and activation height; it does not replace a full +mainnet history reindex or native release-package checks. + +The RC1 and RC2 results below describe those earlier candidates. Release +packages must be built from the reviewed final tag and checked before +publication. These source tests do not verify packages that have not yet +been built. + +--- + +## RC2 — second release candidate + +RC2 adds the changes below to RC1. The code change list covers source through +`943243613ce7f75492eae8b4a074859eeb5ba6d1`. RC2 is a test release before the +final v9.26.6 release. Release packages must be built from the final RC2 tag +and verified before distribution. + +**Full-node and mining operators must upgrade before mainnet block +24,490,000.** Thaw Day changes the rules used to agree on valid blocks, even +for operators who do not use DigiDollar. Older software can disagree after +activation. Installing the update prepares the node; the block height starts +the scheduled rule changes. + +| Network | Thaw Day activation block | +| --- | --- | +| Mainnet | **24,490,000** | +| Testnet26 | **432,100** | + +Testnet26 has passed block 432,100 and Thaw Day activated there. RC2 is being +tested on the already-active testnet. Mainnet block 24,490,000 remains the +future production activation height. Calendar estimates in the preserved RC1 +notes are historical. The RC2 repair that removes wallet state from DigiDollar +validation also applies when checking blocks below Thaw Day. + +### What the totals count + +| Candidate | Corrective fixes | Usability, performance and other improvements | Tests, tools and documentation | Total | +| --- | ---: | ---: | ---: | ---: | +| RC1 | 53 | 23 | 7 | **83** | +| RC2 | 12 | 9 | 3 | **24** | +| RC1 + RC2 subtotal | 65 | 32 | 10 | **107** | + +Each numbered entry counts one distinct change group. Related repairs are +grouped, and a repair's supporting tests are included with that repair. A +separate test tool or coverage expansion has its own entry. Version labels, +wallet version artwork and edits to these notes are not extra improvements. +These totals are not counts of security vulnerabilities or outside reports. +The categories describe the main purpose of each group, not its severity. + +### Corrective fixes since RC1 + +1. **Change:** DigiDollar block and mempool validation read amounts and vault identity from chain records independently of the wallet's local script table.\ + **Why:** Nodes checking the same transaction must reach the same result regardless of their wallet history. + +2. **Change:** Header synchronization calculates mining work using the header's actual chain history, including competing branches.\ + **Why:** Valid headers must receive the work score required by the existing mining rules. + +3. **Change:** Incoming block checks verify proof of work before consulting more expensive chain history.\ + **Why:** Invalid block submissions should be rejected without unnecessary history lookups. + +4. **Change:** The DigiByte send form identifies a missing or invalid address, an invalid amount, or an amount too small to send.\ + **Why:** Clicking Send should explain the field that needs attention. + +5. **Change:** DigiDollar transaction dates and amounts, position dates and receive-request dates sort by their underlying values.\ + **Why:** Formatted text can put dates and money in the wrong order. + +6. **Change:** DigiDollar transaction CSV exports write amounts as plain decimal numbers.\ + **Why:** Spreadsheets and accounting tools need numeric amounts without a currency suffix. + +7. **Change:** DigiDollar pages distinguish expired, abandoned and conflicted mint attempts from confirmed vaults and completed redemptions.\ + **Why:** An unsuccessful mint must not appear as collateral that was redeemed or can still be redeemed. + +8. **Change:** The pending DigiDollar balance excludes local mint attempts whose confirmation window has expired.\ + **Why:** A mint that cannot confirm on the current chain should not inflate the pending balance. + +9. **Change:** Redemption commands and estimates use the correct block boundary for the transaction's time lock.\ + **Why:** A vault must not be reported as redeemable one block before its redemption can enter the mempool. + +10. **Change:** Redemption status finds the actual collateral output when an older mint places its outputs in a different order.\ + **Why:** An unconfirmed redemption must remain pending until it confirms, including when ordinary change precedes the collateral. + +11. **Change:** Bounds the recent Dandelion transaction hashes remembered for each peer while keeping exact checks for retained entries.\ + **Why:** Long-running peer connections need predictable memory use without weakening transaction relay checks. + +12. **Change:** Clears Dandelion peer routes before shutdown deletes peer connections.\ + **Why:** Shutdown must not leave transaction relay referring to a peer that has already been removed. + +### Usability and logging improvements since RC1 + +13. **Change:** DigiDollar dates use the computer's regional format, with columns sized to fit.\ + **Why:** Dates should follow the user's settings across the wallet. + +14. **Change:** The DigiDollar overview and mint pages explain missing prices, unavailable checks and restrictions that pause minting.\ + **Why:** Users need to know why minting is unavailable and when they can check collateral and fees. + +15. **Change:** DigiDollar controls, labels, dropdowns and status text follow the wallet's light and dark themes consistently.\ + **Why:** Both themes need readable text and recognizable controls. + +16. **Change:** Repeated redemption requests distinguish a pending redemption, a redeemed vault and another inactive position.\ + **Why:** Users need the position's actual state before deciding whether to retry. + +17. **Change:** `estimatecollateral` includes mint restrictions and their reason alongside its collateral figures.\ + **Why:** A collateral estimate alone does not mean a mint can currently proceed. + +18. **Change:** DigiDollar commands distinguish an unavailable next-block oracle quote from unavailable health state.\ + **Why:** The error should identify which information is missing. + +19. **Change:** `senddigidollar` help explains that the sending wallet needs spendable DGB for the transaction fee.\ + **Why:** A DigiDollar balance cannot pay a DGB network fee. + +20. **Change:** Repeated wallet reconciliation messages during synchronization require `-debug=digidollar`.\ + **Why:** Routine progress should not fill the normal log once per block. + +21. **Change:** Routine compact-block header messages follow the network debug setting.\ + **Why:** Normal logs should stay quiet when network debugging is disabled. + +### Tests, tools and documentation since RC1 + +22. **Change:** Adds a repeatable, isolated Thaw Day rehearsal with build records and checks before, across and after activation.\ + **Why:** The same wallet, oracle and chain scenarios need to be repeatable on each candidate. + +23. **Change:** Expands tests for DigiDollar transfer recovery after a mint is disconnected and later confirms again, including after restart.\ + **Why:** Tests must cover normal wallet rebroadcast and balance recovery as well as rejection while the mint is unconfirmed. + +24. **Change:** Corrects the private security-reporting instructions.\ + **Why:** Sensitive reports need to reach the project's designated private channels. + +### Verification and known limits + +The combined source passed 3,755 unit test cases and the rendered Qt test +suite. Of 421 scheduled extended functional-test entries, 404 passed and +17 were skipped. None failed. The skips cover unsupported Signet, unavailable +old release binaries, disabled USDT tracepoints and special test IP addresses. +All 256 fuzz targets passed under AddressSanitizer and UndefinedBehaviorSanitizer, +which check for memory errors and undefined behavior. That run replayed saved +inputs and generated inputs for the target without a saved corpus. +Focused shutdown tests also reproduced the stale-route failure before the fix +and passed under AddressSanitizer after the fix. + +The isolated Thaw Day rehearsal built from commit +`8ae095ae01b3bbdac13723db346c3970343ec544` also passed. It repeated DigiDollar +minting, sending, redemption and accounting before and after the exact activation +height. It also passed restart, backup restore, boundary rollback, competing-branch +reorg, full private reindex and empty-directory fresh-sync checks. + +These results verify the source build used for testing. They do not certify the +final RC2 release binaries. The release build record must identify the final tag, +binary hashes, build options, completed checks, skips and unresolved findings. +An isolated rehearsal does not establish public-network agreement. + +Large DigiByte sends from wallets with many small coins can still require +consolidation. RC2 does not add a guided or automatic consolidation flow for +that reported case. The abandoned-transaction icon has not been replaced. +These notes do not claim that every reported synchronization problem or +external wallet, pool or service issue has been resolved. + +The scheduled Thaw Day rules, mining difficulty, block rewards and the $1 +minimum DigiDollar output are unchanged from RC1. New minting status messages +report current conditions; a successful mint does not make a later warning +incorrect if those conditions change. + +Before installing a published build, back up wallets and configuration, verify +the published signed checksums, and stop the old program normally. Keep the +required DigiDollar block history. Use the release's reviewed recovery +instructions if an older candidate stopped on a rejected block. + +Report ordinary problems with the version, network, block height, expected +result and relevant logs with private information removed. Use +[the private security-reporting instructions](SECURITY.md) for sensitive +reports. + +## RC1 — preserved release notes + +The following is the unchanged RC1 note from tag `v9.26.6rc1`, commit +`390f71d5cb74bf519dc0510cc5d8e9c70fefa3cc`. Its version, counts, dates, +measurements and test results refer to RC1. The RC2 section above gives the +RC2 additions, combined count and verification limits. + +--- + +# DigiByte Core v9.26.6 release notes + +**Current version: v9.26.6rc1.** This is the first release candidate: a test +build before the final release. + +## Summary + +v9.26.6 includes **83 fixes and improvements**. It improves wallet recovery, +DigiDollar (DD) sending and redemption, wallet displays, node stability, price +services, memory use and startup. The count also includes tests and documentation. + +**Node and mining operators must upgrade before mainnet block 24,490,000, +expected around November 1, 2026.** This applies even if you only use DGB. + +Thaw Day changes consensus: the rules nodes use to agree on valid blocks. +Older software may follow a different chain after the change. + +| When | What takes effect | +| --- | --- | +| When the upgraded program runs | Wallet, stability, memory and startup improvements | +| At Thaw Day | The new DigiDollar block rules, together at one height per network | + +We are testing Thaw Day on **testnet first**. The final mainnet release follows +the remaining tests and release review. + +## What you need to do + +1. **Read the upgrade notes below.** Check scripts that send DD amounts and + make sure the node has enough disk space. +2. **Back up your wallets and keep existing backups.** Keep your configuration + and oracle keys too. +3. **Verify the build, then replace the old program.** Check the release's + signed checksums. Stop the old node normally and wait for it to exit first. + RC1 is for the test exercise; use the approved final build for the mainnet rollout. +4. **Let startup finish.** The node may need to check or rebuild accounting. + Do not mistake a long scan for a stopped program. +5. **Check the running node.** Confirm its version, network, sync progress and + Thaw Day height. Check any wallet or oracle service you operate. + +Full nodes, miners, pools, exchanges, oracle services and wallet backends need +the update. If a provider runs your wallet or exchange service, check its +upgrade notice. + +## Thaw Day: testnet first, then mainnet + +| Network | Activation block | Estimated date | +| --- | --- | --- | +| Testnet26 | **432,100** | September 18–19, 2026, before the September 21 test target | +| Mainnet | **24,490,000** | Around November 1, 2026 | + +**The block number starts the change. The date is an estimate.** Faster or +slower block production can move that date. The old rules remain below the +height, including when old blocks are checked again. + +Release files and instructions must be available at least **3 days before +testnet activation** and **14 days before mainnet activation**. Track block +progress during that time. + +There is no miner vote or local public-network setting to change Thaw Day. +Postponing it requires a replacement build and coordinated upgrades before +the original height. Editing an announcement cannot postpone installed software. + +The testnet exercise will check the switch itself, minting, transfers, +redemptions, oracle prices and agreement between independent nodes. Tests also +cover restarts and chain changes, where recent blocks are replaced by another +branch. Public testnet activation and observation have not yet finished. + +To check your node's schedule: + +```bash +digibyte-cli getdigidollardeploymentinfo +digibyte-cli -testnet getdigidollardeploymentinfo +``` + +The result shows the height and whether the rules apply to the current tip +or next block. The tip is the latest block on that node's chain. + +## Upgrade details that matter + +### Amounts sent by scripts + +Four commands accept `amount_unit`: `senddigidollar`, +`sendmanydigidollar`, `redeemdigidollar` and `getredemptioninfo`. +The amount filters in `listdigidollarpositions` and +`listdigidollaraddresses` use it too. + +| Amount supplied | Meaning | +| --- | --- | +| `25000`, with no unit | 25,000 cents, or $250.00 | +| A decimal, with no unit | Refused; choose cents or dollars | +| `amount_unit="cents"` | Whole cents only | +| `amount_unit="dollars"` | Dollars, with at most two decimal places | + +**Existing scripts using whole cents keep working. Scripts using decimals +must specify dollars.** The new unit argument goes last in a positional call. +For example, this requests a $250.00 send: + +```bash +digibyte-cli -named senddigidollar address="" amount="250.00" amount_unit="dollars" +``` + +`mintdigidollar` still takes whole cents. + +The **$1 minimum DD output is unchanged**. The **$100,000 request limit** +applies to a single send, each sendmany recipient and a requested redemption. +It does not limit the extra DD the rules may require burning in an emergency +redemption. A redemption still closes the whole vault. + +### Minting and redemption after Thaw Day + +A **vault** is DGB locked to back newly minted DigiDollars. **Oracles** provide +the signed DGB prices used by DigiDollar. + +At Thaw Day, a new price check replaces the old mint freeze. It uses prices +already recorded in the chain. Transfers and redemptions stop using the old +volatility freeze, but their other requirements still apply. + +New mints still need an accepted oracle price, enough collateral and a price +that passes the new check. Large price swings can still pause minting. + +Health will count the original DD amounts attached to vaults that remain open. +DD still in circulation is a separate total. Extra DD burned during redemption +can make circulation smaller than the amount attached to open vaults. + +**This can lower health, keep emergency restrictions in place longer or require +more DD to redeem than the old calculation.** Check the current estimate before +confirming. Thaw Day itself creates no tokens and changes no wallet balances. + +### Wallets and service integrations + +- Minting needs the modern descriptor wallet format, private keys and supported + receiving and change addresses. The wallet explains missing support early. +- Existing wallets can still send and redeem if they hold the needed keys. + Recovery of a missing key record cannot replace a missing private key. +- CSV exports add `Amount ($DD)`. Position status can include `expired_mint`, + `abandoned_mint` and `conflicted_mint`. +- Statistics separate `open_vault_principal` from circulating supply. + `selected_health_denominator` identifies the total used for health. + An unavailable total is **not zero**. +- Oracle signing keys and required agreement stay the same. +- Use `-debug=digidollar` if your log tools need routine DD details. + Errors and startup progress remain visible without it. + +### Disk space, startup and recovery + +Pruning deletes older block files to save space. A pruned node must still keep +DigiDollar's required history. On mainnet, the retention floor remains +**23,627,520**. Required blocks and the data used to reverse them must stay. + +That history can exceed a small prune target. The target is not a hard disk +limit, and this release does not add a warning for every overrun. Leave room +for growth and check actual disk use. + +An ordinary upgrade with the required data does not need a full reindex. +A reindex rebuilds the node's chain databases from block history. A late +upgrade may need extra checks; missing files need restoration or download. +Repeated restarts cannot supply missing data. + +If recovery fails, keep the error, logs, wallets and block files. Pause any +automatic restart loop while the cause is checked. After Thaw Day, downgrading +to old software is not a repair. If nodes disagree, compare block hashes at +the same height and coordinate recovery before resuming settlement. + +Feather's RAM improvements work on installation. On Unix, the new database +file allowance may expose a low system open-file limit. Check that limit if +startup reports it or reduces connections. + +## All 83 fixes and improvements + +Each entry states the change and the reason. Related repairs are grouped. +These are implemented changes, not a claim that every outside bug report is fixed. + +### Thaw Day and DigiDollar accounting + +New block rules wait for Thaw Day. Status reporting and preparation are available before it. + +1. **Change:** All new DigiDollar block rules start at one Thaw Day height on each network.\ + **Why:** Nodes need to change rules at the same point. + +2. **Change:** Status shows the scheduled height and whether the current or next block uses the new rules.\ + **Why:** Operators can see when their node reaches Thaw Day. + +3. **Change:** At Thaw Day, new mints use earlier block prices to check for large price swings.\ + **Why:** Nodes checking the same chain need the same minting reference. + +4. **Change:** At Thaw Day, transfers and redemptions stop using the old volatility freeze.\ + **Why:** The minting price safeguard should not apply that freeze to these operations. + +5. **Change:** At Thaw Day, checks use the prices, coins and history belonging to the block being checked.\ + **Why:** A restart or a different branch must not change the inputs to that block's checks. + +6. **Change:** At Thaw Day, health counts the original DD amounts attached to vaults still open.\ + **Why:** The system needs a consistent measure of what those vaults back. + +7. **Change:** Vault totals are saved with chain data and updated as blocks are added or reversed.\ + **Why:** Accounting must stay with the right chain through restarts and interrupted saves. + +8. **Change:** At Thaw Day, validation and recovery give each vault the same transaction-based identity.\ + **Why:** Both paths must find the same vault. + +9. **Change:** Recovery checks saved vault totals and unchecked Thaw Day history, and rebuilds totals from available records when needed.\ + **Why:** Late upgrades and interrupted recovery must finish their checks before using the results. + +10. **Change:** Status separates DD still in circulation from the amounts attached to open vaults.\ + **Why:** Burning extra tokens can make these two totals differ. + +11. **Change:** From Thaw Day, saved circulating-supply totals are checked against chain records and repaired when needed.\ + **Why:** An incorrect saved total should not keep carrying forward. + +12. **Change:** Coin scans use a consistent order and read older accepted vault records consistently.\ + **Why:** Recent changes saved at different times must still give the same accounting totals. + +### Node crashes, hangs and transaction sharing + +Dandelion is DigiByte's system for passing transactions between nodes. + +13. **Change:** Fixes a Dandelion crash when recent blocks are replaced by another chain branch.\ + **Why:** Pending transaction records must remain usable during a chain change. + +14. **Change:** Fixes hangs during transaction-sharing checks, peer disconnections and incoming announcements.\ + **Why:** Tasks running at the same time must not get stuck waiting for each other. + +15. **Change:** Protects shared transaction records when the usual Dandelion relay peer is unavailable.\ + **Why:** The fallback send path must handle those records safely too. + +16. **Change:** After loading a saved chain state, the relay checks pending transactions against that chain.\ + **Why:** The relay must follow the chain the node is using. + +17. **Change:** Fixes conflicting wallet and chain locks during wallet loading, imports, rescans and DigiDollar checks.\ + **Why:** Wallet work and chain work must not block each other indefinitely. + +### Wallet recovery, sending and redemption + +18. **Change:** Saves a mint's owner key and recovery records before sending the transaction.\ + **Why:** The recovery information must be saved before the mint can reach the network. + +19. **Change:** Rebuilds a missing vault-key record from a matching key already in the wallet.\ + **Why:** A missing record should not stop redemption when the correct key still exists. + +20. **Change:** Refuses to replace a vault's saved owner key with a different key.\ + **Why:** Existing vault ownership records must stay intact. + +21. **Change:** Reports an error if the key for a new DigiDollar address cannot be saved.\ + **Why:** The wallet should save the required key before offering the address. + +22. **Change:** Releases eligible coins reserved by failed, abandoned or expired mints, including after restart.\ + **Why:** Unsuccessful mints should not leave spendable DGB reserved forever. + +23. **Change:** Keeps mint keys and recovery history during cleanup and checks whether the mint can still confirm.\ + **Why:** A later confirmation or chain change can make that history necessary again. + +24. **Change:** Labels expired, abandoned and conflicted mints separately from completed redemptions.\ + **Why:** Users need to distinguish a failed mint from a closed vault. + +25. **Change:** Saves affected transaction states together when abandoning a transaction, and reports failed saves.\ + **Why:** An incomplete save must not look successful or release coins incorrectly. + +26. **Change:** Coordinates simultaneous mint and redemption requests so they do not select the same coins.\ + **Why:** One request should not conflict with another before either has finished. + +27. **Change:** Calculates redemption fees from the transaction being built and selects more fee coins when needed.\ + **Why:** Many small DGB coins can make a transaction larger than a fixed estimate. + +28. **Change:** Explains when small DGB coins cost more in fees than they can contribute.\ + **Why:** Users need to know when to combine small coins instead of retrying. + +29. **Change:** Stops a mint or send if required DGB change has no usable destination.\ + **Why:** Leftover funds must not go to an address invented by the builder. + +30. **Change:** Checks the redemption address and keeps leftover fee money in the sending wallet.\ + **Why:** Collateral may go to a chosen external address; fee change should stay in the wallet. + +31. **Change:** Adds explicit cents or dollars to four amount commands and two filters, with checks for invalid amounts.\ + **Why:** A decimal point must not silently change the amount being requested. + +32. **Change:** Rejects sends, requested redemptions and redemption estimates above $100,000; sendmany keeps its per-recipient cap.\ + **Why:** The limit should be checked early and match between estimates and submitted requests. + +33. **Change:** Prevents large amounts from overflowing wallet totals, displayed collateral ratios and collateral diagnostic calculations.\ + **Why:** Overflow can turn a large number into a misleading small or negative number. + +34. **Change:** Explains missing minting support before confirmation or passphrase prompts.\ + **Why:** Users should know their wallet cannot mint before approving the operation. + +35. **Change:** Eight more DigiDollar wallet commands wait for the wallet to process the latest block.\ + **Why:** A newly confirmed balance or transaction should not be missed. + +36. **Change:** Mint and redemption estimates follow the next block's rules and explain unavailable or restricted conditions.\ + **Why:** The estimate should use the same rules as the transaction builder. + +37. **Change:** Loads compatible saved fee estimates correctly after restart.\ + **Why:** The node should keep the useful fee history it already learned. + +### Desktop wallet displays and buttons + +38. **Change:** The Redeem button on a locked wallet opens the passphrase prompt.\ + **Why:** An owner needs to unlock the wallet to redeem; wallets without private keys still cannot sign. + +39. **Change:** Shows redemption results and mint refusals in dialogs.\ + **Why:** Important messages must be visible even without desktop notifications. + +40. **Change:** Redemption success messages show the transaction ID and explain that collateral returns after confirmation.\ + **Why:** Sending a transaction is different from confirming it. + +41. **Change:** Shows wallet-created mints with separate rows for DD created, DGB locked and the network fee.\ + **Why:** A mint should not look like money sent out and back, or count its fee as collateral. + +42. **Change:** Shows the DGB fee for a DigiDollar transfer on its own row.\ + **Why:** A DGB fee should not disappear into a dollar amount. + +43. **Change:** Gives DGB and DigiDollar separate columns in transaction tables and CSV exports.\ + **Why:** Each currency needs its own value for sorting and exporting. + +44. **Change:** Uses the correct currency for copied amounts, overview entries and notifications.\ + **Why:** A DigiDollar payment should not appear as zero DGB. + +45. **Change:** Keeps DigiDollar rows visible when the DGB minimum-amount filter is used.\ + **Why:** A filter for DGB amounts should not hide dollar transactions. + +46. **Change:** Restores saved column widths after loading the table and keeps the DD column visible.\ + **Why:** Opening the wallet should preserve the layout and show both currencies. + +47. **Change:** Transaction details show DD amounts, vault information, lock periods and the actual mint collateral, even with reordered outputs.\ + **Why:** Users need the actual transaction details instead of misleading zero amounts. + +48. **Change:** Gives returned DD its own row, labelled “DigiDollar change returned,” with an explanation.\ + **Why:** Extra selected DD comes back as change; the vault still closes in full. + +49. **Change:** Keeps showing the confirmation count after a DigiDollar transaction is confirmed.\ + **Why:** The count remains useful after the first few confirmations. + +50. **Change:** Keeps amounts typed above the send limit and explains why they cannot be sent.\ + **Why:** Dropping the final digit could leave a smaller amount than intended. + +51. **Change:** Allows full DigiDollar addresses to be typed and corrected in the send form.\ + **Why:** The old partial-address cutoff could prevent entry of a valid address. + +### Oracle price services + +52. **Change:** Fixes an alternate way of combining oracle public keys and keeps cached results tied to the right network settings.\ + **Why:** Price verification must use the intended keys and settings. + +53. **Change:** Starts oracle services before other tasks use them and waits for those tasks before destroying the services.\ + **Why:** Background work must not use a service that is not ready or has already stopped. + +54. **Change:** Retries stored public signing messages when the first send had no connected peer.\ + **Why:** Signing should recover when a connection becomes available. + +55. **Change:** Protects oracle settings and the current signing period from conflicting reads and changes.\ + **Why:** Tasks running together must not read settings while another task changes them. + +56. **Change:** Shows oracle operator names from the selected network's list.\ + **Why:** Operators should be easy to recognize without changing their keys or signing rules. + +### RAM and disk use + +The block index is the node's directory of known blocks. A header is a block's short summary. + +57. **Change:** Removes eight mining lookup pointers from each block's memory record.\ + **Why:** Finding earlier blocks through existing links saves permanent RAM. + +58. **Change:** Reads less-used saved block details from disk and keeps a limited set of recent reads in RAM.\ + **Why:** Every block no longer needs those details in memory all the time. + +59. **Change:** Stores mining work scores in less space, with an exact full-size value when needed.\ + **Why:** Saving RAM must not round the score used to choose a chain. + +60. **Change:** Gives block records space in shared memory chunks and removes unnecessary lookup bookkeeping.\ + **Why:** Millions of separate allocations waste space. + +61. **Change:** Reads needed parts of database files while still checking for incomplete or damaged data.\ + **Why:** The node can avoid whole-file memory mappings without dropping those checks. + +62. **Change:** Reserves open-file capacity for databases before assigning it to network connections.\ + **Why:** Database and network work need to share the system's file limit. + +63. **Change:** Uses a smaller sorted copy of changed coins during accounting scans.\ + **Why:** The scan needs a stable copy, but not the previous extra lookup table. + +64. **Change:** Keeps unsaved headers in RAM and saves block and reversal data before recording their disk locations.\ + **Why:** A failed save must not lose pending data or point to an unfinished file write. + +65. **Change:** Handles missing or unreadable local block headers as storage errors.\ + **Why:** A local disk failure must not be blamed on another node's block. + +### Startup, recovery and mining + +66. **Change:** Loads the block directory without first reading it only to count its entries.\ + **Why:** Startup can begin loading straight away and report the count as it goes. + +67. **Change:** Reads the last saved block before building lists of possible chain ends.\ + **Why:** Startup avoids making a large list it would immediately discard. + +68. **Change:** Avoids repeated warning calculations for old periods that cannot trigger the warning.\ + **Why:** Startup does less work while keeping checks at the boundary and afterward. + +69. **Change:** Reuses processed oracle price records and avoids repeating completed startup work.\ + **Why:** The same records should not need repeated parsing and setup. + +70. **Change:** Shows reconstruction progress, allows cancellation and keeps incomplete results marked as not ready.\ + **Why:** Users need to see progress without mistaking a partial scan for a finished one. + +71. **Change:** Stops startup with specific recovery information when required history cannot be read.\ + **Why:** Repeated restarts cannot replace missing block or reversal data. + +72. **Change:** Keeps the required DigiDollar history through chain changes, including when no blocks can yet be pruned.\ + **Why:** Deleting old files must not remove records still needed to check transactions. + +73. **Change:** Prepares starting Thaw Day totals when the preceding block is added, rather than for every mining request.\ + **Why:** Miners should not repeatedly scan all coins while asking for work. + +### Logs, tests and instructions + +74. **Change:** Moves routine DigiDollar, oracle and wallet messages into debug logs and removes three repeated transaction messages.\ + **Why:** Normal logs should stay readable while retaining errors and startup progress. + +75. **Change:** Stops reporting a false key-write error when re-saving the same key during a payment to the same wallet.\ + **Why:** A successful payment should not look like a failed save. + +76. **Change:** Documents the DD fields returned by raw-transaction commands so their result checks also work.\ + **Why:** Command help and automated checks need to match the actual output. + +77. **Change:** Lets source-reading tests find the checkout from a different working directory.\ + **Why:** The directory used to launch a test should not cause a false failure. + +78. **Change:** Corrects test setup for pruning, low-numbered ports, fees, script rules and Thaw Day.\ + **Why:** Tests must use the intended network rules and data. + +79. **Change:** Captures and checks errors from tests that deliberately cause storage failures.\ + **Why:** Successful error tests should not print unexplained fatal-error messages. + +80. **Change:** Adds checks for activation, accounting, extra burns, restarts, chain changes, wallet recovery, oracle retries and memory use.\ + **Why:** These tests help catch future changes that break the repairs. + +81. **Change:** Fixes the supplied configuration example and refreshes command help and manuals.\ + **Why:** The example must load correctly and describe the available settings. + +82. **Change:** Updates node, wallet, exchange and oracle instructions for upgrades, recovery, amounts and accounting.\ + **Why:** Operators need instructions for the behavior added in this release. + +83. **Change:** Adds setup steps for nodes sharing a router and for block-filter services used by lightweight wallets.\ + **Why:** Correct ports and service settings help those setups work; this does not claim a mobile-wallet code fix. + +## Tests completed and work still ahead + +These are the recorded results for source commit +`d2097819f260f4d82409643fe9f7263cdd7e3eaa`: + +| Check | Result | +| --- | --- | +| C++ unit tests | 3,739 passed | +| Extended functional tests | 394 passed, 17 skipped, none failed | +| Desktop wallet tests | Passed | +| Fuzz tests | Saved inputs tested across 255 public targets; 28 longer runs passed | +| Extra script and fee-estimator fuzz tests | Passed | + +Unit tests check pieces of the program. Functional tests run nodes and check +how they behave. Fuzz tests try many inputs to find unexpected behavior. +A skipped test is not a pass. + +One RC1 desktop run settled at about **3.65–3.67 GiB of RAM** and started in +**108 seconds**. These figures describe that workload. They do not predict +startup on every computer or the cost of the future Thaw Day accounting scan. + +Before the final mainnet release, complete the full mainnet history replay, +public testnet activation and observation, remaining platform checks and final +release review. + +Some reports about mobile wallets, pools and outside services still need those +projects' investigation. Mining difficulty, block rewards and the $1 DD output +minimum are unchanged by this release. + +## Reporting a problem + +Include the version, network, block height and hash, what you expected, what +happened and relevant log lines. Remove private information from logs first. +Report security-sensitive details through the project's private security +channel, rather than a public bug report. diff --git a/REPO_MAP.md b/REPO_MAP.md index f3fc44e61d2..48155f12138 100644 --- a/REPO_MAP.md +++ b/REPO_MAP.md @@ -433,8 +433,7 @@ - `CalculateNextWorkRequired()` → core difficulty calculation: adjusts target based on actual vs expected timespan - `InitialDifficulty()` → returns genesis difficulty target for each algo - `CheckProofOfWork()` → validates that a block hash meets the required difficulty target -- `GetLastBlockIndexForAlgo()` → walks chain backwards to find previous block using same mining algorithm -- `GetLastBlockIndexForAlgoFast()` → optimized version using cached algo data +- `GetLastBlockIndexForAlgo()` → walks chain backwards to find previous block using same mining algorithm. This is the only per-algorithm lookup; the second, cache-backed version was removed to save memory - `GetPoWAlgoHash()` → hashes block header using the correct algorithm (SHA256d, Scrypt, Groestl, Skein, Qubit/Odocrypt) - `PermittedDifficultyTransition()` → validates difficulty change between consecutive blocks is within allowed range @@ -606,7 +605,7 @@ - `CBlockUndo` (class) → undo data for an entire block: all CTxUndo entries (excluding coinbase) ### src/validation.cpp / .h -- ⚠️ ~7060 lines. DigiDollar/oracle-aware: activation gating via `DigiDollar::IsDigiDollarEnabled`, `Consensus::IsOracleActive`, MuSig2 v0x03 bundle extraction in `ConnectBlock` (~lines 3099-3108), `SCRIPT_VERIFY_DIGIDOLLAR` flag set when `DEPLOYMENT_DIGIDOLLAR` is active (`GetBlockScriptFlags` at line 2755, flag set at lines 2795-2798), and incremental DD supply tracking via `DigiDollar::SystemHealthMonitor::OnMint{Connected,Disconnected}` / `OnRedeem{Connected,Disconnected}`. +- ⚠️ ~7,680 lines. DigiDollar/oracle-aware: activation gating via `DigiDollar::IsDigiDollarEnabled`, `Consensus::IsOracleActive`, MuSig2 v0x03 bundle extraction in `ConnectBlock` (`src/validation.cpp:3402-3440`), `SCRIPT_VERIFY_DIGIDOLLAR` flag set when `DEPLOYMENT_DIGIDOLLAR` is active (`GetBlockScriptFlags` at `src/validation.cpp:3030`, flag set at 3071-3072), and incremental DD supply tracking via `DigiDollar::SystemHealthMonitor::OnMint{Connected,Disconnected}` / `OnRedeem{Connected,Disconnected}`. - `Chainstate` (class) → manages a single validated chain state (UTXO set + block index) - `ActivateBestChain()` → selects and activates the best valid chain tip, connecting new blocks - `ConnectTip()` → connects a single new block to the chain tip, executing all transactions diff --git a/REPO_MAP_DIGIDOLLAR.md b/REPO_MAP_DIGIDOLLAR.md index ed9fddd7c43..22a08901cf5 100644 --- a/REPO_MAP_DIGIDOLLAR.md +++ b/REPO_MAP_DIGIDOLLAR.md @@ -23,6 +23,22 @@ This is the granular file index for all DigiDollar and Oracle source code. Read ## DigiDollar Core +### src/digidollar/amount.h +- The one place that turns an amount typed by an RPC caller into integer cents. No floating point, no guessing. +- `DDAmountUnit` (enum class) → `UNSPECIFIED` (only a whole number of cents is accepted), `CENTS`, `DOLLARS` +- `DDAmountError` (enum class) → `NONE`, `NOT_A_NUMBER`, `AMBIGUOUS_UNIT`, `CENTS_NOT_INTEGRAL`, `TOO_MANY_DECIMALS`, `NEGATIVE`, `TOO_LARGE` +- `DDAmountParseResult` (struct) → `error`, `cents`, plain-English `message`, `ok()` +- `MAX_DD_RPC_AMOUNT_CENTS` → 10,000,000 cents ($100,000.00): the most a single `senddigidollar` request, a single `sendmanydigidollar` recipient, or a `redeemdigidollar` principal may name. It bounds typed amounts only; a wallet-computed emergency-redemption burn is not capped by it. +- `DD_AMOUNT_NO_CAP` → pass as `max_cents` to accept anything that fits in a `CAmount` +- `DD_AMOUNT_AMBIGUOUS_MESSAGE` → the exact refusal text for a decimal amount given with no unit +- `ParseDDAmountUnit(name)` → `"cents"` or `"dollars"`, otherwise empty +- `DDAmountUnitName(unit)` → `"unspecified"`, `"cents"` or `"dollars"` +- `ParseDDAmount(text, unit, max_cents)` → the parser itself +- `FormatDDAmountDollars(cents)` / `FormatDDAmountCents(cents)` → display helpers + +### src/digidollar/amount.cpp +- Implementation of the amount parser and formatters + ### src/digidollar/digidollar.h - `MAX_DIGIDOLLAR` → `21000000000LL * 100` (21B dollars in cents): per-output serialization bound only. DigiDollar has no global supply cap; total circulating DD is constrained by available DGB collateral, per-block minting rate, and the alerting threshold `AlertThresholds::ALERT_DD_SUPPLY` (= 10000000000 cents = 100M DD) which fires monitoring alerts but does NOT block minting (commit `99b1f79480`). - `CDigiDollarOutput` (class) → represents a DigiDollar UTXO with Taproot-based redemption paths @@ -185,7 +201,7 @@ This is the granular file index for all DigiDollar and Oracle source code. Read - `CalculateExpectedERRAdjustment(systemHealth)` → delegates to ERR system ### src/digidollar/validation.cpp -- Full implementation (~3,026 lines) +- Full implementation (~3,274 lines) - `ExtractDDAmountFromTxRef()` (static) → shared helper: parses OP_RETURN with type-aware field extraction (MINT: only first value is DD amount; TRANSFER: all values are DD amounts); SECURITY: verifies source tx has DD marker to prevent DD-from-nothing attacks - `ExtractDDAmountFromBlockDb()` (static) → universal fallback: loads creating tx from block database using coin height - `ValidationCache` (struct) → thread-safe cache for script types and amounts (max 10k entries) @@ -293,6 +309,14 @@ This is the granular file index for all DigiDollar and Oracle source code. Read ### src/consensus/volatility.cpp - Implementation of volatility monitoring with sliding window calculations +### src/consensus/digidollar_state.h +- `DigiDollar::ChainstateHealth` (struct) → the health inputs that belong to exactly one UTXO set state, stored with the chainstate so a restart does not have to rebuild them + - Fields: `format_version`, `genesis_hash`, `rules_version`, `activation_height`, `digidollar_height`, `best_block`, `open_vault_principal`, `collateral`, `active_vaults`, `history_checked` + - `IsValid()` → versions, heights and amounts all in range and self-consistent + - `Matches(genesis, block)` / `Matches(genesis, block, activation, dd_activation)` → the record belongs to this chain, this block and these activation heights + - `AddVault(principal, value)` / `RemoveVault(principal, value)` → checked arithmetic; a failure leaves the record unchanged + - A missing record is not the same as a verified system with no open vaults. Only validating the activated history may set `history_checked`. + ### src/consensus/digidollar_transaction_validation.h - **Mint Validation:** - `ValidateMintAmount(amount, ddParams)` → validates against consensus min/max @@ -355,11 +379,11 @@ This is the granular file index for all DigiDollar and Oracle source code. Read - `GetLatestPrice()` → returns most recent consensus price - `UpdateCachedPrice(epoch)` → refreshes price cache - **Validation:** - - `ValidateMuSig2Bundle(bundle, block_height, params, error)` → static: V1 validator; checks bitmap parses, participants ≥ `nOracleConsensusRequired`, members ∈ [0, `nOraclePubkeyCount`), runs `MuSig2OracleAggregator::ComputeAggregatePubkeyFromBitmap`, and BIP-340-verifies the aggregate signature against `ComputeOracleBundleHash(bundle)` (`src/oracle/bundle_manager.cpp:2430`) + - `ValidateMuSig2Bundle(bundle, block_height, params, error)` → static: V1 validator; checks bitmap parses, participants ≥ `nOracleConsensusRequired`, members ∈ [0, `nOraclePubkeyCount`), runs `MuSig2OracleAggregator::ComputeAggregatePubkeyFromBitmap`, and BIP-340-verifies the aggregate signature against `ComputeOracleBundleHash(bundle)` (`src/oracle/bundle_manager.cpp:2468`) - `ValidateBundle(bundle, height, params)` → static: thin wrapper around `HasMuSig2Quorum` (used by tests/RPC) - `GetRequiredConsensus(height, params)` → returns `nOracleConsensusRequired` - - `CalculateConsensusPrice(bundle, params)` → static: IQR-filtered median over the off-chain attestations; price-range checks only (no wall-clock dependence) so consensus is deterministic during IBD/replay - - `HasMuSig2Quorum(bundle, params)` (free function in `bundle_manager.cpp:85`) → checks that a v0x03 bundle is complete (signature length = 64, bitmap parses, ≥ `nOracleConsensusRequired` participants) + - `CalculateConsensusPrice(bundle, params)` → static: IQR-filtered median over the off-chain attestations; price-range checks only (no wall-clock dependence) so consensus is deterministic during initial sync and reindex + - `HasMuSig2Quorum(bundle, params)` (free function in `src/oracle/bundle_manager.cpp:86`) → checks that a v0x03 bundle is complete (signature length = 64, bitmap parses, ≥ `nOracleConsensusRequired` participants) - **Network:** - `BroadcastMessage(message)` → broadcasts via P2P - `ProcessIncomingMessage(message)` → handles incoming P2P oracle message @@ -384,7 +408,7 @@ This is the granular file index for all DigiDollar and Oracle source code. Read - `GetOraclePriceForHeight(height)` → retrieves cached price for height - `RemovePriceCache(height)` → removes price during block disconnect - `OracleDataValidator` (class) → validates oracle data in blocks and transactions - - `ValidateBlockOracleData(block, pindex_prev, params, state)` → V1 entry point. Returns true pre-activation; otherwise requires DD mint/redeem blocks to carry exactly one valid v0x03 bundle (`bad-oracle-missing`, `bad-oracle-multiple-outputs`, `bad-oracle-malformed`, `bad-oracle-legacy`, `bad-oracle-musig2`, `bad-oracle-timestamp`). DD transfer-only and non-DD blocks may omit the bundle. Implemented at `src/oracle/bundle_manager.cpp:2151`. + - `ValidateBlockOracleData(block, pindex_prev, params, state)` → V1 entry point. Returns true pre-activation; otherwise requires DD mint/redeem blocks to carry exactly one valid v0x03 bundle (`bad-oracle-missing`, `bad-oracle-multiple-outputs`, `bad-oracle-malformed`, `bad-oracle-legacy`, `bad-oracle-musig2`, `bad-oracle-timestamp`). DD transfer-only and non-DD blocks may omit the bundle. Implemented at `src/oracle/bundle_manager.cpp:2187`. - `ValidateOraclePriceForTx(tx, oracle_price, height)` → sanity-check the oracle price feeding a DD tx (range and non-zero) - `ValidateOracleMessage(message, params)` → checks `message.IsValid()` plus chainparams authorization and `VerifyAttestation()` - `ValidateOracleBundle(bundle, height, params)` → wraps `OracleBundleManager::ValidateMuSig2Bundle`; rejects non-MuSig2 bundles with `bad-oracle-legacy`-style logging @@ -453,7 +477,7 @@ This is the granular file index for all DigiDollar and Oracle source code. Read - `Reset()` → restores default state ### src/oracle/mock_oracle.cpp -- Deterministic regtest helper. Test keys derived from `SHA256("digibyte_regtest_oracle_N")` (N=0..6); matching pubkeys are pushed into `consensus.vOraclePublicKeys` in `chainparams.cpp:1249-1255`. +- Deterministic regtest helper. Test keys derived from `SHA256("digibyte_regtest_oracle_N")` (N=0..6); matching pubkeys are pushed into `consensus.vOraclePublicKeys` in `src/kernel/chainparams.cpp:1309-1315`. - Builds bundles that satisfy the regtest 4-of-7 quorum (`consensus.nOracleConsensusRequired`). - Used only by regtest. Production `OP_CHECKPRICE` is reserved and deterministically disabled; it never falls back to `MockOracleManager` or live node-local oracle state. @@ -563,7 +587,7 @@ This is the granular file index for all DigiDollar and Oracle source code. Read - `HasConsensus(min_required)` → checks if ≥ min_required valid messages exist - `GetConsensusPrice(min_required)` → calculates median price from valid messages - `ValidateEpoch(current_epoch)` → checks epoch consistency -- `OracleNodeInfo` (struct) → oracle node definition: id, pubkey, endpoint, is_active +- `OracleNodeInfo` (struct) → oracle node definition: id, pubkey, endpoint, is_active; display_name is local roster metadata excluded from serialization - `SelectOraclesForEpoch(all_oracles, epoch)` → deterministic selection of active `OracleNodeInfo` entries across the configured 35-slot roster - `GetCurrentEpoch(block_height)` → calculates epoch from block height - `OracleP2P` (namespace) → unit-testable P2P validation helpers; production relay admission, per-peer rate limiting, stale-epoch rejection, and dedup live in `src/net_processing.cpp` @@ -626,6 +650,12 @@ This is the granular file index for all DigiDollar and Oracle source code. Read ## Wallet Integration +### src/wallet/digidollarmintcapability.h +- `wallet::GetDigiDollarMintWalletError(wallet)` → checks mint wallet support without allocating keys or requiring unlock; descriptor/private-key/HD support, Taproot receiving and bech32 change descriptors are required. Actual owner-key derivation and signing must still succeed. + +### src/wallet/digidollarmintconsolidation.h +- Shared Qt/RPC helper for combining confirmed DGB coins before a mint. Returns the merge transaction IDs and any partial failure. Existing wallet transaction metadata prevents repeated merges while confirmation is pending; known failed attempts use the normal abandonment path. It does not change transaction or consensus limits. + ### src/wallet/digidollarwallet.h - `DDTransaction` (struct) → wallet-facing DD transaction: txid, amount, timestamp, confirmations, incoming, address, category (send/receive/mint/redeem), blockheight, blockhash, fee, comment, abandoned, lock_tier - `WalletDDBalance` (struct) → address → balance mapping with last_updated timestamp @@ -823,7 +853,10 @@ Files outside the DigiDollar/Oracle directories that contain DD integration code - ⚠️ `DEPLOYMENT_DIGIDOLLAR` buried-deployment name registration (`DeploymentName(BuriedDeployment)` + `GetBuriedDeployment()` for `-testactivationheight`); removed from `VersionBitsDeploymentInfo[]` in the v9.26.5 burial ### src/core_write.cpp -- ⚠️ DD-aware transaction serialization for `decoderawtransaction` RPC output +- ⚠️ `TxToUniv()` adds a `digidollar` object (`type`, `type_id`, `flags`, `flags_hex`) right after the lock time for any transaction carrying the DigiDollar marker (`src/core_write.cpp:186-193`) + +### src/rpc/rawtransaction.cpp +- ⚠️ `DecodeTxDoc()` declares that optional `digidollar` object in the result of `decoderawtransaction` and `getrawtransaction` (`src/rpc/rawtransaction.cpp:107-113`). Before v9.26.6 the field was returned but not declared, so a node started with `-rpcdoccheck` raised an internal error on those calls. ### src/common/args.cpp - ⚠️ `OptionsCategory::DIGIDOLLAR` category definition for CLI args @@ -886,6 +919,9 @@ Files outside the DigiDollar/Oracle directories that contain DD integration code ## Qt GUI +### src/qt/walletmodel.cpp/h +- `WalletModel::getDigiDollarMintWalletError()` → exposes the shared read-only mint capability check to Qt; the mint widget calls it before confirmation and construction checks it again. + ### src/qt/digidollartab.cpp/h - `DigiDollarTab` → main DD tab widget containing the activation overlay and 7 tabs: DD Overview, Send DD, Receive DD, Mint DD, Redeem DD, DD Vault, DD Transactions @@ -1077,6 +1113,23 @@ present in the tree but not compiled into the current unit-test binary. | `musig2_bundle_format_tests.cpp` | MuSig2 bundle serialization format and version handling | | `musig2_bundle_manager_tests.cpp` | MuSig2 bundle manager integration with signing sessions | | `musig2_bundle_mining_tests.cpp` | MuSig2 bundle embedding in coinbase during mining | +| `digidollar_amount_tests.cpp` | Every branch of the RPC amount parser: integer cents with no unit, explicit cents, explicit dollars to two decimals, the refusal of a decimal with no unit, syntax and size limits | +| `digidollar_thawday_tests.cpp` | `IsThawDayActive`: not scheduled is false everywhere, scheduled flips exactly at the height, waits for DigiDollar, negative heights, the regtest knob and its rejection on public networks | +| `digidollar_thawday_chainparams_tests.cpp` | Public Thaw Day schedule and unscheduled defaults; `-ddthawdayheight` sets the regtest height exactly and rejects out-of-range values; DigiDollar activation heights are untouched | +| `digidollar_thawday_selection_tests.cpp` | Which height each caller passes to the Thaw Day predicate, and that the predicate does no arithmetic on the scheduled height | +| `digidollar_thaw_recovery_tests.cpp` | Startup repair of saved DigiDollar totals: independent recheck, progress counted only over activated history, cancellation leaving a resumable prefix | +| `digidollar_thaw_volatility_tests.cpp` | Mint volatility reference window at and above Thaw Day: deviation maths, window endpoints, empty/short/missing cases, recovery as new bundle blocks arrive | +| `digidollar_accounting_transition_tests.cpp` | Vault principal and token supply move separately: ordinary redemption, emergency excess burn with change, burn without closing a vault, exact reverse-order undo | +| `digidollar_chainstate_tests.cpp` | `ChainstateHealth` arithmetic and its exact inverse, nested views, flush failure handling, canonical health from original principal | +| `digidollar_chainstate_db_tests.cpp` | Saved health survives close and reopen; transition markers, absent and invalid records, malformed coin keys never publish a partial rebuild | +| `digidollar_chainstate_validation_tests.cpp` | Candidate-block health stays private until the block connects; a later failure discards an earlier staged mint; independent chainstates keep separate health | +| `digidollar_oracle_startup_tests.cpp` | Startup oracle price-cache scan (`LoadPricesFromChain` / `ShouldLoadStartupOraclePriceForBlock`) uses the same activation predicate as block connect | +| `digidollar_rpc_totals_cache_tests.cpp` | `getdigidollarstats` totals follow new tips and reorgs, require current canonical readiness, and a cancelled rebuild can be retried | +| `digidollar_stats_migration_tests.cpp` | DigiDollar stats index rebuild from retained history, refusal to reuse rows from an older schema, resumable cancellation | +| `digidollar_txbuilder_change_tests.cpp` | Mint and transfer refuse to build without a change address, pay change to the given bech32 address, and refuse a Taproot or empty one | +| `digidollar_wallet_candidate_lock_tests.cpp` | Mint and redemption release the chain lock before wallet coin selection, so the two locks are never held in the wrong order | +| `digidollar_qt_translate_tests.cpp` | Consensus reject reasons shown by the Qt mint flow are translated into plain English instead of raw strings like `minting-blocked-during-err` | +| `oracle_params_aggregation_tests.cpp` | Aggregation over a configured roster: active overloads match explicit copies, cache separates key parity and threshold, full-byte oracle IDs are not narrowed | ### Wallet Tests (`src/wallet/test/`) @@ -1085,6 +1138,10 @@ present in the tree but not compiled into the current unit-test binary. | `digidollar_persistence_wallet_tests.cpp` | Full wallet DD persistence: balances, positions, transactions, keys across restart | | `digidollar_wallet_security_tests.cpp` | Wallet-level DD security: key protection, unauthorized access, encryption boundaries | | `rh59_coincontrol_dd_lock_bypass_tests.cpp` | RH-59: coin-control / lockunspent bypass on preset DD inputs (W7; partially reverted in `ce0abf4e3a`) | +| `digidollar_wallet_lock_safety_tests.cpp` | The wallet never asks the chain a question while holding its own lock: UTXO rebuild, abandon, expired-mint release and rescan | +| `digidollar_wave16_persistence_tests.cpp` | Restart restores positions, DD UTXOs and owner/address keys; a crash after broadcast keeps the owner key on disk; a locked encrypted wallet refuses DD key decryption | +| `digidollar_wave17_helper_asymmetry_tests.cpp` | The wallet's lock-tier helper agrees with the RPC helper and with the canonical tiers, tier 0 included | +| `digidollar_wave17_spendability_tests.cpp` | DD coin selection and the selected-input planner: minimum change, dust avoidance, unknown, wrong-wallet, unconfirmed and spent inputs | ### Qt Tests (`src/qt/test/`) @@ -1092,13 +1149,17 @@ present in the tree but not compiled into the current unit-test binary. |------|--------------| | `digidollarwidgettests.cpp/h` | Qt widget unit tests for DD UI components | | `digidollarwave19widgettests.cpp/h` | Wave 19 Qt unit/signal-slot pins for the release-critical DD UX surface (mint tier dropdown, etc.) | +| `digidollarmintrecordtests.cpp/h` | A mint started from the wallet window saves its record before the transaction is sent, and does not send at all if the wallet cannot save it | ### Python Functional Tests (`test/functional/`) -Authoritative registration is `test/functional/test_runner.py:261-340`. -As of the Wave 23 rerun the standard runner contains 80 DD/oracle/wallet -functional entries. `feature_oracle_p2p.py` remains registered for historical -compatibility but is a legacy/superseded scaffold; the live oracle P2P proof is +Authoritative registration is `test/functional/test_runner.py:266-368`, the +block headed `# DigiDollar tests`. That block holds 102 entries; two more DD +entries sit outside it, `digidollar_listunspent.py --descriptors` near the top +of `BASE_SCRIPTS` and `feature_digidollar_zero_floor_pruning.py` among the +pruning tests, for 104 registered DD/oracle/wallet entries over 103 distinct +files. `feature_oracle_p2p.py` remains registered for historical compatibility +but is a legacy/superseded scaffold; the live oracle P2P proof is `digidollar_wave20_oracle_p2p.py`. | File | Coverage Area | @@ -1185,14 +1246,35 @@ compatibility but is a legacy/superseded scaffold; the live oracle P2P proof is | `wallet_digidollar_transfer_ancestor_reorg.py` | DD transfer ancestor reorg replay | | `wallet_digidollar_transfer_reorg.py` | DD transfer reorg replay | | `wallet_digidollar_wave16_load_rescan.py` | Wave 16 wallet load/rescan persistence coverage | +| `digidollar_rpc_amount_units.py` | One explicit `amount_unit` contract across the amount RPCs: integer cents by default, `dollars` for decimals, a decimal with no unit refused | +| `digidollar_rpc_amount_cap.py` | `senddigidollar` and `redeemdigidollar` refuse amounts above $100,000 at the RPC boundary, under either unit | +| `digidollar_mint_persistence.py --descriptors` | A mint is written to the wallet before the transaction is sent, so a node that stops in between leaves nothing unredeemable | +| `digidollar_mint_change_destination.py` | Leftover DGB from a mint, send or redeem comes back to an address the wallet owns | +| `digidollar_owner_key_recovery.py --descriptors` | Redeeming finds a missing owner key again among the wallet's own keys | +| `digidollar_expired_mint.py --descriptors` | A mint that can no longer be mined stops reserving DGB | +| `digidollar_balance_sync.py` | `getdigidollarbalance` waits for the wallet to catch up with the chain before calling coins confirmed | +| `digidollar_thawday_height.py` | How a node reports the Thaw Day height through `getdigidollardeploymentinfo.thaw_day` (regtest `-ddthawdayheight`) | +| `digidollar_thawday_integration.py` | Ordinary DigiDollar activity, accounting and upgrade across the Thaw Day boundary | +| `digidollar_thawday_sync_state.py` | From Thaw Day on, a block gets the same answer whether the node thinks it is still downloading or caught up | +| `digidollar_thawday_fresh_sync.py` | Fresh nodes and interrupted database writes keep DigiDollar accounting intact | +| `digidollar_health_restart_consensus.py --legacy` | Exact health totals and emergency mint restrictions survive a restart | +| `digidollar_oracle_startup_consensus.py` | The startup oracle price scan uses the same activation gate as block connect, so it is consensus-neutral | +| `digidollar_oracle_gbt_stale_cache.py` | `getblocktemplate` stops serving a stale oracle-bearing template | +| `digidollar_gbt_optin.py` | Mining a DD mint or redeem requires an explicit `getblocktemplate` opt-in | +| `digidollar_oracle_signers.py` | `getoraclesigners` reports the oracle IDs decoded from the v0x03 participation bitmap | +| `digidollar_listoracle_schema.py` | `listoracle` output shape while a local oracle is running | +| `digidollar_testnet26_oracle_roster_rpc.py` | Local mini-testnet roster RPC behaviour: testnet26 identity with 24 deterministic local oracle keys | +| `digidollar_isstandardtx_no_log.py` | `IsStandardTx` writes no unconditional log line for every transaction | +| `feature_digidollar_pruning.py` | A pruned node runs DigiDollar: history at and above the activation floor is retained, older blocks are not | +| `feature_digidollar_zero_floor_pruning.py` | DD history is kept from height zero while ordinary DGB history can still be pruned | ### Fuzz Targets (`src/test/fuzz/`) -Wave 23 registered 247 total fuzz targets in the active fuzz binary. Of those, -52 target names currently match DigiDollar/oracle/MuSig2/DD surfaces. The tree -contains additional DD/oracle fuzz source files and helpers; authoritative -target registration is the `PRINT_ALL_FUZZ_TARGETS_AND_ABORT=1` output from -`src/test/fuzz/fuzz` plus `src/Makefile.test.include`. +Counting the `FUZZ_TARGET` registrations in `src/test/fuzz/*.cpp` gives 253 +distinct target names, of which 59 are DigiDollar, oracle, MuSig2 or DD +targets. That count is from the source, not from a run: the authoritative list +is the `PRINT_ALL_FUZZ_TARGETS_AND_ABORT=1` output of `src/test/fuzz/fuzz` +together with `src/Makefile.test.include`. Current DigiDollar-specific fuzz source inventory: @@ -1233,3 +1315,6 @@ Current oracle/MuSig2 fuzz source inventory: | `oracle_price_message.cpp` | `COraclePriceMessage` parsing, signing, and validation | | `oracle_script_parsing.cpp` | OP_ORACLE script parsing and malformed payload handling | | `oracle_validate_block_data.cpp` | `ValidateBlockOracleData` block-level oracle reject paths | +| `digidollar_amount.cpp` | RPC amount parser: never throws, never guesses a unit, and reports every refusal through `DDAmountParseResult` | +| `digidollar_chainstate.cpp` | `ChainstateHealth` vault arithmetic and the mint volatility reference price | +| `digidollar_prune_blockdb.cpp` | Pruning surface: reading a DD amount from the block database, the activation floor, and coin gating below it | diff --git a/REPO_MAP_GUIDE.md b/REPO_MAP_GUIDE.md index 48aafeb7cea..63453c2c691 100644 --- a/REPO_MAP_GUIDE.md +++ b/REPO_MAP_GUIDE.md @@ -129,10 +129,10 @@ This catches files like `src/kernel/chainparams.cpp`, `src/primitives/transactio | Location | Approx. Count | Coverage Areas | |----------|--------------|----------------| -| `src/test/digidollar_*.cpp`, `src/test/oracle_*.cpp`, `src/test/musig2_*.cpp`, `src/test/redteam_*.cpp`, `src/test/rh*.cpp` | ~150 files | activation, address, DCA, ERR, MuSig2 nonce/partial-sig sessions, bundle creation/validation/format/mining, redteam audit (RH series), wallet integration, consensus replay, GUI/widget | -| `src/wallet/test/digidollar_*.cpp`, `src/wallet/test/rh59_*.cpp` | 6 registered files | persistence, wallet security, Wave 16/17 load-rescan/spendability/helper-asymmetry coverage, lock-bypass | -| `src/qt/test/digidollarwidgettests.cpp`, `src/qt/test/digidollarwave19widgettests.cpp` | 2 files | Qt widget behaviour and Wave 19 address/widget coverage | -| `test/functional/digidollar_*.py`, `wallet_digidollar_*.py`, `feature_oracle_*.py`, `rpc_getoracles_*.py` | 82 registered entries | activation, mint, transfer, redeem, oracle P2P, wallet restore, MuSig2 integration, mempool ordering | +| `src/test/digidollar_*.cpp`, `src/test/oracle_*.cpp`, `src/test/musig2_*.cpp`, `src/test/redteam_*.cpp`, `src/test/rh*.cpp` | ~160 files | activation, address, DCA, ERR, Thaw Day, chainstate health, amount parsing, MuSig2 nonce/partial-sig sessions, bundle creation/validation/format/mining, redteam audit (RH series), wallet integration, reindex and reorg, GUI/widget | +| `src/wallet/test/digidollar_*.cpp`, `src/wallet/test/rh59_*.cpp` | 7 registered files | persistence, wallet security, lock safety, Wave 16/17 load-rescan/spendability/helper-asymmetry coverage, lock-bypass | +| `src/qt/test/digidollarwidgettests.cpp`, `src/qt/test/digidollarwave19widgettests.cpp`, `src/qt/test/digidollarmintrecordtests.cpp` | 3 files | Qt widget behaviour, Wave 19 address/widget coverage, and saving a mint record before the transaction is sent | +| `test/functional/digidollar_*.py`, `wallet_digidollar_*.py`, `feature_digidollar_*.py`, `feature_oracle_*.py`, `rpc_getoracles_*.py` | 104 registered entries | activation, mint, transfer, redeem, amount units, Thaw Day, pruning, oracle P2P, wallet restore, MuSig2 integration, mempool ordering | ## REPO_MAP.md Format @@ -257,7 +257,9 @@ If you add, remove, or rename files or functions, update the affected REPO_MAP.m ## DigiByte-Specific Tips - **5 Mining Algorithms:** SHA-256d, Scrypt, Skein, Qubit, and either Groestl before Odo activation or Odocrypt after `OdoHeight = 9,112,320`. Difficulty adjustment is via MultiShield/DigiSpeed and parameters in `src/kernel/chainparams.cpp`. -- **DigiDollar activation:** Gated by BIP9 bit 23 (`Consensus::DEPLOYMENT_DIGIDOLLAR`). Per-network heights in `src/kernel/chainparams.cpp`: mainnet `nDDActivationHeight = 23627520`, testnet26 `= 600` (port 12033; reset genesis timestamp 1780156800), regtest BIP9 `ALWAYS_ACTIVE` with DD/oracle height gates defaulting to 650. `nDigiDollarMuSig2Height` equals `nDDActivationHeight` on mainnet and testnet26 (`chainparams.cpp:316,688`); on regtest it is `std::min(nDDActivationHeight, DEPLOYMENT_DIGIDOLLAR.min_activation_height)` = `0` while ALWAYS_ACTIVE (`chainparams.cpp:1234-1236`). MuSig2 v0x03 oracle bundles are required as soon as DigiDollar activates. -- **Oracle integration:** `src/primitives/oracle.h` defines structures, `src/oracle/` has the network/MuSig2 layer, and `src/consensus/{dca,err,volatility}.cpp` contain protection rules. The `g_get_oracle_consensus_price` hook exists in `src/script/interpreter.cpp` but is left null in production (`src/init.cpp:282`); `OP_CHECKPRICE` is deterministically disabled (DD-FINAL-005) and no longer consults it — it consumes its operand and pushes FALSE (`src/script/interpreter.cpp:708-735`). +- **DigiDollar activation:** A buried deployment since v9.26.5 (BIP90; historically BIP9 bit 23). `Consensus::Params::DigiDollarHeight` is a plain per-network height and the version-bits state machine no longer runs for it: mainnet 23,869,440, testnet26 600, signet and regtest 0 (`src/kernel/chainparams.cpp:196, 549, 1000, 1135`). Separate static gates keep their historical floors: mainnet `nDDActivationHeight = 23,627,520` (`chainparams.cpp:342`), testnet26 600 (`chainparams.cpp:721`), regtest 650 (`chainparams.cpp:1266`). Testnet26 uses port 12033 and reset genesis timestamp 1780156800. `nDigiDollarMuSig2Height` equals `nDDActivationHeight` on mainnet and testnet26 (`chainparams.cpp:351, 723`); on regtest it is `std::min(nDDActivationHeight, DigiDollarHeight)` = 0 (`chainparams.cpp:1294-1296`). MuSig2 v0x03 oracle bundles are required as soon as DigiDollar activates. Full detail is in `DIGIDOLLAR_ACTIVATION_EXPLAINER.md`. +- **Thaw Day:** A separate coordinated change with one height per network, `Consensus::Params::nDDThawDayHeight`. Mainnet is scheduled at 24,490,000, estimated for November 1, 2026. Testnet26 is scheduled at 432,100, estimated for September 18–19, 2026. The heights trigger activation. Signet is unsupported and remains unscheduled (`std::numeric_limits::max()`). Regtest can set it with `-ddthawdayheight=N`, which is a startup error elsewhere. Read it through `DigiDollar::IsThawDayActive(params, candidate_height)`. See `src/kernel/chainparams.cpp` and the rollout requirements in `DIGIDOLLAR_ACTIVATION_EXPLAINER.md`. +- **Pruning:** With `-prune` set, the node registers a prune lock named `digidollar` at `DigiDollar::EarliestActivationFloor(params)` and keeps every block from there to the tip (`src/node/chainstate.cpp:159-190`). On mainnet that floor is 23,627,520, so a small prune target cannot be reached there. +- **Oracle integration:** `src/primitives/oracle.h` defines structures, `src/oracle/` has the network/MuSig2 layer, and `src/consensus/{dca,err,volatility}.cpp` contain protection rules. The `g_get_oracle_consensus_price` hook exists in `src/script/interpreter.cpp` but is left null in production (`src/init.cpp:2283-2291`); `OP_CHECKPRICE` is deterministically disabled (DD-FINAL-005) and no longer consults it — it consumes its operand and pushes FALSE (`src/script/interpreter.cpp:708-735`). - **BIP324 V2 P2P transport:** Implemented in `src/bip324.cpp`/`src/net.cpp`; opt in with `-v2transport=1`. - **Bitcoin lineage:** Most of `src/` is inherited from Bitcoin Core v26.2. When writing descriptions, note DigiByte-specific modifications vs inherited Bitcoin code; the historical reference trees are in `digibyte-v8.22.2/` and `bitcoin-v26.2-for-digibyte/` (do **not** index those). diff --git a/SECURITY.md b/SECURITY.md index 71e3749f51a..ac14c805284 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -7,14 +7,9 @@ security updates: https://digibyte.org/en/lifecycle/#schedule ## Reporting a Vulnerability -To report security issues send an email to security@digibyte.org (not for support). +Report security issues privately by emailing security@digibyte.io or using +[GitHub's private vulnerability reporting](https://github.com/DigiByte-Core/digibyte/security/advisories/new). +These channels are for security reports, not general support. -The following keys may be used to communicate sensitive information to developers: - -| Name | Fingerprint | -|------|-------------| -| Pieter Wuille | 133E AC17 9436 F14A 5CF1 B794 860F EB80 4E66 9320 | -| Michael Ford | E777 299F C265 DD04 7930 70EB 944D 35F9 AC3D B76A | -| Andrew Chow | 1528 1230 0785 C964 44D3 334D 1756 5732 E08E 5E41 | - -You can import a key by running the following command with that individual’s fingerprint: `gpg --keyserver hkps://keys.openpgp.org --recv-keys ""` Ensure that you put quotes around fingerprints containing spaces. +Include the affected version, the expected behavior, what happened, and steps +to reproduce the problem. Do not put sensitive details in a public issue. diff --git a/THAW_DAY_TEST_GUIDE.md b/THAW_DAY_TEST_GUIDE.md new file mode 100644 index 00000000000..777e0e9eec0 --- /dev/null +++ b/THAW_DAY_TEST_GUIDE.md @@ -0,0 +1,305 @@ +# Thaw Day test guide + +`thawDay.sh` runs the same core DigiDollar checks before Thaw Day and after it +on a private network on this computer. It opens separate Qt wallets and uses +a separate test client. The main test is that the intended rules change at the +boundary while mint, send, receive, redeem, and wallet recovery still work. + +- Eight Qt nodes host eight new wallets and 24 test oracle identities. +- Local price responses pass through real parsers, signing, bundles, and validation. +- The lab activates Thaw Day at 5,000; the release's public heights stay unchanged. +- Windows and the local price server stay open by default for inspection. +- The execution section separates earlier RC2 results from final-release testing. + +## How the test works + +An oracle is a signer that helps agree on a DGB price. MuSig2 combines the chosen +signers' signatures into one. A bundle is the signed price record put in a block. +The lab supplies prices; the client must still parse, sign, include, and validate them. + +```mermaid +flowchart TD + F["One local HTTP server
Six exchange response formats"] + Q["Eight Qt nodes and separate wallets
Real exchange fetchers and parsers"] + O["24 test oracle identities
Real MuSig2 signing"] + B["Real signed oracle bundle
Included in a mined block"] + V["Normal block and DigiDollar validation
Same block hash on the test nodes"] + T["Shared core tests
Before H and after H"] + F --> Q --> O --> B --> V --> T +``` + +The server returns the JSON formats used by Binance, CoinGecko, KuCoin, Gate.io, +HTX, and Crypto.com. These are six local routes, not real exchange connections. +Its log records the routes actually requested. A feed response, signed heartbeat, +and signed price in a mined block are separate evidence. + +Bob, Alice, Charlie, Dave, Eve, Frank, Grace, and Heidi are the eight starting +wallets. The 24 oracle identities are spread across their Qt processes. A later +fresh-sync check opens a ninth Qt node with empty data and no assigned oracle. + +## The lab and release settings + +`H` is the first block height that uses the new Thaw Day rules. The private lab +uses H=5,000 and DigiDollar activation at 600 in the existing `-easypow` test mode. +The release values remain mainnet **24,490,000** and testnet26 **432,100**. +The lab's P2P network marker is `f9 dd a5 66`; it connects only to local peers. + +The separate client is in `builds/thawday-client`. The build uses the current +release commit by default. Pass a commit or tag to test a specific candidate. +Its patch changes lab network settings, startup guards/banner, and exchange URL routing. DigiDollar validation, +accounting, C1/C2/C3, exchange parsers, aggregation, signing, and signature checks +remain in place. There is no validation bypass. Easy test mining does not prove +public-network difficulty behavior. + +Startup requires testnet, `-easypow`, `THAWDAY_LAB=1`, and an exact feed URL of +`http://127.0.0.1:`. The script supplies them. Feed requests disable public +fallback, proxies, and redirects. An unavailable local server means a failed fetch. +Each node uses local RPC/P2P bindings, its own ports, disabled public discovery, +a new data folder, and private Qt preferences. The nested folder name `testnet26` +does not make these fresh private data folders public-chain data. + +Oracle keys are known test keys; wallets are new and their mined coins have no +public-chain value. Do not put real funds, keys, or public-chain data in this client. +The old script, existing wallets, release binaries, live nodes, and running +mainnet/testnet reindexes remain separate and untouched. + +## Build, run, inspect, and stop + +Use a Linux desktop with a working `DISPLAY`, Python 3, the project's Qt 5 build +dependencies, and enough free memory/disk for the small nodes. Process checks use +Linux `/proc`. Run these commands from the release checkout: + +```sh +cd /path/to/digibyte +``` + +Build the separate client only if its worktree does not already exist: + +```sh +./thawDay.sh build +# Or choose an exact candidate: +./thawDay.sh build +``` + +This creates a **fresh** `builds/thawday-client`, applies +`contrib/thawday/client.patch`, and builds daemon, CLI, and Qt with `make -j8`. +It refuses to overwrite an existing worktree. Before using an existing build, +check that `base_commit` in `builds/thawday-client/THAWDAY_BUILD.json` is the +exact commit you intend to test. The runner checks the build against its own +record; it does not require that record to match the current release checkout. +After the run, verify the same commit in its `run.json`. Keep the source commit +and hashes of the test helpers with the evidence. The build does not run the full test suites. +Keep one heavy job at a time and leave existing builds/reindexes alone. + +Check the script's price-wait logic without starting any nodes: + +```sh +python3 contrib/thawday/test_rehearsal.py +``` + +These seven small checks cover the last allowed block, changing oracle rounds, +wrong prices, and a completed quote. They do not replace the full rehearsal. + +Start a new run: + +```sh +./thawDay.sh run +``` + +The default is a new timestamp folder under `builds/thawday-runs/`. Save the exact +path printed by the script. It refuses to reuse an existing folder. To choose one: + +```sh +./thawDay.sh run --run-dir builds/thawday-runs/my-new-run +``` + +The script checks its client build record, patch, source, and binary hashes before +launch. Review any mismatch; do not edit hashes to get past the check. Wallets +are named `thaw-test-bob`, `thaw-test-alice`, and so on. Files and Qt preferences +stay inside the private run folders rather than your normal wallet/settings paths. + +By default, Qt and the price server stay open when the command ends, including +after a failed check. To close only this run's processes automatically, use: + +```sh +./thawDay.sh run --close-after +``` + +From another terminal, inspect or stop the run using its actual path: + +```sh +./thawDay.sh status --run-dir builds/thawday-runs/20260914-143000 +./thawDay.sh stop --run-dir builds/thawday-runs/20260914-143000 +``` + +Replace the example timestamp. `status` prints saved `run.json`; it does not run +a fresh health check. `stop` uses the saved process IDs, start times, and private +RPC ports. It does not stop processes by a broad name match. Evidence and test +wallets stay on disk after stopping. + +## The shared before/after checks + +The workflow follows `test_multi_oracle_testnet.sh` but does not run that script. +One shared `core_suite` runs on each side of H. The script mines to 3,000, funds +fee balances, starts the oracles, and waits for signed heartbeats before the first +group. Mining limits and actual confirmation heights keep that group below H. + +Each group confirms twelve 100-DD mints: one at each of the ten lock tiers plus +two more tier-0 vaults for later checks. It first tries an early tier-0 redemption. +For valid redemptions it waits until one block beyond the greatest **returned +unlock height** of the needed vaults, rather than assuming a fixed block count. + +Transfers run Bob → Alice → Charlie → Bob, with each balance checked. The group +rejects partial redemption, confirms a whole-vault redemption, rejects a second +spend of that vault, and exercises an extra burn. It then checks accounting and +restarts, backs up, restores, and rescans Bob and Alice's wallets. + +For the extra burn, the script builds a normal redemption and keeps it off the +network. It reduces the returned token change by 25 DD, then signs the changed +transaction again with the correct test keys. Only that replacement is sent. +Closing a 100-DD vault burns 125 DD but removes only 100 DD of open principal. +The script checks that the replacement confirms and the original cannot spend +the same vault afterward. It does this once before H and once after H. + +Bob is the miner, and the extra-burn step restarts Bob. A restarted miner starts +its oracle signing session over, and a block that carries a mint or redemption +must include a fresh signed price. So after any restart of Bob, the script only +calls the price ready once a block mined after that restart carries a signed +price. The first full rehearsal stopped here because it mined twelve blocks in +twenty seconds while Bob's signers were still starting, and Bob left the +redemption out of every block. Before the replacement is sent, an ordinary DGB +payment confirms as a control, and the script records Bob's block template to +show the replacement is in it. + +After the pre-Thaw group, Bob reindexes with its wallet loaded and must reach +the same block hash with the same DD balance. Bob's wallet holds addresses that +received change after a mint, which is the case that broke the rc1 reindex on +mainnet. Bob reindexes again at the end of the run, before Heidi. + +Circulating DD means issuance minus the DD actually burned. Open-vault principal +means the original DD minted against vaults still open. An extra burn reduces +circulation by more than the principal removed; those totals can validly differ. +Below H the legacy health denominator remains selected. At/after H, health uses +open-vault principal, without recreating the extra tokens burned. + +The ledger checks the saved transaction bytes and asks which token and vault +outputs remain unspent. It independently totals token amounts, original principal, +collateral, and vault count before comparing them with node reports. Post-H it +also checks the saved health record's block hash/readiness, matches records +across nodes, requires a ready candidate view, and checks its health calculation. + +## Activation, replacement branches, and price limits + +The price starts at $0.030000 per DGB, then rises to $0.045000. Prices are stored +in micro-USD, one millionth of a dollar: 30,000 and 45,000. The script checks that +a fresh signed quote still hits the old freeze, then mines signed ancestor samples. +At tip H-2 the next block is still pre-H. At tip H-1 the next block is H and uses +new rules. At H the script checks that activation is selected. It then waits +for a fresh signed quote before checking the 15-sample reference and cleared +old freeze. That wait can advance the chain beyond H. The after-H core group +must then actually confirm its eligible mints. Exact-boundary transaction +checks also run in the separate `digidollar_thawday_reference_reorg.py` test. + +When a price wait has a height limit, the script keeps the last block available +until that round finishes signing. At an oracle-round boundary, the status RPC +still describes the old round, so the script allows the new boundary block. +Signing status alone never proves success: the mined block must carry the +expected signed price and all test nodes must accept it. If the height limit is +already exhausted without that proof, the script fails instead of waiting for +an already-mined block to change. + +A reorg replaces part of the active chain with another branch. The boundary test +rewinds a follower below H, restarts it, and reconnects the original blocks. It checks +the restored UTXO MuHash, a compact digest of all unspent outputs. It then creates +a short replacement branch with more work and submits its real block bytes to +the other nodes through normal validation. Nodes must choose the replacement +branch and agree on accounting. This short reorg does **not** replace the older +price-sample ancestors and does not prove deep sample-changing reorg behavior. +It runs before the after-H transaction group, so it also does not undo that +group's mints, transfers, or redemptions. The separate Thaw Day functional +tests cover ancestor-changing reorgs and undoing transaction accounting. + +With the reference held at 45,000 micro-USD, the price checks use 54,000 and +36,000: exactly 20% above and below. Mints must reject at both edges, while an +otherwise valid send and eligible ordinary redemption must confirm. At 53,999 +and 36,001, just inside those edges, actual mints must confirm. The script checks +that the reference stayed fixed, then restores the 45,000 price. + +Heidi finally reindexes the small lab chain; a ninth node syncs from empty data. +The nodes must reach the same block hash and satisfy the ledger checks. Alternating +starting nodes have the DD stats index on/off. This is not every index/pruning mode. + +## Execution status + +The final-release run `thaw-final-03` passed on October 1, 2026. It used source +commit `86c81769024a47f332228700a1ff7c9203c8a1ef` with the recorded lab patch. +It finished at height **5,686**, with all nine nodes on block +`0000040c0d0dcbd4be1b05dee7c9ea36ad2204d99a9b64382abb921b9b789e6b`. +The run recorded **144 checkpoints**, no failed check, and a successful process +exit. All private test processes then stopped normally. + +The evidence is in `builds/final-verification/thaw-final-03/`. The adjacent +`thaw-final-03-build.json` and `thaw-final-03-helpers.json` record the client and +helper hashes. The production source matches the separately tested source at +`a96e6b10b6`; the later commit changes only the rehearsal wait and its tests. + +The previous final run, `thaw-final-02`, stopped because the script consumed its +last permitted block before a signed price was ready. Its logs are preserved. +The corrected script passed seven regression checks and independent review +before this fresh full run. A failed or incomplete run is not counted as a pass. + +The table below describes the completed final-release run. It does not replace +public-network observation, an older-binary comparison, a full mainnet history +reindex, or native Windows/macOS package checks. + +| Check | Before H | At/after H | +| --- | --- | --- | +| Twelve mints covering all ten tiers | PASS | PASS | +| Early/partial/repeated redemption rejection | PASS | PASS | +| Confirmed send/receive and exact balances | PASS | PASS | +| Whole-vault redemption and extra burn | PASS | PASS | +| Direct token/vault ledger and selected health rules | PASS | PASS | +| Bob/Alice restart, backup, restore and rescan | PASS | PASS | +| Bob reindex with its wallet loaded, same hash and balance | PASS | PASS | +| Legacy freeze and H-1/H rule selection | PASS | PASS | +| Boundary rewind, restart, reconnect and replacement branch | PASS | PASS | +| Both exact 20% mint rejections; send/redeem stay usable | Not a pre-H C1 rule | PASS | +| Actual mints just inside both price edges | Not a pre-H C1 rule | PASS | +| Lab reindex and fresh sync to the same hash | Separate end-of-run check | PASS | + +For historical comparison, run `rc2-rehearsal-09` completed the whole integrated harness with no failed +check. It used the test client built from the v9.26.6rc2 commit `e3ad0a4522` +(daemon sha256 `16e127189d75`), reached final height **5,658** with tip hash +`0000073bc7d85ac72c80328c6e17ba26aabd9cc2d4f907c94cba6c4113e5856c`, and recorded +143 recorded checkpoints with zero failures. The evidence is under +`builds/thawday-runs/rc2-rehearsal-09/`. A pass here is the lab exercise only; it +does not replace the separate audit, public testnet activation and observation, +the older-binary comparison, or the full mainnet history reindex. + +## Qt inspection, evidence, and limits + +Automation uses RPC, the program interface, while real Qt wallets run. It does +not click every GUI form. Inspect balances, positions, transaction history, fees, +collateral, and error/status messages during the printed `before` and `after` +phases. Manual flows and screenshots are separate evidence: record the run, +build, phase, and transaction. There is no automatic pause for every screen. + +Keep the whole run folder. `run.json` records the build, ports, processes, status, +and final hash/height if completed; `results.jsonl` records individual checks. +Also keep `test-client.patch`, `ancestor-samples.json`, `feed-requests.jsonl`, +`price-micro-usd.txt`, console/node logs, test wallets and backups. The separate +client has `THAWDAY_BUILD.json`. The run records the old script's hash without +executing it. A missing later check record is not a PASS. + +Preserve errors and logs. A failed check may be a harness defect, missing +prerequisite, timeout, or product defect; the message alone does not decide which. +Use the run's `stop` command for cleanup and a new folder for another attempt. +Do not weaken validation or change an expected answer to make a check pass. + +Even a complete lab PASS does not replace the separate audit, full current test +suites, real older-binary comparison, full historical mainnet/testnet reindex, +public testnet observation periods, or operator coordination. It does not cover +every attack, crash, storage fault, deep reorg, platform, or resource limit. +The same client before/after H is not a mixed-version test; lab reindex is not +public-history reindex; heartbeats alone do not prove every signer joined mined +bundles; and open Qt windows do not prove every GUI action was used. diff --git a/configure.ac b/configure.ac index 2632ff681d3..d5d552724a8 100644 --- a/configure.ac +++ b/configure.ac @@ -1,14 +1,14 @@ AC_PREREQ([2.69]) define(_CLIENT_VERSION_MAJOR, 9) define(_CLIENT_VERSION_MINOR, 26) -define(_CLIENT_VERSION_BUILD, 5) +define(_CLIENT_VERSION_BUILD, 6) define(_CLIENT_VERSION_RC, 0) define(_CLIENT_VERSION_SUFFIX, []) define(_CLIENT_VERSION_IS_RELEASE, true) define(_COPYRIGHT_YEAR, 2026) define(_COPYRIGHT_HOLDERS,[The %s developers]) define(_COPYRIGHT_HOLDERS_SUBSTITUTION,[[DigiByte Core]]) -AC_INIT([DigiByte],m4_join([.], _CLIENT_VERSION_MAJOR, _CLIENT_VERSION_MINOR, _CLIENT_VERSION_BUILD)m4_if(_CLIENT_VERSION_RC, [0], _CLIENT_VERSION_SUFFIX, [rc]_CLIENT_VERSION_RC),[https://github.com/digibyte/digibyte/issues],[digibyte],[https://digibyte.org/]) +AC_INIT([DigiByte],m4_join([.], _CLIENT_VERSION_MAJOR, _CLIENT_VERSION_MINOR, _CLIENT_VERSION_BUILD)m4_if(_CLIENT_VERSION_RC, [0], _CLIENT_VERSION_SUFFIX, [rc]_CLIENT_VERSION_RC),[https://github.com/DigiByte-Core/digibyte/issues],[digibyte],[https://digibyte.io/]) AC_CONFIG_SRCDIR([src/validation.cpp]) AC_CONFIG_HEADERS([src/config/digibyte-config.h]) AC_CONFIG_AUX_DIR([build-aux]) diff --git a/build_digibyte_ubuntu.sh b/contrib/build_digibyte_ubuntu.sh similarity index 100% rename from build_digibyte_ubuntu.sh rename to contrib/build_digibyte_ubuntu.sh diff --git a/deploy_testnet_oracle.sh b/contrib/deploy_testnet_oracle.sh similarity index 100% rename from deploy_testnet_oracle.sh rename to contrib/deploy_testnet_oracle.sh diff --git a/contrib/devtools/gen-digibyte-conf.sh b/contrib/devtools/gen-digibyte-conf.sh index 85dc0b40a35..a778e15ca87 100755 --- a/contrib/devtools/gen-digibyte-conf.sh +++ b/contrib/devtools/gen-digibyte-conf.sh @@ -52,6 +52,7 @@ EOF ${DIGIBYTED} --help \ | sed '1,/Print this help message and exit/d' \ | sed -E 's/^[[:space:]]{2}\-/#/' \ + | sed -E 's/^[[:space:]]{2}([^[:space:]])/# \1/' \ | sed -E 's/^[[:space:]]{7}/# /' \ | sed -E '/[=[:space:]]/!s/#.*$/&=1/' \ | awk '/^#[a-z]/{x=$0;next}{if (NF==0) print x"\n",x="";else print}' \ diff --git a/finish_oracle_setup.sh b/contrib/finish_oracle_setup.sh similarity index 100% rename from finish_oracle_setup.sh rename to contrib/finish_oracle_setup.sh diff --git a/contrib/thawday/build-client.sh b/contrib/thawday/build-client.sh new file mode 100644 index 00000000000..26d5b0aedc3 --- /dev/null +++ b/contrib/thawday/build-client.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Build a separate client. Never replace the release wallet or its source. +set -euo pipefail +TASK_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)" +TASK_CLIENT="$TASK_ROOT/builds/thawday-client" +TASK_BASE="$(git -C "$TASK_ROOT" rev-parse --verify "${1:-HEAD}^{commit}")" +TASK_PATCH="$TASK_ROOT/contrib/thawday/client.patch" +if [[ -e "$TASK_CLIENT" ]]; then + echo "The test checkout already exists at $TASK_CLIENT." + echo "Keep it and its evidence. Use the existing build, or review that checkout before rebuilding it." + exit 1 +fi +git -C "$TASK_ROOT" worktree add --detach "$TASK_CLIENT" "$TASK_BASE" +git -C "$TASK_CLIENT" apply --check "$TASK_PATCH" +git -C "$TASK_CLIENT" apply "$TASK_PATCH" +cd "$TASK_CLIENT" +./autogen.sh +CC=gcc CXX=g++ CFLAGS='-O2 -g1' CXXFLAGS='-O2 -g1' LDFLAGS='-Wl,--no-keep-memory' \ + ./configure --with-gui=qt5 --enable-wallet --disable-tests --disable-qt-tests \ + --disable-bench --disable-fuzz-binary --disable-shared --with-pic +make -j8 -C src digibyted digibyte-cli qt/digibyte-qt +python3 "$TASK_ROOT/contrib/thawday/record-build.py" "$TASK_CLIENT" "$TASK_PATCH" +echo "The separate test client is built. Run ./thawDay.sh from the release checkout." diff --git a/contrib/thawday/burn_transaction.py b/contrib/thawday/burn_transaction.py new file mode 100644 index 00000000000..36529d15acb --- /dev/null +++ b/contrib/thawday/burn_transaction.py @@ -0,0 +1,120 @@ +"""Helpers for the isolated Thaw Day rehearsal; they never launch or stop nodes. + +Put this checkout's test/functional directory on sys.path before importing. +RPC callbacks have the form rpc(method_name, *positional_parameters). +Only call these helpers against the isolated rehearsal wallets and chain. +""" + +from decimal import Decimal + +from digidollar_thawday_extra_burn import metadata, script_number, token_outputs +from test_framework.key import compute_xonly_pubkey, sign_schnorr +from test_framework.messages import COIN, CTxInWitness, CTxOut, tx_from_hex +from test_framework.script import CScript, CScriptNum, OP_RETURN, TaprootSignatureHash + + +def require(condition, message): + if not condition: + raise ValueError(message) + + +def satoshis(value): + value = Decimal(str(value)) * COIN + require(value == value.to_integral_value(), "DGB amount has sub-satoshi precision") + return int(value) + + +def extra_burn_redemption(rpc, original_hex, principal_cents, extra_cents, + source_transactions, owner_key_for, + collateral_outpoint, collateral_input=0): + """Return a valid re-signed normal redemption and its exact burn evidence. + + original_hex must be a wallet-built, matured NORMAL redemption kept out of + the mempool with walletbroadcast=0. All source inputs must be confirmed. + source_transactions maps each known DD txid to CTransaction or raw hex. + owner_key_for maps an x-only public key (bytes) to its legitimate secret + (32 bytes), or is a callable accepting that public key. known_owner_keys() + supplies the existing public descriptor fixture's map, if imported BEFORE + this vault was minted. collateral_outpoint is (mint_txid, collateral_vout). + + No existing signature survives an output change. This keeps the same + collateral leaf/control proof and signs its normal owner path again. + Wallet signing supplies token and fee signatures. This function neither + broadcasts nor mines. A partial wallet signing result is expected ONLY + for the collateral input; final testmempoolaccept must allow the result. + """ + require(isinstance(principal_cents, int) and principal_cents > 0, "Use positive integer cents") + require(isinstance(extra_cents, int) and extra_cents > 0, "Use positive integer extra cents") + original = tx_from_hex(original_hex) + original.rehash() + require(original.hash not in rpc("getrawmempool"), "Original redemption is already in the mempool") + original_acceptance = rpc("testmempoolaccept", [original_hex], 0)[0] + require(original_acceptance.get("allowed") is True, + "Original redemption must independently pass: " + str(original_acceptance)) + tx = tx_from_hex(original_hex) + require(0 <= collateral_input < len(tx.vin), "Collateral input index is out of range") + source = tx.vin[collateral_input].prevout + require((f"{source.hash:064x}", source.n) == tuple(collateral_outpoint), "Collateral input names another vault") + require(len(tx.wit.vtxinwit) > collateral_input, "Missing collateral witness") + stack = tx.wit.vtxinwit[collateral_input].scriptWitness.stack + require(len(stack) == 3, "Expected a normal owner signature, leaf, and control proof") + _, leaf, control = stack + fields = list(CScript(leaf)) + require(len(fields) >= 2 and isinstance(fields[-2], bytes) and len(fields[-2]) == 32, + "Normal leaf does not expose the expected owner public key") + owner_public = fields[-2] + secret = owner_key_for(owner_public) if callable(owner_key_for) else owner_key_for.get(owner_public) + require(secret is not None, "The test has no legitimate private key for this collateral owner") + require(compute_xonly_pubkey(secret)[0] == owner_public, "Owner secret does not match the leaf") + index, dd_fields = metadata(tx) + require(script_number(dd_fields[2]) == 3 and len(dd_fields) == 4, "Expected standard redemption metadata") + change_before = script_number(dd_fields[3]) + change_after = change_before - extra_cents + require(change_after >= 100, "Consolidate token inputs first; extra burn must leave at least 100 cents change") + outputs_before = [output.serialize() for output in tx.vout] + tx.vout[index].scriptPubKey = CScript([OP_RETURN, b"DD", dd_fields[2], CScriptNum(change_after)]) + require(all(output.serialize() == outputs_before[i] for i, output in enumerate(tx.vout) if i != index), + "An output other than the DD metadata changed") + spent = [] + token_input = 0 + for txin in tx.vin: + txid = f"{txin.prevout.hash:064x}" + coin = rpc("gettxout", txid, txin.prevout.n, False) + require(coin is not None, "Source input is absent from the confirmed UTXO set") + script = bytes.fromhex(coin["scriptPubKey"]["hex"]) + amount = satoshis(coin["value"]) + spent.append(CTxOut(amount, script)) + source_tx = source_transactions.get(txid) + if source_tx is not None: + if isinstance(source_tx, str): + source_tx = tx_from_hex(source_tx) + source_tx.rehash() + require(source_tx.hash == txid, "Source transaction registry has the wrong bytes") + token_input += token_outputs(source_tx).get(txin.prevout.n, 0) + elif amount == 0 and len(script) == 34 and script[:2] == b"\x51\x20": + raise ValueError("DD source transaction missing from the independent amount registry") + require(token_input - change_before == principal_cents, "Original is not the expected normal full-principal burn") + require(token_input - sum(token_outputs(tx).values()) == principal_cents + extra_cents, "Wrong extra burn") + tx.wit.vtxinwit = [CTxInWitness() for _ in tx.vin] + for txin in tx.vin: + txin.scriptSig = b"" + signed = rpc("signrawtransactionwithwallet", tx.serialize().hex(), [], "DEFAULT") + tx = tx_from_hex(signed["hex"]) + while len(tx.wit.vtxinwit) < len(tx.vin): + tx.wit.vtxinwit.append(CTxInWitness()) + require(all(tx.wit.vtxinwit[i].scriptWitness.stack or tx.vin[i].scriptSig + for i in range(len(tx.vin)) if i != collateral_input), + "Wallet did not sign every token and fee input") + digest = TaprootSignatureHash(tx, spent, 0, input_index=collateral_input, + scriptpath=True, script=CScript(leaf)) + tx.wit.vtxinwit[collateral_input].scriptWitness.stack = [sign_schnorr(secret, digest), leaf, control] + tx.rehash() + raw = tx.serialize().hex() + acceptance = rpc("testmempoolaccept", [raw], 0)[0] + require(acceptance.get("allowed") is True, "Re-signed extra burn rejected: " + str(acceptance)) + return {"tx": tx, "hex": raw, "txid": tx.hash, + "token_input_cents": token_input, "change_before_cents": change_before, + "change_after_cents": change_after, "principal_closed_cents": principal_cents, + "actual_burn_cents": principal_cents + extra_cents, + "original_acceptance": original_acceptance, "acceptance": acceptance} + diff --git a/contrib/thawday/client.patch b/contrib/thawday/client.patch new file mode 100644 index 00000000000..88789f21a63 --- /dev/null +++ b/contrib/thawday/client.patch @@ -0,0 +1,144 @@ +diff --git a/src/chainparams.cpp b/src/chainparams.cpp +index 69d29e5806..01385c1b75 100644 +--- a/src/chainparams.cpp ++++ b/src/chainparams.cpp +@@ -14,6 +14,7 @@ + #include + #include + #include ++#include + + #include + #include +@@ -168,6 +169,13 @@ std::unique_ptr CreateChainParams(const ArgsManager& args, c + + void SelectParams(const ChainType chain) + { ++ // Help setup constructs every network before arguments are parsed. Apply ++ // the guard here, where the node selects its actual network after parsing. ++ if (chain != ChainType::TESTNET || !gArgs.GetBoolArg("-easypow", false)) { ++ throw std::runtime_error("THAW DAY TEST CLIENT: only -testnet -easypow is supported. This client is not for real funds."); ++ } ++ (void)ThawDayLabFeedURL(); ++ + SelectBaseParams(chain); + globalChainParams = CreateChainParams(gArgs, chain); + } +diff --git a/src/init.cpp b/src/init.cpp +index 9bb3ffa73e..37fa3c54fc 100644 +--- a/src/init.cpp ++++ b/src/init.cpp +@@ -1379,6 +1379,8 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) + return false; + } + ++ LogPrintf("THAW DAY TEST CLIENT: PRIVATE REHEARSAL ONLY. NOT FOR REAL FUNDS. DigiDollar height 600; Thaw Day height 5000.\n"); ++ + LogPrintf("Using at most %i automatic connections (%i file descriptors available)\n", nMaxConnections, nFD); + + // Warn about relative -datadir path. +diff --git a/src/kernel/chainparams.cpp b/src/kernel/chainparams.cpp +index fe0d7cebf5..5fbc6cc86b 100644 +--- a/src/kernel/chainparams.cpp ++++ b/src/kernel/chainparams.cpp +@@ -728,6 +728,13 @@ public: + + private: + void EnableLocalMiniTestnetMode() { ++ // These settings isolate this rehearsal from every public network. ++ consensus.nDDThawDayHeight = 5000; ++ pchMessageStart = {0xf9, 0xdd, 0xa5, 0x66}; ++ nDefaultPort = 28444; ++ vSeeds.clear(); ++ vFixedSeeds.clear(); ++ + consensus.fPowAllowMinDifficultyBlocks = true; + consensus.fEasyPow = true; + digidollarParams.activeOracles = 24; +diff --git a/src/oracle/exchange.cpp b/src/oracle/exchange.cpp +index 7a60633476..b96f0c94c7 100644 +--- a/src/oracle/exchange.cpp ++++ b/src/oracle/exchange.cpp +@@ -15,6 +15,7 @@ + #include + #include + #include ++#include + #include + #include + +@@ -88,6 +89,8 @@ static int CurlInterruptCallback(void* clientp, double, double, double, double) + BaseExchangeFetcher::BaseExchangeFetcher(const std::string& name, const std::string& url) + : exchange_name(name), base_url(url) + { ++ // Exercise each real parser using only the private fixture server. ++ base_url = ThawDayLabFeedURL() + "/" + exchange_name; + #ifdef HAVE_LIBCURL + // Ensure curl is globally initialized (safe to call multiple times) + static bool curl_initialized = []() { +@@ -121,6 +124,12 @@ BaseExchangeFetcher::~BaseExchangeFetcher() + + std::string BaseExchangeFetcher::HttpGet(const std::string& url) + { ++ // A hard-coded or future fallback URL must not escape the fixture server. ++ const std::string fixture_prefix = base_url + "/"; ++ if (url.compare(0, fixture_prefix.size(), fixture_prefix) != 0) { ++ LogPrintf("THAW DAY TEST CLIENT: refused an exchange request outside the local fixture.\n"); ++ return ""; ++ } + #ifdef HAVE_LIBCURL + if (m_interrupt_callback && m_interrupt_callback()) { + LogPrint(BCLog::DIGIDOLLAR, "HttpGet: interrupted before request to %s\n", url); +@@ -169,13 +178,14 @@ std::string BaseExchangeFetcher::HttpGet(const std::string& url) + // Set timeout + curl_easy_setopt(curl, CURLOPT_TIMEOUT, static_cast(timeout_seconds)); + +- // Follow redirects +- curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); +- curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 3L); ++ // The fixture must be reached directly and cannot redirect to the Internet. ++ curl_easy_setopt(curl, CURLOPT_PROXY, ""); ++ curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L); ++ curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 0L); + #if CURL_AT_LEAST_VERSION(7, 85, 0) +- curl_easy_setopt(curl, CURLOPT_PROTOCOLS_STR, "https"); ++ curl_easy_setopt(curl, CURLOPT_PROTOCOLS_STR, "http"); + #else +- curl_easy_setopt(curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS); ++ curl_easy_setopt(curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP); + #endif + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L); + +--- /dev/null ++++ b/src/util/thawday_lab.h +@@ -0,0 +1,30 @@ ++// This file belongs only to the isolated Thaw Day rehearsal client. ++#ifndef DIGIBYTE_UTIL_THAWDAY_LAB_H ++#define DIGIBYTE_UTIL_THAWDAY_LAB_H ++ ++#include ++ ++#include ++#include ++#include ++ ++inline std::string ThawDayLabFeedURL() ++{ ++ const char* enabled = std::getenv("THAWDAY_LAB"); ++ if (!enabled || std::string{enabled} != "1") { ++ throw std::runtime_error("THAW DAY TEST CLIENT: set THAWDAY_LAB=1. This client is not for real funds."); ++ } ++ ++ const char* configured = std::getenv("THAWDAY_FEED_URL"); ++ const std::string url = configured ? configured : ""; ++ const std::string prefix = "http://127.0.0.1:"; ++ uint16_t port{0}; ++ const std::string port_text = url.compare(0, prefix.size(), prefix) == 0 ? url.substr(prefix.size()) : ""; ++ if (port_text.empty() || port_text.find_first_not_of("0123456789") != std::string::npos || ++ !ParseUInt16(port_text, &port) || port == 0) { ++ throw std::runtime_error("THAW DAY TEST CLIENT: THAWDAY_FEED_URL must be http://127.0.0.1:. Public exchange access is disabled."); ++ } ++ return url; ++} ++ ++#endif // DIGIBYTE_UTIL_THAWDAY_LAB_H diff --git a/contrib/thawday/extra_burn.py b/contrib/thawday/extra_burn.py new file mode 100644 index 00000000000..a46370666a5 --- /dev/null +++ b/contrib/thawday/extra_burn.py @@ -0,0 +1,161 @@ +"""Confirm an ordinary extra burn using only the private rehearsal wallets.""" + +def apply_extra_burn_case(lab, phase, price, active): + from test_framework.messages import tx_from_hex + from digidollar_thawday_extra_burn import known_owner_keys, token_outputs + from burn_transaction import extra_burn_redemption, require + + bob = lab.nodes[0] + ceiling = None if active else lab.state["height"] - 1000 + position_id = lab.state[phase + "_extra_burn_position"] + position = lab.open_positions[position_id] + principal = int(position["dd_minted"]) + extra = 2500 + actual_burn = principal + extra + vaults = [entry for entry in lab.vaults if entry[0] == position_id] + require(len(vaults) == 1 and vaults[0][2] == principal, "Extra-burn vault is missing or ambiguous") + collateral_outpoint = tuple(vaults[0][:2]) + require(bob.rpc("getblockcount") > position["unlock_height"], "Extra-burn vault has not matured") + require(active or bob.rpc("getblockcount") < ceiling, "No pre-Thaw extra-burn block budget remains") + lab.await_quote(price, ceiling) + lab.sync() + balances_before = [node.balance() for node in lab.nodes] + tokens_before, principal_before = lab.expected_supply, lab.expected_principal + vaults_before = len(lab.open_positions) + require(bob.rpc("getdigidollarbalance", wallet=True)["unconfirmed"] == 0, + "Consolidation requires no unconfirmed Bob tokens") + require(balances_before[0] >= actual_burn + 100, "Bob needs enough tokens for extra burn and one dollar change") + inputs = bob.rpc("listdigidollarunspent", wallet=True) + require(inputs and all(row["confirmations"] >= 1 and row["spendable"] and row["safe"] for row in inputs), + "Bob has token inputs that are not confirmed, safe, and spendable") + require(sum(row["amount"] for row in inputs) == balances_before[0], "Listed token inputs do not equal Bob's confirmed balance") + selected = [{"txid": row["txid"], "vout": row["vout"]} for row in inputs] + destination = bob.rpc("getdigidollaraddress", wallet=True) + consolidation = bob.rpc("senddigidollar", destination, balances_before[0], + "Thaw Day extra-burn input consolidation", 0, selected, "cents", wallet=True) + consolidation_id = lab.confirm(bob, consolidation, ceiling) + consolidation_tx = tx_from_hex(lab.dd_transactions[consolidation_id]) + outputs = token_outputs(consolidation_tx) + require(len(outputs) == 1 and sum(outputs.values()) == balances_before[0], "Consolidation did not create exactly one whole-balance DD output") + token_outpoint = (consolidation_id, next(iter(outputs))) + require([node.balance() for node in lab.nodes] == balances_before, "Self-consolidation changed a DD balance") + lab.note(phase + ": extra-burn token inputs consolidated", { + "txid": consolidation_id, "selected_inputs": selected, + "token_outpoint": token_outpoint, "amount_cents": balances_before[0]}) + + # Keep the original private even if any later assertion fails. Broadcasting + # is restored only after a confirmed replacement conflicts the original. + previous_args = list(bob.record.get("extra_args", [])) + base_args = [arg for arg in previous_args if not arg.startswith("-walletbroadcast") + and not arg.startswith("-nowalletbroadcast")] + bob.record["extra_args"] = base_args + ["-walletbroadcast=0"] + lab.save_state() + bob.stop() + bob.start() + bob.start_oracles() + lab.sync() + lab.await_quote(price, ceiling) + original_id = None + try: + mempool_before = sorted(bob.rpc("getrawmempool")) + ordinary = bob.rpc("redeemdigidollar", position_id, principal, wallet=True) + original_id = ordinary["txid"] + require(ordinary["err_active"] is False and ordinary["required_dd_burn"] == principal, + "Extra burn needs a healthy normal full-principal redemption") + require(sorted(bob.rpc("getrawmempool")) == mempool_before, + "Private original changed the mempool") + raw_original = bob.rpc("gettransaction", original_id, wallet=True)["hex"] + original = tx_from_hex(raw_original) + require(any((f"{txin.prevout.hash:064x}", txin.prevout.n) == token_outpoint for txin in original.vin), + "Normal redemption did not spend the consolidated token output") + original_path = lab.run / (phase + "-extra-burn-original.hex") + original_path.write_text(raw_original + "\n") + lab.state[phase + "_extra_burn"] = {"status": "PRIVATE_ORIGINAL", "position_id": position_id, + "original_txid": original_id} + lab.save_state() + wallet_methods = {"signrawtransactionwithwallet"} + rpc = lambda method, *params: bob.rpc(method, *params, wallet=method in wallet_methods) + replacement = extra_burn_redemption(rpc, raw_original, principal, extra, + lab.dd_transactions, known_owner_keys(), collateral_outpoint) + require(replacement["txid"] != original_id, "Extra burn did not change the transaction id") + require(replacement["token_input_cents"] == balances_before[0], "Redemption selected an unexpected token amount") + replacement_path = lab.run / (phase + "-extra-burn-replacement.hex") + replacement_path.write_text(replacement["hex"] + "\n") + lab.note(phase + ": private normal and re-signed extra burn both admitted", { + "original_txid": original_id, "replacement_txid": replacement["txid"], + "original_absent_from_mempool": original_id not in bob.rpc("getrawmempool"), + "original_acceptance": replacement["original_acceptance"], "replacement_acceptance": replacement["acceptance"], + "original_change_cents": replacement["change_before_cents"], + "replacement_change_cents": replacement["change_after_cents"], + "required_normal_burn_cents": principal, "actual_burn_cents": actual_burn}) + # Control: an ordinary DGB payment confirms under the same signed price. + # Bob's wallet is not broadcasting here, so Alice sends it to Bob. + alice = lab.nodes[1] + control = alice.rpc("sendtoaddress", bob.rpc("getnewaddress", "extra-burn control", "bech32", wallet=True), 1, wallet=True) + control_id = lab.confirm(alice, control, ceiling, record=False) + lab.await_quote(price, ceiling) + sent = bob.rpc("sendrawtransaction", replacement["hex"], 0) + require(sent == replacement["txid"] and original_id not in bob.rpc("getrawmempool"), + "Broadcast did not submit only the replacement") + # Diagnostic only, non-fatal. getblocktemplate includes oracle-priced DD + # txs only when the caller passes the "digidollar-oracle" rule; the lab + # miner (generatetoaddress) opts into that by default, so this mirrors + # what it will actually build. + try: + template = bob.rpc("getblocktemplate", {"rules": ["segwit", "digidollar-oracle"]}) + lab.note(phase + ": miner template right after submitting the replacement", { + "control_txid": control_id, "template_height": template["height"], + "replacement_in_template": sent in [entry["txid"] for entry in template["transactions"]]}) + except Exception as diagnostic_error: + lab.note(phase + ": template diagnostic unavailable", {"error": str(diagnostic_error)}) + # Bob restarted with broadcasting off, so its MuSig2 signing session began + # cold. await_quote above already waited for a signed price mined after the + # restart, so the session is warm and the miner (which opts into + # oracle-priced DD txs by default) confirms the redemption on the next + # block. Keep a small extra budget in case the epoch is mid-settle. + confirmed = lab.confirm(bob, sent, ceiling, attempts=24) + require(confirmed == sent, "Confirmed txid does not match the replacement") + require(bob.rpc("gettxout", *collateral_outpoint, False) is None, "Extra burn left the vault unspent") + expected_balances = list(balances_before) + expected_balances[0] -= actual_burn + require([node.balance() for node in lab.nodes] == expected_balances, + "Extra burn changed wallet DD balances by the wrong amount") + original_state = bob.rpc("gettransaction", original_id, wallet=True) + require(original_state["confirmations"] < 0, "Original is not conflicted after replacement confirmation") + require(sent in original_state.get("walletconflicts", []), "Original does not identify the confirmed replacement conflict") + require(original_id not in bob.rpc("getrawmempool"), "Conflicted original appeared in mempool") + lab.expected_supply -= actual_burn + lab.expected_principal -= principal + del lab.open_positions[position_id] + lab.state[phase + "_extra_burn"] = {"status": "CONFIRMED", "position_id": position_id, + "original_txid": original_id, "replacement_txid": sent, + "actual_burn_cents": actual_burn, "principal_closed_cents": principal} + lab.save_state() + lab.note(phase + ": normal redemption burned extra DD without reducing extra principal", { + "replacement_txid": sent, "original_confirmations": original_state["confirmations"], + "walletconflicts": original_state.get("walletconflicts", []), + "supply_before_cents": tokens_before, "supply_after_cents": lab.expected_supply, + "principal_before_cents": principal_before, "principal_after_cents": lab.expected_principal, + "vaults_before": vaults_before, "vaults_after": len(lab.open_positions), + "wallet_balances_before": balances_before, "wallet_balances_after": expected_balances}) + except BaseException as error: + lab.note(phase + ": extra-burn case stopped with original broadcasting still disabled", { + "original_txid": original_id, "error": str(error), "walletbroadcast": 0}) + raise + + bob.stop() + bob.record["extra_args"] = base_args + ["-walletbroadcast=1"] + lab.save_state() + bob.start() + bob.start_oracles() + lab.sync() + lab.await_quote(price, ceiling) + require(bob.rpc("gettransaction", original_id, wallet=True)["confirmations"] < 0, + "Original lost its conflict after broadcast was restored") + require(original_id not in bob.rpc("getrawmempool"), "Original rebroadcast after restart") + require([node.balance() for node in lab.nodes] == expected_balances, + "Extra-burn balances changed after broadcasting was restored") + lab.note(phase + ": extra-burn restart preserved conflict and balances", { + "original_txid": original_id, "replacement_txid": sent, "walletbroadcast": 1, + "wallet_balances": expected_balances}) + return lab.state[phase + "_extra_burn"] diff --git a/contrib/thawday/record-build.py b/contrib/thawday/record-build.py new file mode 100644 index 00000000000..49c00f6deb7 --- /dev/null +++ b/contrib/thawday/record-build.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +"""Record the source and binaries that the isolated rehearsal will open.""" +import hashlib +import json +from pathlib import Path +import subprocess +import sys + +client, patch = (Path(argument).resolve() for argument in sys.argv[1:]) + + +def entry(name): + with (client / name).open("rb") as handle: + value = hashlib.file_digest(handle, "sha256").hexdigest() + return {"path": name, "sha256": value} + + +manifest = { + "base_commit": subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=client, text=True).strip(), + "patch_sha256": hashlib.sha256(patch.read_bytes()).hexdigest(), + "changed_files": [entry(name) for name in ( + "src/chainparams.cpp", "src/init.cpp", "src/kernel/chainparams.cpp", + "src/oracle/exchange.cpp", "src/util/thawday_lab.h")], + "binaries": [entry(name) for name in ("src/digibyted", "src/digibyte-cli", "src/qt/digibyte-qt")], + "build_command": "make -j8 -C src digibyted digibyte-cli qt/digibyte-qt", +} +(client / "THAWDAY_BUILD.json").write_text(json.dumps(manifest, indent=2) + "\n") diff --git a/contrib/thawday/rehearsal.py b/contrib/thawday/rehearsal.py new file mode 100644 index 00000000000..9a239a77d00 --- /dev/null +++ b/contrib/thawday/rehearsal.py @@ -0,0 +1,907 @@ +#!/usr/bin/env python3 +"""Rehearse the same DigiDollar operations before and after Thaw Day. + +This follows test_multi_oracle_testnet.sh. It uses fresh local wallets and +real oracle signing, with controlled exchange responses for repeatable prices. +It never opens an existing wallet or connects to the public testnet. +""" + +import argparse +import base64 +from decimal import Decimal +import hashlib +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +import json +import os +from pathlib import Path +import signal +import socket +import subprocess +import sys +import time +import urllib.error +import urllib.parse +import urllib.request + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT / "test/functional")) +H = 5000 +BASE_PRICE = 30000 +NEW_PRICE = 45000 +NAMES = ["bob", "alice", "charlie", "dave", "eve", "frank", "grace", "heidi"] +SLOTS = [[0, 1, 16, 18], [2, 3, 17, 19], [4, 5, 20], [6, 7, 21], + [8, 9, 22], [10, 11, 23], [12, 13], [14, 15]] + + +class Failure(RuntimeError): + pass + + +class RPCError(Failure): + def __init__(self, error): + self.code = error["code"] + super().__init__(error["message"]) + + +def save(path, value): + temporary = path.with_suffix(path.suffix + ".new") + temporary.write_text(json.dumps(value, indent=2, default=str) + "\n") + temporary.replace(path) + + +def digest(path): + with path.open("rb") as handle: + return hashlib.file_digest(handle, "sha256").hexdigest() + + +def check(condition, message): + if not condition: + raise Failure(message) + + +def wait_for(fn, description, timeout=180): + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + if fn(): + return + time.sleep(1) + raise Failure("Timed out: " + description) + + +def free_port(): + with socket.socket() as sock: + sock.bind(("127.0.0.1", 0)) + return sock.getsockname()[1] + + +def process_identity(pid): + try: + # The start time distinguishes this process from a later reused PID. + return Path(f"/proc/{pid}/stat").read_text().rsplit(")", 1)[1].split()[19] + except FileNotFoundError: + return None + + +def serve_feed(run): + """Keep the real exchange parsers; supply only their HTTP responses.""" + state = json.loads((run / "run.json").read_text()) + + class Handler(BaseHTTPRequestHandler): + def log_message(self, *_args): + pass + + def do_GET(self): + route = urllib.parse.urlsplit(self.path) + query = urllib.parse.parse_qs(route.query) + price = Decimal((run / "price-micro-usd.txt").read_text().strip()) / 1000000 + text_price = format(price, ".6f") + routes = { + "/Binance/api/v3/ticker/price": {"price": text_price}, + "/CoinGecko/api/v3/simple/price": {"digibyte": {"usd": float(price)}}, + "/KuCoin/api/v1/market/orderbook/level1": {"data": {"price": text_price}}, + "/Gate.io/api/v4/spot/tickers": [{"last": text_price}], + "/HTX/market/detail/merged": {"tick": {"close": float(price)}}, + "/Crypto.com/exchange/v1/public/get-tickers": {"result": {"data": [{"a": text_price}]}}, + } + response = routes.get(route.path) + if route.path.startswith("/Binance/") and query.get("symbol") != ["DGBUSDT"]: + response = None + body = json.dumps(response if response is not None else {"error": "Unknown test feed route"}).encode() + self.send_response(200 if response is not None else 404) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + with (run / "feed-requests.jsonl").open("a") as log: + log.write(json.dumps({"time": time.time(), "path": self.path, + "price_micro_usd": int(price * 1000000), + "served": response is not None}) + "\n") + + ThreadingHTTPServer(("127.0.0.1", state["feed_port"]), Handler).serve_forever() + + +class Node: + def __init__(self, lab, index, record): + self.lab, self.index, self.record = lab, index, record + self.name = record["name"] + self.directory = Path(record["datadir"]) + self.wallet = "thaw-test-" + self.name + self.process = None + + def rpc(self, method, *params, wallet=False): + cookie = self.directory / "testnet26" / ".cookie" + authorization = base64.b64encode(cookie.read_bytes().strip()).decode() + path = "/wallet/" + urllib.parse.quote(self.wallet) if wallet else "/" + request = urllib.request.Request( + f"http://127.0.0.1:{self.record['rpc_port']}{path}", + json.dumps({"jsonrpc": "2.0", "id": "thawday", "method": method, "params": list(params)}).encode(), + {"Authorization": "Basic " + authorization, "Content-Type": "application/json"}) + # Local RPC must not use an HTTP proxy inherited from the desktop. + opener = urllib.request.build_opener(urllib.request.ProxyHandler({})) + try: + response = opener.open(request, timeout=300) + except urllib.error.HTTPError as error: + response = error + data = json.loads(response.read(), parse_float=Decimal) + if data.get("error"): + raise RPCError(data["error"]) + return data["result"] + + def start(self, reindex=False): + self.directory.mkdir(exist_ok=True) + marker = self.directory / "THAW_DAY_TEST_ONLY" + if not marker.exists(): + check(not list(self.directory.iterdir()), "Refusing a nonempty unmarked test directory") + marker.write_text(str(self.lab.run) + "\n") + (self.directory / "digibyte.conf").write_text("# This file belongs to the isolated Thaw Day test.\n") + check(marker.read_text().strip() == str(self.lab.run), "Wrong test directory owner") + command = [str(self.lab.client / "src/qt/digibyte-qt"), "-testnet", "-easypow", + f"-datadir={self.directory}", f"-conf={self.directory / 'digibyte.conf'}", + "-settings=0", "-server=1", "-listen=1", "-listenonion=0", "-dnsseed=0", + "-fixedseeds=0", "-discover=0", "-upnp=0", "-natpmp=0", "-onion=0", + "-proxy=0", "-bind=127.0.0.1", "-rpcbind=127.0.0.1", "-rpcallowip=127.0.0.1", + f"-port={self.record['p2p_port']}", f"-rpcport={self.record['rpc_port']}", + "-digidollar=1", "-txindex=1", f"-digidollarstatsindex={self.index % 2}", + "-fallbackfee=0.0001", "-dandelion=0", "-dbcache=32", "-par=1", + "-maxconnections=64", "-debug=digidollar", "-assumevalid=0", "-min=0", "-splash=0"] + command.extend(self.record.get("extra_args", [])) + command.append("-connect=0" if self.index == 0 else + f"-connect=127.0.0.1:{self.lab.nodes[0].record['p2p_port']}") + if (self.directory / "testnet26" / "wallets" / self.wallet).exists(): + command.append("-wallet=" + self.wallet) + if reindex: + command.append("-reindex=1") + environment = os.environ.copy() + environment.update(THAWDAY_LAB="1", THAWDAY_FEED_URL=f"http://127.0.0.1:{self.lab.state['feed_port']}") + environment["XDG_CONFIG_HOME"] = str(self.directory / "qt-preferences") + output = (self.lab.run / f"{self.name}-console.log").open("ab") + self.process = subprocess.Popen(command, env=environment, stdout=output, stderr=subprocess.STDOUT, + start_new_session=True) + output.close() + self.record.update(pid=self.process.pid, start_time=process_identity(self.process.pid), command=command) + self.lab.save_state() + + def ready(): + check(self.process.poll() is None, f"{self.name} stopped. Read its console log.") + try: + self.rpc("getblockchaininfo") + return True + except (OSError, urllib.error.URLError): + return False + except RPCError as error: + if error.code == -28: + return False + raise + wait_for(ready, self.name + " startup", 600) + if self.wallet not in self.rpc("listwallets"): + self.rpc("createwallet", self.wallet) + self.record["running"] = True + if self.index == 0: + # A restarted miner starts its oracle signing session over. A price + # already in the chain does not prove that the miner can sign a new + # one, and a block that carries a mint or redemption needs a fresh + # signed price. Only a signed price in a block mined after this + # start counts as ready. + self.lab.miner_started_at = self.rpc("getblockcount") + self.lab.save_state() + + def stop(self): + if not self.record.get("pid"): + return + identity = process_identity(self.record["pid"]) + if identity is None: + self.record["running"] = False + return + check(identity == self.record["start_time"], "Test process identity changed") + try: + self.rpc("stop") + except (OSError, urllib.error.URLError, RPCError): + # A startup error can happen before RPC opens. Signal only the + # exact process this rehearsal started, never a process-name match. + os.kill(self.record["pid"], signal.SIGTERM) + if self.process: + self.process.wait(timeout=180) + else: + wait_for(lambda: process_identity(self.record["pid"]) is None, self.name + " shutdown") + self.record["running"] = False + self.lab.save_state() + + def start_oracles(self): + for slot in SLOTS[self.index] if self.index < len(SLOTS) else []: + key = hashlib.sha256(f"digibyte_testnet_oracle_{slot}".encode()).hexdigest() + result = self.rpc("startoracle", slot, key) + check(result["success"] is True, f"Test oracle {slot} failed to start") + + def balance(self): + return self.rpc("getdigidollarbalance", wallet=True)["confirmed"] + + +class Lab: + def __init__(self, run, client=None): + self.run = run.resolve() + self.state = json.loads((run / "run.json").read_text()) if (run / "run.json").exists() else {} + self.client = Path(client or self.state["client"]).resolve() + self.nodes = [Node(self, index, record) for index, record in enumerate(self.state.get("nodes", []))] + self.expected_supply = 0 + self.expected_principal = 0 + self.open_positions = {} + self.dd_transactions = {} + self.vaults = [] + self.phase = "setup" + self.miner_started_at = -1 + + def save_state(self): + save(self.run / "run.json", self.state) + + def note(self, label, evidence): + print(label, flush=True) + with (self.run / "results.jsonl").open("a") as output: + output.write(json.dumps({"time": time.time(), "check": label, "evidence": evidence}, default=str) + "\n") + + def setup(self): + check(os.environ.get("DISPLAY"), "A desktop DISPLAY is needed for the eight test Qt windows") + binaries = [self.client / name for name in ("src/qt/digibyte-qt", "src/digibyte-cli", "src/digibyted")] + check(all(path.is_file() for path in binaries), "Build the separate Thaw Day client first") + manifest = json.loads((self.client / "THAWDAY_BUILD.json").read_text()) + patch = (ROOT / "contrib/thawday/client.patch").read_bytes() + check(b"THAWDAY_LAB" in patch and b"THAWDAY_FEED_URL" in patch, + "This is not the guarded Thaw Day test client") + check(hashlib.sha256(patch).hexdigest() == manifest["patch_sha256"], "The test patch changed after the build") + actual_base = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=self.client, text=True).strip() + check(actual_base == manifest["base_commit"], "The test client base changed after the build") + for entry in manifest["changed_files"] + manifest["binaries"]: + check(digest(self.client / entry["path"]) == entry["sha256"], "A test source or binary changed after the recorded build") + changed = set(subprocess.check_output(["git", "diff", "--name-only", "HEAD"], cwd=self.client, text=True).splitlines()) + check(changed <= {entry["path"] for entry in manifest["changed_files"]}, "Unexpected changes in the test client") + self.state = {"client": str(self.client), "run_dir": str(self.run), "height": H, + "base_commit": subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=self.client, text=True).strip(), + "binary_sha256": {str(path): digest(path) for path in binaries}, + "original_script_sha256": digest(ROOT / "test_multi_oracle_testnet.sh"), + "harness_sha256": digest(Path(__file__)), + "feed_port": free_port(), "nodes": [], "status": "RUNNING"} + (self.run / "rehearsal.py").write_bytes(Path(__file__).read_bytes()) + (self.run / "test-client.patch").write_bytes(patch) + (self.run / "price-micro-usd.txt").write_text(str(BASE_PRICE)) + used = {self.state["feed_port"]} + for name in NAMES: + ports = [] + while len(ports) < 2: + port = free_port() + if port not in used: + used.add(port) + ports.append(port) + self.state["nodes"].append({"name": name, "datadir": str(self.run / name), + "p2p_port": ports[0], "rpc_port": ports[1]}) + self.save_state() + guard_directory = self.run / "guard-checks" + guard_directory.mkdir() + guard_config = guard_directory / "digibyte.conf" + guard_config.write_text("# Empty test configuration.\n") + checks = ((["-chain=main", "-easypow"], "1", "http://127.0.0.1:1", "only -testnet -easypow"), + (["-testnet", "-easypow"], "0", "http://127.0.0.1:1", "set THAWDAY_LAB=1"), + (["-testnet", "-easypow"], "1", "http://example.invalid:80", "must be http://127.0.0.1")) + for flags, enabled, feed_url, expected in checks: + environment = os.environ.copy() + environment.update(THAWDAY_LAB=enabled, THAWDAY_FEED_URL=feed_url) + result = subprocess.run([str(self.client / "src/digibyted"), f"-datadir={guard_directory}", + f"-conf={guard_config}", "-settings=0", "-connect=0", "-listen=0"] + flags, + env=environment, capture_output=True, text=True, timeout=20) + check(result.returncode != 0 and expected in result.stderr, "A test-client startup guard did not refuse unsafe settings") + self.note("The test client refuses public mode and missing or public feed settings", {}) + feed_log = (self.run / "feed-console.log").open("ab") + feed = subprocess.Popen([sys.executable, str(Path(__file__).resolve()), "feed", "--run-dir", str(self.run)], + stdout=feed_log, stderr=subprocess.STDOUT, start_new_session=True) + feed_log.close() + self.state["feed_pid"] = feed.pid + self.state["feed_start_time"] = process_identity(feed.pid) + self.save_state() + self.nodes = [Node(self, index, record) for index, record in enumerate(self.state["nodes"])] + for node in self.nodes: + node.start() + from wallet_taproot import KEYS + from test_framework.descriptors import descsum_create + imported = self.nodes[0].rpc("importdescriptors", [{ + "desc": descsum_create(f"tr({KEYS[0]['xprv']}/*)"), "timestamp": "now", + "active": True, "internal": False, "range": [0, 63], "next_index": 0, + }], wallet=True) + check(imported[0]["success"], "The public test owner-key fixture could not be imported") + self.mining_address = self.nodes[0].rpc("getnewaddress", "Thaw Day test mining", "bech32", wallet=True) + self.state["mining_address"] = self.mining_address + self.save_state() + self.note("Eight separate test wallets are open", {"run_dir": str(self.run)}) + + def sync(self): + tip = self.nodes[0].rpc("getbestblockhash") + wait_for(lambda: all(n.rpc("getbestblockhash") == tip for n in self.nodes if n.record.get("running")), + "test nodes to agree on the exact block hash", 300) + for node in self.nodes: + if node.record.get("running"): + node.rpc("syncwithvalidationinterfacequeue") + + def reconnect_hub(self): + # After a node reindexes, the peer connections it accepted while still in + # initial block download stop relaying new blocks. The hub (node 0) + # accepts the spokes' connections, so a hub reindex leaves every spoke + # connected but not receiving new blocks. Re-establish each running + # spoke's link to the hub so block relay resumes cleanly. + hub = self.nodes[0] + addr = f"127.0.0.1:{hub.record['p2p_port']}" + for node in self.nodes[1:]: + if not node.record.get("running"): + continue + for method, params in (("disconnectnode", [addr]), ("addnode", [addr, "onetry"])): + try: + node.rpc(method, *params) + except RPCError: + pass + wait_for(lambda: all(n.rpc("getconnectioncount") >= 1 for n in self.nodes[1:] + if n.record.get("running")), + "spokes to reconnect to the hub", 60) + + def mine(self, count=1, ceiling=None): + height = self.nodes[0].rpc("getblockcount") + check(ceiling is None or height + count <= ceiling, "Test helper would cross Thaw Day too early") + result = self.nodes[0].rpc("generatetoaddress", count, self.mining_address, 2000000000, "sha256d", wallet=True) + check(len(result) == count, "Mining returned fewer blocks than requested") + self.sync() + return result + + def mine_to(self, target): + while self.nodes[0].rpc("getblockcount") < target: + current = self.nodes[0].rpc("getblockcount") + self.mine(min(40, target - current), ceiling=target) + if current % 400 == 0: + self.note("Advancing the local test chain", {"height": self.nodes[0].rpc("getblockcount")}) + + def price(self, price): + temporary = self.run / "price-micro-usd.new" + temporary.write_text(str(price)) + temporary.replace(self.run / "price-micro-usd.txt") + + def await_quote(self, price, ceiling=None): + # A restarted miner signs its next price in the next 40-block epoch, + # so the wait must cover a whole epoch plus the signing round. + deadline = time.monotonic() + 420 + while time.monotonic() < deadline: + views = [node.rpc("getprotectionstatus")["volatility"] for node in self.nodes[:3]] + height = self.nodes[0].rpc("getblockcount") + signed = self.signed_bundle(height) + if (signed and signed["price_micro_usd"] == price and signed["epoch"] == (height + 1) // 40 and + signed["height"] > self.miner_started_at and + all(view["quote_available"] and view["candidate_price_micro_usd"] == price for view in views)): + self.note("Candidate price is ready", {"price_micro_usd": price, "signed_height": signed["height"], + "signers": signed["signers"], "miner_started_at": self.miner_started_at, "views": views}) + return + if ceiling is not None: + check(height < ceiling, f"Reached the height limit {ceiling} without a signed candidate quote") + if height + 1 == ceiling and height // 40 == (height + 1) // 40: + # Keep the last block available until the signing round + # finishes. An already-mined empty block cannot gain a bundle. + # At an epoch boundary, this RPC still describes the old + # round, so let the boundary block carry the new bundle. + session = self.nodes[0].rpc("getdigidollardeploymentinfo")["musig2_session"] + if session["state"] != "complete" or session["epoch"] != (height + 1) // 40: + time.sleep(3) + continue + self.mine(1, ceiling=ceiling) + time.sleep(3) + raise Failure(f"No signed candidate quote at {price} micro-USD before the deadline") + + def signed_bundle(self, height, expected=None): + sys.path.insert(0, str(ROOT / "test/functional")) + from test_framework.script import CScript + node = self.nodes[0] + block_hash = node.rpc("getblockhash", height) + block = node.rpc("getblock", block_hash, 2) + scripts = [bytes.fromhex(out["scriptPubKey"]["hex"]) for out in block["tx"][0]["vout"]] + matches = [script for script in scripts if script.startswith(b"\x6a\xbf")] + if not matches: + return None + check(len(matches) == 1, "More than one oracle bundle in a coinbase") + fields = list(CScript(matches[0])) + check(len(fields) == 4 and fields[2] == b"\x03", "Unexpected oracle bundle format") + payload = fields[3] + width = payload[0] + check(len(payload) == 1 + width + 4 + 8 + 8 + 64, "Malformed oracle bundle length") + bitmap = int.from_bytes(payload[1:1 + width], "little") + check(bitmap.bit_count() >= 7 and bitmap >> 24 == 0, "Unexpected oracle signer set") + offset = 1 + width + 4 + price = int.from_bytes(payload[offset:offset + 8], "little") + check(expected is None or expected == price, "Signed block price differs from the expected price") + # All nodes must accept this block through their normal validation path. + check(all(n.rpc("getblockhash", height) == block_hash for n in self.nodes), "Nodes disagree on the signed block") + return {"height": height, "hash": block_hash, "price_micro_usd": price, + "epoch": int.from_bytes(payload[1 + width:1 + width + 4], "little"), + "signers": [slot for slot in range(24) if bitmap & (1 << slot)]} + + def confirm(self, node, result, ceiling=None, attempts=12, record=True): + # record=False for an ordinary DGB tx (e.g. a control payment) that must + # not enter dd_transactions, which the accounting pass decodes as DD. + txid = result["txid"] if isinstance(result, dict) else result + for _ in range(attempts): + tx = node.rpc("gettransaction", txid, wallet=True) + if tx["confirmations"] > 0: + self.sync() + if record: + self.dd_transactions[txid] = tx["hex"] + block = node.rpc("getblock", tx["blockhash"]) + check(txid in block["tx"], "Wallet confirmation does not match the block bytes") + check(self.phase != "before" or block["height"] < H, "A before-Thaw transaction confirmed too late") + check(self.phase != "after" or block["height"] >= H, "An after-Thaw transaction confirmed too early") + self.note("Confirmed " + self.phase + " transaction", {"txid": txid, + "block_hash": block["hash"], "height": block["height"]}) + return txid + self.mine(1, ceiling=ceiling) + time.sleep(1) + raise Failure("Transaction did not confirm: " + txid) + + def expect_error(self, node, method, params, words): + before = sorted(node.rpc("getrawmempool")) + try: + node.rpc(method, *params, wallet=True) + except RPCError as error: + check(any(word.lower() in str(error).lower() for word in words), + f"Wrong rejection for {method}: {error}") + check(before == sorted(node.rpc("getrawmempool")), "Rejected call changed the mempool") + return {"code": error.code, "reason": str(error)} + raise Failure("Expected a rejection from " + method) + + def account(self, phase, active): + self.sync() + from test_framework.messages import tx_from_hex + from test_framework.script import CScript, OP_RETURN + from digidollar_thawday_extra_burn import metadata, script_number + tokens = 0 + for txid, raw in self.dd_transactions.items(): + transaction = tx_from_hex(raw) + # A full redemption with no DD change carries no DD OP_RETURN and no + # token output. It is a valid redemption (consensus accepts it) that + # contributes nothing to the circulating token count, so skip it + # rather than demand the metadata every other DD tx has. + dd_marked = [out for out in transaction.vout + if out.scriptPubKey[:1] == bytes([OP_RETURN]) + and len(list(CScript(out.scriptPubKey))) >= 2 + and list(CScript(out.scriptPubKey))[1] == b"DD"] + token_indices = [index for index, out in enumerate(transaction.vout) + if out.nValue == 0 and len(out.scriptPubKey) == 34 + and out.scriptPubKey[:2] == b"\x51\x20"] + if not dd_marked: + check(not token_indices, "A DD tx without metadata unexpectedly has token outputs") + continue + _, fields = metadata(transaction) + kind = script_number(fields[2]) + amounts = [script_number(value) for value in (fields[3:] if kind == 2 else fields[3:4])] + indices = [index for index, out in enumerate(transaction.vout) + if out.nValue == 0 and len(out.scriptPubKey) == 34 and out.scriptPubKey[:2] == b"\x51\x20"] + if kind == 3 and amounts == [0]: + amounts = [] + check(len(indices) == len(amounts), "Independent token count cannot decode this transaction") + tokens += sum(amount for index, amount in zip(indices, amounts) + if self.nodes[0].rpc("gettxout", txid, index, False) is not None) + principal, collateral, vault_count = 0, 0, 0 + for txid, index, original in self.vaults: + coin = self.nodes[0].rpc("gettxout", txid, index, False) + if coin is not None: + principal += original + collateral += int(coin["value"] * 100000000) + vault_count += 1 + check(tokens == self.expected_supply, "Independent token coins disagree with the test ledger") + check(principal == self.expected_principal and vault_count == len(self.open_positions), + "Independent vault coins disagree with the test ledger") + records = [] + for node in self.nodes: + stats = node.rpc("getdigidollarstats") + # Below Thaw Day, exact circulating supply is only guaranteed on a node + # with a synchronized stats index. Without it, getdigidollarstats uses + # the documented legacy fallback, a vault-based figure that after a + # restart does not subtract extra burns (see CLAUDE.md). At and after + # Thaw Day the chainstate owns the supply and it is exact on every node. + # The independent on-chain token count above already checks true + # circulating against the chain, so this only asserts the node's field + # where it is contractually exact. + if active or node.index % 2 == 1: + check(stats["total_dd_supply"] == self.expected_supply, f"Wrong circulating DD on {node.name}") + check(stats["active_positions"] == len(self.open_positions), f"Wrong vault count on {node.name}") + check(int(stats["total_collateral_locked"] * 100000000) == collateral, "Wrong collateral total") + if active: + record = stats["canonical_health"] + check(record["ready"] and record["history_checked"], "Saved health state is not ready") + check(record["format_version"] == 1 and record["rules_version"] == 1 and + record["activation_height"] == H and record["digidollar_height"] == 600 and + record["genesis_hash"] == node.rpc("getblockhash", 0), "Health record has the wrong rules or network") + check(record["block_hash"] == node.rpc("getbestblockhash"), "Health record belongs to a different block") + check(record["open_vault_principal"] == self.expected_principal, "Wrong principal in open vaults") + check(record["collateral"] == collateral and record["active_vaults"] == vault_count, "Wrong saved vault totals") + candidate = node.rpc("getprotectionstatus")["next_block_health"] + check(candidate["ready"], "Candidate health is not ready at the accounting checkpoint") + if candidate["ready"]: + check(candidate["canonical_health"] == record, "Consensus view differs from the statistics view") + quote = candidate["oracle_price_micro_usd"] + health = min(30000, collateral * (quote // 10) * 100 // (100000000 * 1000 * principal)) if principal else 30000 + check(candidate["health_percentage"] == health, "Candidate health uses the wrong amounts or quote") + check(stats["selected_health_denominator"] == "open_vault_principal", "Wrong post-Thaw health definition") + records.append(record) + else: + check(stats["selected_health_denominator"] == "legacy_supply", "New health definition used too early") + if records: + check(all(record == records[0] for record in records), "Nodes disagree on the durable health record") + self.note(phase + ": accounting matches", {"circulating_cents": self.expected_supply, + "open_principal_cents": self.expected_principal, "vaults": len(self.open_positions), + "canonical_record": records[0] if records else None}) + + def recover_wallet(self, node, phase): + amounts = lambda data: {key: data[key] for key in ("confirmed", "unconfirmed", "total")} + before = amounts(node.rpc("getdigidollarbalance", wallet=True)) + positions = node.rpc("listdigidollarpositions", False, wallet=True) + backup = self.run / f"{phase}-{node.name}-wallet.dat" + node.rpc("backupwallet", str(backup), wallet=True) + check(backup.is_file() and backup.stat().st_size > 0, "Wallet backup was not written") + node.stop() + node.start() + node.start_oracles() + self.sync() + check(amounts(node.rpc("getdigidollarbalance", wallet=True)) == before, "Wallet balance changed after restart") + restored_name = node.wallet + "-restored-" + phase + node.rpc("restorewallet", restored_name, str(backup)) + original = node.wallet + node.wallet = restored_name + try: + node.rpc("rescanblockchain", wallet=True) + check(amounts(node.rpc("getdigidollarbalance", wallet=True)) == before, "Backup restore changed DD balance") + after = node.rpc("listdigidollarpositions", False, wallet=True) + keys = ("position_id", "dd_minted", "lock_tier", "is_active", "status") + simplify = lambda items: sorted([{key: item.get(key) for key in keys} for item in items], key=lambda item: item["position_id"]) + check(simplify(after) == simplify(positions), "Backup restore changed vault ownership or status") + finally: + node.wallet = original + node.rpc("unloadwallet", restored_name) + self.note(phase + ": wallet restart and backup restore match", {"wallet": node.name, "backup_sha256": digest(backup)}) + + def core_suite(self, phase, price, active): + self.phase = phase + self.note("Starting the " + phase + " DigiDollar tests", {"price_micro_usd": price}) + self.await_quote(price, None if active else H - 1000) + bob, alice, charlie = self.nodes[:3] + positions = [] + for tier in list(range(10)) + [0, 0]: + result = bob.rpc("mintdigidollar", 10000, tier, wallet=True) + txid = self.confirm(bob, result, None if active else H - 1000) + position = next(item for item in bob.rpc("listdigidollarpositions", wallet=True) if item["position_id"] == txid) + check(position["dd_minted"] == 10000 and position["lock_tier"] == tier, "Minted vault has wrong terms") + self.open_positions[txid] = position + from test_framework.messages import tx_from_hex + raw = tx_from_hex(self.dd_transactions[txid]) + collateral_indices = [index for index, out in enumerate(raw.vout) + if out.nValue > 0 and len(out.scriptPubKey) == 34 and out.scriptPubKey[:2] == b"\x51\x20"] + check(len(collateral_indices) == 1, "Mint has no unique collateral output") + self.vaults.append((txid, collateral_indices[0], 10000)) + positions.append(txid) + self.expected_supply += 10000 + self.expected_principal += 10000 + self.note(phase + ": all ten mint tiers confirmed", {"positions": positions}) + early = self.expect_error(bob, "redeemdigidollar", [positions[0], 10000], ["Position locked until block"]) + self.note(phase + ": early redemption rejected", early) + balances = [node.balance() for node in (bob, alice, charlie)] + for sender, receiver, amount in ((bob, alice, 2500), (alice, charlie, 1000), (charlie, bob, 500)): + address = receiver.rpc("getdigidollaraddress", wallet=True) + self.confirm(sender, sender.rpc("senddigidollar", address, amount, wallet=True), None if active else H - 1000) + expected = [balances[0] - 2000, balances[1] + 1500, balances[2] + 500] + check([node.balance() for node in (bob, alice, charlie)] == expected, "Send/receive totals do not match") + self.note(phase + ": send and receive balances match", {"before": balances, "after": expected}) + unlock = max(self.open_positions[txid]["unlock_height"] for txid in (positions[0], positions[10], positions[11])) + 1 + check(active or unlock < H - 1000, "The pre-Thaw vaults would mature too close to H") + self.mine_to(unlock) + self.await_quote(price, None if active else H - 1000) + partial = self.expect_error(bob, "redeemdigidollar", [positions[0], 5000], ["partial", "full", "entire", "amount"]) + self.note(phase + ": partial vault redemption rejected", partial) + before = bob.balance() + self.confirm(bob, bob.rpc("redeemdigidollar", positions[0], 10000, wallet=True), None if active else H - 1000) + check(bob.balance() == before - 10000, "Normal redemption burned the wrong amount") + self.expected_supply -= 10000 + self.expected_principal -= 10000 + del self.open_positions[positions[0]] + again = self.expect_error(bob, "redeemdigidollar", [positions[0], 10000], ["redeemed", "active", "spent", "found"]) + self.note(phase + ": full redemption confirmed; second redemption rejected", again) + self.state[phase + "_spare_position"] = positions[11] + self.state[phase + "_extra_burn_position"] = positions[10] + self.save_state() + from extra_burn import apply_extra_burn_case + apply_extra_burn_case(self, phase, price, active) + self.await_quote(price, None if active else H - 1000) + self.account(phase, active) + self.recover_wallet(bob, phase) + self.recover_wallet(alice, phase) + self.await_quote(price, None if active else H - 1000) + self.account(phase + " after recovery", active) + + def mint_record(self, node, amount, tier): + from test_framework.messages import tx_from_hex + result = node.rpc("mintdigidollar", amount, tier, wallet=True) + txid = self.confirm(node, result) + position = next(item for item in node.rpc("listdigidollarpositions", wallet=True) + if item["position_id"] == txid) + raw = tx_from_hex(self.dd_transactions[txid]) + indices = [index for index, out in enumerate(raw.vout) + if out.nValue > 0 and len(out.scriptPubKey) == 34 and out.scriptPubKey[:2] == b"\x51\x20"] + check(len(indices) == 1, "Mint has no unique collateral output") + self.vaults.append((txid, indices[0], amount)) + self.open_positions[txid] = position + self.expected_supply += amount + self.expected_principal += amount + return txid + + def mint_only_pause(self): + # This runs entirely after Thaw Day, so there is no boundary to keep + # blocks below. Each price edge (and restoring 45000 at the end) needs a + # full oracle epoch to re-sign, more than a tight block cap allows, so + # let await_quote mine as many blocks as the signing round needs. + ceiling = None + bob, alice, charlie = self.nodes[:3] + for price, restricted, spare in ((54000, True, "after"), (53999, False, None), + (36000, True, "before"), (36001, False, None)): + self.price(price) + self.await_quote(price, ceiling) + status = bob.rpc("getprotectionstatus")["volatility"] + check(status["reference_price_micro_usd"] == NEW_PRICE, "The fixed reference moved during the edge tests") + check(status["minting_restricted"] is restricted, "Wrong result at a 20 percent boundary") + check(status["all_operations_restricted"] is False, "A mint pause blocked all operations") + if restricted: + rejection = self.expect_error(bob, "mintdigidollar", [10000, 0], ["Minting volatility pause"]) + balance = [alice.balance(), charlie.balance()] + self.confirm(alice, alice.rpc("senddigidollar", charlie.rpc("getdigidollaraddress", wallet=True), 100, wallet=True)) + self.confirm(charlie, charlie.rpc("senddigidollar", alice.rpc("getdigidollaraddress", wallet=True), 100, wallet=True)) + check([alice.balance(), charlie.balance()] == balance, "A transfer during the mint pause changed value") + position = self.state[spare + "_spare_position"] + result = bob.rpc("redeemdigidollar", position, 10000, wallet=True) + check(result["err_active"] is False and result["required_dd_burn"] == 10000, + "Mint-pause redemption did not take the expected normal route") + self.confirm(bob, result) + self.expected_supply -= 10000 + self.expected_principal -= 10000 + del self.open_positions[position] + self.note("Mint pause rejects mint but allows send and eligible redemption", { + "price_micro_usd": price, "status": status, "rejection": rejection, + "redemption": result}) + else: + txid = self.mint_record(bob, 10000, 0) + self.note("A mint just inside the price boundary confirmed", {"price_micro_usd": price, "txid": txid}) + self.await_quote(price, ceiling) + self.account("Price boundary " + str(price), True) + self.price(NEW_PRICE) + self.await_quote(NEW_PRICE, ceiling) + + def boundary(self): + self.phase = "boundary" + self.price(NEW_PRICE) + self.await_quote(NEW_PRICE, H - 300) + frozen = self.nodes[0].rpc("getprotectionstatus")["volatility"] + check(frozen["rejection_reason"] == "legacy_volatility_freeze", "The legacy freeze was not reproduced") + rejected = self.expect_error(self.nodes[0], "mintdigidollar", [10000, 0], ["volatility", "frozen", "freeze"]) + self.note("A fresh signed price still hits the old mint freeze", {"status": frozen, "mint_rejection": rejected}) + self.mine_to(H - 280) + self.await_quote(NEW_PRICE, H - 260) + self.mine_to(H - 260) + samples = [] + for _ in range(20): + block = self.mine(1, ceiling=H - 240)[0] + sample = self.signed_bundle(self.nodes[0].rpc("getblockheader", block)["height"]) + if sample and sample["price_micro_usd"] == NEW_PRICE: + samples.append(sample) + time.sleep(1) + check(len(samples) >= 15, "Need at least fifteen signed new-price ancestor samples") + save(self.run / "ancestor-samples.json", samples) + self.mine_to(H - 40) + self.await_quote(NEW_PRICE, H - 10) + self.mine_to(H - 2) + self.await_quote(NEW_PRICE, H - 2) + before = self.nodes[0].rpc("getdigidollardeploymentinfo")["thaw_day"] + check(not before["active_at_tip"] and not before["active_next_block"], "Thaw Day started before H") + self.expect_error(self.nodes[0], "mintdigidollar", [10000, 0], ["volatility", "frozen", "freeze"]) + self.note("Candidate H-1 still uses the old rules", before) + self.mine(1, ceiling=H - 1) + boundary = self.nodes[0].rpc("getdigidollardeploymentinfo")["thaw_day"] + check(not boundary["active_at_tip"] and boundary["active_next_block"], "Candidate H did not select Thaw Day") + self.note("Tip H-1 selects the new rules for the next block", boundary) + # H is also an oracle epoch boundary. An empty block can advance the + # chain while the next signing session gathers a valid quote. + self.mine(1, ceiling=H) + after = self.nodes[0].rpc("getdigidollardeploymentinfo")["thaw_day"] + check(after["active_at_tip"] and after["active_next_block"], "Block H did not activate Thaw Day") + self.note("Block H uses the new rules", after) + self.await_quote(NEW_PRICE) + volatility = self.nodes[0].rpc("getprotectionstatus")["volatility"] + check(volatility["reference_price_micro_usd"] == NEW_PRICE and volatility["sample_count"] == 15, + "The new rule did not use the signed ancestor samples") + check(not volatility["minting_restricted"] and not volatility["all_operations_restricted"], "The old freeze survived Thaw Day") + self.account("Thaw Day boundary", True) + self.note("The old freeze clears under the new ancestor rule", volatility) + + def reindex_node(self, node, phase): + """Reindex one node with its wallet loaded and require the same tip.""" + tip = self.nodes[0].rpc("getbestblockhash") + height = self.nodes[0].rpc("getblockcount") + balance = node.balance() + node.stop() + node.start(reindex=True) + wait_for(lambda: node.rpc("getblockcount") == height and node.rpc("getbestblockhash") == tip, + node.name + " to finish its full reindex", 600) + node.start_oracles() + self.reconnect_hub() + self.sync() + check(node.balance() == balance, node.name + " changed its DD balance across the reindex") + self.note(phase + ": " + node.name + " reindexed to the same block with its wallet loaded", + {"height": height, "hash": tip, "dd_balance_cents": balance}) + + def reindex_and_fresh_sync(self): + # Bob minted and sent DigiDollar, so its wallet holds reused owner + # addresses. A reindex of Bob is the check that would have caught the + # rc1 defect. Heidi never held DigiDollar and reindexes as before. + self.reindex_node(self.nodes[0], "End of run") + self.await_quote(NEW_PRICE) + node = self.nodes[7] + tip = self.nodes[0].rpc("getbestblockhash") + height = self.nodes[0].rpc("getblockcount") + node.stop() + node.start(reindex=True) + wait_for(lambda: node.rpc("getblockcount") == height and node.rpc("getbestblockhash") == tip, + "the small test chain to finish its full reindex", 600) + node.start_oracles() + self.reconnect_hub() + self.sync() + self.account("After full test-chain reindex", True) + fresh = {"name": "fresh", "datadir": str(self.run / "fresh"), "p2p_port": free_port(), "rpc_port": free_port()} + self.state["nodes"].append(fresh) + observer = Node(self, len(self.nodes), fresh) + self.nodes.append(observer) + observer.start() + self.sync() + self.account("After empty-directory fresh sync", True) + self.note("Fresh sync and full test-chain reindex match", {"height": height, "hash": tip}) + + def boundary_reorg(self): + """Replace the boundary blocks without changing any validation rule.""" + original_tip = self.nodes[0].rpc("getbestblockhash") + original_height = self.nodes[0].rpc("getblockcount") + first_removed = self.nodes[0].rpc("getblockhash", H - 1) + parent = self.nodes[0].rpc("getblockhash", H - 2) + before = self.nodes[0].rpc("gettxoutsetinfo", "muhash") + follower = self.nodes[7] + follower.rpc("setnetworkactive", False) + follower.rpc("invalidateblock", first_removed) + check(follower.rpc("getbestblockhash") == parent, "Follower did not rewind below the boundary") + status = follower.rpc("getdigidollardeploymentinfo")["thaw_day"] + check(not status["active_at_tip"] and not status["active_next_block"], "Follower kept activated rules below H") + follower.stop() + follower.record["extra_args"] = ["-networkactive=0"] + follower.start() + check(follower.rpc("getbestblockhash") == parent, "Restart changed the intentionally rewound test branch") + follower.rpc("reconsiderblock", first_removed) + check(follower.rpc("getbestblockhash") == original_tip, "Follower did not restore the original chain") + after = follower.rpc("gettxoutsetinfo", "muhash") + check(before["muhash"] == after["muhash"], "Rewind and replay changed the UTXO set") + follower.record["extra_args"] = [] + follower.rpc("setnetworkactive", True) + follower.start_oracles() + self.account("After boundary rewind, restart, and replay", True) + # Disable peer traffic while building the controlled competing branch. + # Each follower still checks submitted raw blocks through normal validation. + for node in self.nodes: + node.rpc("setnetworkactive", False) + node.rpc("invalidateblock", first_removed) + check(node.rpc("getbestblockhash") == parent, "A node did not reach the fork parent") + address = self.nodes[0].rpc("getnewaddress", "Thaw Day replacement branch", "bech32", wallet=True) + replacement = [] + for _ in range(original_height - (H - 2) + 2): + block_hash = self.nodes[0].rpc("generatetoaddress", 1, address, 2000000000, "sha256d", wallet=True)[0] + raw = self.nodes[0].rpc("getblock", block_hash, 0) + for node in self.nodes[1:]: + result = node.rpc("submitblock", raw) + check(result is None, f"Replacement block was not accepted on {node.name}: {result}") + check(node.rpc("getbestblockhash") == block_hash, "Replacement block did not become the tip") + replacement.append(block_hash) + for node in self.nodes: + node.rpc("reconsiderblock", first_removed) + check(node.rpc("getbestblockhash") == replacement[-1], "Node chose the shorter original branch") + node.rpc("setnetworkactive", True) + self.sync() + self.await_quote(NEW_PRICE) + self.account("After a replacement branch crosses Thaw Day", True) + self.note("Boundary rollback and competing-branch tests passed", { + "old_tip": original_tip, "fork_parent": parent, "replacement_blocks": replacement, + "restored_utxo_muhash": after["muhash"], + "limit": "This short branch test does not replace the older price-sample ancestors."}) + + def run_tests(self): + self.setup() + self.mine_to(3000) + for node in self.nodes[1:]: + address = node.rpc("getnewaddress", "Thaw Day test fees", "bech32", wallet=True) + self.nodes[0].rpc("sendtoaddress", address, 100, wallet=True) + self.mine(5) + for node in self.nodes: + node.start_oracles() + wait_for(lambda: len([entry for entry in self.nodes[0].rpc("getoracles", True) + if entry.get("heartbeat_status") == "fresh"]) == 24, + "all twenty-four signed oracle heartbeats", 180) + self.note("All twenty-four test oracles sent signed heartbeats", {}) + self.core_suite("before", BASE_PRICE, False) + self.reindex_node(self.nodes[0], "before") + self.boundary() + self.boundary_reorg() + self.core_suite("after", NEW_PRICE, True) + self.mint_only_pause() + self.reindex_and_fresh_sync() + self.state["status"] = "PASS" + self.state["final_height"] = self.nodes[0].rpc("getblockcount") + self.state["final_hash"] = self.nodes[0].rpc("getbestblockhash") + self.save_state() + self.note("The implemented rehearsal checks passed", {"height": self.state["final_height"], "hash": self.state["final_hash"]}) + + def stop(self): + for node in reversed(self.nodes): + node.stop() + pid = self.state.get("feed_pid") + if pid and process_identity(pid) == self.state.get("feed_start_time"): + os.kill(pid, signal.SIGTERM) + self.note("Only this rehearsal's test processes were stopped", {}) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("command", choices=("run", "status", "stop", "feed"), nargs="?", default="run") + parser.add_argument("--client", type=Path, default=ROOT / "builds/thawday-client") + parser.add_argument("--run-dir", type=Path) + parser.add_argument("--close-after", action="store_true", help="Close only the test windows when the run ends") + args = parser.parse_args() + os.umask(0o077) + if args.command == "run": + run = args.run_dir or ROOT / "builds/thawday-runs" / time.strftime("%Y%m%d-%H%M%S") + check(not run.exists(), "Use a new run directory; previous evidence must be kept") + run.mkdir(parents=True) + lab = Lab(run, args.client) + try: + lab.run_tests() + except Exception as error: + lab.state["status"] = "FAIL" + lab.state["error"] = str(error) + lab.save_state() + lab.note("The rehearsal stopped at a failed check", {"error": str(error)}) + raise + finally: + if args.close_after: + lab.stop() + else: + print(f"Test windows stay open. Evidence: {run}", flush=True) + print(f"Close only these test windows: ./thawDay.sh stop --run-dir {run}", flush=True) + else: + check(args.run_dir is not None, "Specify the rehearsal directory with --run-dir") + if args.command == "feed": + serve_feed(args.run_dir) + elif args.command == "stop": + Lab(args.run_dir).stop() + else: + print((args.run_dir / "run.json").read_text()) + + +if __name__ == "__main__": + main() diff --git a/contrib/thawday/test_rehearsal.py b/contrib/thawday/test_rehearsal.py new file mode 100644 index 00000000000..4a5426f8b0d --- /dev/null +++ b/contrib/thawday/test_rehearsal.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +"""Check rehearsal control flow without starting nodes or changing a chain.""" + +import unittest +from unittest.mock import patch + +import rehearsal + + +class QuoteWaitTests(unittest.TestCase): + def make_lab(self, *, ready_at=12, reported_epoch=124, bundle_epoch=None, price=45000): + clock = {"now": 0} + lab = object.__new__(rehearsal.Lab) + lab.miner_started_at = 4800 + state = {"height": 4988, "signed": None, "mined": []} + + class Node: + def rpc(self, method): + if method == "getblockcount": + return state["height"] + if method == "getprotectionstatus": + return {"volatility": { + "quote_available": True, + "candidate_price_micro_usd": price, + }} + if method == "getdigidollardeploymentinfo": + return {"musig2_session": { + "epoch": reported_epoch, + "state": "complete" if clock["now"] >= ready_at else "signing", + }} + raise AssertionError(method) + + lab.nodes = [Node(), Node(), Node()] + lab.note = lambda *args: None + lab.signed_bundle = lambda height: state["signed"] + + def mine(count, ceiling=None): + self.assertEqual(count, 1) + state["height"] += 1 + if ceiling is not None: + self.assertLessEqual(state["height"], ceiling) + state["mined"].append((state["height"], clock["now"])) + state["signed"] = None + if clock["now"] >= ready_at: + state["signed"] = { + "price_micro_usd": price, + "epoch": reported_epoch if bundle_epoch is None else bundle_epoch, + "height": state["height"], + "signers": list(range(7)), + } + + lab.mine = mine + return lab, state, clock + + def wait(self, lab, clock, ceiling): + def sleep(seconds): + clock["now"] += seconds + + with patch.object(rehearsal.time, "monotonic", lambda: clock["now"]), \ + patch.object(rehearsal.time, "sleep", sleep): + lab.await_quote(45000, ceiling) + + def test_reserves_last_block_until_signatures_are_ready(self): + lab, state, clock = self.make_lab() + self.wait(lab, clock, 4990) + self.assertEqual(state["height"], 4990) + self.assertEqual(len(state["mined"]), 2) + self.assertGreaterEqual(state["mined"][-1][1], 12) + self.assertEqual(state["signed"]["price_micro_usd"], 45000) + + def test_completed_old_epoch_cannot_spend_the_last_block(self): + lab, state, clock = self.make_lab(ready_at=0, reported_epoch=123) + with self.assertRaises(rehearsal.Failure): + self.wait(lab, clock, 4990) + self.assertEqual(state["height"], 4989) + + def test_reports_exhausted_height_instead_of_waiting_for_an_old_block(self): + lab, state, clock = self.make_lab() + state["height"] = 4990 + with self.assertRaisesRegex(rehearsal.Failure, "height limit"): + self.wait(lab, clock, 4990) + self.assertEqual(clock["now"], 0) + self.assertEqual(state["mined"], []) + + def test_accepts_a_valid_quote_already_at_the_height_limit(self): + lab, state, clock = self.make_lab(ready_at=0) + state["height"] = 4989 + lab.mine(1, ceiling=4990) + self.wait(lab, clock, 4990) + self.assertEqual(len(state["mined"]), 1) + + def test_can_mine_a_ready_bundle_at_the_next_epoch_boundary(self): + # The status RPC describes the tip's round, even when the next round + # has already signed a bundle for the boundary block. + lab, state, clock = self.make_lab(ready_at=0, reported_epoch=124, bundle_epoch=125) + state["height"] = 4999 + self.wait(lab, clock, 5000) + self.assertEqual(state["height"], 5000) + self.assertEqual(state["signed"]["epoch"], 125) + + def test_no_height_limit_still_allows_progress_to_a_signed_block(self): + lab, state, clock = self.make_lab() + self.wait(lab, clock, None) + self.assertGreaterEqual(state["mined"][-1][1], 12) + self.assertIsNotNone(state["signed"]) + + def test_wrong_price_is_never_accepted(self): + lab, state, clock = self.make_lab(ready_at=0, price=30000) + with self.assertRaises(rehearsal.Failure): + self.wait(lab, clock, 4990) + + +if __name__ == "__main__": + unittest.main() diff --git a/digidollar/HOUSEKEEPING_TO-DOs.md b/digidollar/HOUSEKEEPING_TO-DOs.md index e9986465a9e..e3940e09c30 100644 --- a/digidollar/HOUSEKEEPING_TO-DOs.md +++ b/digidollar/HOUSEKEEPING_TO-DOs.md @@ -263,7 +263,7 @@ testnet, or PRE rehearsal material. Tasks: -- Add formal `digidollar/RELEASE_v9.26.2.md` release notes. +- Add formal `doc/release-notes/RELEASE_v9.26.2.md` release notes. - Update root `README.md` to explain that v9.26.2 is the formal DigiDollar mainnet activation release. - Update `doc/release-notes.md`; it still contains stale DigiDollar language diff --git a/doc/README.md b/doc/README.md index c8d7a74c9d7..af07f3a59c7 100644 --- a/doc/README.md +++ b/doc/README.md @@ -67,6 +67,12 @@ The DigiByte repo's [root README](/README.md) contains relevant information on t - [Dnsseed Policy](dnsseed-policy.md) - [Benchmarking](benchmarking.md) +### Release documentation + +- [v9.26.4 pruning documentation](v9.26.4/V9.26.4_PRUNING_EXPLAINER.md) +- [v9.26.2 archived documentation-validation prompt](v9.26.2/Z_DOC_VALIDATION_v9.26.2_PROMPT.md) +- [Release-note archive](release-notes/), including [v8.26.2](release-notes/RELEASE-NOTES-8.26.2.md), [v9.26.2](release-notes/RELEASE_v9.26.2.md), and [v9.26.5](release-notes/RELEASE_v9.26.5.md) + ### Resources * Discuss on the [DigiByteTalk](https://digibytetalk.org/) forums, in the [Development & Technical Discussion board](https://digibytetalk.org/index.php?board=6.0). * Discuss project-specific development on #digibyte-core-dev on Libera Chat. If you don't have an IRC client, you can use [web.libera.chat](https://web.libera.chat/#digibyte-core-dev). diff --git a/doc/digidollar-operations.md b/doc/digidollar-operations.md new file mode 100644 index 00000000000..72f61840a63 --- /dev/null +++ b/doc/digidollar-operations.md @@ -0,0 +1,293 @@ +# DigiDollar node operations + +This guide describes the v9.26.6 development source. Mainnet Thaw Day is +scheduled at **24,490,000**, estimated for November 1, 2026. Testnet26 is +scheduled at **432,100**, estimated for September 18–19, 2026. Block heights +trigger activation. Signet is unsupported and remains unscheduled. No +public-network Thaw Day activation or sustained network test has been completed +for this candidate. These instructions are not a release-readiness announcement. + +## Coordinate the activation + +Publish the tagged source, binaries, checksums and heights at least 14 days +before mainnet activation and at least 3 days before testnet activation. The +current testnet estimate requires distribution by the evening of September 15, +2026, in America/Boise. Height 432,100 is expected on September 18–19, before +the September 21 target. Recheck the block rate, since the calendar estimate +can move. Miners, oracles, exchanges and full-node operators need time to +upgrade and check their nodes. A published height does not prove that all +operators have upgraded or that release verification is complete. + +If the upgrade window or required release checks cannot be met, coordinate a +replacement release before the scheduled height to postpone or disable +activation. Operators must install that replacement before the old height. +Changing an announcement or a local setting cannot postpone activation in an +installed public-network binary. Preserve the prior schedule and replacement +build identities so operators can verify which rules they are running. + +## Upgrade and preserve recovery data + +Use the release owner's reviewed build and verify its published checksums +when a release is available. Before upgrading, record the installed version, +network, tip height and block hash. Back up every wallet with `backupwallet` +to a protected location outside the data directory. Keep existing backups. +A chain database, transaction ID or DD address cannot replace private keys. + +```bash +digibyte-cli -rpcwallet=your_wallet backupwallet "/secure/backup/your_wallet.dat" +digibyte-cli getblockchaininfo +digibyte-cli getdigidollardeploymentinfo +digibyte-cli stop +``` + +Use `-testnet` on each command for testnet26. Wait for the process to exit +before replacing binaries or copying a complete data directory. Preserve +wallet files, configuration, logs and retained blocks and undo data. Undo data +lets the node reverse a block when the chain changes. Start the +replacement with the same network and data directory. Check startup messages, +chain progress, loaded wallets and oracle status before resuming service. + +An ordinary upgrade does not require deleting chain data or running a full +reindex. At or above a scheduled Thaw Day height, the node must establish the +matching health state and verify any previously unchecked history before +using it. Missing or unreadable required blocks need recovery; repeated +restarts or deleting indexes cannot supply missing source data. Preserve the +specific error and follow a reviewed repair plan. Do not treat incomplete +health data as zero or disable validation to get past a startup failure. + +All full-node operators and block producers need the coordinated upgrade, +including services that handle only DGB. Older software may disagree with +blocks under the new rules. Once a height is distributed in binaries, changing +a notice cannot postpone it. After activation, an ordinary downgrade to old +software is not a recovery plan. If chain agreement becomes uncertain, retain +the last agreed hash and diagnostics and coordinate recovery with the release +owner. Exchanges and custodians should have a policy for holding deposits and +withdrawals during such an incident. + +See the [activation guide](../DIGIDOLLAR_ACTIVATION_EXPLAINER.md) for the height +boundary and the [wallet guide](../DIGIDOLLAR_WALLET_INTEGRATION.md) for key +recovery and wallet support. + +## Startup progress and interrupted recovery + +Required oracle and health reconstruction finishes before RPC warmup ends. +Follow the startup log and Qt splash messages. Oracle reconstruction reports +the number of blocks checked out of its scan range. Legacy health +reconstruction reports unspent outputs checked; its total is unknown during +the scan. These are counts for the current stage, not an estimate of remaining +time. Updates are periodic, and a disk operation can delay the next update. +Normal logging includes this progress without `debug=digidollar`. + +When canonical recovery is needed, its first log identifies the chain height, +block hash, genesis hash, record and rule versions, and configured DigiDollar +and accounting heights. Later messages identify the work being performed: + +| Stage | Meaning | +|-------|---------| +| Verifying saved DigiDollar health totals | Reconstructing principal, collateral and vault count from the matching unspent outputs and creating transactions. | +| Checking retained DigiDollar history | Reading the required blocks and undo data before changing an unchecked chain. | +| Rewinding unchecked DigiDollar history | Moving back through history whose activated rules have not yet been checked. | +| Reconstructing DigiDollar health before activation | Establishing the accounting state from which the remaining history can be validated. | +| Verifying DigiDollar history in order | Validating the retained blocks in order under each block's applicable rules. | +| Saving DigiDollar recovery progress | Persisting the resulting chainstate and health record. | + +A start with matching checked history need not visit every stage. Block stages +show completed/total counts; output scans report counts while the total is +unknown. A stage's completion or a cleared progress display does not establish +that startup succeeded. Check the final completion or error message and the +accepted height and hash. Recovery's final log includes elapsed time and the +height and hash where it completed or stopped. + +`DigiDollar health totals verified` means the independently reconstructed +totals agree with the saved totals, or that no prior totals needed comparison. +`DigiDollar health totals repaired` reports the old and new principal in cents, +collateral in satoshis and open-vault count after saving the replacement. +This repairs derived accounting from available source data. It does not repair +missing or damaged block, undo or chainstate files. + +To interrupt startup, press `q` on the Qt splash screen or use the service's +normal stop procedure. RPC commands may still be unavailable during warmup. +Allow the process to exit before copying data or starting it again. Cancellation +is checked between reconstruction steps; it does not interrupt an individual +disk read or an atomic database write immediately. + +Cancelled oracle and legacy-health scans do not publish their partial results. +Canonical recovery can have already saved an earlier chain height. A restart +rechecks that saved state: a rewind leaves unchecked history, while a reindex +preserves only the history completed so far. The remaining blocks still need +normal validation. Do not assume an interrupted recovery kept the original +tip or that the previous stage's percentage is a durable restart position. + +If startup reports unavailable required history, preserve the error, its stage, +the height and hash, and the build and configuration identity. Keep every +wallet and backup, the original data directory, logs, retained blocks and undo +files. Restore the required source data through a reviewed restore or download +procedure that keeps block files, chainstate and indexes consistent. After +repair, start normally with the same network and data directory. These startup +paths do not download or repair missing source files automatically. + +Reported DigiDollar retained-history and canonical-recovery failures end the +current startup attempt without entering the generic database-rebuild prompt. +Reported oracle or late legacy-health reconstruction failures also stop +startup. A service supervisor can still launch another process, so pause its +restart loop while diagnosing a repeated failure. There is no persistent +retry marker to clear and no index to delete to unlock a retry. Repeated +restarts cannot supply missing data. + +After successful startup, check `getdigidollarstats` against the accepted tip. +At and above Thaw Day, `canonical_health.ready` and `history_checked` must be +true, with the matching `block_hash`, genesis, rule versions and activation +heights. Unavailable state is not zero. `open_vault_principal` counts the +original DD amounts attached to vaults that remain open; `total_dd_supply` +reports circulating cents separately. Extra burns can make those values differ +without corrupting accounting. Below Thaw Day, use a synchronized stats index +for verified circulation because the legacy fallback scans vault amounts. +Tip health and `next_block_health` have separate heights, rules and readiness; +the next-block result also requires its applicable oracle quote. A ready tip +record alone does not establish next-block readiness at the activation boundary. + +Source: [startup handling](../src/init.cpp), +[retained-history checks](../src/node/chainstate.cpp), +[canonical recovery](../src/validation.cpp), +[health reconstruction](../src/digidollar/health.cpp), +[oracle reconstruction](../src/oracle/bundle_manager.cpp), +[splash shutdown](../src/qt/splashscreen.cpp) and +[health reporting](../src/rpc/digidollar.cpp). + +## Two nodes behind one router + +Give each node its own peer-to-peer (P2P) listening port and matching TCP +forwarding rule. +For example, reserve these LAN addresses and configure: + +| Node | LAN address | Core P2P port | Router TCP forward | +|------|-------------|---------------|--------------------| +| A | `192.168.1.10` | `12024` | external `12024` to `192.168.1.10:12024` | +| B | `192.168.1.11` | `12028` | external `12028` to `192.168.1.11:12028` | + +Node A's mainnet configuration: + +```ini +[main] +listen=1 +port=12024 +natpmp=0 +upnp=0 +``` + +Node B uses the same settings with `port=12028`. Allow its selected TCP port +in the host firewall too. Disable automatic mapping on both nodes for this +manual setup. One external address and port cannot forward to both nodes at +once. Test inbound reachability from outside the LAN; a successful local +connection does not test the router rule. + +For testnet26, put the settings under `[test]`, select testnet with `testnet=1` +before all sections, and use distinct ports such as `12033` and `12034`. +If both processes run on one computer, they also need separate data directories +and distinct ports for the local remote procedure call (RPC) interface. Keep +RPC bound locally; these router rules are for P2P +traffic, and do not require exposing RPC to the internet. + +If you explicitly advertise an address using `externalip=public-address:port`, +use the reachable external P2P port for that node. This setting advertises an +address; it does not create a router mapping. A router behind carrier NAT may +also need an upstream mapping or a reachable host supplied by its provider. + +Source: [listening port](../src/net.cpp), [port mapping](../src/mapport.cpp) +and [network options](../src/init.cpp). + +## Pruning with DigiDollar + +A DigiDollar spend has to read the block that created the coin it spends. Those +blocks are always at or above the DigiDollar activation floor, so a pruned node +has to keep that whole stretch of chain. + +When `-prune` is set, the node registers a retention floor at startup and keeps +every block from that height to the tip. Blocks below it may still be removed. +The floor is fixed per network and does not move when the chain reorganises. + +| Network | Retention floor | Roughly | +|---------|-----------------|---------| +| Mainnet | 23,627,520 | about 568,000 blocks at the tip recorded on 12 September 2026, growing by about 5,760 a day | +| Testnet26 | 600 | almost the whole chain | +| Regtest | 0 | the whole chain | +| Signet (unsupported) | 0 in chain parameters | No supported node startup | + +Two things follow on mainnet. A small `-prune` target such as `prune=550` cannot +be reached: the node still starts and still removes everything below the floor, +but it will not shrink past the blocks it has to keep. And if a block above the +floor is already missing when a pruned node starts, startup stops with: + +``` +DigiDollar state not ready: retained block history is incomplete. Restore the +required block and undo files or download the missing DigiDollar-era history. +``` + +Restore the missing block and undo files from a backup, or resync. Do not delete +retained history to save space. + +Source: [chainstate load](../src/node/chainstate.cpp), +[prune range](../src/validation.cpp), [pruning](../src/node/blockstorage.cpp). + +## Serve compact block filters + +Compact block filters let supporting light wallets select blocks relevant to +their wallet. Core's peer service is disabled by default. An index alone does +not enable the peer service. Add both settings to the chosen network section: + +```ini +blockfilterindex=basic +peerblockfilters=1 +``` + +Restart the node cleanly after changing its configuration. It needs the source +blocks to build the index. Do not remove retained history to make room for the +index; first check available disk space and the node's storage settings. +Core refuses `peerblockfilters=1` without the basic filter index. + +Check locally: + +```bash +digibyte-cli getindexinfo +digibyte-cli getnetworkinfo +digibyte-cli getbestblockhash +# Use the returned block hash below: +digibyte-cli getblockfilter "block_hash" "basic" +``` + +In `getindexinfo`, check the basic filter index's `synced` flag and +`best_block_height` against the chain tip. `getnetworkinfo.localservicesnames` +should include `COMPACT_FILTERS`. Successful `getblockfilter` proves a local +lookup works. It does not prove that a phone can reach the server or retrieve +filters over P2P. + +Before publishing an endpoint, use an actual supported client on another +network to connect, request filter headers and filters, and continue syncing. +Record the client and Core versions, network, endpoint, date, tested height +range and outcome. Verify the listening port and router rules above. Available +inbound capacity and client retention need separate checks; advertising a +service does not reserve a mobile client's connection. No real-client result +is claimed by this guide. + +Source: [service default](../src/net_processing.h), [startup requirements](../src/init.cpp), +[P2P filter handling](../src/net_processing.cpp), [index status](../src/rpc/node.cpp) +and [local filter lookup](../src/rpc/blockchain.cpp). + +## Routine logs and temporary diagnostics + +Use normal logging for everyday operation. For a diagnostic session, enable +`debug=digidollar` in the selected network section or start with +`-debug=digidollar`. Routine DigiDollar and oracle messages use this category. +Useful errors, warnings and startup/recovery progress still appear without it. +Add `debug=net` only when investigating network traffic; it can be verbose. +Remove temporary debug settings when finished. + +Enabling a debug category changes the default startup log-shrink behavior. +Set `shrinkdebugfile=1` explicitly if startup shrinking is wanted while debug +logging is enabled. This acts at startup, not as a continuous log-size limit. +Monitor free disk space and use the host's log-retention policy for long runs. +Keep relevant diagnostics before rotating or removing logs. + +Source: [DigiDollar category](../src/logging.cpp), +[startup logging options](../src/init/common.cpp) and +[oracle logging](../src/oracle/node.cpp). diff --git a/doc/man/digibyte-cli.1 b/doc/man/digibyte-cli.1 index 1f1330b8c36..5e86525e1f0 100644 --- a/doc/man/digibyte-cli.1 +++ b/doc/man/digibyte-cli.1 @@ -1,13 +1,13 @@ -.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.1. -.TH DIGIBYTE-CLI "1" "July 2024" "digibyte-cli v26.2.0" "User Commands" +.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. +.TH DIGIBYTE-CLI "1" "September 2026" "digibyte-cli v9.26.6rc1" "User Commands" .SH NAME -digibyte-cli \- manual page for digibyte-cli v26.2.0 +digibyte-cli \- manual page for digibyte-cli v9.26.6rc1 .SH SYNOPSIS .B digibyte-cli -[\fI\,options\/\fR] \fI\, \/\fR[\fI\,params\/\fR] \fI\,Send command to DigiByte Core\/\fR +[\fI\,options\/\fR] \fI\, \/\fR[\fI\,params\/\fR] \fI\,Send command to DigiByte\/\fR .br .B digibyte-cli -[\fI\,options\/\fR] \fI\,-named \/\fR[\fI\,name=value\/\fR]... \fI\,Send command to DigiByte Core (with named arguments)\/\fR +[\fI\,options\/\fR] \fI\,-named \/\fR[\fI\,name=value\/\fR]... \fI\,Send command to DigiByte (with named arguments)\/\fR .br .B digibyte-cli [\fI\,options\/\fR] \fI\,help List commands\/\fR @@ -15,7 +15,7 @@ digibyte-cli \- manual page for digibyte-cli v26.2.0 .B digibyte-cli [\fI\,options\/\fR] \fI\,help Get help for a command\/\fR .SH DESCRIPTION -DigiByte Core RPC client version v26.2.0 +DigiByte RPC client version v9.26.6rc1 .SH OPTIONS .HP \-? @@ -91,8 +91,8 @@ Password for JSON\-RPC connections .HP \fB\-rpcport=\fR .IP -Connect to JSON\-RPC on (default: 8332, testnet: 18332, signet: -38332, regtest: 18443) +Connect to JSON\-RPC on (default: 14022, testnet: 14026, signet: +19443, regtest: 18443) .HP \fB\-rpcuser=\fR .IP @@ -170,12 +170,12 @@ network seed node(s)) .IP Use the test chain. Equivalent to \fB\-chain\fR=\fI\,test\/\fR. .SH COPYRIGHT -Copyright (C) 2009-2024 The DigiByte Core developers +Copyright (C) 2009-2026 The DigiByte Core developers -Please contribute if you find DigiByte Core useful. Visit - for further information about the software. +Please contribute if you find DigiByte useful. Visit +for further information about the software. The source code is available from . This is experimental software. Distributed under the MIT software license, see the accompanying file COPYING -or +or \ No newline at end of file diff --git a/doc/man/digibyte-qt.1 b/doc/man/digibyte-qt.1 index 5c23afbb3f7..25bb268a956 100644 --- a/doc/man/digibyte-qt.1 +++ b/doc/man/digibyte-qt.1 @@ -1,12 +1,12 @@ -.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.1. -.TH DIGIBYTE-QT "1" "July 2024" "digibyte-qt v26.2.0" "User Commands" +.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. +.TH DIGIBYTE-QT "1" "September 2026" "digibyte-qt v9.26.6rc1" "User Commands" .SH NAME -digibyte-qt \- manual page for digibyte-qt v26.2.0 +digibyte-qt \- manual page for digibyte-qt v9.26.6rc1 .SH SYNOPSIS .B digibyte-qt [\fI\,command-line options\/\fR] .SH DESCRIPTION -DigiByte Core version v26.2.0 +DigiByte version v9.26.6rc1 .SH OPTIONS .HP \-? @@ -28,9 +28,9 @@ datadir as a warning, not an error. If this block is in the chain assume that it and its ancestors are valid and potentially skip their script verification (0 to verify all, default: -00000000000000000001a0a448d6cf2546b06801389cc030b2b18c6491266815, +ade47d5ccbb92cb1d965b97a187bdbf65bf74be6a3709cb6a01339f8c2856deb, testnet: -0000000000000093bcb68c03a9a168ae252572d348a2eaeba2cdf9231d73206f, +0000000000000000000000000000000000000000000000000000000000000000, signet: 0000013d778ba3f914530f11f6b69869c9fab54acff85acd7b8201d111f19b7f) .HP @@ -81,13 +81,17 @@ Run in the background as a daemon and accept commands (default: 0) Wait for initialization to be finished before exiting. This implies \fB\-daemon\fR (default: 0) .HP +\fB\-dandelion\fR +.IP +Enable Dandelion Transaction Relay Protocol (default: 1) +.HP \fB\-datadir=\fR .IP Specify data directory .HP \fB\-dbcache=\fR .IP -Maximum database cache size MiB (4 to 16384, default: 450). In +Maximum database cache size MiB (4 to 16384, default: 1024). In addition, unused mempool memory is shared for this cache (see \fB\-maxmempool\fR). .HP @@ -178,10 +182,15 @@ having it fork into the background). .IP Execute command on startup. .HP +\fB\-sysperms\fR +.IP +Create new files with system default permissions, instead of umask 077 +(only effective with disabled wallet functionality) +.HP \fB\-txindex\fR .IP Maintain a full transaction index, used by the getrawtransaction rpc -call (default: 0) +call (default: 1) .HP \fB\-version\fR .IP @@ -213,8 +222,8 @@ Default duration (in seconds) of manually configured bans (default: Bind to given address and always listen on it (default: 0.0.0.0). Use [host]:port notation for IPv6. Append =onion to tag any incoming connections to that address and port as incoming Tor connections -(default: 127.0.0.1:8334=onion, testnet: 127.0.0.1:18334=onion, -signet: 127.0.0.1:38334=onion, regtest: 127.0.0.1:18445=onion) +(default: 127.0.0.1:14122=onion, testnet: 127.0.0.1:14126=onion, +signet: 127.0.0.1:19445=onion, regtest: 127.0.0.1:18543=onion) .HP \fB\-cjdnsreachable\fR .IP @@ -338,7 +347,7 @@ Support filtering of blocks and transaction with bloom filters (default: \fB\-port=\fR .IP Listen for connections on . Nodes not using the default ports -(default: 8333, testnet: 18333, signet: 38333, regtest: 18444) +(default: 12024, testnet: 12033, signet: 38443, regtest: 18444) are unlikely to get incoming connections. Not relevant for I2P (see doc/i2p.md). .HP @@ -452,7 +461,7 @@ limited by the fee estimate for the longest target .IP A fee rate (in DGB/kvB) that will be used when fee estimation has insufficient data. 0 to entirely disable the fallbackfee feature. -(default: 0.00) +(default: 0.01) .HP \fB\-keypool=\fR .IP @@ -469,7 +478,7 @@ allows the use of partial spend avoidance (default: 0.00) \fB\-mintxfee=\fR .IP Fee rates (in DGB/kvB) smaller than this are considered zero fee for -transaction creation (default: 0.00001) +transaction creation (default: 0.10) .HP \fB\-paytxfee=\fR .IP @@ -573,11 +582,12 @@ Debugging/Testing options: Output debug and trace logging (default: \fB\-nodebug\fR, supplying is optional). If is not supplied or if = 1, output all debug and trace logging. can be: addrman, -bench, blockstorage, cmpctblock, coindb, estimatefee, http, i2p, -ipc, leveldb, libevent, mempool, mempoolrej, net, proxy, prune, -qt, rand, reindex, rpc, scan, selectcoins, tor, txpackages, -txreconciliation, util, validation, walletdb, zmq. This option -can be specified multiple times to output multiple categories. +bench, blockstorage, cmpctblock, coindb, dandelion, digidollar, +estimatefee, http, i2p, ipc, leveldb, libevent, mempool, +mempoolrej, net, proxy, prune, qt, rand, reindex, rpc, scan, +selectcoins, tor, txpackages, txreconciliation, util, validation, +walletdb, zmq. This option can be specified multiple times to +output multiple categories. .HP \fB\-debugexclude=\fR .IP @@ -611,7 +621,8 @@ Prepend debug output with timestamp (default: 1) \fB\-maxtxfee=\fR .IP Maximum total fees (in DGB) to use in a single wallet transaction; -setting this too low may abort large transactions (default: 0.10) +setting this too low may abort large transactions (default: +100.00) .HP \fB\-printtoconsole\fR .IP @@ -679,7 +690,7 @@ signaling (default: 0) \fB\-minrelaytxfee=\fR .IP Fees (in DGB/kvB) smaller than this are considered zero fee for -relaying, mining and transaction creation (default: 0.00001) +relaying, mining and transaction creation (default: 0.001) .HP \fB\-permitbaremultisig\fR .IP @@ -699,6 +710,10 @@ relaying transactions (default: 1) .PP Block creation options: .HP +\fB\-algo=\fR +.IP +Mining algorithm: sha256d, scrypt, groestl, skein, qubit, odo +.HP \fB\-blockmaxweight=\fR .IP Set maximum BIP141 block weight (default: 3996000) @@ -706,7 +721,7 @@ Set maximum BIP141 block weight (default: 3996000) \fB\-blockmintxfee=\fR .IP Set lowest fee rate (in DGB/kvB) for transactions to be included in -block creation. (default: 0.00001) +block creation. (default: 0.001) .PP RPC server options: .HP @@ -751,8 +766,8 @@ Password for JSON\-RPC connections .HP \fB\-rpcport=\fR .IP -Listen for JSON\-RPC connections on (default: 8332, testnet: -18332, signet: 38332, regtest: 18443) +Listen for JSON\-RPC connections on (default: 14022, testnet: +14026, signet: 19443, regtest: 18443) .HP \fB\-rpcserialversion\fR .IP @@ -810,13 +825,139 @@ Reset all settings changed in the GUI \fB\-splash\fR .IP Show splash screen on startup (default: 1) +.PP +DigiDollar options: +.HP +\fB\-digidollar\fR +.IP +Enable DigiDollar stablecoin features (activation follows the network's +configured block height) +.HP +\fB\-digidollarstatsindex\fR +.IP +Maintain DigiDollar stats index for network\-wide DD supply tracking +(default: 1) +.IP +calculatecollateralrequirement +.IP +Calculate DGB collateral needed for a DD mint (RPC/console) +.IP +estimatecollateral +.IP +Estimate collateral requirement by tier (RPC/console) +.IP +getdcamultiplier +.IP +Get the current DCA multiplier for collateral (RPC/console) +.IP +getdigidollaraddress +.IP +Get or create a DigiDollar receive address (RPC/console) +.IP +getdigidollarbalance +.IP +Show your DigiDollar balance (RPC/console) +.IP +getdigidollardeploymentinfo +.IP +Get DigiDollar activation/deployment status (RPC/console) +.IP +getdigidollarstats +.IP +Get network\-wide DigiDollar statistics (RPC/console) +.IP +getprotectionstatus +.IP +Get liquidation protection status (RPC/console) +.IP +getredemptioninfo +.IP +Get info about a collateral position for redemption (RPC/console) +.IP +importdigidollaraddress +.IP +Validate a DigiDollar address; V1 watch\-only import is unsupported/no\-op +(RPC/console) +.IP +listdigidollaraddresses +.IP +List all DigiDollar addresses in wallet (RPC/console) +.IP +listdigidollarpositions +.IP +List all collateral positions and minted DD (RPC/console) +.IP +listdigidollartxs +.IP +List DigiDollar transaction history (RPC/console) +.IP +mintdigidollar +.IP +Mint DigiDollars by locking DGB as collateral (RPC/console) +.IP +redeemdigidollar +.IP +Redeem DigiDollars to unlock DGB collateral (RPC/console) +.IP +senddigidollar +.IP +Send DigiDollars to another address (RPC/console) +.IP +validateddaddress +.IP +Validate a DigiDollar address (RPC/console) +.PP +Oracle options: +.HP +\fB\-oraclebundlewaitms=\fR +.IP +When exactly one oracle message/attestation short of quorum, wait up to + milliseconds before mining without oracle data (default: +2000, 0 to disable) +.HP +\fB\-oraclebundlewaitpollms=\fR +.IP +Polling interval in milliseconds while waiting for near\-quorum oracle +data (default: 200) +.IP +createoraclekey +.IP +Generate an oracle signing key in your wallet (RPC/console) +.IP +getalloracleprices +.IP +Get price history from all oracles (RPC/console) +.IP +getoracleprice +.IP +Get current oracle\-reported DGB/USD price (RPC/console) +.IP +getoraclepubkey +.IP +Get the public key for an oracle ID (RPC/console) +.IP +getoracles +.IP +List all oracle nodes and their status (RPC/console) +.IP +listoracle +.IP +List oracle configuration and keys (RPC/console) +.IP +startoracle +.IP +Start oracle price reporting for your assigned ID (RPC/console) +.IP +stoporacle +.IP +Stop oracle price reporting (RPC/console) .SH COPYRIGHT -Copyright (C) 2009-2024 The DigiByte Core developers +Copyright (C) 2009-2026 The DigiByte Core developers -Please contribute if you find DigiByte Core useful. Visit - for further information about the software. +Please contribute if you find DigiByte useful. Visit +for further information about the software. The source code is available from . This is experimental software. Distributed under the MIT software license, see the accompanying file COPYING -or +or \ No newline at end of file diff --git a/doc/man/digibyte-tx.1 b/doc/man/digibyte-tx.1 index 9b941d037f0..0d2d791338e 100644 --- a/doc/man/digibyte-tx.1 +++ b/doc/man/digibyte-tx.1 @@ -1,7 +1,7 @@ -.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.1. -.TH DIGIBYTE-TX "1" "July 2024" "digibyte-tx v26.2.0" "User Commands" +.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. +.TH DIGIBYTE-TX "1" "September 2026" "digibyte-tx v9.26.6rc1" "User Commands" .SH NAME -digibyte-tx \- manual page for digibyte-tx v26.2.0 +digibyte-tx \- manual page for digibyte-tx v9.26.6rc1 .SH SYNOPSIS .B digibyte-tx [\fI\,options\/\fR] \fI\, \/\fR[\fI\,commands\/\fR] \fI\,Update hex-encoded digibyte transaction\/\fR @@ -9,7 +9,7 @@ digibyte-tx \- manual page for digibyte-tx v26.2.0 .B digibyte-tx [\fI\,options\/\fR] \fI\,-create \/\fR[\fI\,commands\/\fR] \fI\,Create hex-encoded digibyte transaction\/\fR .SH DESCRIPTION -DigiByte Core digibyte\-tx utility version v26.2.0 +DigiByte digibyte\-tx utility version v9.26.6rc1 .SH OPTIONS .HP \-? @@ -134,12 +134,12 @@ set=NAME:JSON\-STRING .IP Set register NAME to given JSON\-STRING .SH COPYRIGHT -Copyright (C) 2009-2024 The DigiByte Core developers +Copyright (C) 2009-2026 The DigiByte Core developers -Please contribute if you find DigiByte Core useful. Visit - for further information about the software. +Please contribute if you find DigiByte useful. Visit +for further information about the software. The source code is available from . This is experimental software. Distributed under the MIT software license, see the accompanying file COPYING -or +or \ No newline at end of file diff --git a/doc/man/digibyte-util.1 b/doc/man/digibyte-util.1 index 9976688eb19..bc260b4ad3d 100644 --- a/doc/man/digibyte-util.1 +++ b/doc/man/digibyte-util.1 @@ -1,12 +1,12 @@ -.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.1. -.TH DIGIBYTE-UTIL "1" "July 2024" "digibyte-util v26.2.0" "User Commands" +.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. +.TH DIGIBYTE-UTIL "1" "September 2026" "digibyte-util v9.26.6rc1" "User Commands" .SH NAME -digibyte-util \- manual page for digibyte-util v26.2.0 +digibyte-util \- manual page for digibyte-util v9.26.6rc1 .SH SYNOPSIS .B digibyte-util [\fI\,options\/\fR] [\fI\,commands\/\fR] \fI\,Do stuff\/\fR .SH DESCRIPTION -DigiByte Core digibyte\-util utility version v26.2.0 +DigiByte digibyte\-util utility version v9.26.6rc1 .SH OPTIONS .HP \-? @@ -54,12 +54,12 @@ grind .IP Perform proof of work on hex header string .SH COPYRIGHT -Copyright (C) 2009-2024 The DigiByte Core developers +Copyright (C) 2009-2026 The DigiByte Core developers -Please contribute if you find DigiByte Core useful. Visit - for further information about the software. +Please contribute if you find DigiByte useful. Visit +for further information about the software. The source code is available from . This is experimental software. Distributed under the MIT software license, see the accompanying file COPYING -or +or \ No newline at end of file diff --git a/doc/man/digibyte-wallet.1 b/doc/man/digibyte-wallet.1 index 0166b6023e0..e63a24f24ea 100644 --- a/doc/man/digibyte-wallet.1 +++ b/doc/man/digibyte-wallet.1 @@ -1,11 +1,11 @@ -.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.1. -.TH DIGIBYTE-WALLET "1" "July 2024" "digibyte-wallet v26.2.0" "User Commands" +.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. +.TH DIGIBYTE-WALLET "1" "September 2026" "digibyte-wallet v9.26.6rc1" "User Commands" .SH NAME -digibyte-wallet \- manual page for digibyte-wallet v26.2.0 +digibyte-wallet \- manual page for digibyte-wallet v9.26.6rc1 .SH DESCRIPTION -DigiByte Core digibyte\-wallet version v26.2.0 +DigiByte digibyte\-wallet version v9.26.6rc1 .PP -digibyte\-wallet is an offline tool for creating and interacting with DigiByte Core wallet files. +digibyte\-wallet is an offline tool for creating and interacting with DigiByte wallet files. By default digibyte\-wallet will act on wallets in the default mainnet wallet directory in the datadir. To change the target wallet, use the \fB\-datadir\fR, \fB\-wallet\fR and \fB\-regtest\fR/\-signet/\-testnet arguments. .SS "Usage:" @@ -110,12 +110,12 @@ salvage Attempt to recover private keys from a corrupt wallet. Warning: \&'salvage' is experimental. .SH COPYRIGHT -Copyright (C) 2009-2024 The DigiByte Core developers +Copyright (C) 2009-2026 The DigiByte Core developers -Please contribute if you find DigiByte Core useful. Visit - for further information about the software. +Please contribute if you find DigiByte useful. Visit +for further information about the software. The source code is available from . This is experimental software. Distributed under the MIT software license, see the accompanying file COPYING -or +or \ No newline at end of file diff --git a/doc/man/digibyted.1 b/doc/man/digibyted.1 index 3cf6ec5b8d7..a4621fa03c1 100644 --- a/doc/man/digibyted.1 +++ b/doc/man/digibyted.1 @@ -1,12 +1,12 @@ -.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.1. -.TH DIGIBYTED "1" "July 2024" "digibyted v26.2.0" "User Commands" +.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. +.TH DIGIBYTED "1" "September 2026" "digibyted v9.26.6rc1" "User Commands" .SH NAME -digibyted \- manual page for digibyted v26.2.0 +digibyted \- manual page for digibyted v9.26.6rc1 .SH SYNOPSIS .B digibyted -[\fI\,options\/\fR] \fI\,Start DigiByte Core\/\fR +[\fI\,options\/\fR] \fI\,Start DigiByte\/\fR .SH DESCRIPTION -DigiByte Core version v26.2.0 +DigiByte version v9.26.6rc1 .SH OPTIONS .HP \-? @@ -28,9 +28,9 @@ datadir as a warning, not an error. If this block is in the chain assume that it and its ancestors are valid and potentially skip their script verification (0 to verify all, default: -00000000000000000001a0a448d6cf2546b06801389cc030b2b18c6491266815, +ade47d5ccbb92cb1d965b97a187bdbf65bf74be6a3709cb6a01339f8c2856deb, testnet: -0000000000000093bcb68c03a9a168ae252572d348a2eaeba2cdf9231d73206f, +0000000000000000000000000000000000000000000000000000000000000000, signet: 0000013d778ba3f914530f11f6b69869c9fab54acff85acd7b8201d111f19b7f) .HP @@ -81,13 +81,17 @@ Run in the background as a daemon and accept commands (default: 0) Wait for initialization to be finished before exiting. This implies \fB\-daemon\fR (default: 0) .HP +\fB\-dandelion\fR +.IP +Enable Dandelion Transaction Relay Protocol (default: 1) +.HP \fB\-datadir=\fR .IP Specify data directory .HP \fB\-dbcache=\fR .IP -Maximum database cache size MiB (4 to 16384, default: 450). In +Maximum database cache size MiB (4 to 16384, default: 1024). In addition, unused mempool memory is shared for this cache (see \fB\-maxmempool\fR). .HP @@ -178,10 +182,15 @@ having it fork into the background). .IP Execute command on startup. .HP +\fB\-sysperms\fR +.IP +Create new files with system default permissions, instead of umask 077 +(only effective with disabled wallet functionality) +.HP \fB\-txindex\fR .IP Maintain a full transaction index, used by the getrawtransaction rpc -call (default: 0) +call (default: 1) .HP \fB\-version\fR .IP @@ -213,8 +222,8 @@ Default duration (in seconds) of manually configured bans (default: Bind to given address and always listen on it (default: 0.0.0.0). Use [host]:port notation for IPv6. Append =onion to tag any incoming connections to that address and port as incoming Tor connections -(default: 127.0.0.1:8334=onion, testnet: 127.0.0.1:18334=onion, -signet: 127.0.0.1:38334=onion, regtest: 127.0.0.1:18445=onion) +(default: 127.0.0.1:14122=onion, testnet: 127.0.0.1:14126=onion, +signet: 127.0.0.1:19445=onion, regtest: 127.0.0.1:18543=onion) .HP \fB\-cjdnsreachable\fR .IP @@ -338,7 +347,7 @@ Support filtering of blocks and transaction with bloom filters (default: \fB\-port=\fR .IP Listen for connections on . Nodes not using the default ports -(default: 8333, testnet: 18333, signet: 38333, regtest: 18444) +(default: 12024, testnet: 12033, signet: 38443, regtest: 18444) are unlikely to get incoming connections. Not relevant for I2P (see doc/i2p.md). .HP @@ -452,7 +461,7 @@ limited by the fee estimate for the longest target .IP A fee rate (in DGB/kvB) that will be used when fee estimation has insufficient data. 0 to entirely disable the fallbackfee feature. -(default: 0.00) +(default: 0.01) .HP \fB\-keypool=\fR .IP @@ -469,7 +478,7 @@ allows the use of partial spend avoidance (default: 0.00) \fB\-mintxfee=\fR .IP Fee rates (in DGB/kvB) smaller than this are considered zero fee for -transaction creation (default: 0.00001) +transaction creation (default: 0.10) .HP \fB\-paytxfee=\fR .IP @@ -573,11 +582,12 @@ Debugging/Testing options: Output debug and trace logging (default: \fB\-nodebug\fR, supplying is optional). If is not supplied or if = 1, output all debug and trace logging. can be: addrman, -bench, blockstorage, cmpctblock, coindb, estimatefee, http, i2p, -ipc, leveldb, libevent, mempool, mempoolrej, net, proxy, prune, -qt, rand, reindex, rpc, scan, selectcoins, tor, txpackages, -txreconciliation, util, validation, walletdb, zmq. This option -can be specified multiple times to output multiple categories. +bench, blockstorage, cmpctblock, coindb, dandelion, digidollar, +estimatefee, http, i2p, ipc, leveldb, libevent, mempool, +mempoolrej, net, proxy, prune, qt, rand, reindex, rpc, scan, +selectcoins, tor, txpackages, txreconciliation, util, validation, +walletdb, zmq. This option can be specified multiple times to +output multiple categories. .HP \fB\-debugexclude=\fR .IP @@ -611,7 +621,8 @@ Prepend debug output with timestamp (default: 1) \fB\-maxtxfee=\fR .IP Maximum total fees (in DGB) to use in a single wallet transaction; -setting this too low may abort large transactions (default: 0.10) +setting this too low may abort large transactions (default: +100.00) .HP \fB\-printtoconsole\fR .IP @@ -679,7 +690,7 @@ signaling (default: 0) \fB\-minrelaytxfee=\fR .IP Fees (in DGB/kvB) smaller than this are considered zero fee for -relaying, mining and transaction creation (default: 0.00001) +relaying, mining and transaction creation (default: 0.001) .HP \fB\-permitbaremultisig\fR .IP @@ -699,6 +710,10 @@ relaying transactions (default: 1) .PP Block creation options: .HP +\fB\-algo=\fR +.IP +Mining algorithm: sha256d, scrypt, groestl, skein, qubit, odo +.HP \fB\-blockmaxweight=\fR .IP Set maximum BIP141 block weight (default: 3996000) @@ -706,7 +721,7 @@ Set maximum BIP141 block weight (default: 3996000) \fB\-blockmintxfee=\fR .IP Set lowest fee rate (in DGB/kvB) for transactions to be included in -block creation. (default: 0.00001) +block creation. (default: 0.001) .PP RPC server options: .HP @@ -751,8 +766,8 @@ Password for JSON\-RPC connections .HP \fB\-rpcport=\fR .IP -Listen for JSON\-RPC connections on (default: 8332, testnet: -18332, signet: 38332, regtest: 18443) +Listen for JSON\-RPC connections on (default: 14022, testnet: +14026, signet: 19443, regtest: 18443) .HP \fB\-rpcserialversion\fR .IP @@ -788,13 +803,139 @@ subject to empty whitelists. \fB\-server\fR .IP Accept command line and JSON\-RPC commands +.PP +DigiDollar options: +.HP +\fB\-digidollar\fR +.IP +Enable DigiDollar stablecoin features (activation follows the network's +configured block height) +.HP +\fB\-digidollarstatsindex\fR +.IP +Maintain DigiDollar stats index for network\-wide DD supply tracking +(default: 1) +.IP +calculatecollateralrequirement +.IP +Calculate DGB collateral needed for a DD mint (RPC/console) +.IP +estimatecollateral +.IP +Estimate collateral requirement by tier (RPC/console) +.IP +getdcamultiplier +.IP +Get the current DCA multiplier for collateral (RPC/console) +.IP +getdigidollaraddress +.IP +Get or create a DigiDollar receive address (RPC/console) +.IP +getdigidollarbalance +.IP +Show your DigiDollar balance (RPC/console) +.IP +getdigidollardeploymentinfo +.IP +Get DigiDollar activation/deployment status (RPC/console) +.IP +getdigidollarstats +.IP +Get network\-wide DigiDollar statistics (RPC/console) +.IP +getprotectionstatus +.IP +Get liquidation protection status (RPC/console) +.IP +getredemptioninfo +.IP +Get info about a collateral position for redemption (RPC/console) +.IP +importdigidollaraddress +.IP +Validate a DigiDollar address; V1 watch\-only import is unsupported/no\-op +(RPC/console) +.IP +listdigidollaraddresses +.IP +List all DigiDollar addresses in wallet (RPC/console) +.IP +listdigidollarpositions +.IP +List all collateral positions and minted DD (RPC/console) +.IP +listdigidollartxs +.IP +List DigiDollar transaction history (RPC/console) +.IP +mintdigidollar +.IP +Mint DigiDollars by locking DGB as collateral (RPC/console) +.IP +redeemdigidollar +.IP +Redeem DigiDollars to unlock DGB collateral (RPC/console) +.IP +senddigidollar +.IP +Send DigiDollars to another address (RPC/console) +.IP +validateddaddress +.IP +Validate a DigiDollar address (RPC/console) +.PP +Oracle options: +.HP +\fB\-oraclebundlewaitms=\fR +.IP +When exactly one oracle message/attestation short of quorum, wait up to + milliseconds before mining without oracle data (default: +2000, 0 to disable) +.HP +\fB\-oraclebundlewaitpollms=\fR +.IP +Polling interval in milliseconds while waiting for near\-quorum oracle +data (default: 200) +.IP +createoraclekey +.IP +Generate an oracle signing key in your wallet (RPC/console) +.IP +getalloracleprices +.IP +Get price history from all oracles (RPC/console) +.IP +getoracleprice +.IP +Get current oracle\-reported DGB/USD price (RPC/console) +.IP +getoraclepubkey +.IP +Get the public key for an oracle ID (RPC/console) +.IP +getoracles +.IP +List all oracle nodes and their status (RPC/console) +.IP +listoracle +.IP +List oracle configuration and keys (RPC/console) +.IP +startoracle +.IP +Start oracle price reporting for your assigned ID (RPC/console) +.IP +stoporacle +.IP +Stop oracle price reporting (RPC/console) .SH COPYRIGHT -Copyright (C) 2009-2024 The DigiByte Core developers +Copyright (C) 2009-2026 The DigiByte Core developers -Please contribute if you find DigiByte Core useful. Visit - for further information about the software. +Please contribute if you find DigiByte useful. Visit +for further information about the software. The source code is available from . This is experimental software. Distributed under the MIT software license, see the accompanying file COPYING -or +or \ No newline at end of file diff --git a/doc/release-notes.md b/doc/release-notes.md index ff72e0230ae..a69ce249a47 100644 --- a/doc/release-notes.md +++ b/doc/release-notes.md @@ -1,361 +1,10 @@ -26.2 Release Notes -================== +# DigiByte Core v9.26.6 release notes -DigiByte Core version 26.2 is now available from: +The combined [v9.26.6 release notes](../RELEASE_v9.26.6.md) contain the final +changes above the preserved RC2 and RC1 notes, the change counts, activation +heights, upgrade information and verification limits. - +That document is the single release-note source for v9.26.6. +Release packages must be verified before distribution. -This release includes new features, various bug fixes and performance -improvements, as well as updated translations. - -Please report bugs using the issue tracker at GitHub: - - - -To receive security and update notifications, please subscribe to: - - - -How to Upgrade -============== - - -DigiDollar Oracle Phase 3: MuSig2 Aggregate Signatures -------------------------------------------------------- - -Phase 3 of the DigiDollar oracle system introduces MuSig2 (BIP-327) aggregate -signatures, replacing the individual per-oracle Schnorr signatures used in -Phase 2. This reduces on-chain oracle data from ~277 bytes (Phase 2, 4 oracles) -to ~84 bytes (Phase 3, 17 oracles) by combining all participant signatures into -a single 64-byte aggregate signature with a compact participation bitmap. - -### Activation Heights - -- **Mainnet**: TBD (will be set after final testnet validation) -- **Testnet**: Block 1,000 -- **Regtest**: Block 10 - -### Bundle Format (v0x03) - -The new v0x03 on-chain format is: -`OP_RETURN OP_ORACLE <0x03> ` - -### Version Gating - -v0x03 bundles are rejected before the Phase 3 activation height on each network. -The `nDigiDollarPhase3Height` consensus parameter controls activation. Nodes -running this version will correctly parse and validate v0x03 bundles once Phase 3 -activates, while continuing to accept v0x01 and v0x02 bundles from earlier phases. - - -Performance Improvements --------------- - -Validation speed and network propagation performance have been greatly -improved, leading to much shorter sync and initial block download times. - -- The script signature cache has been reimplemented as a "cuckoo cache", - allowing for more signatures to be cached and faster lookups. -- Assumed-valid blocks have been introduced which allows script validation to - be skipped for ancestors of known-good blocks, without changing the security - model. See below for more details. -- In some cases, compact blocks are now relayed before being fully validated as - per BIP152. -- P2P networking has been refactored with a focus on concurrency and - throughput. Network operations are no longer bottlenecked by validation. As a - result, block fetching is several times faster than previous releases in many - cases. -- The UTXO cache now claims unused mempool memory. This speeds up initial block - download as UTXO lookups are a major bottleneck there, and there is no use for - the mempool at that stage. - - -Manual Pruning --------------- - -DigiByte Core has supported automatically pruning the blockchain since 0.11. Pruning -the blockchain allows for significant storage space savings as the vast majority of -the downloaded data can be discarded after processing so very little of it remains -on the disk. - -Manual block pruning can now be enabled by setting `-prune=1`. Once that is set, -the RPC command `pruneblockchain` can be used to prune the blockchain up to the -specified height or timestamp. - -`getinfo` Deprecated --------------------- - -The `getinfo` RPC command has been deprecated. Each field in the RPC call -has been moved to another command's output with that command also giving -additional information that `getinfo` did not provide. The following table -shows where each field has been moved to: - -|`getinfo` field | Moved to | -|------------------|-------------------------------------------| -`"version"` | `getnetworkinfo()["version"]` -`"protocolversion"`| `getnetworkinfo()["protocolversion"]` -`"walletversion"` | `getwalletinfo()["walletversion"]` -`"balance"` | `getwalletinfo()["balance"]` -`"blocks"` | `getblockchaininfo()["blocks"]` -`"timeoffset"` | `getnetworkinfo()["timeoffset"]` -`"connections"` | `getnetworkinfo()["connections"]` -`"proxy"` | `getnetworkinfo()["networks"][0]["proxy"]` -`"difficulty"` | `getblockchaininfo()["difficulty"]` -`"testnet"` | `getblockchaininfo()["chain"] == "test"` -`"keypoololdest"` | `getwalletinfo()["keypoololdest"]` -`"keypoolsize"` | `getwalletinfo()["keypoolsize"]` -`"unlocked_until"` | `getwalletinfo()["unlocked_until"]` -`"paytxfee"` | `getwalletinfo()["paytxfee"]` -`"relayfee"` | `getnetworkinfo()["relayfee"]` -`"errors"` | `getnetworkinfo()["warnings"]` - -ZMQ On Windows --------------- - -Previously the ZeroMQ notification system was unavailable on Windows -due to various issues with ZMQ. These have been fixed upstream and -now ZMQ can be used on Windows. Please see [this document](https://github.com/digibyte-core/digibyte/blob/master/doc/zmq.md) for -help with using ZMQ in general. - -Nested RPC Commands in Debug Console ------------------------------------- - -The ability to nest RPC commands has been added to the debug console. This -allows users to have the output of a command become the input to another -command without running the commands separately. - -The nested RPC commands use bracket syntax (i.e. `getwalletinfo()`) and can -be nested (i.e. `getblock(getblockhash(1))`). Simple queries can be -done with square brackets where object values are accessed with either an -array index or a non-quoted string (i.e. `listunspent()[0][txid]`). Both -commas and spaces can be used to separate parameters in both the bracket syntax -and normal RPC command syntax. - -Network Activity Toggle ------------------------ - -A RPC command and GUI toggle have been added to enable or disable all p2p -network activity. The network status icon in the bottom right hand corner -is now the GUI toggle. Clicking the icon will either enable or disable all -p2p network activity. If network activity is disabled, the icon will -be grayed out with an X on top of it. - -Additionally the `setnetworkactive` RPC command has been added which does -the same thing as the GUI icon. The command takes one boolean parameter, -`true` enables networking and `false` disables it. - -Out-of-sync Modal Info Layer ----------------------------- - -When DigiByte Core is out-of-sync on startup, a semi-transparent information -layer will be shown over top of the normal display. This layer contains -details about the current sync progress and estimates the amount of time -remaining to finish syncing. This layer can also be hidden and subsequently -unhidden by clicking on the progress bar at the bottom of the window. - -Support for JSON-RPC Named Arguments ------------------------------------- - -Commands sent over the JSON-RPC interface and through the `digibyte-cli` binary -can now use named arguments. This follows the [JSON-RPC specification](http://www.jsonrpc.org/specification) -for passing parameters by-name with an object. - -`digibyte-cli` has been updated to support this by parsing `name=value` arguments -when the `-named` option is given. - -Some examples: - - src/digibyte-cli -named help command="help" - src/digibyte-cli -named getblockhash height=0 - src/digibyte-cli -named getblock blockhash=000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f - src/digibyte-cli -named sendtoaddress address="(snip)" amount="1.0" subtractfeefromamount=true - -The order of arguments doesn't matter in this case. Named arguments are also -useful to leave out arguments that should stay at their default value. The -rarely-used arguments `comment` and `comment_to` to `sendtoaddress`, for example, can -be left out. However, this is not yet implemented for many RPC calls, this is -expected to land in a later release. - -The RPC server remains fully backwards compatible with positional arguments. - -Opt into RBF When Sending -------------------------- - -A new startup option, `-walletrbf`, has been added to allow users to have all -transactions sent opt into RBF support. The default value for this option is -currently `false`, so transactions will not opt into RBF by default. The new -`bumpfee` RPC can be used to replace transactions that opt into RBF. - -Sensitive Data Is No Longer Stored In Debug Console History ------------------------------------------------------------ - -The debug console maintains a history of previously entered commands that can be -accessed by pressing the Up-arrow key so that users can easily reuse previously -entered commands. Commands which have sensitive information such as passphrases and -private keys will now have a `(...)` in place of the parameters when accessed through -the history. - -Retaining the Mempool Across Restarts -------------------------------------- - -The mempool will be saved to the data directory prior to shutdown -to a `mempool.dat` file. This file preserves the mempool so that when the node -restarts the mempool can be filled with transactions without waiting for new transactions -to be created. This will also preserve any changes made to a transaction through -commands such as `prioritisetransaction` so that those changes will not be lost. - -Final Alert ------------ - -The Alert System was [disabled and deprecated](https://digibyte.org/en/alert/2016-11-01-alert-retirement) in DigiByte Core 0.12.1 and removed in 0.13.0. -The Alert System was retired with a maximum sequence final alert which causes any nodes -supporting the Alert System to display a static hard-coded "Alert Key Compromised" message which also -prevents any other alerts from overriding it. This final alert is hard-coded into this release -so that all old nodes receive the final alert. - -GUI Changes ------------ - - - After resetting the options by clicking the `Reset Options` button - in the options dialog or with the `-resetguioptions` startup option, - the user will be prompted to choose the data directory again. This - is to ensure that custom data directories will be kept after the - option reset which clears the custom data directory set via the choose - datadir dialog. - - - Multiple peers can now be selected in the list of peers in the debug - window. This allows for users to ban or disconnect multiple peers - simultaneously instead of banning them one at a time. - - - An indicator has been added to the bottom right hand corner of the main - window to indicate whether the wallet being used is a HD wallet. This - icon will be grayed out with an X on top of it if the wallet is not a - HD wallet. - -Low-level RPC changes ----------------------- - - - `importprunedfunds` only accepts two required arguments. Some versions accept - an optional third arg, which was always ignored. Make sure to never pass more - than two arguments. - - - The first boolean argument to `getaddednodeinfo` has been removed. This is - an incompatible change. - - - RPC command `getmininginfo` loses the "testnet" field in favor of the more - generic "chain" (which has been present for years). - - - A new RPC command `preciousblock` has been added which marks a block as - precious. A precious block will be treated as if it were received earlier - than a competing block. - - - A new RPC command `importmulti` has been added which receives an array of - JSON objects representing the intention of importing a public key, a - private key, an address and script/p2sh - - - Use of `getrawtransaction` for retrieving confirmed transactions with unspent - outputs has been deprecated. For now this will still work, but in the future - it may change to only be able to retrieve information about transactions in - the mempool or if `txindex` is enabled. - - - A new RPC command `getmemoryinfo` has been added which will return information - about the memory usage of DigiByte Core. This was added in conjunction with - optimizations to memory management. See [Pull #8753](https://github.com/digibyte-core/digibyte/pull/8753) - for more information. - - - A new RPC command `bumpfee` has been added which allows replacing an - unconfirmed wallet transaction that signaled RBF (see the `-walletrbf` - startup option above) with a new transaction that pays a higher fee, and - should be more likely to get confirmed quickly. - -HTTP REST Changes ------------------ - - - UTXO set query (`GET /rest/getutxos//-/- - /.../-.`) responses were changed to return status - code `HTTP_BAD_REQUEST` (400) instead of `HTTP_INTERNAL_SERVER_ERROR` (500) - when requests contain invalid parameters. - -Minimum Fee Rate Policies -------------------------- - -Since the changes in 0.12 to automatically limit the size of the mempool and improve the performance of block creation in mining code it has not been important for relay nodes or miners to set `-minrelaytxfee`. With this release the following concepts that were tied to this option have been separated out: -- incremental relay fee used for calculating BIP 125 replacement and mempool limiting. (1000 satoshis/kB) -- calculation of threshold for a dust output. (effectively 3 * 1000 satoshis/kB) -- minimum fee rate of a package of transactions to be included in a block created by the mining code. If miners wish to set this minimum they can use the new `-blockmintxfee` option. (defaults to 1000 satoshis/kB) - -The `-minrelaytxfee` option continues to exist but is recommended to be left unset. -======= -If you are running an older version, shut it down. Wait until it has completely -shut down (which might take a few minutes in some cases), then run the -installer (on Windows) or just copy over `/Applications/DigiByte-Qt` (on macOS) -or `digibyted`/`digibyte-qt` (on Linux). - -Upgrading directly from a version of DigiByte Core that has reached its EOL is -possible, but it might take some time if the data directory needs to be migrated. Old -wallet versions of DigiByte Core are generally supported. - -Compatibility -============== - -DigiByte Core is supported and extensively tested on operating systems -using the Linux kernel, macOS 11.0+, and Windows 7 and newer. DigiByte -Core should also work on most other Unix-like systems but is not as -frequently tested on them. It is not recommended to use DigiByte Core on -unsupported systems. - -Notable changes -=============== - -### Script - -- #29853: sign: don't assume we are parsing a sane TapMiniscript - -### P2P and network changes - -- #29691: Change Luke Dashjr seed to dashjr-list-of-p2p-nodes.us -- #30085: p2p: detect addnode cjdns peers in GetAddedNodeInfo() - -### RPC - -- #29869: rpc, bugfix: Enforce maximum value for setmocktime -- #28554: bugfix: throw an error if an invalid parameter is passed to getnetworkhashps RPC -- #30094: rpc: move UniValue in blockToJSON -- #29870: rpc: Reword SighashFromStr error message - -### Build - -- #29747: depends: fix mingw-w64 Qt DEBUG=1 build -- #29985: depends: Fix build of Qt for 32-bit platforms with recent glibc -- #30151: depends: Fetch miniupnpc sources from an alternative website -- #30283: upnp: fix build with miniupnpc 2.2.8 - -### Misc - -- #29776: ThreadSanitizer: Fix #29767 -- #29856: ci: Bump s390x to ubuntu:24.04 -- #29764: doc: Suggest installing dev packages for debian/ubuntu qt5 build -- #30149: contrib: Renew Windows code signing certificate - -Credits -======= - -Thanks to everyone who directly contributed to this release: - -- Antoine Poinsot -- Ava Chow -- Cory Fields -- dergoegge -- fanquake -- glozow -- Hennadii Stepanov -- Jameson Lopp -- jonatack -- laanwj -- Luke Dashjr -- MarcoFalke -- nanlour -- willcl-ark - -As well as to everyone that helped with translations on -[Transifex](https://www.transifex.com/digibyte/digibyte/). +Notes for earlier releases are under [release-notes/](release-notes/). diff --git a/RELEASE-NOTES-8.26.2.md b/doc/release-notes/RELEASE-NOTES-8.26.2.md similarity index 100% rename from RELEASE-NOTES-8.26.2.md rename to doc/release-notes/RELEASE-NOTES-8.26.2.md diff --git a/RELEASE_v9.26.2.md b/doc/release-notes/RELEASE_v9.26.2.md similarity index 100% rename from RELEASE_v9.26.2.md rename to doc/release-notes/RELEASE_v9.26.2.md diff --git a/RELEASE_v9.26.5.md b/doc/release-notes/RELEASE_v9.26.5.md similarity index 100% rename from RELEASE_v9.26.5.md rename to doc/release-notes/RELEASE_v9.26.5.md diff --git a/doc/release-notes/release-notes-9.26.4.md b/doc/release-notes/release-notes-9.26.4.md index c3455cc3831..fe84ded3e06 100644 --- a/doc/release-notes/release-notes-9.26.4.md +++ b/doc/release-notes/release-notes-9.26.4.md @@ -14,7 +14,7 @@ the floor is never treated as vault collateral, whether it appears as the redemp input 0 (rejected as `bad-collateral-release-not-vault`) or as a fee input (see `ValidateCollateralReleaseAmount` in `src/digidollar/validation.cpp`). This rule is what keeps pruned and full nodes in agreement, and the pre-release mainnet scan found -no existing coins it affects (`V9.26.4_MAINNET_VALIDATION.md`). v9.26.2's Groestl +no existing coins it affects (`doc/v9.26.4/V9.26.4_MAINNET_VALIDATION.md`). v9.26.2's Groestl algolock and the DigiDollar BIP9 deployment are carried forward unchanged, and a pruned v9.26.4 node accepts and rejects exactly the same blocks and transactions as a full node. Upgrading is optional — a node that does not set `-prune` behaves like diff --git a/doc/release-notes/release-notes-9.26.6.md b/doc/release-notes/release-notes-9.26.6.md new file mode 100644 index 00000000000..c23edebf5ec --- /dev/null +++ b/doc/release-notes/release-notes-9.26.6.md @@ -0,0 +1,6 @@ +# DigiByte Core v9.26.6 release notes + +Read the combined [v9.26.6 release notes](../../RELEASE_v9.26.6.md), with the +final changes above the preserved RC2 and RC1 notes. That document is the single source +for the change counts, activation heights, upgrade information and +verification limits. diff --git a/doc/v9.26.2/Z_DOC_VALIDATION_v9.26.2_PROMPT.md b/doc/v9.26.2/Z_DOC_VALIDATION_v9.26.2_PROMPT.md new file mode 100644 index 00000000000..eb01267c832 --- /dev/null +++ b/doc/v9.26.2/Z_DOC_VALIDATION_v9.26.2_PROMPT.md @@ -0,0 +1,464 @@ +# v9.26.2 — Final Root-Markdown Release Validation Prompt + +**Archive note:** This prompt records the v9.26.2 documentation review. Its root-directory scope, branch, and release settings describe that historical review. File references now point to their current locations. This archived copy is retained in version control. Repository file paths below are relative to the repository root. + +## Goal + +Validate that **every Markdown (`.md`) file in the repository root directory** is +accurate, current, internally consistent, and ready to ship with the formal, +full, final **DigiByte Core v9.26.2 / DigiDollar v1** release. + +"Ready to ship" means each root `.md` file is proven, against live source on the +current `feature/digidollar-v1` tree, to: + +1. **Match the code.** Every factual, technical, numeric, or behavioral claim is + backed by current source (`src/`), build files, chainparams, tests, scripts, + or RPC/Qt registration — not by memory, not by an older release note, not by + a stale sibling doc. +2. **Reflect DigiByte and DGB correctly.** The docs describe DigiByte (DGB), the + DigiByte chain values, DigiByte ports/addresses/magic, and DigiDollar — not + Bitcoin (BTC), Bitcoin defaults, or Bitcoin Core lineage values left over + from the upstream fork. Inherited Bitcoin terminology, BTC units, Bitcoin + block times, Bitcoin ports, Bitcoin RPC examples, or "Bitcoin Core" branding + that should read "DigiByte Core" are defects. +3. **Read clearly for both AI agents and humans.** Each doc is well-structured, + unambiguous, navigable, free of contradictions, free of dead/broken internal + references, and easy for an AI agent or a human reader (developer, exchange, + miner, oracle operator, or end user) to understand and act on. +4. **State the correct version and release status.** Where a doc references a + version, it agrees with the source of truth: `configure.ac` + (`9` / `26` / `2`, `RC=0`, `IS_RELEASE=true`) and `src/clientversion.h`. + +This is a **documentation-validation** prompt, not a code audit. Do not hunt for +code bugs. Do not change `src/`, tests, build files, or scripts. The only files +you may modify are root-directory `.md` files, and only to make them match the +code that already exists. **Code is the source of truth; the docs must conform to +the code, never the reverse.** If a doc and the code disagree, the doc is wrong +unless the discrepancy reveals an actual code defect — in which case you record +it as `CODE_DISCREPANCY_FOR_JARED` and do **not** edit code to fit the doc. + +This rule from `CLAUDE.md` governs the entire run: + +> Avoid widening doc claims beyond what code shows. Treat code as truth. + +## Scope — root `.md` files only + +In scope: **only `.md` files in the repository root** (`/home/jared/Code/digibyte/*.md`). +No subdirectories. No `docs/`, no `src/`, no `doc/`, no `contrib/`. Re-enumerate +the live set at the start of the run; do not trust this list blindly: + +```bash +cd ~/Code/digibyte +ls -1 *.md | sort +git status --short -- '*.md' +``` + +The root `.md` set at the time this prompt was written (re-confirm live — files +may have been added or removed): + +**Tier A — Public / release-facing docs (highest bar; ship with the release):** + +| File | Purpose | Primary code surface to validate against | +|------|---------|------------------------------------------| +| `README.md` | Project front page, links, version badge | `configure.ac`, repo links, `INSTALL.md` | +| `INSTALL.md` | Build/install entry | `doc/build-*`, `configure.ac` | +| `SECURITY.md` | Security policy / disclosure | low code coupling — validate contacts/keys/links/policy are current; "low coupling" is not an exemption from checking | +| `CONTRIBUTING.md` | Contributor workflow | branch model, build/test commands | +| `ARCHITECTURE.md` | Core DigiByte system design | `src/` core: chainparams, consensus, net, pow | +| `DIGIDOLLAR_EXPLAINER.md` | User-facing DigiDollar V1 summary | `src/digidollar/`, `src/consensus/digidollar*` | +| `DIGIDOLLAR_ARCHITECTURE.md` | DD mint/transfer/redeem/collateral/health/state | `src/digidollar/`, `src/consensus/`, `src/script/` | +| `DIGIDOLLAR_ACTIVATION_EXPLAINER.md` | BIP9 gating of DD/oracle surface | `src/consensus/params.h`, `src/kernel/chainparams.cpp`, `src/deploymentinfo*` | +| `DIGIDOLLAR_ORACLE_EXPLAINER.md` | User-facing oracle/MuSig2 summary | `src/oracle/`, `src/primitives/oracle.h` | +| `DIGIDOLLAR_ORACLE_ARCHITECTURE.md` | Exchange aggregation, bundle lifecycle, MuSig2, P2P | `src/oracle/`, `src/net_processing.cpp`, `src/protocol.cpp` | +| `ORACLE_BUNDLE_EXPLAINER.md` | Oracle bundle wire/lifecycle explainer | `src/primitives/oracle.h`, `src/oracle/bundle_manager.cpp`, `src/consensus/` | +| `ORACLE_DISCOVERY_ARCHITECTURE.md` | Oracle endpoint discovery design | `src/oracle/`, discovery/P2P code | +| `DIGIDOLLAR_ORACLE_SETUP.md` | Oracle setup + migration runbook | oracle RPCs, `src/oracle/`, operator flags | +| `DIGIDOLLAR_WALLET_INTEGRATION.md` | Wallet/RPC integration guide | `src/wallet/`, `src/wallet/rpc/wallet.cpp`, `src/rpc/digidollar.cpp` | +| `DIGIDOLLAR_EXCHANGE_INTEGRATION.md` | Exchange/custody integration guide | DD RPCs, address formats, units | +| `DIGIDOLLAR_MINING_INTEGRATION_GUIDE.md` | Miner integration for DD/oracle bundles | `src/node/miner.cpp`, `getblocktemplate`, bundle injection | + +**Tier B — AI-agent / maintainer guides (must be code-accurate; AI relies on them):** + +| File | Purpose | Primary code surface to validate against | +|------|---------|------------------------------------------| +| `CLAUDE.md` | AI agent guide, branch rules, code-surface warnings | the entire DD/oracle surface it summarizes | +| `REPO_MAP.md` | Core DigiByte file index | `src/` core file tree | +| `REPO_MAP_DIGIDOLLAR.md` | DD/oracle file index | `src/digidollar/`, `src/oracle/`, DD RPC/Qt/test tree | +| `REPO_MAP_GUIDE.md` | How to maintain/interpret the repo maps | the two repo maps | + +**Tier C — Internal / non-release (verify they are NOT misleading; lightest bar, +do not ship-block on prose polish):** + +| File | Purpose | +|------|---------| +| `digidollar/HOUSEKEEPING_TO-DOs.md` | Internal task list | +| `Z_RED_HORNET_FINAL_PROMPT.md` | Internal audit prompt (gitignored) | +| `Z_RED_HORNET_v2.md` | Internal audit prompt (gitignored) | +| `doc/v9.26.2/Z_DOC_VALIDATION_v9.26.2_PROMPT.md` | This archived prompt (tracked) | + +For Tier C: confirm tier classification is correct, confirm gitignored internal +prompts are still gitignored, and confirm none of them are misleading — meaning +no statement that contradicts current code, no outdated procedure that could +confuse a maintainer, and no public-facing falsehood that could leak into a +release. Verify by spot-checking the critical claims in each Tier-C file against +code, not by skimming for obvious lies. Do not polish their prose and do not +ship-block on them. If a Tier-C file is actually stale internal cruft, recommend +(do not perform) deletion and record it under `HOUSEKEEPING`. + +Re-derive each file's tier from its actual content during Wave 0; do not assume +the table is still correct. + +## Operating principles + +- **Code is truth.** Open the cited source and read it with your own eyes. A claim + is validated only when you have read the current source and can cite the exact + `file:line` that backs it. Reading the source is mandatory, not optional. +- **Do not hallucinate.** Never invent or "remember" a constant, RPC name, file + path, port, height, oracle count, quorum, opcode value, address prefix, version + string, or behavior. If you cannot find it in code, the claim is `UNVERIFIED` + until proven, and you must not assert it as fact in a doc. +- **Minimal, surgical edits.** Fix only what is wrong or unclear. Do not rewrite + docs wholesale, do not restructure for taste, do not "improve" passing prose. + Every edit closes a validated drift, ambiguity, or DigiByte/DGB-correctness + defect. No drive-by rewrites. +- **Preserve voice and format.** Match each doc's existing heading style, tone, + table format, and code-fence conventions. A reader should not be able to tell a + validation pass happened, except that the content is now correct. +- **Don't fix code.** If a doc accurately describes code and the code looks wrong, + that is out of scope — log it as `CODE_DISCREPANCY_FOR_JARED` with evidence and + move on. This run never edits non-`.md` files. +- **No proof-by-proxy.** The only acceptable proof for a technical claim is a + current `file:line` from **original source** (`src/`, `configure.ac`, build + files, registered tests). You may **not** cite, as proof: the canonical-facts + ledger, `CLAUDE.md`, a sibling doc, an old report, an RC release note, memory, + or training knowledge. Those are leads; source is proof. A command/test result + may be cited **only as supplementary** evidence, and only with a one-line + explanation of exactly what the output proves and which source it corresponds + to — never as a standalone substitute for reading the code. +- **Stop and ask Jared** before: deleting any doc, merging/splitting docs, + changing a doc's public meaning in a way that implies a protocol/release + decision, or "fixing" a doc to match code where the code itself appears wrong. + +## Canonical facts ledger (build this FIRST, in Wave 0) + +Most doc drift is a number or term that disagrees with code or with a sibling +doc. Before validating any doc, extract the **canonical values from source** into +a single ledger (`reports/doc_validation_canonical_facts.md`). Every later wave +checks each doc against this one ledger so all 20+ docs end up internally +consistent with each other and with code. At minimum, pin from code: + +- **Version / release:** `configure.ac` major/minor/build/RC/suffix/IS_RELEASE; + `src/clientversion.h`. Confirm the release name every doc should use is + **v9.26.2** (final, not RC). +- **Chain constants** (`src/chainparams*`, `src/consensus/`, `src/kernel/chainparams.cpp`, + `src/validation.cpp`, `src/amount`/`policy`): block time, `SUBSIDY`, + `COINBASE_MATURITY` / `COINBASE_MATURITY_2`, `MAX_MONEY`, min relay / default + tx fee in **DGB/kB** (not BTC/vB), P2P ports (mainnet `12024`, testnet26 + `12033`), RPC ports, network magic, bech32 HRPs (`dgb`/`dgbt`/`dgbrt`), DD + address prefixes (`DD`/`TD`/`RD`). + - **Block-time nuance — validate carefully, do not blindly "correct".** DigiByte + is multi-algo. The effective/average block time is **15s** (`BLOCK_TIME_SECONDS` + in `src/validation.h`), while the per-algorithm target spacing is **60s** + (`nTargetSpacing` in `src/kernel/chainparams.cpp`). A doc saying "15 second + blocks" is correct for the effective rate; a doc saying "60s" may be correct + if it explicitly means per-algorithm spacing. Confirm which the doc means + against both source locations before changing anything. +- **DigiDollar consensus** (`src/digidollar/`, `src/consensus/digidollar*`, + `src/script/script.h`): DD tx version marker `0x0770`, type field encoding + (1=MINT/2=TRANSFER/3=REDEEM), mint min/max cents, the 10 lock tiers and their + canonical durations + confirmation-window rule, DD amount unit (1 cent), + `MAX_DIGIDOLLAR`, opcode values `OP_DIGIDOLLAR=0xbb` … `OP_ORACLE=0xbf` and + the `OP_CHECKPRICE` deterministically-disabled status. +- **Activation** (`src/consensus/params.h`, `src/kernel/chainparams.cpp`, + `src/deploymentinfo.cpp`): BIP9 bit (23), `DEPLOYMENT_DIGIDOLLAR` start / + min-activation-height / window / threshold per network (mainnet, testnet26, + regtest), and the `nDDActivationHeight` / `nOracleActivationHeight` / + `nDigiDollarMuSig2Height` values per network. +- **Oracle roster** (`src/kernel/chainparams.cpp` `vOracleNodes` / + `consensus.vOraclePublicKeys` / `nOraclePubkeyCount` / `nOracleConsensusRequired`, + `src/primitives/oracle.h`): per network — total slots, active count, and + consensus/quorum required. **This is the single highest-risk drift area.** + Re-derive the values yourself from `src/kernel/chainparams.cpp` and cite the + lines. As of this writing the code declares **mainnet 35 slots / 7-of-35**, + **testnet26 35 slots / 7-of-35**, and **regtest 7 slots / 4-of-7** (chainparams + `nOraclePubkeyCount` / `nOracleConsensusRequired`, with `vOraclePublicKeys` + holding 35 / 35 / 7 keys respectively) — confirm these are still current, then + treat **any** doc that says otherwise (e.g. stale "21 active" or "9-of-17" + figures from the RC era) as a defect to correct to the code's actual numbers. +- **RPC surface** (`src/rpc/digidollar.cpp` `RegisterDigiDollarRPCCommands`, + `src/wallet/rpc/wallet.cpp` `GetWalletRPCCommands`): the exact list and count + of registered DD/oracle RPCs (base vs wallet-context), and which named RPCs are + intentionally absent/legacy/dead (`sendoracleprice`, `submitoracleprice`, + `src/rpc/digidollar_transactions.cpp` entries). +- **P2P / wire** (`src/protocol.cpp`, `src/net_processing.cpp`): oracle message + wire names and their gating. + +When two docs disagree, the ledger value (from code) wins, and **both** docs are +corrected. Record in the ledger any place where the code itself is ambiguous or +where two source locations disagree — that is a `CODE_DISCREPANCY_FOR_JARED`. + +## Fleet model — waves of up to 5 sub-agents + +Run in **waves**, **up to 5 sub-agents per wave**, at **maximum ultracode effort**. +The main agent is the **orchestrator**: it owns the canonical-facts ledger, wave +dispatch, synthesis, conflict resolution between sub-agents, applying edits, +re-verification, the running ledger, and final signoff. Sub-agents investigate +and propose; the orchestrator decides and edits. If two sub-agents disagree about +a fact, the orchestrator re-derives it from code and is the tiebreaker. + +**Orchestrator must spot-check, not trust.** Before applying a sub-agent's +corrections or marking any doc `VERIFIED`, the orchestrator independently re-reads +at least ~20% of that sub-agent's `file:line` citations (and 100% of any citation +behind a consensus/roster/version/activation/RPC-surface claim) by opening the +cited source itself. If any cited line does not actually say what the sub-agent +claimed, or is too vague to prove the claim, the whole doc is bounced back for +re-validation. Sub-agent assertions are leads until the orchestrator has seen the +code. + +**Default per-agent unit of work: one document, validated end to end.** Each +sub-agent owns one root `.md` file (or a small cluster of tightly related ones), +reads it line by line, and for every claim either (a) cites the current +`file:line` that proves it, (b) proposes the corrected text with the code +citation that justifies the correction, or (c) marks it `UNVERIFIED` and records +the search performed (specific files opened, search terms used, and why the claim +could not be confirmed). Larger docs (e.g. `DIGIDOLLAR_ARCHITECTURE.md`, +`DIGIDOLLAR_ORACLE_ARCHITECTURE.md`, `REPO_MAP*.md`) may warrant a whole agent +each; tiny docs (`SECURITY.md`, `INSTALL.md`) can be clustered — but clustering is +a grouping for efficiency only, never an excuse for less depth: every clustered +doc gets the full D1–D4 treatment with the same rigor as a solo doc. + +A **material technical claim** — the thing that always requires a source citation +— is any statement a reader could act on or that affects correctness: a version, +number, constant, port, address/HRP/prefix, opcode, height, oracle count/quorum, +RPC/CLI/config name, API field, behavior, procedure step, or feature-availability +claim. Expository prose that depends on such a claim must be backed by the same +citation. Pure narrative with no actable content needs no citation but also must +not contradict code. + +Each sub-agent must check **all four dimensions** for its doc(s): + +- **D1 — Code accuracy:** every material technical claim matches current source; + cite `file:line` from **original source**, never from the canonical-facts + ledger or a sibling doc (the ledger is a consistency cross-check, not proof). + Flag every value that disagrees with the canonical-facts ledger. +- **D2 — DigiByte/DGB correctness:** no stray Bitcoin/BTC branding, units, + defaults, ports, block times, or "Bitcoin Core" labels that should be DigiByte; + DGB units and DigiByte-specific values used throughout; address/HRP/prefix + examples are DigiByte, not Bitcoin. +- **D3 — Readability for AI + humans:** clear structure, working internal links + and file references, no contradictions within the doc or across docs, correct + cross-references, terminology consistent with the canonical ledger, no + confusing or stale instructions, examples that actually run/parse. +- **D4 — Release-readiness:** version/status correct (v9.26.2 final), no + "RC"/"draft"/"TODO"/"coming soon"/placeholder language in Tier A/B docs, no + references to removed features or renamed symbols, no dead links to deleted + files. + +Give each sub-agent the context bundle below plus the canonical-facts ledger plus +its explicit file list and the specific code paths to read. Sub-agents **propose +diffs with citations**; they do not edit files. The orchestrator applies and +re-verifies. This keeps edits consistent across all docs and prevents two agents +from making contradictory changes. + +### Sub-agent context bundle (every sub-agent must read first) + +1. `CLAUDE.md` +2. The canonical-facts ledger (`reports/doc_validation_canonical_facts.md`) +3. The specific doc(s) it owns +4. The code paths listed for that doc in the Scope table +5. For DD/oracle docs, also: `DIGIDOLLAR_ARCHITECTURE.md`, + `DIGIDOLLAR_ORACLE_ARCHITECTURE.md`, `REPO_MAP_DIGIDOLLAR.md` as cross-refs + (but remember those are themselves under validation — trust code over them, + and never cite them as proof in your report). + +Sub-agents must not edit any file. Sub-agents must not redesign docs or invent +new sections. If a doc section describes a feature, the sub-agent's job is to +confirm the description matches code, not to reimagine the feature. A sub-agent +may not report a claim as `VERIFIED` unless its report carries the exact +original-source `file:line` it read; "looks right", "consistent with the ledger", +or "matches CLAUDE.md" are not verification. + +## Per-document validation checklist + +For each root `.md` file, the owning sub-agent produces a structured report: + +- **File + tier.** +- **Claim inventory:** the material technical claims, each marked + `VERIFIED (file:line)`, `DRIFT → corrected text (file:line)`, or `UNVERIFIED`. +- **DigiByte/DGB findings:** any Bitcoin/BTC leftovers, wrong units, wrong ports, + wrong branding, with the corrected DigiByte value and its code citation. +- **Readability findings:** contradictions, broken links, dead file references, + ambiguous instructions, cross-doc term mismatches. +- **Release-readiness findings:** version/status/placeholder/removed-feature + issues. +- **Proposed diff:** exact before→after text for each correction, each with a + code citation. Minimal edits only. +- **Cross-doc notes:** any value that must also change in another doc to stay + consistent (hand to orchestrator). +- **CODE_DISCREPANCY_FOR_JARED:** any place the doc looks right but the code looks + wrong, or two code locations disagree. Evidence only; no code edits. + +## Suggested wave plan (adapt to the live file set) + +Re-enumerate first; add or split waves as needed. Do not exceed 5 sub-agents per +wave. The orchestrator applies all corrections from a wave, re-verifies against +code, updates the ledger, and (if authorized) commits before starting the next +wave. + +- **Wave 0 — Ground truth & inventory (orchestrator + up to 5 scouts).** + Re-enumerate root `.md` files. Build the canonical-facts ledger from code. + Resolve the high-risk oracle-roster / quorum / version numbers from source. + Classify each file into Tier A/B/C. Produce the per-doc → code-surface map. + Output: `reports/doc_validation_canonical_facts.md` and the wave plan. + +- **Wave 1 — Core repo docs (Tier A/B).** `README.md`, `INSTALL.md`, + `SECURITY.md`, `CONTRIBUTING.md`, `ARCHITECTURE.md` (+ `CLAUDE.md` if not held + for Wave 5). Validate version badge/links, build/install steps, branch model, + and core architecture against `src/` core, `configure.ac`, and build docs. + +- **Wave 2 — DigiDollar core docs.** `DIGIDOLLAR_EXPLAINER.md`, + `DIGIDOLLAR_ARCHITECTURE.md`, `DIGIDOLLAR_ACTIVATION_EXPLAINER.md`. Validate + mint/transfer/redeem/collateral/health, lock tiers, opcodes, DD tx marker, and + BIP9 activation against `src/digidollar/`, `src/consensus/`, `src/script/`, + `src/kernel/chainparams.cpp`, `src/consensus/params.h`. + +- **Wave 3 — Oracle docs.** `DIGIDOLLAR_ORACLE_EXPLAINER.md`, + `DIGIDOLLAR_ORACLE_ARCHITECTURE.md`, `ORACLE_BUNDLE_EXPLAINER.md`, + `ORACLE_DISCOVERY_ARCHITECTURE.md`, `DIGIDOLLAR_ORACLE_SETUP.md`. Validate + exchange aggregation, bundle v0x03 lifecycle, MuSig2, P2P wire names/gating, + roster/quorum, discovery, and operator setup against `src/oracle/`, + `src/primitives/oracle.h`, `src/net_processing.cpp`, `src/protocol.cpp`, and + the oracle RPCs. + +- **Wave 4 — Integration / operator guides.** `DIGIDOLLAR_WALLET_INTEGRATION.md`, + `DIGIDOLLAR_EXCHANGE_INTEGRATION.md`, `DIGIDOLLAR_MINING_INTEGRATION_GUIDE.md`. + Validate every RPC name/param/return, address format, unit, and miner/bundle + step against `src/wallet/rpc/wallet.cpp`, `src/rpc/digidollar.cpp`, + `src/node/miner.cpp`, and registration tables. + +- **Wave 5 — AI-agent maps, internal docs, and cross-doc consistency.** + `CLAUDE.md`, `REPO_MAP.md`, `REPO_MAP_DIGIDOLLAR.md`, `REPO_MAP_GUIDE.md`, and + Tier C files. Validate that the repo maps match the actual file tree (no listed + file is missing, no major file is unlisted), that `CLAUDE.md`'s surface + warnings match code, and run a **final cross-doc consistency sweep**: every + shared number/term agrees across all docs and with the canonical ledger. + +- **Wave 6+ (add-on) — Reconciliation.** Any doc that failed, any cross-doc + conflict surfaced late, any `CODE_DISCREPANCY_FOR_JARED` needing a second pass. + Add waves until every Tier A/B doc is `VERIFIED` or has an explicit, recorded + open item. + +## Fix discipline (verify → correct → re-verify) + +For every correction: + +1. **Prove the drift** against current code with an original-source `file:line` + citation. No source citation, no edit. +2. **Apply the minimal edit** to the `.md` file — change only the wrong words, + numbers, links, or branding. Preserve surrounding prose and formatting. +3. **Re-verify** the corrected text against the same code citation. Confirm you + did not introduce a new inconsistency with a sibling doc or the ledger. +4. **Propagate** the same fact to every other doc that repeats it (orchestrator + tracks shared facts so all docs stay consistent). +5. **Record** the correction in the running ledger with before/after and citation. + +Do not "fix" by deleting hard-to-verify content. If a claim cannot be verified, +either prove it from code, soften it to exactly what code supports, or escalate +it — do not silently drop or invent. Do not introduce new claims a reader cannot +verify from code. + +## Anti-hallucination rules + +- Cite current original-source `file:line` for every material claim that ends up + in a doc or that you assert in a report. Command/test output may accompany a + citation as supplementary proof, with a note on what it demonstrates, but never + replaces reading the source. +- Never invent filenames, RPC names, test names, config keys, line numbers, + commit hashes, release facts, oracle slots, quorum values, ports, opcodes, + address prefixes, or activation states. +- If unsure, open the file and read it. If still unsure, mark `UNVERIFIED` and + record what evidence is missing (files opened, terms searched) — never guess + into a doc. +- Old reports, RC release notes, the canonical-facts ledger, `CLAUDE.md`, and + sibling docs are **leads, not proof**. Reconfirm against current source. +- Separate observation from inference; label inferences as inferences and cite + the source/observation each inference rests on. An inference that affects code + correctness or release readiness must link to the code it depends on. + +## Commit discipline + +Never push. Before any edit: `git status --short -- '*.md'` — do not overwrite +unrelated local work. Only `.md` files in root may be staged by this run. + +If commits are authorized, commit cleanly: + +- Group by doc or by a single shared fact corrected across docs. +- One logical correction set per commit; no unrelated mega-commits. +- Commit message names the doc(s) and the nature of the fix, e.g. + `docs: correct oracle roster/quorum numbers to match chainparams (v9.26.2)`. +- Commit body cites the code that justifies the change. +- Do not mix Tier-C housekeeping with Tier-A release-doc corrections. + +If commits are not authorized, leave edits uncommitted and record the exact +proposed commit split. + +## Reporting + +Maintain a durable ledger from Wave 0 to signoff: + +- Canonical facts: `reports/doc_validation_canonical_facts.md` +- Running ledger: `reports/doc_validation_ledger.md` +- Final report: `reports/doc_validation_final_report.md` + +Each wave summary records: wave number; docs covered; sub-agent assignments; per +doc D1–D4 findings; corrections applied (before→after + citation); cross-doc +facts propagated; `UNVERIFIED` items; `CODE_DISCREPANCY_FOR_JARED` items; +re-verification result; commit hash/status. + +Final report records: the canonical-facts ledger; per-doc final status +(`VERIFIED` / `VERIFIED WITH OPEN ITEMS` / `BLOCKED`); every correction with its +original-source citation; every `UNVERIFIED` item still open; every +`CODE_DISCREPANCY_FOR_JARED` gathered into one section for Jared; the cross-doc +consistency result; and a single release verdict. A doc may be marked `VERIFIED` +**only** if the report carries, or references in the running ledger, the +`file:line` citations behind its material claims — and the orchestrator has +spot-checked a sample of them against source. "VERIFIED" with no recorded +citations is not allowed. + +## Final signoff criteria for v9.26.2 + +The doc set is release-ready only when **all** are true: + +- Every Tier A and Tier B root `.md` file is `VERIFIED` against current code — + each material claim backed by a recorded original-source `file:line` the + orchestrator spot-checked — or has an explicitly listed, Jared-accepted open + item. No doc is `VERIFIED` on intuition or proxy citations. +- Every shared fact (version, oracle roster/quorum, activation heights, ports, + opcodes, RPC counts, constants, addresses, units) is identical across all docs + and equals the code-derived canonical value. +- No Tier A/B doc contains Bitcoin/BTC branding, units, ports, or defaults that + should be DigiByte/DGB. +- No Tier A/B doc contains RC/draft/TODO/placeholder/"coming soon" language, + references to removed features, or dead links to deleted files. +- Every doc reads clearly for both AI agents and humans (structure, links, no + contradictions). +- All `CODE_DISCREPANCY_FOR_JARED` items are gathered in one section for Jared's + decision; none were silently patched, and no code was edited. +- Tier C internal/gitignored files are confirmed non-misleading and still + correctly classified/ignored. + +## Final output to Jared + +Keep it short enough to read and complete enough to act on: + +- Overall verdict: READY / READY WITH OPEN ITEMS / NOT READY. +- Root `.md` files validated (count) and per-doc status table. +- Corrections applied (count) and commit status. +- Cross-doc consistency result. +- DigiByte/DGB-correctness result. +- Open `UNVERIFIED` items. +- `CODE_DISCREPANCY_FOR_JARED` items needing Jared's decision. +- Exact report paths. + +Do not hide risk. Do not rely on memory. Code is the source of truth; the docs +must conform to it. Validate, do not rewrite. Fix only what is proven wrong. diff --git a/V9.26.4_MAINNET_VALIDATION.md b/doc/v9.26.4/V9.26.4_MAINNET_VALIDATION.md similarity index 100% rename from V9.26.4_MAINNET_VALIDATION.md rename to doc/v9.26.4/V9.26.4_MAINNET_VALIDATION.md diff --git a/V9.26.4_PRUNING_EXPLAINER.md b/doc/v9.26.4/V9.26.4_PRUNING_EXPLAINER.md similarity index 97% rename from V9.26.4_PRUNING_EXPLAINER.md rename to doc/v9.26.4/V9.26.4_PRUNING_EXPLAINER.md index c4b847963e8..1371e949b80 100644 --- a/V9.26.4_PRUNING_EXPLAINER.md +++ b/doc/v9.26.4/V9.26.4_PRUNING_EXPLAINER.md @@ -58,8 +58,8 @@ release v9.26.4 Qt on **live mainnet** with `prune=2000` and no txindex: normally in ~2 min (the first prune only has a one-time ~8-min step marking 21M old block-index entries). -Full test record: `V9.26.4_MAINNET_VALIDATION.md`. Design + test plan: -`V9.26.4_PRUNING_PLAN.md`. +Full test record: `doc/v9.26.4/V9.26.4_MAINNET_VALIDATION.md`. Design + test plan: +`doc/v9.26.4/V9.26.4_PRUNING_PLAN.md`. **For pools — upgrade once. How to set up `digibyte.conf`:** diff --git a/V9.26.4_PRUNING_PLAN.md b/doc/v9.26.4/V9.26.4_PRUNING_PLAN.md similarity index 99% rename from V9.26.4_PRUNING_PLAN.md rename to doc/v9.26.4/V9.26.4_PRUNING_PLAN.md index 36ac48dcf3e..dec52975fa2 100644 --- a/V9.26.4_PRUNING_PLAN.md +++ b/doc/v9.26.4/V9.26.4_PRUNING_PLAN.md @@ -281,7 +281,7 @@ functionally a v9.26.3. # Part II — TDD Test Plan -Companion to `V9.26.4_PRUNING_PLAN.md`. This is the test blueprint that must be **100% +Companion to `doc/v9.26.4/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 @@ -320,7 +320,7 @@ matter on mainnet.) ## 1. TDD workflow (per change) -For each of the 6 changes in `V9.26.4_PRUNING_PLAN.md §4`: +For each of the 6 changes in `doc/v9.26.4/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 diff --git a/V9.26.4_PR_EXPLAINER.md b/doc/v9.26.4/V9.26.4_PR_EXPLAINER.md similarity index 97% rename from V9.26.4_PR_EXPLAINER.md rename to doc/v9.26.4/V9.26.4_PR_EXPLAINER.md index b16c4ee8e18..8d7bc78e958 100644 --- a/V9.26.4_PR_EXPLAINER.md +++ b/doc/v9.26.4/V9.26.4_PR_EXPLAINER.md @@ -143,7 +143,7 @@ remove `prune=`, set `txindex=1`, restart with `-reindex`. - a **truncated/partially-restored DD-era block file** (which passes the index-flag guard) is caught by the fail-closed startup reconstruction and refuses to start — then recovers with `-reindex`. -- **Live mainnet validation** (full record: `V9.26.4_MAINNET_VALIDATION.md`): +- **Live mainnet validation** (full record: `doc/v9.26.4/V9.26.4_MAINNET_VALIDATION.md`): we ran the release Qt binary against a copy of a synced mainnet datadir with `prune=2000` and no txindex, on the live network: - the prune lock registered at 23,627,520 while DigiDollar is still @@ -288,9 +288,9 @@ a full v9.26.4 node (txindex + stats index) — separate ports, same live networ | Document | Content | |---|---| -| `V9.26.4_PRUNING_PLAN.md` | Design + full TDD test plan | -| `V9.26.4_PRUNING_EXPLAINER.md` | Short shareable community explainer | -| `V9.26.4_MAINNET_VALIDATION.md` | The live mainnet test record | +| `doc/v9.26.4/V9.26.4_PRUNING_PLAN.md` | Design + full TDD test plan | +| `doc/v9.26.4/V9.26.4_PRUNING_EXPLAINER.md` | Short shareable community explainer | +| `doc/v9.26.4/V9.26.4_MAINNET_VALIDATION.md` | The live mainnet test record | | `doc/release-notes/release-notes-9.26.4.md` | Operator-facing release notes & limitations | *Fine print: pruned nodes don't serve deep history to new peers (archival nodes diff --git a/V9.26.4_TESTNET_PRUNE_SUMMARY.md b/doc/v9.26.4/V9.26.4_TESTNET_PRUNE_SUMMARY.md similarity index 97% rename from V9.26.4_TESTNET_PRUNE_SUMMARY.md rename to doc/v9.26.4/V9.26.4_TESTNET_PRUNE_SUMMARY.md index efdc609efc7..8a02e178d04 100644 --- a/V9.26.4_TESTNET_PRUNE_SUMMARY.md +++ b/doc/v9.26.4/V9.26.4_TESTNET_PRUNE_SUMMARY.md @@ -54,7 +54,7 @@ testnet proves the **functional** side (a pruned node validates / mints / redeem DigiDollar correctly and stays in lockstep with full nodes), while the **disk-saving** side was proven on **mainnet** separately: **38 GB → 0.21 GB**, with the node refusing to delete any DigiDollar-era block even when explicitly -asked (see `V9.26.4_MAINNET_VALIDATION.md`). +asked (see `doc/v9.26.4/V9.26.4_MAINNET_VALIDATION.md`). ## Every scenario we ran (all passed) @@ -72,7 +72,7 @@ Nothing is left on the testnet list. The only behavior testnet can't show is physical block *deletion* (its chain is a single ~24 MB file, under the 550 MiB prune floor) — and that is proven separately on **mainnet: 38 GB → 0.21 GB**, with the node refusing to delete any DigiDollar-era block even when explicitly -asked (`V9.26.4_MAINNET_VALIDATION.md`). Our automated regtest suite adds 15 +asked (`doc/v9.26.4/V9.26.4_MAINNET_VALIDATION.md`). Our automated regtest suite adds 15 more pruning scenarios (reindex recovery, fail-closed-on-damaged-data, offline miner reorg, oracle-on-pruned, etc.). diff --git a/V9.26.4_TESTNET_PRUNE_TESTS.md b/doc/v9.26.4/V9.26.4_TESTNET_PRUNE_TESTS.md similarity index 98% rename from V9.26.4_TESTNET_PRUNE_TESTS.md rename to doc/v9.26.4/V9.26.4_TESTNET_PRUNE_TESTS.md index 98a91f39ef5..276e8d4d833 100644 --- a/V9.26.4_TESTNET_PRUNE_TESTS.md +++ b/doc/v9.26.4/V9.26.4_TESTNET_PRUNE_TESTS.md @@ -2,7 +2,7 @@ *— DigiSwarm, DGB AI dev team · live testnet26, 2026-07-02* -Companion to `V9.26.4_MAINNET_VALIDATION.md`. Where the mainnet record proves the +Companion to `doc/v9.26.4/V9.26.4_MAINNET_VALIDATION.md`. Where the mainnet record proves the prune lock and disk behavior on the live mainnet chain (DigiDollar still *signaling*), this record exercises the **full operational battery on testnet26, where DigiDollar is already ACTIVE** (activation height 599/600) — so a pruned @@ -64,7 +64,7 @@ deleted once *every* block in it is prunable — this one holds the whole chain tip, so it can never be removed. So testnet cannot demonstrate actual block *deletion*; the prune lock is registered at 600 and would clamp if deletion ever occurred. Real deletion + the clamp are proven on mainnet: **38 GB → 0.21 GB**, `pruneblockchain 23774000` clamped to -23,511,221 (`V9.26.4_MAINNET_VALIDATION.md`). +23,511,221 (`doc/v9.26.4/V9.26.4_MAINNET_VALIDATION.md`). ### T3 — Full-vs-pruned parity (live) Full node (`~/.dgb-tnF`, txindex + stats index) vs pruned node (UTXO-scan fallback), same tip: diff --git a/docs/ORACLE_OPERATOR_GUIDE.md b/docs/ORACLE_OPERATOR_GUIDE.md index 77397218ebd..7d08a0b3722 100644 --- a/docs/ORACLE_OPERATOR_GUIDE.md +++ b/docs/ORACLE_OPERATOR_GUIDE.md @@ -7,20 +7,26 @@ DigiDollar requires oracle operators to provide real-time DGB/USD price feeds. Oracle public keys are **hardcoded in `src/kernel/chainparams.cpp`** — every oracle operator must: -1. Run a current DigiByte Core release (RC44 is the current testnet26 release candidate at the time of writing) and create a descriptor wallet +1. Use the reviewed build for your network and create a descriptor wallet 2. Run `createoraclekey` to generate their oracle keypair inside the wallet 3. Export an offline recovery copy with `exportoracleprivkey` after unlocking encrypted wallets with `walletpassphrase` 4. Send their **public key only** to the DigiByte Core maintainer 5. The maintainer adds their key to `chainparams.cpp` and ships a new release 6. The operator runs `startoracle` — the wallet provides the private key automatically -For current testnet release/migration mechanics (testnet26, P2P port 12033, RPC port 14026) and retired testnet decommissioning notes, follow `DIGIDOLLAR_ORACLE_SETUP.md`. +For current testnet release/migration mechanics (testnet26, P2P port 12033, RPC port 14026) and retired testnet decommissioning notes, follow [DIGIDOLLAR_ORACLE_SETUP.md](../DIGIDOLLAR_ORACLE_SETUP.md). +For upgrades, two nodes behind one router, compact filters and temporary logging, +read [node operations](../doc/digidollar-operations.md). Mainnet Thaw Day is +scheduled at 24,490,000, estimated for November 1, 2026. Testnet26 is scheduled +at 432,100, estimated for September 18–19, 2026. The heights trigger activation. +Signet is unsupported and remains unscheduled. No public-network Thaw Day +activation or soak test is claimed. --- ## Step-by-Step: For Oracle Operators -### Step 1: Compile and Run DigiByte Core (current RC) +### Step 1: Compile and Run the Selected Build ```bash cd ~/Code/digibyte @@ -50,7 +56,7 @@ Current releases create descriptor wallets by default. No special flags needed. Replace `0` with the oracle ID slot assigned to you by the maintainer. -Mainnet and testnet chainparams allocate 35 oracle slots (IDs 0–34), and all 35 are part of the current RC44 consensus-active MuSig2 roster (`consensus.vOraclePublicKeys`). Slot ID 35 is outside the configured roster. Regtest has 7 slots (IDs 0–6) with 4-of-7 consensus. +Mainnet and testnet chainparams allocate 35 oracle slots (IDs 0–34), and all 35 are part of the configured consensus-active MuSig2 roster (`consensus.vOraclePublicKeys`). Slot ID 35 is outside the configured roster. Regtest has 7 slots (IDs 0–6) with 4-of-7 consensus. **Output:** ```json @@ -134,7 +140,12 @@ process listings. # "is_running": true ← Price thread is active ``` -### Step 8: Monitor +### Step 9: Monitor + +Routine diagnostics require `-debug=digidollar`. Enable it only while diagnosing +a problem; useful errors and startup/recovery progress remain visible without +it. See [logging guidance](../doc/digidollar-operations.md#routine-logs-and-temporary-diagnostics) +for startup shrinking and disk retention. ```bash tail -f ~/.digibyte/testnet26/debug.log | grep -i oracle @@ -213,7 +224,10 @@ A single broken endpoint (bad URL or quota-blocked) is non-fatal; the round logs ### 5. Recover from wallet corruption -If the wallet DB is unreadable, the oracle key is gone. There is no on-chain way to rotate keys without a chainparams update. +An unreadable wallet does not establish that its key is lost. Preserve the file +and diagnostics before repair, and use a known-good backup or exported key if +available. A replacement public key requires coordinated chainparams changes; +it cannot take over the existing slot through a local setting. - Restore the wallet from `backupwallet` if one exists; the `oraclekey` record carries the private key. - If you exported the private key, create or load a replacement wallet, unlock it with `walletpassphrase` if encrypted, then run `importoracleprivkey [replace]`. Start the oracle only after the imported public key matches the slot configured in chainparams. @@ -248,10 +262,16 @@ Then recompile and distribute the updated binary. | Network | Total Slots | Active (in MuSig2 quorum) | Consensus | Notes | |---------|------------|---------------------------|-----------|-------| -| Mainnet | 35 (IDs 0-34) | 35 (slots 0-34) | 7 signatures from active keyset | DigiDollar/MuSig2 activates at BIP9 min height 23,627,520. Slot 28 uses the DigiHash Mining Pool key; slot 31 uses the Peer2Peer / DigiRoos key. | +| Mainnet | 35 (IDs 0-34) | 35 (slots 0-34) | 7 signatures from active keyset | DigiDollar buried activation is 23,869,440; static MuSig2 floor is 23,627,520. Slot 28 uses the DigiHash Mining Pool key; slot 31 uses the Peer2Peer / DigiRoos key. | | Testnet (testnet26) | 35 (IDs 0-34) | 35 (slots 0-34) | 7 signatures from active keyset | Active from height 600. Slot 28 uses the DigiHash Mining Pool key; slot 31 uses the Peer2Peer / DigiRoos key. | | Regtest | 7 (IDs 0–6) | 7 | 4-of-7 MuSig2 | Always active. | +`getoracles` reads names from the selected network's roster. Mainnet slot 0 +is `DigiByte.Io Oracle` and slot 11 is `Crypto Corner Shop`; testnet uses `Jared` +and `hallvardo` for those slots. These are display names. They do not change +operator IDs, public keys or the signing quorum. Verify authorization with the +configured ID and key, not a name alone. + To confirm a slot is in the active quorum at runtime, call `getoracles` and inspect the `in_consensus` field, or call `getdigidollardeploymentinfo` and read `oracle_pubkey_count` / @@ -305,4 +325,5 @@ To confirm a slot is in the active quorum at runtime, call --- -*Verified against the DigiByte Core RC44 codebase on `feature/digidollar-v1`. Current public testnet instructions target testnet26 / P2P 12033 / RPC 14026. All RPC commands tested in regtest.* +*Instructions target the current source and testnet26 / P2P 12033 / RPC 14026. +Documentation checks do not establish runtime, network or release readiness.* diff --git a/reports/doc_validation_final_report.md b/reports/doc_validation_final_report.md new file mode 100644 index 00000000000..3ec4eaeb9dc --- /dev/null +++ b/reports/doc_validation_final_report.md @@ -0,0 +1,83 @@ +# DigiByte v9.26.2 — Root-Markdown Release Validation: Final Report + +Run date: 2026-06-25 · Branch: `feature/digidollar-v1` · Driver: `doc/v9.26.2/Z_DOC_VALIDATION_v9.26.2_PROMPT.md` + +Archive note: Findings and work status below describe the June 2026 review. File references have been updated for the documentation archive; this report does not certify the current release. + +## Verdict: **READY WITH 3 OPEN ITEMS FOR JARED** + +All 21 Tier A/B root `.md` docs (incl. `doc/release-notes/RELEASE_v9.26.2.md`) are validated against +current source, cross-doc consistent, and free of stale roster figures, RC labels, +and Bitcoin-branding errors. Three items require Jared's decision (no code-derivable +fix; nothing was invented). Nothing was committed. + +## Method +- **Wave 0:** 5 scouts code-derived the canonical-facts ledger (`reports/doc_validation_canonical_facts.md`). +- **Waves 1–5:** validate→fix→adversarially-verify pipeline over 20 docs (each agent owned one file, applied only minimal source-cited edits; an independent read-only agent on a different model re-checked every edit). +`doc/release-notes/RELEASE_v9.26.2.md` validated separately (created mid-run). +- **Consistency sweep + orchestrator reconciliation:** spot-checked critical numbers against code, fixed the one flagged bad edit, de-RC'd stale labels, bumped version headers, resolved cross-doc drift. +- Totals: 47 sub-agents, ~3.5M tokens, ~2,270 tool calls. + +## Per-doc status +| Doc | Verdict | Notes | +|---|---|---| +| doc/release-notes/RELEASE_v9.26.2.md | VERIFIED (clean) | every claim matches code; ports/roster/activation/RPC confirmed | +| DIGIDOLLAR_WALLET_INTEGRATION.md | VERIFIED | RPC names/params confirmed | +| DIGIDOLLAR_EXCHANGE_INTEGRATION.md | VERIFIED | units/prefixes confirmed | +| DIGIDOLLAR_MINING_INTEGRATION_GUIDE.md | VERIFIED | GBT rule + miner path confirmed | +| ORACLE_DISCOVERY_ARCHITECTURE.md | VERIFIED | P2P getoracles confirmed | +| REPO_MAP_DIGIDOLLAR.md | VERIFIED | RPC count 18 + de-RC applied | +| DIGIDOLLAR_ORACLE_ARCHITECTURE.md | VERIFIED (reconciled) | duplicate heading fixed (5.1/5.4); de-RC; version header | +| DIGIDOLLAR_ARCHITECTURE.md | VERIFIED (reconciled) | version header → v9.26.2 | +| DIGIDOLLAR_ACTIVATION_EXPLAINER.md | VERIFIED | activation values confirmed | +| DIGIDOLLAR_EXPLAINER.md | VERIFIED | last-verified date synced | +| DIGIDOLLAR_ORACLE_EXPLAINER.md | VERIFIED | RC44 label removed from subtitle | +| DIGIDOLLAR_ORACLE_SETUP.md | VERIFIED | de-RC; roster confirmed | +| ORACLE_BUNDLE_EXPLAINER.md | VERIFIED | de-RC; 9-of-17 kept as history | +| ARCHITECTURE.md | VERIFIED | core constants confirmed | +| CLAUDE.md | VERIFIED | surface warnings confirmed; v9.26 lineage refs kept | +| REPO_MAP.md / REPO_MAP_GUIDE.md | VERIFIED | titles → v9.26.2 | +| README.md | VERIFIED w/ open item | see #2 | +| CONTRIBUTING.md | VERIFIED w/ open item | see #3 | +| SECURITY.md | VERIFIED w/ open item | see #1 | +| INSTALL.md | VERIFIED | trailing newline fixed | + +## Corrections applied (orchestrator reconciliation, on top of agent edits) +- Fixed bad edit: duplicate "ConnectBlock() Integration" heading → `5.1 ConnectBlock() Oracle Validation` / `5.4 ConnectBlock() Price-Cache Update` (`DIGIDOLLAR_ORACLE_ARCHITECTURE.md`); both confirmed against `src/validation.cpp:2852`. +- De-RC sweep (current keys are "RC46" in code; final release): removed present-tense `RC44` labels in ORACLE_BUNDLE_EXPLAINER, DIGIDOLLAR_ORACLE_SETUP (×2), DIGIDOLLAR_ORACLE_ARCHITECTURE (×2), REPO_MAP_DIGIDOLLAR (×3), DIGIDOLLAR_ORACLE_EXPLAINER subtitle. Historical `RC38`/`RC41` references kept. +- Version headers/titles → v9.26.2: REPO_MAP, REPO_MAP_DIGIDOLLAR, REPO_MAP_GUIDE, DIGIDOLLAR_ARCHITECTURE, DIGIDOLLAR_ORACLE_ARCHITECTURE. +- Minor: INSTALL.md trailing newline; DIGIDOLLAR_EXPLAINER last-verified date 2026-06-25. + +## Cross-doc consistency: PASS +Shared facts (v9.26.2; 7-of-35 / 4-of-7; bit 23 + heights; opcodes 0xbb–0xbf; +ports 12024/12033; HRPs dgb/dgbt/dgbrt; prefixes DD/TD/RD; 18+17 RPCs) are +consistent across all docs and equal the code value. No `21 active` / `9-of-17` +present-tense figures remain (only correct historical mentions). + +## DigiByte/DGB correctness: PASS +No Bitcoin/BTC branding, unit, or port errors. Remaining "Bitcoin Core" mentions +are accurate lineage/coding-standard references; "40x faster than BTC" and +"DGB/BTC × BTC/USDT" are correct comparisons / real exchange pairs. + +## OPEN ITEMS FOR JARED (not code-derivable — nothing invented) +1. **SECURITY.md PGP table** lists Bitcoin Core devs (Wuille, Ford, Chow), not + DigiByte security key holders. Needs the real DigiByte disclosure keys — + cannot be synthesized from code. +2. **README.md circulating supply** "14,293,304,147 DGB (May 2021)" is ~5 years + stale (pre-existing). Needs a current figure from an external data source. +3. **CONTRIBUTING.md** references a `master` branch (lines 80, 169, 403) and a + `github.com/digibyte/digibyte/.../master/...` URL; repo default is `develop` + and README uses `github.com/DigiByte-Core/digibyte`. Inherited Bitcoin Core + workflow text — confirm DigiByte's actual contributor branch/URL before fixing. + +## CODE_DISCREPANCY_FOR_JARED +None. No doc was found to be correct against incorrect code; no code was edited. + +## Human-check (out of code scope) +- External URL `https://digibyte.io/activation` (RELEASE notes) — confirm live. +- No `v9.26.2` git tag exists yet (latest: `v9.26.0-rc46`, `v9.26.1-pre2`) — + expected for a not-yet-tagged release; tag at release time. + +## Commit status +Uncommitted — left in working tree for review (`git diff -- '*.md'`). Proposed +split: (a) version/title bumps to v9.26.2; (b) oracle de-RC label cleanup; +(c) DIGIDOLLAR_ORACLE_ARCHITECTURE heading fix; (d) minor (INSTALL newline, dates). diff --git a/share/examples/digibyte.conf b/share/examples/digibyte.conf index f0737054f49..318e51451bb 100644 --- a/share/examples/digibyte.conf +++ b/share/examples/digibyte.conf @@ -23,9 +23,9 @@ # If this block is in the chain assume that it and its ancestors are valid # and potentially skip their script verification (0 to verify all, # default: -# 00000000000000000001a0a448d6cf2546b06801389cc030b2b18c6491266815, +# ade47d5ccbb92cb1d965b97a187bdbf65bf74be6a3709cb6a01339f8c2856deb, # testnet: -# 0000000000000093bcb68c03a9a168ae252572d348a2eaeba2cdf9231d73206f, +# 0000000000000000000000000000000000000000000000000000000000000000, # signet: # 0000013d778ba3f914530f11f6b69869c9fab54acff85acd7b8201d111f19b7f) #assumevalid= @@ -68,10 +68,13 @@ # -daemon (default: 0) #daemonwait=1 +# Enable Dandelion Transaction Relay Protocol (default: 1) +#dandelion=1 + # Specify data directory #datadir= -# Maximum database cache size MiB (4 to 16384, default: 450). In +# Maximum database cache size MiB (4 to 16384, default: 1024). In # addition, unused mempool memory is shared for this cache (see # -maxmempool). #dbcache= @@ -148,8 +151,12 @@ # Execute command on startup. #startupnotify= +# Create new files with system default permissions, instead of umask 077 +# (only effective with disabled wallet functionality) +#sysperms=1 + # Maintain a full transaction index, used by the getrawtransaction rpc -# call (default: 0) +# call (default: 1) #txindex=1 # Print version and exit @@ -178,8 +185,8 @@ # Bind to given address and always listen on it (default: 0.0.0.0). Use # [host]:port notation for IPv6. Append =onion to tag any incoming # connections to that address and port as incoming Tor connections -# (default: 127.0.0.1:8334=onion, testnet: 127.0.0.1:18334=onion, -# signet: 127.0.0.1:38334=onion, regtest: 127.0.0.1:18445=onion) +# (default: 127.0.0.1:14122=onion, testnet: 127.0.0.1:14126=onion, +# signet: 127.0.0.1:19445=onion, regtest: 127.0.0.1:18543=onion) #bind=[:][=onion] # If set, then this host is configured for CJDNS (connecting to fc00::/8 @@ -279,7 +286,7 @@ #peerbloomfilters=1 # Listen for connections on . Nodes not using the default ports -# (default: 8333, testnet: 18333, signet: 38333, regtest: 18444) +# (default: 12024, testnet: 12033, signet: 38443, regtest: 18444) # are unlikely to get incoming connections. Not relevant for I2P # (see doc/i2p.md). #port= @@ -378,7 +385,7 @@ # A fee rate (in DGB/kvB) that will be used when fee estimation has # insufficient data. 0 to entirely disable the fallbackfee feature. -# (default: 0.00) +# (default: 0.01) #fallbackfee= # Set key pool size to (default: 1000). Warning: Smaller sizes may @@ -392,7 +399,7 @@ #maxapsfee= # Fee rates (in DGB/kvB) smaller than this are considered zero fee for -# transaction creation (default: 0.00001) +# transaction creation (default: 0.10) #mintxfee= # Fee rate (in DGB/kvB) to add to transactions you send (default: 0.00) @@ -480,11 +487,12 @@ # Output debug and trace logging (default: -nodebug, supplying # is optional). If is not supplied or if = 1, # output all debug and trace logging. can be: addrman, -# bench, blockstorage, cmpctblock, coindb, estimatefee, http, i2p, -# ipc, leveldb, libevent, mempool, mempoolrej, net, proxy, prune, -# qt, rand, reindex, rpc, scan, selectcoins, tor, txpackages, -# txreconciliation, util, validation, walletdb, zmq. This option -# can be specified multiple times to output multiple categories. +# bench, blockstorage, cmpctblock, coindb, dandelion, digidollar, +# estimatefee, http, i2p, ipc, leveldb, libevent, mempool, +# mempoolrej, net, proxy, prune, qt, rand, reindex, rpc, scan, +# selectcoins, tor, txpackages, txreconciliation, util, validation, +# walletdb, zmq. This option can be specified multiple times to +# output multiple categories. #debug= # Exclude debug and trace logging for a category. Can be used in @@ -511,7 +519,8 @@ #logtimestamps=1 # Maximum total fees (in DGB) to use in a single wallet transaction; -# setting this too low may abort large transactions (default: 0.10) +# setting this too low may abort large transactions (default: +# 100.00) #maxtxfee= # Send trace/debug info to console (default: 1 when no -daemon. To disable @@ -570,7 +579,7 @@ #mempoolfullrbf=1 # Fees (in DGB/kvB) smaller than this are considered zero fee for -# relaying, mining and transaction creation (default: 0.00001) +# relaying, mining and transaction creation (default: 0.001) #minrelaytxfee= # Relay non-P2SH multisig (default: 1) @@ -590,11 +599,14 @@ ### Block creation options +# Mining algorithm: sha256d, scrypt, groestl, skein, qubit, odo +#algo= + # Set maximum BIP141 block weight (default: 3996000) #blockmaxweight= # Set lowest fee rate (in DGB/kvB) for transactions to be included in -# block creation. (default: 0.00001) +# block creation. (default: 0.001) #blockmintxfee= @@ -634,8 +646,8 @@ # Password for JSON-RPC connections #rpcpassword= -# Listen for JSON-RPC connections on (default: 8332, testnet: -# 18332, signet: 38332, regtest: 18443) +# Listen for JSON-RPC connections on (default: 14022, testnet: +# 14026, signet: 19443, regtest: 18443) #rpcport= # Sets the serialization of raw transaction or block hex returned in @@ -668,6 +680,132 @@ #server=1 +### DigiDollar options + + +# Enable DigiDollar stablecoin features (activation follows the network's +# configured block height) +#digidollar=1 + +# Maintain DigiDollar stats index for network-wide DD supply tracking +# (default: 1) +#digidollarstatsindex=1 + +# calculatecollateralrequirement +# Calculate DGB collateral needed for a DD mint (RPC/console) + + +# estimatecollateral +# Estimate collateral requirement by tier (RPC/console) + + +# getdcamultiplier +# Get the current DCA multiplier for collateral (RPC/console) + + +# getdigidollaraddress +# Get or create a DigiDollar receive address (RPC/console) + + +# getdigidollarbalance +# Show your DigiDollar balance (RPC/console) + + +# getdigidollardeploymentinfo +# Get DigiDollar activation/deployment status (RPC/console) + + +# getdigidollarstats +# Get network-wide DigiDollar statistics (RPC/console) + + +# getprotectionstatus +# Get liquidation protection status (RPC/console) + + +# getredemptioninfo +# Get info about a collateral position for redemption (RPC/console) + + +# importdigidollaraddress +# Validate a DigiDollar address; V1 watch-only import is unsupported/no-op +# (RPC/console) + + +# listdigidollaraddresses +# List all DigiDollar addresses in wallet (RPC/console) + + +# listdigidollarpositions +# List all collateral positions and minted DD (RPC/console) + + +# listdigidollartxs +# List DigiDollar transaction history (RPC/console) + + +# mintdigidollar +# Mint DigiDollars by locking DGB as collateral (RPC/console) + + +# redeemdigidollar +# Redeem DigiDollars to unlock DGB collateral (RPC/console) + + +# senddigidollar +# Send DigiDollars to another address (RPC/console) + + +# validateddaddress +# Validate a DigiDollar address (RPC/console) + + + +### Oracle options + + +# When exactly one oracle message/attestation short of quorum, wait up to +# milliseconds before mining without oracle data (default: +# 2000, 0 to disable) +#oraclebundlewaitms= + +# Polling interval in milliseconds while waiting for near-quorum oracle +# data (default: 200) +#oraclebundlewaitpollms= + +# createoraclekey +# Generate an oracle signing key in your wallet (RPC/console) + + +# getalloracleprices +# Get price history from all oracles (RPC/console) + + +# getoracleprice +# Get current oracle-reported DGB/USD price (RPC/console) + + +# getoraclepubkey +# Get the public key for an oracle ID (RPC/console) + + +# getoracles +# List all oracle nodes and their status (RPC/console) + + +# listoracle +# List oracle configuration and keys (RPC/console) + + +# startoracle +# Start oracle price reporting for your assigned ID (RPC/console) + + +# stoporacle +# Stop oracle price reporting (RPC/console) + + + # [Sections] # Most options will apply to all networks. To confine an option to a specific # network, add it under the relevant section below. diff --git a/src/Makefile.am b/src/Makefile.am index 5366cd55674..37a9d03c70f 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -157,8 +157,10 @@ DIGIBYTE_CORE_H = \ cuckoocache.h \ dandelion.h \ dbwrapper.h \ + dbwrapper_env.h \ deploymentinfo.h \ deploymentstatus.h \ + digidollar/amount.h \ digidollar/digidollar.h \ digidollar/scripts.h \ digidollar/txbuilder.h \ @@ -218,6 +220,7 @@ DIGIBYTE_CORE_H = \ netgroup.h \ netmessagemaker.h \ node/abort.h \ + node/blockheadercache.h \ node/blockmanager_args.h \ node/blockstorage.h \ node/caches.h \ @@ -241,6 +244,7 @@ DIGIBYTE_CORE_H = \ node/transaction.h \ node/txreconciliation.h \ node/utxo_snapshot.h \ + node/warnings.h \ node/validation_cache_args.h \ noui.h \ outputtype.h \ @@ -304,6 +308,7 @@ DIGIBYTE_CORE_H = \ util/bytevectorhash.h \ util/chaintype.h \ util/check.h \ + util/compact_work.h \ util/epochguard.h \ util/error.h \ util/exception.h \ @@ -349,6 +354,8 @@ DIGIBYTE_CORE_H = \ wallet/context.h \ wallet/crypter.h \ wallet/db.h \ + wallet/digidollarmintcapability.h \ + wallet/digidollarmintconsolidation.h \ wallet/digidollarwallet.h \ wallet/ddcoincontrol.h \ wallet/dump.h \ @@ -398,6 +405,7 @@ libdigibyte_node_a_SOURCES = \ consensus/tx_verify.cpp \ dandelion.cpp \ dbwrapper.cpp \ + dbwrapper_env.cpp \ deploymentstatus.cpp \ digidollar/health.cpp \ flatfile.cpp \ @@ -423,6 +431,7 @@ libdigibyte_node_a_SOURCES = \ net_processing.cpp \ netgroup.cpp \ node/abort.cpp \ + node/blockheadercache.cpp \ node/blockmanager_args.cpp \ node/blockstorage.cpp \ node/caches.cpp \ @@ -649,6 +658,7 @@ libdigibyte_consensus_a_SOURCES = \ consensus/amount.h \ consensus/digidollar.cpp \ consensus/digidollar.h \ + consensus/digidollar_state.h \ consensus/digidollar_transaction_validation.cpp \ consensus/digidollar_transaction_validation.h \ consensus/digidollar_tx.cpp \ @@ -744,6 +754,7 @@ libdigibyte_common_a_SOURCES = \ core_read.cpp \ core_write.cpp \ deploymentinfo.cpp \ + digidollar/amount.cpp \ digidollar/digidollar.cpp \ digidollar/scripts.cpp \ digidollar/txbuilder.cpp \ @@ -1027,6 +1038,7 @@ libdigibytekernel_la_SOURCES = \ consensus/tx_verify.cpp \ core_read.cpp \ dbwrapper.cpp \ + dbwrapper_env.cpp \ deploymentinfo.cpp \ deploymentstatus.cpp \ flatfile.cpp \ diff --git a/src/Makefile.qttest.include b/src/Makefile.qttest.include index 8408e9df6af..602b9660bad 100644 --- a/src/Makefile.qttest.include +++ b/src/Makefile.qttest.include @@ -16,14 +16,20 @@ TEST_QT_MOC_CPP += \ qt/test/moc_addressbooktests.cpp \ qt/test/moc_wallettests.cpp \ qt/test/moc_digidollarwidgettests.cpp \ - qt/test/moc_digidollarwave19widgettests.cpp + qt/test/moc_digidollarwave19widgettests.cpp \ + qt/test/moc_digidollarmintrecordtests.cpp \ + qt/test/moc_ddtransactionrecordtests.cpp \ + qt/test/moc_ddtransactiontabletests.cpp endif # ENABLE_WALLET TEST_QT_H = \ qt/test/addressbooktests.h \ qt/test/apptests.h \ + qt/test/digidollarmintrecordtests.h \ + qt/test/ddtransactiontabletests.h \ qt/test/digidollarwidgettests.h \ qt/test/digidollarwave19widgettests.h \ + qt/test/ddtransactionrecordtests.h \ qt/test/optiontests.h \ qt/test/rpcnestedtests.h \ qt/test/uritests.h \ @@ -45,8 +51,11 @@ qt_test_test_digibyte_qt_SOURCES = \ if ENABLE_WALLET qt_test_test_digibyte_qt_SOURCES += \ qt/test/addressbooktests.cpp \ + qt/test/digidollarmintrecordtests.cpp \ + qt/test/ddtransactiontabletests.cpp \ qt/test/digidollarwidgettests.cpp \ qt/test/digidollarwave19widgettests.cpp \ + qt/test/ddtransactionrecordtests.cpp \ qt/test/wallettests.cpp \ wallet/test/wallet_test_fixture.cpp endif # ENABLE_WALLET diff --git a/src/Makefile.test.include b/src/Makefile.test.include index 18e44eb4192..7ba36af8e5e 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -74,7 +74,14 @@ DIGIBYTE_TESTS =\ test/base58_tests.cpp \ test/base64_tests.cpp \ test/digidollar_activation_tests.cpp \ + test/digidollar_thawday_tests.cpp \ + test/digidollar_thaw_volatility_tests.cpp \ + test/digidollar_thaw_recovery_tests.cpp \ + test/digidollar_chainstate_tests.cpp \ + test/digidollar_chainstate_validation_tests.cpp \ test/digidollar_activation_wave12_tests.cpp \ + test/digidollar_thawday_chainparams_tests.cpp \ + test/digidollar_thawday_selection_tests.cpp \ test/digidollar_oracle_startup_tests.cpp \ test/digidollar_wave26_compat_tests.cpp \ test/digidollar_wave13_parity_tests.cpp \ @@ -95,6 +102,9 @@ DIGIBYTE_TESTS =\ test/digidollar_err_tests.cpp \ test/digidollar_health_dca_tests.cpp \ test/digidollar_health_tests.cpp \ + test/digidollar_chainstate_db_tests.cpp \ + test/digidollar_stats_migration_tests.cpp \ + test/digidollar_accounting_transition_tests.cpp \ test/digidollar_t2_05_tests.cpp \ test/digidollar_opcodes_tests.cpp \ test/digidollar_script_attacks_tests.cpp \ @@ -107,6 +117,7 @@ DIGIBYTE_TESTS =\ test/digidollar_musig2_session_state_tests.cpp \ test/digidollar_oracle_feeds_wave11_tests.cpp \ test/digidollar_hot_path_logging_tests.cpp \ + test/digidollar_registry_isolation_tests.cpp \ test/musig2_net_processing_tests.cpp \ test/rh50_oracle_keyset_alignment_tests.cpp \ test/rh51_checkphase3_v1_split_tests.cpp \ @@ -143,6 +154,7 @@ DIGIBYTE_TESTS =\ test/oracle_rpc_tests.cpp \ test/oracle_wallet_key_tests.cpp \ test/oracle_wallet_autostart_tests.cpp \ + test/oracle_shutdown_order_tests.cpp \ test/oracle_phase2_tests.cpp \ test/musig2_basic_tests.cpp \ test/musig2_bundle_creation_tests.cpp \ @@ -163,9 +175,11 @@ DIGIBYTE_TESTS =\ test/musig2_bundle_mining_tests.cpp \ test/redteam_phase2_audit_tests.cpp \ test/oracle_consensus_threshold_tests.cpp \ + test/oracle_params_aggregation_tests.cpp \ test/digidollar_p2p_tests.cpp \ test/digidollar_rh46_rpc_input_validation_tests.cpp \ test/digidollar_rpc_tests.cpp \ + test/digidollar_rpc_totals_cache_tests.cpp \ test/digidollar_integration_attack_tests.cpp \ test/digidollar_redteam_tests.cpp \ test/digidollar_rh26_tests.cpp \ @@ -197,7 +211,9 @@ DIGIBYTE_TESTS =\ test/digidollar_structures_tests.cpp \ test/digidollar_transaction_tests.cpp \ test/digidollar_transfer_tests.cpp \ + test/digidollar_amount_tests.cpp \ test/digidollar_change_tests.cpp \ + test/digidollar_txbuilder_change_tests.cpp \ test/digidollar_txbuilder_tests.cpp \ test/digidollar_validation_tests.cpp \ test/digidollar_txindex_tests.cpp \ @@ -221,18 +237,26 @@ DIGIBYTE_TESTS =\ test/blockencodings_tests.cpp \ test/blockfilter_index_tests.cpp \ test/blockfilter_tests.cpp \ + test/blockheadercache_tests.cpp \ + test/blockindexstorage_tests.cpp \ test/blockmanager_tests.cpp \ + test/blockmap_tests.cpp \ test/bloom_tests.cpp \ test/bswap_tests.cpp \ test/checkqueue_tests.cpp \ + test/coins_cursor_tests.cpp \ test/coins_tests.cpp \ test/coinstatsindex_tests.cpp \ test/compilerbug_tests.cpp \ + test/compact_work_tests.cpp \ test/compress_tests.cpp \ test/crypto_tests.cpp \ test/cuckoocache_tests.cpp \ test/dbwrapper_tests.cpp \ + test/dbwrapper_env_tests.cpp \ + test/dandelion_stempool_reorg_tests.cpp \ test/dandelion_tests.cpp \ + test/dandelion_shutdown_tests.cpp \ test/denialofservice_tests.cpp \ test/deployment_burial_tests.cpp \ test/descriptor_tests.cpp \ @@ -264,11 +288,13 @@ DIGIBYTE_TESTS =\ test/policy_fee_tests.cpp \ test/policyestimator_tests.cpp \ test/pool_tests.cpp \ + test/pow_algo_lookup_tests.cpp \ test/pow_tests.cpp \ test/prevector_tests.cpp \ test/raii_event_tests.cpp \ test/random_tests.cpp \ test/rest_tests.cpp \ + test/headerresponse_tests.cpp \ test/result_tests.cpp \ test/reverselock_tests.cpp \ test/rpc_tests.cpp \ @@ -329,13 +355,17 @@ DIGIBYTE_TESTS += \ wallet/test/scriptpubkeyman_tests.cpp \ wallet/test/walletload_tests.cpp \ wallet/test/group_outputs_tests.cpp \ + wallet/test/digidollar_mint_cleanup_tests.cpp \ wallet/test/digidollar_persistence_wallet_tests.cpp \ + wallet/test/digidollar_wallet_lock_safety_tests.cpp \ wallet/test/digidollar_wallet_security_tests.cpp \ wallet/test/rh59_coincontrol_dd_lock_bypass_tests.cpp \ wallet/test/digidollar_wave16_persistence_tests.cpp \ wallet/test/digidollar_wave17_spendability_tests.cpp \ wallet/test/digidollar_wave17_helper_asymmetry_tests.cpp \ test/digidollar_rpc_unit_tests.cpp \ + test/digidollar_mint_cancel_tests.cpp \ + test/digidollar_wallet_candidate_lock_tests.cpp \ test/digidollar_wave18_rpc_schema_tests.cpp \ test/digidollar_wallet_hd_tests.cpp @@ -365,8 +395,14 @@ DIGIBYTE_TEST_SUITE += \ wallet/test/init_test_fixture.h endif # ENABLE_WALLET +# Some tests read a file out of the checkout, for example to confirm that a log +# line is still in the source. Tell the test program where the checkout is, so +# those tests work no matter which directory it is started from. See +# src/test/util/source_root.h. +TEST_SOURCE_ROOT_DEF = -DDIGIBYTE_TEST_SOURCE_ROOT=\"$(abs_top_srcdir)\" + test_test_digibyte_SOURCES = $(DIGIBYTE_TEST_SUITE) $(DIGIBYTE_TESTS) $(JSON_TEST_FILES) $(RAW_TEST_FILES) -test_test_digibyte_CPPFLAGS = $(AM_CPPFLAGS) $(DIGIBYTE_INCLUDES) $(TESTDEFS) $(BOOST_CPPFLAGS) $(EVENT_CFLAGS) +test_test_digibyte_CPPFLAGS = $(AM_CPPFLAGS) $(DIGIBYTE_INCLUDES) $(TESTDEFS) $(BOOST_CPPFLAGS) $(EVENT_CFLAGS) $(TEST_SOURCE_ROOT_DEF) $(LEVELDB_CPPFLAGS) test_test_digibyte_LDADD = $(LIBTEST_UTIL) if ENABLE_WALLET test_test_digibyte_LDADD += $(LIBDIGIBYTE_WALLET) @@ -412,6 +448,7 @@ test_fuzz_fuzz_SOURCES = \ test/fuzz/checkqueue.cpp \ test/fuzz/coins_view.cpp \ test/fuzz/coinscache_sim.cpp \ + test/fuzz/compact_work.cpp \ test/fuzz/connman.cpp \ test/fuzz/dandelion.cpp \ test/fuzz/crypto.cpp \ @@ -441,6 +478,7 @@ test_fuzz_fuzz_SOURCES = \ test/fuzz/fuzz_odocrypt_cipher.cpp \ test/fuzz/fuzz_odocrypt_keccak.cpp \ test/fuzz/fuzz_odocrypt_keygen.cpp \ + test/fuzz/digidollar_amount.cpp \ test/fuzz/digidollar_consensus.cpp \ test/fuzz/digidollar_integer_math.cpp \ test/fuzz/digidollar_logic.cpp \ @@ -531,6 +569,7 @@ test_fuzz_fuzz_SOURCES = \ test/fuzz/digidollar_txbuilder.cpp \ test/fuzz/digidollar_txbuilder_validate.cpp \ test/fuzz/digidollar_health.cpp \ + test/fuzz/digidollar_chainstate.cpp \ test/fuzz/digidollar_dca_volatility.cpp \ test/fuzz/digidollar_lock_tier.cpp \ test/fuzz/digidollar_prune_blockdb.cpp \ diff --git a/src/Makefile.test_util.include b/src/Makefile.test_util.include index d8c35cc6309..620b07285e8 100644 --- a/src/Makefile.test_util.include +++ b/src/Makefile.test_util.include @@ -20,6 +20,7 @@ TEST_UTIL_H = \ test/util/random.h \ test/util/script.h \ test/util/setup_common.h \ + test/util/source_root.h \ test/util/str.h \ test/util/transaction_utils.h \ test/util/txmempool.h \ diff --git a/src/chain.cpp b/src/chain.cpp index dc7772f8f69..962b1411c11 100644 --- a/src/chain.cpp +++ b/src/chain.cpp @@ -11,14 +11,50 @@ #include #include -/** - * CBlockIndex default constructor - */ -CBlockIndex::CBlockIndex() +namespace { +class LocalBlockHeader final : public BlockHeaderSource { + const BlockHeaderData m_data; +public: + explicit LocalBlockHeader(const BlockHeaderData& data) : m_data{data} {} + BlockHeaderData Read(const uint256&) const override { return m_data; } + bool IsShared() const override { return false; } +}; +} // namespace + +BlockHeaderData CBlockIndex::GetHeaderData() const { - for (unsigned i = 0; i < NUM_ALGOS_IMPL; i++) { - lastAlgoBlocks[i] = nullptr; - } + if (!m_header_source) return {}; + return m_header_source->Read(m_header_source->IsShared() ? GetBlockHash() : uint256{}); +} + +void CBlockIndex::SetHeaderData(const BlockHeaderData& data) +{ + auto replacement = std::make_unique(data); + if (m_header_source && !m_header_source->IsShared()) delete m_header_source; + m_header_source = replacement.release(); +} + +void CBlockIndex::UseHeaderSource(const BlockHeaderSource& source) +{ + assert(source.IsShared()); + if (m_header_source && !m_header_source->IsShared()) delete m_header_source; + m_header_source = &source; +} + +CBlockIndex::~CBlockIndex() +{ + if (m_header_source && !m_header_source->IsShared()) delete m_header_source; +} + +CBlockIndex::CBlockIndex(const CBlockIndex& other) + : m_chain_work{other.m_chain_work}, + phashBlock{other.phashBlock}, pprev{other.pprev}, pskip{other.pskip}, + nHeight{other.nHeight}, nFile{other.nFile}, nDataPos{other.nDataPos}, nUndoPos{other.nUndoPos}, + nTx{other.nTx}, nChainTx{other.nChainTx}, nStatus{other.nStatus}, + nVersion{other.nVersion}, nTime{other.nTime}, nBits{other.nBits}, + nSequenceId{other.nSequenceId}, nTimeMax{other.nTimeMax} +{ + SetHeaderData(other.GetHeaderData()); } /** @@ -27,22 +63,14 @@ CBlockIndex::CBlockIndex() */ CBlockIndex::CBlockIndex(const CBlockHeader& block) : nVersion(block.nVersion), - hashMerkleRoot(block.hashMerkleRoot), nTime(block.nTime), - nBits(block.nBits), - nNonce(block.nNonce) + nBits(block.nBits) { - // Initialize lastAlgoBlocks to null. - for (unsigned i = 0; i < NUM_ALGOS_IMPL; i++) { - lastAlgoBlocks[i] = nullptr; - } - - // Determine raw algo index from version bits: - int rawAlgo = block.GetAlgo(); // This returns ALGO_UNKNOWN if it doesn't match recognized bits - if (rawAlgo >= 0 && rawAlgo < NUM_ALGOS_IMPL) { - lastAlgoBlocks[rawAlgo] = this; - } else { - // We can log this occurrence: + SetHeaderData({block.hashMerkleRoot, block.nNonce}); + // A block header names its mining algorithm in its version bits. Say so + // once here if the bits are not one of DigiByte's five algorithms; GetAlgo + // below then treats the block as Scrypt. + if (block.GetAlgo() == ALGO_UNKNOWN) { LogPrintf("CBlockIndex ctor: ALGO_UNKNOWN in block version=0x%08x\n", block.nVersion); } } @@ -55,7 +83,7 @@ std::string CBlockFileInfo::ToString() const std::string CBlockIndex::ToString() const { return strprintf("CBlockIndex(pprev=%p, nHeight=%d, merkle=%s, hashBlock=%s)", - pprev, nHeight, hashMerkleRoot.ToString(), GetBlockHash().ToString()); + pprev, nHeight, GetHeaderData().merkle_root.ToString(), GetBlockHash().ToString()); } void CChain::SetTip(CBlockIndex& block) { @@ -116,8 +144,8 @@ CBlockIndex* CChain::FindEarliestAtLeast(int64_t nTime, int height) const } /** - * Return recognized mining algo for this block, forcibly mapping blocks - * below height 145,000 to ALGO_SCRYPT. If none recognized, logs a warning. + * Return the mining algorithm named in this block's version bits. If the bits + * name none of them, log a warning and call it Scrypt. */ int CBlockIndex::GetAlgo() const { @@ -126,13 +154,13 @@ int CBlockIndex::GetAlgo() const // that naturally map to BLOCK_VERSION_SCRYPT (algo bits = 0x0000), so no // special height check is needed. // - // CRITICAL FIX: Previously this had a hardcoded `if (nHeight < 145000)` - // check that forced ALGO_SCRYPT for all blocks below mainnet's multi-algo - // height. This broke testnet/regtest where multi-algo activates much earlier - // (block 100), causing the lastAlgoBlocks[] index to only track Scrypt. - // As a result, GetLastBlockIndexForAlgoFast() returned NULL for all non-Scrypt - // algos, and DigiShield V4 fell back to InitialDifficulty (powLimit) every - // time — difficulty never adjusted for Qubit, Skein, SHA256D, or Odocrypt. + // This used to force Scrypt for every block below mainnet height 145,000. + // That broke testnet and regtest, where multi-algorithm mining starts much + // earlier, at block 100: every earlier block looked like a Scrypt block, + // the difficulty rules could not find a previous block of the other + // algorithms, and DigiShield V4 fell back to the easiest possible + // difficulty every time. Difficulty never adjusted there for Qubit, + // Skein, SHA256D or Odocrypt. switch (nVersion & BLOCK_VERSION_ALGO) { case BLOCK_VERSION_SCRYPT: return ALGO_SCRYPT; case BLOCK_VERSION_SHA256D: return ALGO_SHA256D; @@ -262,9 +290,13 @@ arith_uint256 GetBlockProofBase(const CBlockIndex& block) return (~bnTarget / (bnTarget + 1)) + 1; } -arith_uint256 GetBlockProof(const CBlockIndex& block) +static arith_uint256 GetBlockProofImpl(const CBlockIndex& block, const PreviousAlgoBlocks* previous_algos) { - CBlockHeader header = block.GetBlockHeader(); + // Work uses the header version and time, which remain in memory. Preserve + // CBlockHeader's algorithm decoding, including unknown version bits. + CBlockHeader header; + header.nVersion = block.nVersion; + header.nTime = block.nTime; int nHeight = block.nHeight; const Consensus::Params& params = Params().GetConsensus(); @@ -279,7 +311,9 @@ arith_uint256 GetBlockProof(const CBlockIndex& block) for (int i = 0; i < NUM_ALGOS_IMPL; i++) { if (!IsAlgoActive(block.pprev, params, i)) continue; - unsigned int nBits = GetNextWorkRequired(block.pprev, &header, params, i); + unsigned int nBits = previous_algos + ? GetNextWorkRequired(block.pprev, &header, params, i, *previous_algos) + : GetNextWorkRequired(block.pprev, &header, params, i); arith_uint256 bnTarget; bool fNegative; bool fOverflow; @@ -298,9 +332,21 @@ arith_uint256 GetBlockProof(const CBlockIndex& block) } } +arith_uint256 GetBlockProof(const CBlockIndex& block) +{ + return GetBlockProofImpl(block, nullptr); +} + +arith_uint256 GetBlockProof(const CBlockIndex& block, const PreviousAlgoBlocks& previous_algos) +{ + return GetBlockProofImpl(block, &previous_algos); +} + arith_uint256 GetBlockProof(const CBlockIndex& block, int algo) { - CBlockHeader header = block.GetBlockHeader(); + CBlockHeader header; + header.nVersion = block.nVersion; + header.nTime = block.nTime; int nHeight = block.nHeight; const Consensus::Params& params = Params().GetConsensus(); @@ -327,10 +373,10 @@ int64_t GetBlockProofEquivalentTime(const CBlockIndex& to, const CBlockIndex& fr { arith_uint256 r; int sign = 1; - if (to.nChainWork > from.nChainWork) { - r = to.nChainWork - from.nChainWork; + if (to.GetChainWork() > from.GetChainWork()) { + r = to.GetChainWork() - from.GetChainWork(); } else { - r = from.nChainWork - to.nChainWork; + r = from.GetChainWork() - to.GetChainWork(); sign = -1; } r = r * arith_uint256(params.nPowTargetSpacing) / GetBlockProof(tip); diff --git a/src/chain.h b/src/chain.h index 82c4de58f7d..279db4a2ebc 100644 --- a/src/chain.h +++ b/src/chain.h @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -143,8 +144,28 @@ enum BlockStatus : uint32_t { * candidates to be the next block. A blockindex may have multiple pprev pointing * to it, but at most one of them can be part of the currently active branch. */ +struct BlockHeaderData { + uint256 merkle_root; + uint32_t nonce{0}; +}; + +/** Immutable header fields, shared by all entries owned by one block manager. + * A read failure is a local storage error and must not produce a made-up header. + */ +class BlockHeaderSource { +public: + virtual ~BlockHeaderSource() = default; + virtual BlockHeaderData Read(const uint256& hash) const = 0; + virtual bool IsShared() const { return true; } +}; + class CBlockIndex { + // Shared sources outlive the index. Local sources belong to this entry. + const BlockHeaderSource* m_header_source{nullptr}; + //! (memory only) Total work in the chain up to and including this block. + CompactWork m_chain_work; + public: //! pointer to the hash of the block, if any. Memory is owned by this CBlockIndex const uint256* phashBlock{nullptr}; @@ -167,8 +188,8 @@ class CBlockIndex //! Byte offset within rev?????.dat where this block's undo data is stored unsigned int nUndoPos GUARDED_BY(::cs_main){0}; - //! (memory only) Total amount of work (expected number of hashes) in the chain up to and including this block - arith_uint256 nChainWork{}; + arith_uint256 GetChainWork() const noexcept { return m_chain_work.Get(); } + void SetChainWork(arith_uint256 work) { m_chain_work.Set(work); } //! Number of transactions in this block. //! Note: in a potential headers-first mode, this number cannot be relied upon @@ -197,10 +218,13 @@ class CBlockIndex //! block header int32_t nVersion{0}; - uint256 hashMerkleRoot{}; uint32_t nTime{0}; uint32_t nBits{0}; - uint32_t nNonce{0}; + + BlockHeaderData GetHeaderData() const; + void SetHeaderData(const BlockHeaderData& data); + /** Only call while constructing or loading the index, before publishing it. */ + void UseHeaderSource(const BlockHeaderSource& source); //! (memory only) Sequential id assigned to distinguish order in which blocks are received. int32_t nSequenceId{0}; @@ -208,10 +232,6 @@ class CBlockIndex //! (memory only) Maximum nTime in the chain up to and including this block. unsigned int nTimeMax{0}; - //! DigiByte: Track last block per algorithm for multi-algo mining - CBlockIndex *lastAlgoBlocks[NUM_ALGOS_IMPL]; - - /** * Full constructor that copies fields from a block header. * (Definition is moved to chain.cpp so we can log from there) @@ -242,14 +262,15 @@ class CBlockIndex CBlockHeader GetBlockHeader() const { + const auto data = GetHeaderData(); CBlockHeader block; block.nVersion = nVersion; if (pprev) block.hashPrevBlock = pprev->GetBlockHash(); - block.hashMerkleRoot = hashMerkleRoot; + block.hashMerkleRoot = data.merkle_root; block.nTime = nTime; block.nBits = nBits; - block.nNonce = nNonce; + block.nNonce = data.nonce; return block; } @@ -364,8 +385,8 @@ class CBlockIndex CBlockIndex* GetAncestor(int height); const CBlockIndex* GetAncestor(int height) const; - CBlockIndex(); - ~CBlockIndex() = default; + CBlockIndex() = default; + ~CBlockIndex(); protected: //! CBlockIndex should not allow public copy construction because equality @@ -377,13 +398,18 @@ class CBlockIndex //! //! We declare these protected instead of simply deleting them so that //! CDiskBlockIndex can reuse copy construction. - CBlockIndex(const CBlockIndex&) = default; + CBlockIndex(const CBlockIndex&); CBlockIndex& operator=(const CBlockIndex&) = delete; CBlockIndex(CBlockIndex&&) = delete; CBlockIndex& operator=(CBlockIndex&&) = delete; }; arith_uint256 GetBlockProof(const CBlockIndex& block); +/** + * Use equivalent per-algorithm ancestors when only the recent linked history + * is retained. See GetNextWorkRequired for the required history and lookups. + */ +arith_uint256 GetBlockProof(const CBlockIndex& block, const PreviousAlgoBlocks& previous_algos); arith_uint256 GetBlockProof(const CBlockIndex& block, int algo); /** Return the time it would take to redo the work difference between from and to, assuming the current hashrate corresponds to the difficulty at tip, in seconds. */ @@ -419,6 +445,7 @@ class CDiskBlockIndex : public CBlockIndex SERIALIZE_METHODS(CDiskBlockIndex, obj) { LOCK(::cs_main); + auto header_data = obj.GetHeaderData(); int _nVersion = DUMMY_VERSION; READWRITE(VARINT_MODE(_nVersion, VarIntMode::NONNEGATIVE_SIGNED)); @@ -432,21 +459,23 @@ class CDiskBlockIndex : public CBlockIndex // block header READWRITE(obj.nVersion); READWRITE(obj.hashPrev); - READWRITE(obj.hashMerkleRoot); + READWRITE(header_data.merkle_root); READWRITE(obj.nTime); READWRITE(obj.nBits); - READWRITE(obj.nNonce); + READWRITE(header_data.nonce); + SER_READ(obj, obj.SetHeaderData(header_data)); } uint256 ConstructBlockHash() const { + const auto data = GetHeaderData(); CBlockHeader block; block.nVersion = nVersion; block.hashPrevBlock = hashPrev; - block.hashMerkleRoot = hashMerkleRoot; + block.hashMerkleRoot = data.merkle_root; block.nTime = nTime; block.nBits = nBits; - block.nNonce = nNonce; + block.nNonce = data.nonce; return block.GetHash(); } diff --git a/src/chainparams.cpp b/src/chainparams.cpp index 51037ee67dc..69d29e58062 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -109,6 +109,21 @@ void ReadRegTestArgs(const ArgsManager& args, CChainParams::RegTestOptions& opti options.digidollar_activation_height = static_cast(*digidollar_height); LogPrintf("Setting DigiDollar activation height for regtest to %d (buried deployment)\n", *digidollar_height); } + + // Thaw Day height for regtest: the one height at which every consensus + // change of the DigiDollar Thaw Day release takes effect. Independent of + // -digidollaractivationheight (it may lie before, at, or after DigiDollar + // activation; DigiDollar::IsThawDayActive handles the order). Parsed + // strictly: anything that is not a whole number in [0, int max) is a + // startup error rather than a silent zero. + if (const auto thaw_day_height = args.GetArg("-ddthawdayheight")) { + int32_t height; + if (!ParseInt32(*thaw_day_height, &height) || height < 0 || height >= std::numeric_limits::max()) { + throw std::runtime_error(strprintf("Invalid height value (%s) for -ddthawdayheight: expected a whole number from 0 up to, but not including, %d.", *thaw_day_height, std::numeric_limits::max())); + } + options.dd_thaw_day_height = int{height}; + LogPrintf("Setting DigiDollar Thaw Day height for regtest to %d\n", height); + } } static std::unique_ptr globalChainParams; @@ -120,6 +135,15 @@ const CChainParams &Params() { std::unique_ptr CreateChainParams(const ArgsManager& args, const ChainType chain) { + // The Thaw Day height of a public network is fixed in the release and + // cannot be moved from the command line or the config file; only regtest + // accepts the knob. Refusing to start is safer than quietly ignoring the + // option, which could leave an operator believing a different height is + // in force. + if (chain != ChainType::REGTEST && args.IsArgSet("-ddthawdayheight")) { + throw std::runtime_error(strprintf("-ddthawdayheight is only accepted on regtest; the Thaw Day height of the %s network is fixed in the release.", ChainTypeToString(chain))); + } + switch (chain) { case ChainType::MAIN: return CChainParams::Main(); diff --git a/src/clientversion.cpp b/src/clientversion.cpp index 4931761b956..c33563fc0c6 100644 --- a/src/clientversion.cpp +++ b/src/clientversion.cpp @@ -89,7 +89,7 @@ std::string CopyrightHolders(const std::string& strPrefix) std::string LicenseInfo() { - const std::string URL_SOURCE_CODE = ""; + const std::string URL_SOURCE_CODE = ""; return CopyrightHolders(strprintf(_("Copyright (C) %i-%i").translated, 2009, COPYRIGHT_YEAR) + " ") + "\n" + "\n" + diff --git a/src/coins.cpp b/src/coins.cpp index 4ce4fc8b020..b4ec6ea8e83 100644 --- a/src/coins.cpp +++ b/src/coins.cpp @@ -7,13 +7,19 @@ #include #include #include +#include #include #include +#include +#include +#include + bool CCoinsView::GetCoin(const COutPoint &outpoint, Coin &coin) const { return false; } uint256 CCoinsView::GetBestBlock() const { return uint256(); } +std::optional CCoinsView::GetDigiDollarState() const { return std::nullopt; } std::vector CCoinsView::GetHeadBlocks() const { return std::vector(); } -bool CCoinsView::BatchWrite(CCoinsMap &mapCoins, const uint256 &hashBlock, bool erase) { return false; } +bool CCoinsView::BatchWrite(CCoinsMap &mapCoins, const uint256 &hashBlock, bool erase, const std::optional& dd_state) { return false; } std::unique_ptr CCoinsView::Cursor() const { return nullptr; } bool CCoinsView::HaveCoin(const COutPoint &outpoint) const @@ -26,9 +32,10 @@ CCoinsViewBacked::CCoinsViewBacked(CCoinsView *viewIn) : base(viewIn) { } bool CCoinsViewBacked::GetCoin(const COutPoint &outpoint, Coin &coin) const { return base->GetCoin(outpoint, coin); } bool CCoinsViewBacked::HaveCoin(const COutPoint &outpoint) const { return base->HaveCoin(outpoint); } uint256 CCoinsViewBacked::GetBestBlock() const { return base->GetBestBlock(); } +std::optional CCoinsViewBacked::GetDigiDollarState() const { return base->GetDigiDollarState(); } std::vector CCoinsViewBacked::GetHeadBlocks() const { return base->GetHeadBlocks(); } void CCoinsViewBacked::SetBackend(CCoinsView &viewIn) { base = &viewIn; } -bool CCoinsViewBacked::BatchWrite(CCoinsMap &mapCoins, const uint256 &hashBlock, bool erase) { return base->BatchWrite(mapCoins, hashBlock, erase); } +bool CCoinsViewBacked::BatchWrite(CCoinsMap &mapCoins, const uint256 &hashBlock, bool erase, const std::optional& dd_state) { return base->BatchWrite(mapCoins, hashBlock, erase, dd_state); } std::unique_ptr CCoinsViewBacked::Cursor() const { return base->Cursor(); } size_t CCoinsViewBacked::EstimateSize() const { return base->EstimateSize(); } @@ -179,7 +186,165 @@ void CCoinsViewCache::SetBestBlock(const uint256 &hashBlockIn) { hashBlock = hashBlockIn; } -bool CCoinsViewCache::BatchWrite(CCoinsMap &mapCoins, const uint256 &hashBlockIn, bool erase) { +std::optional CCoinsViewCache::GetDigiDollarState() const +{ + if (!m_dd_state_loaded) { + m_dd_state = base->GetDigiDollarState(); + m_dd_state_loaded = true; + } + if (m_dd_state && (!m_dd_state->IsValid() || m_dd_state->best_block != GetBestBlock())) return std::nullopt; + return m_dd_state; +} + +void CCoinsViewCache::SetDigiDollarState(std::optional state) +{ + m_dd_state = std::move(state); + m_dd_state_loaded = true; +} + +namespace { +// Match the coin database's byte order: transaction hash, then the output +// number encoded as VARINT. Numeric output order differs once the encoding +// grows, so using it here can miss a cache entry that spends a database coin. +bool CoinDatabaseKeyLess(const COutPoint& a, const COutPoint& b) +{ + const int hash_order = a.hash.Compare(b.hash); + if (hash_order != 0) return hash_order < 0; + if (a.n == b.n) return false; + + DataStream a_index, b_index; + a_index << VARINT(a.n); + b_index << VARINT(b.n); + return std::lexicographical_compare(a_index.begin(), a_index.end(), b_index.begin(), b_index.end()); +} + +/** Walks every unspent coin of a coins cache exactly once. + * + * A cache holds only the changes made since the last write to the coins + * database, so the coins it represents are the database contents with those + * changed entries laid over the top. This walk merges the two: it steps through + * the backing cursor and through a sorted copy of the changed entries side by + * side, always taking whichever outpoint comes first. Where both hold the same + * outpoint the cache entry wins, because it is the newer one. Entries the cache + * records as spent hide the database copy and are never handed out. + * + * Merging keeps the outpoint order of the backing cursor, which matters because + * how much of a cache has been written to disk is a local matter: two nodes at + * the same block can hold quite different caches. Walking the cache entries + * separately, in whatever order the hash table happens to hold them, would give + * those two nodes different orders. Merging gives them the same one. + * + * The backing cursor has to use the database's serialized-key order. This + * walk uses that order too, so a cache laid over another cache works the same + * way. Numeric output order is not a substitute for serialized-key order. + * + * The changed entries are copied when the walk starts. Pointing at them would + * be cheaper, but a caller may read a coin from the very view it is walking, + * and reading a coin the cache has not held before adds an entry to it, which + * moves the existing ones about. The DigiDollar vault scan does exactly that. + */ +class CacheCoinsCursor final : public CCoinsViewCursor { + using Entry = std::pair; + std::unique_ptr m_base; + //! The cache's changed entries, sorted in database key order. Spent ones are kept + //! because they are what hides a coin the database still has. + std::vector m_changed; + //! Index into m_changed of the entry being looked at, or the size when + //! there are none left. + size_t m_pos{0}; + //! True when the coin to hand out right now comes from m_changed rather + //! than from the backing cursor. + bool m_from_cache{false}; + + //! Move to the next coin to hand out, or leave nothing to hand out. + void Seek() + { + while (true) { + COutPoint base_key; + const bool base_has_key = m_base->Valid() && m_base->GetKey(base_key); + if (m_base->Valid() && !base_has_key) { + // The backing cursor says it has a coin but will not give up + // its outpoint. Stop here and let the caller find that it + // cannot read this coin, rather than quietly skipping the rest + // of the database and reporting a short answer. + m_from_cache = false; + return; + } + if (m_pos == m_changed.size()) { + // Only the backing cursor is left, valid or finished. + m_from_cache = false; + return; + } + if (base_has_key && CoinDatabaseKeyLess(base_key, m_changed[m_pos].first)) { + m_from_cache = false; + return; + } + if (base_has_key && base_key == m_changed[m_pos].first) { + // The cache changed this coin, so its copy replaces the one in + // the database. Step the database past it either way. + m_base->Next(); + } + if (!m_changed[m_pos].second.IsSpent()) { + m_from_cache = true; + return; + } + // The cache spent this coin, so nobody gets it. + ++m_pos; + } + } + +public: + CacheCoinsCursor(std::unique_ptr base, const CCoinsMap& cache, const uint256& block) + : CCoinsViewCursor(block), m_base(std::move(base)) + { + size_t changed = 0; + for (const auto& [outpoint, entry] : cache) { + if (entry.flags & CCoinsCacheEntry::DIRTY) ++changed; + } + m_changed.reserve(changed); + for (const auto& [outpoint, entry] : cache) { + if (entry.flags & CCoinsCacheEntry::DIRTY) m_changed.emplace_back(outpoint, entry.coin); + } + std::sort(m_changed.begin(), m_changed.end(), + [](const Entry& a, const Entry& b) { return CoinDatabaseKeyLess(a.first, b.first); }); + Seek(); + } + bool GetKey(COutPoint& key) const override + { + if (m_from_cache) { + key = m_changed[m_pos].first; + return true; + } + return m_base->Valid() && m_base->GetKey(key); + } + bool GetValue(Coin& coin) const override + { + if (m_from_cache) { + coin = m_changed[m_pos].second; + return true; + } + return m_base->Valid() && m_base->GetValue(coin); + } + bool Valid() const override { return m_from_cache || m_base->Valid(); } + void CheckStatus() const override { m_base->CheckStatus(); } + void Next() override + { + if (m_from_cache) ++m_pos; + else if (m_base->Valid()) m_base->Next(); + Seek(); + } +}; +} // namespace + +std::unique_ptr CCoinsViewCache::Cursor() const +{ + auto cursor = base->Cursor(); + if (!cursor) return nullptr; + return std::make_unique(std::move(cursor), cacheCoins, GetBestBlock()); +} + +bool CCoinsViewCache::BatchWrite(CCoinsMap &mapCoins, const uint256 &hashBlockIn, bool erase, const std::optional& dd_state) { + if (dd_state && (!dd_state->IsValid() || dd_state->best_block != hashBlockIn)) return false; for (CCoinsMap::iterator it = mapCoins.begin(); it != mapCoins.end(); it = erase ? mapCoins.erase(it) : std::next(it)) { @@ -248,25 +413,51 @@ bool CCoinsViewCache::BatchWrite(CCoinsMap &mapCoins, const uint256 &hashBlockIn } } hashBlock = hashBlockIn; + SetDigiDollarState(dd_state); return true; } bool CCoinsViewCache::Flush() { - bool fOk = base->BatchWrite(cacheCoins, hashBlock, /*erase=*/true); + const auto state = GetDigiDollarState(); + auto failed = [&] { + SetDigiDollarState(std::nullopt); + cachedCoinsUsage = 0; + for (const auto& [_, entry] : cacheCoins) cachedCoinsUsage += entry.coin.DynamicMemoryUsage(); + }; + bool fOk; + try { + fOk = base->BatchWrite(cacheCoins, GetBestBlock(), /*erase=*/true, state); + } catch (...) { + failed(); + throw; + } if (fOk) { if (!cacheCoins.empty()) { /* BatchWrite must erase all cacheCoins elements when erase=true. */ throw std::logic_error("Not all cached coins were erased"); } ReallocateCache(); + cachedCoinsUsage = 0; + } else { + failed(); } - cachedCoinsUsage = 0; return fOk; } bool CCoinsViewCache::Sync() { - bool fOk = base->BatchWrite(cacheCoins, hashBlock, /*erase=*/false); + const auto state = GetDigiDollarState(); + bool fOk; + try { + fOk = base->BatchWrite(cacheCoins, GetBestBlock(), /*erase=*/false, state); + } catch (...) { + SetDigiDollarState(std::nullopt); + throw; + } + if (!fOk) { + SetDigiDollarState(std::nullopt); + return false; + } // Instead of clearing `cacheCoins` as we would in Flush(), just clear the // FRESH/DIRTY flags of any coin that isn't spent. for (auto it = cacheCoins.begin(); it != cacheCoins.end(); ) { diff --git a/src/coins.h b/src/coins.h index f65867c2dcb..641fa64dc0a 100644 --- a/src/coins.h +++ b/src/coins.h @@ -7,6 +7,7 @@ #define DIGIBYTE_COINS_H #include +#include #include #include #include @@ -19,6 +20,7 @@ #include #include +#include #include /** @@ -161,6 +163,8 @@ class CCoinsViewCursor virtual bool Valid() const = 0; virtual void Next() = 0; + //! Distinguish a complete scan from an iterator or key decoding failure. + virtual void CheckStatus() const {} //! Get best block at the time this cursor was created const uint256 &GetBestBlock() const { return hashBlock; } @@ -184,6 +188,9 @@ class CCoinsView //! Retrieve the block hash whose state this CCoinsView currently represents virtual uint256 GetBestBlock() const; + //! Derived health record for this exact view, or not ready. + virtual std::optional GetDigiDollarState() const; + //! Retrieve the range of blocks that may have been only partially written. //! If the database is in a consistent state, the result is the empty vector. //! Otherwise, a two-element vector is returned consisting of the new and @@ -192,7 +199,7 @@ class CCoinsView //! Do a bulk modification (multiple Coin changes + BestBlock change). //! The passed mapCoins can be modified. - virtual bool BatchWrite(CCoinsMap &mapCoins, const uint256 &hashBlock, bool erase = true); + virtual bool BatchWrite(CCoinsMap &mapCoins, const uint256 &hashBlock, bool erase = true, const std::optional& dd_state = std::nullopt); //! Get a cursor to iterate over the whole state virtual std::unique_ptr Cursor() const; @@ -216,9 +223,10 @@ class CCoinsViewBacked : public CCoinsView bool GetCoin(const COutPoint &outpoint, Coin &coin) const override; bool HaveCoin(const COutPoint &outpoint) const override; uint256 GetBestBlock() const override; + std::optional GetDigiDollarState() const override; std::vector GetHeadBlocks() const override; void SetBackend(CCoinsView &viewIn); - bool BatchWrite(CCoinsMap &mapCoins, const uint256 &hashBlock, bool erase = true) override; + bool BatchWrite(CCoinsMap &mapCoins, const uint256 &hashBlock, bool erase = true, const std::optional& dd_state = std::nullopt) override; std::unique_ptr Cursor() const override; size_t EstimateSize() const override; }; @@ -236,6 +244,8 @@ class CCoinsViewCache : public CCoinsViewBacked * declared as "const". */ mutable uint256 hashBlock; + mutable bool m_dd_state_loaded{false}; + mutable std::optional m_dd_state; mutable CCoinsMapMemoryResource m_cache_coins_memory_resource{}; mutable CCoinsMap cacheCoins; @@ -255,10 +265,17 @@ class CCoinsViewCache : public CCoinsViewBacked bool HaveCoin(const COutPoint &outpoint) const override; uint256 GetBestBlock() const override; void SetBestBlock(const uint256 &hashBlock); - bool BatchWrite(CCoinsMap &mapCoins, const uint256 &hashBlock, bool erase = true) override; - std::unique_ptr Cursor() const override { - throw std::logic_error("CCoinsViewCache cursor iteration not supported."); - } + std::optional GetDigiDollarState() const override; + void SetDigiDollarState(std::optional state); + bool BatchWrite(CCoinsMap &mapCoins, const uint256 &hashBlock, bool erase = true, const std::optional& dd_state = std::nullopt) override; + //! Walk every unspent coin this cache represents, exactly once: the coins + //! still held by the backing view, with this cache's unwritten changes laid + //! over them. Coins come out in increasing outpoint order, which is the + //! coins database's own key order, so the order does not depend on how much + //! of the cache has been written to disk. The changed entries are copied + //! when the walk starts, so reading a coin from this cache during a walk is + //! safe. Returns null if the backing view cannot be walked at all. + std::unique_ptr Cursor() const override; /** * Check if we have the given utxo already loaded in this cache. diff --git a/src/consensus/dca.cpp b/src/consensus/dca.cpp index 20d5197dba2..c80d1869ec2 100644 --- a/src/consensus/dca.cpp +++ b/src/consensus/dca.cpp @@ -157,6 +157,13 @@ int DynamicCollateralAdjustment::ApplyDCA(int baseRatio, int systemHealth) return std::numeric_limits::max(); } + return ApplyDCAForHealth(baseRatio, std::clamp(resolvedHealth, 0, 30000)); +} + +int DynamicCollateralAdjustment::ApplyDCAForHealth(int baseRatio, int systemHealth) +{ + if (baseRatio <= 0 || systemHealth < 0 || systemHealth > 30000) return 0; + const int resolvedHealth = systemHealth; int multiplierBps = GetDCAMultiplierBps(resolvedHealth); __int128 adjustedRatio = static_cast<__int128>(baseRatio) * diff --git a/src/consensus/dca.h b/src/consensus/dca.h index b83c7e584db..5bcc992fdb1 100644 --- a/src/consensus/dca.h +++ b/src/consensus/dca.h @@ -105,6 +105,9 @@ class DynamicCollateralAdjustment { */ static int ApplyDCA(int baseRatio, int systemHealth); + /** Apply the same integer formula to an already verified candidate health. */ + static int ApplyDCAForHealth(int baseRatio, int systemHealth); + /** * Get current health tier information. * diff --git a/src/consensus/digidollar_state.h b/src/consensus/digidollar_state.h new file mode 100644 index 00000000000..f27174e3b70 --- /dev/null +++ b/src/consensus/digidollar_state.h @@ -0,0 +1,100 @@ +// Copyright (c) 2026 The DigiByte Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef DIGIBYTE_CONSENSUS_DIGIDOLLAR_STATE_H +#define DIGIBYTE_CONSENSUS_DIGIDOLLAR_STATE_H + +#include +#include +#include + +#include +#include + +namespace DigiDollar { + +/** Derived health inputs belonging to exactly one UTXO state. + * A missing record is distinct from a verified system with no open vaults. + * Only validation of the activated history may establish history_checked. + */ +struct ChainstateHealth { + static constexpr uint32_t FORMAT_VERSION{1}; + static constexpr uint32_t RULES_VERSION{1}; + + uint32_t format_version{FORMAT_VERSION}; + uint256 genesis_hash; + uint32_t rules_version{RULES_VERSION}; + int32_t activation_height{std::numeric_limits::max()}; + int32_t digidollar_height{std::numeric_limits::max()}; + uint256 best_block; + CAmount open_vault_principal{0}; + CAmount collateral{0}; + uint64_t active_vaults{0}; + bool history_checked{false}; + + SERIALIZE_METHODS(ChainstateHealth, obj) + { + READWRITE(obj.format_version, obj.genesis_hash, obj.rules_version, obj.activation_height, obj.digidollar_height, + obj.best_block, obj.open_vault_principal, obj.collateral, + obj.active_vaults, obj.history_checked); + } + + bool IsValid() const + { + return format_version == FORMAT_VERSION && rules_version == RULES_VERSION && + activation_height >= 0 && digidollar_height >= 0 && !genesis_hash.IsNull() && !best_block.IsNull() && + open_vault_principal >= 0 && MoneyRange(collateral) && + (active_vaults == 0 ? open_vault_principal == 0 && collateral == 0 : + open_vault_principal > 0 && collateral > 0); + } + + bool Matches(const uint256& genesis, const uint256& block) const + { + return IsValid() && genesis_hash == genesis && best_block == block; + } + + bool Matches(const uint256& genesis, const uint256& block, int activation, int dd_activation) const + { + return Matches(genesis, block) && activation_height == activation && digidollar_height == dd_activation; + } + + /** Checked, transactional arithmetic; a failure leaves this record unchanged. */ + bool AddVault(CAmount principal, CAmount value) + { + if (principal <= 0 || value <= 0 || !MoneyRange(value) || + open_vault_principal < 0 || collateral < 0 || + principal > std::numeric_limits::max() - open_vault_principal || + collateral > MAX_MONEY - value || + active_vaults == std::numeric_limits::max()) return false; + open_vault_principal += principal; + collateral += value; + ++active_vaults; + return true; + } + + bool RemoveVault(CAmount principal, CAmount value) + { + if (principal <= 0 || value <= 0 || active_vaults == 0 || + principal > open_vault_principal || value > collateral) return false; + const auto remaining = active_vaults - 1; + if (remaining == 0 ? principal != open_vault_principal || value != collateral : + principal == open_vault_principal || value == collateral) return false; + open_vault_principal -= principal; + collateral -= value; + active_vaults = remaining; + return true; + } + + friend bool operator==(const ChainstateHealth& a, const ChainstateHealth& b) + { + return a.format_version == b.format_version && a.genesis_hash == b.genesis_hash && + a.rules_version == b.rules_version && a.activation_height == b.activation_height && a.digidollar_height == b.digidollar_height && a.best_block == b.best_block && + a.open_vault_principal == b.open_vault_principal && a.collateral == b.collateral && + a.active_vaults == b.active_vaults && a.history_checked == b.history_checked; + } + friend bool operator!=(const ChainstateHealth& a, const ChainstateHealth& b) { return !(a == b); } +}; + +} // namespace DigiDollar +#endif // DIGIBYTE_CONSENSUS_DIGIDOLLAR_STATE_H diff --git a/src/consensus/params.h b/src/consensus/params.h index 80ad44df746..6efdf5b439e 100644 --- a/src/consensus/params.h +++ b/src/consensus/params.h @@ -128,6 +128,16 @@ struct Params { * blocks) becomes active. Buried BIP9 deployment (bit 0); enforcement is * OR'd with the static nGroestlDeactivationHeight backstop. */ int AlgoLockHeight{std::numeric_limits::max()}; + /** Block height at which every consensus change of the DigiDollar + * "Thaw Day" release takes effect at once: the new mint-only volatility + * rule, open-vault health accounting, and canonical vault identity. + * The maximum int value means "not scheduled on this network". Blocks + * below this height keep the old rules for ever, including during a + * reindex and across reorgs: which rules a block gets depends only on + * that block's own height, never on where the node's tip is now. + * Read it through DigiDollar::IsThawDayActive(), and never add to it: + * the "not scheduled" value is the largest int and would overflow. */ + int nDDThawDayHeight{std::numeric_limits::max()}; /** Don't warn about unknown BIP 9 activations below this height. * This prevents us from warning about the CSV and segwit activations. */ int MinBIP9WarningHeight; @@ -210,6 +220,8 @@ struct Params { int nOracleRequiredMessages{1}; // Off-chain signed price messages required before MuSig2 aggregation int nOracleTotalOracles{1}; // Total active oracle operators std::vector vOraclePublicKeys; // Hardcoded oracle public keys (hex encoded XOnlyPubKey, sorted) + // Full compressed keys in oracle slot order, retaining point parity in parameter copies. + std::vector> vOracleCompressedPublicKeys; int nDigiDollarMuSig2Height{std::numeric_limits::max()}; // Height when MuSig2 aggregate signatures are valid /** MuSig2 oracle configuration */ diff --git a/src/consensus/volatility.cpp b/src/consensus/volatility.cpp index 2ef87466f06..13924471da2 100644 --- a/src/consensus/volatility.cpp +++ b/src/consensus/volatility.cpp @@ -3,6 +3,8 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include +#include +#include #include #include #include @@ -86,6 +88,79 @@ double CalculateStandardDeviationDouble(const std::vector& values) } // namespace +MintReference BuildMintReference(int candidate_height, const CBlockIndex* parent, + const Consensus::Params& params, const AncestorPriceReader& read_price) +{ + MintReference result; + result.candidate_height = candidate_height; + result.genesis_hash = params.hashGenesisBlock; + if (candidate_height < 0 || (parent ? parent->nHeight != candidate_height - 1 : candidate_height != 0)) { + result.error = "DigiDollar volatility state not ready: candidate parent is unavailable or mismatched"; + return result; + } + if (parent) result.parent_hash = parent->GetBlockHash(); + result.window_start_height = std::max(0, candidate_height - MINT_REFERENCE_MAX_DEPTH); + result.window_end_height = candidate_height - MINT_REFERENCE_MIN_DEPTH; + const int first_dd_height = params.DeploymentHeight(Consensus::DEPLOYMENT_DIGIDOLLAR); + const int oldest = std::max(result.window_start_height, first_dd_height); + std::vector prices; + prices.reserve(MINT_REFERENCE_SAMPLES); + if (result.window_end_height >= oldest) { + const CBlockIndex* ancestor = parent; + while (ancestor && ancestor->nHeight > result.window_end_height) ancestor = ancestor->pprev; + for (int height = result.window_end_height; height >= oldest && prices.size() < MINT_REFERENCE_SAMPLES; --height) { + if (!ancestor || ancestor->nHeight != height || !read_price) { + result.error = "DigiDollar volatility state not ready: required ancestor data is unavailable"; + return result; + } + CAmount price{0}; + const auto status = read_price(*ancestor, price, result.error); + if (status == AncestorPriceResult::UNAVAILABLE || (status == AncestorPriceResult::PRICE && price <= 0)) { + if (result.error.empty()) result.error = "DigiDollar volatility state not ready: required ancestor price is unavailable"; + return result; + } + if (status == AncestorPriceResult::PRICE) prices.push_back(price); + ancestor = ancestor->pprev; + } + } + result.sample_count = prices.size(); + if (!prices.empty()) { + std::sort(prices.begin(), prices.end()); + result.price_micro_usd = prices[(prices.size() - 1) / 2]; + } + result.ready = true; + return result; +} + +MintPriceStatus EvaluateMintPrice(CAmount candidate_price_micro_usd, const MintReference& reference) +{ + MintPriceStatus result; + result.quote_available = candidate_price_micro_usd > 0; + result.ready = reference.ready; + if (!result.quote_available) { + result.reason = "oracle_unavailable"; + } else if (!reference.ready || reference.rules_version != MINT_REFERENCE_RULE_VERSION) { + result.ready = false; + result.reason = "volatility_state_not_ready"; + } else if (reference.sample_count == 0) { + result.reason = "none"; + } else if (reference.price_micro_usd <= 0 || reference.sample_count > MINT_REFERENCE_SAMPLES) { + result.ready = false; + result.reason = "volatility_state_not_ready"; + } else { + // Positive int64 operands and these fixed multipliers fit in signed 128 bits. + // Widen before subtraction or multiplication; equality at 20% rejects. + const __int128 reference_price = reference.price_micro_usd; + const __int128 candidate_price = candidate_price_micro_usd; + const __int128 difference = candidate_price >= reference_price ? candidate_price - reference_price : reference_price - candidate_price; + const __int128 numerator = difference * 10000; + result.restricted = numerator >= reference_price * MINT_MAX_DEVIATION_BPS; + result.deviation_bps = SaturatingInt64(numerator / reference_price); + result.reason = result.restricted ? "volatility_pause" : "none"; + } + return result; +} + // ============================================================================ // Static Member Definitions // ============================================================================ @@ -124,7 +199,7 @@ void VolatilityMonitor::RecordPrice(CAmount price, int64_t timestamp, uint32_t h // Don't record if too recent (< 1 hour interval) if (timestamp - lastPrice.timestamp < MIN_PRICE_INTERVAL) { - LogPrintf("VolatilityMonitor: Skipping price update (too recent: %d seconds)\n", + LogPrint(BCLog::DIGIDOLLAR, "VolatilityMonitor: Skipping price update (too recent: %d seconds)\n", timestamp - lastPrice.timestamp); return; } @@ -134,7 +209,7 @@ void VolatilityMonitor::RecordPrice(CAmount price, int64_t timestamp, uint32_t h // Add new price point priceHistory.emplace_back(price, timestamp, height); - LogPrintf("VolatilityMonitor: Recorded price %s at height %d (timestamp %d)\n", + LogPrint(BCLog::DIGIDOLLAR, "VolatilityMonitor: Recorded price %s at height %d (timestamp %d)\n", FormatMoney(price), height, timestamp); // Clean old history to maintain size limits @@ -171,7 +246,7 @@ void VolatilityMonitor::ClearHistory() currentState = VolatilityState(); lastUpdateHeight = 0; - LogPrintf("VolatilityMonitor: History cleared\n"); + LogPrint(BCLog::DIGIDOLLAR, "VolatilityMonitor: History cleared\n"); } double VolatilityMonitor::CalculateVolatility(int64_t timeWindow) @@ -443,7 +518,7 @@ void VolatilityMonitor::RemovePriceForHeight(uint32_t height) UpdateVolatilityState(); } - LogPrintf("VolatilityMonitor: Removed price data for height %u, remaining=%d\n", + LogPrint(BCLog::DIGIDOLLAR, "VolatilityMonitor: Removed price data for height %u, remaining=%d\n", height, priceHistory.size()); } diff --git a/src/consensus/volatility.h b/src/consensus/volatility.h index 518ad4ae8d6..398a82ae9cd 100644 --- a/src/consensus/volatility.h +++ b/src/consensus/volatility.h @@ -12,13 +12,52 @@ #include #include #include +#include +#include // Forward declarations class COraclePriceMessage; +class CBlockIndex; +namespace Consensus { struct Params; } namespace DigiDollar { namespace Volatility { +/** The reference is bound to one candidate parent; it never stores an allow/reject answer. */ +static constexpr int MINT_REFERENCE_MIN_DEPTH{240}; +static constexpr int MINT_REFERENCE_MAX_DEPTH{1440}; +static constexpr size_t MINT_REFERENCE_SAMPLES{15}; +static constexpr int MINT_REFERENCE_RULE_VERSION{1}; +static constexpr int64_t MINT_MAX_DEVIATION_BPS{2000}; + +enum class AncestorPriceResult { NO_BUNDLE, PRICE, UNAVAILABLE }; +using AncestorPriceReader = std::function; + +struct MintReference { + bool ready{false}; + int candidate_height{0}; + uint256 parent_hash; + uint256 genesis_hash; + int rules_version{MINT_REFERENCE_RULE_VERSION}; + CAmount price_micro_usd{0}; + size_t sample_count{0}; + int window_start_height{0}; + int window_end_height{-1}; + std::string error; +}; + +struct MintPriceStatus { + bool ready{false}; + bool quote_available{false}; + bool restricted{false}; + int64_t deviation_bps{0}; //!< Display only; consensus compares the unrounded wide products. + std::string reason; +}; + +MintReference BuildMintReference(int candidate_height, const CBlockIndex* parent, + const Consensus::Params& params, const AncestorPriceReader& read_price); +MintPriceStatus EvaluateMintPrice(CAmount candidate_price_micro_usd, const MintReference& reference); + // ============================================================================ // Data Structures // ============================================================================ diff --git a/src/dbwrapper.cpp b/src/dbwrapper.cpp index e09f4b3d084..78a7f204f24 100644 --- a/src/dbwrapper.cpp +++ b/src/dbwrapper.cpp @@ -3,6 +3,7 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include +#include #include #include @@ -109,7 +110,7 @@ class CDigiByteLevelDBLogger : public leveldb::Logger { } }; -static void SetMaxOpenFiles(leveldb::Options *options) { +static void SetMaxOpenFiles(leveldb::Options *options, std::optional max_open_files) { // On most platforms the default setting of max_open_files (which is 1000) // is optimal. On Windows using a large file count is OK because the handles // do not interfere with select() loops. On 64-bit Unix hosts this value is @@ -125,16 +126,20 @@ static void SetMaxOpenFiles(leveldb::Options *options) { // See PR #12495 for further discussion. int default_open_files = options->max_open_files; + if (max_open_files) { + options->max_open_files = *max_open_files; + } else { #ifndef WIN32 - if (sizeof(void*) < 8) { - options->max_open_files = 64; - } + if (sizeof(void*) < 8) { + options->max_open_files = 64; + } #endif + } LogPrint(BCLog::LEVELDB, "LevelDB using max_open_files=%d (default=%d)\n", options->max_open_files, default_open_files); } -static leveldb::Options GetOptions(size_t nCacheSize) +static leveldb::Options GetOptions(size_t nCacheSize, std::optional max_open_files) { leveldb::Options options; options.block_cache = leveldb::NewLRUCache(nCacheSize / 2); @@ -147,7 +152,7 @@ static leveldb::Options GetOptions(size_t nCacheSize) // on corruption in later versions. options.paranoid_checks = true; } - SetMaxOpenFiles(&options); + SetMaxOpenFiles(&options, max_open_files); return options; } @@ -198,7 +203,7 @@ void CDBBatch::EraseImpl(Span key) struct LevelDBContext { //! custom environment this database is using (may be nullptr in case of default environment) - leveldb::Env* penv; + leveldb::Env* penv{nullptr}; //! database options used leveldb::Options options; @@ -216,9 +221,24 @@ struct LevelDBContext { leveldb::WriteOptions syncoptions; //! the database itself - leveldb::DB* pdb; + leveldb::DB* pdb{nullptr}; + + ~LevelDBContext() + { + // The database may still use its environment while it shuts down. + delete pdb; + delete options.filter_policy; + delete options.info_log; + delete options.block_cache; + delete penv; + } }; +const leveldb::Options& dbwrapper_private::GetOptions(const CDBWrapper& wrapper) +{ + return wrapper.DBContext().options; +} + CDBWrapper::CDBWrapper(const DBParams& params) : m_db_context{std::make_unique()}, m_name{fs::PathToString(params.path.stem())}, m_path{params.path}, m_is_memory{params.memory_only} { @@ -227,12 +247,16 @@ CDBWrapper::CDBWrapper(const DBParams& params) DBContext().iteroptions.verify_checksums = true; DBContext().iteroptions.fill_cache = false; DBContext().syncoptions.sync = true; - DBContext().options = GetOptions(params.cache_bytes); + DBContext().options = GetOptions(params.cache_bytes, params.max_open_files); DBContext().options.create_if_missing = true; if (params.memory_only) { DBContext().penv = leveldb::NewMemEnv(leveldb::Env::Default()); DBContext().options.env = DBContext().penv; } else { + if (!params.use_mmap) { + DBContext().penv = dbwrapper_private::MakeNoMmapEnv().release(); + DBContext().options.env = DBContext().penv; + } if (params.wipe_data) { LogPrintf("Wiping LevelDB in %s\n", fs::PathToString(params.path)); leveldb::Status result = leveldb::DestroyDB(fs::PathToString(params.path), DBContext().options); @@ -275,19 +299,7 @@ CDBWrapper::CDBWrapper(const DBParams& params) LogPrintf("Using obfuscation key for %s: %s\n", fs::PathToString(params.path), HexStr(obfuscate_key)); } -CDBWrapper::~CDBWrapper() -{ - delete DBContext().pdb; - DBContext().pdb = nullptr; - delete DBContext().options.filter_policy; - DBContext().options.filter_policy = nullptr; - delete DBContext().options.info_log; - DBContext().options.info_log = nullptr; - delete DBContext().options.block_cache; - DBContext().options.block_cache = nullptr; - delete DBContext().penv; - DBContext().options.env = nullptr; -} +CDBWrapper::~CDBWrapper() = default; bool CDBWrapper::WriteBatch(CDBBatch& batch, bool fSync) { @@ -415,6 +427,7 @@ Span CDBIterator::GetValueImpl() const CDBIterator::~CDBIterator() = default; bool CDBIterator::Valid() const { return m_impl_iter->iter->Valid(); } +void CDBIterator::CheckStatus() const { HandleError(m_impl_iter->iter->status()); } void CDBIterator::SeekToFirst() { m_impl_iter->iter->SeekToFirst(); } void CDBIterator::Next() { m_impl_iter->iter->Next(); } diff --git a/src/dbwrapper.h b/src/dbwrapper.h index eeb80e87c43..5945eac63ed 100644 --- a/src/dbwrapper.h +++ b/src/dbwrapper.h @@ -23,6 +23,11 @@ static const size_t DBWRAPPER_PREALLOC_KEY_SIZE = 64; static const size_t DBWRAPPER_PREALLOC_VALUE_SIZE = 1024; +//! Table cache target for buffered block-index and chainstate databases. +static constexpr int BUFFERED_DB_TABLE_CACHE_FILES{64}; +//! LevelDB subtracts ten non-table files from max_open_files for its table cache. +static constexpr int BUFFERED_DB_MAX_OPEN_FILES{BUFFERED_DB_TABLE_CACHE_FILES + 10}; + //! User-controlled performance and debug options. struct DBOptions { //! Compact database on startup. @@ -44,6 +49,10 @@ struct DBParams { bool obfuscate = false; //! Passed-through options. DBOptions options{}; + //! Allow the disk environment to map whole table files into memory. + bool use_mmap = true; + //! Override LevelDB's open-file cache target for this database only. + std::optional max_open_files{}; }; class dbwrapper_error : public std::runtime_error @@ -54,6 +63,10 @@ class dbwrapper_error : public std::runtime_error class CDBWrapper; +namespace leveldb { +struct Options; +} + /** These should be considered an implementation detail of the specific database. */ namespace dbwrapper_private { @@ -63,6 +76,7 @@ namespace dbwrapper_private { * specific database. */ const std::vector& GetObfuscateKey(const CDBWrapper &w); +const leveldb::Options& GetOptions(const CDBWrapper& wrapper); }; // namespace dbwrapper_private @@ -142,6 +156,8 @@ class CDBIterator ~CDBIterator(); bool Valid() const; + //! Throw on an iterator read error, including an error reported at end of scan. + void CheckStatus() const; void SeekToFirst(); @@ -154,6 +170,9 @@ class CDBIterator void Next(); + //! Size of the current serialized key in bytes. + size_t GetKeySize() const { return GetKeyImpl().size(); } + template bool GetKey(K& key) { try { DataStream ssKey{GetKeyImpl()}; @@ -181,6 +200,7 @@ struct LevelDBContext; class CDBWrapper { friend const std::vector& dbwrapper_private::GetObfuscateKey(const CDBWrapper &w); + friend const leveldb::Options& dbwrapper_private::GetOptions(const CDBWrapper& wrapper); private: //! holds all leveldb-specific fields of this class std::unique_ptr m_db_context; diff --git a/src/dbwrapper_env.cpp b/src/dbwrapper_env.cpp new file mode 100644 index 00000000000..57fe4e12bdc --- /dev/null +++ b/src/dbwrapper_env.cpp @@ -0,0 +1,177 @@ +// Copyright (c) 2026 The DigiByte Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#if defined(HAVE_CONFIG_H) +#include +#endif + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#ifdef WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#include +#include +#endif + +namespace { + +leveldb::Status FileError(const std::string& filename, int error) +{ +#ifdef WIN32 + const auto description = Win32ErrorString(error); + if (error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND) { + return leveldb::Status::NotFound(filename, description); + } +#else + const auto description = SysErrorString(error); + if (error == ENOENT || error == ENOTDIR) { + return leveldb::Status::NotFound(filename, description); + } +#endif + return leveldb::Status::IOError(filename, description); +} + +class NoMmapRandomAccessFile final : public leveldb::RandomAccessFile { +public: + explicit NoMmapRandomAccessFile(std::string filename) : m_filename(std::move(filename)) {} + + ~NoMmapRandomAccessFile() override + { +#ifdef WIN32 + if (m_handle != INVALID_HANDLE_VALUE) ::CloseHandle(m_handle); +#else + if (m_fd != -1) ::close(m_fd); +#endif + } + + leveldb::Status Open() + { +#ifdef WIN32 + const auto path = fs::PathFromString(m_filename); + m_handle = ::CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (m_handle == INVALID_HANDLE_VALUE) return FileError(m_filename, ::GetLastError()); +#else + int flags = O_RDONLY; +#ifdef O_CLOEXEC + flags |= O_CLOEXEC; +#endif + do { + m_fd = ::open(m_filename.c_str(), flags); + } while (m_fd == -1 && errno == EINTR); + if (m_fd == -1) return FileError(m_filename, errno); +#ifndef O_CLOEXEC + if (::fcntl(m_fd, F_SETFD, FD_CLOEXEC) == -1) return FileError(m_filename, errno); +#endif +#endif + return leveldb::Status::OK(); + } + + leveldb::Status Read(uint64_t offset, size_t n, leveldb::Slice* result, char* scratch) const override + { + *result = leveldb::Slice(scratch, 0); +#ifdef WIN32 + constexpr uint64_t max_offset = std::numeric_limits::max(); +#else + constexpr uint64_t max_offset = std::numeric_limits::max(); +#endif + if (offset > max_offset || n > max_offset - offset) { + return leveldb::Status::InvalidArgument(m_filename, "Read offset is out of range"); + } + if (n == 0) return leveldb::Status::OK(); + +#ifdef WIN32 + // Keep the seek and read together for a synchronous Windows handle. + const std::lock_guard lock(m_mutex); + LARGE_INTEGER position; + position.QuadPart = static_cast(offset); + if (!::SetFilePointerEx(m_handle, position, nullptr, FILE_BEGIN)) { + return FileError(m_filename, ::GetLastError()); + } +#endif + + size_t done{0}; + while (done < n) { +#ifdef WIN32 + const auto chunk = static_cast(std::min(n - done, std::numeric_limits::max())); + DWORD received{0}; + if (!::ReadFile(m_handle, scratch + done, chunk, &received, nullptr)) { + const auto error = ::GetLastError(); + *result = leveldb::Slice(scratch, done); + if (error == ERROR_HANDLE_EOF) break; + return FileError(m_filename, error); + } +#else + const size_t chunk = std::min(n - done, std::numeric_limits::max()); + const auto received = ::pread(m_fd, scratch + done, chunk, static_cast(offset + done)); + if (received < 0) { + if (errno == EINTR) continue; + const int error = errno; + *result = leveldb::Slice(scratch, done); + return FileError(m_filename, error); + } +#endif + if (received == 0) break; + done += static_cast(received); + } + // EOF is a short read. LevelDB checks block length and checksums itself. + *result = leveldb::Slice(scratch, done); + return leveldb::Status::OK(); + } + + std::string GetName() const override { return m_filename; } + +private: + const std::string m_filename; +#ifdef WIN32 + HANDLE m_handle{INVALID_HANDLE_VALUE}; + mutable std::mutex m_mutex; +#else + int m_fd{-1}; +#endif +}; + +class NoMmapEnv final : public leveldb::EnvWrapper { +public: + NoMmapEnv() : EnvWrapper(leveldb::Env::Default()) {} + + leveldb::Status NewRandomAccessFile(const std::string& filename, leveldb::RandomAccessFile** result) override + { + *result = nullptr; + auto file = std::make_unique(filename); + const auto status = file->Open(); + if (status.ok()) *result = file.release(); + return status; + } +}; + +} // namespace + +namespace dbwrapper_private { + +std::unique_ptr MakeNoMmapEnv() +{ + return std::make_unique(); +} + +} // namespace dbwrapper_private diff --git a/src/dbwrapper_env.h b/src/dbwrapper_env.h new file mode 100644 index 00000000000..16543832aa6 --- /dev/null +++ b/src/dbwrapper_env.h @@ -0,0 +1,24 @@ +// Copyright (c) 2026 The DigiByte Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef DIGIBYTE_DBWRAPPER_ENV_H +#define DIGIBYTE_DBWRAPPER_ENV_H + +#include + +namespace leveldb { +class Env; +} + +namespace dbwrapper_private { + +/** Create a disk environment whose random reads use caller buffers, never mmap. + * Other operations use LevelDB's default environment. Destroy the environment + * after its database. Each open random-access file owns its underlying handle. + */ +std::unique_ptr MakeNoMmapEnv(); + +} // namespace dbwrapper_private + +#endif // DIGIBYTE_DBWRAPPER_ENV_H diff --git a/src/digidollar/amount.cpp b/src/digidollar/amount.cpp new file mode 100644 index 00000000000..d74c2bf54b4 --- /dev/null +++ b/src/digidollar/amount.cpp @@ -0,0 +1,177 @@ +// Copyright (c) 2026 The DigiByte Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#include + +#include +#include +#include + +namespace DigiDollar { + +namespace { + +bool IsAsciiDigit(char c) +{ + return c >= '0' && c <= '9'; +} + +// value = value * 10 + digit, refused instead of overflowing or exceeding max. +bool AppendDigitChecked(CAmount& value, int digit, CAmount max) +{ + if (digit > max) return false; // keeps (max - digit) non-negative below + if (value > (max - digit) / 10) return false; + value = value * 10 + digit; + return true; +} + +DDAmountParseResult Refuse(DDAmountError error, std::string message) +{ + DDAmountParseResult result; + result.error = error; + result.message = std::move(message); + return result; +} + +} // namespace + +std::optional ParseDDAmountUnit(std::string_view name) +{ + if (name == "cents") return DDAmountUnit::CENTS; + if (name == "dollars") return DDAmountUnit::DOLLARS; + return std::nullopt; +} + +std::string DDAmountUnitName(DDAmountUnit unit) +{ + switch (unit) { + case DDAmountUnit::CENTS: return "cents"; + case DDAmountUnit::DOLLARS: return "dollars"; + case DDAmountUnit::UNSPECIFIED: break; + } + return "unspecified"; +} + +DDAmountParseResult ParseDDAmount(std::string_view text, DDAmountUnit unit, CAmount max_cents) +{ + if (max_cents < 0) max_cents = 0; + + if (text.empty()) { + return Refuse(DDAmountError::NOT_A_NUMBER, "amount is not a plain decimal number (it is empty)"); + } + if (text.front() == '-') { + return Refuse(DDAmountError::NEGATIVE, "amount must not be negative"); + } + + // Syntax first: [.] and nothing else. Only after the + // whole text is known to be a plain decimal number do the unit rules and + // the magnitude get a say, so a caller always sees the most basic + // problem first. + std::string_view integer_part; + std::string_view fraction_part; + { + size_t pos = 0; + while (pos < text.size() && IsAsciiDigit(text[pos])) ++pos; + integer_part = text.substr(0, pos); + if (integer_part.empty()) { + return Refuse(DDAmountError::NOT_A_NUMBER, + "amount is not a plain decimal number (digits, optionally followed by a decimal point and more digits)"); + } + if (integer_part.size() > 1 && integer_part.front() == '0') { + return Refuse(DDAmountError::NOT_A_NUMBER, "amount must not have leading zeros"); + } + if (pos < text.size()) { + if (text[pos] != '.') { + return Refuse(DDAmountError::NOT_A_NUMBER, + "amount is not a plain decimal number (no sign, spaces, exponent, or separators are accepted)"); + } + ++pos; + const size_t fraction_start = pos; + while (pos < text.size() && IsAsciiDigit(text[pos])) ++pos; + fraction_part = text.substr(fraction_start, pos - fraction_start); + if (fraction_part.empty()) { + return Refuse(DDAmountError::NOT_A_NUMBER, "amount has a decimal point with no digits after it"); + } + if (pos != text.size()) { + return Refuse(DDAmountError::NOT_A_NUMBER, + "amount is not a plain decimal number (no sign, spaces, exponent, or separators are accepted)"); + } + } + } + const bool has_point = !fraction_part.empty(); + + // Unit rules. A decimal point is only ever read as dollars when the + // caller said "dollars"; it is never inferred from the shape of the text. + switch (unit) { + case DDAmountUnit::UNSPECIFIED: + if (has_point) return Refuse(DDAmountError::AMBIGUOUS_UNIT, DD_AMOUNT_AMBIGUOUS_MESSAGE); + break; + case DDAmountUnit::CENTS: + if (has_point) { + return Refuse(DDAmountError::CENTS_NOT_INTEGRAL, + "amount_unit=cents requires an integer number of cents (no decimal point)"); + } + break; + case DDAmountUnit::DOLLARS: + if (fraction_part.size() > 2) { + return Refuse(DDAmountError::TOO_MANY_DECIMALS, + "amount_unit=dollars allows at most two decimal places (whole cents)"); + } + break; + } + + // Magnitude, in cents, with every step checked against max_cents. For + // dollars the integer digits are worth 100 cents each, so they are + // accumulated against max_cents / 100 and then scaled once. + const std::string too_large = strprintf("amount exceeds the maximum of %d cents ($%s)", + max_cents, FormatDDAmountDollars(max_cents)); + CAmount cents = 0; + if (unit == DDAmountUnit::DOLLARS) { + const CAmount max_whole_dollars = max_cents / 100; + CAmount whole = 0; + for (const char c : integer_part) { + if (!AppendDigitChecked(whole, c - '0', max_whole_dollars)) { + return Refuse(DDAmountError::TOO_LARGE, too_large); + } + } + CAmount fraction_cents = 0; + if (fraction_part.size() >= 1) fraction_cents += (fraction_part[0] - '0') * 10; + if (fraction_part.size() >= 2) fraction_cents += (fraction_part[1] - '0'); + // whole <= max_cents / 100, so whole * 100 <= max_cents and cannot overflow. + cents = whole * 100; + if (fraction_cents > max_cents - cents) { + return Refuse(DDAmountError::TOO_LARGE, too_large); + } + cents += fraction_cents; + } else { + for (const char c : integer_part) { + if (!AppendDigitChecked(cents, c - '0', max_cents)) { + return Refuse(DDAmountError::TOO_LARGE, too_large); + } + } + } + + DDAmountParseResult result; + result.cents = cents; + return result; +} + +std::string FormatDDAmountDollars(CAmount cents) +{ + // Work on the magnitude as unsigned so the most negative value cannot + // overflow when negated. + const bool negative = cents < 0; + const uint64_t magnitude = negative ? (uint64_t{0} - static_cast(cents)) + : static_cast(cents); + return strprintf("%s%d.%02d", negative ? "-" : "", magnitude / 100, magnitude % 100); +} + +std::string FormatDDAmountCents(CAmount cents) +{ + return strprintf("%d", cents); +} + +} // namespace DigiDollar diff --git a/src/digidollar/amount.h b/src/digidollar/amount.h new file mode 100644 index 00000000000..1500b0fca02 --- /dev/null +++ b/src/digidollar/amount.h @@ -0,0 +1,105 @@ +// Copyright (c) 2026 The DigiByte Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef DIGIBYTE_DIGIDOLLAR_AMOUNT_H +#define DIGIBYTE_DIGIDOLLAR_AMOUNT_H + +#include + +#include +#include +#include +#include + +namespace DigiDollar { + +// DigiDollar amounts are integer cents everywhere inside the node. RPC +// callers, however, type amounts as text, and the same text can mean two very +// different things: "10000" is 10,000 cents ($100.00) while "10000.00" written +// by habit means $10,000.00 if it is read as dollars. This unit is the single +// place that turns caller text into cents. It never goes through floating +// point, it never guesses the unit from the shape of the number, and every +// refusal carries a plain-English reason the caller can act on. + +/** How the caller said an amount should be read. */ +enum class DDAmountUnit { + UNSPECIFIED, // No unit given: only a plain integer (cents) is accepted. + CENTS, // An integer number of cents: "5000" is $50.00. + DOLLARS, // Dollars with at most two decimals: "50" or "50.00" is $50.00. +}; + +/** Why an amount text was refused. */ +enum class DDAmountError { + NONE, + NOT_A_NUMBER, // Empty, sign, whitespace, exponent, letters, malformed decimal. + AMBIGUOUS_UNIT, // A decimal point with no unit: dollars, or a typo for cents? + CENTS_NOT_INTEGRAL, // amount_unit=cents together with a decimal point. + TOO_MANY_DECIMALS, // amount_unit=dollars with more than two decimal places. + NEGATIVE, // A leading minus sign. + TOO_LARGE, // Above the maximum the caller allows. +}; + +struct DDAmountParseResult { + DDAmountError error{DDAmountError::NONE}; + CAmount cents{0}; // Meaningful only when error == NONE. + std::string message; // Plain-English reason when error != NONE. + + bool ok() const { return error == DDAmountError::NONE; } +}; + +/** + * The largest DigiDollar amount a single senddigidollar request, a single + * sendmanydigidollar recipient, or a redeemdigidollar principal may name: + * 10,000,000 cents = $100,000.00. It equals the per-output transfer + * consensus limit and the mainnet maximum mint, so no legitimate send or full + * vault redemption is ever blocked by it. It exists to stop a typo from + * moving far more than intended. It bounds only what the caller types: the + * emergency-redemption burn a wallet computes from a vault is not a typed + * amount and is never capped by it. + */ +inline constexpr CAmount MAX_DD_RPC_AMOUNT_CENTS{10'000'000}; + +/** Pass as max_cents to accept any amount that fits in a CAmount. */ +inline constexpr CAmount DD_AMOUNT_NO_CAP{std::numeric_limits::max()}; + +/** The exact text of the refusal for a decimal amount given without a unit. */ +inline constexpr const char* DD_AMOUNT_AMBIGUOUS_MESSAGE{ + "ambiguous amount: pass amount_unit=cents or amount_unit=dollars " + "(an amount with a decimal point is not accepted without a unit)"}; + +/** Parse an amount_unit argument: exactly "cents" or "dollars". */ +std::optional ParseDDAmountUnit(std::string_view name); + +/** "unspecified", "cents" or "dollars". */ +std::string DDAmountUnitName(DDAmountUnit unit); + +/** + * Turn caller text into cents under the given unit contract. + * + * Accepted syntax is a plain decimal number: ASCII digits, an optional single + * '.' followed by at least one digit, no sign, no whitespace, no exponent, no + * thousands separators, and no leading zeros other than a lone "0" before the + * point. Anything else is NOT_A_NUMBER; a leading '-' is NEGATIVE. + * + * - UNSPECIFIED: an integer is cents (the behaviour every existing caller + * relies on); a decimal point is AMBIGUOUS_UNIT and is never guessed. + * - CENTS: an integer is cents; a decimal point is CENTS_NOT_INTEGRAL. + * - DOLLARS: at most two decimals; "12.3" is 1230 cents, "12.345" is + * TOO_MANY_DECIMALS. + * + * The value is accumulated with checked integer arithmetic and refused as + * TOO_LARGE as soon as it would exceed max_cents, so no intermediate can + * overflow whatever the text length. A result is always in [0, max_cents]. + */ +DDAmountParseResult ParseDDAmount(std::string_view text, DDAmountUnit unit, CAmount max_cents); + +/** 5000 -> "50.00", 5 -> "0.05". Parsing the result as dollars gives cents back. */ +std::string FormatDDAmountDollars(CAmount cents); + +/** 5000 -> "5000". Parsing the result as cents (or with no unit) gives cents back. */ +std::string FormatDDAmountCents(CAmount cents); + +} // namespace DigiDollar + +#endif // DIGIBYTE_DIGIDOLLAR_AMOUNT_H diff --git a/src/digidollar/digidollar.cpp b/src/digidollar/digidollar.cpp index 0588f3e9772..0bd93f04e90 100644 --- a/src/digidollar/digidollar.cpp +++ b/src/digidollar/digidollar.cpp @@ -8,6 +8,7 @@ #include #include +#include #include // ===================================== @@ -79,32 +80,36 @@ CCollateralPosition::CCollateralPosition(const COutPoint& outpointIn, CAmount dg CAmount CCollateralPosition::GetCurrentCollateralRatio(CAmount currentPrice) const { - // Handle edge case: no DD minted - if (ddMinted == 0) { - return 0; // Could return MAX value instead - } - - // Calculate: (dgbLocked * currentPrice * 100) / ddMinted - // dgbLocked is in satoshis, currentPrice is in cents (100 = $1.00 DGB price) - // ddMinted is in cents (100 = $1.00 USD) - // Result is percentage * 100 (e.g., 200 for 200%) - - // Calculate DGB value in cents: (satoshis * price_cents) / COIN - CAmount dgbValueCents = (dgbLocked * currentPrice) / COIN; + // dgbLocked is in satoshis. currentPrice is cents per DGB, so 100 means + // $1.00 per DGB. ddMinted is in cents. The answer is a percentage, so 200 + // means the locked DGB is worth twice the DigiDollar minted against it. - // Avoid division by zero - if (dgbValueCents > 0 && ddMinted > 0) { - // Check for potential overflow - if (dgbValueCents > (std::numeric_limits::max() / 100)) { - // Handle overflow case - return a very large ratio - return std::numeric_limits::max(); - } + // With nothing minted there is no ratio to report. A negative amount or a + // negative price can only come from damaged data, and there is no honest + // ratio for those either, so report nothing rather than a made-up number. + if (ddMinted <= 0 || dgbLocked <= 0 || currentPrice <= 0) { + return 0; + } - CAmount ratio = (dgbValueCents * 100) / ddMinted; - return ratio; + // Work out what the locked DGB is worth, then the ratio. Both steps are + // done in a 128-bit type on purpose. A large position at a high price does + // not fit in a 64-bit money amount, and in 64 bits the multiply would wrap + // round and report a small or negative ratio for a position that is in + // fact hugely over-collateralised. + const __int128 valueInCents = (static_cast<__int128>(dgbLocked) * currentPrice) / COIN; + if (valueInCents <= 0) { + return 0; } - return 0; + // When the true ratio is larger than a money amount can hold, report the + // largest amount. That is a ceiling, not the exact figure, and it is the + // closest true statement available: the position is over-collateralised + // beyond anything this type can express. + const __int128 ratio = (valueInCents * 100) / ddMinted; + if (ratio > std::numeric_limits::max()) { + return std::numeric_limits::max(); + } + return static_cast(ratio); } bool CCollateralPosition::IsHealthy(CAmount currentPrice) const @@ -187,4 +192,28 @@ bool IsDigiDollarEnabled(const CBlockIndex* pindexPrev, const Consensus::Params& params.DeploymentHeight(Consensus::DEPLOYMENT_DIGIDOLLAR); } +bool IsThawDayScheduled(const Consensus::Params& params) +{ + return params.nDDThawDayHeight != std::numeric_limits::max(); +} + +bool IsThawDayActive(const Consensus::Params& params, int candidate_height) +{ + // Only ever compare against the configured height; never add to it, + // because the "not scheduled" value is the largest int and would overflow. + if (candidate_height < 0) return false; + if (!IsThawDayScheduled(params)) return false; + // A network with the DigiDollar deployment switched off can never have + // Thaw Day rules, at any height. Checking this explicitly keeps the + // predicate false even at the largest int height, where a bare height + // compare against the "disabled" value (also the largest int) would + // otherwise read as active. + if (!DeploymentEnabled(params, Consensus::DEPLOYMENT_DIGIDOLLAR)) return false; + // The Thaw Day rules are DigiDollar rules, so they cannot apply to a block + // where DigiDollar itself is not yet active. This is the same buried + // height that IsDigiDollarEnabled() compares against. + if (candidate_height < params.DeploymentHeight(Consensus::DEPLOYMENT_DIGIDOLLAR)) return false; + return candidate_height >= params.nDDThawDayHeight; +} + } // namespace DigiDollar \ No newline at end of file diff --git a/src/digidollar/digidollar.h b/src/digidollar/digidollar.h index 8e3c9af13e0..ae6eecee809 100644 --- a/src/digidollar/digidollar.h +++ b/src/digidollar/digidollar.h @@ -98,6 +98,10 @@ class CCollateralPosition } //! System health tracking + //! Returns the collateral ratio as a percentage: 200 means the locked DGB + //! is worth twice the DigiDollar minted against it. Returns 0 when there is + //! nothing to compare, and the largest money amount when the true ratio is + //! too big to fit in one. Never reports a wrapped-round number. CAmount GetCurrentCollateralRatio(CAmount currentPrice) const; bool IsHealthy(CAmount currentPrice) const; CAmount GetRequiredDDForRedemption(int systemCollateral) const; @@ -125,6 +129,25 @@ namespace DigiDollar { */ bool IsDigiDollarEnabled(const CBlockIndex* pindexPrev, const ChainstateManager& chainman); bool IsDigiDollarEnabled(const CBlockIndex* pindexPrev, const Consensus::Params& params); + + /** + * Is Thaw Day scheduled on this network at all? False while + * Consensus::Params::nDDThawDayHeight holds its "not scheduled" value. + */ + bool IsThawDayScheduled(const Consensus::Params& params); + + /** + * Do the Thaw Day rules apply to a block at this height? True only when + * Thaw Day is scheduled, DigiDollar is active at that height + * (candidate_height >= params.DigiDollarHeight), and + * candidate_height >= params.nDDThawDayHeight. + * + * Callers pass the height of the block being checked (validation, + * replay), the intended height (mining: parent height + 1), or the next + * block height on the active chain (mempool, wallet). A negative height + * is never active. Pure: no chain state, no globals. + */ + bool IsThawDayActive(const Consensus::Params& params, int candidate_height); } #endif // DIGIBYTE_DIGIDOLLAR_DIGIDOLLAR_H \ No newline at end of file diff --git a/src/digidollar/health.cpp b/src/digidollar/health.cpp index 2c221bd2d3e..a60467cd6bb 100644 --- a/src/digidollar/health.cpp +++ b/src/digidollar/health.cpp @@ -26,6 +26,7 @@ #include #include +#include #include #include #include @@ -41,6 +42,295 @@ namespace DigiDollar { +namespace { +bool IsCanonicalTaproot(const CScript& script) +{ + int version; + std::vector program; + return script.IsWitnessProgram(version, program) && version == 1 && program.size() == 32; +} + +bool CoinMayContainDigiDollar(const Coin& coin, const Consensus::Params& params) +{ + return !coin.IsSpent() && !coin.IsCoinBase() && params.DigiDollarHeight != std::numeric_limits::max() && + coin.nHeight >= static_cast(params.DigiDollarHeight) && + IsCanonicalTaproot(coin.out.scriptPubKey); +} +} // namespace + +VaultLookupResult LookupCanonicalVault(const COutPoint& outpoint, const Coin& coin, + const Consensus::Params& params, + const CanonicalTxLookup& lookup, + CanonicalVault& vault, std::string& error) +{ + vault = {}; + if (!CoinMayContainDigiDollar(coin, params) || coin.out.nValue <= 0) return VaultLookupResult::NOT_VAULT; + CTransactionRef creating; + if (!lookup || !lookup(outpoint.hash, coin.nHeight, creating) || !creating || + creating->GetHash() != outpoint.hash || outpoint.n >= creating->vout.size() || + creating->vout[outpoint.n] != coin.out) { + error = strprintf("DigiDollar state not ready: restore the creating block at height %u for %s:%u", + coin.nHeight, outpoint.hash.ToString(), outpoint.n); + return VaultLookupResult::NOT_READY; + } + if (!HasDigiDollarMarker(*creating) || creating->IsCoinBase() || DigiDollar::GetDigiDollarTxType(*creating) != DD_TX_MINT) + return VaultLookupResult::NOT_VAULT; + CAmount principal{0}, collateral{0}; + if (!ExtractMintAccountingAmounts(*creating, principal, collateral, false)) { + // The creating block is present and readable; the mint inside it simply + // does not record an amount and a single collateral output that can be + // read back. Every node reading that same block reaches the same answer, + // so this output is not a collateral vault and is left out of the totals. + // That is different from the node having lost the block, which is the + // readiness failure above: reporting this as missing data would stop + // every node at the Thaw Day height for ever, asking the operator to + // restore a block the node already has. + LogPrintf("DigiDollar: mint %s does not record a readable vault amount, so output %s:%u " + "at height %u is not counted as collateral\n", + creating->GetHash().ToString(), outpoint.hash.ToString(), outpoint.n, coin.nHeight); + return VaultLookupResult::NOT_VAULT; + } + // ExtractMintAccountingAmounts requires exactly one positive Taproot output, + // and the outpoint checked above is that output, so the two amounts agree. + // The check stays as a guard. If it ever fired the creating block would still + // be present and readable, so the answer has to be the same on every node: + // not a vault, rather than a halt. + if (collateral != coin.out.nValue) { + LogPrintf("DigiDollar: mint %s records collateral %d but its output %s:%u at height %u holds %d, " + "so it is not counted as collateral\n", + creating->GetHash().ToString(), collateral, outpoint.hash.ToString(), outpoint.n, + coin.nHeight, coin.out.nValue); + return VaultLookupResult::NOT_VAULT; + } + vault = {outpoint, principal, collateral}; + return VaultLookupResult::VAULT; +} + +std::optional CalculateChainstateHealth(const ChainstateHealth& state, CAmount price_micro_usd) +{ + if (!state.IsValid() || price_micro_usd <= 0) return std::nullopt; + return DCA::DynamicCollateralAdjustment::CalculateSystemHealth( + state.collateral, state.open_vault_principal, price_micro_usd / 10); +} + +namespace { +// Counts the full walks of the coin set. See ChainstateHealthRebuildCount(). +std::atomic g_chainstate_health_rebuilds{0}; +} // namespace + +uint64_t ChainstateHealthRebuildCount() { return g_chainstate_health_rebuilds.load(std::memory_order_relaxed); } + +bool ReconstructChainstateHealth(const CCoinsView& view, const Consensus::Params& params, + const CanonicalTxLookup& lookup, + ChainstateHealth& health, std::string& error, + const std::function& interrupted, + CAmount* circulating_supply, + const std::function& progress, + bool* circulating_supply_known) +{ + g_chainstate_health_rebuilds.fetch_add(1, std::memory_order_relaxed); + try { + ChainstateHealth rebuilt; + rebuilt.genesis_hash = params.hashGenesisBlock; + rebuilt.activation_height = params.nDDThawDayHeight; + rebuilt.digidollar_height = params.DigiDollarHeight; + rebuilt.best_block = view.GetBestBlock(); + auto cursor = view.Cursor(); + if (!cursor || cursor->GetBestBlock() != rebuilt.best_block || rebuilt.best_block.IsNull()) { + error = "DigiDollar state not ready: a consistent UTXO view is required"; + return false; + } + CAmount supply{0}; + bool supply_known{true}; + uint64_t scanned{0}; + auto last_progress = SteadyClock::now(); + if (progress) progress(0, 0); + for (; cursor->Valid(); cursor->Next()) { + if (interrupted && interrupted()) { + error = "DigiDollar accounting reconstruction cancelled"; + return false; + } + COutPoint outpoint; + Coin coin; + if (!cursor->GetKey(outpoint) || !cursor->GetValue(coin)) { + error = "DigiDollar state not ready: cannot read the UTXO snapshot"; + return false; + } + CanonicalVault vault; + const auto result = LookupCanonicalVault(outpoint, coin, params, lookup, vault, error); + if (result == VaultLookupResult::NOT_READY) return false; + if (result == VaultLookupResult::VAULT && !rebuilt.AddVault(vault.principal, vault.collateral)) { + error = "DigiDollar state not ready: reconstructed vault totals exceed the supported range"; + return false; + } + if (circulating_supply && CoinMayContainDigiDollar(coin, params) && coin.out.nValue == 0) { + CTransactionRef creating; + if (!lookup || !lookup(outpoint.hash, coin.nHeight, creating) || !creating || + creating->GetHash() != outpoint.hash || outpoint.n >= creating->vout.size() || + creating->vout[outpoint.n] != coin.out) { + error = strprintf("DigiDollar supply not ready: restore the creating block at height %u", coin.nHeight); + return false; + } + if (HasDigiDollarMarker(*creating)) { + CAmount amount{0}; + if (!ExtractDDAmountFromBlockDb(outpoint, coin.nHeight, lookup, amount) || amount <= 0) { + if (!circulating_supply_known) { + error = "DigiDollar supply not ready: invalid creating token metadata"; + return false; + } + supply_known = false; + } else if (amount > std::numeric_limits::max() - supply) { + error = "DigiDollar supply not ready: token total exceeds the supported range"; + return false; + } else { + supply += amount; + } + } + } + if (++scanned % 4096 == 0 && SteadyClock::now() - last_progress >= std::chrono::seconds{1}) { + last_progress = SteadyClock::now(); + if (progress) progress(scanned, 0); + else LogPrintf("DigiDollar accounting reconstruction: %u outputs checked, %u open vaults\n", scanned, rebuilt.active_vaults); + } + } + cursor->CheckStatus(); + if (view.GetBestBlock() != rebuilt.best_block || !rebuilt.IsValid()) { + error = "DigiDollar state not ready: UTXO state changed during reconstruction"; + return false; + } + if (progress) progress(scanned, scanned); + if (interrupted && interrupted()) { + error = "DigiDollar accounting reconstruction cancelled"; + return false; + } + health = rebuilt; + if (circulating_supply && supply_known) *circulating_supply = supply; + if (circulating_supply_known) *circulating_supply_known = supply_known; + return true; + } catch (const dbwrapper_error& exception) { + error = std::string("DigiDollar state not ready: restore unreadable chainstate data: ") + exception.what(); + return false; + } +} + +bool UpdateChainstateHealth(const CTransaction& tx, const std::vector& inputs, + const Consensus::Params& params, const CanonicalTxLookup& lookup, + ChainstateHealth& health, bool undo, std::string& error, uint32_t creating_height) +{ + if (tx.IsCoinBase()) return true; + if (inputs.size() != tx.vin.size()) { + error = "DigiDollar accounting requires complete input coins"; + return false; + } + auto next = health; + // Reverse output creation before input restoration, matching UTXO undo. + auto mint = [&]() { + if (params.DigiDollarHeight == std::numeric_limits::max() || + creating_height < static_cast(params.DigiDollarHeight)) return true; + if (!HasDigiDollarMarker(tx) || DigiDollar::GetDigiDollarTxType(tx) != DD_TX_MINT) return true; + CAmount principal{0}, collateral{0}; + if (!ExtractMintAccountingAmounts(tx, principal, collateral, false)) return false; + return undo ? next.RemoveVault(principal, collateral) : next.AddVault(principal, collateral); + }; + if (undo && !mint()) { + error = "DigiDollar accounting cannot undo minted vault"; + return false; + } + for (size_t i = 0; i < inputs.size(); ++i) { + CanonicalVault vault; + const auto result = LookupCanonicalVault(tx.vin[i].prevout, inputs[i], params, lookup, vault, error); + if (result == VaultLookupResult::NOT_READY) return false; + if (result == VaultLookupResult::VAULT && + !(undo ? next.AddVault(vault.principal, vault.collateral) : next.RemoveVault(vault.principal, vault.collateral))) { + error = "DigiDollar accounting cannot apply vault spend"; + return false; + } + } + if (!undo && !mint()) { + error = "DigiDollar accounting cannot add minted vault"; + return false; + } + health = next; + return true; +} + +SupplyChangeResult GetCirculatingSupplyChange(const CTransaction& tx, const std::vector& inputs, + uint32_t height, const Consensus::Params& params, + const CanonicalTxLookup& lookup, CAmount& change, std::string& error) +{ + if (tx.IsCoinBase()) { change = 0; return SupplyChangeResult::KNOWN; } + if (inputs.size() != tx.vin.size()) { + error = "DigiDollar supply requires complete input coins"; + return SupplyChangeResult::FAILURE; + } + auto token_amount = [&](const COutPoint& outpoint, const Coin& coin, const CanonicalTxLookup& source, + CAmount& amount) { + if (!CoinMayContainDigiDollar(coin, params) || coin.out.nValue != 0) { + amount = 0; + return SupplyChangeResult::KNOWN; + } + CTransactionRef creating; + if (!source || !source(outpoint.hash, coin.nHeight, creating) || !creating || + creating->GetHash() != outpoint.hash || outpoint.n >= creating->vout.size() || + creating->vout[outpoint.n] != coin.out) { + error = strprintf("DigiDollar supply not ready: restore the creating block at height %u", coin.nHeight); + return SupplyChangeResult::FAILURE; + } + if (!HasDigiDollarMarker(*creating)) { + amount = 0; + return SupplyChangeResult::KNOWN; + } + const CanonicalTxLookup verified_source = [&](const uint256& hash, uint32_t height, CTransactionRef& out) { + if (hash != outpoint.hash || height != coin.nHeight) return false; + out = creating; + return true; + }; + if (!ExtractDDAmountFromBlockDb(outpoint, coin.nHeight, verified_source, amount) || amount <= 0) { + return SupplyChangeResult::UNKNOWN_METADATA; + } + return SupplyChangeResult::KNOWN; + }; + __int128 delta{0}; + bool unknown_metadata{false}; + for (size_t i = 0; i < inputs.size(); ++i) { + CAmount amount{0}; + const auto result = token_amount(tx.vin[i].prevout, inputs[i], lookup, amount); + if (result == SupplyChangeResult::FAILURE) return result; + if (result == SupplyChangeResult::UNKNOWN_METADATA) unknown_metadata = true; + else delta -= amount; + } + const auto created = MakeTransactionRef(tx); + const CanonicalTxLookup created_lookup = [&](const uint256& txid, uint32_t coin_height, CTransactionRef& out) { + if (txid != tx.GetHash() || coin_height != height) return false; + out = created; + return true; + }; + for (uint32_t i = 0; i < tx.vout.size(); ++i) { + CAmount amount{0}; + const auto result = token_amount(COutPoint{tx.GetHash(), i}, Coin{tx.vout[i], static_cast(height), false}, created_lookup, amount); + if (result == SupplyChangeResult::FAILURE) return result; + if (result == SupplyChangeResult::UNKNOWN_METADATA) unknown_metadata = true; + else delta += amount; + } + if (unknown_metadata) { + error = "DigiDollar supply unavailable: creating token metadata does not establish an unambiguous amount"; + return SupplyChangeResult::UNKNOWN_METADATA; + } + if (delta < std::numeric_limits::min() || delta > std::numeric_limits::max()) { + error = "DigiDollar circulating supply change exceeds the supported range"; + return SupplyChangeResult::FAILURE; + } + change = static_cast(delta); + return SupplyChangeResult::KNOWN; +} + +bool CalculateCirculatingSupplyChange(const CTransaction& tx, const std::vector& inputs, + uint32_t height, const Consensus::Params& params, + const CanonicalTxLookup& lookup, CAmount& change, std::string& error) +{ + return GetCirculatingSupplyChange(tx, inputs, height, params, lookup, change, error) == SupplyChangeResult::KNOWN; +} + // Static member definitions SystemMetrics SystemHealthMonitor::s_currentMetrics; std::mutex SystemHealthMonitor::s_metricsMutex; @@ -305,6 +595,14 @@ void SystemHealthMonitor::Shutdown() } bool SystemHealthMonitor::ScanUTXOSet(CCoinsView* view, CCoinsView* validation_view, const node::BlockManager* blockman, const CTxMemPool* mempool, const CChain* chain, const Consensus::Params* consensus) +{ + return ScanUTXOSet(view, validation_view, blockman, mempool, chain, consensus, {}).status == HealthScanResult::Status::COMPLETE; +} + +HealthScanResult SystemHealthMonitor::ScanUTXOSet(CCoinsView* view, CCoinsView* validation_view, + const node::BlockManager* blockman, const CTxMemPool* mempool, + const CChain* chain, const Consensus::Params* consensus, + const HealthScanCallbacks& callbacks) { // DigiDollar activation floor: a DD vault output can only be created at/after // activation, which cannot happen below the deployment's minimum activation height. @@ -313,42 +611,63 @@ bool SystemHealthMonitor::ScanUTXOSet(CCoinsView* view, CCoinsView* validation_v // blocks are gone) and a full node (pre-floor blocks are present but hold no vaults). const int dd_floor = consensus ? DigiDollar::EarliestActivationFloor(*consensus) : 0; - // Reset counters - s_currentMetrics.totalDDSupply = 0; - s_currentMetrics.totalCollateral = 0; - s_currentMetrics.totalActivePositions = 0; - s_currentMetrics.systemHealth = 0; - s_currentMetrics.hasCanonicalHealth = false; - - // Reset tier counters - for (auto& tier : s_currentMetrics.tiers) { - tier.ddMinted = 0; - tier.dgbLocked = 0; - tier.positions = 0; - tier.healthRatio = 0; - } + // Keep a complete snapshot available until the replacement scan succeeds. + auto rebuilt = GetCachedMetrics(); + rebuilt.totalDDSupply = 0; + rebuilt.totalCollateral = 0; + rebuilt.totalActivePositions = 0; + rebuilt.systemHealth = 0; + rebuilt.hasCanonicalHealth = false; + + uint64_t outputs_checked{0}; + auto last_progress = SteadyClock::now(); + if (callbacks.progress) callbacks.progress(0, 0); + const auto cancelled = [&] { return callbacks.cancelled && callbacks.cancelled(); }; + const auto cancellation = [] { + LogPrintf("DigiDollar legacy health reconstruction cancelled; cached metrics were not replaced\n"); + return HealthScanResult{HealthScanResult::Status::CANCELLED, "DigiDollar legacy health reconstruction cancelled"}; + }; + const auto publish = [&]() -> HealthScanResult { + if (callbacks.progress) callbacks.progress(outputs_checked, outputs_checked); + if (cancelled()) return cancellation(); + std::lock_guard lock(s_metricsMutex); + s_currentMetrics.totalDDSupply = rebuilt.totalDDSupply; + s_currentMetrics.totalCollateral = rebuilt.totalCollateral; + s_currentMetrics.totalActivePositions = rebuilt.totalActivePositions; + s_currentMetrics.systemHealth = rebuilt.systemHealth; + s_currentMetrics.hasCanonicalHealth = rebuilt.hasCanonicalHealth; + for (auto& tier : s_currentMetrics.tiers) { + tier.ddMinted = 0; + tier.dgbLocked = 0; + tier.positions = 0; + tier.healthRatio = 0; + } + return {}; + }; + if (cancelled()) return cancellation(); if (!view) { LogPrint(BCLog::DIGIDOLLAR, "ScanUTXOSet: No coins view provided\n"); - return true; + return publish(); } if (!blockman) { LogPrint(BCLog::DIGIDOLLAR, "ScanUTXOSet: No block manager provided - skipping UTXO scan (unit test mode?)\n"); - return true; + return publish(); } // Create cursor to iterate all UTXOs (similar to gettxoutsetinfo) std::unique_ptr pcursor(view->Cursor()); if (!pcursor) { LogPrint(BCLog::DIGIDOLLAR, "ScanUTXOSet: Unable to create UTXO cursor\n"); - return true; + return publish(); } LogPrint(BCLog::DIGIDOLLAR, "ScanUTXOSet: Starting blockchain-wide UTXO scan with full transaction access\n"); - LogPrintf("DigiDollar: ========== STARTING UTXO SCAN ==========\n"); + LogPrintf("DigiDollar legacy health reconstruction: checking unspent outputs\n"); bool complete = true; + std::string error; size_t vaults_found = 0; size_t dd_amount_extracted = 0; size_t dd_amount_estimated = 0; @@ -361,6 +680,12 @@ bool SystemHealthMonitor::ScanUTXOSet(CCoinsView* view, CCoinsView* validation_v // Iterate through ALL UTXOs in the blockchain while (pcursor->Valid()) { + if (cancelled()) return cancellation(); + if (++outputs_checked % 4096 == 0 && SteadyClock::now() - last_progress >= std::chrono::seconds{1}) { + last_progress = SteadyClock::now(); + LogPrintf("DigiDollar legacy health reconstruction: %u outputs checked\n", outputs_checked); + if (callbacks.progress) callbacks.progress(outputs_checked, 0); + } COutPoint key; Coin coin; @@ -407,7 +732,7 @@ bool SystemHealthMonitor::ScanUTXOSet(CCoinsView* view, CCoinsView* validation_v if (validation_view) { Coin validation_coin; if (!validation_view->GetCoin(key, validation_coin) || validation_coin.IsSpent()) { - LogPrintf("DigiDollar: Skipping spent DD vault candidate: %s:%d\n", txid.ToString(), key.n); + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Skipping spent DD vault candidate: %s:%d\n", txid.ToString(), key.n); processed_txids.insert(txid); pcursor->Next(); continue; @@ -444,10 +769,10 @@ bool SystemHealthMonitor::ScanUTXOSet(CCoinsView* view, CCoinsView* validation_v // DCA/ERR health, so this must fail CLOSED — report incomplete instead // of silently undercounting supply/collateral. if (chain && dd_floor > 0 && static_cast(coin.nHeight) >= dd_floor) { - LogPrintf("ERROR: ScanUTXOSet: could not read the creating transaction of " - "DD vault candidate %s (coin height %u >= DigiDollar floor %d). " - "Block data is incomplete; restart with -reindex.\n", - txid.ToString(), coin.nHeight, dd_floor); + error = strprintf("DigiDollar legacy health is unavailable: cannot read transaction %s at height %u (block %s). Restore the required retained block data and retry startup.", + txid.ToString(), coin.nHeight, + creating_block ? creating_block->GetBlockHash().ToString() : "unknown"); + LogPrintf("%s\n", error); complete = false; } else { LogPrint(BCLog::DIGIDOLLAR, "ScanUTXOSet: Could not fetch tx %s, skipping unverified DD vault candidate\n", @@ -459,17 +784,16 @@ bool SystemHealthMonitor::ScanUTXOSet(CCoinsView* view, CCoinsView* validation_v } // Add to totals - s_currentMetrics.totalCollateral += collateral; - s_currentMetrics.totalDDSupply += ddAmount; - s_currentMetrics.totalActivePositions++; + rebuilt.totalCollateral += collateral; + rebuilt.totalDDSupply += ddAmount; + rebuilt.totalActivePositions++; vaults_found++; processed_txids.insert(txid); LogPrint(BCLog::DIGIDOLLAR, "ScanUTXOSet: Found DD vault - collateral=%s, DD=%s (exact)\n", FormatMoney(collateral), FormatMoney(ddAmount)); - // ALWAYS log vault findings (not just BCLog::DIGIDOLLAR) - LogPrintf("DigiDollar: UTXO Scanner found vault: %s:%d - Collateral=%s DGB, DD=%s cents\n", + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: UTXO Scanner found vault: %s:%d - Collateral=%s DGB, DD=%s cents\n", txid.ToString(), key.n, FormatMoney(collateral), FormatMoney(ddAmount)); } @@ -479,19 +803,26 @@ bool SystemHealthMonitor::ScanUTXOSet(CCoinsView* view, CCoinsView* validation_v LogPrint(BCLog::DIGIDOLLAR, "ScanUTXOSet: Scan complete - %d UTXOs scanned, %d vault candidates checked, %d P2TR found\n", utxos_scanned, vault_candidates_checked, p2tr_found); LogPrint(BCLog::DIGIDOLLAR, "ScanUTXOSet: Results - Found %d vaults, %s DGB collateral, %s DD supply\n", - vaults_found, FormatMoney(s_currentMetrics.totalCollateral), - FormatMoney(s_currentMetrics.totalDDSupply)); + vaults_found, FormatMoney(rebuilt.totalCollateral), + FormatMoney(rebuilt.totalDDSupply)); LogPrint(BCLog::DIGIDOLLAR, "ScanUTXOSet: Exact amounts: %d, Estimated amounts: %d\n", dd_amount_extracted, dd_amount_estimated); - LogPrintf("DigiDollar: ========== UTXO SCAN COMPLETE ==========\n"); - LogPrintf("DigiDollar: Found %zu vaults, Total Collateral: %s DGB, Total DD: %s cents\n", - vaults_found, FormatMoney(s_currentMetrics.totalCollateral), - FormatMoney(s_currentMetrics.totalDDSupply)); - return complete; + if (!complete) return {HealthScanResult::Status::READ_ERROR, error}; + const auto result = publish(); + if (result.status == HealthScanResult::Status::COMPLETE) { + LogPrintf("DigiDollar legacy health reconstruction complete: %zu vaults, collateral %s DGB, legacy DD total %d cents\n", + vaults_found, FormatMoney(rebuilt.totalCollateral), rebuilt.totalDDSupply); + } + return result; } bool SystemHealthMonitor::ReconstructFromChain(ChainstateManager& chainman) +{ + return ReconstructFromChain(chainman, {}).status == HealthScanResult::Status::COMPLETE; +} + +HealthScanResult SystemHealthMonitor::ReconstructFromChain(ChainstateManager& chainman, const HealthScanCallbacks& callbacks) { // DD-FINAL-003 / AR-CONSENSUS-1: seed the cached system-health metrics from // the on-chain UTXO set at startup so consensus DCA/ERR health is identical @@ -505,18 +836,29 @@ bool SystemHealthMonitor::ReconstructFromChain(ChainstateManager& chainman) if (node::fReindex) { LogPrint(BCLog::DIGIDOLLAR, "Health: skipping startup reconstruction during reindex (block replay rebuilds metrics)\n"); - return true; + return {}; } - const CBlockIndex* tip = WITH_LOCK(::cs_main, return chainman.ActiveChain().Tip()); + // The tip, flushed cursor and validation view must describe the same chain. + LOCK(::cs_main); + const CBlockIndex* tip = chainman.ActiveChain().Tip(); if (tip == nullptr) { - return true; + return {}; + } + if (DigiDollar::IsThawDayActive(chainman.GetConsensus(), tip->nHeight)) { + const auto state = chainman.ActiveChainstate().CoinsTip().GetDigiDollarState(); + // Canonical initialization precedes import and verification. This late + // display initialization must never overwrite its inputs with globals. + if (state && state->Matches(chainman.GetConsensus().hashGenesisBlock, tip->GetBlockHash(), chainman.GetConsensus().nDDThawDayHeight, chainman.GetConsensus().DigiDollarHeight) && state->history_checked) return {}; + return {HealthScanResult::Status::READ_ERROR, + strprintf("DigiDollar canonical health is unavailable at height %d (block %s). Restore the required chainstate history and retry startup.", + tip->nHeight, tip->GetBlockHash().ToString())}; } // Skip the (potentially expensive) full UTXO scan unless DigiDollar is active // at the current tip — pre-activation and non-DD chains have no DD vaults. if (!DigiDollar::IsDigiDollarEnabled(tip, chainman)) { LogPrint(BCLog::DIGIDOLLAR, "Health: skipping startup reconstruction (DigiDollar not active at tip)\n"); - return true; + return {}; } // Flush the in-memory coins cache to disk so the CoinsDB cursor below sees @@ -526,16 +868,12 @@ bool SystemHealthMonitor::ReconstructFromChain(ChainstateManager& chainman) // which recomputes health from the seeded supply/collateral) diverge between // nodes. The reindex path returned above, so by here the datadir is writable. Chainstate& active = chainman.ActiveChainstate(); + if (callbacks.cancelled && callbacks.cancelled()) { + return {HealthScanResult::Status::CANCELLED, "DigiDollar legacy health reconstruction cancelled"}; + } active.ForceFlushStateToDisk(); - bool complete; - { - LOCK(::cs_main); - complete = ScanUTXOSet(&active.CoinsDB(), &active.CoinsTip(), &active.m_blockman, /*mempool=*/nullptr, &active.m_chain, &chainman.GetConsensus()); - } - const SystemMetrics m = GetCachedMetrics(); - LogPrintf("DigiDollar: startup health reconstruction complete - DD supply %s, collateral %s DGB\n", - FormatMoney(m.totalDDSupply), FormatMoney(m.totalCollateral)); - return complete; + return ScanUTXOSet(&active.CoinsDB(), &active.CoinsTip(), &active.m_blockman, /*mempool=*/nullptr, + &active.m_chain, &chainman.GetConsensus(), callbacks); } // ============================================================================ diff --git a/src/digidollar/health.h b/src/digidollar/health.h index fe5d77bc7c8..5b4fdf5dab4 100644 --- a/src/digidollar/health.h +++ b/src/digidollar/health.h @@ -6,6 +6,7 @@ #define DIGIBYTE_DIGIDOLLAR_HEALTH_H #include +#include #include #include #include @@ -15,9 +16,12 @@ #include #include #include +#include +#include // Forward declarations class CCoinsView; +class Coin; class CTxMemPool; class ChainstateManager; class CChain; @@ -36,6 +40,84 @@ namespace wallet { namespace DigiDollar { +using CanonicalTxLookup = std::function; + +struct HealthScanCallbacks { + std::function cancelled; + //! A zero total means that only the completed count is known. + std::function progress; +}; + +struct HealthScanResult { + enum class Status { COMPLETE, CANCELLED, READ_ERROR }; + Status status{Status::COMPLETE}; + std::string error; +}; + +struct CanonicalVault { + COutPoint outpoint; + CAmount principal{0}; + CAmount collateral{0}; +}; + +enum class VaultLookupResult { NOT_VAULT, VAULT, NOT_READY }; + +/** Resolve identity from the creating transaction on the supplied ancestry. + * NOT_READY means only one thing: the creating block could not be read here, so + * another node with that block may still answer. Whenever the creating + * transaction can be read, the answer is VAULT or NOT_VAULT and is the same on + * every node, including for a mint that records no readable amount. + */ +VaultLookupResult LookupCanonicalVault(const COutPoint& outpoint, const Coin& coin, + const Consensus::Params& params, + const CanonicalTxLookup& lookup, + CanonicalVault& vault, std::string& error); + +/** Reconstruct from a consistent UTXO view. This never establishes history proof + * and never mutates either the view or the legacy health monitor. + * An optional availability result allows callers to retain an unknown token + * total when readable legacy metadata has no unambiguous amount. Missing source + * blocks still fail the scan. No partial token total is returned. + */ +bool ReconstructChainstateHealth(const CCoinsView& view, const Consensus::Params& params, + const CanonicalTxLookup& lookup, + ChainstateHealth& health, std::string& error, + const std::function& interrupted = {}, + CAmount* circulating_supply = nullptr, + const std::function& progress = {}, + bool* circulating_supply_known = nullptr); + +/** How many times the whole coin set has been walked to rebuild the accounting. + * This walk reads a block from disk for every unspent DigiDollar output, so it + * should happen once when the node needs a new starting point and not again. + * Only tests and logs read this number; nothing in validation uses it. + */ +uint64_t ChainstateHealthRebuildCount(); + +/** Apply or undo a transaction using the original amounts of its creating vaults. */ +bool UpdateChainstateHealth(const CTransaction& tx, const std::vector& inputs, + const Consensus::Params& params, const CanonicalTxLookup& lookup, + ChainstateHealth& health, bool undo, std::string& error, + uint32_t creating_height = std::numeric_limits::max()); + +enum class SupplyChangeResult { KNOWN, UNKNOWN_METADATA, FAILURE }; + +/** Reporting only: inspect all token sources, distinguishing unavailable amounts + * in retained metadata from missing or inconsistent physical source data. + * Only KNOWN assigns change. An unknown amount never becomes a numeric delta. + */ +SupplyChangeResult GetCirculatingSupplyChange(const CTransaction& tx, const std::vector& inputs, + uint32_t height, const Consensus::Params& params, + const CanonicalTxLookup& lookup, CAmount& change, std::string& error); + +/** Strict reporting wrapper: succeeds only when the complete delta is known. */ +bool CalculateCirculatingSupplyChange(const CTransaction& tx, const std::vector& inputs, + uint32_t height, const Consensus::Params& params, + const CanonicalTxLookup& lookup, CAmount& change, std::string& error); + +/** The common post-activation health formula, without global ERR or price state. */ +std::optional CalculateChainstateHealth(const ChainstateHealth& state, CAmount price_micro_usd); + /** * System-wide health metrics structure * Aggregates all DigiDollar system data for monitoring @@ -188,6 +270,12 @@ class SystemHealthMonitor { //! old silent-undercount behavior. Every caller must decide explicitly. static bool ScanUTXOSet(CCoinsView* view, CCoinsView* validation_view, const node::BlockManager* blockman, const CTxMemPool* mempool, const CChain* chain, const Consensus::Params* consensus); + //! Cancellation and existing read failures leave the cached metrics unchanged. + static HealthScanResult ScanUTXOSet(CCoinsView* view, CCoinsView* validation_view, + const node::BlockManager* blockman, const CTxMemPool* mempool, + const CChain* chain, const Consensus::Params* consensus, + const HealthScanCallbacks& callbacks); + /** * Reconstruct the cached system-health metrics (total DD supply + total * collateral) from the on-chain UTXO set at node startup. @@ -209,6 +297,7 @@ class SystemHealthMonitor { //! (see ScanUTXOSet) — the caller must abort startup rather than run //! consensus with an incomplete health baseline. static bool ReconstructFromChain(ChainstateManager& chainman); + static HealthScanResult ReconstructFromChain(ChainstateManager& chainman, const HealthScanCallbacks& callbacks); /** * Get cached metrics without triggering updates @@ -226,6 +315,12 @@ class SystemHealthMonitor { s_currentMetrics = SystemMetrics(); } + /** Restore metrics when legacy baseline reconstruction fails before publication. */ + static void RestoreLegacyMetrics(const SystemMetrics& metrics) { + std::lock_guard lock(s_metricsMutex); + s_currentMetrics = metrics; + } + /** Set metrics directly for unit tests that need deterministic cached totals */ static void SetMetricsForTesting(const SystemMetrics& metrics) { std::lock_guard lock(s_metricsMutex); // RH-44 diff --git a/src/digidollar/scripts.cpp b/src/digidollar/scripts.cpp index 23b4e302a93..f11526f46f4 100644 --- a/src/digidollar/scripts.cpp +++ b/src/digidollar/scripts.cpp @@ -114,7 +114,7 @@ CScript CreateERRPath(const MintParams& params) return script; } -CScript CreateCollateralP2TR(const MintParams& params) +CScript CreateCollateralP2TR(const MintParams& params, bool register_metadata) { if (params.ddAmount <= 0 || params.lockHeight < 0 || !params.internalKey.IsFullyValid()) { // LogPrintf("DigiDollar: Invalid parameters for P2TR collateral script\n"); @@ -166,7 +166,7 @@ CScript CreateCollateralP2TR(const MintParams& params) // params.ddAmount, scriptPubKey.size()); // Phase 1: Register metadata for testing - RegisterScriptMetadata(scriptPubKey, DigiDollar::ScriptType::COLLATERAL_LOCK, params.ddAmount, params.lockHeight); + if (register_metadata) RegisterScriptMetadata(scriptPubKey, DigiDollar::ScriptType::COLLATERAL_LOCK, params.ddAmount, params.lockHeight); return scriptPubKey; diff --git a/src/digidollar/scripts.h b/src/digidollar/scripts.h index 2c9e2cde08a..3c2048d4a12 100644 --- a/src/digidollar/scripts.h +++ b/src/digidollar/scripts.h @@ -77,7 +77,7 @@ struct MintParams { * @param params Parameters including DD amount, lock period, keys, etc. * @return CScript P2TR script (OP_1 + 32-byte taproot output) */ -CScript CreateCollateralP2TR(const MintParams& params); +CScript CreateCollateralP2TR(const MintParams& params, bool register_metadata = true); /** * Create simple P2TR script for DigiDollar token outputs diff --git a/src/digidollar/txbuilder.cpp b/src/digidollar/txbuilder.cpp index 40727d39b31..ffeed8e1b9b 100644 --- a/src/digidollar/txbuilder.cpp +++ b/src/digidollar/txbuilder.cpp @@ -36,6 +36,84 @@ static const double MAX_FEE_RATIO = 0.5; // Maximum fee as ratio of to static const size_t MAX_TX_INPUTS = 400; // Maximum inputs per transaction to stay under MAX_STANDARD_TX_WEIGHT static const CAmount MIN_DD_TX_FEE = 10000000; // 0.1 DGB minimum DD transaction fee +// A taproot output is OP_1 followed by a 32-byte key. +static bool IsTaprootOutputScript(const CScript& script) +{ + int witnessVersion = -1; + std::vector witnessProgram; + return script.IsWitnessProgram(witnessVersion, witnessProgram) && + witnessVersion == 1 && + witnessProgram.size() == WITNESS_V1_TAPROOT_SIZE; +} + +// Work out where the leftover DGB in a DigiDollar transaction should go. +// +// The caller names the address. The builder must never make one up. A key +// invented here belongs to no wallet, so DGB paid to it can never be spent +// again. When there is no address to use, the build stops and the caller +// reports the error instead of sending the money somewhere unrecoverable. +// +// Mints have one extra rule. In a mint the only taproot output allowed to hold +// DGB is the locked collateral. A taproot change output would be a second one +// and every node would reject the mint, so mints pass allowTaproot = false and +// send their change to an ordinary bech32 address. +static bool ResolveDGBChangeScript(const std::optional& changeDest, + bool allowTaproot, + CScript& scriptOut, + std::string& errorOut) +{ + if (!changeDest.has_value() || !IsValidDestination(*changeDest)) { + errorOut = "No wallet change address is available for the leftover DGB. " + "Unlock the wallet, or check that it can still hand out addresses, then try again."; + return false; + } + + const CScript script = GetScriptForDestination(*changeDest); + if (script.empty()) { + errorOut = "The wallet change address for the leftover DGB could not be used."; + return false; + } + + if (!allowTaproot && IsTaprootOutputScript(script)) { + errorOut = "A mint cannot send its leftover DGB to a taproot address. " + "A mint may hold DGB in only one taproot output, the locked collateral, " + "so the change must go to an ordinary bech32 address."; + return false; + } + + scriptOut = script; + return true; +} + +// Work out where the collateral a redemption unlocks should go. +// +// This is the whole vault, so it matters more than anything else in the +// transaction. The caller names the address. The builder must never make one +// up. An address worked out here from the owner key is not tweaked the way a +// wallet's taproot addresses are, so no wallet watches it and no wallet can +// spend from it. When there is no address to use, the build stops and the +// caller reports the error instead of sending the vault somewhere it can never +// be spent from again. +static bool ResolveCollateralReturnScript(const std::optional& collateralDest, + CScript& scriptOut, + std::string& errorOut) +{ + if (!collateralDest.has_value() || !IsValidDestination(*collateralDest)) { + errorOut = "No address is available for the collateral this redemption unlocks. " + "Unlock the wallet, or check that it can still hand out addresses, then try again."; + return false; + } + + const CScript script = GetScriptForDestination(*collateralDest); + if (script.empty()) { + errorOut = "The address for the collateral this redemption unlocks could not be used."; + return false; + } + + scriptOut = script; + return true; +} + CAmount ApplyCollateralSafetyMargin(CAmount requiredCollateral) { if (requiredCollateral <= 0) { @@ -102,7 +180,7 @@ bool TxBuilder::SelectCoins(const std::vector& utxos, CAmount target, // Stop if we have enough if (total >= target) { - LogPrintf("DigiDollar: SelectCoins succeeded with %d inputs totaling %lld sats (target: %lld)\n", + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: SelectCoins succeeded with %d inputs totaling %lld sats (target: %lld)\n", inputs.size(), total, target); return true; } @@ -181,7 +259,9 @@ CAmount MintTxBuilder::CalculateRequiredCollateral(CAmount ddAmount, int lockDay LogPrintf("DigiDollar TxBuilder: Cannot calculate collateral without canonical system health\n"); return 0; } - int effectiveRatio = DCA::DynamicCollateralAdjustment::ApplyDCA(baseRatio, systemCollateral); + int effectiveRatio = IsThawDayActive(chainParams.GetConsensus(), currentHeight) ? + DCA::DynamicCollateralAdjustment::ApplyDCAForHealth(baseRatio, systemCollateral) : + DCA::DynamicCollateralAdjustment::ApplyDCA(baseRatio, systemCollateral); if (effectiveRatio <= 0 || effectiveRatio == std::numeric_limits::max()) { LogPrintf("DigiDollar TxBuilder: DCA calculation failed (baseRatio=%d, health=%d)\n", baseRatio, systemCollateral); @@ -193,7 +273,7 @@ CAmount MintTxBuilder::CalculateRequiredCollateral(CAmount ddAmount, int lockDay // DD amount is in cents (100 = $1.00), oracle price is in micro-USD (1,000,000 = $1.00) CAmount usdValue = ddAmount; // DD amount = USD value in cents - LogPrintf("DigiDollar TxBuilder: CalculateRequiredCollateral - DD: %d cents, Price: %lld micro-USD ($%.6f), BaseRatio: %d%%, DCA: %.2f, EffectiveRatio: %d%%\n", + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar TxBuilder: CalculateRequiredCollateral - DD: %d cents, Price: %lld micro-USD ($%.6f), BaseRatio: %d%%, DCA: %.2f, EffectiveRatio: %d%%\n", ddAmount, oraclePrice, oraclePrice / 1000000.0, baseRatio, dcaMultiplier, effectiveRatio); // Use 128-bit arithmetic to prevent overflow @@ -227,7 +307,7 @@ CAmount MintTxBuilder::CalculateRequiredCollateral(CAmount ddAmount, int lockDay return 0; } - LogPrintf("DigiDollar TxBuilder: - Required collateral base: %llu sats (%.8f DGB), with 1%% safety margin: %llu sats (%.8f DGB)\n", + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar TxBuilder: - Required collateral base: %llu sats (%.8f DGB), with 1%% safety margin: %llu sats (%.8f DGB)\n", requiredCollateral, requiredCollateral / 100000000.0, requiredWithSafetyMargin, requiredWithSafetyMargin / 100000000.0); @@ -308,11 +388,14 @@ bool MintTxBuilder::ValidateMintParams(const TxBuilderMintParams& params) const return false; } - LogPrintf("ValidateMintParams PASSED\n"); + LogPrint(BCLog::DIGIDOLLAR, "ValidateMintParams PASSED\n"); return true; } int TxBuilder::GetCurrentSystemCollateral() const { + if (IsThawDayActive(chainParams.GetConsensus(), currentHeight)) { + return candidateHealth && *candidateHealth >= 0 && *candidateHealth <= 30000 ? *candidateHealth : -1; + } int systemHealth = DCA::DynamicCollateralAdjustment::GetCurrentSystemHealth(); if (systemHealth >= 0) { return systemHealth; @@ -332,13 +415,6 @@ int TxBuilder::GetCurrentSystemCollateral() const { return -1; } -CKey MintTxBuilder::GenerateChangeKey() const { - // Generate a new key for change - CKey changeKey; - changeKey.MakeNewKey(true); - return changeKey; -} - TxBuilderResult MintTxBuilder::BuildMintTransaction(const TxBuilderMintParams& params) { TxBuilderResult result; @@ -354,6 +430,12 @@ TxBuilderResult MintTxBuilder::BuildMintTransaction(const TxBuilderMintParams& p return result; } + if (IsThawDayActive(chainParams.GetConsensus(), currentHeight)) { + const int health = GetCurrentSystemCollateral(); + if (health < 0) { result.error = "DigiDollar candidate health state not ready"; return result; } + if (health < 100) { result.error = "Minting blocked by candidate emergency health"; return result; } + } + // Create transaction CMutableTransaction tx; tx.SetDigiDollarType(::DD_TX_MINT); @@ -440,22 +522,13 @@ TxBuilderResult MintTxBuilder::BuildMintTransaction(const TxBuilderMintParams& p // If we have significant change, add change output and recalculate if (change >= DUST_THRESHOLD) { - // CRITICAL FIX: Use wallet-provided change destination if available - // This ensures the wallet recognizes the change output as its own! + // The change only goes to the address the caller gave us. If there is + // none, stop here: the money is still in the inputs and nothing has + // been sent. CScript changeScript; - if (params.dgbChangeDest.has_value()) { - // Use wallet-controlled change address (PREFERRED - fixes DGB loss bug) - changeScript = GetScriptForDestination(params.dgbChangeDest.value()); - LogPrintf("DigiDollar: MINT using wallet-provided DGB change destination\n"); - } else { - // Fallback to a non-P2TR script so validation never confuses DGB - // change with DigiDollar collateral. Production wallet/RPC/Qt paths - // should still provide a wallet-controlled change destination. - CKey changeKey = GenerateChangeKey(); - CPubKey changePubkey = changeKey.GetPubKey(); - CTxDestination changeDest{WitnessV0KeyHash(changePubkey)}; - changeScript = GetScriptForDestination(changeDest); - LogPrintf("DigiDollar: WARNING - MINT using non-wallet fallback DGB change destination\n"); + if (!ResolveDGBChangeScript(params.dgbChangeDest, /*allowTaproot=*/false, changeScript, result.error)) { + LogPrintf("DigiDollar: MINT stopped - %s\n", result.error); + return result; } tx.vout.push_back(CTxOut(change, changeScript)); @@ -566,7 +639,7 @@ bool TransferTxBuilder::ValidateTransferParams(const TxBuilderTransferParams& pa return false; } - LogPrintf("DigiDollar: ValidateTransferParams PASSED\n"); + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: ValidateTransferParams PASSED\n"); return true; } @@ -687,7 +760,7 @@ TxBuilderResult TransferTxBuilder::BuildTransferTransaction(const TxBuilderTrans // Add DGB fee inputs (after DD inputs) // Note: Phase 2.1 already selected these UTXOs, so we just add them directly - LogPrintf("DigiDollar: TxBuilder - feeUtxos.size=%d, feeAmounts.size=%d\n", + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: TxBuilder - feeUtxos.size=%d, feeAmounts.size=%d\n", params.feeUtxos.size(), params.feeAmounts.size()); CAmount totalFeeIn = 0; @@ -696,17 +769,17 @@ TxBuilderResult TransferTxBuilder::BuildTransferTransaction(const TxBuilderTrans tx.vin.push_back(CTxIn(utxo)); // Get actual fee UTXO amount from feeAmounts CAmount feeAmount = (i < params.feeAmounts.size()) ? params.feeAmounts[i] : GetDGBFromUTXO(utxo); - LogPrintf("DigiDollar: Fee input %d - using %s: %d sats\n", + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Fee input %d - using %s: %d sats\n", i, (i < params.feeAmounts.size()) ? "feeAmounts[i]" : "GetDGBFromUTXO()", feeAmount); totalFeeIn += feeAmount; - LogPrintf("DigiDollar: Added fee input %s:%d (%d sats)\n", + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Added fee input %s:%d (%d sats)\n", utxo.hash.ToString(), utxo.n, feeAmount); } // Add DD outputs for recipients (all with 0 DGB value) - LogPrintf("DigiDollar: TxBuilder - recipients.size=%d\n", params.recipients.size()); + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: TxBuilder - recipients.size=%d\n", params.recipients.size()); for (const auto& [address, amount] : params.recipients) { - LogPrintf("DigiDollar: Creating DD output - address=%s, amount=%d cents\n", address, amount); + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Creating DD output - address=%s, amount=%d cents\n", address, amount); CTxDestination dest = DecodeDigiDollarAddress(address); const auto* taproot = std::get_if(&dest); if (!taproot) { @@ -725,7 +798,7 @@ TxBuilderResult TransferTxBuilder::BuildTransferTransaction(const TxBuilderTrans // can extract the DD amount when spending this output RegisterScriptMetadata(ddScript, ScriptType::DD_TOKEN_OUTPUT, amount, 0); - LogPrintf("DigiDollar: Added DD output for %s: %d cents (registered)\n", address, amount); + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Added DD output for %s: %d cents (registered)\n", address, amount); } // Add DD change output if needed — EVERY cent of DD must be accounted for. @@ -756,7 +829,7 @@ TxBuilderResult TransferTxBuilder::BuildTransferTransaction(const TxBuilderTrans // because ExtractDDAmount can't find the amount. RegisterScriptMetadata(changeScript, ScriptType::DD_TOKEN_OUTPUT, ddChange, 0); - LogPrintf("DigiDollar: Added DD change output: %d cents (tweaked key, registered)\n", ddChange); + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Added DD change output: %d cents (tweaked key, registered)\n", ddChange); } else if (ddChange < 0) { // This should never happen - SelectDDCoins should ensure enough DD result.error = strprintf("Insufficient DD: need %d cents, have %d cents", totalDDOut, totalDDIn); @@ -768,7 +841,7 @@ TxBuilderResult TransferTxBuilder::BuildTransferTransaction(const TxBuilderTrans CAmount calculatedFee = CalculateFee(tx, params.feeRate); CAmount actualFee = std::max(calculatedFee, MIN_DD_TX_FEE); - LogPrintf("DigiDollar: Fee calculation - calculated: %d sats, minimum: %d sats, actual: %d sats\n", + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Fee calculation - calculated: %d sats, minimum: %d sats, actual: %d sats\n", calculatedFee, MIN_DD_TX_FEE, actualFee); if (totalFeeIn <= 0) { @@ -786,21 +859,17 @@ TxBuilderResult TransferTxBuilder::BuildTransferTransaction(const TxBuilderTrans if (totalFeeIn > 0) { CAmount dgbChange = totalFeeIn - actualFee; if (dgbChange > 0 && dgbChange >= DUST_THRESHOLD) { - // CRITICAL FIX: Use wallet-provided change destination if available - // This ensures the wallet recognizes the change output as its own! + // The change only goes to the address the caller gave us. A wallet + // watches the addresses it handed out and nothing else, so change + // paid anywhere else looks to the owner like money that left the + // wallet and never came back. CScript dgbChangeScript; - if (params.dgbChangeDest.has_value()) { - // Use wallet-controlled change address (PREFERRED - fixes DGB loss bug) - dgbChangeScript = GetScriptForDestination(params.dgbChangeDest.value()); - LogPrintf("DigiDollar: Using wallet-provided DGB change destination\n"); - } else { - // Fallback to spenderKey pubkey (WARNING: wallet may not recognize this!) - // Create DGB change output using a P2WPKH (not P2TR) to differentiate from DD outputs - dgbChangeScript = GetScriptForDestination(WitnessV0KeyHash(params.spenderKey.GetPubKey())); - LogPrintf("DigiDollar: WARNING - Using spenderKey for DGB change (wallet may not recognize!)\n"); + if (!ResolveDGBChangeScript(params.dgbChangeDest, /*allowTaproot=*/true, dgbChangeScript, result.error)) { + LogPrintf("DigiDollar: Transfer stopped - %s\n", result.error); + return result; } tx.vout.push_back(CTxOut(dgbChange, dgbChangeScript)); - LogPrintf("DigiDollar: Added DGB change output: %d sats\n", dgbChange); + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Added DGB change output: %d sats\n", dgbChange); } } @@ -825,7 +894,7 @@ TxBuilderResult TransferTxBuilder::BuildTransferTransaction(const TxBuilderTrans } tx.vout.push_back(CTxOut(0, metadataScript)); - LogPrintf("DigiDollar: Added OP_RETURN with %d DD output amounts\n", ddOutputAmounts.size()); + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Added OP_RETURN with %d DD output amounts\n", ddOutputAmounts.size()); // Final validation - ensure DD conservation // DD amounts are now stored in OP_RETURN, so sum up ddOutputAmounts instead @@ -834,7 +903,7 @@ TxBuilderResult TransferTxBuilder::BuildTransferTransaction(const TxBuilderTrans finalDDOut += amt; } - LogPrintf("DigiDollar: Conservation check - Input: %d cents, Output: %d cents\n", + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Conservation check - Input: %d cents, Output: %d cents\n", totalDDIn, finalDDOut); // DD conservation is absolute — every cent in must equal every cent out. @@ -906,7 +975,7 @@ TxBuilderResult TransferTxBuilder::BuildTransferTransaction(const TxBuilderTrans result.tx = tx; result.success = true; - LogPrintf("DigiDollar: Transaction finalized - %d inputs, %d outputs, version=%d, locktime=%d\n", + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Transaction finalized - %d inputs, %d outputs, version=%d, locktime=%d\n", tx.vin.size(), tx.vout.size(), tx.nVersion, tx.nLockTime); return result; @@ -937,6 +1006,13 @@ CAmount RedeemTxBuilder::CalculateRedemptionAmount(const TxBuilderRedeemParams& // Calculate DGB to release based on DD burned and current conditions + if (IsThawDayActive(chainParams.GetConsensus(), currentHeight)) { + const int health = GetCurrentSystemCollateral(); + if (health < 0 || position.ddMinted <= 0) return 0; + const CAmount required = ERR::EmergencyRedemptionRatio::GetRequiredDDBurn(position.ddMinted, health); + return required > 0 && params.ddToRedeem >= required ? position.dgbLocked : 0; + } + if (params.path == RedemptionPath::ERR) { // ERR (Emergency Redemption Ratio) path: // System < 100% requires MORE DD to burn for full collateral @@ -1086,7 +1162,7 @@ CCollateralPosition RedeemTxBuilder::GetCollateralPosition(const COutPoint& outp position.collateralRatio = 0; } - LogPrintf("DigiDollar: GetCollateralPosition - Found UTXO %s:%d with dgbLocked=%d, ddMinted=%d, unlockHeight=%d\n", + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: GetCollateralPosition - Found UTXO %s:%d with dgbLocked=%d, ddMinted=%d, unlockHeight=%d\n", outpoint.hash.ToString(), outpoint.n, position.dgbLocked, position.ddMinted, position.unlockHeight); return position; @@ -1095,7 +1171,7 @@ CCollateralPosition RedeemTxBuilder::GetCollateralPosition(const COutPoint& outp TxBuilderResult RedeemTxBuilder::BuildRedemptionTransaction(const TxBuilderRedeemParams& params) { TxBuilderResult result; - LogPrintf("DigiDollar: BuildRedemptionTransaction - Starting (path: %d, ddToRedeem: %d)\n", + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: BuildRedemptionTransaction - Starting (path: %d, ddToRedeem: %d)\n", static_cast(params.path), params.ddToRedeem); // Step 1: Validate parameters @@ -1119,16 +1195,26 @@ TxBuilderResult RedeemTxBuilder::BuildRedemptionTransaction(const TxBuilderRedee position.ddMinted = params.ddMinted; position.unlockHeight = params.unlockHeight; position.collateralRatio = CalculatePositionCollateralRatio(position.dgbLocked, position.ddMinted, oraclePrice); - LogPrintf("DigiDollar: Using pre-queried collateral position - dgbLocked: %d, ddMinted: %d, unlockHeight: %d\n", + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Using pre-queried collateral position - dgbLocked: %d, ddMinted: %d, unlockHeight: %d\n", position.dgbLocked, position.ddMinted, position.unlockHeight); } else { // Fallback is fail-safe unless a caller has supplied a chainstate-backed // position lookup in a derived builder. position = GetCollateralPosition(params.collateralOutpoint); - LogPrintf("DigiDollar: Queried collateral position from UTXO - dgbLocked: %d, ddMinted: %d, unlockHeight: %d\n", + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Queried collateral position from UTXO - dgbLocked: %d, ddMinted: %d, unlockHeight: %d\n", position.dgbLocked, position.ddMinted, position.unlockHeight); } + if (IsThawDayActive(chainParams.GetConsensus(), currentHeight)) { + const int health = GetCurrentSystemCollateral(); + if (health < 0) { result.error = "DigiDollar candidate health state not ready"; return result; } + const auto expected = health < 100 ? RedemptionPath::ERR : RedemptionPath::NORMAL; + if (params.path != expected) { + result.error = "Candidate emergency health changed; rebuild the redemption with the current required burn"; + return result; + } + } + CAmount ddToBurn = params.ddToRedeem; if (params.path == RedemptionPath::ERR) { const int systemHealth = GetCurrentSystemCollateral(); @@ -1143,7 +1229,7 @@ TxBuilderResult RedeemTxBuilder::BuildRedemptionTransaction(const TxBuilderRedee return result; } ddToBurn = ERR::EmergencyRedemptionRatio::GetRequiredDDBurn(position.ddMinted, systemHealth); - LogPrintf("DigiDollar: ERR redemption burn requirement - original: %lld, health: %d%%, required burn: %lld\n", + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: ERR redemption burn requirement - original: %lld, health: %d%%, required burn: %lld\n", (long long)position.ddMinted, systemHealth, (long long)ddToBurn); } @@ -1163,7 +1249,7 @@ TxBuilderResult RedeemTxBuilder::BuildRedemptionTransaction(const TxBuilderRedee return result; } - LogPrintf("DigiDollar: Collateral to release: %d sats (%.8f DGB)\n", + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Collateral to release: %d sats (%.8f DGB)\n", dgbToRelease, dgbToRelease / 100000000.0); // Step 5: Build transaction @@ -1172,14 +1258,14 @@ TxBuilderResult RedeemTxBuilder::BuildRedemptionTransaction(const TxBuilderRedee // Set type - always DD_TX_REDEEM (ERR is handled via burn amount, not tx type) // NOTE: Only 2 paths exist - NORMAL and ERR. Both use DD_TX_REDEEM. tx.SetDigiDollarType(::DD_TX_REDEEM); - LogPrintf("DigiDollar: Using DD_TX_REDEEM type (path=%s)\n", + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Using DD_TX_REDEEM type (path=%s)\n", params.path == RedemptionPath::ERR ? "ERR" : "NORMAL"); // Input 0: Collateral UTXO (P2TR) // CRITICAL: nSequence must be < 0xFFFFFFFF to enable OP_CHECKLOCKTIMEVERIFY // Using 0xFFFFFFFE to signal opt-in Replace-By-Fee (BIP125) and enable CLTV tx.vin.push_back(CTxIn(params.collateralOutpoint, CScript(), 0xFFFFFFFE)); - LogPrintf("DigiDollar: Added collateral input: %s:%d (nSequence=0xFFFFFFFE for CLTV)\n", + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Added collateral input: %s:%d (nSequence=0xFFFFFFFE for CLTV)\n", params.collateralOutpoint.hash.ToString(), params.collateralOutpoint.n); // Inputs 1+: DD UTXOs to burn @@ -1187,7 +1273,7 @@ TxBuilderResult RedeemTxBuilder::BuildRedemptionTransaction(const TxBuilderRedee // So they MUST use nSequence < 0xFFFFFFFF to enable locktime checking for (const auto& utxo : params.ddUtxos) { tx.vin.push_back(CTxIn(utxo, CScript(), 0xFFFFFFFE)); - LogPrintf("DigiDollar: Added DD input to burn: %s:%d (nSequence=0xFFFFFFFE for CLTV)\n", utxo.hash.ToString(), utxo.n); + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Added DD input to burn: %s:%d (nSequence=0xFFFFFFFE for CLTV)\n", utxo.hash.ToString(), utxo.n); } // Inputs N+: Fee UTXOs (DGB) @@ -1205,7 +1291,7 @@ TxBuilderResult RedeemTxBuilder::BuildRedemptionTransaction(const TxBuilderRedee for (CAmount amount : params.feeAmounts) { totalFeeIn += amount; } - LogPrintf("DigiDollar: Added %d fee inputs (total: %d sats) from pre-selected UTXOs\n", + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Added %d fee inputs (total: %d sats) from pre-selected UTXOs\n", params.feeUtxos.size(), totalFeeIn); } else { // Fallback: Try to get amounts from UTXO set (shouldn't normally happen) @@ -1214,25 +1300,22 @@ TxBuilderResult RedeemTxBuilder::BuildRedemptionTransaction(const TxBuilderRedee CAmount amount = GetUTXOValue(feeUtxo); totalFeeIn += amount; } - LogPrintf("DigiDollar: Added %d fee inputs (total: %d sats) via GetUTXOValue fallback\n", + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Added %d fee inputs (total: %d sats) via GetUTXOValue fallback\n", params.feeUtxos.size(), totalFeeIn); } } - // Output 0: Collateral returned to owner (100% of locked DGB) - // CRITICAL: This is the FULL collateral amount - must be separate from any DGB change - CTxDestination collateralReturnDest; - if (params.collateralDest.has_value()) { - collateralReturnDest = params.collateralDest.value(); - LogPrintf("DigiDollar: Using provided wallet destination for returned collateral\n"); - } else { - // Fallback to owner key (for backwards compatibility) - CPubKey pubkey = params.ownerKey.GetPubKey(); - collateralReturnDest = CTxDestination{WitnessV1Taproot(XOnlyPubKey(pubkey))}; - LogPrintf("DigiDollar: Using owner key pubkey for returned collateral (wallet may not recognize)\n"); + // Output 0: the collateral this redemption unlocks, all of it. It is always + // its own output, never mixed with the DGB left over after the fee. The + // address comes from the caller; the builder never works one out for + // itself, because the money would then be unspendable. + CScript collateralReturnScript; + if (!ResolveCollateralReturnScript(params.collateralDest, collateralReturnScript, result.error)) { + LogPrintf("DigiDollar: BuildRedemptionTransaction FAILED - %s\n", result.error); + return result; } - tx.vout.push_back(CTxOut(dgbToRelease, GetScriptForDestination(collateralReturnDest))); - LogPrintf("DigiDollar: Added collateral return output: %d sats (100%% of locked collateral)\n", dgbToRelease); + tx.vout.push_back(CTxOut(dgbToRelease, collateralReturnScript)); + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Added collateral return output: %d sats (100%% of locked collateral)\n", dgbToRelease); // Calculate DD change - if we selected more DD UTXOs than needed, return the change CAmount totalDDInput = 0; @@ -1240,14 +1323,14 @@ TxBuilderResult RedeemTxBuilder::BuildRedemptionTransaction(const TxBuilderRedee totalDDInput += amount; } CAmount ddChange = totalDDInput - ddToBurn; - LogPrintf("DigiDollar: DD input total: %d cents, to burn: %d cents, change: %d cents\n", + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: DD input total: %d cents, to burn: %d cents, change: %d cents\n", totalDDInput, ddToBurn, ddChange); // Add DD change output if wallet selected more DD UTXOs than needed // NOTE: DD is fungible - exact-amount enforcement is at the VAULT level (ddToRedeem == position.ddMinted) // not at the DD input level. The wallet may select excess DD UTXOs and receive change. if (ddChange > 0) { - LogPrintf("DigiDollar: DD change: %d cents will be returned to owner\n", ddChange); + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: DD change: %d cents will be returned to owner\n", ddChange); // Create DD change output back to the owner using P2TR CPubKey ownerPubKey = params.ownerKey.GetPubKey(); CScript ddChangeScript = CreateDigiDollarP2TR(XOnlyPubKey(ownerPubKey), ddChange); @@ -1263,7 +1346,7 @@ TxBuilderResult RedeemTxBuilder::BuildRedemptionTransaction(const TxBuilderRedee << CScriptNum(3) // 3 = REDEEM transaction type << CScriptNum(ddChange); // DD change amount in cents tx.vout.push_back(CTxOut(0, metadataScript)); - LogPrintf("DigiDollar: Added OP_RETURN for DD change: %d cents\n", ddChange); + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Added OP_RETURN for DD change: %d cents\n", ddChange); // CRITICAL FIX: Store ddChange in result so wallet can track the change UTXO // Without this, result.ddChange stays 0 and wallet never tracks the change! @@ -1280,19 +1363,23 @@ TxBuilderResult RedeemTxBuilder::BuildRedemptionTransaction(const TxBuilderRedee // Set transaction locktime to unlockHeight (critical for CLTV validation) tx.nLockTime = position.unlockHeight; - LogPrintf("DigiDollar: Set tx.nLockTime = %d (unlockHeight)\n", position.unlockHeight); + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Set tx.nLockTime = %d (unlockHeight)\n", position.unlockHeight); // Calculate actual fees. DigiDollar redemptions must pay the absolute DD // fee floor, independent of transaction size, to preserve the anti-spam // policy used by mint and transfer builders. CAmount calculatedFee = CalculateFee(tx, params.feeRate); result.totalFees = std::max(calculatedFee, MIN_DD_TX_FEE); + if (IsThawDayActive(chainParams.GetConsensus(), currentHeight)) { + if (!MoneyRange(params.minimumFee)) { result.error = "Invalid redemption fee requirement"; return result; } + result.totalFees = std::max(result.totalFees, params.minimumFee); + } if (result.totalFees > calculatedFee) { - LogPrintf("DigiDollar: Calculated redemption fee (%d sats) below DD minimum fee, using %d sats\n", + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Calculated redemption fee (%d sats) below DD minimum fee, using %d sats\n", calculatedFee, result.totalFees); } - LogPrintf("DigiDollar: Calculated fees: %d sats (fee inputs: %d sats)\n", result.totalFees, totalFeeIn); + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Calculated fees: %d sats (fee inputs: %d sats)\n", result.totalFees, totalFeeIn); if (totalFeeIn <= 0) { result.error = "Insufficient fee inputs for DD redemption fee"; @@ -1311,28 +1398,33 @@ TxBuilderResult RedeemTxBuilder::BuildRedemptionTransaction(const TxBuilderRedee if (totalFeeIn > 0) { if (feeChange >= DUST_THRESHOLD) { - // CRITICAL FIX: Use separate destination for DGB change - // This ensures collateral return and DGB change are SEPARATE outputs - CTxDestination changeDest; - if (params.dgbChangeDest.has_value()) { - changeDest = params.dgbChangeDest.value(); - LogPrintf("DigiDollar: Using provided dgbChangeDest for fee change\n"); - } else if (params.collateralDest.has_value()) { - // Fallback: use collateralDest (this WILL merge with collateral if amounts differ) - changeDest = params.collateralDest.value(); - LogPrintf("DigiDollar: WARNING - No dgbChangeDest provided, using collateralDest for fee change (may merge with collateral)\n"); - } else { - // Last resort: use owner key (wallet may not recognize) - CPubKey pubkey = params.ownerKey.GetPubKey(); - changeDest = CTxDestination{WitnessV1Taproot(XOnlyPubKey(pubkey))}; - LogPrintf("DigiDollar: WARNING - Using owner key for fee change (wallet may not recognize)\n"); + // Prefer the caller's own change address, so the returned + // collateral and the leftover fee money stay in separate outputs. + // If there is no change address, the address chosen for the + // returned collateral is used instead: that one also belongs to + // whoever asked for the redemption, so nothing is lost. + // + // Those are the only two addresses used. Anything the builder could + // work out for itself from the owner key would be an address no + // wallet watches and no wallet can spend, so with neither of them + // the build stops. + std::optional changeDest = params.dgbChangeDest; + if (!changeDest.has_value()) { + changeDest = params.collateralDest; + LogPrintf("DigiDollar: No separate change address given, sending fee change to the collateral address\n"); + } + + CScript changeScript; + if (!ResolveDGBChangeScript(changeDest, /*allowTaproot=*/true, changeScript, result.error)) { + LogPrintf("DigiDollar: BuildRedemptionTransaction FAILED - %s\n", result.error); + return result; } - tx.vout.push_back(CTxOut(feeChange, GetScriptForDestination(changeDest))); - LogPrintf("DigiDollar: Added fee change output: %d sats to separate destination\n", feeChange); + tx.vout.push_back(CTxOut(feeChange, changeScript)); + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Added fee change output: %d sats\n", feeChange); } else { // Dust goes to miner as fee result.totalFees += feeChange; - LogPrintf("DigiDollar: Fee change (%d sats) below dust, added to fee\n", feeChange); + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Fee change (%d sats) below dust, added to fee\n", feeChange); } } @@ -1341,12 +1433,168 @@ TxBuilderResult RedeemTxBuilder::BuildRedemptionTransaction(const TxBuilderRedee result.success = true; result.collateralRequired = 0; // No collateral required for redemption - LogPrintf("DigiDollar: BuildRedemptionTransaction SUCCESS - %d inputs, %d outputs, fee: %d sats\n", + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: BuildRedemptionTransaction SUCCESS - %d inputs, %d outputs, fee: %d sats\n", tx.vin.size(), tx.vout.size(), result.totalFees); return result; } +RedeemTxBuilder::FeeEstimate RedeemTxBuilder::EstimateRedemptionFee(const TxBuilderRedeemParams& params) const +{ + FeeEstimate estimate; + + // Whether the transaction carries DD change decides two of its outputs, + // so resolve the burn amount the same way BuildRedemptionTransaction + // does: the caller's amount on the normal path, the health-derived + // amount on the emergency path. + CAmount ddMinted = params.ddMinted; + if (params.collateralAmount <= 0) { + ddMinted = GetCollateralPosition(params.collateralOutpoint).ddMinted; + } + CAmount ddToBurn = params.ddToRedeem; + if (params.path == RedemptionPath::ERR) { + const int systemHealth = GetCurrentSystemCollateral(); + if (systemHealth < 0) { + estimate.error = "ERR system health unavailable"; + return estimate; + } + if (ddMinted <= 0) { + estimate.error = "Original DD minted amount unavailable for ERR redemption"; + return estimate; + } + ddToBurn = ERR::EmergencyRedemptionRatio::GetRequiredDDBurn(ddMinted, systemHealth); + } + CAmount totalDDInput = 0; + for (const CAmount amount : params.ddAmounts) totalDDInput += amount; + const CAmount ddChange = totalDDInput - ddToBurn; + if (ddChange < 0) { + estimate.error = strprintf("Insufficient DD selected (input: %d, need: %d)", totalDDInput, ddToBurn); + return estimate; + } + + // Only sizes matter here, not values or keys. Every input and output + // below has the same serialized length as the one the build creates. + CMutableTransaction skeleton; + skeleton.SetDigiDollarType(::DD_TX_REDEEM); + skeleton.vin.push_back(CTxIn(params.collateralOutpoint, CScript(), 0xFFFFFFFE)); + for (const auto& utxo : params.ddUtxos) { + skeleton.vin.push_back(CTxIn(utxo, CScript(), 0xFFFFFFFE)); + } + for (const auto& utxo : params.feeUtxos) { + skeleton.vin.push_back(CTxIn(utxo)); + } + + // The collateral return goes to the caller's destination, and a legacy + // or SegWit v0 address has a different script length than Taproot. With no + // destination at all the real build stops, so a taproot-sized placeholder + // is enough to finish the measurement. + CScript collateralReturnScript; + if (params.collateralDest.has_value()) { + collateralReturnScript = GetScriptForDestination(params.collateralDest.value()); + } else { + collateralReturnScript = CScript() << OP_1 << std::vector(32, 0); + } + skeleton.vout.push_back(CTxOut(1, collateralReturnScript)); + + if (ddChange > 0) { + // A Taproot token output of the same length as the real DD change + // output, without registering anything in the script registry. + skeleton.vout.push_back(CTxOut(0, CScript() << OP_1 << std::vector(32, 0))); + CScript metadataScript; + metadataScript << OP_RETURN + << std::vector{'D', 'D'} + << CScriptNum(3) + << CScriptNum(ddChange); + skeleton.vout.push_back(CTxOut(0, metadataScript)); + } + + estimate.vsize = EstimateTransactionVSize(skeleton); + const int64_t weight = static_cast(estimate.vsize) * WITNESS_SCALE_FACTOR; + if (weight > MAX_STANDARD_TX_WEIGHT) { + estimate.error = strprintf("Projected redemption transaction is too large: %d weight units (%u vB), standard limit is %d WU. Consolidate DGB fee coins first.", + weight, static_cast(estimate.vsize), MAX_STANDARD_TX_WEIGHT); + return estimate; + } + estimate.fee = std::max(CalculateFee(skeleton, params.feeRate), MIN_DD_TX_FEE); + estimate.ok = true; + return estimate; +} + +bool RedeemTxBuilder::SelectRedemptionFeeInputs(TxBuilderRedeemParams& params, + const FeeCoinSelector& select_coins, + std::string& error, + int max_attempts) const +{ + error.clear(); + if (!select_coins) { + error = "No DGB fee coin selector provided"; + return false; + } + if (max_attempts < 1) max_attempts = 1; + + const auto give_up = [¶ms](std::string& out, std::string message) { + params.feeUtxos.clear(); + params.feeAmounts.clear(); + out = std::move(message); + return false; + }; + + // The first target is the fee of the smallest redemption that can + // exist: the collateral input, the DD inputs and exactly one fee coin. + CAmount target = 0; + { + TxBuilderRedeemParams smallest = params; + smallest.feeUtxos.assign(1, COutPoint()); + smallest.feeAmounts.clear(); + const FeeEstimate first = EstimateRedemptionFee(smallest); + if (!first.ok) return give_up(error, first.error); + target = first.fee; + } + + CAmount lastTotal = -1; + size_t lastCount = 0; + FeeEstimate estimate; + CAmount total = 0; + for (int attempt = 1; attempt <= max_attempts; ++attempt) { + std::vector utxos; + std::vector amounts; + total = 0; + if (!select_coins(target, utxos, amounts, total)) { + return give_up(error, strprintf("Insufficient DGB balance for the redemption fee: need at least %d sats (%.8f DGB) in spendable DGB coins other than the collateral and DD token outputs", + target, target / 100000000.0)); + } + if (utxos.empty() || utxos.size() != amounts.size()) { + return give_up(error, "DGB fee coin selection returned no usable coins"); + } + params.feeUtxos = utxos; + params.feeAmounts = amounts; + + estimate = EstimateRedemptionFee(params); + if (!estimate.ok) return give_up(error, estimate.error); + + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: redemption fee selection attempt %d - %zu fee inputs worth %d sats, projected %u vB, fee %d sats\n", + attempt, utxos.size(), total, static_cast(estimate.vsize), estimate.fee); + + if (total >= estimate.fee) return true; + + if (total <= lastTotal && utxos.size() <= lastCount) { + // The selector returned nothing more than last time: the wallet + // has no further coin to add, so more attempts cannot help. + break; + } + lastTotal = total; + lastCount = utxos.size(); + + // Cover the fee of the transaction this attempt would produce, and + // ask for strictly more than was just selected so the next + // selection cannot be the same set of coins. + target = std::max(estimate.fee, total + 1); + } + + return give_up(error, strprintf("Insufficient DGB fee inputs: %zu DGB coins worth %d sats cannot pay the %d sat fee of the %u vB redemption they would produce, because each additional small coin adds more fee than value. Consolidate small DGB coins into one larger coin and retry.", + lastCount, lastTotal, estimate.fee, static_cast(estimate.vsize))); +} + RedemptionPath RedeemTxBuilder::DetermineRedemptionPath(const TxBuilderRedeemParams& params) const { // Determine redemption path based on system health // NOTE: Only 2 paths exist - NORMAL and ERR @@ -1377,7 +1625,7 @@ CAmount RedeemTxBuilder::CalculateCollateralReturn(CAmount ddAmount, CAmount ori // Since we're redeeming the full position in most cases, we return the full collateral // For partial redemptions, this would be adjusted - LogPrintf("DigiDollar: Calculating collateral return - DD amount: %d, Original collateral: %d, Current price: %d\n", + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Calculating collateral return - DD amount: %d, Original collateral: %d, Current price: %d\n", ddAmount, originalCollateral, currentPrice); // V1 supports only full-position redemption. Normal redemption and ERR both @@ -1386,7 +1634,7 @@ CAmount RedeemTxBuilder::CalculateCollateralReturn(CAmount ddAmount, CAmount ori CAmount returnAmount = originalCollateral; - LogPrintf("DigiDollar: Collateral return calculated: %d sats (%.8f DGB)\n", + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Collateral return calculated: %d sats (%.8f DGB)\n", returnAmount, returnAmount / 100000000.0); return returnAmount; @@ -1407,7 +1655,7 @@ bool RedeemTxBuilder::VerifyRedemptionConditions(const TxBuilderRedeemParams& pa switch (path) { case RedemptionPath::NORMAL: - LogPrintf("DigiDollar: Normal redemption conditions met (timelock expired)\n"); + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Normal redemption conditions met (timelock expired)\n"); return true; case RedemptionPath::ERR: @@ -1423,7 +1671,7 @@ bool RedeemTxBuilder::VerifyRedemptionConditions(const TxBuilderRedeemParams& pa LogPrintf("DigiDollar: ERR redemption FAILED - invalid oracle price (%d)\n", oraclePrice); return false; } - LogPrintf("DigiDollar: ERR redemption conditions met (system health: %d%%)\n", systemHealth); + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: ERR redemption conditions met (system health: %d%%)\n", systemHealth); return true; } diff --git a/src/digidollar/txbuilder.h b/src/digidollar/txbuilder.h index 646b59b22a4..6a0a8010582 100644 --- a/src/digidollar/txbuilder.h +++ b/src/digidollar/txbuilder.h @@ -15,6 +15,7 @@ #include #include +#include #include #include #include @@ -46,8 +47,10 @@ struct TxBuilderMintParams { CAmount feeRate; // Fee rate in sat/vB std::vector utxos; // Available UTXOs for collateral - // Optional: Destination for DGB change output. Production wallet/RPC/Qt - // paths must set this to a wallet-controlled address. + // Where leftover DGB goes. Set this to an address the wallet owns. It must + // be an ordinary bech32 address, never taproot: a mint may hold DGB in only + // one taproot output, the locked collateral. If a mint would leave change + // behind and this is not set, the build fails and no transaction is made. std::optional dgbChangeDest; TxBuilderMintParams() : ddAmount(0), lockDays(0), lockTier(0), feeRate(1000) {} // Default 1000 sat/vB @@ -63,8 +66,9 @@ struct TxBuilderTransferParams { std::vector feeAmounts; // DGB amounts for each fee UTXO (parallel to feeUtxos) CKey spenderKey; // Key for signing DD inputs - // Optional: Destination for DGB change output (if not set, uses spenderKey pubkey) - // CRITICAL: Must be set to a wallet-controlled address to avoid losing DGB! + // Where leftover DGB goes. Set this to an address the wallet owns. If a + // transfer would leave change behind and this is not set, the build fails + // and no transaction is made. std::optional dgbChangeDest; TxBuilderTransferParams() : feeRate(1000) {} // Default 1000 sat/vB @@ -79,17 +83,22 @@ struct TxBuilderRedeemParams { CAmount ddToRedeem; // Amount of DD to burn RedemptionPath path; // Which redemption path to use CKey ownerKey; // Owner's private key (for signing collateral input) - CAmount feeRate; // Fee rate in sat/vB + CAmount feeRate; // Fee rate in sat/kB + CAmount minimumFee{0}; // Fee measured after signing a prior construction attempt std::vector ddUtxos; // DD UTXOs to burn std::vector ddAmounts; // DD amounts for each UTXO (parallel to ddUtxos) std::vector feeUtxos; // DGB UTXOs for fees std::vector feeAmounts; // Amounts of fee UTXOs - // Optional: Destination for returned collateral (if not set, uses ownerKey pubkey) + // Where the collateral this redemption unlocks is paid. That is the whole + // vault, so it always gets its own output. Set this to an address the + // wallet owns, or to the address the user asked for. If it is not set the + // build fails and no transaction is made. std::optional collateralDest; - // Optional: Destination for DGB change output (if not set, uses collateralDest or ownerKey) - // CRITICAL: Must be set to a wallet-controlled address to avoid losing DGB! + // Where leftover fee money goes. Set this to an address the wallet owns so + // it stays in its own output, separate from the returned collateral. If it + // is not set, the collateral address above is used instead. std::optional dgbChangeDest; // Optional pre-queried position data (caller can provide to avoid UTXO lookups) @@ -122,6 +131,7 @@ class TxBuilder { const CChainParams& chainParams; int currentHeight; CAmount oraclePrice; + std::optional candidateHealth; // Helper functions CAmount CalculateFee(const CMutableTransaction& tx, CAmount feeRate) const; @@ -139,6 +149,9 @@ class TxBuilder { TxBuilder(const CChainParams& params, int height, CAmount price); virtual ~TxBuilder() = default; + /** Supply health verified for this builder's candidate height and quote. */ + void SetCandidateHealth(int health) { candidateHealth = health; } + // Validation helpers bool ValidateAmount(CAmount amount) const; bool ValidateFeeRate(CAmount feeRate) const; @@ -195,7 +208,6 @@ class MintTxBuilder : public TxBuilder { private: CScript CreateCollateralScript(const TxBuilderMintParams& params) const; CScript CreateDDOutputScript(const CKey& owner, CAmount amount) const; - CKey GenerateChangeKey() const; }; // Transfer transaction builder @@ -250,6 +262,59 @@ class RedeemTxBuilder : public TxBuilder { */ TxBuilderResult BuildRedemptionTransaction(const TxBuilderRedeemParams& params); + /** + * The size and fee BuildRedemptionTransaction arrives at for exactly + * these parameters. It lays out the same inputs (collateral, every DD + * token, every fee coin) and the same outputs that exist at the moment + * the build fixes its fee (the collateral return, plus the DD change + * token and its OP_RETURN record when the DD inputs exceed the burn), + * runs them through the same size estimator and applies the same + * absolute DD minimum fee. The DGB change output is added after the fee + * is fixed and therefore does not count, in the build or here. + * + * Callers use it to choose fee coins that the build will accept, instead + * of guessing a size in advance. A unit test pins it to the build. + */ + struct FeeEstimate { + bool ok{false}; + size_t vsize{0}; // Projected virtual size, safety margin included. + CAmount fee{0}; // The fee the build will charge, in satoshis. + std::string error; // Why no estimate could be made, when !ok. + }; + FeeEstimate EstimateRedemptionFee(const TxBuilderRedeemParams& params) const; + + /** + * Selects DGB coins worth at least `target` satoshis. On success it + * fills the coins and their amounts (same order, same length) and their + * total; it returns false when the wallet cannot reach the target. The + * wallet's own coin selection is passed in this shape so the builder + * never needs to know about wallets. + */ + using FeeCoinSelector = std::function& utxos, + std::vector& amounts, + CAmount& total)>; + + static constexpr int DEFAULT_FEE_SELECTION_ATTEMPTS{12}; + + /** + * Choose fee coins for a redemption so that the coins actually cover the + * fee of the transaction they produce. Every fee coin makes the + * transaction bigger and so raises the fee, which is why a single + * selection against a guessed size can come up short when a wallet + * holds only small DGB coins. Each attempt asks the selector for at + * least the fee the previous attempt's real size needs, and for + * strictly more than the previous attempt selected, so every attempt + * changes the input set until the coins cover the fee or the attempt + * budget is spent. On success params.feeUtxos and params.feeAmounts + * hold the chosen coins; on failure they are cleared and `error` says + * what the caller can do about it. + */ + bool SelectRedemptionFeeInputs(TxBuilderRedeemParams& params, + const FeeCoinSelector& select_coins, + std::string& error, + int max_attempts = DEFAULT_FEE_SELECTION_ATTEMPTS) const; + /** * Determine the appropriate redemption path based on current conditions * @param params Redemption parameters diff --git a/src/digidollar/validation.cpp b/src/digidollar/validation.cpp index 7af40cd80b5..36b793d7705 100644 --- a/src/digidollar/validation.cpp +++ b/src/digidollar/validation.cpp @@ -3,6 +3,7 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include +#include #include #include #include @@ -50,7 +51,7 @@ struct ValidationCache { if (scriptTypeCache.size() > MAX_CACHE_SIZE) { scriptTypeCache.clear(); amountCache.clear(); - LogPrintf("DigiDollar: Validation cache cleared (size limit reached)\n"); + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Validation cache cleared (size limit reached)\n"); } } }; @@ -78,13 +79,89 @@ static bool CoinHeightMayCreateDigiDollar(const Coin& coin, const ValidationCont return false; } - const int activation_height = EarliestDigiDollarActivationHeight(ctx); + const int activation_height = IsThawDayActive(ctx.params.GetConsensus(), ctx.nHeight) ? + ctx.params.GetConsensus().DigiDollarHeight : EarliestDigiDollarActivationHeight(ctx); return activation_height <= 0 || coin.nHeight >= static_cast(activation_height); } +static std::optional ContextHealthState(const ValidationContext& ctx) +{ + if (!ctx.coins || !ctx.candidateParent) return std::nullopt; + const auto& params = ctx.params.GetConsensus(); + auto state = ctx.coins->GetDigiDollarState(); + if (!state || !state->Matches(params.hashGenesisBlock, ctx.candidateParent->GetBlockHash(), params.nDDThawDayHeight, params.DigiDollarHeight) || + (IsThawDayActive(params, ctx.candidateParent->nHeight) && !state->history_checked)) return std::nullopt; + return state; +} + +static CanonicalTxLookup ContextTransactionLookup(const ValidationContext& ctx) +{ + return [&ctx](const uint256& hash, uint32_t height, CTransactionRef& tx) { + if (height == MEMPOOL_HEIGHT && ctx.mempool) { + tx = ctx.mempool->get(hash); + return static_cast(tx); + } + return ctx.txLookup && ctx.txLookup(hash, height, tx); + }; +} + +static bool CheckCanonicalVaultInputs(const CTransaction& tx, const ValidationContext& ctx, + bool& spends_vault, std::string& error) +{ + spends_vault = false; + if (!ctx.coins) { error = "DigiDollar state not ready: UTXO view unavailable"; return false; } + for (const auto& input : tx.vin) { + Coin coin; + if (!ctx.coins->GetCoin(input.prevout, coin)) continue; + CanonicalVault vault; + const auto result = LookupCanonicalVault(input.prevout, coin, ctx.params.GetConsensus(), + ContextTransactionLookup(ctx), vault, error); + if (result == VaultLookupResult::NOT_READY) return false; + spends_vault |= result == VaultLookupResult::VAULT; + } + return true; +} + +static bool ReadCanonicalTokenAmount(const COutPoint& outpoint, const Coin& coin, + const ValidationContext& ctx, CAmount& amount, TxValidationState& state) +{ + if (coin.nHeight == MEMPOOL_HEIGHT) + return state.Invalid(TxValidationResult::TX_CONSENSUS, "dd-input-amounts-unknown", "Cannot spend unconfirmed DD inputs"); + if (!CoinHeightMayCreateDigiDollar(coin, ctx) || coin.IsCoinBase()) + return state.Invalid(TxValidationResult::TX_CONSENSUS, "dd-input-before-activation"); + CTransactionRef creating; + const auto lookup = ContextTransactionLookup(ctx); + if (!lookup(outpoint.hash, coin.nHeight, creating) || !creating || creating->GetHash() != outpoint.hash || + outpoint.n >= creating->vout.size() || creating->vout[outpoint.n] != coin.out) + return state.Error("DigiDollar state not ready: restore token creating transaction"); + if (!ExtractDDAmountFromBlockDb(outpoint, coin.nHeight, lookup, amount)) + return state.Invalid(TxValidationResult::TX_CONSENSUS, "dd-input-amounts-unknown"); + return true; +} + +// Whether the oracle-dependent checks are being skipped for this candidate. +// +// The skip flag is set from how far the node doing the checking has got: a node +// that is still downloading blocks sets it, a node that is up to date does not. +// That means two honest nodes could reach different answers about the same +// block, which is a chain split. Below the Thaw Day height the flag is still +// honoured, and must be: the blocks already on the chain were accepted under it +// and every existing node would disagree with a new one if that moved. From the +// Thaw Day height on the flag is ignored, so whether a block is valid depends +// only on that block and the chain it builds on. +static bool OracleChecksSkipped(const ValidationContext& ctx) +{ + return ctx.skipOracleValidation && !IsThawDayActive(ctx.params.GetConsensus(), ctx.nHeight); +} + static std::optional ResolveCanonicalHealth(const ValidationContext& ctx, const char* operation) { + if (IsThawDayActive(ctx.params.GetConsensus(), ctx.nHeight)) { + const auto state = ContextHealthState(ctx); + return state ? CalculateChainstateHealth(*state, ctx.oraclePriceMicroUSD) : std::nullopt; + } + const DigiDollar::SystemMetrics metrics = DigiDollar::SystemHealthMonitor::GetCachedMetrics(); @@ -129,6 +206,8 @@ static std::optional ResolveCanonicalHealth(const ValidationContext& ctx, // Script Analysis Functions // ============================================================================ +// Test-only helper. It answers from the script metadata registry, which is +// local wallet state, so block and mempool validation must never call it. ScriptType IdentifyScriptType(const CScript& script) { // Quick rejection for obviously non-P2TR scripts if (!IsCanonicalP2TROutput(script)) { @@ -157,10 +236,20 @@ bool IsDDTokenScript(const CScript& script) { return type == ScriptType::DD_TOKEN_OUTPUT; } +// Consensus reads DigiDollar amounts from the chain only: a mint or redeem +// OP_RETURN, or the transaction that created a token output. The script +// metadata registry is filled in by the wallet and the transaction builder and +// is keyed by output script alone, so an owner address that is used again keeps +// only the amount written last. Block validity must never depend on it. The +// registry form below exists for unit tests that have no chain to read. bool ExtractDDAmount(const CScript& script, CAmount& amount) { - // Phase 1: Use metadata registry for scripts created by Create*P2TR functions + return ExtractDDAmount(script, amount, /*allow_registry=*/false); +} + +bool ExtractDDAmount(const CScript& script, CAmount& amount, bool allow_registry) { + // Test-only: the registry is never read by block or mempool validation. ScriptMetadata metadata; - if (GetScriptMetadata(script, metadata)) { + if (allow_registry && GetScriptMetadata(script, metadata)) { if (metadata.type == ScriptType::DD_TOKEN_OUTPUT || metadata.type == ScriptType::COLLATERAL_LOCK) { amount = metadata.ddAmount; @@ -463,7 +552,7 @@ bool ExtractDDAmountFromBlockDb(const COutPoint& prevout, uint32_t coinHeight, bool ExtractMintAccountingAmounts(const CTransaction& tx, CAmount& ddAmount, - CAmount& collateralAmount) + CAmount& collateralAmount, bool allow_registry) { ddAmount = 0; collateralAmount = 0; @@ -474,7 +563,7 @@ bool ExtractMintAccountingAmounts(const CTransaction& tx, const int ddIdx = FindDDOpReturn(tx); if (ddIdx < 0 || - !ExtractDDAmount(tx.vout[ddIdx].scriptPubKey, ddAmount) || + !ExtractDDAmount(tx.vout[ddIdx].scriptPubKey, ddAmount, allow_registry) || ddAmount <= 0) { ddAmount = 0; return false; @@ -594,13 +683,11 @@ static bool ExtractRedemptionDDOutputs(const CTransaction& tx, CAmount& totalDDO return false; } - CAmount ddAmount = 0; - if (foundOpReturn) { - ddAmount = ddAmountFromOpReturn; - } else if (!ExtractDDAmount(output.scriptPubKey, ddAmount)) { + // A change output is only countable through the serialized amount. + if (!foundOpReturn) { return false; } - if (!AddDDAmount(totalDDOutputs, ddAmount)) { + if (!AddDDAmount(totalDDOutputs, ddAmountFromOpReturn)) { return false; } } @@ -665,8 +752,7 @@ bool ExtractRedemptionAccountingAmounts(const CTransaction& tx, CAmount ddAmount = 0; if ((ExtractDDAmountFromPrevTx(tx.vin[i].prevout, ddAmount) && ddAmount > 0) || - (txLookup && ExtractDDAmountFromBlockDb(tx.vin[i].prevout, coin.nHeight, txLookup, ddAmount) && ddAmount > 0) || - (ExtractDDAmount(coin.out.scriptPubKey, ddAmount) && ddAmount > 0)) { + (txLookup && ExtractDDAmountFromBlockDb(tx.vin[i].prevout, coin.nHeight, txLookup, ddAmount) && ddAmount > 0)) { if (!AddDDAmount(totalDDInputs, ddAmount)) { return false; } @@ -760,6 +846,14 @@ static bool LookupPreviousTransaction(const COutPoint& prevout, bool SpendsDigiDollarCollateralVault(const CTransaction& tx, const ValidationContext& ctx) { + if (IsThawDayActive(ctx.params.GetConsensus(), ctx.nHeight)) { + bool spends{false}; + std::string error; + // Unknown creating data must enter the full validator, which reports a + // readiness error instead of treating the input as ordinary DGB. + return !CheckCanonicalVaultInputs(tx, ctx, spends, error) || spends; + } + if (ctx.coins == nullptr) { return false; } @@ -778,10 +872,7 @@ bool SpendsDigiDollarCollateralVault(const CTransaction& tx, continue; } - if (DigiDollar::IsRegisteredCollateralVaultScript(coin.out.scriptPubKey)) { - return true; - } - + // Only the transaction that created the coin can say it is a vault. CTransactionRef prev_tx; if (LookupPreviousTransaction(txin.prevout, coin.nHeight, ctx, prev_tx) && IsMintCollateralOutput(prev_tx, txin.prevout.n)) { @@ -857,7 +948,7 @@ CAmount CalculateRequiredCollateral(CAmount ddAmount, int64_t lockTime, LogPrint(BCLog::DIGIDOLLAR, " System health: %d%%\n", systemHealth); // Apply DCA multiplier based on canonical system health. - int effectiveRatio = GetEffectiveCollateralRatio(baseRatio, systemHealth, ctx.params); + int effectiveRatio = GetEffectiveCollateralRatio(baseRatio, systemHealth, ctx.params, IsThawDayActive(ctx.params.GetConsensus(), ctx.nHeight)); if (effectiveRatio <= 0 || effectiveRatio == std::numeric_limits::max()) { return 0; } @@ -896,10 +987,12 @@ CAmount CalculateRequiredCollateral(CAmount ddAmount, int64_t lockTime, } int GetEffectiveCollateralRatio(int baseRatio, int systemCollateral, - const CChainParams& params) { + const CChainParams& params, bool canonical_health) { // Use the new DCA system for more comprehensive health calculation double multiplier = DigiDollar::DCA::DynamicCollateralAdjustment::GetDCAMultiplier(systemCollateral); - int effectiveRatio = DigiDollar::DCA::DynamicCollateralAdjustment::ApplyDCA(baseRatio, systemCollateral); + int effectiveRatio = canonical_health + ? DigiDollar::DCA::DynamicCollateralAdjustment::ApplyDCAForHealth(baseRatio, systemCollateral) + : DigiDollar::DCA::DynamicCollateralAdjustment::ApplyDCA(baseRatio, systemCollateral); LogPrint(BCLog::DIGIDOLLAR, "DCA: Base ratio %d%%, system health %d%%, multiplier %.1fx -> effective ratio %d%%\n", baseRatio, systemCollateral, multiplier, effectiveRatio); @@ -960,22 +1053,22 @@ bool ValidateCollateralRatio(CAmount dgbLocked, CAmount ddMinted, // Get expected ratio for comparison const auto& ddParams = ctx.params.GetDigiDollarParams(); int baseRatio = GetCollateralRatioForLockTime(lockTime, ddParams); - int effectiveRatio = GetEffectiveCollateralRatio(baseRatio, ctx.systemCollateral, ctx.params); - - LogPrintf("DigiDollar: Collateral validation details:\n"); - LogPrintf(" DGB locked: %d satoshis (%.2f DGB)\n", dgbLocked, dgbLocked / (double)COIN); - LogPrintf(" DD minted: %d cents ($%.2f)\n", ddMinted, ddMinted / 100.0); - LogPrintf(" Lock time: %d blocks (~%d days)\n", lockTime, lockTime / (24 * 60 * 4)); - LogPrintf(" Oracle price: %lld micro-USD ($%.6f per DGB)\n", ctx.oraclePriceMicroUSD, ctx.oraclePriceMicroUSD / 1000000.0); - LogPrintf(" DGB value: %d cents ($%.2f)\n", dgbValueInCents, dgbValueInCents / 100.0); - LogPrintf(" Base ratio: %d%%, Effective ratio: %d%%, Actual ratio: %d%%\n", + int effectiveRatio = GetEffectiveCollateralRatio(baseRatio, ctx.systemCollateral, ctx.params, IsThawDayActive(ctx.params.GetConsensus(), ctx.nHeight)); + + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Collateral validation details:\n"); + LogPrint(BCLog::DIGIDOLLAR, " DGB locked: %d satoshis (%.2f DGB)\n", dgbLocked, dgbLocked / (double)COIN); + LogPrint(BCLog::DIGIDOLLAR, " DD minted: %d cents ($%.2f)\n", ddMinted, ddMinted / 100.0); + LogPrint(BCLog::DIGIDOLLAR, " Lock time: %d blocks (~%d days)\n", lockTime, lockTime / (24 * 60 * 4)); + LogPrint(BCLog::DIGIDOLLAR, " Oracle price: %lld micro-USD ($%.6f per DGB)\n", ctx.oraclePriceMicroUSD, ctx.oraclePriceMicroUSD / 1000000.0); + LogPrint(BCLog::DIGIDOLLAR, " DGB value: %d cents ($%.2f)\n", dgbValueInCents, dgbValueInCents / 100.0); + LogPrint(BCLog::DIGIDOLLAR, " Base ratio: %d%%, Effective ratio: %d%%, Actual ratio: %d%%\n", baseRatio, effectiveRatio, actualRatio); - LogPrintf(" Required collateral: %d satoshis (%.2f DGB)\n", + LogPrint(BCLog::DIGIDOLLAR, " Required collateral: %d satoshis (%.2f DGB)\n", requiredCollateral, requiredCollateral / (double)COIN); - LogPrintf(" System collateral: %d%%\n", ctx.systemCollateral); + LogPrint(BCLog::DIGIDOLLAR, " System collateral: %d%%\n", ctx.systemCollateral); bool isValid = dgbLocked >= requiredCollateral; - LogPrintf(" Result: %s\n", isValid ? "VALID" : "INVALID"); + LogPrint(BCLog::DIGIDOLLAR, " Result: %s\n", isValid ? "VALID" : "INVALID"); return isValid; } @@ -984,6 +1077,9 @@ bool ValidateCollateralRatio(CAmount dgbLocked, CAmount ddMinted, // Path-Specific Validation Functions // ============================================================================ +// Test-only helper. It reads the lock height from the script metadata registry, +// which is local wallet state. Consensus enforces the timelock through the +// vault script itself and through the creating mint transaction. bool ValidateNormalRedemption(const CScript& script, int currentHeight) { // Phase 1 simplified implementation // Extract lock height from script metadata if available @@ -999,7 +1095,7 @@ bool ValidateNormalRedemption(const CScript& script, int currentHeight) { return false; } // Timelock has expired - redemption allowed - LogPrintf("DigiDollar: Normal redemption allowed - timelock expired\n"); + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Normal redemption allowed - timelock expired\n"); return true; } @@ -1007,7 +1103,7 @@ bool ValidateNormalRedemption(const CScript& script, int currentHeight) { // For testing purposes, allow redemptions when height > 0 // In Phase 2, this would extract timelock from witness data during script execution if (currentHeight > 0) { - LogPrintf("DigiDollar: Normal redemption validation simplified (Phase 1) - allowing based on height > 0\n"); + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Normal redemption validation simplified (Phase 1) - allowing based on height > 0\n"); return true; } @@ -1028,7 +1124,7 @@ bool ValidateEmergencyRedemption(const CScript& script, } } - LogPrintf("DigiDollar: Emergency redemption - %d signatures provided, %d required\n", + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Emergency redemption - %d signatures provided, %d required\n", validSigs, requiredSigs); return validSigs >= requiredSigs; @@ -1039,7 +1135,7 @@ bool ValidateERRRedemption(const CScript& script, int systemCollateral) { // ERR (Emergency Redemption Ratio) activates when system < 100% collateralized bool errActive = systemCollateral < 100; - LogPrintf("DigiDollar: ERR validation - System collateral: %d%%, ERR %s\n", + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: ERR validation - System collateral: %d%%, ERR %s\n", systemCollateral, errActive ? "ACTIVE" : "INACTIVE"); return errActive; @@ -1073,9 +1169,10 @@ bool ValidateDigiDollarScript(const CScript& script, return true; } - // Extract and validate DD amount if present + // Extract and validate DD amount if present. This helper is test-only and + // classifies scripts through the registry, so it reads the amount there too. CAmount amount; - if (!ExtractDDAmount(script, amount)) { + if (!ExtractDDAmount(script, amount, /*allow_registry=*/true)) { if (serror) *serror = SCRIPT_ERR_INVALID_DD_AMOUNT; LogPrintf("DigiDollar: Script validation failed - cannot extract DD amount\n"); return false; @@ -1099,7 +1196,7 @@ bool ValidateDigiDollarScript(const CScript& script, break; } - LogPrintf("DigiDollar: Script validation passed - Type: %d, Amount: %d cents\n", + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Script validation passed - Type: %d, Amount: %d cents\n", static_cast(type), amount); return true; @@ -1112,7 +1209,8 @@ bool ValidateDigiDollarScript(const CScript& script, bool ValidateMintTransaction(const CTransaction& tx, const ValidationContext& ctx, TxValidationState& state) { - LogPrintf("DigiDollar: Validating mint transaction (txid: %s)\n", tx.GetHash().ToString()); + const bool canonical = IsThawDayActive(ctx.params.GetConsensus(), ctx.nHeight); + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Validating mint transaction (txid: %s)\n", tx.GetHash().ToString()); // 1. Basic structural checks if (tx.vin.empty()) { @@ -1150,16 +1248,30 @@ bool ValidateMintTransaction(const CTransaction& tx, // 3. Volatility protection checks for minting. A local sync-state flag // must not change post-activation mint validity. - if (Volatility::VolatilityMonitor::ShouldFreezeMinting()) { + if (!IsThawDayActive(ctx.params.GetConsensus(), ctx.nHeight) && Volatility::VolatilityMonitor::ShouldFreezeMinting()) { LogPrintf("DigiDollar: Minting frozen due to high volatility\n"); return state.Invalid(TxValidationResult::TX_CONSENSUS, "minting-frozen-volatility"); } - if (Volatility::VolatilityMonitor::ShouldFreezeAll()) { + if (!IsThawDayActive(ctx.params.GetConsensus(), ctx.nHeight) && Volatility::VolatilityMonitor::ShouldFreezeAll()) { LogPrintf("DigiDollar: All DD operations frozen due to extreme volatility\n"); return state.Invalid(TxValidationResult::TX_CONSENSUS, "all-operations-frozen"); } + if (IsThawDayActive(ctx.params.GetConsensus(), ctx.nHeight)) { + if (!ctx.candidateParent || ctx.candidateParent->nHeight != ctx.nHeight - 1 || + ctx.mintReference.candidate_height != ctx.nHeight || + ctx.mintReference.parent_hash != ctx.candidateParent->GetBlockHash() || + ctx.mintReference.genesis_hash != ctx.params.GetConsensus().hashGenesisBlock) { + return state.Error("DigiDollar volatility state not ready: candidate reference is unavailable or mismatched"); + } + const auto status = Volatility::EvaluateMintPrice(ctx.oraclePriceMicroUSD, ctx.mintReference); + if (!status.ready) return state.Error(ctx.mintReference.error.empty() ? + "DigiDollar volatility state not ready" : ctx.mintReference.error); + if (status.restricted) return state.Invalid(TxValidationResult::TX_CONSENSUS, + "minting-volatility-pause", "Candidate oracle price differs by at least 20% from the ancestor reference"); + } + // 4. Analyze outputs to find DD amounts and collateral CAmount totalDD = 0; CAmount totalCollateral = 0; @@ -1183,24 +1295,18 @@ bool ValidateMintTransaction(const CTransaction& tx, bool isP2TR = IsCanonicalP2TROutput(output.scriptPubKey); bool isOpReturn = (output.scriptPubKey.size() > 0 && output.scriptPubKey[0] == OP_RETURN); - // Check script type using metadata - ScriptType scriptType = IdentifyScriptType(output.scriptPubKey); + // Outputs are classified by structure only. The script metadata + // registry is local wallet state and must not influence block validity. CAmount ddAmount = 0; bool hasDDAmount = ExtractDDAmount(output.scriptPubKey, ddAmount); if (output.nValue > 0 && !isOpReturn) { // Any output with value could be collateral in a mint transaction - // Check if this is actually a DD TOKEN script with non-zero value (invalid) - if (scriptType == ScriptType::DD_TOKEN_OUTPUT) { - LogPrintf("DigiDollar: DD token output has non-zero DGB value: %d\n", output.nValue); - return state.Invalid(TxValidationResult::TX_CONSENSUS, "dd-output-value"); - } - // Check if it's P2TR (required for collateral) // Non-P2TR outputs are allowed as change outputs - skip them if (!isP2TR) { // This is a change output (P2WPKH, P2SH, etc.) - not collateral - LogPrintf("DigiDollar: Output %zu is non-P2TR change output (value=%d, scriptSize=%d)\n", + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Output %zu is non-P2TR change output (value=%d, scriptSize=%d)\n", i, output.nValue, output.scriptPubKey.size()); continue; // Skip to next output - change outputs are allowed } @@ -1260,7 +1366,7 @@ bool ValidateMintTransaction(const CTransaction& tx, try { CScriptNum txTypeNum(data, true); txType = txTypeNum.getint(); - LogPrintf("DigiDollar: Extracted tx type from OP_RETURN: %d\n", txType); + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Extracted tx type from OP_RETURN: %d\n", txType); } catch (const std::exception&) {} } if (txType != static_cast(DD_TX_MINT)) { @@ -1282,7 +1388,7 @@ bool ValidateMintTransaction(const CTransaction& tx, // Allow up to 8 bytes for DD amounts (int64_t range) CScriptNum ddAmountNum(data, true, 8); totalDD = ddAmountNum.GetInt64(); - LogPrintf("DigiDollar: Extracted DD amount from OP_RETURN: %lld cents ($%.2f)\n", + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Extracted DD amount from OP_RETURN: %lld cents ($%.2f)\n", static_cast(totalDD), totalDD / 100.0); } catch (const scriptnum_error&) { LogPrintf("DigiDollar: Malformed DD amount in mint OP_RETURN\n"); @@ -1308,7 +1414,7 @@ bool ValidateMintTransaction(const CTransaction& tx, // Allow up to 8 bytes for lock heights (int64_t range) CScriptNum lockHeightNum(data, true, 8); lockTime = lockHeightNum.GetInt64(); - LogPrintf("DigiDollar: Extracted lock height from OP_RETURN: %lld blocks\n", static_cast(lockTime)); + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Extracted lock height from OP_RETURN: %lld blocks\n", static_cast(lockTime)); } catch (const scriptnum_error&) { LogPrintf("DigiDollar: Malformed lock height in mint OP_RETURN\n"); return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-mint-lock-height", @@ -1331,7 +1437,7 @@ bool ValidateMintTransaction(const CTransaction& tx, CScriptNum lockTierNum(data, true); int64_t lockTier = lockTierNum.getint(); claimedLockTier = lockTier; - LogPrintf("DigiDollar: Extracted lock tier from OP_RETURN: %lld\n", static_cast(lockTier)); + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Extracted lock tier from OP_RETURN: %lld\n", static_cast(lockTier)); // Validate tier is in range (0-9) if (lockTier < 0 || lockTier > 9) { @@ -1401,7 +1507,7 @@ bool ValidateMintTransaction(const CTransaction& tx, if (data.size() == 32) { ownerXOnlyPubKeyData = data; hasOwnerPubKey = true; - LogPrintf("DigiDollar: Extracted owner x-only pubkey from OP_RETURN (%d bytes)\n", data.size()); + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Extracted owner x-only pubkey from OP_RETURN (%d bytes)\n", data.size()); } else { LogPrintf("DigiDollar: SECURITY - Invalid owner pubkey size in OP_RETURN: %d (expected 32)\n", data.size()); return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-mint-owner-pubkey", @@ -1463,7 +1569,7 @@ bool ValidateMintTransaction(const CTransaction& tx, // 4. Calculate DD amount if not extracted from metadata (skip for historical blocks) // For mint transactions, DD amount = (collateral * oracle_price * 100) / (collateral_ratio * COIN) - if (!ctx.skipOracleValidation && hasDDOutput && totalDD == 0 && totalCollateral > 0) { + if (!OracleChecksSkipped(ctx) && hasDDOutput && totalDD == 0 && totalCollateral > 0) { // Calculate DD amount from collateral and oracle price CAmount oraclePrice = ctx.oraclePriceMicroUSD; if (oraclePrice <= 0) { @@ -1471,15 +1577,32 @@ bool ValidateMintTransaction(const CTransaction& tx, return state.Invalid(TxValidationResult::TX_CONSENSUS, "invalid-oracle-price"); } - // Calculate max DD that can be minted with this collateral at minimum ratio - // Oracle price is in cents (100 = $1.00), DD is in cents (100 = $1.00) - // DD_cents = (collateral_sats * price_cents) / (min_ratio * COIN) - // But we don't know the tier/ratio yet, so use a conservative 200% (tier 1) - int minRatio = 200; - totalDD = (totalCollateral * oraclePrice) / (minRatio * COIN); - - LogPrintf("DigiDollar: Calculated DD amount from collateral: %d cents ($%.2f) from %d DGB at %d cents\n", - totalDD, totalDD / 100.0, totalCollateral / COIN, oraclePrice); + // Work out roughly how much DigiDollar this collateral could back, so + // the log has a figure in it. The lock tier is not known here, so this + // uses the strictest ratio, 200%. + // + // This figure never decides anything. Reaching this point means the + // transaction carries no DigiDollar OP_RETURN, and the lock height check + // a few lines below refuses every such transaction. + // + // The multiply is done in a 128-bit type on purpose. One collateral + // output of about 14.2 million DGB, at a DGB price near today's, does + // not fit in a 64-bit money amount once multiplied by the price. In + // 64 bits that multiply wraps round, which is undefined behaviour in + // a rule the whole network has to agree on. + const int minRatio = 200; + const __int128 estimate = (static_cast<__int128>(totalCollateral) * oraclePrice) + / (static_cast<__int128>(minRatio) * COIN); + if (estimate < 0 || estimate > std::numeric_limits::max()) { + // The numbers are far outside anything real, so there is no amount + // to report. Leave it at zero; the checks below refuse this + // transaction either way. + LogPrintf("DigiDollar: Collateral and price are too large to work out a DigiDollar amount\n"); + } else { + totalDD = static_cast(estimate); + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Calculated DD amount from collateral: %d cents ($%.2f) from %d DGB at %d cents\n", + totalDD, totalDD / 100.0, totalCollateral / COIN, oraclePrice); + } } // 5. Ensure we have both required output types @@ -1532,7 +1655,7 @@ bool ValidateMintTransaction(const CTransaction& tx, expectedParams.internalKey = DigiDollar::GetCollateralNUMSKey(); expectedParams.oracleKeys = DigiDollar::GetOracleKeys(15); - CScript expectedCollateral = DigiDollar::CreateCollateralP2TR(expectedParams); + CScript expectedCollateral = DigiDollar::CreateCollateralP2TR(expectedParams, /*register_metadata=*/false); if (expectedCollateral.empty()) { LogPrintf("DigiDollar: SECURITY - Failed to reconstruct expected P2TR collateral\n"); return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-collateral-reconstruction", @@ -1550,7 +1673,7 @@ bool ValidateMintTransaction(const CTransaction& tx, "Internal key must be the NUMS point to prevent key-path spending."); } - LogPrintf("DigiDollar: NUMS verification passed - collateral P2TR matches expected output\n"); + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: NUMS verification passed - collateral P2TR matches expected output\n"); } // 6. Validate total DD amount against mint limits @@ -1574,14 +1697,14 @@ bool ValidateMintTransaction(const CTransaction& tx, // higher ratio. A 1-hour lock would require only 200% instead of 1000% collateral. const std::optional resolvedHealth = ResolveCanonicalHealth(ctx, "mint"); if (!resolvedHealth.has_value()) { + if (IsThawDayActive(ctx.params.GetConsensus(), ctx.nHeight)) return state.Error("DigiDollar state not ready: canonical health unavailable"); return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-system-health", "Missing deterministic system health for mint validation"); } - ValidationContext collateralCtx(ctx.nHeight, ctx.oraclePriceMicroUSD, *resolvedHealth, - ctx.params, ctx.coins, ctx.skipOracleValidation, - ctx.txLookup, ctx.mempool); + ValidationContext collateralCtx(ctx); + collateralCtx.systemCollateral = *resolvedHealth; int64_t lockPeriod = lockTime - ctx.nHeight; if (lockPeriod <= 0) { @@ -1626,12 +1749,12 @@ bool ValidateMintTransaction(const CTransaction& tx, } // 9. Log successful validation - LogPrintf("DigiDollar: Mint validation successful:\n"); - LogPrintf(" Total DD: %d cents ($%.2f)\n", totalDD, totalDD / 100.0); - LogPrintf(" Total collateral: %d satoshis (%.2f DGB)\n", totalCollateral, totalCollateral / (double)COIN); - LogPrintf(" Required collateral: %d satoshis (%.2f DGB)\n", requiredCollateral, requiredCollateral / (double)COIN); - LogPrintf(" Lock time: %d blocks (~%d days)\n", lockTime, lockTime / (24 * 60 * 4)); - LogPrintf(" Oracle price: %lld micro-USD ($%.6f per DGB)\n", ctx.oraclePriceMicroUSD, ctx.oraclePriceMicroUSD / 1000000.0); + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Mint validation successful:\n"); + LogPrint(BCLog::DIGIDOLLAR, " Total DD: %d cents ($%.2f)\n", totalDD, totalDD / 100.0); + LogPrint(BCLog::DIGIDOLLAR, " Total collateral: %d satoshis (%.2f DGB)\n", totalCollateral, totalCollateral / (double)COIN); + LogPrint(BCLog::DIGIDOLLAR, " Required collateral: %d satoshis (%.2f DGB)\n", requiredCollateral, requiredCollateral / (double)COIN); + LogPrint(BCLog::DIGIDOLLAR, " Lock time: %d blocks (~%d days)\n", lockTime, lockTime / (24 * 60 * 4)); + LogPrint(BCLog::DIGIDOLLAR, " Oracle price: %lld micro-USD ($%.6f per DGB)\n", ctx.oraclePriceMicroUSD, ctx.oraclePriceMicroUSD / 1000000.0); return true; } @@ -1655,7 +1778,7 @@ bool ValidateTransferTransaction(const CTransaction& tx, } // Volatility protection checks for transfers (skip for historical blocks) - if (!ctx.skipOracleValidation && Volatility::VolatilityMonitor::ShouldFreezeAll()) { + if (!IsThawDayActive(ctx.params.GetConsensus(), ctx.nHeight) && !ctx.skipOracleValidation && Volatility::VolatilityMonitor::ShouldFreezeAll()) { LogPrintf("DigiDollar: All DD operations frozen due to extreme volatility\n"); return state.Invalid(TxValidationResult::TX_CONSENSUS, "all-operations-frozen"); } @@ -1796,6 +1919,16 @@ bool ValidateTransferTransaction(const CTransaction& tx, CAmount ddAmt = 0; bool found = false; + if (IsThawDayActive(ctx.params.GetConsensus(), ctx.nHeight)) { + Coin coin; + if (!ctx.coins || !ctx.coins->GetCoin(txin.prevout, coin)) + return state.Error("DigiDollar state not ready: token UTXO unavailable"); + if (coin.out.nValue != 0) continue; + if (!ReadCanonicalTokenAmount(txin.prevout, coin, ctx, ddAmt, state)) return false; + if (!AddDDAmount(inputDD, ddAmt)) return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-transfer-dd-input-amount"); + ++ddInputCount; + continue; + } // Confirmed-only policy: DD inputs created by mempool transactions // cannot supply authoritative OP_RETURN amounts yet. @@ -1828,16 +1961,6 @@ bool ValidateTransferTransaction(const CTransaction& tx, } } - // 3. Try coins view + metadata registry (may be stale — last resort) - if (!found && ctx.coins) { - Coin coin; - if (ctx.coins->GetCoin(txin.prevout, coin) && coin.out.nValue == 0) { - if (ExtractDDAmount(coin.out.scriptPubKey, ddAmt) && ddAmt > 0) { - found = true; - } - } - } - if (found) { inputDD += ddAmt; ddInputCount++; @@ -1893,7 +2016,7 @@ bool ValidateTransferTransaction(const CTransaction& tx, // 2. Add new output UTXOs // 3. Update DD balance tracking - LogPrintf("DigiDollar: Transfer transaction validated successfully - DD: %d cents (%d inputs, %d outputs)\n", + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Transfer transaction validated successfully - DD: %d cents (%d inputs, %d outputs)\n", outputDD, tx.vin.size(), ddOutputCount); return true; @@ -1903,7 +2026,7 @@ bool ValidateRedemptionTransaction(const CTransaction& tx, const ValidationContext& ctx, TxValidationState& state) { // Redemption transactions unlock collateral and burn DD tokens - LogPrintf("DigiDollar: Validating redemption transaction %s\n", tx.GetHash().ToString()); + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Validating redemption transaction %s\n", tx.GetHash().ToString()); // Extract DD transaction type for specific redemption path validation DigiDollarTxType txType = DigiDollar::GetDigiDollarTxType(tx); @@ -1914,7 +2037,7 @@ bool ValidateRedemptionTransaction(const CTransaction& tx, } // Volatility protection checks for redemptions (skip for historical blocks) - if (!ctx.skipOracleValidation && Volatility::VolatilityMonitor::ShouldFreezeAll()) { + if (!IsThawDayActive(ctx.params.GetConsensus(), ctx.nHeight) && !ctx.skipOracleValidation && Volatility::VolatilityMonitor::ShouldFreezeAll()) { LogPrintf("DigiDollar: All DD operations frozen due to extreme volatility\n"); return state.Invalid(TxValidationResult::TX_CONSENSUS, "all-operations-frozen"); } @@ -1959,6 +2082,11 @@ bool ValidateRedemptionTransaction(const CTransaction& tx, ddInputIndices.push_back(i); CAmount ddAmount = 0; + if (IsThawDayActive(ctx.params.GetConsensus(), ctx.nHeight)) { + if (!ReadCanonicalTokenAmount(input.prevout, coin, ctx, ddAmount, state)) return false; + if (!AddDDAmount(totalDDInputs, ddAmount)) return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-redeem-dd-input-amount"); + continue; + } if (ExtractDDAmountFromPrevTx(input.prevout, ddAmount) && ddAmount > 0) { if (!AddDDAmount(totalDDInputs, ddAmount)) { return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-redeem-dd-input-amount", @@ -1974,16 +2102,6 @@ bool ValidateRedemptionTransaction(const CTransaction& tx, } LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: DD input %d - amount: %lld cents (from block db)\n", i, (long long)ddAmount); - } else if (ExtractDDAmount(coin.out.scriptPubKey, ddAmount) && ddAmount > 0) { - // Last resort for unit tests and legacy in-memory flows. - // The registry is keyed only by scriptPubKey, so repeated - // sends to the same P2TR key can overwrite the amount. - if (!AddDDAmount(totalDDInputs, ddAmount)) { - return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-redeem-dd-input-amount", - "Redemption DD input amount exceeds per-output serialization bounds"); - } - LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: DD input %d - amount: %lld cents (from registry fallback)\n", - i, (long long)ddAmount); } else { LogPrintf("DigiDollar: WARNING - Could not extract DD amount from DD input %d\n", i); } @@ -2122,7 +2240,16 @@ bool ValidateRedemptionTransaction(const CTransaction& tx, "Redemption transactions may have at most one DD change output"); } - if (foundOpReturn && ddAmountFromOpReturn > 0) { + if (IsThawDayActive(ctx.params.GetConsensus(), ctx.nHeight)) { + CAmount ddAmount{0}; + // Validate change with the creating-output parser used by later + // spends, circulation indexing, and UTXO reconstruction. + if (!ExtractDDAmountFromTxRef(MakeTransactionRef(tx), COutPoint{tx.GetHash(), static_cast(outIdx)}, ddAmount) || + !AddDDAmount(totalDDOutputs, ddAmount)) { + return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-redeem-dd-output-amount", + "Redemption DD change requires an unambiguous serialized amount"); + } + } else if (foundOpReturn && ddAmountFromOpReturn > 0) { // Use the authoritative amount from OP_RETURN if (!AddDDAmount(totalDDOutputs, ddAmountFromOpReturn)) { return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-redeem-dd-output-amount", @@ -2131,16 +2258,10 @@ bool ValidateRedemptionTransaction(const CTransaction& tx, LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Output %d - DD P2TR: %lld cents (from OP_RETURN)\n", outIdx, (long long)ddAmountFromOpReturn); } else { - // Fallback to metadata registry (may be stale) - CAmount ddAmount = 0; - if (ExtractDDAmount(output.scriptPubKey, ddAmount)) { - if (!AddDDAmount(totalDDOutputs, ddAmount)) { - return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-redeem-dd-output-amount", - "Redemption DD output amount exceeds per-output serialization bounds"); - } - LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Output %d - DD P2TR: %lld cents (from metadata)\n", - outIdx, (long long)ddAmount); - } + // A change output with no serialized amount adds nothing to the + // output total. Below Thaw Day this matches every node that has + // no local registry entry for the script. + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Output %d - DD P2TR without a serialized amount\n", outIdx); } } } @@ -2185,14 +2306,14 @@ bool ValidateRedemptionTransaction(const CTransaction& tx, const std::optional resolvedHealth = ResolveCanonicalHealth(ctx, "redemption"); if (!resolvedHealth.has_value()) { + if (IsThawDayActive(ctx.params.GetConsensus(), ctx.nHeight)) return state.Error("DigiDollar state not ready: canonical health unavailable"); return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-system-health", "Missing deterministic system health for redemption validation"); } - ValidationContext redemptionCtx(ctx.nHeight, ctx.oraclePriceMicroUSD, *resolvedHealth, - ctx.params, ctx.coins, ctx.skipOracleValidation, - ctx.txLookup, ctx.mempool); + ValidationContext redemptionCtx(ctx); + redemptionCtx.systemCollateral = *resolvedHealth; // Validate redemption path (NORMAL or ERR) based on deterministic system health if (redemptionCtx.systemCollateral < 100) { @@ -2218,7 +2339,7 @@ bool ValidateRedemptionTransaction(const CTransaction& tx, return false; } - LogPrintf("DigiDollar: Redemption transaction %s validated successfully\n", tx.GetHash().ToString()); + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Redemption transaction %s validated successfully\n", tx.GetHash().ToString()); return true; } @@ -2240,15 +2361,19 @@ bool ValidateNormalRedemptionConditions(const CTransaction& tx, ctx.nHeight, tx.nLockTime)); } - // Check if ERR (Emergency Redemption Ratio) is active (skip for historical blocks) - // Use systemCollateral from context (percentage, where 100 = 100% collateralized) - if (!ctx.skipOracleValidation && ctx.systemCollateral < 100) { + // A redemption that burns only the original amount is not allowed while + // system health is below 100 per cent; the owner has to burn extra + // DigiDollars instead. Below the Thaw Day height this check is skipped when + // the oracle checks are skipped, exactly as it always has been. From the + // Thaw Day height on it always runs, so a node still downloading blocks and + // a node that is up to date give the same answer for the same block. + if (!OracleChecksSkipped(ctx) && ctx.systemCollateral < 100) { LogPrintf("DigiDollar: Normal redemption rejected - ERR active (system health: %d%%)\n", ctx.systemCollateral); return state.Invalid(TxValidationResult::TX_CONSENSUS, "redemption-err-active", strprintf("ERR active - system health %d%% (normal redemptions blocked)", ctx.systemCollateral)); } - LogPrintf("DigiDollar: Normal redemption validation passed (height: %d >= locktime: %d, system health: %d%%)\n", + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Normal redemption validation passed (height: %d >= locktime: %d, system health: %d%%)\n", ctx.nHeight, tx.nLockTime, ctx.systemCollateral); return true; } @@ -2261,7 +2386,7 @@ bool ValidateEmergencyRedemptionConditions(const CTransaction& tx, // - User must burn MORE DD than originally minted to get FULL collateral back // - Example: At 80% ratio, burn 125 DD to get back collateral for 100 DD position - LogPrintf("DigiDollar: Validating emergency (ERR) redemption conditions\n"); + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Validating emergency (ERR) redemption conditions\n"); // IMPORTANT: Check structural requirements FIRST before checking ERR activation // This ensures more specific error messages for invalid transactions @@ -2297,25 +2422,33 @@ bool ValidateEmergencyRedemptionConditions(const CTransaction& tx, double errRatio = ERR::EmergencyRedemptionRatio::CalculateERRAdjustment(systemHealth); double ddMultiplier = 1.0 / errRatio; // How much MORE DD is needed - LogPrintf("DigiDollar: ERR redemption - system health: %d%%, ratio: %.2f, DD burn multiplier: %.2fx\n", + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: ERR redemption - system health: %d%%, ratio: %.2f, DD burn multiplier: %.2fx\n", systemHealth, errRatio, ddMultiplier); // DD burn accounting is enforced by ValidateCollateralReleaseAmount(), which // has the collateral mint metadata needed to calculate the required ERR burn. - LogPrintf("DigiDollar: ERR redemption conditions validated - user must burn %.1f%% extra DD to get full collateral\n", + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: ERR redemption conditions validated - user must burn %.1f%% extra DD to get full collateral\n", (ddMultiplier - 1.0) * 100); return true; } bool ValidateCollateralReleaseAmount(const CTransaction& tx, - const ValidationContext& ctx, + const ValidationContext& input_ctx, CAmount ddBurned, TxValidationState& state) { + ValidationContext ctx(input_ctx); + const bool canonical = IsThawDayActive(ctx.params.GetConsensus(), ctx.nHeight); + if (canonical) { + const auto health = ResolveCanonicalHealth(ctx, "redemption"); + if (!health) return state.Error("DigiDollar state not ready: canonical health unavailable"); + ctx.systemCollateral = *health; + } // Validate collateral release amount for redemption transactions // Must verify that DGB released is proportional to DD burned if (ctx.coins == nullptr) { + if (canonical) return state.Error("DigiDollar state not ready: collateral UTXO view unavailable"); // No UTXO access — cannot validate collateral proportionality. // This path should not be hit during ConnectBlock (always has coins view). LogPrintf("DigiDollar: WARNING - No coins view for collateral release validation\n"); @@ -2358,8 +2491,16 @@ bool ValidateCollateralReleaseAmount(const CTransaction& tx, "Redemption input 0 must spend the canonical DigiDollar collateral vault output"); } + if (canonical) { + CanonicalVault vault; + std::string reason; + const auto result = LookupCanonicalVault(tx.vin[0].prevout, collateralCoin, ctx.params.GetConsensus(), + ContextTransactionLookup(ctx), vault, reason); + if (result == VaultLookupResult::NOT_READY) return state.Error(reason); + if (result != VaultLookupResult::VAULT) return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-collateral-release-not-vault"); + } CTransactionRef collateralPrevTx; - if (LookupPreviousTransaction(tx.vin[0].prevout, collateralCoin.nHeight, ctx, collateralPrevTx) && + if (!canonical && LookupPreviousTransaction(tx.vin[0].prevout, collateralCoin.nHeight, ctx, collateralPrevTx) && !IsMintCollateralOutput(collateralPrevTx, tx.vin[0].prevout.n)) { LogPrintf("DigiDollar: Redemption rejected - input 0 is not the canonical collateral output of its creating mint (%s:%u)\n", tx.vin[0].prevout.hash.ToString(), tx.vin[0].prevout.n); @@ -2453,7 +2594,7 @@ bool ValidateCollateralReleaseAmount(const CTransaction& tx, }; // Try txindex (authoritative — reads creating tx from indexed database) - if (!found && g_txindex) { + if (!canonical && !found && g_txindex) { uint256 block_hash; CTransactionRef prev_tx; if (g_txindex->FindTx(tx.vin[0].prevout.hash, block_hash, prev_tx)) { @@ -2477,21 +2618,6 @@ bool ValidateCollateralReleaseAmount(const CTransaction& tx, } } - // Last resort for legacy/unit-test contexts that lack tx lookup. This is intentionally - // after authoritative sources because script metadata can be overwritten by failed mints - // that reuse the same collateral script. - if (!found) { - ScriptMetadata metadata; - if (GetScriptMetadata(collateralCoin.out.scriptPubKey, metadata) && - metadata.type == DigiDollar::ScriptType::COLLATERAL_LOCK && - metadata.ddAmount > 0 && - metadata.lockHeight >= 0) { - originalDDMinted = metadata.ddAmount; - originalLockHeight = metadata.lockHeight; - found = true; - } - } - if (!found || originalDDMinted <= 0) { // SECURITY: REJECT if we cannot determine original DD amount. // A consensus rule must never be silently bypassed. @@ -2588,8 +2714,16 @@ bool ValidateCollateralReleaseAmount(const CTransaction& tx, // DD-lookalike fee input would be rejected on full nodes but accepted on pruned // nodes. Mirrors the pre-floor gate in SpendsDigiDollarCollateralVault(). bool isCollateral = false; + if (canonical) { + CanonicalVault vault; + std::string reason; + const auto result = LookupCanonicalVault(tx.vin[i].prevout, coin, ctx.params.GetConsensus(), + ContextTransactionLookup(ctx), vault, reason); + if (result == VaultLookupResult::NOT_READY) return state.Error(reason); + isCollateral = result == VaultLookupResult::VAULT; + } CTransactionRef prev_tx; - if ((dd_activation_height <= 0 || + if (!canonical && (dd_activation_height <= 0 || coin.nHeight >= static_cast(dd_activation_height)) && LookupPreviousTransaction(tx.vin[i].prevout, coin.nHeight, ctx, prev_tx)) { isCollateral = IsMintCollateralOutput(prev_tx, tx.vin[i].prevout.n); @@ -2613,7 +2747,7 @@ bool ValidateCollateralReleaseAmount(const CTransaction& tx, const __int128 maxAllowedDGBOutputs = static_cast<__int128>(allowedRelease) + static_cast<__int128>(totalFeeInputs); - LogPrintf("DigiDollar: Collateral release check - totalOutputs: %lld, feeInputs: %lld, allowedCollateral: %lld\n", + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Collateral release check - totalOutputs: %lld, feeInputs: %lld, allowedCollateral: %lld\n", (long long)totalDGBOutputs, (long long)totalFeeInputs, (long long)allowedRelease); @@ -2638,7 +2772,7 @@ bool ValidateCollateralReleaseAmount(const CTransaction& tx, (long long)totalDGBOutputs, (long long)allowedRelease)); } - LogPrintf("DigiDollar: Collateral release validated - outputs: %lld, allowed collateral: %lld, fee inputs: %lld, ddBurned: %lld/%lld (original: %lld)\n", + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Collateral release validated - outputs: %lld, allowed collateral: %lld, fee inputs: %lld, ddBurned: %lld/%lld (original: %lld)\n", (long long)totalDGBOutputs, (long long)allowedRelease, (long long)totalFeeInputs, (long long)ddBurned, (long long)requiredDDBurn, (long long)originalDDMinted); @@ -2656,7 +2790,7 @@ bool ValidateScriptPathSpending(const CTransaction& tx, // The Schnorr signature verification is handled by the consensus engine // No additional validation needed here for Phase 1 - LogPrintf("DigiDollar: Script path spending validation - using key-path (Schnorr), validation passed\n"); + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Script path spending validation - using key-path (Schnorr), validation passed\n"); return true; // Allow key-path spending (standard Taproot) } @@ -2667,6 +2801,23 @@ bool ValidateScriptPathSpending(const CTransaction& tx, bool ValidateDigiDollarTransaction(const CTransaction& tx, const ValidationContext& ctx, TxValidationState& state) { + const bool canonical = IsThawDayActive(ctx.params.GetConsensus(), ctx.nHeight); + if (canonical) { + if (!ContextHealthState(ctx)) return state.Error("DigiDollar state not ready: verify accounting for the candidate parent"); + bool spends{false}; + std::string error; + if (!CheckCanonicalVaultInputs(tx, ctx, spends, error)) return state.Error(error); + if (HasDigiDollarMarker(tx) && ctx.coins) { + for (const auto& input : tx.vin) { + Coin coin; + if (!ctx.coins->GetCoin(input.prevout, coin) || coin.out.nValue != 0 || + !IsCanonicalP2TROutput(coin.out.scriptPubKey) || coin.nHeight == MEMPOOL_HEIGHT) continue; + CAmount amount{0}; + if (!ReadCanonicalTokenAmount(input.prevout, coin, ctx, amount, state)) return false; + } + } + } + // Check if this is a DD transaction if (!HasDigiDollarMarker(tx)) { if (SpendsDigiDollarCollateralVault(tx, ctx)) { @@ -2707,7 +2858,8 @@ bool ValidateDigiDollarTransaction(const CTransaction& tx, return state.Invalid(TxValidationResult::TX_CONSENSUS, "minting-blocked-during-err"); } - if (txType == DD_TX_MINT && ctx.oraclePriceMicroUSD > 0 && + if (!IsThawDayActive(ctx.params.GetConsensus(), ctx.nHeight) && + txType == DD_TX_MINT && ctx.oraclePriceMicroUSD > 0 && Volatility::VolatilityMonitor::WouldCandidateFreezeMinting(ctx.oraclePriceMicroUSD)) { LogPrintf("DigiDollar: Mint candidate oracle price would cross volatility freeze threshold " "(candidate=%lld)\n", @@ -2805,7 +2957,7 @@ bool ValidateCollateralOutput(const CTxOut& output, const CTransaction& tx, // - Check for valid redemption paths // - Validate timelock parameters - LogPrintf("DigiDollar: Collateral output validation passed - Value: %d DGB\n", + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Collateral output validation passed - Value: %d DGB\n", output.nValue / COIN); return true; @@ -2833,7 +2985,7 @@ bool ValidateDDOutput(const CTxOut& output, const CTransaction& tx, // (P2TR with value=0 indicates DD token output) // Phase 2 will use UTXO database tracking for proper identification - LogPrintf("DigiDollar: DD output validation passed\n"); + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: DD output validation passed\n"); return true; } @@ -2875,7 +3027,7 @@ int64_t ExtractLockTime(const CScript& script) { int64_t maxLock = 10 * 365 * 24 * 60 * 4; // 10 years if (lockValue >= minLock && lockValue <= maxLock) { - LogPrintf("DigiDollar: Extracted lock time: %d blocks (~%d days)\n", + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Extracted lock time: %d blocks (~%d days)\n", lockValue, lockValue / (24 * 60 * 4)); return lockValue; } @@ -2945,7 +3097,15 @@ CAmount GetSystemCollateralRatio() { bool ValidateERRRedemption(const CTransaction& tx, const ValidationContext& ctx, TxValidationState& state) { - LogPrintf("DigiDollar: Validating ERR redemption transaction\n"); + const bool canonical = IsThawDayActive(ctx.params.GetConsensus(), ctx.nHeight); + if (canonical) { + const auto health = ResolveCanonicalHealth(ctx, "redemption"); + if (!health) return state.Error("DigiDollar state not ready: canonical health unavailable"); + if (!ERR::EmergencyRedemptionRatio::ShouldActivateERR(*health)) + return state.Invalid(TxValidationResult::TX_CONSENSUS, "err-not-active"); + return ValidateRedemptionTransaction(tx, ctx, state); + } + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Validating ERR redemption transaction\n"); // Check if ERR should be active based on system health if (!DigiDollar::ERR::EmergencyRedemptionRatio::ShouldActivateERR(ctx.systemCollateral)) { @@ -2956,7 +3116,7 @@ bool ValidateERRRedemption(const CTransaction& tx, // Get current ERR state DigiDollar::ERR::ERRState errState = DigiDollar::ERR::EmergencyRedemptionRatio::GetCurrentState(); - if (!errState.isActive) { + if (!canonical && !errState.isActive) { LogPrintf("DigiDollar: ERR state not active\n"); return state.Invalid(TxValidationResult::TX_CONSENSUS, "err-state-inactive"); } @@ -3018,18 +3178,26 @@ bool ValidateERRRedemption(const CTransaction& tx, return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-err-no-collateral-return"); } - LogPrintf("DigiDollar: ERR redemption validation passed - DD burned: %lld, required: %lld, collateral returned: %lld\n", + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: ERR redemption validation passed - DD burned: %lld, required: %lld, collateral returned: %lld\n", (long long)ddInputAmount, (long long)requiredDDBurn, (long long)collateralOutputAmount); return true; } bool ShouldBlockMintingDuringERR(const ValidationContext& ctx) { + if (IsThawDayActive(ctx.params.GetConsensus(), ctx.nHeight)) { + const auto health = ResolveCanonicalHealth(ctx, "mint"); + return !health || ERR::EmergencyRedemptionRatio::ShouldActivateERR(*health); + } // Check if ERR is currently active, passing oracle price from validation context // so ShouldBlockMinting can calculate system health without querying the global oracle. return DigiDollar::ERR::EmergencyRedemptionRatio::ShouldBlockMinting(ctx.oraclePriceMicroUSD); } bool ShouldBlockNormalRedemptionsDuringERR(const ValidationContext& ctx) { + if (IsThawDayActive(ctx.params.GetConsensus(), ctx.nHeight)) { + const auto health = ResolveCanonicalHealth(ctx, "redemption"); + return !health || ERR::EmergencyRedemptionRatio::ShouldActivateERR(*health); + } // Check if ERR is currently active DigiDollar::ERR::ERRState errState = DigiDollar::ERR::EmergencyRedemptionRatio::GetCurrentState(); return errState.isActive; diff --git a/src/digidollar/validation.h b/src/digidollar/validation.h index c81a22eba9a..87c2536710f 100644 --- a/src/digidollar/validation.h +++ b/src/digidollar/validation.h @@ -10,6 +10,7 @@ #include