diff --git a/.gitattributes b/.gitattributes index c9cf4a7d9cd..7ed0ab9b1a4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,4 @@ src/clientversion.cpp export-subst + +# Keep executable build helpers usable after a Windows checkout or WSL copy. +*.sh text eol=lf diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 2efb6d23d61..e471dbb6cc7 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -69,11 +69,11 @@ jobs: # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -94,18 +94,18 @@ jobs: # â„šī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - if: matrix.build-mode == 'manual' - name: Quick build for CodeQL analysis + name: Build wallet and tests for CodeQL analysis shell: bash run: | - # Quick minimal build just for CodeQL analysis - # Match the simple Ubuntu 22.04 CI configuration + # C/C++ manual mode analyzes only compiled translation units. Keep + # wallet and tests enabled so Paymaster persistence, signing and RPC + # orchestration are present in the CodeQL database. ./autogen.sh - ./configure --disable-wallet --disable-tests --disable-bench --disable-fuzz --disable-fuzz-binary \ + ./configure --disable-bench --disable-fuzz --disable-fuzz-binary \ --with-gui=no --disable-man --disable-zmq --without-utils --without-daemon - # Build all available libraries for analysis - make -j$(nproc) -C src + make -j$(nproc) -C src test/test_digibyte - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@v4 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 00000000000..ca9691f517f --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,22 @@ +# 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. + +name: Dependency Review + +on: + pull_request: + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + - name: Reject newly introduced vulnerable dependencies + uses: actions/dependency-review-action@v4 + with: + fail-on-severity: high diff --git a/.github/workflows/paymaster-security.yml b/.github/workflows/paymaster-security.yml new file mode 100644 index 00000000000..87814d61771 --- /dev/null +++ b/.github/workflows/paymaster-security.yml @@ -0,0 +1,166 @@ +# 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. + +name: Paymaster Security + +on: + pull_request: + paths: + - '.github/workflows/paymaster-security.yml' + - 'src/dandelion.cpp' + - 'src/net.cpp' + - 'src/net.h' + - 'src/net_processing.cpp' + - 'src/netbase.cpp' + - 'src/netbase.h' + - 'src/paymaster/**' + - 'src/protocol.cpp' + - 'src/protocol.h' + - 'src/rpc/net.cpp' + - 'src/rpc/digidollar.cpp' + - 'src/Makefile.am' + - 'src/Makefile.test.include' + - 'src/test/fuzz/paymaster_*.cpp' + - 'src/test/paymaster_*.cpp' + - 'src/wallet/paymaster*.cpp' + - 'src/wallet/paymaster*.h' + - 'src/wallet/db.h' + - 'src/wallet/digidollarwallet.cpp' + - 'src/wallet/rpc/paymaster*.cpp' + - 'src/wallet/rpc/paymaster*.h' + - 'src/wallet/spend.cpp' + - 'src/wallet/test/fuzz/paymaster_*.cpp' + - 'src/wallet/test/paymaster_*.cpp' + - 'src/wallet/walletdb.cpp' + - 'src/wallet/walletdb.h' + - 'test/functional/*paymaster*.py' + - 'test/functional/test_framework/paymaster.py' + schedule: + - cron: '17 3 * * 0' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + asan-ubsan-unit: + name: ASan and UBSan Paymaster unit tests + runs-on: ubuntu-22.04 + timeout-minutes: 90 + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + autoconf automake bsdmainutils build-essential clang libboost-dev \ + libdb++-dev libevent-dev libminiupnpc-dev libnatpmp-dev \ + libsqlite3-dev libtool pkg-config python3 systemtap-sdt-dev + + - name: Configure sanitizer build + env: + CC: clang + CXX: clang++ + run: | + ./autogen.sh + ./configure \ + --with-sanitizers=address,undefined \ + --disable-bench --disable-fuzz --disable-fuzz-binary \ + --with-gui=no --disable-man --disable-zmq \ + --without-utils --without-daemon + + - name: Build unit test binary + run: make -j2 -C src test/test_digibyte + + - name: Run Paymaster unit tests + env: + ASAN_OPTIONS: detect_leaks=1:halt_on_error=1 + LSAN_OPTIONS: suppressions=${{ github.workspace }}/test/sanitizer_suppressions/lsan + UBSAN_OPTIONS: suppressions=${{ github.workspace }}/test/sanitizer_suppressions/ubsan:print_stacktrace=1:halt_on_error=1:report_error_type=1 + run: | + set -euo pipefail + # Include newly split suites (including wallet load/maintenance) + # automatically instead of maintaining a second suite inventory. + ./src/test/test_digibyte \ + '--run_test=paymaster_*' --log_level=test_suite --report_level=short \ + 2>&1 | tee paymaster-sanitizer.log + + - name: Upload test log + if: always() + uses: actions/upload-artifact@v4 + with: + name: paymaster-sanitizer-test-log + path: paymaster-sanitizer.log + if-no-files-found: ignore + + sanitizer-fuzz: + name: Scheduled Paymaster sanitizer fuzzing + if: github.event_name != 'pull_request' + runs-on: ubuntu-22.04 + timeout-minutes: 120 + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + autoconf automake bsdmainutils build-essential clang libboost-dev \ + libdb++-dev libevent-dev libminiupnpc-dev libnatpmp-dev \ + libsqlite3-dev libtool pkg-config python3 systemtap-sdt-dev + + - name: Configure fuzz build + env: + CC: clang + CXX: clang++ + run: | + ./autogen.sh + ./configure \ + --enable-fuzz \ + --with-sanitizers=address,fuzzer,undefined \ + --disable-bench --with-gui=no --disable-man --disable-zmq \ + --without-utils --without-daemon + + - name: Build fuzz binary + run: make -j2 -C src test/fuzz/fuzz + + - name: Run bounded Paymaster campaigns + env: + ASAN_OPTIONS: detect_leaks=1:halt_on_error=1 + LSAN_OPTIONS: suppressions=${{ github.workspace }}/test/sanitizer_suppressions/lsan + UBSAN_OPTIONS: suppressions=${{ github.workspace }}/test/sanitizer_suppressions/ubsan:print_stacktrace=1:halt_on_error=1:report_error_type=1 + run: | + set -euo pipefail + for target in \ + paymaster_wire_envelopes \ + paymaster_stateful_security \ + paymaster_pool_lifecycle \ + paymaster_persistence_records + do + corpus="${RUNNER_TEMP}/paymaster-corpus/${target}" + mkdir -p "$corpus" + FUZZ="$target" ./src/test/fuzz/fuzz \ + -max_total_time=300 -timeout=30 "$corpus" + done + + - name: Upload reproducer artifacts + if: failure() + uses: actions/upload-artifact@v4 + with: + name: paymaster-fuzz-reproducers + path: | + crash-* + leak-* + timeout-* + oom-* + if-no-files-found: ignore diff --git a/.github/workflows/sbom.yml b/.github/workflows/sbom.yml new file mode 100644 index 00000000000..e7ae652c656 --- /dev/null +++ b/.github/workflows/sbom.yml @@ -0,0 +1,34 @@ +# 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. + +name: Export SPDX SBOM + +on: + release: + types: [published] + workflow_dispatch: + +permissions: + contents: read + +jobs: + export: + runs-on: ubuntu-latest + steps: + - name: Export repository dependency graph as SPDX + env: + GH_TOKEN: ${{ github.token }} + run: | + gh api \ + -H 'Accept: application/vnd.github+json' \ + -H 'X-GitHub-Api-Version: 2026-03-10' \ + "/repos/${GITHUB_REPOSITORY}/dependency-graph/sbom" \ + --jq '.sbom' > digibyte-dependency-graph.spdx.json + + - name: Upload SPDX artifact + uses: actions/upload-artifact@v4 + with: + name: digibyte-dependency-graph-sbom + path: digibyte-dependency-graph.spdx.json + if-no-files-found: error diff --git a/.gitignore b/.gitignore index 0b089e7e26a..bc84e201f03 100644 --- a/.gitignore +++ b/.gitignore @@ -187,3 +187,5 @@ RC43_FIX_WORK_PROMPT.md Z_RED_HORNET_FINAL_PROMPT.md Z_RED_HORNET_v2.md Z_DOC_VALIDATION_v9.26.2_PROMPT.md + +.ai \ No newline at end of file diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 86d536d457a..97d2f51f467 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,11 +1,34 @@ # DigiByte Blockchain Architecture -**DigiByte Core v9.26.2 (Based on Bitcoin Core v26.2)** +**DigiByte Core v9.26.5 (Based on Bitcoin Core v26.2)** *Comprehensive Technical Documentation* -*Last Updated: 2026-05-20* +*Last Updated: 2026-07-28* *Validation Status: Spot-validated against live `feature/digidollar-v1` code surfaces listed in Appendix D* --- +## Paymaster architecture in this branch + +The v9.26.6rc2 integration keeps the provider panel in +`src/qt/paymasterwidget.cpp`, behind a small embedding interface. The concrete +`PaymasterSendWidget` owns fee selection and client-session presentation while +the original send form retains its editable inputs and ordinary transfer path. +Paymaster record codecs remain `WalletBatch` methods in `wallet/paymasterdb.cpp`; +`PaymasterStore` still owns atomic state transitions. Free functions in +`wallet/rpc/paymaster_send.cpp` isolate the existing send RPC integration without +adding RPC endpoints, persisted state, or another service layer. See the +[integration notes](doc/digidollar-paymaster-v9.26.6rc2-integration.md) for scope +and pending verification. + +For the Paymaster implementation at `bd270044c1`, see the +[developer starting point](PAYMASTER.md) and +[implementation reference](doc/digidollar-paymaster-implementation.md). +It adds collaborative DD transfer construction, wallet authorization and +persistence, provider discovery, and direct P2P sessions. The resulting +transaction is an ordinary `DD_TX_TRANSFER`. The broader branch also contains +arithmetic portability changes in consensus source, which require separate +equivalence review. Verification is tracked in the +[Paymaster release gate](doc/digidollar-paymaster-release-gate.md). + ## Executive Summary DigiByte is a decentralized, open-source blockchain focused on speed, security, and decentralization. This document provides a complete architectural overview of the DigiByte Core implementation, validated against the actual codebase. @@ -166,6 +189,12 @@ digibyte/ │ │ ├── node.cpp # Oracle node operations │ │ └── mock_oracle.cpp # Testing mock oracle │ │ +│ ├── paymaster/ # Non-consensus DD fee-funding protocol +│ │ ├── txbuilder.cpp # Deterministic collaborative transfer builder +│ │ ├── protocol.cpp # Intent, quote, submit, and result validation +│ │ ├── manager.cpp # Discovery and direct-message coordination +│ │ └── provider.cpp # Provider policy, fees, and pool readiness +│ │ │ ├── rpc/ # RPC commands │ │ ├── blockchain.cpp # Chain queries │ │ ├── mining.cpp # Mining commands @@ -361,16 +390,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 +499,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 +522,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 +719,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 +737,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 @@ -757,6 +790,7 @@ PeerManagerImpl (Message Processing) | MANUAL | -addnode connections | 8 | | INBOUND | Peer-initiated | Up to 125 total | | FEELER | Quality testing | 1 | +| PAYMASTER | Short-lived isolated Paymaster session | At most 1 in high-privacy mode | ### 9.3 Dandelion++ Privacy @@ -814,6 +848,16 @@ DANDELION_FLUFF = 10 // 10% immediate fluff probability | **ORACLECONSENSUS** / **ORACLEATTESTATION** | MuSig2 oracle consensus/attestation coordination | | **ORACLEMUSIGNONCE** / **ORACLEMUSIGCONTEXT** / **ORACLEMUSIGPARTIALSIG** | MuSig2 nonce/context/partial-signature exchange | | **ORACLEHEARTBEAT** | Oracle liveness heartbeat | +| **SENDPMASTERS** / **PMANNOUNCE** / **GETPMASTERS** | Paymaster discovery and bounded announcement exchange | +| **PMCAPREQ** / **PMCAPRESP** | Selected-provider operational capacity proof | +| **PMQUOTEREQ** / **PMQUOTERESP** | Bound payment intent and provider quote | +| **PMSUBMIT** / **PMRESULT** | User-signed PSBT submission and monotonic provider result | + +All Paymaster message handling is gated by DigiDollar activation. On an +isolated connection, `PMCAPREQ`, `PMQUOTEREQ`, `PMSUBMIT`, `PMRECOVERYREQ`, +and `PMRECOVERYSUBMIT` flow client-to-provider; their response counterparts +flow provider-to-client. `GETPMASTERS` returns at most 16 entries from a +rotating bounded directory view. --- @@ -899,6 +943,20 @@ class DigiDollarWallet { }; ``` +Paymaster state is wallet scoped. `PaymasterStore` persists sessions, exact +provider attempts, input reservations, final provider commits, self-recovery +transactions, local reputation, sponsorship authorizations, and permanent +idempotency tombstones in the wallet database. The provider store additionally +persists pool-successor provenance, wallet-local liquidity targets, restartable +maintenance records, finite maintenance-fee accounting, and the latest reviewed +carrier-withdrawal plan. Paymaster reservations also use the ordinary wallet +lock set, so regular DD and DGB coin selection cannot spend active pool or +reserved inputs. Immediately before a new provider signature, all committed +template inputs are rechecked against chainstate and mempool. If client +recovery has already spent a user input, the provider atomically persists a +signed `REJECTED` result and releases only the still-unused pool entries bound +to that commit key; no provider signature or final commit is created. + --- ## 11. RPC Interface @@ -926,6 +984,7 @@ Authentication Methods: | **rawtransaction** | createrawtransaction, signrawtransaction... | rawtransaction.cpp | | **wallet** | getnewaddress, sendtoaddress, listunspent... | wallet/rpc/*.cpp | | **digidollar** | mintdigidollar, redeemdigidollar, getoracleprice... | digidollar.cpp | +| **paymaster** | listpaymasters, getpaymasteroffers, session and provider operations... | rpc/digidollar.cpp, wallet/rpc/paymaster*.cpp | ### 11.3 DigiByte-Specific RPC @@ -941,6 +1000,14 @@ Authentication Methods: | `validateddaddress` (wallet) | `wallet/rpc/wallet.cpp` | Validate DD address | | `mintdigidollar` / `senddigidollar` / `redeemdigidollar` (wallet) | `wallet/rpc/wallet.cpp` | DigiDollar mint/transfer/redeem | | `setmockoracleprice` / `getmockoracleprice` / `simulatepricevolatility` / `enablemockoracle` | `rpc/digidollar.cpp` | Regtest-only oracle helpers | +| `listpaymasters` | `rpc/digidollar.cpp` | List locally verified, unexpired announcements | +| `getpaymasteroffers` / `requestpaymasterquote` | `wallet/rpc/paymaster_discovery.cpp` | Select offers and advance a persistent client session | +| `getdigidollarsendsession` / `resolvepaymastersession` | `wallet/rpc/paymaster_client.cpp` | Inspect, retry, fall back, or recover a session safely | +| `createpaymasteridentity` / `setpaymasterpolicy` / `preparepaymasterpool` | `wallet/rpc/paymaster_provider.cpp` | Configure a wallet-scoped provider and its isolated pools | +| `setpaymasterliquiditypolicy` / `getpaymasterliquiditystatus` | `wallet/rpc/paymaster_provider.cpp` | Configure finite automatic pool-maintenance targets/budgets and inspect confirmed, pending, or missing capacity | +| `withdrawpaymastercarrier` | `wallet/rpc/paymaster_provider.cpp` | Preview and execute wallet-owned carrier-excess consolidation or release one stopped-provider carrier slot | +| `createrestrictedpaymasterdescriptor` | `wallet/rpc/paymaster_provider.cpp` | Create a non-gossiped provider-signed restricted sponsorship descriptor | +| `startpaymaster` / `stoppaymaster` / `getpaymasterinfo` | `wallet/rpc/paymaster_runtime.cpp`, `wallet/rpc/paymaster_provider.cpp` | Operate and inspect an explicitly enabled provider | Both `sendoracleprice` and `submitoracleprice` are absent from the source tree — `sendoracleprice` was removed as a fake-price-injection vulnerability and `submitoracleprice` never existed. Oracle prices come exclusively from live exchange aggregation. See `REPO_MAP_DIGIDOLLAR.md` for the complete RPC inventory. @@ -1154,6 +1221,105 @@ This avoids rescanning the entire UTXO set for every health check. The ERR syste **Result:** All nodes see identical network statistics, updated incrementally per block. +### 12.7 Paymaster Network (non-consensus) + +**Specification:** [`DIGIDOLLAR_PAYMASTER_NETWORK_PROPOSAL_EN.md`](DIGIDOLLAR_PAYMASTER_NETWORK_PROPOSAL_EN.md) +**Key files:** `src/paymaster/`, `src/wallet/paymaster*.{cpp,h}`, +`src/wallet/rpc/paymaster*.{cpp,h}` + +The Paymaster Network lets a wallet with confirmed DD but no spendable DGB +construct a normal `DD_TX_TRANSFER` with a provider supplying the DGB miner-fee +inputs. It introduces no transaction type, script rule, service bit, chain +parameter, or privileged validator role. + +``` +bounded announcement gossip + ↓ +local admission-proof validation and offer selection + ↓ +isolated BIP324-v2 PAYMASTER connection to one selected provider + ↓ +authenticated Capacity V5 proof for one exact provider resource set + ↓ +payment intent → resource-bound quote → local client manifest/firewall + ↓ +provider manifest/budget firewall → role-limited signatures + ↓ +full witness/script/mempool validation → atomic exact raw-tx commit + ↓ +ordinary DD_TX_TRANSFER broadcast and wallet confirmation/reorg tracking + ↓ +wallet-owned carrier/DGB successors → confirmation-gated pool reuse + ↓ +finite-budget maintenance only when configured targets remain missing +``` + +- `-paymaster=1` enables discovery, relay, and client support. Provider + operation additionally requires an eligible descriptor wallet, a persisted + BIP86 identity and policy, prepared admission/operational pools, explicit + enablement, and `startpaymaster`. +- `USER_PAID` and public or restricted `SPONSORED` offers use the same + transaction protocol. Restricted capabilities are payment-bound and their + durable records retain hashes rather than plaintext authorization material. +- Standard privacy requires v2 transport with no v1 fallback. High privacy is + onion-only, uses Tor stream isolation, permits one provider attempt, and has + no clearnet fallback. This reduces metadata; it does not provide anonymity. +- Persistent `request_id` sessions prevent duplicate payment. Once user + signatures make a session ambiguous, timeout does not release its inputs; + recovery uses exact-provider retry or a same-input `cancel_to_self` spend + funded by a distinct Capacity-validated recovery provider when the client + has no DGB. +- Paymaster is current-only: wire requests and responses must use protocol V5, + and every intent, quote, manifest, recovery object, session, attempt, safety + ledger, pool entry, and maintenance record must use its exact current + persisted version. There is no automatic migration or executable legacy + fallback. An unsupported Paymaster record stops only the affected Paymaster + operation and is never erased or rewritten implicitly. +- The signed Capacity proof is verified against identity, BIP86 control proofs + and live chainstate before the client discloses its intent, DD outpoints, or + restricted capability. The quote must consume the exact resource snapshot. +- Client and provider signatures are gated by immutable wallet-local + authorization manifests. Finite provider and client safety policies reserve + worst-case fees atomically, persist rolling limits across restart, and make + public sponsorship bounded rather than unlimited. +- Client requests normally treat the RPC amount as the recipient amount. An + explicit wallet-local exact-outflow mode instead derives a recipient amount + whose value plus the existing rounded service fee equals the requested gross + amount. The gross amount, fee treatment, exact recipient/fee split, and input + snapshot are persisted and bound by the client authorization manifest. A + wallet sweep selects only confirmed ordinary spendable DD and fails closed if + that snapshot changes; it does not alter the Paymaster wire protocol. +- A final result is accepted only after reconstructing the trusted PSBT and + verifying the non-witness transaction, txid, wtxid, all witnesses and scripts + with the real prevouts, followed by mempool preflight when it is not already + known. Final transitions and their economic reservations are monotonic and + atomic. +- Wallet lifecycle maintenance runs the same persistent reconciliation every + 30 seconds, in addition to startup and chain-tip notifications. Expired + unsigned quotes and Capacity reservations are therefore released through the + atomic store paths even when no further Paymaster RPC arrives and no block is + connected. +- A successful provider commit registers the exact wallet-owned carrier return + and sufficiently large DGB change as provenance-bound pending successors. + Confirmation makes them available; reorg or conflict returns them to a safe + pending/invalid state. Startup reconciliation can recover still-unspent + historical successors without guessing by amount or output position. +- A wallet-local `ProviderLiquidityPolicy` defines admission/operational DGB and + carrier targets plus finite maintenance-fee ceilings per transaction, hour, + and day. A started automatic provider reuses free successors first, resumes + durable maintenance idempotently, creates only missing slots, and accepts no + new work until the target is confirmed. Paid maintenance requires explicit + operator approval; zero limits never mean unlimited. +- Carrier fees can accumulate inside operational carrier outputs. The + preview-bound `withdrawpaymastercarrier` RPC can retain exactly 1.00 DD per + carrier and combine the excess, or release one carrier locally while reducing + the target. Reserved or authorized carriers remain protected. +- Client/provider readiness requires an unpruned node, a fully synchronized + `txindex=1`, complete DD transaction data, and an unlocked wallet for new + signatures. Client quote creation fails before intent processing when these + archival requirements are unavailable. Ordinary relay and validation do not + require client or provider readiness. + --- ## 13. Oracle System @@ -1206,10 +1372,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: @@ -1398,11 +1564,12 @@ debug=validation ### Unit Tests - `src/test/` - C++ unit tests (Boost.Test); see `REPO_MAP_DIGIDOLLAR.md` for the granular DigiDollar/Oracle/MuSig2/Red-Hornet test inventory (~150 unit suites at last count). -- `src/wallet/test/` - DigiDollar wallet suites cover persistence, security, Wave 16/17 spendability, helper asymmetry, and rh59 lock-bypass, plus base wallet tests. +- `src/test/paymaster_*` - Paymaster builder, protocol, provider, sponsorship, discovery, wire, selection, reputation, and state-machine tests. +- `src/wallet/test/` - DigiDollar wallet suites plus Paymaster identity, role-limited PSBT, persistence, reservation, recovery, reorg, and idempotency tests. - `src/qt/test/` - DigiDollar Qt widget tests (`digidollarwidgettests.cpp`, `digidollarwave19widgettests.cpp`); generated `moc_*.cpp` files are build products. ### Functional Tests -- `test/functional/` - Python integration tests; current DD/oracle coverage is registered through `digidollar_*`, `wallet_digidollar_*`, and `feature_oracle_p2p.py` entries in `test_runner.py`. +- `test/functional/` - Python integration tests; DD/oracle coverage is registered through `digidollar_*`, `wallet_digidollar_*`, and `feature_oracle_p2p.py`; `wallet_paymaster_provider.py` covers the descriptor-provider lifecycle, high-privacy selection rejection, user-paid/public/restricted transfers, and recovery. ### Running Tests ```bash @@ -1443,6 +1610,7 @@ This architecture document has been spot-validated against the active DigiByte C | RPC Interface | Verified | `rpc/server.cpp`, `rpc/digidollar.cpp`, `httprpc.cpp`, `wallet/rpc/wallet.cpp` | | DigiDollar Stablecoin | Verified | `digidollar/*`, `consensus/dca.cpp`, `consensus/err.cpp` | | Oracle System | Verified | `oracle/*`, `primitives/oracle.h` | +| Paymaster Network | Implemented; release-gate testing in progress | `paymaster/*`, `wallet/paymaster*`, `wallet/rpc/paymaster*`, Qt DD send/provider surfaces | ### Key Verified Constants @@ -1505,9 +1673,17 @@ This document is consistent with: - **DIGIDOLLAR_ORACLE_ARCHITECTURE.md** - Complete oracle system specification - **REPO_MAP.md** - Granular core-DigiByte file index - **REPO_MAP_DIGIDOLLAR.md** - Granular DigiDollar/oracle file index +- **DIGIDOLLAR_PAYMASTER_NETWORK_PROPOSAL_EN.md** - Approved Paymaster V1 protocol and acceptance criteria +- **doc/digidollar-paymaster.md** - Client and provider operation guide --- -*Document Version: 2.1* -*Generated from DigiByte Core v9.26.2 codebase analysis (`feature/digidollar-v1`)* -*Updated: 2026-05-20* +*Document Version: 2.2* +*Generated from DigiByte Core v9.26.5 codebase analysis (`feature/digidollar-paymaster-v1`)* +*Updated: 2026-07-28* + +Finite Paymaster pool setup now uses the existing wallet maintenance journal and +30-second scheduler independently of provider startup. Exact output plans are +saved before transaction creation; confirmed-input funding avoids stempool-only +change dependencies. See [automatic pool setup](doc/digidollar-paymaster-pool-setup.md) +for fee authorization, V3/V4 persistence compatibility, and legacy adoption. 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..2c2bf59850b 100644 --- a/DIGIDOLLAR_ARCHITECTURE.md +++ b/DIGIDOLLAR_ARCHITECTURE.md @@ -1,9 +1,25 @@ # 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. + +## Paymaster architecture in this branch + +The v9.26.6rc2 integration keeps the provider panel in +`src/qt/paymasterwidget.cpp`, behind a small embedding interface. See the +[integration notes](doc/digidollar-paymaster-v9.26.6rc2-integration.md) for scope +and pending verification. + +For the Paymaster implementation at `bd270044c1`, see the +[developer starting point](PAYMASTER.md) and +[implementation reference](doc/digidollar-paymaster-implementation.md). +It adds collaborative DD transfer construction, wallet authorization and +persistence, provider discovery, and direct P2P sessions. The resulting +transaction is an ordinary `DD_TX_TRANSFER`. The broader branch also contains +arithmetic portability changes in consensus source, which require separate +equivalence review. Verification is tracked in the +[Paymaster release gate](doc/digidollar-paymaster-release-gate.md). ## Executive Summary @@ -15,17 +31,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 +221,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 +597,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 +619,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 +683,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 +717,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 +761,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:** +### 7.3 Reconstruction and reporting -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. +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. -**Verification:** +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. -✅ **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 +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. -✅ **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! -``` - -**Impact on Architecture:** - -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 - -**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 - -**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 +889,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 +909,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 +1497,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 +1506,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,13 +1696,19 @@ 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` | --- *Document last validated 2026-06-25 against the source on `feature/digidollar-v1`. RPC commands: 35 registered total (18 registered in `RegisterDigiDollarRPCCommands` at `src/rpc/digidollar.cpp`, 17 in `GetWalletRPCCommands` at `src/wallet/rpc/wallet.cpp`, including `sendmanydigidollar`, `listdigidollarunspent`, `listdigidollarutxos`, `exportoracleprivkey`, and `importoracleprivkey`). DD price/position/transaction/oracle-operation RPCs are activation-gated; `getdigidollardeploymentinfo` and wallet-local oracle key-management RPCs (`createoraclekey`, `exportoracleprivkey`, `importoracleprivkey`) remain usable before activation. Oracle aggregation: 6 active exchange fetchers via libcurl (Binance, KuCoin, Gate.io, HTX, Crypto.com, CoinGecko). `MockOracleManager` is a regtest helper only. DigiDollar transfers/redeems are confirmed-only (commit `0b4959f563`). `sendoracleprice` RPC was removed as a fake-price-injection vulnerability and `submitoracleprice` does not exist anywhere in the source tree. Pre-V1 oracle bundle versions are rejected at block validation once DigiDollar is active (commit `f2bb0a19a4`).* + +Finite Paymaster pool setup now uses the existing wallet maintenance journal and +30-second scheduler independently of provider startup. Exact output plans are +saved before transaction creation; confirmed-input funding avoids stempool-only +change dependencies. See [automatic pool setup](doc/digidollar-paymaster-pool-setup.md) +for fee authorization, V3/V4 persistence compatibility, and legacy adoption. 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_PAYMASTER_HARDENING_PLAN.md b/DIGIDOLLAR_PAYMASTER_HARDENING_PLAN.md new file mode 100644 index 00000000000..748fa2294e8 --- /dev/null +++ b/DIGIDOLLAR_PAYMASTER_HARDENING_PLAN.md @@ -0,0 +1,201 @@ +# DigiDollar Paymaster hardening plan and review requirements + +**Source:** `bd270044c1`, `feature/digidollar-paymaster-v1`. +**Reconciled:** 2026-09-11. **Status:** controls implemented in this branch; +independent review and release acceptance remain open. + +This English plan replaces the July hardening notes and outdated compatibility +status. It is a current control/verification map, not a new security audit or +formal proof. Read the [implementation reference](doc/digidollar-paymaster-implementation.md) +for flows and the [release gate](doc/digidollar-paymaster-release-gate.md) for +dated evidence and remaining acceptance. + +## 1. Threat model and invariant + +The honest party's local software, wallet and keys are trusted. The remote +client/provider can modify messages, lie about resources, reconnect, replay, +equivocate, delay or disappear. Peers can flood discovery/direct paths and a +crash can interrupt any persistence, signing or broadcast boundary. + +Before a wallet signs, every movement of its DD/DGB must match local persisted +authorization. Retry/result processing cannot create fresh spending authority. +An identity signature and encrypted transport do not substitute for validation. +Public sponsorship is financially bounded, not Sybil-fair. Local compromise, +key theft and malicious release artifacts are outside this counterparty model. + +## 2. H1: authenticated Capacity before disclosure + +**Implemented:** V5 `PMCAPREQ`/`PMCAPRESP` verifies genesis, provider identity, +request/session, nonce, reference block, expiry, BIP86 control proofs, creating +transactions and live resources before intent, DD outpoints or restricted +capability are disclosed. The later quote must match the persisted snapshot's +exact resource set and funding roles. + +**Review:** foreign/spent/stale resources, invalid proofs, changed resources, +cross-session replay, old-version downgrade and early disclosure. + +**Source:** [wire.cpp](src/paymaster/wire.cpp), +[protocol.cpp](src/paymaster/protocol.cpp), +[validation.cpp](src/paymaster/validation.cpp), +[paymaster_discovery.cpp](src/wallet/rpc/paymaster_discovery.cpp). + +## 3. H2: independent local authorization + +**Implemented:** client/provider manifests bind exact inputs, destinations, +fees, offer/policy, capacity and template. Client authority includes the +canonical order and requested fee/privacy/selection modes. Provider authority +names the exact budget reservation/commit key. Signing is role-scoped and +requires `SIGHASH_DEFAULT`; unknown fields and substituted outputs fail closed. + +New provider signatures require current local safety policy. Already durable +exact commits retain their historical non-null policy/SPENT-ledger binding for +safe completion. Retry never reconstructs missing ledger authority. RPC, Qt, +manual processing and automatic service share these checks. + +**Review:** mutate inputs/outputs/fees/change, roles, PSBT fields, sighash, +request options, offers, policies and commitments; test policy changes before +signing and after final commit. High-level Paymaster calls must remain +prepare-only without an exact `authorization_commitment`, regardless of the +`prepare_only` flag. Verify the explicit second-step acceptance for RPC and Qt. + +**Source:** [psbt.cpp](src/paymaster/psbt.cpp), +[reservation.h](src/paymaster/reservation.h), +[paymasterpsbt.cpp](src/wallet/paymasterpsbt.cpp), +[paymaster_processing.cpp](src/wallet/rpc/paymaster_processing.cpp). + +## 4. H3: exact final validation and atomic commit + +**Implemented:** provider final bytes, authority, budget/result and pool state +commit before broadcast. The client validates raw/non-witness transaction, +txid, wtxid, every input/output/witness and actual-prevout script execution +against the trusted PSBT, with mempool preflight when applicable. + +Late negative results cannot replace a final commit or release its authority. +Local chain reconciliation separately rolls confirmation, finance and pool +availability back on reorg. Mempool/stempool is not confirmation. + +**Review:** invalid witnesses, same txid/different witness, conflicts, late +results, every atomic write/commit failure, restart around wallet insertion and +broadcast, exact commit restoration and reorg without duplicate finance events. + +**Source:** [paymasterstore_finalization.cpp](src/wallet/paymasterstore_finalization.cpp), +[paymasterstore_reconciliation.cpp](src/wallet/paymasterstore_reconciliation.cpp), +[paymaster_processing.cpp](src/wallet/rpc/paymaster_processing.cpp). + +## 5. H4 and H5: finite economic exposure + +**Implemented:** provider policies bound transaction fees, concurrently reserved +fees, rolling hour/day spending/completions and active-quote/request counts. +Quote reservation and final expenditure use atomic durable accounting and a +monotonic accounting time. Network-group and pseudonymous recipient buckets +limit repeated demand without raw IPs as persistent rate-limit keys. + +Client policy bounds per-transfer/rolling-day service fees; RPC cannot raise +wallet limits. Fee reservations survive concurrency/restart. A valid all-zero +configuration disables an unused provider model; missing or partial limits do +not mean unlimited service. Paid maintenance has separate finite budgets and +explicit approval. Pool principal is distinct from profit. + +**Review:** last-slot/budget races, clock rollback, concurrent quotes, policy +changes, rollback/expiry/restart, public-budget consumption, restricted +capability reuse and duplicate accounting. + +**Source:** [provider.cpp](src/paymaster/provider.cpp), +[paymasterstore_provider.cpp](src/wallet/paymasterstore_provider.cpp), +[paymasterprovider.cpp](src/wallet/paymasterprovider.cpp), +[paymaster_provider.cpp](src/wallet/rpc/paymaster_provider.cpp). + +## 6. H6: work bounds, replay and evidence + +**Implemented:** cheap size/count/time/rate checks precede costly work. Queues +are bounded/directional and semantic replay identities survive peer changes. +Exact retries are idempotent; contradictory signed Capacity/quote claims cause +durable evidence and local provider blocking. + +The first valid signed claim is staged atomically as evidence before deeper +validation/acknowledgement; it is not spending authority. Database or local +authority failures leave work unacknowledged and distinct from invalid remote +artifacts. Generation-bound leases protect queue ownership across database +operations. Time arithmetic saturates at integer bounds. + +**Review:** reconnect/restart replay, contradictory concurrent claims, expiry +overflow, starvation, overlapping leases/ABA, failed persistence and retry, +cross-session bindings and floods before signature validation. + +**Source:** [manager.cpp](src/paymaster/manager.cpp), +[wire.cpp](src/paymaster/wire.cpp), +[paymaster_integration.cpp](src/wallet/rpc/paymaster_integration.cpp), +[paymasterstore_reputation.cpp](src/wallet/paymasterstore_reputation.cpp). + +## 7. H7: recovery without client DGB + +**Implemented:** ordinary fallback is allowed only before a user PSBT exists. +Signed sessions retain inputs across timeouts. `retry_same` creates no quote, +attempt or signature. `cancel_to_self` can use a distinct eligible user-paid +provider passing V5 Capacity and inherited privacy requirements. + +The recovery manifest binds original client inputs, fresh wallet-owned returns, +bounded service fee and validated provider resources. The client must explicitly +authorize the exact recovery commitment before signing. Confirmation of the +original or recovery spend determines the outcome; mempool cancellation does +not erase already signed authority. + +**Review:** post-signature fallback, original-provider reuse, wrong return +scripts, excessive fees, changed inputs, original/recovery races, restart, +exact retry and lack of an available no-client-DGB recovery path. + +**Source:** [recovery.cpp](src/paymaster/recovery.cpp), +[paymaster_client.cpp](src/wallet/rpc/paymaster_client.cpp), +[paymasterstore_recovery.cpp](src/wallet/paymasterstore_recovery.cpp). + +## 8. H8 and current lifecycle coverage + +Unit/wallet tests cover protocol, builder, PSBT, authority and persistence +failure injection. Functional tests cover payments, discovery, readiness, +maintenance, fallback/contention and reorg; Qt tests cover shared workflow +authority. The [security workflow](.github/workflows/paymaster-security.yml) +defines wire, stateful-security, pool-lifecycle and persistence fuzz targets. + +Test/workflow presence is coverage, not a successful run on the release head. +July broad-matrix and August compatibility runs remain dated evidence. Repeat +affected surfaces after source/build/dependency changes with exact revision and +binary identity retained. + +Current review must include preview-bound pool operations/withdrawals, +successor provenance/pending capacity, maintenance budgets, finance rollback, +authoritative GUI snapshots and current-only record decode errors. Ordinary +coin selection must protect Paymaster reservations and provider pools even +when safety state is unreadable. + +## 9. Privacy, retention and unavoidable limits + +The provider can retain the transaction it signs; confirmed transactions are +public. High privacy requires Tor/onion isolation but its real deployment +acceptance remains open. Wallet encryption protects keys, not every persisted +endpoint, request, manifest, PSBT, final transaction or timestamp. Restricted +capability secrets are removed before request persistence. + +Retention compacts eligible completed records after the safety depth while +preserving idempotency and unresolved authority. There is no destructive reset +or implicit record-format migration. Ordinary old-wallet compatibility is a +separate question from older experimental Paymaster records. + +A provider can censor/delay, double-promise resources to isolated clients, +consume allowed sponsorship budgets or later broadcast already authorized +bytes. Those risks do not permit different client outflows. Without client DGB +or a reachable eligible independent provider, recovery may be delayed. + +## 10. Remaining acceptance + +1. Bind results to the selected source/binaries and repeat affected local, + functional, Qt and sanitizer/fuzz checks after later changes. +2. Retain and, where affected, repeat official-v9.26.5 compatibility evidence + from 2026-08-08/09; it is no longer accurate to call compatibility untested. +3. Complete real-Tor checks with isolation, capture/logging guards, one attempt + and no clearnet/v1 downgrade. +4. Complete or explicitly disposition independent review of authority, + concurrent persistence and recovery, including the wider branch's separate + arithmetic portability changes. + +The release gate is the status record. The supported claim is “implemented +with dated test evidence; release acceptance open,” not formal or audited safety. diff --git a/DIGIDOLLAR_PAYMASTER_NETWORK_PROPOSAL_EN.md b/DIGIDOLLAR_PAYMASTER_NETWORK_PROPOSAL_EN.md new file mode 100644 index 00000000000..b9914a9ae77 --- /dev/null +++ b/DIGIDOLLAR_PAYMASTER_NETWORK_PROPOSAL_EN.md @@ -0,0 +1,186 @@ +# DigiDollar Paymaster design specification + +**Source:** `feature/digidollar-paymaster-v1` at `bd270044c1`. +**Reconciled:** 2026-09-11. **Feature milestone:** V1. **Wire protocol:** V5. + +This document replaces the earlier concept draft with the design implemented +in the inspected branch. The filename is retained for existing references and +the 28 acceptance-criterion identifiers are preserved. It describes the +current feature, not an unimplemented proposal. It does not claim release +approval or independent security certification. + +Read [PAYMASTER.md](PAYMASTER.md) for context and review order, the +[implementation reference](doc/digidollar-paymaster-implementation.md) for +protocol/state details and source locations, the +[implementation plan](DIGIDOLLAR_PAYMASTER_PLAN.md) for work packages, and the +[release gate](doc/digidollar-paymaster-release-gate.md) for verification status. + +## 1. Purpose and scope + +A client holding confirmed DigiDollar but no DGB can transfer DD by asking a +provider to contribute the DGB miner-fee inputs. The client and provider +authorize and sign only their respective inputs in one ordinary +`DD_TX_TRANSFER`. The provider signs last, commits the final transaction +durably, and broadcasts it. + +The funding models are user-paid percentage fees, public zero-fee sponsorship, +and restricted zero-fee sponsorship with a signed descriptor and payment-bound +capability. There is no privileged provider roster, new transaction type, +special sighash, DD output exemption, prepaid client balance at the provider, +or transfer of user private keys. + +Client/provider operation requires DigiDollar activation, an unpruned node, +synchronized transaction indexing and BIP324 v2. One session has one recipient. +Paymaster mint/redeem sponsorship and multi-recipient batching are outside this +implementation. Existing five-argument `senddigidollar` calls retain direct +wallet-funded DGB fees. + +## 2. Architecture decisions + +| Decision | Rationale and current consequence | +|---|---| +| One normal DD transfer | Existing validators check the final transaction; the provider has no consensus privilege. | +| Dedicated deterministic builder | All inputs/outputs are fixed before signing, including explicit wallet-owned change and carrier returns. | +| DD-capable PSBT with roles | Each wallet signs only locally authorized inputs; unknown fields/roles and non-default sighashes fail closed. | +| Public admission, private operational exchange | Announcements prove separate admission liquidity. Only the selected provider/client exchange reveals operational payment resources. | +| Mandatory V5 Capacity | Provider identity and resources are authenticated before client payment details are disclosed. | +| Atomic wallet state | Sessions, attempts, reservations, authorizations and exact commits survive lost messages/restarts without fresh authority. | +| Separate authority and chain observation | A final commit, mempool acceptance, confirmation and reorganization have distinct meanings. | +| Shared RPC/Qt authority | The GUI uses the same wallet checks, snapshots and allowed actions. | +| Finite local budgets | Client fees, provider losses, concurrent reservations and maintenance spending are bounded explicitly. | +| Current-only experimental formats | Old Paymaster protocol/record versions fail closed without implicit migration or downgrade. | + +These are Paymaster design decisions. The broader branch also changes DCA, +ERR and volatility source to use portable 128-bit arithmetic. Review their +equivalence independently; absence of a new Paymaster consensus rule does not +mean every consensus source file is unchanged. + +## 3. Transaction and fee contract + +DD uses integer cents; DGB uses integer satoshis. User-paid fees use checked +integer arithmetic, rounded up to one cent: + +```text +fee_cents = ceil(recipient_cents * fee_rate_bps / 10000) +user_DD_in = recipient_DD + user_DD_change + service_fee_DD +provider_DGB_in = provider_DGB_change + miner_fee_DGB +``` + +Every DD output remains between 100 and 10,000,000 cents and total DD is +conserved. A 1-to-99-cent fee uses a confirmed provider-owned DD carrier whose +replacement output returns principal plus fee. A fee of at least 100 cents can +use a normal provider-fee output. Zero-fee sponsorship needs no unused carrier. +See the [worked carrier example](doc/digidollar-paymaster-implementation.md#why-a-carrier-is-needed). + +Creating transactions, outpoints, scripts and values are checked locally. +Client change belongs to the client; carrier/DGB returns belong to the provider. +Exact-outflow options support fee deduction from a fixed gross DD amount and +all ordinary confirmed spendable DD. Core rejects cent-rounding gaps, excludes +reserved/provider-pool inputs and rejects a changed sweep snapshot before +committing reservations. + +## 4. Discovery and signing contract + +`sendpmasters`, `getpmasters` and `pmannounce` operate on upgraded ordinary peers +after DigiDollar activation. Eligibility requires identity/admission checks +and disjoint admission/operational resources. Isolated directional Paymaster +connections carry bounded Capacity, quote, submit, result and recovery messages. + +Before disclosure of DD inputs, intent or restricted capability, V5 Capacity +authenticates the network, provider, request/session, nonce, reference block, +expiry and exact operational resources. The quote must use the validated +resource set. Both wallets persist their local authorization manifest and +independently reconstruct the full transaction immediately before signing. + +The high-level Paymaster call first prepares an exact authorization. The +caller must return its `authorization_commitment` with the same request before +Core can create a new user signature; `prepare_only=false` does not bypass this +guard. Headless clients implement the same explicit acceptance step as Qt. + +The client signs only its DD inputs; the provider signs only its own inputs. +Provider policy and budget reservations must match local signing authority. +One exact final raw transaction and related authority/accounting are committed +before broadcast. Client result processing checks all transaction/witness +bytes against the trusted template and verifies signatures with actual prevouts. + +Standard direct transport requires v2 without v1 fallback. High privacy adds +onion transport, stream isolation, one provider attempt and no clearnet fallback, +plus capture/logging readiness guards. The provider still sees its co-signed +transaction and confirmed transactions remain public. + +## 5. State, recovery and operations contract + +`request_id` is the durable idempotency key, bound to a canonical request hash. +Each attempt binds one provider, offer, capacity, quote and template. Exact +retries cannot create replacement authority or reconstruct missing budget rows. + +Ordinary `fallback` is allowed only before a user PSBT exists. +`abandon_unsigned` requires that no transaction authorization can exist. After +signing, timeout never releases inputs: use inspection, `retry_same`, or +explicitly authorized `cancel_to_self`. A distinct eligible user-paid provider +can supply recovery DGB. Recovery uses the original client inputs, fresh +client-owned DD returns and only the bounded recovery service fee. Cancellation +is final after confirmation, not after mempool acceptance. + +Provider start, automatic servicing, optional autostart and paid maintenance +have separate settings/authorizations. Pool successors, maintenance, budgets +and finance reconcile through confirmation, conflict, restart and reorganization. +Pending successors are not confirmed usable liquidity. + +Durable records can contain metadata and signed artifacts without field-level +encryption. Eligible completed detail is compacted after the safety depth into +idempotency tombstones. Ambiguous, unconfirmed or unreadable authority is not +automatically erased. + +## 6. V1 acceptance criteria + +These requirements preserve the IDs in the +[release matrix](doc/digidollar-paymaster-release-gate.md). A requirement or +test being present does not establish a current release pass. + +| ID | Required behavior | +|---:|---| +| 1 | A client without DGB completes a confirmed DD payment using provider DGB inputs. | +| 2 | Neither party transfers private keys or seed. | +| 3 | Automatic eligibility requires at least three disjoint confirmed admission DGB slots and, for user-paid offers, admission carriers. Operational resources are separate and not gossiped. | +| 4 | Fees from 0.01 through 0.99 DD use valid carriers without sub-1-DD outputs. | +| 5 | Percentage fees and exact total charges are calculated and ordered locally with bounded integer arithmetic. | +| 6 | Each wallet signs only its own locally authorized inputs. | +| 7 | A signed attempt rejects changes to recipient, amount, fee, change, inputs or template. Ordinary fallback is pre-user-PSBT only and preserves client DD inputs. | +| 8 | Provider failure cannot create duplicate confirmed payment through fresh-input retry; signed ambiguity retains original inputs and permitted recovery actions. | +| 9 | Existing DD validators validate the ordinary final transaction without Paymaster configuration. | +| 10 | The Paymaster protocol adds no consensus parameter, script rule, transaction type or DD conservation rule. Review separate arithmetic portability changes for equivalence. | +| 11 | Local reputation distinguishes neutral failures and never overrides identity, liquidity or authorization checks. | +| 12 | Headless selection uses durable sessions and hard DD fee caps, requires explicit acceptance of the prepared authorization commitment before signing, and does not create a second payment on identical request retry. | +| 13 | Existing five-argument `senddigidollar` calls retain the wallet-funded DGB path. | +| 14 | Public/restricted sponsorship use the common transaction protocol, charge zero DD service fee and require no prepaid provider account. | +| 15 | Restricted capabilities are payment-bound, not gossiped and not persisted as plaintext secrets; required cryptographic bindings and consumption state are retained. | +| 16 | Change uses explicit fresh internal destinations; production Paymaster logging does not expose payment artifacts or restricted secrets. | +| 17 | High privacy enforces onion transport, stream isolation, one attempt and no silent clearnet/v1 fallback. | +| 18 | Only the selected provider receives the necessary payment intent and discloses the operational resource set for that attempt. Local fee/privacy tolerances are not globally broadcast. | +| 19 | Eligible completed detail is reduced after the safety depth while permanent idempotency and conflicting-request protection remain. | +| 20 | Documentation and UI distinguish transport privacy/pseudonymity from anonymity against the provider or public chain. | +| 21 | Signed ambiguity exposes safe inspection, exact retry and same-input self-return. Ordinary fallback is pre-signature, timeout does not free signed inputs and cancellation requires confirmation. | +| 22 | The provider commits the exact final transaction and related authority/accounting before broadcast and restores that same commit on recovery. | +| 23 | A crash after client signing resumes the persisted exact PSBT without a new quote, reservation, authorization, signature or attempt. | +| 24 | One recipient is accepted per session; every DD output, including carrier and recovery returns, respects the 100-to-10,000,000-cent bounds. | +| 25 | Client/provider readiness requires unpruned operation, synchronized txindex and complete creating-transaction data. Existing validators need no Paymaster feature; only upgraded peers relay discovery. | +| 26 | Signed result authority is monotonic; final commitment binds exact raw bytes. Related wallet records, indexes and reservation/consumption transitions commit atomically. | +| 27 | Mempool/stempool is not confirmation; cancellation remains pending until confirmed. RPC/Qt share authoritative session, phase, broadcast and confirmation state; local chain reconciliation handles reorgs. | +| 28 | Wallet locking pauses the same session without fallback. Provider policy enforces a positive absolute DGB fee ceiling and finite safety limits for active funding models. | + +## 7. Hardening and changes from the initial design + +The initial criteria are supplemented by mandatory V5 Capacity, independent +local manifests, full witness validation, finite provider/client budgets, +durable replay/equivocation handling and authorized independent-provider recovery. +These map to H1-H8 in the release gate and +[hardening plan](DIGIDOLLAR_PAYMASTER_HARDENING_PLAN.md). + +The earlier concept's broader post-signature fallback is superseded by the +implemented pre-user-PSBT boundary. Current functionality also includes pool +successor reuse, paid maintenance, provider finance/backup workflows, exact +outflow and current-only persistence. The current source definitions, linked +from the implementation reference, define message bounds, record schemas, +RPC parameters and allowed transitions. No obsolete wire schema or implicit +migration is specified here. diff --git a/DIGIDOLLAR_PAYMASTER_PLAN.md b/DIGIDOLLAR_PAYMASTER_PLAN.md new file mode 100644 index 00000000000..90896fe14f6 --- /dev/null +++ b/DIGIDOLLAR_PAYMASTER_PLAN.md @@ -0,0 +1,131 @@ +# DigiDollar Paymaster implementation plan and delivery status + +**Reviewed:** 2026-09-11. **Source:** `bd270044c1`, +`feature/digidollar-paymaster-v1`. **Status:** implemented in the branch; +release acceptance remains open. + +This plan describes delivered work packages and remaining acceptance work. It +supersedes the prospective plan and July-only status. Historical test counts +remain dated evidence in the [release gate](doc/digidollar-paymaster-release-gate.md). +This documentation update does not assert a new build, regression run or audit. + +Start with [PAYMASTER.md](PAYMASTER.md), then read the +[design specification](DIGIDOLLAR_PAYMASTER_NETWORK_PROPOSAL_EN.md) and +[implementation reference](doc/digidollar-paymaster-implementation.md). + +## 1. Delivery objective + +Enable confirmed DD payments without client DGB by collaborating with a +provider supplying miner-fee inputs. Preserve local keys, ordinary DD validity, +explicit fee authorization, durable idempotency and recovery after signing. + +Current scope includes user-paid fees, public/restricted sponsorship, discovery, +automatic/manual provider operation, finite budgets, liquidity maintenance, +finance reporting, recovery, RPC and Qt. Paymaster mint/redeem sponsorship and +multi-recipient sessions are not implemented. + +## 2. Delivered work packages + +“Implemented” means present and wired into the inspected source. It does not +mean independently audited or freshly verified on a release binary. + +| Package | Current implementation | Acceptance focus | +|---|---|---| +| Transaction | Deterministic collaborative builder, checked cents/satoshis, carriers and existing output bounds. | Conservation, exact outputs, invalid prevouts and amount boundaries. | +| Signing | Trusted PSBT, explicit roles, default sighash and local client/provider manifests. | Mutated inputs/outputs/fees/policies and unauthorized signatures. | +| Wallet state | Atomic sessions, attempts, locks, signed PSBTs, exact commits, budget/replay/recovery records. | Failure at each write, restart, exact retry, no invented authority. | +| Provider | Descriptor identity, offers/sponsorship, separate admission/operational pools and finite safety budgets. | Resource ownership, concurrent last-slot/budget races and capability binding. | +| Network | Signed directory, isolated direct channels, V5 Capacity, bounded directional queues and persistent replay evidence. | Activation/transport, malformed messages, reconnect, equivocation and disclosure ordering. | +| Client | Offer filtering/ranking, idempotency, fee policy, explicit confirmation, exact-outflow and all-spendable-DD options. | Exact order, selected-provider-only effects, rounding gaps and changed sweep balance. | +| Recovery | Unsigned abandonment/fallback, exact signed retry, same-input return using own DGB or a distinct provider. | Post-signature fallback rejection, durable locks, recovery commitment and confirmation race. | +| Lifecycle | Automatic/manual service, optional autostart, successor reuse, paid target maintenance and preview-bound withdrawals. | Restart without duplicate work, pending targets, policy races and maintenance budgets. | +| Finance/retention | DD income/DGB cost ledger, principal separation, reorg reconciliation, backup acknowledgement and tombstones. | No duplicate accounting, rollback/reconfirmation and unresolved-authority retention. | +| RPC/Qt | Shared Core authority, snapshots/allowed actions, asynchronous UI and guided setup. | Stale-response rejection, fee/recovery confirmation and lock/unlock behavior. | +| Build/tests | Automake/MSVC, arithmetic portability, unit/wallet/Qt/functional coverage and sanitizer/fuzz workflows. | Selected-revision builds, regression and compatibility evidence. | + +The implementation reference and +[repository map](REPO_MAP_DIGIDOLLAR.md#paymaster-network) identify each package, +including the current split wallet-store and RPC modules. + +## 3. Review sequence + +### A. Transaction and authorization + +Read amount types, builder, PSBT validation and manifests first. Check a carrier +transfer by hand, then malformed creating transactions, output bounds, fee +rounding, roles and changed destinations. Each wallet must authorize its exact +outflows, inputs, returns and fee exposure independently. + +### B. Persistence and failure boundaries + +Trace client authorization into durable signed PSBT state and provider signing +into the exact final commit. Pool, budget and authority writes must be atomic +where required. Inspect restart before/after each write, wallet insertion and +broadcast boundary. Unreadable/old-format records are failures, not missing +authority that a remote message may reconstruct. + +Distinguish immutable remote authorization from local chain observation: reorgs +can restore pending state without authorizing a different spend. + +### C. Provider and discovery + +Check admission/operational disjointness, authenticated Capacity before intent, +and exact quote/resource binding. Review sponsorship capabilities, finite +safety budgets, replay identities, work bounds, queue ownership and equivocation. + +### D. Client, runtime and interfaces + +Trace high-level `senddigidollar` and explicit quote/PSBT flows through common +validation. Check that a Paymaster call without `authorization_commitment` +only prepares authority and that the identical request must echo the exact +commitment before a new user signature. Verify five-argument compatibility and the narrow insufficient-DGB +condition for automatic fallback. Check separate authorization for client +signing, recovery, provider start, autostart and paid maintenance. + +Inspect Qt using current Core snapshots and allowed actions. Include pool +maintenance/withdrawal previews and finance; reviewing only the send dialog +misses durable provider operations. + +### E. Compatibility and wider branch + +Review final transfers with old DD validators and old-only bridge behavior +separately from upgraded direct connectivity. Old wallet-file compatibility is +not migration support for old Paymaster record formats. + +The branch includes portable 128-bit arithmetic in DCA/ERR/volatility and +build/Qt integration. Those changes require their own equivalence/regression +review. [PAYMASTER.md](PAYMASTER.md) provides initial commit navigation; later +commits must also be included when reviewing each final slice. + +## 4. Recorded verification + +| Date | Existing record | Scope limit | +|---|---|---| +| 2026-07-30 | MSVC Debug/Release, focused Paymaster/wallet checks, full 3,605-case core suites, eight native Qt suites and sanitizer/fuzz campaigns. | Dated candidate evidence, not an automatic pass of later commits. | +| 2026-08-08 | Windows/WSL multi-provider, fallback/contention and reorg checks; official-v9.26.5 wallet/validator compatibility. | Specific tests and candidate runs. | +| 2026-08-09 | Official old-only bridge and unchanged-datadir upgrade scenarios. | Specific compatibility scenarios, not full release approval. | +| 2026-09-11 | Documentation reconciled with `bd270044c1`. | Source/document review, no new executable test pass. | + +The previous plan's “mixed-version compatibility entirely open” statement is +outdated. August evidence covers specific wallet, validator, relay and upgrade +scenarios. A selected release still requires suitable reruns after affected +changes. The release gate retains details and outstanding requirements. + +## 5. Remaining acceptance work + +1. Select the exact release source and binaries. Record toolchain/configuration, + binary identity, commands, exit codes and retained output. +2. Re-run affected unit/wallet, functional and Qt surfaces after later changes. + Repeat the multi-provider/failover/reorg group on the release build and + include lifecycle, exact-outflow and current-only persistence behavior. +3. Complete the agreed platform, arithmetic-equivalence and compatibility + regression matrix; repeat sanitizer/fuzz checks after relevant changes. +4. Test high privacy with real Tor/onion transport, stream isolation, + capture/logging guards, one attempt and no clearnet/v1 fallback. +5. Obtain independent review of authority, persistence, concurrency and recovery; + record findings and their disposition before release approval. +6. Update the release gate with exact evidence and an explicit decision. + +A green subset, documentation update or configured CI workflow alone does not +close release acceptance. When behavior changes, update the design/reference, +operator guide, source-map entries and affected acceptance evidence together. diff --git a/DIGIDOLLAR_WALLET_INTEGRATION.md b/DIGIDOLLAR_WALLET_INTEGRATION.md index ced12dde5ee..30b1e4d3e63 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 @@ -193,11 +193,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 +259,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 +304,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,10 +402,20 @@ 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. | -| `backupwallet path` / `restorewallet new_name path` | Full DD state including owner keys | Restored wallet is loaded under `new_name`; existing wallet is untouched. | +| `-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; for a provider wallet, also the Paymaster identity key, policies, pool/session records, finance ledger and backup metadata | Restored wallet is loaded under `new_name`; existing wallet is untouched. A restored provider wallet asks for a fresh local backup again. | | `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. | +Paymaster-provider state has stronger recovery requirements than ordinary +on-chain DD ownership. A seed or descriptor import can recover keys and +chain-visible outputs, but it does not reconstruct the wallet-local provider +identity metadata, authorization records, pool lifecycle, safety policies or +finance history. Use a complete `backupwallet` file (or an equivalent verified +full-wallet database backup) for an instance-preserving Paymaster recovery. +Creating a fresh wallet creates a new provider ID and a separate finance +ledger. The Qt provider wizard and operator pages keep a non-blocking backup +reminder visible after identity creation and material configuration changes. + ### Operator wallet recovery (descriptor wallet) ```bash @@ -351,11 +434,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 +515,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 +569,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 +624,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 +688,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 +703,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 +726,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 +747,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 +768,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 +803,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/PAYMASTER.md b/PAYMASTER.md new file mode 100644 index 00000000000..632a93381f7 --- /dev/null +++ b/PAYMASTER.md @@ -0,0 +1,100 @@ +# DigiDollar Paymaster: developer starting point + +The v9.26.6rc2 integration, RPC migration and pending build/runtime gates are +recorded in the [integration notes](doc/digidollar-paymaster-v9.26.6rc2-integration.md). + +The Paymaster feature lets a wallet transfer DigiDollar (DD) without owning DGB +for the miner fee. A separate provider contributes DGB inputs to the same +transaction. The user signs its DD inputs, the provider signs its own inputs, +and the result is an ordinary `DD_TX_TRANSFER` validated by existing DigiDollar +rules. A provider may sponsor the fee or charge an explicitly authorized DD +service fee. + +This is the entry point for reviewing `feature/digidollar-paymaster-v1`. +Documentation was reconciled on **2026-09-11** against source commit +**`bd270044c1`**. The feature is implemented in this branch; release approval, +an independent review, and a real Tor deployment check remain outstanding in +the recorded release gate. Updating documentation does not renew old test +results or establish that a release candidate has passed. + +## Read these documents in order + +| Document | What it answers | +|---|---| +| [Implementation reference](doc/digidollar-paymaster-implementation.md) | How does the current code work? Transaction example, protocol sequence, trust boundaries, state and persistence rules, and source map. | +| [Implementation plan](DIGIDOLLAR_PAYMASTER_PLAN.md) | Why was the work divided this way? Delivered work packages, review order, remaining work, and acceptance expectations. | +| [Hardening plan](DIGIDOLLAR_PAYMASTER_HARDENING_PLAN.md) | What must hold when either remote party is malicious? Required controls and their implementation/test locations. | +| [Operator and RPC guide](doc/digidollar-paymaster.md) | How are clients and providers configured and used? Includes fee selection, liquidity, finance, and recovery. | +| [Release gate](doc/digidollar-paymaster-release-gate.md) | Which checks have dated evidence, which still need a release-build rerun, and what remains open? | +| [Design specification](DIGIDOLLAR_PAYMASTER_NETWORK_PROPOSAL_EN.md) | What are the current architectural decisions, constraints, and 28 numbered V1 acceptance criteria? | + +The implementation reference describes the inspected source. The release gate +records verification evidence. The design specification preserves criterion +numbering while updating the requirements to the current implementation. +Changes from the initial design are listed explicitly in the reference. These +documents are not evidence of an upstream merge or a production release. + +## Why this involves more than a fee helper + +The ordinary wallet path selects and signs its own DD and DGB inputs. A +Paymaster payment crosses two wallets and can stop after one wallet has signed +but before either party knows whether the transaction was broadcast. Supporting +that safely requires explicit transaction construction, selective signing, +durable input reservations, exact retries, and confirmation-aware recovery. + +Public provider discovery adds identity and liquidity proofs, a bounded P2P +directory, and a separate direct session. Provider operation adds finite fee +budgets, pool maintenance, and restart handling. RPC and Qt expose the same +wallet authority and state. Tests cover those boundaries and their failure +cases, which explains the spread across networking, wallet, GUI, and build +files. + +## Review the branch in bounded pieces + +The inspected branch has merge base +`16159311b34449cd970871bcd1532561b0d92cdd` with the locally available upstream +`develop`. Recompute the base if reviewing a later revision. + +```sh +git log --reverse --oneline 16159311b34449cd970871bcd1532561b0d92cdd..HEAD +git diff --stat 16159311b34449cd970871bcd1532561b0d92cdd...HEAD +git diff 16159311b34449cd970871bcd1532561b0d92cdd...HEAD -- src/paymaster +git diff 16159311b34449cd970871bcd1532561b0d92cdd...HEAD -- src/wallet +``` + +| Review slice | Initial implementation commits | Review focus | +|---|---|---| +| Build and portability | `15b5ebcd8e` | MSVC/static Qt support and arithmetic portability. | +| Protocol and peers | `3883c3a7ca` | Deterministic templates, protocol records, discovery, direct transport and bounded queues. | +| Wallet and RPC | `652ff3a3fc` | Local authorization, atomic persistence, restart and recovery. | +| Qt | `6de567e570` | Explicit confirmation, asynchronous operations, shared Core state. | +| Tests | `c046701d05` | Positive transfers and malicious-message, failure, and lifecycle scenarios. | +| Initial documentation | `be7a3acbf4` | Original architectural and operational explanation. | + +These are navigation anchors, not an exhaustive list: later commits add +liquidity maintenance, finance, exact-outflow payments, compatibility coverage, +persistence hardening, RPC/store file splits, and Qt fixes. Review the final +diff for each slice as well as its initial commit. + +The Paymaster protocol does not require new consensus rules. The broader +branch nevertheless **does modify consensus source files**: the portability +work replaces native `__int128` uses in DCA, ERR, and volatility calculations +with the types in [src/util/int128.h](src/util/int128.h). Review arithmetic +equivalence and platform behavior separately. “No Paymaster consensus change” +must not be used to skip these changes. + +## What the first review should establish + +1. Every outgoing DD/DGB amount and destination matches local authority before + signing, including fees, change, carriers, and recovery outputs. +2. Capacity is authenticated before the client discloses its payment intent. +3. A crash or retry cannot create a second authorization, free signed inputs, + overspend a provider budget, or replace an exact committed transaction. +4. Mempool acceptance, confirmation, and reorganization are represented + consistently in the wallet, RPC, and GUI. +5. The release gate is tied to the revision and binaries being proposed for + release, with real-Tor and independent-review results recorded separately. + +Start code reading with the component table in the implementation reference. +For a finer inventory, use the +[Paymaster section of the repository map](REPO_MAP_DIGIDOLLAR.md#paymaster-network). 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/README.md b/README.md index 148ab4f187e..376f3ca78e7 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,15 @@ +## DigiDollar Paymaster development branch + +For the purpose, current design, implementation plan, code review map, and +verification status of this branch, start with +**[DigiDollar Paymaster: developer starting point](PAYMASTER.md)**. +The feature allows a separate provider to fund the DGB miner fee for an +ordinary DigiDollar transfer. The linked release gate distinguishes dated +test evidence from outstanding release acceptance. + # Development is now occurring on the DigiByte-Core repo As of 2020, development has now moved to the DigiByte-Core organization, under the digibyte repo diff --git a/RELEASE_v9.26.6.md b/RELEASE_v9.26.6.md new file mode 100644 index 00000000000..640c8bfe235 --- /dev/null +++ b/RELEASE_v9.26.6.md @@ -0,0 +1,519 @@ +# DigiByte Core v9.26.6 release notes + +**Current version: v9.26.6rc2.** This is the second release candidate: a test +build before the final release. It replaces rc1, which could reject a valid +DigiDollar mint block during a reindex on a node holding a DigiDollar wallet. +See "Changes since rc1" below. + +## Summary + +v9.26.6 includes **85 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. + +## Changes since rc1 + +**Block validation no longer reads the wallet's script table.** The node keeps +an in-memory table that the wallet fills in with the DigiDollar amount behind +each output script. It is keyed by the script alone, so an owner address that +is used again keeps only the last amount. In rc1, block validation below Thaw +Day read that table before the chain data. A mainnet rc1 node whose wallet had +minted $100 and later held $98 of change at the same address rejected the +first DigiDollar mint block, 23,869,549, while reindexing, then stopped +following the chain. Nodes without such a wallet accepted the block. Validation +now reads amounts from the chain only. The Thaw Day heights and rules are +unchanged. + +**If an rc1 node is stuck**, install rc2, start it, and run: + +```bash +digibyte-cli reconsiderblock 00000000000000052cc3d211b3d16196a3585d46474202dfa42e9f770d02e9bd +``` + +The node validates the remaining blocks and catches up. A fresh `-reindex` +on rc2 also works. Do not start `-reindex` on an rc1 node that holds a +DigiDollar wallet. + +**A wallet log line no longer repeats once per block** during a reindex or a +first sync. It now needs `-debug=digidollar`. + +## 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. + RC2 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 85 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. + +### Reindex and block validation (rc2) + +84. **Change:** Block and mempool validation read DigiDollar amounts from the chain only, never from the wallet's in-memory script table.\ + **Why:** A node's answer about a block must not depend on what its own wallet has done since. + +85. **Change:** The wallet's "ReconcilePositionStates skipped" message moves into the DigiDollar debug category.\ + **Why:** It was written once per block during a reindex and filled the log. + +## Tests completed and work still ahead + +These are the recorded results for the rc2 source commit +`653484decd` (the block validation repair) with the rc2 version bump on top: + +| Check | Result | +| --- | --- | +| C++ unit tests | 3,742 passed, none failed | +| Extended functional tests | 396 passed, 17 skipped, none failed | +| Desktop wallet tests | Passed | +| Mainnet node with the affected wallet | Accepted block 23,869,549 after `reconsiderblock` and caught up to the tip, 24,222,044, with 0 rejected blocks | +| Thaw Day testnet lab exercise | Passed: isolated run `rc2-rehearsal-09` crossed Thaw Day at lab height 5,000, ran every DigiDollar feature before and after it, and finished at height 5,658 with 143 checks and 0 failures | + +The rc1 results for commit `d2097819f260f4d82409643fe9f7263cdd7e3eaa` were +3,739 unit tests passed, 394 functional tests passed with 17 skipped, desktop +wallet tests passed, and fuzz tests over 255 public targets with 28 longer +runs. The fuzz targets were not rerun for rc2. + +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 reindex, +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..578c0f7761e 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 @@ -1929,6 +1928,9 @@ The Qt GUI provides the graphical interface for DigiByte Core. Key non-DigiDolla ### src/wallet/walletdb.cpp / .h - `WalletBatch` → typed DB record reader/writer for the wallet (record types: keymeta, ckey, hdchain, descriptor, name, purpose, âš ī¸ DD positions / DD UTXOs / DD oracle keys; the DD-specific records are documented in `REPO_MAP_DIGIDOLLAR.md`) +### src/wallet/paymasterdb.cpp +- Paymaster `WalletBatch` codecs and record keys, extracted without changing storage formats or transaction boundaries. See `REPO_MAP_DIGIDOLLAR.md` for the Paymaster subsystem and tests. + ### src/wallet/wallettool.cpp / .h - `digibyte-wallet` (CLI tool) backend: `create`, `info`, `salvage`, `dump`, `createfromdump` diff --git a/REPO_MAP_DIGIDOLLAR.md b/REPO_MAP_DIGIDOLLAR.md index ed9fddd7c43..1a4ac581d50 100644 --- a/REPO_MAP_DIGIDOLLAR.md +++ b/REPO_MAP_DIGIDOLLAR.md @@ -1,8 +1,17 @@ -# REPO_MAP_DIGIDOLLAR.md — DigiDollar + Oracle Subsystem v9.26.2 +# REPO_MAP_DIGIDOLLAR.md — DigiDollar, Oracle, and Paymaster Subsystems v9.26.5 -*Last updated: 2026-05-20 (v9.26.2 / `feature/digidollar-v1`)* +*Last updated: 2026-08-08 (v9.26.5 / `feature/digidollar-paymaster-v1`)* -This is the granular file index for all DigiDollar and Oracle source code. Read `DIGIDOLLAR_ARCHITECTURE.md` and `DIGIDOLLAR_ORACLE_ARCHITECTURE.md` first for system design context. +This is the granular file index for DigiDollar, Oracle, and Paymaster source code. +Read `DIGIDOLLAR_ARCHITECTURE.md` and `DIGIDOLLAR_ORACLE_ARCHITECTURE.md` for DD +and oracle design context. For Paymaster, start with [PAYMASTER.md](PAYMASTER.md) +and the [current implementation reference](doc/digidollar-paymaster-implementation.md), +then use the [design specification](DIGIDOLLAR_PAYMASTER_NETWORK_PROPOSAL_EN.md) +and [implementation plan](DIGIDOLLAR_PAYMASTER_PLAN.md). +The 28 acceptance criteria and additional hardening checks map to dated evidence +in the [release gate](doc/digidollar-paymaster-release-gate.md). +Paymaster navigation was reconciled against `bd270044c1` on 2026-09-11; the +earlier inventory date above does not represent a new whole-repository audit. --- @@ -12,17 +21,34 @@ This is the granular file index for all DigiDollar and Oracle source code. Read 2. [Consensus Rules (`src/consensus/`)](#consensus-rules) 3. [Oracle System (`src/oracle/`)](#oracle-system) 4. [Oracle Primitives (`src/primitives/`)](#oracle-primitives) -5. [RPC Interface (`src/rpc/`)](#rpc-interface) -6. [Wallet Integration (`src/wallet/`)](#wallet-integration) -7. [Index (`src/index/`)](#index) -8. [Scattered References](#scattered-references) -9. [Qt GUI](#qt-gui) -10. [Tests](#tests) +5. [Paymaster Network (`src/paymaster/`)](#paymaster-network) +6. [RPC Interface (`src/rpc/`)](#rpc-interface) +7. [Wallet Integration (`src/wallet/`)](#wallet-integration) +8. [Index (`src/index/`)](#index) +9. [Scattered References](#scattered-references) +10. [Qt GUI](#qt-gui) +11. [Tests](#tests) --- ## 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 +211,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 +319,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 +389,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 +418,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 +487,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 +597,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` @@ -577,6 +611,112 @@ This is the granular file index for all DigiDollar and Oracle source code. Read --- +## Paymaster Network + +The Paymaster subsystem is wallet and P2P policy code. Its final transaction is +an ordinary `DD_TX_TRANSFER`; nothing under `src/paymaster/` defines consensus +or chain parameters. + +### src/paymaster/types.{h,cpp} +- Strong `DDCents` and `DGBSatoshis` value types, protocol constants, funding, + privacy, fee, and selection enums. +- `ComputePaymasterFee()` performs bounded basis-point calculation rounded up + to a whole cent; `IsCanonicalRequestId()` enforces lowercase UUID form. + +### src/paymaster/txbuilder.{h,cpp} +- `CollaborativeTransferParams` describes fully resolved user/provider inputs + and explicit recipient, change, carrier, and metadata outputs. +- `BuildUnsignedCollaborativeTransfer()` deterministically builds and validates + the one-recipient collaborative `DD_TX_TRANSFER` without keys or fallback + defaults. + +### src/paymaster/psbt.{h,cpp} +- Creates and validates the committed collaborative PSBT template, explicit + input roles, allowed fields, signature stage, and `SIGHASH_DEFAULT` policy. +- `IsInputOwnedBy()` is the common role boundary used by wallet signing. +- `ClientAuthorizationManifest` and `ProviderAuthorizationManifest` bind each + side's complete local authority. The final validator reconstructs the trusted + PSBT and verifies txid, wtxid, every witness and every script against the + actual prevouts before execution. + +### src/paymaster/protocol.{h,cpp} +- Defines chain-bound `PaymentIntent`, `PaymasterQuote`, verified capacity + inputs, and monotonic `PaymasterResult` artifacts. +- Validates exact request/session/provider/policy/fee/input/output bindings and + constructs provider quotes from locally verified parameters. + +### src/paymaster/provider.{h,cpp} +- Defines provider identity/settings/policy records, admission and operational + pool entries, successor provenance, and readiness summaries. Pool V2 adds + `PENDING_SUCCESSOR`, `RELEASED`, and `INVALIDATED` lifecycle states plus an + origin commit/maintenance binding. +- Provider settings V2 persist the recommended automatic queue-processing mode + and optional autostart. Settings are accepted only at V2; V1 records fail + closed and are neither migrated nor overwritten by a settings update. +- Enforces funding-model, fee-rate, amount, TTL, and positive absolute network + fee caps; plans 0-cent, carrier, and normal provider-fee outputs. +- Defines finite `ProviderSafetyPolicy`/budget and `ClientSafetyPolicy`/fee + ledgers. Worst-case fees, rolling hour/day totals, active quotes, request + rates, and monotonic accounting time are validated independently of remote + input. +- Defines `ProviderLiquidityPolicy`, restartable maintenance records/ledger, + and preview-bound carrier-withdrawal plans. Automatic maintenance has explicit + DGB/carrier targets and finite per-transaction/hour/day fees; zero never means + unlimited paid maintenance. +- Defines the genesis/provider-bound `ProviderFinanceEvent`, UTC daily totals, + durable finance ledger, and path-free provider backup-reminder metadata. + Confirmed income and operating costs are derived idempotently while active + pool principal remains separate wallet-owned capital. + +### src/paymaster/directory.{h,cpp} and validation.{h,cpp} +- Parses signed short-lived announcements, verifies identity/admission + envelopes and chainstate-backed proof transactions, and maintains the bounded + local provider directory. +- Admission validation rejects both spent chainstate entries and current + mempool conflicts. Bounded directory reads rotate over active entries while + copying only the requested full announcements. +- Admission and operational liquidity remain separate; only admission proofs + enter global gossip. + +### src/paymaster/wire.{h,cpp} and manager.{h,cpp} +- Defines bounded capacity, quote, submit, and result messages for isolated + `PAYMASTER` connections and validates cheap size/time/count constraints before + signature work. +- `Manager` owns the node directory plus bounded, replay-protected, + peer/session/wallet-scoped direct-message queues and provider runtime state. +- Its wallet-scoped work guard serializes automatic scheduler work with manual + expert RPCs and exposes stopped, unlock/readiness wait, active, manual, + drain-only, and error service states without persisting a passphrase. +- Protocol V5 makes `PMCAPREQ`/`PMCAPRESP` mandatory before intent disclosure, + rejects every older Paymaster wire format, persists semantic replays + independently of peer ID, and binds a later quote to the exact validated + resource snapshot. Paymaster persistence is also current-only: old records + fail closed without migration, deletion, or implicit replacement. + +### src/paymaster/client.{h,cpp} and reputation.{h,cpp} +- Builds bound client intents, validates quote responses, calculates exact + local total cost, and selects deterministically with privacy and local + reputation constraints. +- Reputation is local, day-bucketed, and treats neutral failures separately + from provider failures. + +### src/paymaster/reservation.{h,cpp} +- Versioned session, attempt, reservation, authorization, final-commit, + self-recovery, and idempotency-tombstone records. +- Every record is accepted only at its exact `CURRENT_VERSION`; version fields + remain for future format development, not for range-based compatibility. +- Defines legal session/attempt transitions including wallet-unlock waits, + ambiguous signed states, mempool-vs-confirmed recovery, and finality. +- Persists validated Capacity snapshots/resource bindings, authorization + manifests, provider budget reservations, client fee reservations, replay + evidence, and alternative-provider recovery state. + +### src/paymaster/sponsorship.{h,cpp} +- Validates public/restricted sponsorship descriptors, one-payment capability + bindings, zero service fee, and hash-only durable authorization records. + +--- + ## RPC Interface ### src/rpc/digidollar.h @@ -587,7 +727,10 @@ This is the granular file index for all DigiDollar and Oracle source code. Read - `getdigidollarstatus()` → stale header declaration only; no implementation or RPC registration exists in the current source tree - **Core Transactions:** - `mintdigidollar()` → mints DD by locking DGB collateral with specified lock tier - - `senddigidollar()` → sends DD to another address (confirmed inputs only, RC32+) + - `senddigidollar()` → sends DD to another address (confirmed inputs only, + RC32+); its optional Paymaster exact-outflow mode can deduct the rounded + service fee from a fixed gross amount and atomically bind all ordinary + spendable DD for an explicit wallet sweep - `sendmanydigidollar()` → sends DD to multiple recipients in one transaction (RC32+, commit `9143bed9b9`) - `redeemdigidollar()` → redeems DD to unlock DGB collateral - `listdigidollarpositions()` → lists all collateral positions and minted DD @@ -614,6 +757,87 @@ This is the granular file index for all DigiDollar and Oracle source code. Read ### src/rpc/digidollar.cpp - Full implementation of all DD RPC commands - Integrates with wallet, oracle, health monitoring systems +- Adds node `listpaymasters` and the optional seventh + `senddigidollar` options argument, following upstream `amount_unit` (`fee_mode`, canonical `request_id`, hard + service-fee cap, privacy profile, selection mode, explicit + `subtract_paymaster_fee_from_amount`, and `send_all_spendable_dd`). Direct DGB + funding keeps the full recipient amount; exact-gross inversion is used only + for an actual Paymaster path. + +### src/wallet/rpc/paymaster*.{h,cpp} +- `paymaster.cpp` and `paymaster_internal.h` contain private serialization, + validation, readiness, accounting, and exact-final helpers shared across the + domain translation units; `paymaster.h` remains the public registration and + wallet-lifecycle surface. +- `paymaster_send.cpp/h` owns `senddigidollar` option decoding, durable-session + resumption, balance/fee-funding checks and Paymaster result presentation. + Ordinary amount/address validation and direct transfer stay in the original + RPC handler; argument positions and validation order are unchanged. +- `paymaster_client.cpp` owns persistent session inspection/resolution, + alternative recovery, role-limited PSBT processing, submit, and client result + handling. +- `paymaster_discovery.cpp` owns offer discovery, automatic/manual quote + selection, reputation, pool inspection, reservations, and quote cancellation. +- `paymaster_provider.cpp` owns provider identity, policies, safety limits, + liquidity preparation/rebalancing/withdrawal, finance reporting, backup + acknowledgement, and provider status. +- `paymaster_processing.cpp` owns the bounded provider handlers for result, + submit, Capacity, quote, and recovery messages. +- `paymaster_runtime.cpp` owns provider start/stop and the automatic service + scheduler; `paymaster_integration.cpp` owns wallet reconciliation and durable + equivocation-inbox maintenance. +- Client RPCs expose offers, bounded persistent-session listing and exact + status/resolution, reputation, quote advancement, role-limited PSBT + processing, submit, and result handling. Refresh responses include one + authoritative attempt/artifact/action snapshot so Qt never merges state from + different observations. +- Provider RPCs create the BIP86 identity, configure policy/enablement, prepare + and rebalance isolated pools, inspect/cancel reservations, process inboxes, + create non-gossiped restricted sponsorship descriptors, and explicitly + start/stop wallet-scoped operation. +- `setpaymasterruntimesettings` persists automatic/manual operation and optional + autostart. `getpaymasterinfo`/`startpaymaster` expose the effective mode, + autostart, service state, privacy-neutral service error, and start-time safety + policy; `getpaymasterinfo.settings_present` distinguishes a persisted + disabled configuration from identity-only state. Manual processing RPCs fail + while automatic servicing is active. Advertised operating-policy changes + require a stopped provider and hold the manager work guard through the + wallet commit, preventing an autostart race and stale peer announcements. +- `setpaymasterliquiditypolicy` persists automatic replenishment, exact + admission/operational DGB and carrier targets, explicit paid-maintenance + approval, and finite fee ceilings. `getpaymasterliquiditystatus` reports + ready/pending/missing slots, maintenance state/accounting, carrier principal, + and withdrawable carrier excess; `getpaymasterinfo` embeds that summary. +- Manual preparation and rebalancing return a policy/pool-bound `plan_id` that + must be echoed for execution. They share the wallet provider-work guard with + automatic maintenance, reject operational DGB slots below the advertised fee + ceiling as target capacity, and retain higher-value usable entries first when + reducing a pool. Provider safety-policy commits use the same guard so a + scheduler reservation cannot straddle a limit change. +- Automatic service reconciliation promotes confirmed successors, resumes + restartable DGB/carrier maintenance, creates only missing targets, and blocks + new requests until confirmed capacity is available while still allowing safe + completion of durable submissions. +- `withdrawpaymastercarrier` binds execution to the unchanged latest preview. + `all_excess` retains exactly 1.00 DD in each replacement carrier and combines + only wallet-owned excess under the maintenance budget; `release_slot` is a + stopped-provider, fee-free local release that reduces the operational target. +- `getpaymasterfinancestatus` reconciles exact wallet-native provider income, + DGB costs, pool capital, UTC period summaries and bounded cursor-paginated + events. Current Oracle valuation is optional and never replaces native + accounting. `acknowledgepaymasterproviderbackup` records an explicit external + full-wallet-backup acknowledgement without storing a path. +- New client quote creation and automatic `senddigidollar` Paymaster fallback + share the provider's `-prune=0`, synchronized `-txindex=1`, node-readiness, + activation, BIP324-v2, and mainnet message-capture gates. High privacy also + preflights onion-proxy presence, stream isolation, IP logging, and message + capture before creating a session or reservation. +- `getpaymasteroffers` can preview a fixed gross amount. It returns the exact + recipient, service-fee, and total values for mathematically exact candidates; + cent-rounding gaps fail before input reservation. `requestpaymasterquote` + retains its exact-recipient semantics. +- Shared response formatting exposes authoritative session/pending/finality, + fee, provider, quote/policy, broadcast, and confirmation state. ### src/rpc/digidollar_transactions.{h,cpp} *(legacy / unregistered)* - 9-entry command table (`digidollar_transaction_commands` at `src/rpc/digidollar_transactions.cpp:384–395`) defining `getdigidollarinfo`, `getdigidollaraddress`, `getdigidollarbalance`, `mintdigidollar`, `transferdigidollar`, `redeemdigidollar`, `getredemptioninfo`, `listredeemablepositions`, `createrawddtransaction`. (`setmockoracleprice` is NOT in this table — comment at line 393 explicitly delegates to `rpc/digidollar.cpp`.) @@ -626,6 +850,9 @@ 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/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 @@ -650,7 +877,14 @@ This is the granular file index for all DigiDollar and Oracle source code. Read - `UpdatePositionStatus(dd_timelock_id, active)` → updates position active flag in DB - **Balance Tracking (Task 5.2):** - `GetDDBalance(addr)` → balance for specific address or total - - `GetTotalDDBalance()` → confirmed DD balance across all addresses + - `GetTotalDDBalance()` → gross confirmed wallet-owned DD, including + Paymaster-reserved client inputs and provider carrier outputs + - `GetDDBalanceSummary()` → one-lock snapshot of gross confirmed, + ordinary spendable, Paymaster-reserved, and pending DD + - `GetSpendableDDBalance()` → confirmed DD selectable by ordinary + send/redeem flows; excludes every durable Paymaster reservation + - `GetPaymasterReservedDDBalance()` → confirmed wallet-owned DD protected + for active Paymaster sessions or live provider carrier pools - `GetPendingDDBalance()` → unconfirmed but trusted DD balance - `ScanForDDUTXOs()` → full UTXO scan at startup (expensive) - `ProcessTransactionForDD(tx, txid)` → incremental UTXO update per block @@ -732,6 +966,72 @@ This is the granular file index for all DigiDollar and Oracle source code. Read - `UnSelect(output)` / `UnSelectAll()` → removes selections - `ListSelected()` → returns vector of selected outpoints +### src/wallet/paymasterstore*.{h,cpp} +- `paymasterstore.cpp` and `paymasterstore_internal.h` provide shared + fail-closed record validation, canonical artifact decoding, Capacity binding, + and atomic helper boundaries; `paymasterstore.h` remains the public store API. +- `paymasterstore_client.cpp` owns client sessions, reservations, Capacity + snapshots, intent state, and safe fallback transitions. +- `paymasterstore_provider.cpp` owns provider quote admission, authorization, + expiry, capacity lifecycle, and quote-equivocation state. +- `paymasterstore_finalization.cpp` owns user authorization, provider commits, + exact results, and final client observations. +- `paymasterstore_recovery.cpp` owns self-recovery and independent-provider + recovery state; `paymasterstore_reputation.cpp` owns outcomes, reputation, + and durable equivocation evidence. +- `paymasterstore_reconciliation.cpp` owns exact-final rebroadcast validation, + wallet/mempool/reorg reconciliation, and safe pruning. +- `PaymasterStore` atomically persists sessions, exact append-only attempts, + reservations, authorizations, provider commits/results, self-recovery raw + transactions, outcome markers, and permanent idempotency tombstones. +- Integrates Paymaster and ordinary wallet locks, restores state at startup, + reconciles mempool/confirmation/reorg observations, and prunes session detail + only after the configured safety depth. +- Before a new provider signature, the RPC path rechecks every committed + template input against chainstate plus mempool. A template already spent by + client recovery is atomically marked `REJECTED`, its unused provider pool + entries are released, and a signed durable rejection result is retained. +- Quote, pool and fee-budget reservations are written and transitioned as one + database unit. Policy changes remain strict for new authority while an exact + already-authorized or committed artifact retains only the historical binding + required for safe completion; orphan half-writes fail closed. +- The exact provider commit atomically registers manifest-bound wallet-owned + carrier return and sufficiently large DGB change as `PENDING_SUCCESSOR` pool + entries. Confirmation, reorg, conflict, and startup reconstruction use the + persisted origin key, so a 1.00 DD carrier can accumulate service fees and be + reused without a separate carrier transaction. + +### src/wallet/load.{h,cpp} +- Schedules wallet-wide Paymaster reconciliation every 30 seconds, alongside + the existing startup and chain-tip hooks. This drives the store's atomic + unsigned-quote and Capacity-reservation expiry even without a subsequent RPC + or block notification, while keeping loaded-wallet lifetimes safe through a + `GetWallets()` shared-pointer snapshot. +- Schedules a short bounded provider-service pass for every loaded wallet. + Automatic providers perform at most one request step and one submit step per + pass; manual providers remain operator-driven, locked wallets pause without + consuming work, and opt-in autostart waits for readiness/unlock. +- Startup and scheduled reconciliation recover historical pool successors and + durable maintenance transactions. The automatic pass repairs missing targets + before accepting new requests and exposes maintenance approval, + replenishment, and confirmation-wait service states. + +### src/wallet/paymasteridentity.{h,cpp} +- Creates and loads the wallet-managed untweaked BIP86/x-only provider identity + from an eligible descriptor wallet with local private keys. +- Rejects legacy, watch-only, and external-signer provider wallets. + +### src/wallet/paymasterpsbt.{h,cpp} +- Signs only the expected user or provider input role using wallet-owned keys, + after strict collaborative-template validation. +- Produces user input-control proofs without exposing private key material. + +### src/wallet/paymasterprovider.{h,cpp} +- Wallet-specific provider proof construction, announcement generation, + operational slot reservation, and policy/pool readiness integration. +- Persists and validates wallet-local liquidity policy and maintenance-ledger + state alongside the existing provider settings, safety policy, and pools. + --- ## Index @@ -823,7 +1123,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 @@ -836,6 +1139,20 @@ Files outside the DigiDollar/Oracle directories that contain DD integration code - âš ī¸ Price/consensus/MuSig2/getoracles/heartbeat handlers all use `IsOracleP2PActive`; `ORACLEHEARTBEAT` is also signed, roster-limited, deduplicated, and rate-limited - âš ī¸ `GETORACLES` is rate-limited to 10 requests/minute/peer, accepts epochs in `[current_epoch - 24, current_epoch + 1]`, and replies with matching fresh `ORACLEPRICE` messages plus recent `ORACLEHEARTBEAT` messages - âš ī¸ Oracle message validation, rate limiting, and Misbehaving scoring for invalid MuSig2/heartbeat messages +- âš ī¸ Paymaster discovery messages are handled on ordinary upgraded peers; + capacity/quote/submit/result messages are accepted only on isolated + `ConnectionType::PAYMASTER` peers with bounded pre-crypto validation. Every + Paymaster handler shares the DigiDollar activation gate, and direct request + versus response direction is enforced before deserialization and queueing. + +### src/net.h / src/net.cpp / src/node/connection_types.{h,cpp} +- âš ī¸ Defines the short-lived `PAYMASTER` connection type, v2-only/no-fallback + policy, high-privacy onion and Tor-isolation constraints, and connection + reporting. + +### src/node/context.{h,cpp} +- âš ī¸ Owns the node-level `Paymaster::Manager`; wallet contexts reference it + without moving durable wallet state into `NodeContext`. ### src/node/transaction.cpp - âš ī¸ DD-aware transaction broadcast handling, oracle data relay @@ -859,6 +1176,15 @@ Files outside the DigiDollar/Oracle directories that contain DD integration code ### src/wallet/walletdb.h / src/wallet/walletdb.cpp - âš ī¸ `WalletBatch` DD persistence methods: `WriteDDBalance()`, `WriteDDTimeLock()`, `ReadDDTimeLock()`, `WriteDDTransaction()`, `WriteDDOwnerKey()`, `WriteDDAddressKey()`, `EraseDDTimeLock()` (74 references) - âš ī¸ `WalletBatch` encrypted DD key methods (T4-03a): `WriteCryptedDDOwnerKey()`, `ReadCryptedDDOwnerKey()`, `EraseCryptedDDOwnerKey()`, `WriteCryptedDDAddressKey()`, `ReadCryptedDDAddressKey()`, `EraseCryptedDDAddressKey()` +- Paymaster methods and record-key definitions live in `src/wallet/paymasterdb.cpp`. + They remain members of `WalletBatch`, sharing its original database batch, + write accounting and explicit transaction. No storage migration is involved. +- âš ī¸ Paymaster DB methods persist versioned provider pools, liquidity policy, + restartable maintenance ledger, and the latest carrier-withdrawal preview so + successor/maintenance recovery and exact plan execution survive restart. +- âš ī¸ Paymaster DB methods also persist a chain/provider-bound finance ledger, + derived UTC daily totals and provider backup-reminder timestamps. The full + wallet database is required to preserve this off-chain provider metadata. - âš ī¸ DB keys: `DD_CRYPTED_ADDRESS_KEY` ("ddcaddrkey"), `DD_CRYPTED_OWNER_KEY` ("ddcownerkey") ### src/wallet/spend.cpp @@ -886,11 +1212,92 @@ 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 +- `DigiDollarTab` → main DD tab widget containing the activation overlay and 7 + client tabs. Its wallet-scoped Paymaster operator console is hidden by default + and can be enabled independently in the Qt wallet options. + +### src/qt/paymasterwidget.cpp/h +- `DigiDollarPaymasterWidget` exposes the small embedding interface used by the + DigiDollar tab; `CreatePaymasterWidget` constructs the private implementation. +- The Paymaster console provides setup, identity/policy/pool controls, + start/stop, readiness, reservations, warnings, and recovery actions. Its + wallet-scoped first-visit gate offers a recommended guided path or an + explicitly acknowledged expert path. Configuration, Safety limits, and + Liquidity remain locked until a path completes, while Activity and recovery + remains reachable to avoid hiding durable work. Existing Core provider state + automatically bypasses the first-visit gate. + Its + overview explains the provider role, translates readiness codes into guided + next steps, and keeps raw backend diagnostics in collapsed technical details. + Configuration explains both funding models and their valid simultaneous + public use, enforces the sponsored-only restricted combination in the form, + reloads the wallet's persisted policy without overwriting unsaved edits, + summarizes values in ordinary units, and can restore recommended unsaved + policy defaults. Safety limits provide visible setup guidance and + per-section safe defaults for provider funding models, quote throttles, and + client fee budgets. Scroll pages use the active Qt palette, and operator + spin boxes and combo boxes deliberately ignore mouse-wheel value changes so + wheel input remains available for page scrolling. Liquidity presents the + admission/operational and DGB/carrier split as a guided first-time workflow, + restores policy-aware recommended targets without moving funds, formats + preview results in ordinary DGB/DD units, and enables execution only for the + exact target set most recently previewed. It also explains automatic + successor reuse, requests one-time approval for finite paid maintenance, + reports pending/confirmed/missing targets and maintenance costs, and exposes + separate preview-first actions for carrier excess and full slot release. + Activity and recovery shows the + automatic service state and queue summary in the recommended mode; explicit + one-at-a-time processing controls appear only in manual expert mode. It + distinguishes non-spending request handling from potentially spending + submitted-payment handling, summarizes durable reservations without leading + with identifiers, explains restart recovery, and keeps raw RPC results + collapsed by default. + The operator navigation includes a green-theme Finances page with native DD + income, DGB operating costs, current-price result estimate, model breakdown, + wallet-owned pool capital, 250-row cursor pages, privacy-limited asynchronous + complete CSV export with atomic file replacement, + and links to the existing preview-first liquidity controls. Overview includes + a compact finance summary. Backup notices on Overview, Finances and a newly + completed identity setup invoke WalletView's existing full-wallet backup + dialog or explicitly acknowledge another full-wallet backup flow; no path is + persisted and the reminder does not block provider start. + The guided setup dialog uses a non-native, theme-controlled classic wizard + surface so its page text and controls remain readable under both dark and + light Windows themes. It covers prerequisites, identity, service model, + offer limits, finite safety profiles (including fully editable Custom + aggregate, request, and maintenance limits), policy-aware liquidity, + validation, and a final funding review. Its first page names and pins the current wallet, + recommends a dedicated provider wallet, requires explicit confirmation, and + directs operators through the normal wallet menu when another wallet should + be created or selected. Wallet eligibility is checked again before any + mutation. It requires complete authoritative provider/safety/liquidity + snapshots, imports existing persisted values on reopen, and provides an + explicit restore-defaults action on every editable page. Applying the + reviewed plan runs an asynchronous, retryable sequence that first disables + an existing enabled provider, reuses or creates the provider identity, + persists a safety bridge when required, then the operating, final safety, + and liquidity policies, rechecks the live pool, and creates only + still-missing outputs after a separate funding confirmation. Runtime, + autostart, and requested enablement are restored last. Completed steps remain + idempotently resumable after an error, while closing before Apply writes + nothing. The completion page states that no additional Save actions are + required and returns to an automatically refreshed Overview while new + liquidity confirms. Fresh setup defaults to autostart off; a deliberately + selected enabled/autostart combination may start later when readiness passes. + Qt's Stop action first disables saved autostart when necessary, and disabling + the provider configuration persistently stops the runtime. ### src/qt/digidollaroverviewwidget.cpp/h -- `DigiDollarOverviewWidget` → DD balance overview, system health display +- `DigiDollarOverviewWidget` → DD balance overview and system health display. + With no Paymaster-reserved DD it preserves the original compact balance + layout. While a client session or live provider carrier pool protects DD, it + temporarily splits the display into ordinary available, Paymaster-reserved, + and gross wallet total; wallet USD valuation continues to use the gross + wallet-owned amount. ### src/qt/digidollarmintwidget.cpp/h - `DigiDollarMintWidget` → mint DD interface with tier selection and collateral calculator @@ -899,7 +1306,24 @@ Files outside the DigiDollar/Oracle directories that contain DD integration code - `DigiDollarPositionsWidget` → displays DDTimeLock positions with lock status and health ### src/qt/digidollarsendwidget.cpp/h -- `DigiDollarSendWidget` → send DD to address with amount validation +- `DigiDollarSendWidget` owns editable recipient/amount/comment fields, + coin-control, amount/address validation and direct DGB-funded DD sending. +- Its concrete Paymaster child reads an immutable form snapshot and calls + limited presentation hooks for field locking, privacy, messages and completion. + The form does not retain a second copy of the Paymaster session state. + +### src/qt/paymastersendwidget.cpp/h +- `PaymasterSendWidget` owns DGB/Automatic/Paymaster fee choices, offer and safety + controls, persistent-session presentation, polling, exact-offer confirmation, + retries and recovery. It reuses `WalletModel::executeRpcAsync` and the existing + confirmation guards; Core remains authoritative for spending and persistence. +- Wallet-generation guards and Qt lifetime guards discard stale callbacks. + Existing translation context, object names and two-stage authorization remain. +- Paymaster-specific test transports and snapshot injection belong to this child. + +### src/qt/walletmodel.cpp/h +- `executeRpcAsync()` provides the queued wallet-RPC bridge used by Paymaster UI + operations so wallet unlock contexts are not retained across network waits. ### src/qt/digidollarreceivewidget.cpp/h - `DigiDollarReceiveWidget` → generate/display DD receive addresses @@ -934,6 +1358,16 @@ present in the tree but not compiled into the current unit-test binary. | File | Coverage Area | |------|--------------| +| `paymaster_types_tests.cpp` | Strong amount types, fee rounding/bounds, exact gross-to-recipient inversion (including cent-rounding gaps), and canonical request UUIDs | +| `paymaster_txbuilder_tests.cpp` | Deterministic collaborative transfer construction, prevout resolution, conservation, carrier/change, and fee bounds | +| `paymaster_psbt_tests.cpp` | Exact PSBT template, input roles, signature stages, gross/recipient/fee authorization binding, unexpected-field rejection, and `SIGHASH_DEFAULT` | +| `paymaster_protocol_tests.cpp` / `paymaster_wire_tests.cpp` | V5 order/capacity bindings, explicit no-downgrade parsing, semantic replay, and bounded envelopes | +| `paymaster_provider_tests.cpp` / `paymaster_recovery_tests.cpp` | Finite safety and maintenance budgets, pool-successor provenance/lifecycle, carrier-withdrawal plans, atomic reservation accounting, provider-finance idempotency/reorg/model totals, malicious-counterparty rejection, and distinct-provider recovery | +| `paymaster_protocol_tests.cpp` | Intent/quote/result binding, monotonic result sequences, exact final artifacts, and optional-field serialization | +| `paymaster_provider_tests.cpp` / `paymaster_sponsorship_tests.cpp` | Provider policy, BIP86 identity shape, separated pools, fee boundaries, public/restricted sponsorship and durable hash-only authorization | +| `paymaster_directory_tests.cpp` / `paymaster_wire_tests.cpp` | Signed announcement replacement/expiry, bounded rotating directory reads, admission mempool conflicts, bounded direct messages, owner/session scoping, replay and resource limits | +| `paymaster_client_tests.cpp` / `paymaster_reputation_tests.cpp` | Exact additive and fixed-gross candidate selection, cost ordering, deterministic ties, privacy tolerance, quote binding, cooldowns, and local neutral/provider outcomes | +| `paymaster_reservation_tests.cpp` | Session and attempt state transitions, ambiguous reservations, mempool non-finality, and immutable terminal states | | `digidollar_activation_tests.cpp` | Buried-height activation gating and deployment status checks (BIP9 state-machine cases removed in the v9.26.5 burial) | | `digidollar_activation_wave12_tests.cpp` | Wave 12 activation boundary and deployment predicate regressions | | `digidollar_address_tests.cpp` | DD address encoding/decoding, version bytes, network-specific prefixes, validation | @@ -1077,14 +1511,39 @@ 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/`) | File | Coverage Area | |------|--------------| +| `paymaster_wallet_identity_tests.cpp` | Descriptor/local-key eligibility and BIP86 identity persistence; legacy, watch-only, and external-signer rejection; fail-closed coin selection for unreadable reservation/pool safety records | +| `paymaster_wallet_psbt_tests.cpp` | Wallet ownership proofs and signing only the requested collaborative input role | +| `paymaster_wallet_store_tests.cpp` | Atomic sessions/reservations/commits, append-only artifacts, exact retry, tombstones, self-recovery, restart, mempool, confirmation, reorg, retention, and no-mutation handling of unreadable expiry/reliability records | +| `paymaster_wallet_security_tests.cpp` | Policy-change liveness, manifest/budget binding, the automated signature → DB commit → wallet insertion → broadcast restart matrix, final-witness validation, and malicious client/provider persistence failures | | `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,17 +1551,34 @@ 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.) | +| `paymasterwidgettests.cpp/h` | Fee modes/caps, fixed-gross deduction and wallet-empty summaries, offers, persistent session states, provider controls, finance cards/details/capital and backup actions, recovery, and asynchronous UI wiring | +| `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 | + +The original Qt suite retains DD behavior and boundary integration tests; +`digidollartestutil.cpp/h` provides the single shared descriptor-wallet fixture. +`src/wallet/test/paymaster_wallet_load_tests.cpp` owns the seven Paymaster +maintenance/load regression cases formerly embedded in `walletload_tests.cpp`. ### 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 -`digidollar_wave20_oracle_p2p.py`. +Authoritative registration is the `BASE_SCRIPTS` list in +`test/functional/test_runner.py`. `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 | |------|--------------| +| `wallet_paymaster_readiness.py` | Pre-session client/provider rejection with disabled Paymaster or inactive DigiDollar, pruning, missing txindex or BIP324, and unsafe high-privacy logging/proxy/capture configuration | +| `p2p_paymaster.py` | Paymaster activation/negotiation, V1 direct-message rejection and size limits; bounded malformed announcements over two real V2 peers across reconnects with ping and block-relay liveness | +| `wallet_paymaster_lifecycle.py` | Abrupt provider-process termination after client authorization, older authorized-client SQLite backup rejection without a duplicate payment, plus automatic DGB/carrier replenishment, pending-target accounting, restart idempotency, locked-wallet autostart pause, unlock continuation, confirmation promotion, and absence of duplicate maintenance transactions | +| `wallet_paymaster_rpc.py` | HTTP authentication/method-whitelist and mixed-batch rejection, wallet-scoped session lookup/cancellation, direct RPC contracts plus automatic carrier-maintenance gates: deliberate zero target, disabled automation, missing paid-maintenance approval, exactly one pending replacement, restart without duplication, confirmation promotion, and rolling-budget exhaustion without another transaction | +| `wallet_paymaster_provider.py` | Descriptor-provider identity/policy/pools, discovery, high-privacy Clearnet/multi-attempt rejection before session creation, user-paid client without DGB, exact 50.00-DD wallet sweep to 49.75-DD recipient plus 0.25-DD provider fee, cent-rounding-gap rejection, public/restricted sponsorship, provider-finance periods/pagination/native accounting and backup acknowledgement, exact result processing, recovery, wallet lock, restart persistence, runtime cleanup on wallet unload/reload, production-log redaction of payment artifacts, and optional official-pre-Paymaster third-node relay/validation with no Paymaster-announcement traffic | +| `wallet_paymaster_offer_selection.py` | Three independent current-version daemons with two differently priced USER_PAID providers and one DGB-less client; verifies both visible offers, exact-total ordering, monotonic price replacements, stale-announcement expiry/fresh refresh, automatic cheapest selection, selected-provider-only capacity/intent/reservations, one completed transfer, and no pool/budget/finance mutation at the unselected provider | +| `wallet_paymaster_failover.py` | Four current-version daemons with two independent providers, one relay, and two client wallets contending for a single cheap operational slot; verifies explicit pre-signature fallback, immutable DD inputs across providers, sequential direct-channel replacement, the post-user-PSBT no-fallback boundary, exactly one standby-provider payment, and clean release of the unsigned contender | +| `wallet_paymaster_reorg.py` | Three current-version daemons create a confirmed Paymaster branch and a longer transaction-free competing branch; verifies session `CONFIRMED`→`MEMPOOL` rollback, finance `confirmed`→`pending`, successor liquidity `available`→`pending_successor`, and exact-txid reconfirmation without duplicate finance events | +| `p2p_paymaster_v9_26_5_bridge.py` | Current provider and client separated only by an official v9.26.5 node; proves ordinary DGB/DD relay and old-node mining, no Paymaster-gossip relay or eligible offer across the old bridge, fail-closed selection, and discovery restoration after a direct current-current link | +| `wallet_v9_26_5_compatibility.py --legacy-wallet/--descriptors` | Bidirectional official-v9.26.5/current DGB wallet transfers, descriptor-only bidirectional DD transfers, relay to both mempools, identical raw transactions, confirmation by the receiving version, exact wallet accounting/history, raw v9.26.5 wallet-directory loading by the current daemon, preserved encryption/active-DD state, post-upgrade signing and spending, restart persistence, and a non-Paymaster round trip back to v9.26.5 | +| `wallet_v9_26_5_inplace_upgrade.py` | Official-v9.26.5 descriptor wallets upgraded by replacing the daemon on the unchanged datadir; verifies blocks/Chainstate/txindex, wallet-independent `mempool.dat` loading, encrypted multi-wallet pending DGB/DD state, exact raw transactions and histories, current-version confirmation, active-position redemption, and final restart persistence | | `digidollar_activation.py` | Basic activation of DD features at the buried height on regtest (BIP9 signaling lifecycle removed in the v9.26.5 burial) | | `digidollar_activation_boundary.py` | Activation edge cases: exact height, off-by-one, pre/post activation behavior | | `digidollar_activation_multinode.py` | Multi-node activation state and deployment synchronization | @@ -1185,14 +1661,33 @@ 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 | - -### 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`. +| `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/`, `src/wallet/test/fuzz/`) + +Fuzz target registration is authoritative in `src/Makefile.test.include` and +the `PRINT_ALL_FUZZ_TARGETS_AND_ABORT=1` output from the built fuzz binary. +This branch also registers Paymaster protocol, lifecycle and wallet persistence targets. Current DigiDollar-specific fuzz source inventory: @@ -1210,6 +1705,24 @@ Current DigiDollar-specific fuzz source inventory: | `digidollar_validation_deep.cpp` | Deep mint/transfer/redeem validation fuzzing | | `digidollar_wave15_parsers.cpp` | Wave 15 parser hardening for DD metadata and script formats | +Current Paymaster fuzz source inventory: + +| File | Coverage Area | +|------|--------------| +| `paymaster_pool_lifecycle.cpp` | Stateful provider-pool and maintenance-ledger operation ordering, successor creation/confirmation/reorg/conflict, restart serialization, target accounting, withdrawal binding, and duplicate-operation resistance | +| `paymaster_wire.cpp` | Announcement and all direct-message deserialization plus bounded envelope-validation paths | +| `paymaster_stateful.cpp` | Stateful Capacity → Intent → Quote → Submit → Result → Recovery transitions, budgets, replay, and timestamp/amount boundaries | +| `wallet/test/fuzz/paymaster_persistence.cpp` | Raw wallet-record decoding across all 35 status-aware Paymaster codecs plus property mutations for session-id, template-commitment, and unsigned-txid referential integrity | + +### Paymaster Assurance Workflows (`.github/workflows/`) + +| File | Coverage Area | +|------|--------------| +| `codeql.yml` | Wallet-enabled C++ CodeQL build of `test_digibyte`, including Paymaster wallet, persistence, provider, and RPC translation units | +| `paymaster-security.yml` | Pull-request/weekly ASan+UBSan selection of all `paymaster_*` suites (including wallet load), uploaded test logs, and scheduled/manual campaigns for the four Paymaster fuzz targets; headless build, not Qt coverage | +| `dependency-review.yml` | Pull-request dependency-diff review with high-severity failure threshold | +| `sbom.yml` | Release/manual export of the repository dependency graph as SPDX JSON | + Current oracle/MuSig2 fuzz source inventory: | File | Coverage Area | @@ -1233,3 +1746,22 @@ 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 | + +### Finite pool setup and recovery + +- `src/wallet/rpc/paymaster_provider.cpp`: `preparepaymasterpool` persists bounded + setup approvals and explicit legacy mappings; `RunPaymasterPoolPreparation` + continues exact steps independently of service startup; disabled providers + can accept explicit approval while execution waits for enablement. +- `src/paymaster/provider.{h,cpp}`: V4 maintenance records add setup request and + authorization bindings while retaining V3 readability and recurring budgets. +- `src/wallet/rpc/paymaster.cpp`: shared maintenance reconciliation recovers + unique wallet transactions and exact setup fees into the provider pool, + including reconfirmation after a conflict. Finance recovery also reads the + provider-bound retirement marker stored with signed wallet transactions. +- `test/functional/wallet_paymaster_pool_setup.py`: Dandelion, restart, funding, + fee, cancellation, legacy adoption, recurring refill and conflicting-reorg + regressions; registered once in the runner. diff --git a/REPO_MAP_GUIDE.md b/REPO_MAP_GUIDE.md index 48aafeb7cea..6b80989fef3 100644 --- a/REPO_MAP_GUIDE.md +++ b/REPO_MAP_GUIDE.md @@ -115,6 +115,16 @@ Index ALL DigiDollar and Oracle source files, wherever they live in the repo. | `src/oracle/musig2_aggregator.{cpp,h}`, `musig2_session.{cpp,h}`, `musig2_session_manager.{cpp,h}`, `musig2_orchestrator.{cpp,h}`, `musig2_oracle_participation.{cpp,h}`, `musig2_messages.h`, `musig2_session_mining.h` | MuSig2 v0x03 nonce/partial-sig session lifecycle | | `src/primitives/oracle.cpp/h` | Oracle data structures (`COraclePriceMessage`, `COracleBundle`, `OracleNodeInfo`, `SelectOraclesForEpoch`, MuSig2 v0x03 fields) | +**Paymaster source files:** + +| Location | Files | +|----------|-------| +| `src/paymaster/` | Non-consensus collaborative builder, protocol objects, provider policy/pools, directory, wire validation, manager, sponsorship, reservations, selection, and local reputation | +| `src/wallet/paymaster*.{cpp,h}` | Wallet identity, role-limited PSBT signing, provider operation, and durable session/reservation/commit/recovery storage | +| `src/wallet/rpc/paymaster.{cpp,h}` | Wallet-scoped Paymaster client and provider RPCs | +| `src/rpc/digidollar.cpp` | Node `listpaymasters` and additive `senddigidollar` options integration | +| `src/qt/digidollarsendwidget.*`, `src/qt/digidollartab.*`, `src/qt/walletmodel.*` | Asynchronous client flow and wallet-scoped provider console | + **Also find scattered references:** ```bash rg -l "digidollar|DigiDollar" src \ @@ -129,10 +139,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`, `src/test/paymaster_*.cpp` | Registered suites | activation, address, DCA, ERR, MuSig2, Paymaster protocol/network/provider/client coverage, and redteam audit | +| `src/wallet/test/digidollar_*.cpp`, `src/wallet/test/rh59_*.cpp`, `src/wallet/test/paymaster_*.cpp` | Registered wallet suites | persistence, wallet security, DD spendability, Paymaster identity/PSBT/store/recovery, and lock-bypass | +| `src/qt/test/digidollarwidgettests.cpp`, `src/qt/test/digidollarwave19widgettests.cpp`, `src/qt/test/digidollarmintrecordtests.cpp`, `src/qt/test/ddtransactionrecordtests.cpp`, `src/qt/test/ddtransactiontabletests.cpp` | Registered suites | Qt widgets, mint persistence and transaction records/tables | +| `test/functional/digidollar_*.py`, `wallet_digidollar_*.py`, `feature_oracle_*.py`, `rpc_getoracles_*.py`, `wallet_paymaster_*.py` | Registered functional entries | activation, mint, transfer, redeem, oracle P2P, wallet restore, MuSig2 integration, Paymaster provider/client flow, mempool ordering | ## REPO_MAP.md Format @@ -257,7 +267,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/THAW_DAY_TEST_GUIDE.md b/THAW_DAY_TEST_GUIDE.md new file mode 100644 index 00000000000..ed2e19a95e7 --- /dev/null +++ b/THAW_DAY_TEST_GUIDE.md @@ -0,0 +1,255 @@ +# 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. +- Execution results are PENDING in the table below; this guide claims no full-run PASS. + +## 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`, built from the v9.26.6rc2 +commit. The rc1 lab client and its evidence are kept in `builds/thawday-client-rc1`. +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 /home/jared/Code/digibyte +``` + +Build the separate client only if its worktree does not already exist: + +```sh +./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. The delivered workspace may already +contain the build; use it in that case. The build does not run the full test suites. +Keep one heavy job at a time and leave existing builds/reindexes alone. + +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 /home/jared/Code/digibyte/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 /home/jared/Code/digibyte/builds/thawday-runs/20260914-143000 +./thawDay.sh stop --run-dir /home/jared/Code/digibyte/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 the 15-sample reference and cleared old freeze. +The after-H core group must then actually confirm its eligible mints. + +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. + +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 + +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 checks 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. + +| 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 | + +## 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/build_msvc/README.md b/build_msvc/README.md index 93ed64f0fe4..5e6793a9dac 100644 --- a/build_msvc/README.md +++ b/build_msvc/README.md @@ -34,7 +34,22 @@ To build DigiByte Core with the GUI, a static build of Qt is required. 1. Download a single ZIP archive of Qt source code from https://download.qt.io/official_releases/qt/ (e.g., [`qt-everywhere-opensource-src-5.15.10.zip`](https://download.qt.io/official_releases/qt/5.15/5.15.10/single/qt-everywhere-opensource-src-5.15.10.zip)), and expand it into a dedicated folder. The following instructions assume that this folder is `C:\dev\qt-source`. -2. Open "x64 Native Tools Command Prompt for VS 2022", and input the following commands: +2. Qt 5.15.10's bundled zlib needs the repository's `Z_PREFIX` backport so it +can be linked together with the zlib used by vcpkg's static libcurl. Apply the +patch from the root of the extracted Qt source tree (the directory containing +`qtbase`). Replace `C:\dev\digibyte` with the path to this repository: + +```cmd +cd C:\dev\qt-source +git apply --check C:\dev\digibyte\depends\patches\qt\zlib-prefix-crc32-combine.patch +git apply C:\dev\digibyte\depends\patches\qt\zlib-prefix-crc32-combine.patch +``` + +The patch gives Qt's three `crc32_combine_*` helpers the same private zlib +prefix as its other bundled zlib symbols. Do not replace it with +`/FORCE:MULTIPLE` or by omitting vcpkg's `zlib.lib`. + +3. Open "x64 Native Tools Command Prompt for VS 2022", and input the following commands: ```cmd cd C:\dev\qt-source mkdir build @@ -44,6 +59,10 @@ nmake nmake install ``` +For an existing configured Qt 5.15.10 build, apply the patch to its source tree +and rerun `nmake` followed by `nmake install` in the existing build directory. +Reconfiguration is not required. + One could speed up building with [`jom`](https://wiki.qt.io/Jom), a replacement for `nmake` which makes use of all CPU cores. To build DigiByte Core without Qt, unload or disable the `digibyte-qt`, `libdigibyte_qt` and `test_digibyte-qt` projects. diff --git a/build_msvc/common.init.vcxproj b/build_msvc/common.init.vcxproj index 6ea018d8463..1f8fbe3fad8 100644 --- a/build_msvc/common.init.vcxproj +++ b/build_msvc/common.init.vcxproj @@ -14,7 +14,6 @@ true true $(Configuration) - x86-windows-static x64-windows-static @@ -35,20 +34,12 @@ Debug x64 - - Release - Win32 - - - Debug - Win32 - false false - v142 + v143 Unicode No $(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName)\ @@ -58,7 +49,7 @@ true true - v142 + v143 Unicode $(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName)\ $(Platform)\$(Configuration)\$(ProjectName)\ @@ -96,15 +87,15 @@ Level3 NotUsing - /utf-8 /Zc:__cplusplus /std:c++17 %(AdditionalOptions) - 4018;4244;4267;4334;4715;4805;4834 + /utf-8 /Zc:__cplusplus /std:c++20 %(AdditionalOptions) + 4018;4244;4267;4715;4805 true - _SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING;_SILENCE_CXX17_OLD_ALLOCATOR_MEMBERS_DEPRECATION_WARNING;ZMQ_STATIC;NOMINMAX;WIN32;HAVE_CONFIG_H;_CRT_SECURE_NO_WARNINGS;_SCL_SECURE_NO_WARNINGS;_CONSOLE;_WIN32_WINNT=0x0601;_WIN32_IE=0x0501;WIN32_LEAN_AND_MEAN;%(PreprocessorDefinitions) - ..\..\src;..\..\src\univalue\include;..\..\src\secp256k1\include;..\..\src\leveldb\include;..\..\src\leveldb\helpers\memenv;%(AdditionalIncludeDirectories) + _SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING;SECP256K1_STATIC;ZMQ_STATIC;CURL_STATICLIB;HAVE_LIBCURL;NOMINMAX;WIN32;HAVE_CONFIG_H;_CRT_SECURE_NO_WARNINGS;_SCL_SECURE_NO_WARNINGS;_CONSOLE;_WIN32_WINNT=0x0601;_WIN32_IE=0x0501;WIN32_LEAN_AND_MEAN;%(PreprocessorDefinitions) + ..\..\src;..\..\src\minisketch\include;..\..\src\univalue\include;..\..\src\secp256k1\include;..\..\src\leveldb\include;..\..\src\leveldb\helpers\memenv;%(AdditionalIncludeDirectories) Console - Iphlpapi.lib;ws2_32.lib;Shlwapi.lib;kernel32.lib;user32.lib;gdi32.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) + Iphlpapi.lib;ws2_32.lib;Shlwapi.lib;kernel32.lib;user32.lib;gdi32.lib;comdlg32.lib;advapi32.lib;bcrypt.lib;crypt32.lib;secur32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) true diff --git a/build_msvc/common.init.vcxproj.in b/build_msvc/common.init.vcxproj.in index 9cce546268e..ad0c06178fe 100644 --- a/build_msvc/common.init.vcxproj.in +++ b/build_msvc/common.init.vcxproj.in @@ -90,12 +90,12 @@ /utf-8 /Zc:__cplusplus /std:c++20 %(AdditionalOptions) 4018;4244;4267;4715;4805 true - _SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING;SECP256K1_STATIC;ZMQ_STATIC;NOMINMAX;WIN32;HAVE_CONFIG_H;_CRT_SECURE_NO_WARNINGS;_SCL_SECURE_NO_WARNINGS;_CONSOLE;_WIN32_WINNT=0x0601;_WIN32_IE=0x0501;WIN32_LEAN_AND_MEAN;%(PreprocessorDefinitions) + _SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING;SECP256K1_STATIC;ZMQ_STATIC;CURL_STATICLIB;HAVE_LIBCURL;NOMINMAX;WIN32;HAVE_CONFIG_H;_CRT_SECURE_NO_WARNINGS;_SCL_SECURE_NO_WARNINGS;_CONSOLE;_WIN32_WINNT=0x0601;_WIN32_IE=0x0501;WIN32_LEAN_AND_MEAN;%(PreprocessorDefinitions) ..\..\src;..\..\src\minisketch\include;..\..\src\univalue\include;..\..\src\secp256k1\include;..\..\src\leveldb\include;..\..\src\leveldb\helpers\memenv;%(AdditionalIncludeDirectories) Console - Iphlpapi.lib;ws2_32.lib;Shlwapi.lib;kernel32.lib;user32.lib;gdi32.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) + Iphlpapi.lib;ws2_32.lib;Shlwapi.lib;kernel32.lib;user32.lib;gdi32.lib;comdlg32.lib;advapi32.lib;bcrypt.lib;crypt32.lib;secur32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) true diff --git a/build_msvc/common.qt.init.vcxproj b/build_msvc/common.qt.init.vcxproj index cc8063e545b..78f54284f48 100644 --- a/build_msvc/common.qt.init.vcxproj +++ b/build_msvc/common.qt.init.vcxproj @@ -9,8 +9,8 @@ $(QtIncludeDir);$(QtIncludeDir)\QtNetwork;$(QtIncludeDir)\QtCore;$(QtIncludeDir)\QtWidgets;$(QtIncludeDir)\QtGui; .\QtGeneratedFiles\qt $(QtBaseDir)\bin - $(QtPluginsLibraryDir)\platforms\qminimal.lib;$(QtPluginsLibraryDir)\platforms\qwindows.lib;$(QtPluginsLibraryDir)\styles\qwindowsvistastyle.lib;$(QtLibraryDir)\Qt5WindowsUIAutomationSupport.lib;$(QtLibraryDir)\qtfreetype.lib;$(QtLibraryDir)\qtharfbuzz.lib;$(QtLibraryDir)\qtlibpng.lib;$(QtLibraryDir)\qtpcre2.lib;$(QtLibraryDir)\Qt5AccessibilitySupport.lib;$(QtLibraryDir)\Qt5Core.lib;$(QtLibraryDir)\Qt5Concurrent.lib;$(QtLibraryDir)\Qt5EventDispatcherSupport.lib;$(QtLibraryDir)\Qt5FontDatabaseSupport.lib;$(QtLibraryDir)\Qt5Gui.lib;$(QtLibraryDir)\Qt5Network.lib;$(QtLibraryDir)\Qt5PlatformCompositorSupport.lib;$(QtLibraryDir)\Qt5ThemeSupport.lib;$(QtLibraryDir)\Qt5Widgets.lib;$(QtLibraryDir)\Qt5WinExtras.lib;$(QtLibraryDir)\qtmain.lib;Wtsapi32.lib;userenv.lib;netapi32.lib;imm32.lib;Dwmapi.lib;version.lib;winmm.lib;UxTheme.lib - $(QtPluginsLibraryDir)\platforms\qwindowsd.lib;$(QtPluginsLibraryDir)\platforms\qminimald.lib;$(QtPluginsLibraryDir)\styles\qwindowsvistastyled.lib;$(QtLibraryDir)\*d.lib;Wtsapi32.lib;crypt32.lib;userenv.lib;netapi32.lib;imm32.lib;Dwmapi.lib;version.lib;winmm.lib;UxTheme.lib + $(QtPluginsLibraryDir)\platforms\qminimal.lib;$(QtPluginsLibraryDir)\platforms\qwindows.lib;$(QtPluginsLibraryDir)\styles\qwindowsvistastyle.lib;$(QtLibraryDir)\Qt5WindowsUIAutomationSupport.lib;$(QtLibraryDir)\qtfreetype.lib;$(QtLibraryDir)\qtharfbuzz.lib;$(QtLibraryDir)\qtlibpng.lib;$(QtLibraryDir)\qtpcre2.lib;$(QtLibraryDir)\Qt5AccessibilitySupport.lib;$(QtLibraryDir)\Qt5Core.lib;$(QtLibraryDir)\Qt5Concurrent.lib;$(QtLibraryDir)\Qt5EventDispatcherSupport.lib;$(QtLibraryDir)\Qt5FontDatabaseSupport.lib;$(QtLibraryDir)\Qt5Gui.lib;$(QtLibraryDir)\Qt5Network.lib;$(QtLibraryDir)\Qt5PlatformCompositorSupport.lib;$(QtLibraryDir)\Qt5ThemeSupport.lib;$(QtLibraryDir)\Qt5Widgets.lib;$(QtLibraryDir)\Qt5WinExtras.lib;$(QtLibraryDir)\qtmain.lib;qrencode.lib;Wtsapi32.lib;userenv.lib;netapi32.lib;imm32.lib;Dwmapi.lib;version.lib;winmm.lib;UxTheme.lib + $(QtPluginsLibraryDir)\platforms\qwindowsd.lib;$(QtPluginsLibraryDir)\platforms\qminimald.lib;$(QtPluginsLibraryDir)\styles\qwindowsvistastyled.lib;$(QtLibraryDir)\*d.lib;qrencoded.lib;Wtsapi32.lib;crypt32.lib;userenv.lib;netapi32.lib;imm32.lib;Dwmapi.lib;version.lib;winmm.lib;UxTheme.lib diff --git a/build_msvc/digibyte-wallet/digibyte-wallet.vcxproj b/build_msvc/digibyte-wallet/digibyte-wallet.vcxproj index 39329ac1af3..1d1a1ab30e1 100644 --- a/build_msvc/digibyte-wallet/digibyte-wallet.vcxproj +++ b/build_msvc/digibyte-wallet/digibyte-wallet.vcxproj @@ -24,6 +24,9 @@ {6190199c-6cf4-4dad-bfbd-93fa72a760c1} + + {460fee33-1fe1-483f-b3bf-931ff8e969a5} + {b53a5535-ee9d-4c6f-9a26-f79ee3bc3754} @@ -39,6 +42,9 @@ {bb493552-3b8c-4a8c-bf69-a6e7a51d2ea6} + + {18430fef-6b61-4c53-b396-718e02850f1b} + diff --git a/build_msvc/digibyte_config.h b/build_msvc/digibyte_config.h index 3ddbc969cf7..9b8c5808c4a 100644 --- a/build_msvc/digibyte_config.h +++ b/build_msvc/digibyte_config.h @@ -5,20 +5,17 @@ #ifndef DIGIBYTE_DIGIBYTE_CONFIG_H #define DIGIBYTE_DIGIBYTE_CONFIG_H -/* Define if building universal (internal helper macro) */ -/* #undef AC_APPLE_UNIVERSAL_BUILD */ - /* Version Build */ -#define CLIENT_VERSION_BUILD 0 +#define CLIENT_VERSION_BUILD 6 /* Version is release */ #define CLIENT_VERSION_IS_RELEASE true /* Major version */ -#define CLIENT_VERSION_MAJOR 22 +#define CLIENT_VERSION_MAJOR 9 /* Minor version */ -#define CLIENT_VERSION_MINOR 0 +#define CLIENT_VERSION_MINOR 26 /* Copyright holder(s) before %s replacement */ #define COPYRIGHT_HOLDERS "The %s developers" @@ -41,33 +38,12 @@ /* Define to 1 to enable SQLite wallet */ #define USE_SQLITE 1 -/* Define to 1 to enable ZMQ functions */ +/* Define this symbol to enable ZMQ functions */ #define ENABLE_ZMQ 1 -/* define if the Boost library is available */ -#define HAVE_BOOST /**/ - -/* define if the Boost::Filesystem library is available */ -#define HAVE_BOOST_FILESYSTEM /**/ - /* define if external signer support is enabled (requires Boost::Process) */ #define ENABLE_EXTERNAL_SIGNER /**/ -/* define if the Boost::System library is available */ -#define HAVE_BOOST_SYSTEM /**/ - -/* define if the Boost::Unit_Test_Framework library is available */ -#define HAVE_BOOST_UNIT_TEST_FRAMEWORK /**/ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_BYTESWAP_H */ - -/* Define this symbol if the consensus lib has been built */ -#define HAVE_CONSENSUS_LIB 1 - -/* define if the compiler supports basic C++11 syntax */ -#define HAVE_CXX11 1 - /* Define to 1 if you have the declaration of `be16toh', and to 0 if you don't. */ #define HAVE_DECL_BE16TOH 0 @@ -136,141 +112,23 @@ */ #define HAVE_DECL_SETSID 0 -/* Define to 1 if you have the declaration of `strerror_r', and to 0 if you - don't. */ -#define HAVE_DECL_STRERROR_R 0 - -/* Define to 1 if you have the declaration of `strnlen', and to 0 if you - don't. */ -#define HAVE_DECL_STRNLEN 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_DLFCN_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_ENDIAN_H */ - -/* Define to 1 if the system has the `dllexport' function attribute */ -#define HAVE_FUNC_ATTRIBUTE_DLLEXPORT 1 - -/* Define to 1 if the system has the `dllimport' function attribute */ -#define HAVE_FUNC_ATTRIBUTE_DLLIMPORT 1 - -/* Define to 1 if the system has the `visibility' function attribute */ -#define HAVE_FUNC_ATTRIBUTE_VISIBILITY 1 - -/* Define this symbol if the BSD getentropy system call is available */ -/* #undef HAVE_GETENTROPY */ - -/* Define this symbol if the BSD getentropy system call is available with - sys/random.h */ -/* #undef HAVE_GETENTROPY_RAND */ - -/* Define to 1 if you have the header file. */ -#define HAVE_INTTYPES_H 1 - -/* Define this symbol if you have malloc_info */ -/* #undef HAVE_MALLOC_INFO */ - -/* Define this symbol if you have mallopt with M_ARENA_MAX */ -/* #undef HAVE_MALLOPT_ARENA_MAX */ - -/* Define to 1 if you have the header file. */ -#define HAVE_MEMORY_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_MINIUPNPC_MINIUPNPC_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_MINIUPNPC_UPNPCOMMANDS_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_MINIUPNPC_UPNPERRORS_H 1 - -/* Define this symbol if you have MSG_DONTWAIT */ -/* #undef HAVE_MSG_DONTWAIT */ - -/* Define this symbol if you have MSG_NOSIGNAL */ -/* #undef HAVE_MSG_NOSIGNAL */ - -/* Define if you have POSIX threads libraries and header files. */ -//#define HAVE_PTHREAD 1 - -/* Have PTHREAD_PRIO_INHERIT. */ -//#define HAVE_PTHREAD_PRIO_INHERIT 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_STDINT_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_STDIO_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_STDLIB_H 1 - -/* Define to 1 if you have the `strerror_r' function. */ -/* #undef HAVE_STRERROR_R */ - -/* Define to 1 if you have the header file. */ -#define HAVE_STRINGS_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_STRING_H 1 - -/* Define this symbol if the BSD sysctl(KERN_ARND) is available */ -/* #undef HAVE_SYSCTL_ARND */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_ENDIAN_H */ - -/* Define this symbol if the Linux getrandom system call is available */ -/* #undef HAVE_SYS_GETRANDOM */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_PRCTL_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_SELECT_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_STAT_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_TYPES_H 1 - -/* Define to 1 if you have the header file. */ -//#define HAVE_UNISTD_H 1 - -/* Define if the visibility attribute is supported. */ -#define HAVE_VISIBILITY_ATTRIBUTE 1 - -/* Define to the sub-directory where libtool stores uninstalled libraries. */ -#define LT_OBJDIR ".libs/" +/* Define if the dllexport attribute is supported. */ +#define HAVE_DLLEXPORT_ATTRIBUTE 1 /* Define to the address where bug reports for this package should be sent. */ -#define PACKAGE_BUGREPORT "https://github.com/DigiByte-Core/digibyte/issues" +#define PACKAGE_BUGREPORT "https://github.com/digibyte/digibyte/issues" /* Define to the full name of this package. */ #define PACKAGE_NAME "DigiByte Core" /* Define to the full name and version of this package. */ -#define PACKAGE_STRING "DigiByte Core 22.0.0" - -/* Define to the one symbol short name of this package. */ -#define PACKAGE_TARNAME "digibyte" +#define PACKAGE_STRING "DigiByte Core 9.26.6" /* Define to the home page for this package. */ -#define PACKAGE_URL "https://digibyte.org/" +#define PACKAGE_URL "https://digibytecore.org/" /* Define to the version of this package. */ -#define PACKAGE_VERSION "22.0.0" - -/* Define to necessary symbol if this constant uses a non-standard name on - your system. */ -/* #undef PTHREAD_CREATE_JOINABLE */ - -/* Define this symbol if the qt platform is cocoa */ -/* #undef QT_QPA_PLATFORM_COCOA */ +#define PACKAGE_VERSION "9.26.6" /* Define this symbol if the minimal qt platform exists */ #define QT_QPA_PLATFORM_MINIMAL 1 @@ -278,53 +136,11 @@ /* Define this symbol if the qt platform is windows */ #define QT_QPA_PLATFORM_WINDOWS 1 -/* Define this symbol if the qt platform is xcb */ -/* #undef QT_QPA_PLATFORM_XCB */ - /* Define this symbol if qt plugins are static */ #define QT_STATICPLUGIN 1 -/* Define to 1 if you have the ANSI C header files. */ -#define STDC_HEADERS 1 - -/* Define to 1 if strerror_r returns char *. */ -/* #undef STRERROR_R_CHAR_P */ - -/* Define this symbol to build in assembly routines */ -//#define USE_ASM 1 - -/* Define if dbus support should be compiled in */ -/* #undef USE_DBUS */ - /* Define if QR support should be compiled in */ -//#define USE_QRCODE 1 - -/* UPnP support not compiled if undefined, otherwise value (0 or 1) determines - default state */ -//#define USE_UPNP 0 - -/* Define WORDS_BIGENDIAN to 1 if your processor stores words with the most - significant byte first (like Motorola and SPARC, unlike Intel). */ -#if defined AC_APPLE_UNIVERSAL_BUILD -# if defined __BIG_ENDIAN__ -# define WORDS_BIGENDIAN 1 -# endif -#else -# ifndef WORDS_BIGENDIAN -/* # undef WORDS_BIGENDIAN */ -# endif -#endif - -/* Enable large inode numbers on Mac OS X 10.5. */ -#ifndef _DARWIN_USE_64_BIT_INODE -# define _DARWIN_USE_64_BIT_INODE 1 -#endif - -/* Number of bits in a file offset, on hosts where this is settable. */ -#define _FILE_OFFSET_BITS 64 - -/* Define for large files, on AIX-style hosts. */ -/* #undef _LARGE_FILES */ +#define USE_QRCODE 1 /* Windows Universal Platform constraints */ #if !defined(WINAPI_FAMILY) || (WINAPI_FAMILY == WINAPI_FAMILY_DESKTOP_APP) diff --git a/build_msvc/digibyte_config.h.in b/build_msvc/digibyte_config.h.in index 62894b15500..8723381627b 100644 --- a/build_msvc/digibyte_config.h.in +++ b/build_msvc/digibyte_config.h.in @@ -139,6 +139,9 @@ /* Define this symbol if qt plugins are static */ #define QT_STATICPLUGIN 1 +/* Define if QR support should be compiled in */ +#define USE_QRCODE 1 + /* Windows Universal Platform constraints */ #if !defined(WINAPI_FAMILY) || (WINAPI_FAMILY == WINAPI_FAMILY_DESKTOP_APP) /* Either a desktop application without API restrictions, or and older system diff --git a/build_msvc/libdigibyte_consensus/libdigibyte_consensus.vcxproj b/build_msvc/libdigibyte_consensus/libdigibyte_consensus.vcxproj index 1c52e4c4357..aa0c8d404bd 100644 --- a/build_msvc/libdigibyte_consensus/libdigibyte_consensus.vcxproj +++ b/build_msvc/libdigibyte_consensus/libdigibyte_consensus.vcxproj @@ -8,21 +8,101 @@ StaticLibrary - - - - - - - - - - - - - + + $(IntDir)arith_uint256.obj + + + $(IntDir)consensus_digidollar.obj + + + $(IntDir)consensus_digidollar_transaction_validation.obj + + + $(IntDir)consensus_digidollar_tx.obj + + + $(IntDir)consensus_merkle.obj + + + $(IntDir)consensus_tx_check.obj + + + $(IntDir)crypto_blake.obj + + + $(IntDir)crypto_bmw.obj + + + $(IntDir)crypto_cubehash.obj + + + $(IntDir)crypto_echo.obj + + + $(IntDir)crypto_groestl.obj + + + $(IntDir)crypto_jh.obj + + + $(IntDir)crypto_keccak.obj + + + $(IntDir)crypto_luffa.obj + + + $(IntDir)crypto_shavite.obj + + + $(IntDir)crypto_simd.obj + + + $(IntDir)crypto_skein.obj + + + $(IntDir)crypto_odocrypt.obj + + + $(IntDir)crypto_KeccakP-800-reference.obj + + + $(IntDir)crypto_scrypt.obj + + + $(IntDir)crypto_scrypt-sse2.obj + + + $(IntDir)hash.obj + + + $(IntDir)primitives_block.obj + + + $(IntDir)primitives_transaction.obj + + + $(IntDir)pubkey.obj + + + $(IntDir)script_digibyteconsensus.obj + + + $(IntDir)script_interpreter.obj + + + $(IntDir)script_script.obj + + + $(IntDir)script_script_error.obj + + + $(IntDir)uint256.obj + + + $(IntDir)util_strencodings.obj + - \ No newline at end of file + diff --git a/build_msvc/libdigibyte_consensus/libdigibyte_consensus.vcxproj.in b/build_msvc/libdigibyte_consensus/libdigibyte_consensus.vcxproj.in new file mode 100644 index 00000000000..af330723d72 --- /dev/null +++ b/build_msvc/libdigibyte_consensus/libdigibyte_consensus.vcxproj.in @@ -0,0 +1,16 @@ + + + + + {2B384FA8-9EE1-4544-93CB-0D733C25E8CE} + + + StaticLibrary + + +@SOURCE_FILES@ + + + + + diff --git a/build_msvc/libdigibyte_qt/libdigibyte_qt.vcxproj b/build_msvc/libdigibyte_qt/libdigibyte_qt.vcxproj index e0b6980ec4e..faa48660798 100644 --- a/build_msvc/libdigibyte_qt/libdigibyte_qt.vcxproj +++ b/build_msvc/libdigibyte_qt/libdigibyte_qt.vcxproj @@ -64,6 +64,19 @@ + + + + + + + + + + + + + @@ -120,6 +133,20 @@ + + + + + + + + + + + + + + @@ -148,6 +175,10 @@ + + @@ -189,7 +220,7 @@ - + There was an error executing the libdigibyte_qt resource code generation task. diff --git a/build_msvc/libsecp256k1/libsecp256k1.vcxproj b/build_msvc/libsecp256k1/libsecp256k1.vcxproj index 777515aa3af..aece77014bc 100644 --- a/build_msvc/libsecp256k1/libsecp256k1.vcxproj +++ b/build_msvc/libsecp256k1/libsecp256k1.vcxproj @@ -2,7 +2,7 @@ - {BB493552-3B8C-4A8C-BF69-A6E7A51D2EA6} + {BB493552-3B8C-4A8C-BFBD-93FA72A760C1} StaticLibrary @@ -14,7 +14,7 @@ - ENABLE_MODULE_RECOVERY;ENABLE_MODULE_EXTRAKEYS;ENABLE_MODULE_SCHNORRSIG;ENABLE_MODULE_ELLSWIFT;%(PreprocessorDefinitions) + ENABLE_MODULE_RECOVERY;ENABLE_MODULE_EXTRAKEYS;ENABLE_MODULE_SCHNORRSIG;ENABLE_MODULE_ELLSWIFT;ENABLE_MODULE_MUSIG;%(PreprocessorDefinitions) USE_ASM_X86_64;%(UndefinePreprocessorDefinitions) ..\..\src\secp256k1;%(AdditionalIncludeDirectories) 4146;4244;4267;4334 diff --git a/build_msvc/libsecp256k1/libsecp256k1.vcxproj.in b/build_msvc/libsecp256k1/libsecp256k1.vcxproj.in new file mode 100644 index 00000000000..19da8940c40 --- /dev/null +++ b/build_msvc/libsecp256k1/libsecp256k1.vcxproj.in @@ -0,0 +1,26 @@ + + + + + {BB493552-3B8C-4A8C-BFBD-93FA72A760C1} + + + StaticLibrary + + + + + + + + + @SECP256K1_MODULES@;%(PreprocessorDefinitions) + USE_ASM_X86_64;%(UndefinePreprocessorDefinitions) + ..\..\src\secp256k1;%(AdditionalIncludeDirectories) + 4146;4244;4267;4334 + + + + + + diff --git a/build_msvc/msvc-autogen.py b/build_msvc/msvc-autogen.py index cf431969b8a..e07d13873fe 100755 --- a/build_msvc/msvc-autogen.py +++ b/build_msvc/msvc-autogen.py @@ -14,6 +14,7 @@ libs = [ 'libdigibyte_cli', 'libdigibyte_common', + 'libdigibyte_consensus', 'libdigibyte_crypto', 'libdigibyte_node', 'libdigibyte_util', @@ -24,6 +25,14 @@ 'libtest_util', ] +secp256k1_modules = [ + 'RECOVERY', + 'EXTRAKEYS', + 'SCHNORRSIG', + 'ELLSWIFT', + 'MUSIG', +] + ignore_list = [ ] @@ -90,6 +99,126 @@ def set_properties(vcxproj_filename, placeholder, content): with open(vcxproj_filename, 'w', encoding='utf-8') as vcxproj_file: vcxproj_file.write(vcxproj_in_file.read().replace(placeholder, content)) + +def parse_makefile_variable(makefile, variable): + """Return literal entries from all assignments to one make variable.""" + entries = [] + collecting = False + with open(makefile, 'r', encoding='utf-8') as file: + for raw_line in file.read().splitlines(): + line = raw_line.strip() + if not collecting: + match = re.match(r'^' + re.escape(variable) + r'\s*\+?=\s*(.*)$', line) + if not match: + continue + line = match.group(1).strip() + continued = line.endswith('\\') + if continued: + line = line[:-1].rstrip() + if line and not line.startswith('$'): + entries.extend(entry for entry in line.split() if not entry.startswith('$')) + collecting = continued + return entries + + +def add_missing_items(content, marker, items): + missing = [item for item in items if item not in content] + if missing: + if marker not in content: + raise RuntimeError('Unexpected MSVC project layout') + content = content.replace(marker, '\n'.join(missing) + '\n' + marker, 1) + return content + + +def sync_qt_test_project(): + makefile = os.path.join(SOURCE_DIR, 'Makefile.qttest.include') + project = os.path.join(SOURCE_DIR, '../build_msvc/test_digibyte-qt/test_digibyte-qt.vcxproj') + source_entries = parse_makefile_variable( + makefile, 'qt_test_test_digibyte_qt_SOURCES') + moc_entries = parse_makefile_variable(makefile, 'TEST_QT_MOC_CPP') + + with open(project, 'r', encoding='utf-8') as file: + content = file.read() + + generated_marker = ' ' + source_items = [] + for source in source_entries: + if source.endswith('.cpp'): + include = '..\\..\\src\\' + source.replace('/', '\\') + item = f' ' + # Preserve hand-authored object-name metadata and remove an older + # simple duplicate if a previous generator run added one. + if f'\n' in content: + content = content.replace(item + '\n', '') + if f'Include="{include}"' not in content: + source_items.append(item) + content = add_missing_items(content, generated_marker, source_items) + + generated_items = [] + moc_header_items = [] + for source in moc_entries: + if not source.endswith('.cpp'): + continue + filename = os.path.basename(source.replace('\\', '/')) + generated_items.append( + f' ') + header = filename.removeprefix('moc_').removesuffix('.cpp') + '.h' + moc_header_items.append( + f' ') + generated_end_marker = ' \n \n ' + source_items.append(item) + content = add_missing_items(content, generated_marker, source_items) + + moc_items = [] + for source in moc_entries: + if not source.endswith('.cpp'): + continue + filename = os.path.basename(source.replace('\\', '/')) + item = f' ' + moc_items.append(item) + content = add_missing_items(content, resource_marker, moc_items) + + with open(project, 'w', encoding='utf-8') as file: + file.write(content) + def main(): parser = argparse.ArgumentParser(description='DigiByte-core msbuild configuration initialiser.') parser.add_argument('-toolset', nargs='?', default=DEFAULT_PLATFORM_TOOLSET, @@ -97,6 +226,11 @@ def main(): ' default is %s.'%DEFAULT_PLATFORM_TOOLSET) args = parser.parse_args() set_properties(os.path.join(SOURCE_DIR, '../build_msvc/common.init.vcxproj'), '@TOOLSET@', args.toolset) + set_properties( + os.path.join(SOURCE_DIR, '../build_msvc/libsecp256k1/libsecp256k1.vcxproj'), + '@SECP256K1_MODULES@', + ';'.join(f'ENABLE_MODULE_{module}' for module in secp256k1_modules), + ) for makefile_name in os.listdir(SOURCE_DIR): if 'Makefile' in makefile_name: @@ -109,6 +243,8 @@ def main(): content += ' $(IntDir)' + object_filename + '\n' content += ' \n' set_properties(vcxproj_filename, '@SOURCE_FILES@\n', content) + sync_qt_project() + sync_qt_test_project() parse_config_into_dgb_config() copyfile(os.path.join(SOURCE_DIR,'../build_msvc/digibyte_config.h'), os.path.join(SOURCE_DIR, 'config/digibyte-config.h')) diff --git a/build_msvc/test_digibyte-qt/test_digibyte-qt.vcxproj b/build_msvc/test_digibyte-qt/test_digibyte-qt.vcxproj index 3133bcf773f..8ffe2fb03b0 100644 --- a/build_msvc/test_digibyte-qt/test_digibyte-qt.vcxproj +++ b/build_msvc/test_digibyte-qt/test_digibyte-qt.vcxproj @@ -24,12 +24,25 @@ + + + + + + + + + + + + + @@ -101,6 +114,12 @@ + + + + + + diff --git a/build_msvc/test_digibyte/test_digibyte.vcxproj b/build_msvc/test_digibyte/test_digibyte.vcxproj index 6ca6f531478..30113fbbd05 100644 --- a/build_msvc/test_digibyte/test_digibyte.vcxproj +++ b/build_msvc/test_digibyte/test_digibyte.vcxproj @@ -8,6 +8,11 @@ Application $(SolutionDir)$(Platform)\$(Configuration)\ + + + /bigobj %(AdditionalOptions) + + diff --git a/build_msvc/vcpkg.json b/build_msvc/vcpkg.json index bfd6dff6911..e161779a8fc 100644 --- a/build_msvc/vcpkg.json +++ b/build_msvc/vcpkg.json @@ -4,19 +4,31 @@ "dependencies": [ "berkeleydb", "boost-multi-index", + "boost-multiprecision", "boost-process", "boost-signals2", "boost-test", "boost-thread", + { + "name": "curl", + "default-features": false, + "features": ["ssl"] + }, "sqlite3", { "name": "libevent", "features": ["thread"] }, + "libqrencode", "zeromq" ], "builtin-baseline": "f14984af3738e69f197bf0e647a8dca12de92996", "overrides": [ + { + "name": "curl", + "version": "8.21.0", + "port-version": 1 + }, { "name": "libevent", "version": "2.1.12#7" diff --git a/configure.ac b/configure.ac index 2632ff681d3..8db9aa041cf 100644 --- a/configure.ac +++ b/configure.ac @@ -1,8 +1,8 @@ AC_PREREQ([2.69]) define(_CLIENT_VERSION_MAJOR, 9) define(_CLIENT_VERSION_MINOR, 26) -define(_CLIENT_VERSION_BUILD, 5) -define(_CLIENT_VERSION_RC, 0) +define(_CLIENT_VERSION_BUILD, 6) +define(_CLIENT_VERSION_RC, 2) define(_CLIENT_VERSION_SUFFIX, []) define(_CLIENT_VERSION_IS_RELEASE, true) define(_COPYRIGHT_YEAR, 2026) @@ -1626,7 +1626,7 @@ PKG_CHECK_MODULES([LIBCURL], [libcurl >= 7.50.0], if test "$TARGET_OS" = "darwin"; then LIBCURL_LIBS="$LIBCURL_LIBS -framework CoreFoundation -framework SystemConfiguration" fi], - [AC_MSG_WARN([libcurl not found, oracle will use mock exchange data])]) + [AC_MSG_WARN([libcurl not found, live oracle exchange fetching will be unavailable])]) AM_CONDITIONAL([HAVE_LIBCURL], [test "$use_libcurl" = "yes"]) 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..0b77d35ad2b --- /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=e3ad0a4522410b77ebd46e1cf17c65d944fee25b +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 -b test/thawday-client-rc2 "$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..0db42283aac --- /dev/null +++ b/contrib/thawday/rehearsal.py @@ -0,0 +1,897 @@ +#!/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 None or self.nodes[0].rpc("getblockcount") < ceiling: + 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/depends/packages/qt.mk b/depends/packages/qt.mk index 060406efc55..2a2cc35cbf6 100644 --- a/depends/packages/qt.mk +++ b/depends/packages/qt.mk @@ -24,6 +24,7 @@ $(package)_patches += fix-macos-linker.patch $(package)_patches += memory_resource.patch $(package)_patches += windows_lto.patch $(package)_patches += zlib-timebits64.patch +$(package)_patches += zlib-prefix-crc32-combine.patch $(package)_qttranslations_file_name=qttranslations-$($(package)_suffix) $(package)_qttranslations_sha256_hash=38b942bc7e62794dd072945c8a92bb9dfffed24070aea300327a3bb42f855609 @@ -258,6 +259,7 @@ define $(package)_preprocess_cmds patch -p1 -i $($(package)_patch_dir)/guix_cross_lib_path.patch && \ patch -p1 -i $($(package)_patch_dir)/windows_lto.patch && \ patch -p1 -i $($(package)_patch_dir)/zlib-timebits64.patch && \ + patch -p1 -i $($(package)_patch_dir)/zlib-prefix-crc32-combine.patch && \ mkdir -p qtbase/mkspecs/macx-clang-linux &&\ cp -f qtbase/mkspecs/macx-clang/qplatformdefs.h qtbase/mkspecs/macx-clang-linux/ &&\ cp -f $($(package)_patch_dir)/mac-qmake.conf qtbase/mkspecs/macx-clang-linux/qmake.conf && \ diff --git a/depends/patches/qt/zlib-prefix-crc32-combine.patch b/depends/patches/qt/zlib-prefix-crc32-combine.patch new file mode 100644 index 00000000000..8fb2bf1087b --- /dev/null +++ b/depends/patches/qt/zlib-prefix-crc32-combine.patch @@ -0,0 +1,24 @@ +Backport the missing Z_PREFIX mappings added upstream after Qt 5.15.10. + +Qt builds its bundled zlib with Z_PREFIX so that it can coexist with another +zlib in the same process. zlib 1.2.12 added three public crc32-combine helper +symbols, but Qt 5.15.10's bundled zconf.h does not prefix them. This makes a +static Qt5Core.lib collide with the zlib.lib used by libcurl. + +Upstream Qt commit: 54ea62756a +Qt bug: QTBUG-116236 + +diff --git a/qtbase/src/3rdparty/zlib/src/zconf.h b/qtbase/src/3rdparty/zlib/src/zconf.h +--- a/qtbase/src/3rdparty/zlib/src/zconf.h ++++ b/qtbase/src/3rdparty/zlib/src/zconf.h +@@ -40,7 +40,10 @@ + # endif + # define crc32 z_crc32 + # define crc32_combine z_crc32_combine + # define crc32_combine64 z_crc32_combine64 ++# define crc32_combine_gen z_crc32_combine_gen ++# define crc32_combine_gen64 z_crc32_combine_gen64 ++# define crc32_combine_op z_crc32_combine_op + # define crc32_z z_crc32_z + # define deflate z_deflate + # define deflateBound z_deflateBound 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..1bec35f2673 100644 --- a/doc/README.md +++ b/doc/README.md @@ -61,12 +61,26 @@ The DigiByte repo's [root README](/README.md) contains relevant information on t - [Translation Process](translation_process.md) - [Translation Strings Policy](translation_strings_policy.md) - [JSON-RPC Interface](JSON-RPC-interface.md) +- [DigiDollar Paymaster: developer starting point](../PAYMASTER.md) +- [Paymaster implementation reference](digidollar-paymaster-implementation.md) +- [Paymaster design specification](../DIGIDOLLAR_PAYMASTER_NETWORK_PROPOSAL_EN.md) +- [Paymaster implementation plan](../DIGIDOLLAR_PAYMASTER_PLAN.md) +- [Paymaster hardening plan](../DIGIDOLLAR_PAYMASTER_HARDENING_PLAN.md) +- [Paymaster operator and RPC guide](digidollar-paymaster.md) +- [Paymaster release gate and dated evidence](digidollar-paymaster-release-gate.md) +- [Paymaster v9.26.6rc2 integration and verification](digidollar-paymaster-v9.26.6rc2-integration.md) - [Unauthenticated REST Interface](REST-interface.md) - [Shared Libraries](shared-libraries.md) - [BIPS](bips.md) - [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/digidollar-paymaster-edge-case-review.md b/doc/digidollar-paymaster-edge-case-review.md new file mode 100644 index 00000000000..c8232b1ed8d --- /dev/null +++ b/doc/digidollar-paymaster-edge-case-review.md @@ -0,0 +1,347 @@ +# Paymaster workflow and edge-case review + +Review date: 2026-09-20. Base commit: +`354b711f92524cfab57bc163fca2384d20b8e90e`, branch +`integration/paymaster-v9.26.6rc2`, including the uncommitted automatic pool-setup +implementation. This is a source and test-coverage review of that working tree, +not a runtime certification of the base commit or the pending changes. + +## Scope and evidence + +The review follows the Paymaster RPC, wallet/store, scheduler, protocol/network, +finance and Qt entry points and their integration boundaries. The inventory +contains 17 Paymaster core/wallet unit-test source files, the Qt +`PaymasterWidgetTests` suite, 10 registered Paymaster functional entries +(including the optional old-node bridge), and four registered Paymaster fuzz +targets. Counts describe source inventory, not execution or branch coverage. + +Relevant guidance: root and `src/AGENTS.md`, `test/functional/AGENTS.md`, +`CLAUDE.md`, `CONTRIBUTING.md`, architecture/repo maps, developer notes, +functional/unit/Qt test documentation, and the existing Paymaster release gate. +The new Dandelion regression deliberately departs from the generic advice to +disable Dandelion in transaction tests: disabling the affected subsystem hides +this failure class. + +All findings below are supported by source control flow. Their exact runtime +reproductions remain to be implemented and executed on freshly built binaries. +No live node, existing testnet wallet, balance, or transaction was changed in +this review. No production source was edited during this review. + +## Local corrections and verification + +The following fixes and regression sources were added after the review below. +The findings retain their original audit wording and line references; they are +not descriptions of the corrected working tree. Runtime acceptance is pending. + +| Finding | Correction | Regression added or extended | +|---|---|---| +| EC-01 | Preserve FOUND/NOT_FOUND/read-error/version-error through AUTO dispatch. | `paymaster_wallet_store_tests/auto_dispatch_rejects_unreadable_session_before_funding_or_signing`: live records, tombstones, future/truncated versions, storage failure, no mutation. | +| EC-02 | Approve while disabled, execute only when enabled; both Qt entry points accept pending work and require acceptance/fee fields. | `PaymasterWidgetTests::paymasterGuidedSetupBoundsSafetyAndRetriesFailedStep` now has funded and deferred rows; the liquidity workflow rejects missing fields. `wallet_paymaster_pool_setup.py` checks disabled approval without signing or implicit enablement. | +| EC-03 | Recurring DGB refill selects confirmed funding. | `wallet_paymaster_pool_setup.py` adds simultaneous carrier/DGB deficits under Dandelion and verifies no child signature before parent confirmation. | +| EC-04 | Reconcile failed signed setup again and restore only unspent invalidated outputs. | `paymaster_wallet_identity_tests/pool_setup_reconciles_conflict_reversal_atomically` calls actual reconciliation through conflict, abandonment, commit failure and reconfirmation. The functional setup test adds a real conflicting block and exact transaction reconfirmation. A still-conflicted signed transaction intentionally remains protected; it is not cancelled as unsigned. | +| EC-05 | Store a provider-bound marker with the signed wallet transaction before broadcast, reconstruct missing retirement events from that existing durable record, and check DD retirement fees before commit. | `paymaster_wallet_identity_tests/retirement_finance_recovers_wallet_commit_after_write_failure_and_restart` exercises failed finance persistence and wallet reopening; `wallet_paymaster_provider.py` checks one exact fee event for each retirement asset transaction across repeated reconciliation. | + +Retirement also rejects a pending finite setup to avoid contradicting an older +approval (part of G04). A fresh preview of an unchanged outstanding setup now +returns its original plan ID and approved fee total, so a lost execute response +can be retried without a second job. The functional setup test covers that path. +The Dandelion success scenario explicitly approves 0.3 DGB per step to leave +room for the conservative multi-output DD estimate; it separately checks the +unchanged 0.2-DGB default and refusal under an insufficient fee cap. These tests do not close the complete G01-G14 matrix: +real disk exhaustion, process-kill windows, deterministic thread races, full +backup/clock/retention combinations, sustained network load and real Tor remain +separate release evidence. Historical unmarked retirement transactions are not +guessed into the finance ledger. No consensus, P2P or ordinary wallet-selection +code was changed for these corrections. + +Local checks: targeted MSVC `/Zs` compilation of the changed wallet/RPC units, +the two affected wallet test files, and the Qt production/test units. Qt uses +the installed v143 toolset required by the project; the newer compiler default +is incompatible with the installed Qt 5.15 headers. Fresh Qt MOC generation and syntax checking of that generated unit passed. +Both modified functional tests passed Python AST parsing and `--help` import/ +argument registration checks; `git diff --check` passed. No rebuilt executable test or live +node action is claimed by syntax checks. + +WSL follow-up (2026-09-20): the operator run passed 234 Paymaster unit cases +and all 46 Qt cases, but failed one maintenance-version unit case. That test +incorrectly rejected version 3 after the current record version became 4. +The corrected test explicitly accepts versions 3 and 4 and rejects versions +0, 1, 2 and 5, before and after serialization, without changing production +validation. An incremental WSL build and the corrected single test passed +(66 assertions). This initial result was superseded by the combined run below. + +The functional runner stopped before its scenarios because `digibyte_scrypt` +was missing. Install the CI Python dependencies (`digibyte_scrypt`, `pyzmq`, +`pypandoc`) in a virtual environment and run the runner with that environment's +Python. On Ubuntu, creating the environment may require the operator to install +`python3-venv`; building Python extensions may require `python3-dev`. +Use `--disable-external-signer` when the configure Boost.Process probe fails; +the selected Paymaster scenarios do not require external signing support. + +The subsequent operator run passed the Paymaster unit and Qt suites and four +of nine functional scenarios. Follow-up corrections cover stale setup-journal +state before retirement and internal retirement metadata leaking into wallet +RPC results. Functional fixtures now inspect the reconciled setup response, +capture read-only pool snapshots after readiness reconciliation, provide spent +prevouts when signing the competing reorg transaction, reconsider the original +invalidated block, and assert liquidity confirmation before ACTIVE after unlock. +The recovery-provider fixture explicitly approves the same 0.3-DGB setup fee +ceiling as the other multi-output DD fixture; production defaults remain intact. +All five previously failing functional scenarios passed targeted WSL reruns. +Incremental daemon builds, Python AST parsing and `git diff --check` passed. +The combined operator WSL run completed on 2026-09-20 at 20:19:37 +02:00: +Paymaster unit tests, all 46 Paymaster Qt cases and all nine selected functional +scenarios passed (exit codes 0/0/0). Evidence: operator log +`paymaster-retest-20260920-201621.log`. This verifies the selected Paymaster +matrix, not the complete DigiByte test suite or outstanding security release gates. + +Operator runtime verification must use fresh binaries containing these changes. +In a configured Linux/WSL checkout, run the build and existing Paymaster matrix +at the end of this document. For a narrow first pass after that build: + +```bash +./src/test/test_digibyte --run_test=paymaster_wallet_store_tests/auto_dispatch_rejects_unreadable_session_before_funding_or_signing,paymaster_wallet_identity_tests/pool_setup_reconciles_conflict_reversal_atomically,paymaster_wallet_identity_tests/retirement_finance_recovers_wallet_commit_after_write_failure_and_restart --log_level=test_suite --report_level=short +env -u DIGIBYTE_QT_TEST_OUTPUT QT_QPA_PLATFORM=offscreen DIGIBYTE_QT_TEST_SUITE=PaymasterWidgetTests DIGIBYTE_QT_TEST_FUNCTION=paymasterGuidedSetupBoundsSafetyAndRetriesFailedStep ./src/qt/test/test_digibyte-qt +python3 test/functional/test_runner.py wallet_paymaster_pool_setup.py wallet_paymaster_provider.py -j1 +``` + +Do not run an old executable against these names and count absence/skips as +success. The full build and provider functional test are delegated to the +operator under the repository's compute policy. Expect minutes to tens of +minutes or longer; success requires exit zero and all selected tests actually +executed. Existing Testnet data directories must not be used. + +### Windows operator commands + +Run in PowerShell from `D:\Digibyte\digibyte-fork` after other build/test jobs +finish. Prerequisites are the installed Visual Studio v143 toolset, static Qt +at `D:\Qt51510\install`, the existing vcpkg dependencies and configured +`test/config.ini`. This builds the four required programs and then runs the +focused wallet suites, Paymaster Qt suite and two functional scenarios. All +node processes belong to temporary functional-test directories. + +```powershell +& { + Set-Location 'D:\Digibyte\digibyte-fork' + $msbuild = 'C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\MSBuild.exe' + foreach ($target in @('digibyted', 'digibyte-cli', 'test_digibyte', 'test_digibyte-qt')) { + & $msbuild "build_msvc\$target\$target.vcxproj" /p:Configuration=Release /p:Platform=x64 /p:PlatformToolset=v143 /p:QTBASEDIR=D:\Qt51510\install '/p:SolutionDir=D:\Digibyte\digibyte-fork\build_msvc\' /m:1 /verbosity:minimal + if ($LASTEXITCODE -ne 0) { throw "Build failed: $target ($LASTEXITCODE)" } + } + $env:PYTHONUTF8 = '1' + $env:DIGIBYTED = (Resolve-Path 'build_msvc\x64\Release\digibyted.exe').Path + $env:DIGIBYTECLI = (Resolve-Path 'build_msvc\x64\Release\digibyte-cli.exe').Path + & .\build_msvc\x64\Release\test_digibyte.exe --run_test=paymaster_wallet_store_tests,paymaster_wallet_identity_tests --log_level=test_suite --report_level=short + if ($LASTEXITCODE -ne 0) { throw "Wallet tests failed: $LASTEXITCODE" } + Remove-Item Env:DIGIBYTE_QT_TEST_FUNCTION, Env:DIGIBYTE_QT_TEST_OUTPUT -ErrorAction SilentlyContinue + $env:DIGIBYTE_QT_TEST_SUITE = 'PaymasterWidgetTests' + & .\build_msvc\x64\Release\test_digibyte-qt.exe + if ($LASTEXITCODE -ne 0) { throw "Qt tests failed: $LASTEXITCODE" } + python -u test/functional/test_runner.py wallet_paymaster_pool_setup.py wallet_paymaster_provider.py -j1 + if ($LASTEXITCODE -ne 0) { throw "Functional tests failed: $LASTEXITCODE" } +} +``` + +## Original findings requiring correction + +### EC-01 — P1: AUTO can treat an unreadable existing session as a new ordinary send + +Sources: `src/wallet/paymasterstore_client.cpp:191`, +`src/wallet/rpc/paymaster_send.cpp:189`, `:259`, `:290`, +`src/rpc/digidollar.cpp:2464`. + +`GetSessionByRequestId` returns `false` for both NOT_FOUND and a session read +error. A tombstone read error also becomes `false`. The AUTO entry point uses +that boolean to decide whether to resume a durable payment. If the read fails, +it can reach a successful ordinary DGB-funded preflight and return `nullopt`; +`senddigidollar` then calls the ordinary transfer implementation. + +Trigger: retry a previously completed or authorized AUTO request, inject a +read error/unknown version for that request's session or tombstone, and provide +another eligible DD input plus sufficient DGB. The original input reservation +does not protect these other ordinary coins. The retry can become a second +payment instead of failing closed. This finding concerns a previously existing +Paymaster session, not the intended direct-DGB behavior of a genuinely new AUTO +request. + +Existing coverage: store tests reject corrupt/old session records and test +CreateOrJoinSession rollback. They do not exercise the AUTO dispatcher with +unreadable session state and independently sufficient direct funding. + +Required regression: parameterize live-session and tombstone errors, malformed +and future versions, sufficient/insufficient direct DGB, and terminal/nonterminal +sessions. Invoke the real send boundary; require an actionable database/version +error, identical wallet transaction count, no new signature/broadcast, unchanged +balances and reservations. A genuine NOT_FOUND case must retain normal AUTO +behavior. Preserve the read status across the dispatch boundary; do not add a +second session store. + +### EC-02 — P2: the Qt setup wizard and automatic-setup RPC have incompatible contracts + +Sources: `src/qt/paymasterwidget.cpp:10527`, `:10740`, `:10757`, `:10800`, +`src/wallet/rpc/paymaster_provider.cpp:2201`. + +The wizard disables an existing provider at step 1 and executes pool funding at +step 8; it persists the chosen enabled state only at step 10. A new provider is +also initially disabled. The new execution RPC refuses a disabled provider. +Consequently, a wizard setup with missing pool outputs cannot complete this +sequence. A pool that already meets its targets takes the no-funding branch and +can hide the incompatibility. + +Independently, the wizard requires `executed=true`. The new durable contract +legitimately returns `accepted=true, executed=false` while waiting for funds, +index synchronization, or an approved fee opportunity. The wizard treats that +accepted command as a failure. Its result validator still accepts the old +response shape without requiring the new acceptance/fee fields. + +Existing coverage: `paymasterGuidedSetupBoundsSafetyAndRetriesFailedStep` uses an +injected funding response with zero missing outputs. Other injected RPC tests +exercise UI actions without enforcing the backend's new enabled-state gate. + +Required regressions: fresh disabled wallet and existing enabled wallet, both +with missing slots; accepted-but-deferred funding; partial DD-only completion; +wallet relock after acceptance; wizard retry, close and wallet switch after a +lost response. Assert exact approval, one durable plan, correct pending text, +and no unintended autostart. Align approval while disabled and execution while +enabled deliberately; do not silently enable provider operation just to make +the wizard pass. Acceptance must not be rendered as confirmed readiness. + +### EC-03 — P2: recurring DGB replenishment retains the original stem-parent failure + +Sources: `src/wallet/rpc/paymaster.cpp:2028`, `:2181`, `:2193`, +`src/wallet/rpc/paymaster_runtime.cpp:459`, `src/wallet/coincontrol.h:21`, +`src/node/transaction.cpp:98`. + +The finite-setup path now selects confirmed DGB inputs. Recurring +`RunAutomaticDGBReplenishment` still uses default coin control with minimum +depth zero. When carrier replenishment or another wallet send has consumed the +only confirmed funding coin, safe wallet change can be stem-only. A following +DGB replenishment can sign and save a child that the ordinary mempool fee +precheck rejects as missing/spent, reproducing the original setup failure in a +different workflow. + +The maintenance journal helps prevent another replacement from being created; +that does not prove prompt continuation or rebroadcast of the saved child. +Current lifecycle/replenishment tests disable Dandelion and generally fund a +mining wallet with many confirmed coins. + +Required regression: enable Dandelion with an inert stem-capable peer, use one +confirmed non-pool DGB input, and simultaneously create a DD-carrier and DGB +liquidity deficit. Cover parent confirmation, stem eviction/fluff, restart and +lost RPC response. Assert exact transaction identity, bounded fees and eventual +readiness without unrelated replenishments. Reuse the confirmed-funding rule or +an equally explicit existing-parent continuation rule within Paymaster code. + +### EC-04 — P2: a failed setup step can permanently block further setup and service + +Sources: `src/wallet/rpc/paymaster.cpp:1269`, `:1397`, +`src/wallet/rpc/paymaster_provider.cpp:1742`, `:2175`, `:2195`, +`src/wallet/rpc/paymaster_runtime.cpp:450`. + +Reconciliation moves an abandoned/conflicted setup transaction to FAILED, then +skips FAILED records on later passes. `HasPendingPoolPreparation` nevertheless +counts FAILED as pending. Cancellation only releases PLANNED records without a +transaction ID. New setup commands are rejected while pending, and automatic +provider service stops accepting new work/replenishing while setup is pending. + +This makes a normal cancellation insufficient to clear the blockage. More +seriously, if a conflicting branch disappears and the exact original setup +transaction later confirms, the skipped FAILED record cannot be promoted to +complete. A conflict observation is not necessarily irreversible chain truth. +The periodic continuation keeps reporting the conflict; there is no normal +setup-level resolution path in the inspected implementation. + +Required regressions: setup -> confirmation -> conflicting reorg -> reconfirm +original; abandoned signed transaction; one conflicted step with one unsigned +sibling; explicit cancellation followed by a newly approved plan. Keep saved +bytes and ambiguous obligations protected. Re-observe exact signed transactions +and define an explicit safe resolution rather than releasing signatures based +only on timeout or absence from a mempool. + +### EC-05 — P2: pool retirement can lose fee accounting across a partial failure + +Sources: `src/wallet/rpc/paymaster_provider.cpp:2532`, `:2586`, `:2616`, +`src/wallet/rpc/paymaster.cpp:1740`, `:1762`, `:1890`. + +`rebalancepaymasterpool` commits/broadcasts DD retirement and then DGB retirement, +and only afterwards writes their POOL_RETIREMENT finance events. It does not +persist a maintenance operation containing those retirement transaction +bindings first. If the DGB step fails after DD retirement, the process exits, or +the finance write fails, an already committed retirement can lack its fee event. + +Finance reconciliation refreshes existing events and reconstructs setup, +provider-commit and maintenance events. That is not reconstruction of a missing +retirement event. A retry may see already-spent inputs and a changed plan, so it +does not necessarily revisit the missing accounting write. The wallet retains +the transaction; the finding is incomplete recovery/accounting, not evidence +that the principal disappeared. + +Required regression: interrupt/fail each boundary after DD commit, after DGB +commit, after pool write and before/at finance write. Restart and retry must +recover each original transaction and one fee event per transaction, with +unchanged targets and no duplicate retirement. Extend the existing durable +operation mechanism instead of adding a separate retirement database. + +## Additional test gaps, without a demonstrated defect claim + +| ID | Workflow / boundary | Existing protection and coverage | Additional case and invariant | +|---|---|---|---| +| G01 | DD replenishment and carrier excess withdrawal | Planner fee caps, maintenance reservations and exact-plan calls exist. | These paths still call `TransferDigiDollarMany`, which can auto-abandon a rejected wallet transaction; actual-fee checks run after commit. Inject transient oracle/policy/relay failures and test fee cap -1/equal/+1 and dust-sized DGB change. Establish that no over-cap transaction reaches commit and that rejected signed bytes have an explicit recovery policy. The conservative planner alone is not a test of that boundary; no actual cap overrun was reproduced here. | +| G02 | Finite setup crash consistency | V3/V4 codec tests, journal write/abort tests and graceful restart are present. | Kill/fail after approval persistence, after each wallet commit, after broadcast but before pool reconciliation, and between journal/pool writes. Reopen real SQLite and verify one step/tx/output set. Existing provider-final-commit rollback tests do not cover these setup transitions. | +| G03 | Concurrent setup, stop, cancel and policy changes | Work guard and wallet locks exist; concurrent quote/last-slot tests already exist. | Race execute/execute, tick/execute, cancel/commit, disable/tick, policy update/tick and unload/tick using deterministic barriers. A losing operation must not overwrite a newer journal state or create an additional signature. | +| G04 | Manual pool edits during a deferred setup | Rebalance blocks active pool reservations; pending unsigned setup is a separate durable condition. | Start a funded-partial or unfunded setup, then retire slots/change liquidity targets/release a carrier. Define whether to reject, cancel unsigned work or require a revised explicit approval. Verify that later funding does not silently undo the operator's newer pool decision. | +| G05 | Exact legacy-transaction adoption | Positive adoption of a saved DGB child is in the new regression. | Wrong wallet, DD transaction, foreign input/output, reserved pool input, duplicate/out-of-range vout, wrong amount/purpose, spent output, fee excess, conflict between preview and execute, and repeated adoption after registration. Require no mutation for every rejection and no replacement transaction. | +| G06 | Funding boundaries | Missing DGB, one funding input, normal recovery and a tiny fee cap are covered. | No DD, exact 1-DD carriers, subminimum DD change, fragmented DD, selected/reserved/locked/immature coins, balance equal to principal but below fees, and sufficient total balance with insufficient eligible confirmed balance. Diagnostics must distinguish confirmation, unlock, fee cap and genuine shortfall. | +| G07 | Confirmation and reorg lifecycle | The functional reorg test rolls back/reconfirms a payment; store tests cover successor states. | Actual chain reorgs of setup, replenishment, retirement and withdrawal, including parent/child and an already-spent successor. The maintenance-source unit test constructs valid state shapes; it does not execute the reconciliation function through a real reorg. Never expose both source and successor as spendable. | +| G08 | Full payment finalization under Dandelion | Exact-final validation explicitly checks both pools and compares witness bytes. | Run USER_PAID, public/restricted SPONSORED, retry_same and cancel_to_self with stem acceptance, delayed result, disconnect, eviction and parent reorg. Require no finality before confirmation and no premature reservation release. Do not infer this coverage from the new setup-only stem test. | +| G09 | Client AUTO, selected inputs and send-all | Two-stage Paymaster authorization, fee rounding, balance snapshots and ordinary DGB sends have focused coverage. | Vary direct funding between retries and between a preview and authorization; inject session/tombstone failures (EC-01), delayed incoming DD and an ordinary competing wallet send. Keep the intended distinction: a new AUTO request may directly pay with DGB; an existing Paymaster session must not switch to another payment. | +| G10 | Settings, readiness and scheduler | Locked autostart, disabled Paymaster, no txindex, pruning and activation gates are tested. | Deferred setup with walletbroadcast=0, network disabled, txindex temporarily behind, reindex/rescan, activation/oracle availability transitions, and unlock timeout during construction. Test `--enable-debug` lock order while callbacks run; no synchronous queue wait on the scheduler or while holding wallet locks. | +| G11 | Time, retention and bounded journals | Budget high-water, token buckets and serialization bounds are tested. | NTP rollback during reconciliation, forward jumps past expiry, 255/256/257 maintenance records, repeated unsigned cancel/reapprove, pruning older completed setup records and retry of their old plan IDs. Open signatures stay protected; bounded history must fail clearly without duplicate execution. | +| G12 | Persistence, backups and versions | Provider/client write-failure tests and one older-client-backup functional case exist. | Real disk-full/commit failure at maintenance/setup/retirement boundaries; provider restore with budget and sponsorship records older than on-chain payments; V3-only, mixed V3/V4, unknown/truncated V4. Backup rollback limits are already documented and are not solved by a rescan. Never run original and restored provider copies concurrently. | +| G13 | Discovery, private transport and malformed peers | Extensive Capacity/quote binding, cross-peer replay, lease/ABA, rate-limit, fairness and payload-bound tests exist. | Actual Tor proxy/authentication failure and reconnect at every private protocol stage, then sustained validly encoded invalid proofs while ordinary DGB and legitimate Paymaster payments run. Measure bounded CPU/RSS and progress, not only rejection counts. Existing real-Tor/load gates remain open. | +| G14 | Qt and operator observations | Wallet-switch callbacks, unlock leases, material-change confirmation, recovery and finance UI tests exist. | New accepted/pending/error setup statuses, missing acceptance/fee fields, lost execute response and later restart; keep exact approved fee limits visible. Use contract-faithful injected responses and at least one backend integration smoke test; a mock returning zero missing outputs cannot test funding. | + +These gaps are combinations of existing boundaries. Reuse the current fixtures, +wallet/store fault injection and balance oracles; avoid another implementation of +coin selection, fee calculation, reservation logic or session state in tests. + +## Suggested execution order and acceptance + +1. Add failing regressions for EC-01 and EC-02, then correct the dispatch/UI + contracts. Add the recurring Dandelion case EC-03 alongside the setup case. +2. Cover EC-04 and EC-05 with actual reconciliation and durable transaction + boundaries, not only serialization or hand-assigned final states. +3. Add the focused G01-G07 cases before treating pool automation as accepted. + Retain the existing client/protocol/security suites; they already test + important adversarial behavior and should not be rewritten wholesale. +4. Run lifecycle/environment combinations G08-G14, the ordinary wallet/DD + regressions and the existing sanitizer/fuzz and deployment release gates. + +For every financial case record eligible balances, wallet/mempool/stempool +transaction identities, live chain outpoints, pool states, reservations, fee +ledgers and finance events before/after. Assert the expected changes exactly; +RPC success or test-case counts alone do not prove the money/state invariant. + +Full builds and suites remain operator tasks under `AGENTS.md`. Use a configured +Linux/WSL checkout containing the reviewed changes, wallet/SQLite/Qt tests +enabled, freshly rebuilt binaries and isolated regtest directories. The old +prepared WSL runner pins the pre-change commit and does not test this tree. +Builds typically take tens of minutes; the full provider functional scenario and +combined Paymaster matrix can also take tens of minutes or longer. + +Existing baseline commands from the checkout root (these do not add the missing +regressions above): + +```bash +make -j2 -C src digibyted digibyte-cli test/test_digibyte qt/test/test_digibyte-qt +./src/test/test_digibyte --run_test='paymaster_*' --log_level=test_suite --report_level=short +env -u DIGIBYTE_QT_TEST_FUNCTION -u DIGIBYTE_QT_TEST_OUTPUT QT_QPA_PLATFORM=offscreen DIGIBYTE_QT_TEST_SUITE=PaymasterWidgetTests ./src/qt/test/test_digibyte-qt +python3 test/functional/test_runner.py p2p_paymaster.py wallet_paymaster_readiness.py wallet_paymaster_rpc.py wallet_paymaster_provider.py wallet_paymaster_pool_setup.py wallet_paymaster_lifecycle.py wallet_paymaster_offer_selection.py wallet_paymaster_failover.py wallet_paymaster_reorg.py -j1 +``` + +The optional `p2p_paymaster_v9_26_5_bridge.py` requires the separately documented +old binary. Require exit code zero, no unexpected skips, matching source/binary +identification and the per-case invariants. RPC `--coverage` measures method +invocation, not edge-case or branch coverage. No executable suite, live-node +experiment, new fault-injection regression or sanitizer run was performed in +this review; there is no new runtime pass claim. diff --git a/doc/digidollar-paymaster-implementation.md b/doc/digidollar-paymaster-implementation.md new file mode 100644 index 00000000000..5f965773d36 --- /dev/null +++ b/doc/digidollar-paymaster-implementation.md @@ -0,0 +1,441 @@ +# DigiDollar Paymaster implementation reference + +**Source baseline:** `feature/digidollar-paymaster-v1` at `bd270044c1`. +**Documentation review:** 2026-09-11. **Wire protocol:** V5. + +This reference describes the inspected implementation and the invariants a +reviewer should check. It is not a new implementation proposal or a claim that +the full release matrix has been rerun. Start with [PAYMASTER.md](../PAYMASTER.md) +for the reading order, branch scope, and commit navigation. The +[release gate](digidollar-paymaster-release-gate.md) records dated test evidence. + +## 1. Problem, scope, and participants + +DigiDollar outputs carry DD amounts while the miner fee is paid in DGB. The +ordinary send path requires the sender's wallet to fund both. Paymaster adds a +collaborative transfer in which the sender provides DD and a provider supplies +DGB for the fee. Both parties retain their private keys. + +| Participant | Responsibility | Authority it does not receive | +|---|---|---| +| Client wallet | Select DD, authorize recipient and fee, validate the entire transaction, sign client inputs, retain recovery state. | No right to spend unreserved provider liquidity. | +| Provider wallet | Prove identity/resources, issue a quote under local budgets, validate client authorization, sign provider inputs, commit and broadcast. | No discretionary custody or signing authority over client DD. | +| Upgraded relay | Relay bounded admission announcements and directory requests. | No payment authorization or privileged consensus role. | +| Existing validator/miner | Validate/mine the ordinary resulting DD transfer. | No requirement to run provider discovery or a Paymaster wallet. | + +The supported economic models are `USER_PAID`, public `SPONSORED`, and +restricted `SPONSORED`. Sponsored payments have zero client service fee; +restricted sponsorship additionally requires a payment-bound capability. +There is one recipient per Paymaster session. This path does not sponsor mint +or redemption operations and does not add Paymaster batching to +`sendmanydigidollar`. + +## 2. Transaction construction and fee accounting + +DD values use integer cents; DGB values use satoshis. For a user-paid offer: + +```text +service_fee_cents = ceil(recipient_amount_cents * fee_rate_bps / 10000) +user_DD_in = recipient_DD + user_DD_change + service_fee_DD +provider_DGB_in = provider_DGB_change + miner_fee_DGB +``` + +The implementation uses checked integer arithmetic and rejects invalid amounts +and rates. Each DD output must remain between 100 and 10,000,000 cents. Inputs +are resolved against their creating transactions and the local UTXO view; +DD inputs must be confirmed. A quote or PSBT cannot substitute its own claimed +prevout values for those checks. + +### Why a carrier is needed + +A fee below 1 DD cannot be represented as its own DD output. For a positive +fee below 100 cents, the provider contributes a confirmed DD carrier and +receives its principal plus the service fee in a replacement carrier output. + +Example: the client has 20.00 DD and no DGB, sends 10.00 DD, and accepts a +50-basis-point offer (0.5%, or 0.05 DD). The provider uses a 1.00 DD carrier. + +| Asset | Inputs | Outputs / fee | +|---|---|---| +| DD | Client 20.00 + provider carrier 1.00 = 21.00 DD | Recipient 10.00 + client change 9.95 + provider carrier successor 1.05 = 21.00 DD | +| DGB | Provider fee inputs | Provider DGB change plus the miner fee | + +There is no 0.05-DD output. The provider earns 0.05 DD while retaining its +1.00-DD principal. This is a carrier's accounting role; it is not newly minted +DD or an exception to conservation. Zero-fee sponsorship needs no carrier for +an unused service fee. Fees of at least 1 DD can use a normal provider-fee +output, subject to the same output limits. + +The builder finalizes all inputs and outputs before signing. Change scripts +are explicit and wallet-owned; the normal collaborative client supplies no +DGB inputs. Inspect [txbuilder.cpp](../src/paymaster/txbuilder.cpp), +[types.cpp](../src/paymaster/types.cpp), and the output-role definitions in +[txbuilder.h](../src/paymaster/txbuilder.h). + +### Exact total outflow + +`subtract_paymaster_fee_from_amount=true` makes the supplied amount the exact +DD outflow. Core finds a recipient amount whose rounded fee yields exactly +that total, rejecting cent-rounding gaps with `PAYMASTER_NO_EXACT_GROSS_OFFER`. +For example, 50.00 DD at 50 basis points becomes 49.75 DD to the recipient and +0.25 DD fee. `send_all_spendable_dd=true` additionally binds the spendable input +snapshot and rejects a changed balance before reservation. The +[operator guide](digidollar-paymaster.md#exact-total-outflow-and-emptying-a-dd-wallet) +documents the options and exclusions. + +## 3. Discovery and payment sequence + +```mermaid +sequenceDiagram + participant R as Upgraded relay + participant C as Client wallet + participant P as Provider wallet + participant N as DigiByte network + P->>R: Signed admission announcement + R->>C: Directory / admission proofs + Note over C: Filter locally and select one provider + C->>P: PMCAPREQ over isolated v2 connection + P->>C: PMCAPRESP with identity and operational proofs + Note over C: Validate and persist exact capacity snapshot + C->>P: PMQUOTEREQ with bound payment intent + Note over P: Validate intent and reserve pool / budget atomically + P->>C: PMQUOTERESP with exact transaction and fee + Note over C: Rebuild, authorize, sign client inputs, persist exact PSBT + C->>P: PMSUBMIT + Note over P: Revalidate, sign provider inputs, durably commit final bytes + P->>N: Broadcast ordinary DD_TX_TRANSFER + P->>C: Signed PMRESULT + Note over C: Verify final bytes, scripts, and local network observation + N-->>C: Mempool / block / reorganization observations +``` + +This diagram shows logical responsibilities; broadcast completion and result +delivery are not an atomic network operation. A lost result must be recoverable +from the durable provider commit. + +1. **Discovery:** `sendpmasters`, `getpmasters`, and `pmannounce` operate on + upgraded ordinary peers. The directory checks identity and dedicated, + confirmed admission resources. Admission liquidity is separate from the + operational pool. Only admission proofs are gossiped. +2. **Selection:** offers are filtered for readiness, amount, funding model, + authorization, local reputation, privacy, and fee limits. Ranking uses exact + local totals. Only the selected provider receives operational requests. +3. **Capacity:** before DD outpoints, payment intent, or restricted capability + are disclosed, V5 authenticates the provider and the exact operational + resource set. Verify genesis, identity signature, request/session binding, + nonce, reference block, expiry, BIP86 control proofs, creating transactions, + and live outpoints. Persist the validated snapshot. +4. **Quote:** the signed intent binds the local order and user input-control + proofs. The quote must match the selected offer, policy, capacity, recipient, + fee, change, and complete unsigned transaction. The provider reserves pool + entries and fee budget atomically. +5. **Client signing:** Core validates its local authorization manifest against + the trusted template. The client signs only its own DD inputs with + `SIGHASH_DEFAULT`. The exact signed PSBT and authorization become durable + before being sent. The high-level call must round-trip the exact + `authorization_commitment` returned by preparation before a new user + signature is possible; background work does not replace that acceptance. +6. **Provider signing:** Core rechecks local policy, reservation, template, + client signatures, and live inputs. It signs only its own inputs and commits + final raw bytes, result bindings, pool successors, and budget transitions + before broadcast. +7. **Result:** the client reconstructs the trusted template and verifies txid, + wtxid, all inputs/outputs and witnesses with actual prevouts. When needed, + mempool preflight must succeed. An authenticated provider claim alone is + insufficient to establish confirmation. + +The [wire records](../src/paymaster/wire.h), +[protocol records](../src/paymaster/protocol.h), and +[P2P command names](../src/protocol.cpp) define the actual encoding. V5 is +required; there is no downgrade to earlier Paymaster exchanges. Alternative +recovery uses `pmrecreq`, `pmrecresp`, `pmrecsub`, and `pmrecresult` on the +isolated direct connection. + +## 4. Authorization and trust boundaries + +The local wallet and software are trusted; the remote party may be malicious. +A transport connection does not authorize spending. Each wallet independently +reconstructs the exact transaction and validates local authority immediately +before a new signature. + +| Artifact | Binding reviewed before signing or execution | +|---|---| +| `ValidatedCapacitySnapshot` | Authenticated provider and exact operational resources for the selected attempt. | +| `ClientAuthorizationManifest` | Canonical local request, requested fee/privacy/selection modes, provider/offer/policy, capacity, user inputs, recipient/amount, wallet-owned change, fee ceiling, expiry and template. | +| `ProviderAuthorizationManifest` | Validated intent, reserved provider inputs, provider-owned returns/change, exact fees, safety policy, and the precise budget reservation/commit key. | +| `AlternativeRecoveryManifest` | Original session and inputs, distinct recovery provider, wallet-owned DD returns, bounded fee and inherited privacy. | +| Durable provider commit | One exact final raw transaction and corresponding immutable authorization/accounting bindings. | + +Unknown PSBT fields, incorrect roles, substituted outputs, and non-default +sighashes are rejected. RPC, GUI, automatic service, retry and recovery share +these checks. New provider signatures require the current safety-policy +binding. An already committed exact transaction retains the historical binding +needed for completion; changing policy does not create new signing authority +or invalidate an otherwise recoverable exact commit. + +Signed contradictory capacity or quote claims are stored as local evidence and +block the provider locally. The first valid signed claim is staged as evidence +before deeper validation; it does not itself authorize a spend. Database +failures are kept distinct from invalid remote artifacts so work is not +acknowledged and lost on an unsuccessful persistence operation. + +See [psbt.cpp](../src/paymaster/psbt.cpp), +[reservation.h](../src/paymaster/reservation.h), and the +[hardening plan](../DIGIDOLLAR_PAYMASTER_HARDENING_PLAN.md). + +## 5. Sessions, retries, and finality + +`request_id` is a canonical lowercase UUID bound to a canonical request hash. +Repeating the same request resumes its session; conflicting reuse is rejected. +A session owns its user inputs and can contain separate provider attempts. +An attempt binds one provider, quote, capacity snapshot, and transaction +template. A retry reuses exact persisted artifacts. + +| State / phase | Meaning for the caller | +|---|---| +| `CREATED`, `INPUTS_RESERVED` | Request established and inputs protected. | +| `AWAITING_WALLET_UNLOCK`, `AWAITING_USER_SIGNATURE` | The same session awaits local action; neither authorizes provider fallback by itself. | +| `AUTHORIZED`, `PENDING_PROVIDER` | Signed authority may exist; protect inputs and inspect the pending phase. | +| `USER_SIGNATURE_SENT` | The exact client PSBT is durable and may be at the provider. | +| `PROVIDER_SIGNED_KNOWN`, `PENDING_NETWORK` | Provider signing/final commit is known, but network delivery or observation is incomplete. | +| `STEMPOOL`, `MEMPOOL` | Transaction observed in a relay pool, not a confirmed payment. | +| `CANCEL_MEMPOOL` | Recovery transaction is in the mempool; cancellation is still pending. | +| `CONFIRMED`, `CANCELED_SAFE` | Chain-confirmed payment or safe cancellation; chain reconciliation still handles reorganization. | +| `FAILED`, `CONFLICTED` | Inspect the recorded reason and Core-derived allowed actions; do not infer that arbitrary inputs are free. | + +These are explanatory groups, not a complete transition table. The exact +session and attempt enums and legal transitions are in +[reservation.h](../src/paymaster/reservation.h) and +[reservation.cpp](../src/paymaster/reservation.cpp). `DGB_COMMITTING` also +exists for the ordinary DGB-funded session path. + +`resolvepaymastersession` exposes `refresh`, `retry_same`, `fallback`, +`abandon_unsigned`, and `cancel_to_self`. Mutation must be present in the +current Core-derived `allowed_actions`: + +- `fallback` is allowed only **before a user PSBT exists**, preserving the + reserved DD input set when moving to a new provider attempt. +- `abandon_unsigned` releases only a session for which no transaction + authorization can exist. +- `retry_same` creates no new quote, attempt, reservation or signature. +- After signing, timeout does not release inputs. `cancel_to_self` prepares or + resumes one exact same-input return transaction. A distinct eligible + `USER_PAID` provider can supply DGB when the client has none. Signing requires + the exact recovery authorization commitment, and cancellation is final only + after confirmation. + +Remote messages cannot undo a final committed authorization. Separately, local +chain reconciliation can roll back a confirmation after a reorganization and +restore pending finance/pool state. This distinction is tested by +[wallet_paymaster_reorg.py](../test/functional/wallet_paymaster_reorg.py). + +## 6. Persistence and provider operation + +`PaymasterStore` persists sessions, attempts, reservations, manifests, signed +PSBTs, final transactions, budget ledgers, recovery and replay evidence in the +wallet database. Related writes use atomic batches; missing or unreadable +authority cannot be synthesized from a remote message. Records require their +exact current versions. Older experimental Paymaster records fail closed; +ordinary v9.26.5 wallet compatibility does not imply migration of old Paymaster +state. + +Confirmation promotes wallet-owned carrier/DGB successors into usable provider +liquidity. Reorganization and conflict reconciliation reverse that availability +when necessary. Optional automatic maintenance tracks pending work and only +fills missing targets. Paid maintenance requires explicit approval and finite +per-transaction/hour/day DGB fee limits. Manual pool operations and withdrawals +bind execution to a preview so a stale plan cannot silently spend different +resources. + +Provider operation requires explicit wallet configuration and start. Automatic +queue processing and optional autostart are separate settings. Wallet locking +pauses signing; automatic service does not authorize client payments. Provider +finance records keep DD income, DGB costs, pool principal, and optional oracle +valuation separate. A backup acknowledgement records an operator action, not +proof that a backup is usable. + +Eligible completed records are reduced to idempotency tombstones after the +240-block reorganization safety depth. Ambiguous/unconfirmed/unreadable records +are not automatically removed. Wallet encryption protects keys but does not +add field-level encryption to Paymaster metadata and signed artifacts. See the +[retention description](digidollar-paymaster.md#local-metadata-and-retention). + +## 7. Runtime and compatibility boundaries + +Client/provider readiness requires DigiDollar activation, Paymaster enabled, +an unpruned node, fully synchronized `txindex`, and BIP324 v2. Provider wallets +must be descriptor wallets with local private keys. Relay nodes do not need to +operate such a wallet. `-paymaster=0` disables Paymaster discovery, relay and +client support; provider startup remains explicit with the default enabled. + +Standard direct sessions require v2 without v1 fallback. High privacy adds an +onion provider, Tor stream isolation, one provider attempt, and no clearnet +fallback. Readiness also enforces capture/logging restrictions described in the +operator guide. The provider still sees the transaction it co-signs, and the +blockchain remains public. + +An old node can validate the final ordinary transfer but cannot relay the +Paymaster discovery protocol. A topology connected only through old peers can +therefore carry normal transactions while Paymaster selection remains +unavailable. The dedicated old-bridge test deliberately injects an announcement +and observes that it is not relayed; this differs from the normal old-leaf test +where Paymaster announcements are not sent to that node. + +The overall branch includes arithmetic portability changes in consensus files; +the [branch review note](../PAYMASTER.md#review-the-branch-in-bounded-pieces) +explains why these still require separate equivalence review. + +## 8. Source map and RPC entry points + +| Area | Start here | +|---|---| +| Arithmetic and deterministic transaction | [types.cpp](../src/paymaster/types.cpp), [txbuilder.cpp](../src/paymaster/txbuilder.cpp) | +| Protocol, PSBT and local authorization | [protocol.cpp](../src/paymaster/protocol.cpp), [psbt.cpp](../src/paymaster/psbt.cpp) | +| Identity, admission and chainstate verification | [directory.cpp](../src/paymaster/directory.cpp), [validation.cpp](../src/paymaster/validation.cpp), [paymasteridentity.cpp](../src/wallet/paymasteridentity.cpp) | +| P2P and bounded work queues | [wire.cpp](../src/paymaster/wire.cpp), [manager.cpp](../src/paymaster/manager.cpp), [net_processing.cpp](../src/net_processing.cpp) | +| Client order and offer selection | [client.cpp](../src/paymaster/client.cpp), [reputation.cpp](../src/paymaster/reputation.cpp), [paymaster_discovery.cpp](../src/wallet/rpc/paymaster_discovery.cpp) | +| Wallet state and atomic commits | [paymasterstore.h](../src/wallet/paymasterstore.h), [paymasterstore_client.cpp](../src/wallet/paymasterstore_client.cpp), [paymasterstore_provider.cpp](../src/wallet/paymasterstore_provider.cpp), [paymasterstore_finalization.cpp](../src/wallet/paymasterstore_finalization.cpp) | +| Wallet record codecs | [paymasterdb.cpp](../src/wallet/paymasterdb.cpp), existing `WalletBatch` declarations and transaction semantics | +| Send RPC integration | [paymaster_send.cpp](../src/wallet/rpc/paymaster_send.cpp), existing amount units, options and durable-session ordering | +| Recovery, reconciliation and pruning | [recovery.cpp](../src/paymaster/recovery.cpp), [paymasterstore_recovery.cpp](../src/wallet/paymasterstore_recovery.cpp), [paymasterstore_reconciliation.cpp](../src/wallet/paymasterstore_reconciliation.cpp) | +| Provider policies, budgets and liquidity | [provider.cpp](../src/paymaster/provider.cpp), [paymasterprovider.cpp](../src/wallet/paymasterprovider.cpp), [paymaster_provider.cpp](../src/wallet/rpc/paymaster_provider.cpp) | +| Request/submit/result handlers and runtime | [paymaster_processing.cpp](../src/wallet/rpc/paymaster_processing.cpp), [paymaster_runtime.cpp](../src/wallet/rpc/paymaster_runtime.cpp), [paymaster_integration.cpp](../src/wallet/rpc/paymaster_integration.cpp) | +| Wallet signing bridge | [paymasterpsbt.cpp](../src/wallet/paymasterpsbt.cpp), [paymaster_client.cpp](../src/wallet/rpc/paymaster_client.cpp) | +| Existing wallet integration and protection | [digidollarwallet.cpp](../src/wallet/digidollarwallet.cpp), [spend.cpp](../src/wallet/spend.cpp), [load.cpp](../src/wallet/load.cpp) | +| Qt | [paymastersendwidget.cpp](../src/qt/paymastersendwidget.cpp), embedded by [digidollarsendwidget.cpp](../src/qt/digidollarsendwidget.cpp), provider panel in [paymasterwidget.cpp](../src/qt/paymasterwidget.cpp), embedded through [paymasterwidget.h](../src/qt/paymasterwidget.h), [walletmodel.cpp](../src/qt/walletmodel.cpp), [paymasterconfirmation.h](../src/qt/paymasterconfirmation.h) | + +The high-level entry is `senddigidollar` in +[src/rpc/digidollar.cpp](../src/rpc/digidollar.cpp); the optional seventh `options` +argument follows upstream `amount_unit` and preserves the upstream direct-DGB API. `fee_mode=auto` +falls back only after a concrete insufficient-DGB preflight, not on wallet lock +or unrelated failure. Lower-level quote/PSBT APIs are explicit multi-step +interfaces and do not bypass Core authorization. + +| Caller | Principal RPCs | +|---|---| +| Client | `getpaymasteroffers`, `requestpaymasterquote`, `walletprocesspaymasterpsbt`, `submitpaymasterdigidollar`, `getdigidollarsendsession`, `listdigidollarsendsessions`, `resolvepaymastersession` | +| Fee safety | `setpaymasterclientsafetypolicy`, `getpaymasterclientsafetystatus`, `setpaymastersafetypolicy`, `getpaymastersafetystatus` | +| Provider setup/runtime | `createpaymasteridentity`, `setpaymasterpolicy`, `setpaymasterenabled`, `setpaymasterruntimesettings`, `startpaymaster`, `stoppaymaster`, `getpaymasterinfo` | +| Liquidity/finance | `preparepaymasterpool`, `rebalancepaymasterpool`, `setpaymasterliquiditypolicy`, `getpaymasterliquiditystatus`, `withdrawpaymastercarrier`, `getpaymasterfinancestatus`, `acknowledgepaymasterproviderbackup` | + +This is a navigation inventory. Exact parameter/result schemas come from the +`RPCHelpMan` definitions and `help ` on a binary built from the reviewed +revision. Wallet registration is in [wallet.cpp](../src/wallet/rpc/wallet.cpp); +`listpaymasters` is node-level. Units and examples are in the operator guide. + +### High-level payment authorization + +For a Paymaster path, `senddigidollar` without `authorization_commitment` +prepares the exact authorization and returns without creating a new user +signature. Once preparation is complete, the caller reviews the provider, +funding model, recipient, amount, fee and returned commitment, then repeats +the same request with that exact commitment. A mismatch is rejected. The +`prepare_only` flag is a compatibility hint; setting it to false cannot bypass +the missing-commitment guard. A headless application must implement this +explicit acceptance step too. + +Prepared, authorized and confirmed are different results. Read +`authorization_required`, `authorization_accepted`, the session state and +Core-derived actions; do not treat the first RPC response as a payment receipt. +This does not change the ordinary direct-DGB path. See +[paymaster_discovery.cpp](../src/wallet/rpc/paymaster_discovery.cpp) and the +[operator example](digidollar-paymaster.md#prepare-and-authorize-a-paymaster-payment). + +## 9. Changes from the initial design + +| Design area | Current behavior / review implication | +|---|---| +| “V1” project label | Refers to the feature milestone; current wire protocol is V5, with individually versioned persistent records. | +| Broad same-input fallback after signing | Ordinary provider fallback stops once a user PSBT exists. Signed ambiguity uses exact retry or explicitly authorized cancellation. | +| General PSBT-based collaboration | Dedicated Paymaster validators, local manifests and role-limited wallet signing enforce the collaborative path. | +| Basic pools and manual processing | Successor reuse, finite automatic liquidity maintenance, finance reporting, runtime modes and explicit autostart are implemented. | +| One high-level automatic send | Paymaster selection/preparation returns an exact authorization commitment; a caller must explicitly round-trip it before a new user signature. | +| Recipient amount plus fee | Optional exact-gross and all-spendable-DD modes now exist with checked cent rounding. | +| Private/temporary protocol state | Durable recovery artifacts and metadata are not field-encrypted; retention must preserve safety and idempotency. | +| Legacy compatibility | Ordinary old wallets/validators and old Paymaster wire/database formats are different compatibility questions; the latter have no implicit migration. | +| Tests and acceptance | The 28 criteria are review identifiers, not a claim of current release approval. Dated local evidence, release-build reruns and external gates remain distinct. | + +## 10. Verification map and remaining work + +Additional security-boundary checks, dated local results and the remaining +operator evidence are recorded in the [release gate](digidollar-paymaster-release-gate.md#additional-security-boundary-checks-2026-09-19). + +| Concern | Existing verification surface | +|---|---| +| Amounts, carriers, PSBT roles and mutation rejection | `src/test/paymaster_*_tests.cpp`, `src/wallet/test/paymaster_wallet_psbt_tests.cpp` | +| Atomic authority, budgets, failures and restart | `src/wallet/test/paymaster_wallet_store_tests.cpp`, `src/wallet/test/paymaster_wallet_security_tests.cpp` | +| Wallet maintenance and load | [paymaster_wallet_load_tests.cpp](../src/wallet/test/paymaster_wallet_load_tests.cpp) | +| DGB-less payment and sponsored variants | [wallet_paymaster_provider.py](../test/functional/wallet_paymaster_provider.py) | +| Multiple offers and selected-provider-only effects | [wallet_paymaster_offer_selection.py](../test/functional/wallet_paymaster_offer_selection.py) | +| Contention, fallback boundary and single payment | [wallet_paymaster_failover.py](../test/functional/wallet_paymaster_failover.py) | +| Confirmation, finance and pool rollback | [wallet_paymaster_reorg.py](../test/functional/wallet_paymaster_reorg.py) | +| Activation, index and transport prerequisites | [wallet_paymaster_readiness.py](../test/functional/wallet_paymaster_readiness.py) | +| RPC authentication/whitelists, wallet isolation, abrupt termination, older authorized-backup rejection, liquidity and restart | [wallet_paymaster_lifecycle.py](../test/functional/wallet_paymaster_lifecycle.py), [wallet_paymaster_rpc.py](../test/functional/wallet_paymaster_rpc.py) | +| P2P and old-version compatibility | [p2p_paymaster.py](../test/functional/p2p_paymaster.py), [p2p_paymaster_v9_26_5_bridge.py](../test/functional/p2p_paymaster_v9_26_5_bridge.py), [wallet_v9_26_5_compatibility.py](../test/functional/wallet_v9_26_5_compatibility.py), [wallet_v9_26_5_inplace_upgrade.py](../test/functional/wallet_v9_26_5_inplace_upgrade.py) | +| Qt authorization and displayed state | [paymasterwidgettests.cpp](../src/qt/test/paymasterwidgettests.cpp); DD boundary cases remain in [digidollarwidgettests.cpp](../src/qt/test/digidollarwidgettests.cpp) | +| All `paymaster_*` suites under headless sanitizers and four bounded fuzz targets | [paymaster-security.yml](../.github/workflows/paymaster-security.yml) | + +After building the selected revision with wallet support using the repository's +[platform instructions](README.md#building), a focused entry point is: + +```sh +./src/test/test_digibyte --run_test=paymaster_types_tests,paymaster_txbuilder_tests,paymaster_psbt_tests --log_level=error --report_level=short +python3 test/functional/test_runner.py --jobs=1 wallet_paymaster_readiness.py wallet_paymaster_provider.py wallet_paymaster_offer_selection.py wallet_paymaster_failover.py wallet_paymaster_reorg.py +``` + +These commands assume a configured Unix build with binaries in the normal +build paths and the functional runner's generated configuration available. +Use the repository's [functional-test instructions](../test/functional/README.md) +for another build layout. They are review commands, not a claim that they were +run for this documentation update. Full builds, regression suites, fuzzing and +external compatibility environments belong to the agreed release matrix. + +Remaining acceptance work is to bind results to an exact release revision and +binaries, rerun affected surfaces after later code changes, complete real-Tor +deployment testing, and obtain or explicitly disposition independent review. +The [release gate](digidollar-paymaster-release-gate.md) is the status record. + +## Finite automatic pool setup (integration branch update) + +The original direct `preparepaymasterpool` commit sequence is replaced by a +finite command in the existing maintenance ledger. Both planned output sets +are persisted before signing/broadcast, and reconciliation reuses wallet-saved +transactions instead of constructing duplicates. Setup funding uses confirmed +parents and does not alter the DigiByte fee precheck or Dandelion implementation. +The existing wallet tick continues approved work even with provider autostart +disabled. Recurring maintenance consent remains separate. See the +[operator and persistence reference](digidollar-paymaster-pool-setup.md) for the +new optional fields, explicit legacy adoption and verification commands. +Executable regression results remain pending a fresh operator build. + +### Workflow edge-case corrections (2026-09-20) + +AUTO dispatch preserves the database read status of live sessions and retained +idempotency tombstones. Only `NOT_FOUND` permits the ordinary-funding fallback; +an unreadable or unsupported existing record fails before coin selection. + +Finite setup accepts durable approval while provider operation is disabled, +then waits for enablement before execution. Qt treats accepted/deferred work as +pending and requires the new acceptance and fee-limit fields. Recurring DGB +replenishment, like initial setup, requires confirmed funding. Failed signed +setup transactions remain observable after conflicts and can rejoin their exact +pool outputs after reconfirmation, without signing a replacement. + +Pool retirement stores `paymaster_retirement_provider` with each signed wallet +transaction in `CommitTransaction`, before broadcast. Finance reconciliation +reuses the existing transaction-cost reconstruction for these provider-bound +markers and idempotent event IDs. A crash, a later asset-step failure or a failed +finance write cannot erase this reconstruction source. No separate retirement +database or binary record layout is introduced. DD retirement uses the existing +plan/build API and checks the exact fee before commit; rejected saved bytes are +retained. Old unmarked retirements with missing events cannot be inferred safely +from arbitrary wallet sends and are not claimed as repaired history. +The marker remains internal: the common wallet transaction JSON formatter omits +it from `gettransaction`, `listtransactions` and `listsinceblock`, preserving +the existing public response schemas. Before retirement execution, reconcile +the maintenance journal so already-confirmed setup steps do not incorrectly +block retirement as pending; genuinely pending setup still blocks it. + + +Regression details and runtime acceptance are tracked in +[the edge-case review](digidollar-paymaster-edge-case-review.md#local-corrections-and-verification). diff --git a/doc/digidollar-paymaster-pool-setup.md b/doc/digidollar-paymaster-pool-setup.md new file mode 100644 index 00000000000..98ed64b1222 --- /dev/null +++ b/doc/digidollar-paymaster-pool-setup.md @@ -0,0 +1,197 @@ +# Automatic finite Paymaster pool setup + +`preparepaymasterpool` authorizes a finite setup, independently of recurring +liquidity maintenance and provider service startup. Its default call remains a +read-only preview. Execution persists the exact output scripts before either +transaction is created. The existing wallet stores signed transactions; the +existing maintenance ledger stores their pool purpose, fee ceiling, request +binding, policy/identity/network binding, and progress. + +## Preview, authorize, observe + +Example options for USER_PAID (amounts in integer satoshis or DD cents): + +```json +{ + "admission_dgb_slots": 3, + "operational_dgb_slots": 1, + "admission_carrier_slots": 3, + "operational_carrier_slots": 1, + "maximum_fee_satoshis": 20000000 +} +``` + +1. Call `preparepaymasterpool` with these options and inspect `plan_id`, pool + principal, `maximum_fee_satoshis` and `maximum_total_fee_satoshis`. +2. Send the same options with `execute=true` and the returned `plan_id`. +3. Inspect `getpaymasterpoolinfo.preparation`. `accepted=true` acknowledges a + durable command; `executed=true` only reports a transaction committed during + that particular call. Acceptance alone is not evidence of a finished pool. + +The default fee ceiling is **0.2 DGB per setup transaction**. A new command +creates at most one DD-carrier transaction and one DGB transaction, so its +maximum total fee is the displayed ceiling times the number of missing asset +classes. A retry cannot increase this ceiling, change targets, or replace a +saved transaction. Changed inventory produces a new preview identity. + +An explicit execute call may save the reviewed approval while the provider +setting is disabled. It returns `accepted=true, executed=false` and reports +`PAYMASTER_PROVIDER_DISABLED`; it does not enable the provider or sign a new +transaction. This permits the Qt setup wizard to save funding approval before +applying the operator's chosen enabled/runtime settings. Both the wizard and +manual pool controls distinguish acceptance from execution and readiness. + +The existing 30-second wallet-maintenance tick continues the command when +Paymaster and the wallet's provider setting are enabled and wallet/txindex are +synchronized. It works with `running=false`, manual service mode, and +`autostart=false`. It does not start the provider or enable recurring maintenance. +New signatures require an unlocked wallet. Disabling the provider setting +pauses setup; ordinary wallet rebroadcast of already signed transactions remains +possible, as before. + +DGB setup coin selection requires confirmed inputs. DD setup also checks its +planned parents for confirmation. Therefore, a carrier transaction's stempool +change cannot immediately become the input of a DGB setup transaction whose +normal fee precheck cannot see that change. Other suitable confirmed wallet +coins may fund the DGB step. DD setup reuses the existing build-and-sign API, +checks the exact signed fee before commit, and retains rejected transactions +without invoking the ordinary DD send helper's automatic-abandon behavior. +Ordinary wallet coin-selection and Dandelion rules are unchanged. + +## Waiting, failures, and cancellation + +Each step reports its asset, operation ID, plan ID, state, last diagnostic, +fee ceiling, actual saved fee, and transaction ID when present. + +- `pending_creation`: no transaction has been saved for that step. A missing + balance, locked wallet, or fee exceeding the approved ceiling pauses work. +- `pending_confirmation`: a specific transaction is saved and tracked. Recovery + reuses those exact bytes, including after an immediate broadcast rejection. +- `complete`: the transaction is confirmed and its pool outputs are reconciled. +- `conflict`: a saved transaction is abandoned or conflicted. No automatic + replacement is authorized; the other uncompleted steps of that plan pause. +- `cancelled`: an explicitly cancelled step had no saved transaction. + +Conflict and abandonment observations are not terminal journal states: every +later reconciliation checks the same saved transaction again. If it becomes +valid/confirmed after a reorg, its existing outputs are restored without a new +transaction. An output already spent by a successor is never made available +again. A still-conflicted or abandoned signed transaction continues to block +replacement setup; cancellation only removes genuinely unsigned work. + +Uncreated DGB steps expose eligible confirmed funding, eligible funding including +safe unconfirmed change, required pool principal, and +`funding_shortfall_upper_bound_satoshis`. The shortfall includes the full fee +ceiling, so it is a conservative estimate rather than an exact funding demand. +Unconfirmed change is counted separately from immediately usable confirmed coins. +Additional funding can resume the original command without creating more slots +than approved. + +If an execute response is lost, repeat the preview with the unchanged options. +An outstanding matching approval keeps its original plan ID and fee total; use +that ID for the retry. This does not grant a new approval or create another set +of slots. A changed request or provider policy cannot inherit the old approval. + +A provider-policy change pauses execution with `PAYMASTER_POOL_POLICY_CHANGED`. +Unknown versions, unreadable records, ambiguous transaction matches and invalid +fees fail closed. No signature or reservation is discarded on a timeout. + +To revoke unexecuted steps, pass the target options, the **original** `plan_id`, +`execute=true`, and `cancel=true`. Recovery first checks for wallet-saved +transactions; only genuinely unsigned steps become `cancelled`. Already saved +transactions remain tracked and may still be broadcast. The response reports +whether any unsigned step was cancelled. To change a fee ceiling or targets, +cancel eligible steps, obtain a fresh preview and explicitly authorize it. + +Recurring automatic replenishment uses its existing separate consent and rolling +budgets. DGB replenishment also selects confirmed funding, so the same stem-only +parent cannot cause a signed child to fail the normal mempool fee precheck. It cannot construct replacement liquidity while finite setup is pending; +existing payment submissions can still complete. Pool retirement is rejected +while finite setup is pending; cancel eligible unsigned work before changing the +pool in a contradictory direction. + +## Explicit recovery of a transaction created by the older implementation + +Do not run another plain setup for the reported legacy partial failure. The +old transaction may still be valid even when it is absent from the mempool. +Do not abandon it or modify the wallet database to force another attempt. + +After validating the updated build on isolated regtest, inspect the transaction +and current pool with the same wallet and data directory used for setup. The +operator must supply: + +- `recover_dgb_txid`: the existing **wallet** transaction ID; +- `recover_dgb_outputs`: every intended DGB pool output as + `{"vout": , "purpose": "admission" | "operational"}`; +- the original desired pool totals and a sufficient finite fee ceiling. + +Do not guess indices from ordering. Decode the wallet transaction and identify +its exact amounts and scripts. The remaining ordinary change is not a pool slot. +First preview these options, then execute the identical options with the returned +plan ID. The preview does not register or broadcast anything. + +Adoption requires owned inputs and outputs, no Paymaster input reservation or +pool-input consumption, exact missing-slot values and counts, unspent mapped +outputs, no abandonment/conflict, and an actual fee within the ceiling. It +binds the transaction's witness hash and exact mapping to the preview. No +replacement transaction is constructed. Registration is recovered before an +attempt to broadcast the already saved transaction; outputs remain unavailable +for provider service until confirmed. + +For the 2026-09-20 incident, the candidate is +`b9c9b9ca0e18a8a8b9e59edb8e93b1cbaf3a99f4f532be31d3fea2ef064b7f30`. +The already registered DD carriers must remain in the pool. Their confirmed +change is sufficient according to the supplied diagnostic snapshot, but current +transaction, spend, and registration state must be checked again by the operator. +No command in development has been run against that existing testnet node. + +## Persistence and verification + +The wallet maintenance record advances from V3 to V4. V3 records remain readable +without gaining setup authority. V4 adds two setup kinds plus authorization, +request binding and diagnostic fields. Older binaries cannot interpret V4 +records; do not downgrade a wallet after this build writes such records. There is no second +journal, transaction-byte copy, service thread or RPC endpoint. + +Run from the root of an already configured Linux checkout containing these +changes, with wallet, SQLite and tests enabled. The prepared WSL runner still +pins commit `354b711f92`; that older snapshot does **not** contain this fix. +After these changes are committed, update its pinned revision before using it +for this acceptance run. Do not run stale binaries against the new tests. + +Build first (typically tens of minutes; the focused tests take minutes): + +```bash +make -j2 -C src digibyted digibyte-cli test/test_digibyte +``` + +Focused tests are registered in the existing suites. Require exit code zero and +no unexpected skips for each command: + +```bash +./src/test/test_digibyte --run_test=paymaster_provider_tests,paymaster_wallet_identity_tests --log_level=test_suite --report_level=short +python3 test/functional/test_runner.py wallet_paymaster_pool_setup.py wallet_paymaster_provider.py wallet_paymaster_rpc.py wallet_paymaster_lifecycle.py wallet_paymaster_failover.py wallet_paymaster_offer_selection.py -j1 +``` + +Use freshly rebuilt binaries. The functional regression intentionally enables +Dandelion and uses isolated regtest wallets and an inert stem-capable peer. It +covers a single DGB funding input, deferred child creation, restart, identical +retry, changed request rejection, missing funding, disabling/re-enabling setup, +wallet locking, fee limits, unsigned cancellation, and explicit legacy adoption. +Existing funded fixtures share a bounded confirmation helper so the ordinary +provider, offer-selection, failover and legacy-bridge tests also allow a second +setup transaction after confirmation of its parent. The optional bridge test +requires its separately documented v9.26.5 binary. +Unit coverage checks V3/V4 compatibility, authorization, wrong assets, amount +bounds, write failure, transaction abort and unknown record versions. + +Full builds and these executable tests are operator verification steps. Static +syntax checks alone do not establish that the runtime matrix passed. + +Local verification on 2026-09-20: targeted MSVC syntax checks passed for the +changed C++ translation units and the wallet database codec; the Qt check used +the installed compatible MSVC 14.43 toolset. Python AST checks passed for all +seven affected scripts, the new test imports through `--help`, and +`git diff --check` passed. No newly rebuilt executable tests have run. The +optional Python flake8 check was not run because that module is not installed +in the current Windows Python environment. diff --git a/doc/digidollar-paymaster-release-gate.md b/doc/digidollar-paymaster-release-gate.md new file mode 100644 index 00000000000..59adc91aeb6 --- /dev/null +++ b/doc/digidollar-paymaster-release-gate.md @@ -0,0 +1,345 @@ +# DigiDollar Paymaster V1 release gate + +The v9.26.6rc2 integration, RPC migration and pending build/runtime gates are +recorded in the [integration notes](digidollar-paymaster-v9.26.6rc2-integration.md). + +**Status review:** 2026-09-11, source commit `bd270044c1` on +`feature/digidollar-paymaster-v1`. See the [developer starting point](../PAYMASTER.md) +and [implementation reference](digidollar-paymaster-implementation.md). + +This matrix maps the 28 acceptance criteria in +[`DIGIDOLLAR_PAYMASTER_NETWORK_PROPOSAL_EN.md`](../DIGIDOLLAR_PAYMASTER_NETWORK_PROPOSAL_EN.md) +and the post-proposal adversarial-counterparty hardening to their verification +surfaces. It is an audit aid, not a substitute for a clean release build and +test run. + +> **Current gate: RELEASE ACCEPTANCE OPEN.** The implementation is present in +> the inspected branch. The recorded broad local matrix belongs to the +> 2026-07-30 candidate; additional functional and official-v9.26.5 compatibility +> results were recorded on 2026-08-08 and 2026-08-09. Later source changes mean +> those results must not be presented as a fresh complete pass of `bd270044c1`. +> A release revision, affected-surface reruns, real-Tor deployment testing and +> independent review still need to be resolved. The 2026-09-11 documentation +> update does not run or renew the executable test matrix. + +Status meanings: + +- **Covered**: implementation and a focused automated test exist; this does not + imply that the test passed on the current source tree. +- **Candidate pass**: historical candidate evidence; the listed in-tree acceptance checks passed on the dated + working-tree candidate; this does not by itself satisfy an unlisted release + gate. +- **Partial**: important coverage exists, but the listed test gap remains. +- **External**: verification requires binaries or environments outside one + in-tree test process. + +The last column records the gate originally associated with each criterion. +The local MSVC, functional, broad-regression, Qt, and same-revision WSL reruns +covered by the 2026-07-30 matrix are satisfied for that working-tree candidate. +Real-Tor deployment and independent-review requirements remain open. Official +v9.26.5/current wallet-file, mixed-node relay, old-only bridge, and unchanged- +datadir upgrade compatibility have dated passing evidence from 2026-08-08/09. The historical +snapshot records only superseded pre-hardening states. + +| # | Coverage status | Coverage or historical evidence | Original/remaining release gate | +|---:|---|---|---| +| 1 | Covered | `wallet_paymaster_provider.py` completes a USER_PAID transfer from a client with no DGB. | Final functional rerun. | +| 2 | Covered | Paymaster PSBT and wallet tests verify role-scoped signing and never transfer wallet secrets. | Final focused unit run. | +| 3 | Covered | Provider/directory tests verify disjoint admission proofs; the provider functional test exercises separate admission and operational pools. | Final focused unit and functional run. | +| 4 | Covered | Fee-boundary and transaction-builder tests cover carrier fees from 0.01 through 0.99 DD. | Final focused unit run. | +| 5 | Covered | Client selection tests use exact rounded totals and deterministic local sorting; `wallet_paymaster_offer_selection.py` additionally exposes differently priced offers from two independent provider daemons, applies monotonic repricing, expires stale announcements, and accepts a fresh replacement. | Final focused unit run and the registered multi-provider functional run. | +| 6 | Covered | PSBT tests prove the user and provider sign only their own inputs. | Final focused unit run. | +| 7 | Covered | Template-binding tests reject mutations and require a newly verified attempt for provider-dependent fallback changes. | Final focused unit run. | +| 8 | Covered | Persistent request IDs, monotonic attempt state, same-input recovery, and duplicate-payment guards are tested. `wallet_paymaster_failover.py` additionally proves the exact reserved DD input set survives a real provider change and produces one payment. | Crash-specific variants are tracked under 22 and 23. | +| 9 | Covered | A three-node functional rerun on 2026-08-08 used the official v9.26.5 daemon: the old node accepted the Paymaster-created transaction in its mempool and validated it in the same block/tip as the upgraded nodes. The unchanged-datadir upgrade and old-only bridge scenarios also passed against that official daemon on 2026-08-09. | Satisfied by the official v9.26.5/current functional reruns. | +| 10 | Covered | Paymaster transaction/protocol rules are wallet/P2P policy. The broader branch also changes DCA, ERR and volatility source to use portable 128-bit types; those changes require separate arithmetic-equivalence review. | Final diff review, cross-platform arithmetic checks and broad consensus regression run. | +| 11 | Covered | Reputation tests cover local-only storage, neutral failures, cooldown, and security-first eligibility. | Final focused unit run. | +| 12 | Covered | Headless selection, fee caps, durable sessions, and request-id idempotency are covered by client/store and provider functional tests. | Final focused and functional run. | +| 13 | Covered | Legacy `senddigidollar` without options remains on the wallet-funded path and has regression coverage. | Final broad wallet regression run. | +| 14 | Covered | The provider functional test completes public and restricted zero-service-fee sponsorship with the common protocol. | Final functional rerun. | +| 15 | Covered | Capability tests bind restricted authorization to the payment, store only its hash, reject reuse, and keep it out of gossip. | Final focused and functional run. | +| 16 | Covered | Builder tests require fresh internal DD/DGB change; the provider functional test scans production logs for payment artifacts and secrets. | Final functional rerun with `debug=net,rpc`. | +| 17 | Covered | Readiness tests enforce onion-only high privacy, Tor isolation, one attempt, and no v1/clearnet fallback. | Perform a deployment smoke test with a real Tor proxy/onion service. | +| 18 | Covered | `wallet_paymaster_offer_selection.py` proves automatic cheapest-provider selection and no mutation at the unselected provider. `wallet_paymaster_failover.py` adds two-client contention for one cheap operational slot and sequential completion through the standby provider. | Repeat both registered multi-provider functional tests on the release build. | +| 19 | Covered | Store tests cover retention redaction, tombstones, idempotency, mempool observation, confirmation, and reorg handling. `wallet_paymaster_reorg.py` adds a real longer-chain rollback/reconfirmation of client state, provider finance, and successor liquidity. | Final focused unit and registered reorg functional run. | +| 20 | Covered | Documentation states the pseudonymity boundary; the Qt privacy selector carries the same warning, and its assertion passed with Qt 5.15.10. | Final broad Qt regression run. | +| 21 | Covered | Recovery tests cover recheck, exact retry, same-input fallback/self-return, durable locks, and confirmation-dependent cancellation finality. The functional failover test pins both safe pre-signature fallback and rejection after a user PSBT exists. | Crash-specific variants remain under 22 and 23. | +| 22 | Covered | Every provider-commit write and the commit operation have deterministic rollback injection; a reopened post-commit snapshot contains the exact raw transaction, bindings, pool state, sponsorship state, and `PENDING_NETWORK` subphase used by retry/broadcast recovery. | Final focused and functional rerun. | +| 23 | Covered | Every user-authorization write and the commit operation have deterministic rollback injection; a reopened successful snapshot contains the exact persisted user-signed PSBT hash, attempt, authorization, and `USER_SIGNATURE_SENT` subphase without another variant. | Final focused and functional rerun. | +| 24 | Covered | Request parsing, analysis, builder, and boundary tests enforce one recipient and the 100–10,000,000-cent bounds on every DD output. The `paymaster_wire_envelopes` target completed 100,000 ASan/UBSan/libFuzzer runs after its timestamp-overflow finding was fixed and replayed successfully. | Final focused unit run. | +| 25 | Covered | Readiness tests enforce unpruned, txindex-complete client/provider operation. The official-v9.26.5 leaf scenario proves old-node validation of the ordinary final transaction. On 2026-08-09 `p2p_paymaster_v9_26_5_bridge.py` additionally passed ordinary DGB/DD relay across an old-only bridge while Paymaster discovery failed closed until a direct current link existed. | Satisfied by the official v9.26.5 leaf and bridge runs. | +| 26 | Covered | Signed monotonic `PMRESULT` and one commit key/raw transaction are tested; deterministic failure at each atomic provider-commit write and at commit proves complete rollback of the commit, sponsorship, pool, attempt, and session records. | Final focused unit run. | +| 27 | Covered | Store/RPC tests distinguish mempool from confirmation and expose the shared state/finality fields; Qt renders those fields. | Final RPC and Qt regression run. | +| 28 | Covered | Locked-wallet tests pause the same session without fallback, and provider policy requires a positive absolute DGB network-fee cap. | Final focused and functional run. | + +## Revision and evidence requirements + +Before a release decision, attach the exact source commit, build configuration, +binary identification, command, exit code, date, and retained output for each +required surface. A test file or workflow being present is coverage, not a +successful run. The records below retain earlier reported counts without +claiming a new independent reproduction. + +Later branch changes include current-only persistence handling, wallet metadata +documentation, RPC/store decomposition, compatibility extensions, and Qt +workflow hardening. Re-run the checks affected by those changes on the selected +release revision. Independent review and real-Tor testing remain explicit +external gates; documentation reconciliation does not close either one. + +## Adversarial-counterparty hardening gate + +These checks extend, rather than replace, the 28 proposal criteria. A release +must demonstrate the invariant that once an honest side signs, every movement +of its own DD or DGB exactly matches its locally persisted authorization +manifest. + +| ID | Current status | Implementation/coverage surface | Required acceptance | +|---|---|---|---| +| H1 | Candidate pass | Protocol V5 requires `PMCAPREQ`/`PMCAPRESP`; the client validates provider identity, BIP86 control proofs, creating transactions, live Chainstate resources, reference block, nonce, request/session binding, and expiry before disclosing intent or capability. Validated resources are persisted and bound to the later quote. | Malicious-provider tests for foreign/spent/double-promised resources, bad control proofs, stale references, quote/resource mismatch, reconnect, and explicit no-downgrade behavior. | +| H2 | Candidate pass | `ClientAuthorizationManifest` and `ProviderAuthorizationManifest` are built from local authority and revalidated by the shared core immediately before client/provider signature, retry, recovery, and broadcast. Unknown PSBT fields, roles, outputs, and non-default sighashes fail closed. | Mutation matrix for inputs, outputs, amounts, recipients, fees, change ownership, PSBT fields, roles, sighashes, policy, capacity, and template commitments through RPC, Qt, and background paths. | +| H3 | Candidate pass | Final processing reconstructs the trusted PSBT, checks raw/non-witness transaction, txid, wtxid, exact inputs/outputs/all witnesses, runs the script interpreter with actual prevouts, and performs a mempool preflight when needed. Final state is monotonic and result/attempt/session writes are atomic. The registered functional reorg rolls one real final transaction back and reconfirms the same txid without a duplicate finance event. | Invalid-witness, same-txid/different-witness, mempool-conflict, late-result, replay, reorg, restart, failure-injection, and immutable-final-state tests. | +| H4 | Candidate pass | `ProviderSafetyPolicy` and `ProviderBudgetReservation` apply finite per-model transaction/reserved/hour/day fee and completion limits plus active-quote and request limits. Quote reserve, final spend, and safe release are atomic; ledger and accounting-time high-water survive restart. The functional contention case holds the only cheap slot for one client while a second safely moves to the standby provider. | Last-budget/last-slot races, concurrent quotes, clock rollback, crash at every write, restart, expiry, public-sponsorship drain, and balance-oracle tests proving no overspend. | +| H5 | Candidate pass | `ClientSafetyPolicy` durably limits service fee per transfer and rolling day; the effective cap is the minimum of wallet policy, RPC limit, and signed offer. | Per-transfer/daily boundary, concurrent reservation, policy change, restart, exact retry, rejection without policy, and RPC/Qt parity tests. | +| H6 | Candidate pass | Semantic replay keys are independent of peer ID; exact retries are idempotent and conflicting content is rejected. The first valid signed Capacity/quote claim is staged evidence-only before ACK/deep validation, with persisted-authority failures kept distinct from invalid remote artifacts. Generation-bound RAII leases protect inbox ownership across the database boundary. Critical replay state persists. Token buckets cover peer/netgroup/provider/session, work is fairly queued, cheap announcement/rate checks precede crypto, TTL math saturates, and signed equivocation evidence locally blocks the provider. | Cross-peer/reconnect/restart replay, DB-write failure and retry, lease overlap/ABA, exact request/session binding, queue fairness, netgroup/announcement flood, expiry race, sequential and simultaneous conflicting Capacity/quote, persistent block, and timestamp/amount boundary fuzz tests. | +| H7 | Candidate pass | `resolvepaymastersession ... cancel_to_self` can use a distinct `USER_PAID` provider that passes Capacity V5. The recovery manifest permits only the original user inputs, fresh wallet-owned DD returns, a locally capped recovery fee, and recovery-provider DGB inputs. | End-to-end recovery without client DGB, same-provider rejection, privacy inheritance, malicious outputs/fees, ambiguous original broadcast race, restart, exact retry, and confirmation-dependent release tests. | +| H8 | Candidate pass | Stateful fuzz targets cover Capacity → Intent → Quote → Submit → Result → Recovery; focused wallet/P2P tests cover manifests, budgets, replay, and finality. On 2026-07-30 the exact synchronized candidate was rebuilt with Clang ASan/UBSan/libFuzzer, all saved artifacts/corpora replayed successfully, and both fresh targets completed 100,000 runs. | Repeat this matrix after any affected source, build, or dependency change. | + +## Recorded candidate verification (2026-07-30 through 2026-08-09) + +These are previously recorded results for the candidates and dates named below. +They do not certify the current branch head or a tagged release artifact: + +| Surface | Verified result | +|---|---| +| MSVC Debug | `test_digibyte`, `digibyted`, and `digibyte-cli` built successfully. The 16 focused Paymaster suites plus `walletload_tests` passed 196/196, and the complete suite passed 3,605/3,605 with exit code 0. The MSVC Debug CRT still emits its known process-shutdown allocation dump after the successful result; this remains diagnostic follow-up rather than a hidden test failure. | +| MSVC Release | `test_digibyte`, `digibyted`, `digibyte-cli`, `digibyte-qt`, and `test_digibyte-qt` built successfully. The focused Paymaster plus `walletload_tests` selection passed 196/196, and the complete core suite passed 3,605/3,605 with exit code 0. | +| Functional Debug | `wallet_paymaster_provider.py` and `wallet_paymaster_readiness.py` completed successfully against the Debug candidate. Explicit safe port seeds avoided locally reserved Windows TCP ranges. | +| Functional Release | `wallet_paymaster_provider.py` and `wallet_paymaster_readiness.py` completed successfully against the Release candidate. | +| Multi-provider selection (2026-08-08) | On 2026-08-08 the registered `wallet_paymaster_offer_selection.py --descriptors` scenario passed all 17 framework unit tests and its 1/1 functional test in 27 seconds. Two independent current-version provider daemons advertised 50-bps and 200-bps USER_PAID offers to a DGB-less client; the client ordered both exact totals, selected only the cheaper provider, completed and confirmed one transfer, and left the expensive provider's pool, safety budget, and finance ledger unchanged. | +| Extended multi-provider/reorg checks (2026-08-08) | On 2026-08-08 one registered local Windows `test_runner.py --jobs=1` invocation passed all 17 framework unit tests plus `wallet_paymaster_offer_selection.py --descriptors` in 21 seconds, `wallet_paymaster_failover.py --descriptors` in 47 seconds, and `wallet_paymaster_reorg.py --descriptors` in 9 seconds (77 seconds accumulated). The same registered group then passed under x86_64 WSL with all 17 framework tests and durations of 12, 24, and 3 seconds (39 seconds accumulated). The scenarios cover monotonic repricing and expiry/refresh; one-slot contention, same-input standby fallback, post-signature rejection, and exact single-provider accounting; plus finance/pool rollback and same-txid reconfirmation. | +| Official v9.26.5/current compatibility | On 2026-08-08 an x86_64 WSL build ran both registered `wallet_v9_26_5_compatibility.py` variants and the three-node `wallet_paymaster_provider.py --descriptors` scenario against the official v9.26.5 Linux daemon. Each invocation passed all 17 framework unit tests. The legacy and descriptor wallet variants passed in 36 accumulated seconds; the provider scenario passed in 27 seconds after its restart path was corrected to reconnect the third node. | +| New official-v9.26.5 scenarios | On 2026-08-09 `p2p_paymaster_v9_26_5_bridge.py --descriptors` passed against the official Linux daemon in 38 seconds after all 17 framework tests passed. It relayed and confirmed ordinary DGB and DD through the old-only bridge, proved through raw message capture that the old node received but did not relay `pmannounce`, failed Paymaster selection closed, and restored discovery over a direct current-current link. `wallet_v9_26_5_inplace_upgrade.py --descriptors` subsequently passed in 7 seconds after all 17 framework tests passed. It created encrypted v9.26.5 wallets, two active positions, and isolated pending DGB/DD transfers; loaded the exact datadir's blocks, Chainstate, txindex, and mempool with the current daemon while wallets were disabled; then loaded, reannounced, confirmed, spent, redeemed, and restarted the inherited wallet state. | +| Native Qt | Qt 5.15.10 was rebuilt and installed with the bundled-zlib namespace backport. Inspection found the three affected `z_crc32_combine_*` symbols prefixed and no conflicting unprefixed definitions in `Qt5Core.lib`; the Release Qt targets linked and all eight registered suites completed with exit code 0. | +| WSL2 sanitizers/fuzzing | The synchronized 2026-07-30 source candidate rebuilt successfully with Clang `-fsanitize=address,fuzzer,undefined`. One saved wire artifact plus the 710-file wire and 2,365-file stateful corpora replayed successfully. Fresh `paymaster_wire_envelopes` and `paymaster_stateful_security` campaigns each completed 100,000 executions with exit code 0. Wire added 289 units in 12 seconds at 373 MB peak RSS; stateful added 628 units in 2,004 seconds at 562 MB peak RSS. | + +Any source, build, or dependency change that can affect a verification surface +invalidates that surface's dated evidence and requires the affected checks to +be repeated. + +## Required closing sequence + +Steps 1, 2, 4, 5, 6, and 8 have dated candidate evidence above. Step 3 has +passing local and WSL evidence, but still requires the listed release-build +rerun. Steps 7 and 9 remain open. Completed steps must be repeated after any +affected source, build, or dependency change. + +1. **Dated candidate evidence — final review:** The final Paymaster security + diff was reviewed for consensus isolation, protocol V5/no-downgrade, + manifest call-site completeness, atomic wallet writes, finite limits, and + absence of raw IP/payment material in persistent rate/accounting keys. No + obvious release-blocking counterparty theft path was found; this does not + replace the independent review in step 9. +2. **Dated candidate evidence — MSVC:** Build `test_digibyte`, `digibyted`, and `digibyte-cli` with MSVC in Debug and + Release. Run the complete focused Paymaster unit/wallet suites in both + configurations, including malicious-counterparty, expiry, replay, + last-budget/slot, crash, restart, and final-witness regressions. +3. **Dated local evidence — functional:** Run + `wallet_paymaster_provider.py`, `wallet_paymaster_offer_selection.py`, + `wallet_paymaster_failover.py`, `wallet_paymaster_reorg.py`, and + `wallet_paymaster_readiness.py` with + candidate binaries. The provider scenario must cover USER_PAID, public and + restricted SPONSORED, finite budgets, drain-only completion, distinct-provider + no-client-DGB recovery, restart, exact retry, capability replay, and balance + invariants. The selection scenario must expose two differently priced offers + from independent provider daemons, choose the lowest exact total without a + provider override, and leave the unselected provider untouched. On + 2026-08-08 the newly registered descriptor selection scenario passed its + 17 framework unit tests and 1/1 functional test in 27 seconds. The extended + selection, failover/contention, and reorg scripts subsequently passed one + registered local current-build `test_runner.py` run including all 17 + framework unit tests; repeat it on the release build before closing this + step. +4. **Dated candidate evidence — official + v9.26.5/current compatibility:** On + 2026-08-08 both registered variants of + `wallet_v9_26_5_compatibility.py` passed against the official v9.26.5 Linux + daemon. The run covered raw legacy and descriptor wallet-directory loading + without an explicit upgrade, encryption and wallet state across restart, + post-load signing and spending, active DD records in the descriptor wallet, + and reopening in v9.26.5 after current-version use without Paymaster. Both + versions sent and received DGB directly, and the descriptor wallets sent and + received DD directly. The test also required cross-version address + validation, both-mempool relay with identical raw transaction bytes, + confirmation by the receiving version, and exact amount/direction history + after restart. The three-node `wallet_paymaster_provider.py --descriptors` + scenario also passed with the official v9.26.5 daemon: the Paymaster result + remained an ordinary `DD_TX_TRANSFER`, reached the old node's mempool with + identical raw bytes, was validated in the same block/tip, and no Paymaster + announcement was received or sent by the old node. + `p2p_paymaster_v9_26_5_bridge.py --descriptors` now covers the old-only P2P + bridge and passed against the official daemon on 2026-08-09. + `wallet_v9_26_5_inplace_upgrade.py --descriptors` adds the unchanged-datadir + pending-state/redeem path and also passed against the official daemon on + 2026-08-09. +5. **Dated candidate evidence — Qt 5.15.10:** Build and run the complete native Qt 5.15.10 suite, including client/provider + safety-policy RPC parity, authorization confirmation, warning persistence, + recovery, and authoritative finality rendering. +6. **Dated candidate evidence — WSL2:** The exact synchronized candidate was + rebuilt in WSL2/Ubuntu with Clang/libFuzzer plus ASan/UBSan. Saved artifacts + and both corpora replayed successfully; fresh `paymaster_wire_envelopes` and + stateful Capacity → Recovery campaigns each completed 100,000 runs with + clean exit codes. +7. **Open — real Tor deployment:** Exercise high-privacy mode against a real Tor proxy and onion service, + including stream isolation, `-logips=0`, capture rejection, one-attempt + enforcement, and no clearnet/v1 fallback. +8. **Dated candidate evidence — broad local regressions:** The complete MSVC + Debug and Release core suites each passed 3,605/3,605, and the complete + native Qt suite passed all eight registered suites. Platform- or + configuration-specific release matrices beyond these local surfaces remain + separate release-review responsibilities. +9. **Open — independent review:** Have the counterparty model, manifest and + budget invariants, and race/crash behavior assessed independently. Until + this is completed or explicitly dispositioned by release review, the + implementation must not be described as formally proven or independently + audited. + +## Historical verification snapshot (2026-07-28; superseded) + +The following results are retained for provenance only. They predate the +adversarial-counterparty hardening and **do not satisfy the current gate**: + +- MSVC Debug and Release core builds passed; the test project used `/bigobj` + for existing large DigiDollar test translation units. +- The then-current focused Paymaster unit set reported 75/75 passing in Debug + and Release. +- The then-current provider/readiness functional tests and frozen/current + mixed-version runs reported passing. +- The then-current native Qt 5.15.10 targeted and complete suites reported + passing exit codes. +- `paymaster_wire_envelopes` first found a signed timestamp overflow. After the + corresponding saturating-time fix, its saved artifact and a fresh 100,000-run + ASan/UBSan campaign reported success. +- Python syntax checks and `git diff --check` reported success. +- A broad Release run that excluded separately tracked BIP324 vectors was not + clean: it reported network-dependent oracle, DigiDollar performance, RBF, and + Windows command-text failures. That historical disposition remains context, + not a waiver for the new common-revision rerun. + +## Additional security boundary checks (2026-09-19) + +The integration working tree adds tests and CI coverage without changing the +production protocol, RPC permissions, wallet schema, or consensus. These results +do not close the overall release gate or the previously recorded unrelated and +unresolved integration failures. + +| Check | Local evidence | Limit | +|---|---|---| +| RPC access and wallet isolation | `wallet_paymaster_rpc.py --descriptors`: passed, 40 s. Missing/bad authentication returns HTTP 401; a read-only method whitelist rejects provider changes, signing and withdrawal with HTTP 403, including mixed batches. Request/session IDs cannot read or cancel records in another loaded wallet. | Existing node RPC authority is reused; RPC users are not per-wallet tenants. | +| Real database recovery | `wallet_paymaster_lifecycle.py --descriptors`: passed, 35 s. Abruptly terminates the test provider child process after durable quote/client authorization, resumes one payment, and restores an older authorized-client SQLite backup after confirmation. | The older backup safely rejects the spent Capacity inputs; it does not automatically reconstruct the missing provider result. No power-loss/disk-full or stale-provider-ledger guarantee is inferred. | +| Bounded V2 adversarial traffic | `p2p_paymaster.py`: passed, 14 s. Two real V2 daemons send 32 malformed announcements each across three connection rounds; invalid offers never enter the directory, ping and block relay remain functional. | Not a sustained CPU/RSS test or a test of expensive, well-formed invalid signatures. | +| Extracted wallet load suite | `paymaster_wallet_load_tests`: 7 cases, 172 assertions passed in the existing Windows unit binary. | This binary predates the latest RPC result-schema fixes; no fresh full-build or sanitizer pass is claimed. | +| Security CI | Selects every registered `paymaster_*` suite, including wallet load; adds the send-RPC/build/helper triggers and uploads the actual unit-test log. | Current-revision Linux ASan/UBSan/fuzz execution is still pending. Qt is excluded by the headless configuration. | + +The new functional scenarios ran with the local daemon built after the RPC schema +fixes. No production source or public interface was changed for these checks. + +### Focused operator rerun + +Prerequisites: freshly built daemon/CLI/unit binaries for the reviewed revision, +Python, and the existing descriptor-wallet/SQLite support. Working directory is +the repository root. Expect minutes for this focused selection, with exit code 0 +and all selected cases passing (no unexpected skips): + +```powershell +Set-Location 'D:\Digibyte\digibyte-fork' +$env:PYTHONUTF8 = '1' +python -u test/functional/test_runner.py wallet_paymaster_rpc.py wallet_paymaster_lifecycle.py p2p_paymaster.py -j1 +if ($LASTEXITCODE -ne 0) { throw "Paymaster boundary tests failed: $LASTEXITCODE" } +.\build_msvc\x64\Release\test_digibyte.exe '--run_test=paymaster_*' --report_level=short +if ($LASTEXITCODE -ne 0) { throw "Paymaster unit tests failed: $LASTEXITCODE" } +``` + +After the reviewed changes are committed and pushed, the operator can dispatch +the existing GitHub workflow from this directory with an authenticated GitHub +CLI. Verify that the selected run's head SHA is the intended revision. Expect +tens of minutes or longer; require both jobs to pass and retain the test log: + +```powershell +gh workflow run paymaster-security.yml --ref integration/paymaster-v9.26.6rc2 +gh run list --workflow paymaster-security.yml --branch integration/paymaster-v9.26.6rc2 --limit 5 +gh run watch --exit-status +``` + +The Qt and full-build commands remain in the [integration notes](digidollar-paymaster-v9.26.6rc2-integration.md#operator-build-and-runtime-gates). +A successful headless workflow does not replace those Qt checks. The local +Python runtime checks and `git diff --check` do not replace flake8 or actionlint; +those tools were unavailable in the inspected environment. + +### Remaining deployment and artifact evidence + +- Repeat real Tor/onion testing with the reviewed binaries: stream isolation, + proxy failure and authentication failure must retain the existing no-clearnet/ + no-V1 fallback and secret-redaction guarantees. Keep the independent review + and current-revision sanitizer/fuzz gates open until evidence is returned. +- In an isolated fault-injection environment, cover interruption during database + writes, actual storage exhaustion and older provider backups. Verify live + outpoints before use and reconcile post-backup sponsorship and budget records; + ordinary wallet rescanning alone is not evidence of ledger restoration. +- Extend the bounded network case with sustained, correctly encoded invalid + proofs while legitimate Paymaster payments and ordinary DGB sends run. Record + CPU/RSS, completion and reservation/budget counts. Unit queue/rate-limit tests + already cover deterministic limits; do not duplicate those algorithms in a + second production limiter. +- Verify Windows wallet/backup/diagnostic access controls with disposable wallet + data and a separate unprivileged OS account. Encrypted private keys do not + imply encrypted Paymaster metadata. Test backup destinations and any actual + database sidecar files, not only the original wallet directory. +- Tie the release inventory to the exact binaries and link inputs. The existing + SPDX workflow exports GitHub's repository dependency graph; it is not proof + of the dependencies embedded in a Windows executable. Include static Qt, + bundled Qt third-party code, vcpkg packages and externally supplied libraries. + Reconcile MSBuild link records with versions/source hashes and the executable + hashes, then review vendor advisories for those exact versions. A DLL listing + alone omits static dependencies. The local vcpkg status file did not enumerate + every manifest dependency, so no complete artifact inventory is claimed. + +From a Visual Studio developer PowerShell in the repository root, these read-only +commands collect starting evidence after the final build (seconds, except hashing +large artifacts). Keep compiler/link logs from that same build alongside it: + +```powershell +$env:QTBASEDIR = 'D:\Qt51510\install' +& "$env:QTBASEDIR\bin\qmake.exe" -query QT_VERSION +Get-Content build_msvc/vcpkg.json +Get-Content build_msvc/vcpkg_installed/vcpkg/status +Get-FileHash .\build_msvc\x64\Release\digibyte-qt.exe, .\src\digibyted.exe, .\src\digibyte-cli.exe -Algorithm SHA256 +dumpbin /dependents .\build_msvc\x64\Release\digibyte-qt.exe +``` + +AI-assisted audit input (web pages, peer-provided strings, logs and code comments) +is untrusted data, not permission to execute instructions or weaken checks. Use +only disposable regtest secrets in shared diagnostics; do not submit production +wallets, keys, capabilities or credentials to external audit services. This is a +development workflow rule, not a reason to add an LLM filter to the wallet. + +### Automatic setup follow-up (2026-09-20) + +The testnet partial setup exposed a missing Dandelion scenario: a carrier parent +was stem-only when the immediate DGB child's normal fee precheck ran. Finite +setup now journals both output plans, waits for confirmed funding, and reuses +saved transactions after restart or rejection. V4 journal validation and explicit +legacy adoption add verification obligations. The registered +`wallet_paymaster_pool_setup.py` regression and affected unit suites must pass +on freshly rebuilt binaries before live recovery. Prior green results do not +cover this change; runtime acceptance is pending operator execution. + +### Workflow edge-case review (2026-09-20) + +The [working-tree edge-case review](digidollar-paymaster-edge-case-review.md) +records five source-level findings and a cross-workflow regression matrix. +It includes AUTO session-read failure, the setup wizard/RPC contract, recurring +Dandelion funding, setup conflict recovery and retirement accounting. These +findings now have local code corrections and targeted regression sources, as +listed in the review's remediation section. Runtime validation on freshly built +binaries remains open. This does not renew prior runtime results or approve the +pending automatic-setup changes. diff --git a/doc/digidollar-paymaster-v9.26.6rc2-integration.md b/doc/digidollar-paymaster-v9.26.6rc2-integration.md new file mode 100644 index 00000000000..6ba13329ea4 --- /dev/null +++ b/doc/digidollar-paymaster-v9.26.6rc2-integration.md @@ -0,0 +1,378 @@ +# Paymaster integration with DigiByte v9.26.6rc2 + +## Scope and base + +This integration adapts Paymaster to upstream commit +`998140ec6ba951ceaea40b22ddcdc89e8fa7154e`, starting from Paymaster commit +`16da1f57f7d588fe571a10a6be4edf5316e8e250`. The local integration branch is +`integration/paymaster-v9.26.6rc2`. + +Upstream is the authority for consensus, Thaw Day, wallet state, amount parsing, +and existing RPC argument positions. This is source integration, not release +acceptance. Historical v9.26.5 compatibility results do not certify this revision. + +## Changes that keep the upstream surface small + +- The provider console and setup wizard have moved out of `digidollartab.cpp` + into `src/qt/paymasterwidget.cpp`. `paymasterwidget.h` exposes only the + embedding interface. The moved implementation retains its behavior; the + DigiDollar tab owns the widget through Qt parenting. +- Mint, redeem, positions and receive-request widgets use the release versions. + Paymaster does not require cosmetic changes to these widgets. +- DD ownership discovery and history rendering use the release implementations. + Paymaster-specific spendability, pool exclusions, idempotency and reservations + remain integrated with the wallet. +- `ExtractDDAmountFromTransaction` checks the supplied transaction's hash and + delegates to the unchanged upstream `ExtractDDAmountFromTxRef` parser. The + original consensus callers remain unchanged. +- Existing payment-data redactions remain; routine transfer diagnostics follow + upstream's `BCLog::DIGIDOLLAR` category. +- The mock wallet database retains upstream transaction and write callbacks, + with the Paymaster write/commit failure hooks needed for atomicity tests. +- Existing MSVC portability support remains necessary. Newly introduced uses of + native 128-bit arithmetic use `util::int128_t`, preserving expressions and + bounds. This is a platform adaptation, not a Paymaster consensus rule; the + existing cross-platform arithmetic review requirement remains open. +- MSVC project inputs are regenerated from Makefiles, including the separate + Paymaster panel and new upstream Qt test suites. Duplicate portability + includes in existing test sources were removed. + +## Paymaster separation after integration commit 89325d45b9 + +- `wallet/paymasterdb.cpp` contains the unchanged 1,780-line codec block and + the original Paymaster record keys. Only two previously file-local key names + needed matching `extern` declarations; key bytes and record formats are unchanged. +- `PaymasterSendWidget` is one concrete Qt child. It owns the existing fee + controls, session state, timers and test injection. The form retains editable + fields, validation and ordinary sending. Read-only snapshots and narrow form + presentation hooks avoid duplicated editable state. Generic dialogs/formatting, + wallet RPC execution and confirmation guards are reused. The existing + `DigiDollarSendWidget` translation context and control object names are preserved. +- `wallet/rpc/paymaster_send.cpp` contains free functions for options and fee + funding. It resumes durable sessions before balance/coin selection, then either + returns the existing Paymaster result or continues the ordinary transfer. + There is no new public RPC, session store, database transaction or migration. +- 31 of the 133 Qt test methods moved to `PaymasterWidgetTests`; the other 102 + remain in `DigiDollarWidgetTests`. The shared wallet fixture has one definition + in `digidollartestutil.cpp/h`. Seven Paymaster maintenance tests moved unchanged + to `paymaster_wallet_load_tests`; ordinary wallet-load tests stay in place. +- The existing `init.cpp` LF conversion is outside this refactor. Against the + release it accounts for a 2,419/2,411 added/deleted-line diff; ignoring line-end + whitespace leaves eight additions. No further normalization or history rewrite + is performed here. + +The refactor follows the repository/`src` agent guidance, `CLAUDE.md`, the +DigiDollar architecture and maps, the refactoring rules in `CONTRIBUTING.md`, +`doc/developer-notes.md`, the test guides and MSVC build instructions. Mechanical +moves, interface rebinding and documentation are evaluated separately. Full +build/link and runtime acceptance remain subject to the operator gates below. + +## RPC migration + +The signature is: + +```text +senddigidollar address amount [comment] [fee_rate] [selected_inputs] [amount_unit] [options] +``` + +Upstream owns argument six, `amount_unit`. Paymaster options now occupy argument +seven. A positional caller from the previous Paymaster development branch must +insert `"cents"` before its options object. Named `options` calls remain valid. +Fee caps and persisted payment amounts remain integer cents even when the RPC +request explicitly uses dollars. Qt and functional Paymaster callers have been +updated. The RPC contract test now checks that a dollars retry resumes the same +session as its equivalent cents request. + +## Verification performed + +- All 16 conflicted paths resolved; no unmerged index entries remain. +- Targeted MSVC `/Zs` checks cover the panel, tab, send widget, Qt widget tests, + Qt test runner, wallet, RPC, shared mock database, transaction builder, + validation adapter, updated arithmetic and affected consensus tests. +- Functional Python files parse successfully. +- MSVC generation is idempotent and project XML parses. +- A mechanical comparison confirms that the moved panel implementation differs + only in its embedding interface and class naming. +- The integration diff relative to the release has no whitespace errors. + A check relative to the older Paymaster HEAD also reports whitespace already + present in imported upstream files; those files were not reformatted. + +Syntax checks do not link binaries or execute wallet, network or Qt behavior. +Existing executables have not been used as evidence for the updated sources. + +## Refactor verification and review size + +Local checks for this refactor: + +- MSVC v143 (14.43), C++20 `/Zs`, `/W3`, `/WX` and the repository's warning + exclusions passed for 12 changed C++ translation units and four generated + Qt metaobjects. These are compile checks, not a linked build or runtime test. +- Qt 5.15.10 MOC generated all four affected metaobjects. The MSVC generator + was idempotent and all 29 discovered project XML files parsed successfully. + New production/test sources and MOC entries are registered exactly once; + MSVC wallet tests are also covered by the existing `*_tests.cpp` project glob. +- The full 1,780-line database codec block, all 40 record keys and all seven + wallet maintenance test bodies matched their previous definitions. +- All 133 Qt test methods are present exactly once. Normalizing only the suite + name and concrete-child test access reproduces every original test body. +- Both extracted RPC blocks matched after parameter/indentation normalization; + their checks and order are unchanged. All 67 Paymaster/client-safety fields + moved to the concrete child; the original form retains only its child pointer. +- The Qt string-literal comparison found no removed or changed literals; only + the new child object name was added. Existing translation context and CSS + ancestor selectors remain applicable. This does not replace visual tests. + +Against the DigiByte release, the following original-file diffs shrink. Counts +are ordinary Git added/deleted lines, including comments and whitespace; moved +code still exists in the new files and is not counted as a reduction in total +implementation size. + +| Original file | Before refactor | After refactor | +|---|---:|---:| +| `src/qt/digidollarsendwidget.cpp` | +4577 / -271 | +306 / -284 | +| `src/wallet/walletdb.cpp` | +2440 / -643 | +443 / -466 | +| `src/rpc/digidollar.cpp` | +390 / -27 | +145 / -36 | +| `src/qt/test/digidollarwidgettests.cpp` | +6428 / -101 | +1453 / -111 | +| `src/wallet/test/walletload_tests.cpp` | +1177 / -2 | +7 / -2 | + +Across all affected production `.cpp`/`.h` files, including new files, physical +line count changes from 18,473 to 18,747 (+274). These additions establish the +headers, concrete-widget embedding, input snapshot and function boundaries; +there is no second implementation of the payment or persistence algorithms. +Affected test source/header files grow by 204 physical lines for separate test +classes, includes and the shared-fixture interface; no test body is duplicated. + +At the initial refactor handoff, linking and runtime gates were pending. The +operator subsequently built fresh MSVC Release binaries and supplied the runtime +results below. Full runtime acceptance remains **pending**. + +## Windows runtime follow-up (2026-09-19) + +- The operator's full unit run executed 3,973 cases: 3,968 passed, three passed + with warnings and two failed. `blockmap_tests/map_allocation_budget` also + fails in isolation; `oracle_bundle_manager_tests/broadcast_consensus_proposal_no_spam` + passes in isolation. These failures still require resolution or a documented + baseline comparison; the full unit gate is not passed. +- With the test runner's default `QT_QPA_PLATFORM=minimal`, both + `WalletTests::walletTests()` and + `DigiDollarWidgetTests::redeemWidgetLockedWalletClickRequestsUnlock()` crash + in `QMessageBox::showEvent()` with Windows exception `0xc0000005`. +- The installed Qt 5.15.10 source calls + `QGuiApplication::platformNativeInterface()->nativeResourceForWindow()` + without checking the interface pointer in `qt_getWindowsSystemMenu()`. + The minimal integration inherits the null native interface implementation. + Selecting the native Windows backend avoids this unsupported dialog path. +- Both individual cases complete with exit zero using + `QT_QPA_PLATFORM=windows`. `QT_FORCE_STDERR_LOGGING=1` keeps Qt messages and + QtTest text output in the console/log instead of the Windows debugger sink. + This changes the test environment only; it does not change payment code. +- The subsequent operator run completed all 12 Qt suites with the Windows + backend: 216 passed, three failed, zero skipped (including fixture and + data-driven entries), exit 3. There was no access-violation crash. +- Two test drivers used `QMessageBox::done(Yes)` for static warning helpers. + Those helpers return `standardButton(clickedButton())`, so the tests had not + actually confirmed expert setup or external backup acknowledgement. They now + click the real Yes button and retain the original state/result assertions. +- The mint persistence test used `MockableDatabase::m_pass=false`, which also + fails reads. The Paymaster reservation guard correctly stopped coin selection + before the intended write failure. The existing `m_refuse_write` hook now + refuses the mint owner-key record while leaving reads available. An added + assertion proves that the refusal was reached; the no-broadcast, empty-mempool + and absent-position assertions remain in place. Production reservation checks + and payment logic are unchanged. +- The Qt test executable was rebuilt and linked successfully. The three exact + cases (`digiDollarAmountLabelsUseCurrencyPrefix`, + `paymasterFinancesAndBackupWorkflow`, + `mintDoesNotSendWhenTheWalletCannotSaveIt`) each pass in isolation with exit + zero and no skips. These test-only follow-ups add 14 physical lines beyond + the extraction snapshot above. +- The operator's full Qt rerun on 2026-09-19 completed all 12 suites with exit + zero: 219 passed, zero failed, zero skipped and zero blacklisted (including + fixture and data-driven entries). This includes all 45 PaymasterWidgetTests + entries and all 110 DigiDollarWidgetTests entries. Evidence: + `digibyte-qt-fixed-20260919-171705.log`, ending in `All tests passed.`; + the operator also reported `Qt-Exitcode: 0`. The Windows Qt runtime gate is + passed for this build. PowerShell's `NativeCommandError` rendering of native + stderr did not indicate a failing Qt result in this run. +- Run Windows GUI tests in an interactive desktop session: dialogs can appear + briefly. Overall runtime acceptance remains pending because the known unit + failures and the functional-suite gate are still open. The successful Windows + Qt run does not establish cross-platform acceptance. + +## Extended functional run and failure analysis (2026-09-19) + +The operator's extended Windows run completed with **424 entries: 384 passed, +12 failed and 28 skipped**, in 14,812 seconds. The original log is +`%TEMP%/digibyte-functional-tests.log`; retained failed-test data is under +`%TEMP%/test_runner_â‚ŋ_🏃_20260919_173124`. This was a failed overall run. + +### Paymaster corrections + +The four failed Paymaster entries exposed RPC documentation/contract mismatches +and stale test fixtures. Corrections preserve RPC values, validation order, +reservation enforcement, transaction construction and persistent formats: + +- Document `privacy_profile` on `senddigidollar` replay, and `privacy_profile` + plus `to_address` on `requestpaymasterquote` replay. Finalized requests can + still carry their persisted authorization details. +- Share the full `SessionToJSON` result schema between `getdigidollarsendsession` + and `resolvepaymastersession`, including provider identity, privacy, offer, + policy and exact amount/fee fields. The previous duplicated schema omitted + the authorization fields once a session advanced beyond the unsigned state. +- Preserve explicit null `attempt`, `recovery`, `result_status` and + `result_sequence`. The upstream `RPCResult` has no nullable union type. Use + its existing per-field `skip_type_check` facility, as `getwalletinfo` does + for polymorphic `scanning`, only on those four fields. This relaxes help-schema + checking for those fields, not input validation or authorization checks. + Existing unsigned-session tests assert null results; the lifecycle test also + checks the populated attempt and signed-result types explicitly. +- Fund the RPC fixture with two confirmed 750-cent DD outputs instead of one + 1,500-cent output. Two simultaneous sessions require disjoint reservations; + the test now asserts that explicitly. Total funding remains unchanged. +- Check the authoritative action-set rejection after signing, including the + absence of `fallback` and an unchanged session after the rejected RPC. The + store's independent `PAYMASTER_FALLBACK_AUTHORIZATION_MAY_EXIST` guard and + its unit tests remain intact. +- Fix provider-pool expectations: only two 0.30 DGB slots meet the raised fee + ceiling, so a five-slot target needs three additional slots (0.90 DGB). + Rebalancing retains two spent 0.20 DGB entries: the later pool has 14 records, + of which 12 are available and two are spent. A side-effect-free preview does + not return a `pool` key; compare `getpaymasterpoolinfo` before/after instead. + +The changed RPC translation units (`rpc/digidollar.cpp`, +`wallet/rpc/paymaster_client.cpp`, `wallet/rpc/paymaster_discovery.cpp`) were +compiled with the installed MSVC Release configuration and the daemon relinked. +Full library archives were assembled from all project-listed existing object +files after the targeted compiles. Python syntax and `git diff --check` passed. +No full build or full test-suite rerun was performed for these corrections. +The GUI and unit-test executables still require the operator's normal rebuild +before claiming they contain the latest RPC descriptions. + +Focused reruns retain `rpcdoccheck=1` and normal test timeouts: + +| Test | Latest result | Evidence under `build_msvc/` | +| --- | --- | --- | +| `wallet_paymaster_lifecycle.py --descriptors` | Passed, 26 s; runner exit 0, including the additional signed-result type assertions | `paymaster-lifecycle-fixed.log` | +| `wallet_paymaster_rpc.py --descriptors` | Passed, 33 s | `paymaster-functional-fixes-third.log` | +| `wallet_paymaster_failover.py --descriptors` | Passed, 29 s | `paymaster-functional-fixes-third.log` | +| `wallet_paymaster_provider.py --descriptors` | Passed, 48 s; runner exit 0 | `paymaster-provider-fixed.log` | + +Earlier diagnostic runs in those logs contain failures preceding the final +corrections; they are not successful full-matrix results. + +The separate timing diagnostic passed both selected tests (runner exit 0): + +```powershell +Set-Location 'D:\Digibyte\digibyte-fork' +$env:PYTHONUTF8 = '1' +python -u test/functional/test_runner.py wallet_digidollar_rc33_regressions.py feature_config_args.py -j1 --timeout-factor=3 +$LASTEXITCODE +``` + +Evidence: `build_msvc/nonpaymaster-timeout-diagnostic.log`. This command changes +only test time budgets, not consensus, wallet checks or the default test setup. +The runner reported 273 seconds for both tests together. + +### Other failures and relationship to Paymaster + +Source comparisons use the exact upstream release commit +`998140ec6ba951ceaea40b22ddcdc89e8fa7154e`. An unchanged error path is evidence +about origin, not a substitute for running an upstream binary with the same +compiler, flags and machine load. + +| Failure | Evidence and attribution | +| --- | --- | +| `feature_filelock.py` | Test expects `DigiByte Core is probably already running`; binary emits `DigiByte is probably already running`. Test, lock-error expression and `CLIENT_NAME_RAW` match upstream. This is an upstream test/message mismatch, not a Paymaster change. | +| `rpc_blockchain.py`, both transport variants | Test removes undo data while the DigiDollar stats index still needs it. Node logs `CustomAppend: DigiDollar supply index needs retained block/undo data at height 127`, followed by fatal shutdown. Test, blockchain RPC and the failing index branch are upstream code; integration changes in the index only substitute portable 128-bit arithmetic elsewhere. No Paymaster request is involved in this failure. | +| `feature_dbcrash.py` | Startup exceeds 480 s while the legacy DigiDollar health reconstruction scans P2TR UTXOs and fetches transactions. The test and complete `ScanUTXOSet` implementation match upstream. The retained log shows scanning still progressing, not a Paymaster database failure. Exact comparative runtime remains unmeasured. | +| `digidollar_testnet26_oracle_roster_rpc.py` | Mining batches of 25 testnet26/easypow blocks exceed the 120 s RPC wait; the node continues mining to height 175. Test, mining RPC, PoW and chain parameters match upstream. Both the upstream and local MSVC Release configurations disable optimization. No direct Paymaster cause identified; performance attribution needs an equivalent baseline run. | +| `wallet_digidollar_rc33_regressions.py` | Original mint RPC exceeds 30 s while consolidating 2,000 inputs in two passes (about 31.2 s). It also performs Paymaster reservation reads; their performance contribution cannot be ruled out. With the same corrected daemon, unchanged test and `--timeout-factor=3`, the complete test passes in 145 s. This establishes timing sensitivity, not Paymaster-independent performance. | +| `feature_config_args.py` | Fixed-seed expectation races startup: the log context's two-second deadline begins before node startup, while `ThreadOpenConnections` first sleeps 500 ms. The failed node is stopped only about 173 ms after that thread starts. The test/helper and relevant seed-loop code match upstream. With the same daemon and unchanged test, `--timeout-factor=3` passes in 128 s; this is a timing-sensitive test assumption, with possible startup overhead not quantified. | +| `feature_pruning.py` | Node 2 remains at height 1319 while node 0 reaches 1553; logs reject low-work headers. The headers filter matches upstream. However, the integration merge added a direct-block-submission workaround to this test, and that workaround still does not complete the reorg. Treat this as an unresolved integration-test adaptation, not as a proven unrelated upstream failure or a proven Paymaster production regression. | +| Unit: `blockmap_tests/map_allocation_budget` | 5,244,112 bytes versus 5,505,040 gives about 95.26%, exceeding the 95% assertion. Test, block-index structures and memory estimator match upstream. This is reproducible in isolation and has no Paymaster-dependent input. | +| Unit: `oracle_bundle_manager_tests/broadcast_consensus_proposal_no_spam` | Fails in the full run but passes in isolation. The unchanged broadcast function retains a static 30-second throttle across `manager.Clear()`. This explains order/time sensitivity; no Paymaster-specific modification exists in the failing method or test body. A full upstream comparison has not been run. | + +Do not describe all remaining failures as unrelated to the integration. +In particular, the pruning-test adaptation and possible minting overhead remain +open. Expensive crash/pruning/testnet baseline builds and reruns are delegated +to the operator; none are marked passed on source inspection alone. + +For a focused continuation after rebuilding, the operator can run the remaining +long cases from this repository root (existing Python, daemon and CLI required; +expect tens of minutes to hours): + +```powershell +Set-Location 'D:\Digibyte\digibyte-fork' +$env:PYTHONUTF8 = '1' +python -u test/functional/test_runner.py --extended feature_pruning.py feature_dbcrash.py digidollar_testnet26_oracle_roster_rpc.py -j1 --timeout-factor=3 2>&1 | + Tee-Object -FilePath (Join-Path $env:TEMP 'digibyte-long-failure-diagnostics.log') +$diagnosticExit = $LASTEXITCODE +Write-Host "Diagnostic exit code: $diagnosticExit" +``` + +Success requires every selected entry to pass and exit code 0. A timeout-only +rerun does not establish causality. Compare against a separately built pinned +upstream daemon using the same compiler settings and equivalent test fixtures; +in particular, disclose the integration-only pruning workaround when comparing. +Do not reuse the old run's data directories for these fresh tests. + +The 28 original skips include unsupported Windows/POSIX/USDT or signet cases, +interface-address prerequisites and unavailable historical release binaries. +The Paymaster v9.26.5 bridge and v9.26.5 wallet compatibility/in-place-upgrade +checks lacked the required historical binary; they remain unverified. + +## Operator build and runtime gates + +Run from the repository root in a Visual Studio developer PowerShell with the +v143 x64 toolchain, Python, the existing static Qt 5.15.10 build and installed +vcpkg dependencies. Set `QTBASEDIR` to the static Qt installation and +`PAYMASTER_VCPKG_INSTALLED` to the directory containing `x64-windows-static`. +The full build and runtime matrix are intentionally delegated to the operator. + +The currently installed workspace dependencies are available at the following +paths; no package installation is required for this refactor: + +```powershell +Set-Location 'D:\Digibyte\digibyte-fork' +$env:QTBASEDIR = 'D:\Qt51510\install' +$env:PAYMASTER_VCPKG_INSTALLED = 'D:\Digibyte\digibyte-fork\build_msvc\vcpkg_installed\x64-windows-static\' +python build_msvc/msvc-autogen.py +& 'C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\MSBuild.exe' build_msvc/digibyte.sln -m:1 -verbosity:minimal -p:Configuration=Release -p:Platform=x64 -p:QtBaseDir="$env:QTBASEDIR" -p:VcpkgInstalledDir="$env:PAYMASTER_VCPKG_INSTALLED" -p:VcpkgManifestInstall=false +$LASTEXITCODE +``` + +Expect a full build to take tens of minutes or longer. Success means exit zero +and fresh daemon, CLI, unit-test and Qt-test binaries. Do not run the following +against binaries left over from the earlier branch. + +```powershell +.\build_msvc\x64\Release\test_digibyte.exe '--run_test=paymaster_*,walletload_tests,digidollar_amount_tests,digidollar_txbuilder_change_tests,digidollar_wallet_lock_safety_tests,digidollar_mint_cleanup_tests' --report_level=short +$LASTEXITCODE +$env:PYTHONUTF8 = '1' +python test/functional/test_runner.py -j1 wallet_paymaster_rpc.py wallet_paymaster_provider.py wallet_paymaster_failover.py wallet_paymaster_reorg.py wallet_paymaster_offer_selection.py digidollar_rpc_amount_units.py +$LASTEXITCODE +$env:QT_QPA_PLATFORM = 'windows' +$env:QT_FORCE_STDERR_LOGGING = '1' +Remove-Item Env:DIGIBYTE_QT_TEST_FUNCTION, Env:DIGIBYTE_QT_TEST_OUTPUT -ErrorAction SilentlyContinue +$env:DIGIBYTE_QT_TEST_SUITE = 'PaymasterWidgetTests,DigiDollarWidgetTests,DigiDollarMintRecordTests,DDTransactionRecordTests,DDTransactionTableTests,RPCNestedTests' +.\build_msvc\x64\Release\test_digibyte-qt.exe +$LASTEXITCODE +Remove-Item Env:DIGIBYTE_QT_TEST_SUITE +``` + +Expect minutes to tens of minutes for this focused runtime matrix. Success means +all selected tests pass without unexpected skips. Before release, also run the +broader consensus/wallet regression and Thaw Day matrix, lock-order/debug checks, +Linux arithmetic parity, and mixed-node interoperability with the official +v9.26.6rc2 daemon. Real Tor deployment and independent review remain in the +[Paymaster release gate](digidollar-paymaster-release-gate.md). + +The local integration commit records source reconciliation; the runtime gates +above remain incomplete and are required before release approval. +Follow CONTRIBUTING.md for upstream review and the signed merge workflow. The +two pre-existing untracked operator documents are not part of this integration. + +The subsequent finite automatic setup change is documented separately in +[automatic pool setup](digidollar-paymaster-pool-setup.md). It requires a fresh +build and the new Dandelion regression; the earlier integration test results +do not establish runtime acceptance for that change. diff --git a/doc/digidollar-paymaster.md b/doc/digidollar-paymaster.md new file mode 100644 index 00000000000..9a9c7a12a46 --- /dev/null +++ b/doc/digidollar-paymaster.md @@ -0,0 +1,950 @@ +# DigiDollar Paymaster Network + +The Paymaster Network lets a wallet pay confirmed DigiDollar (DD) while a +selected provider contributes the DGB inputs needed for the miner fee. The +result is an ordinary `DD_TX_TRANSFER`; Paymasters have no consensus privilege +and existing validators need no Paymaster configuration. + +**Documentation baseline:** source commit `bd270044c1` on +`feature/digidollar-paymaster-v1`, reviewed 2026-09-11. This is the operator and +RPC guide. Developers should start with [PAYMASTER.md](../PAYMASTER.md) and the +[implementation reference](digidollar-paymaster-implementation.md). + +The current design and 28 V1 acceptance criteria are in the +[design specification](../DIGIDOLLAR_PAYMASTER_NETWORK_PROPOSAL_EN.md). +The [implementation plan](../DIGIDOLLAR_PAYMASTER_PLAN.md) explains the work +packages; the [hardening plan](../DIGIDOLLAR_PAYMASTER_HARDENING_PLAN.md) maps +the counterparty protections. Verification evidence and outstanding release +acceptance are recorded in the +[release gate](digidollar-paymaster-release-gate.md). + +## Privacy and safety model + +Paymaster transport and data minimization improve privacy but do not guarantee +anonymity. The selected provider necessarily learns the final transaction and +the confirmed transaction remains publicly analyzable. Standard mode requires +BIP324 v2 without v1 fallback. High-privacy mode additionally requires an onion +provider and Tor stream isolation, allows one provider attempt, and has no +clearnet fallback. + +The wallet never sends a private key or seed. Protocol V5 treats the remote +counterparty as potentially malicious. Each side reconstructs the complete +transaction locally and signs only after a wallet-local authorization manifest +has been revalidated immediately before signing. The client signs only its DD +inputs; the provider signs only its pool inputs; both require +`SIGHASH_DEFAULT`. Unknown PSBT fields, roles, outputs, or sighash modes fail +closed. + +This protects an honest wallet from signing a different recipient, amount, +change output, fee, or input set merely because a modified counterparty asks it +to. It is not a formal proof and does not make the remote service trustworthy. +Public sponsorship is financially bounded but cannot be made Sybil-fair. A +provider can retain data it necessarily receives, and can later publish the +exact transaction that the client already authorized. Local wallet compromise, +malicious release artifacts, and key theft are outside this threat model. + +### Local metadata and retention + +Wallet encryption protects private-key use; it must not be interpreted as +encryption of every value in the wallet database. Paymaster restart and +recovery records can contain provider endpoints, canonical requests and +responses, authorization manifests, unsigned and signed PSBTs, final +transactions, timing information, and links between attempts. High-privacy +mode protects the network path and suppresses endpoint-bearing logs, but it +does not add field-level encryption to these durable wallet records. + +Treat the wallet file, full-wallet backups, storage snapshots, and diagnostic +archives as sensitive metadata even when private keys are encrypted. Do not +attach them to public issue reports. The secret restricted-sponsorship +capability is deliberately removed before a quote request is persisted; only +its cryptographic binding is retained. + +These records are retained while they can still be needed for exact retry, +budget accounting, conflict handling, or recovery. Once an observed final +transaction reaches the 240-block reorganization safety depth, reconciliation +replaces an eligible final session and its validated children with a compact +idempotency tombstone. The tombstone still retains the minimum fields needed +to prevent a conflicting reuse, including the request/session binding, final +state and transaction identifier, fee modes, and local payment-order flags. +Unconfirmed, ambiguous, conflicting, or unreadable records are not deleted +automatically. Stopping Paymaster, locking the wallet, or disabling the feature +does not erase durable records. Do not edit or delete Paymaster database keys +manually; there is intentionally no destructive reset RPC. + +An unreadable input-reservation record remains conservatively reserved. If the +provider-pool safety record cannot be decoded, automatic wallet coin selection +offers no inputs instead of treating an unknown pool as empty. Preserve the +wallet and diagnose or replace the development Paymaster data; do not work +around this condition by editing individual database keys. + +## Node requirements + +Discovery, relay, and client support default to `-paymaster=1`; use +`-paymaster=0` to disable all three. Starting a client session or provider also +requires: + +```ini +digidollar=1 +paymaster=1 +prune=0 +txindex=1 +v2transport=1 +``` + +The Paymaster P2P handshake, directory exchange, announcements, and direct +session messages all remain inactive until the shared DigiDollar activation +gate is active at the current chain tip. + +The wallet requires `txindex` to be fully synchronized before it creates a +quote or a new Paymaster signature. Discovery and relay remain available to +upgraded nodes that are not configured for client or provider operation. + +Automatic transfers require the exact Paymaster protocol V5 Capacity feature. +Peers that implement only an earlier Paymaster flow are excluded; there is no +silent downgrade. BIP324 v2 is still mandatory for direct transport, but is not +a substitute for the signed Provider Capacity proof. + +Provider configuration and runtime state are wallet scoped. The normal first +start remains an explicit operator action. After that start, the recommended +`automatic` mode services the bounded provider queues without an operator click +for every message. This does **not** authorize a client signature: the client +still confirms the exact provider, model, recipient amount, and service fee +before its wallet signs. Optional provider autostart is a separate opt-in and +defaults to off. A provider wallet must be a descriptor wallet with local +private keys; legacy, watch-only, and external-signer wallets are not eligible. +`-paymasterendpoint=` sets the announced endpoint. Loopback endpoints +are accepted only on regtest. + +## Client use + +Existing five-argument `senddigidollar` calls are unchanged and continue to use +the wallet's own DGB. Paymaster use is selected by the optional seventh `options` +object, after upstream `amount_unit` (`"cents"` or `"dollars"`). The older +Paymaster development branch used argument six for options; positional callers +must insert `"cents"` before that object when upgrading. Named `options` calls +remain unambiguous. Consult `help senddigidollar` for the exact argument schema used by the +running binary. + +Example options object: + +```json +{ + "fee_mode": "auto", + "request_id": "550e8400-e29b-41d4-a716-446655440101", + "maximum_paymaster_fee_cents": 5, + "privacy": "standard", + "selection": "lowest_total_cost" +} +``` + +Before the first Paymaster transfer, configure finite wallet-local service-fee +limits. The RPC limit in `senddigidollar` can lower, but never raise, these +wallet limits: + +```text +setpaymasterclientsafetypolicy { + "maximum_service_fee_per_transaction_cents": 100, + "maximum_service_fee_per_day_cents": 1000 +} +getpaymasterclientsafetystatus +``` + +Both limits must be positive. The effective per-transfer maximum is the minimum +of the wallet policy, the request's `maximum_paymaster_fee_cents`, and the +signed offer. Daily usage and active fee reservations are durable across +restart and use a monotonic accounting-time high-water mark. + +- `fee_mode=dgb` always uses direct DGB funding. +- `fee_mode=paymaster` requires a Paymaster. +- `fee_mode=auto` may fall back only when the direct funding preflight reports + insufficient DGB fee inputs. Wallet-lock and other errors do not trigger a + fallback. +- `request_id` is a canonical lowercase UUID and is the durable idempotency key. + Retrying the same request resumes the same session rather than creating a + second payment. +- `maximum_paymaster_fee_cents` is a hard service-fee cap. +- `privacy` is `standard` or `high`; `selection` is `lowest_total_cost` or + `privacy_weighted`. + +### Prepare and authorize a Paymaster payment + +For a Paymaster path, every `senddigidollar` call without an +`authorization_commitment` prepares the payment without creating a new user +signature. This remains true when `prepare_only` is omitted or false. A client +application must explicitly accept the exact returned commitment before Core +may sign. `prepare_only=true` and `authorization_commitment` are mutually +exclusive. + +After configuring client safety limits, prepare a 10.00-DD recipient payment +using an integer amount of 1,000 cents: + +```text +senddigidollar
1000 "" 0 null "cents" { + "fee_mode": "paymaster", + "request_id": "550e8400-e29b-41d4-a716-446655440120", + "maximum_paymaster_fee_cents": 100 +} +``` + +Advance or inspect the same session as indicated by Core until its exact +authorization is ready. Review the returned provider, funding model, +recipient amount, fee and maximum outflow. To approve, repeat the identical +request with the returned commitment: + +```text +senddigidollar 1000 "" 0 null "cents" { + "fee_mode": "paymaster", + "request_id": "550e8400-e29b-41d4-a716-446655440120", + "maximum_paymaster_fee_cents": 100, + "authorization_commitment": "" +} +``` + +The placeholders must be replaced by the original recipient and exact returned +commitment. A mismatch is rejected. A locked wallet may still require unlocking +the same session. Inspect `authorization_required`, `authorization_accepted`, +session state and allowed actions; a prepared or authorized request is not yet +a confirmed payment. The examples are RPC-console notation; JSON-RPC clients +should pass structured parameters and shell users must quote the JSON for +their shell. Integer `amount` values are cents; values containing a decimal +point are interpreted as DD dollars by this RPC. + +### Exact total outflow and emptying a DD wallet + +By default, `amount` is the amount delivered to the recipient and a user-paid +Paymaster service fee is additional. Set +`subtract_paymaster_fee_from_amount=true` to instead treat `amount` as the exact +maximum DD outflow. Core derives an exact recipient amount for each eligible +offer so that: + +```text +recipient amount + rounded Paymaster service fee = requested total outflow +``` + +For example, a total outflow of 5,000 cents with a 50-basis-point offer becomes +4,975 cents to the recipient plus a 25-cent service fee. A sponsored offer +delivers all 5,000 cents. The existing cent-rounding rule is unchanged; when no +exact split exists, Core returns `PAYMASTER_NO_EXACT_GROSS_OFFER` before it +reserves inputs or requests a signature. + +`send_all_spendable_dd=true` additionally binds the request to every confirmed, +ordinary spendable DD input in the wallet. It requires the subtract option, +cannot be combined with manually selected inputs, and excludes unconfirmed, +reserved, and provider-pool DD. If that spendable snapshot changes before the +reservation is committed, Core stops with `PAYMASTER_SWEEP_BALANCE_CHANGED`. + +Preparation example for an exact 50.00 DD wallet sweep; the same explicit +authorization step above is required before signing: + +```text +senddigidollar
5000 "" 0 null "cents" { + "fee_mode": "paymaster", + "request_id": "550e8400-e29b-41d4-a716-446655440110", + "maximum_paymaster_fee_cents": 100, + "subtract_paymaster_fee_from_amount": true, + "send_all_spendable_dd": true +} +``` + +`getpaymasteroffers 5000 +{"subtract_paymaster_fee_from_amount":true}` previews the exact recipient, +service-fee, and total values. With `fee_mode=auto`, successful direct DGB +funding still delivers the full entered amount; subtraction applies only if the +wallet actually falls back to a Paymaster. The lower-level +`requestpaymasterquote` continues to receive the already-derived recipient +amount. + +Useful client inspection and recovery RPCs: + +```text +listpaymasters +getpaymasteroffers +getdigidollarsendsession +listdigidollarsendsessions +resolvepaymastersession +getpaymasterreputation +clearpaymasterreputation +setpaymasterclientsafetypolicy +getpaymasterclientsafetystatus +``` + +`resolvepaymastersession` permits ordinary provider `fallback` only before a +user PSBT exists; `abandon_unsigned` additionally requires that no transaction +authorization can exist. Use the current Core-derived `allowed_actions`, not +the age of a quote, to determine which mutations are permitted. + +After the user signature, timeout never releases the reserved inputs. An +ambiguous session must be checked again, retried with its exact provider and +artifacts, or recovered with `cancel_to_self`. A cancellation is not final when +merely accepted to the mempool; it becomes final only after confirmation. + +## Capacity and signing firewalls + +Relayed admission reserves are checked against both the active UTXO view and +the current mempool; an outpoint already used by a mempool transaction is not +advertised as available capacity. Directory replies copy at most the requested +bounded subset and rotate their starting point so a directory larger than one +reply is not permanently truncated to the same providers. + +Before sending a payment intent, user DD outpoints, or a restricted capability, +the client sends `PMCAPREQ` and validates `PMCAPRESP`. The response binds the +genesis, provider and request IDs, client nonce, reference block, expiry, and +one exact operational resource set. The client verifies the provider identity +signature, every BIP86 control proof, creating transaction, chainstate outpoint, +value, and output script. The resulting `ValidatedCapacitySnapshot` is +persisted and the quote must use the same DGB and carrier outpoints. + +Conflicting signed Capacity proofs or quotes are retained as local +equivocation evidence and block that provider locally. A proof cannot be reused +for contradictory local sessions. Exact protocol retries are idempotent; +reusing the same semantic message identity with different content is rejected, +even when it arrives over another peer connection. + +The isolated channel is directional: the outbound client half sends only +Capacity, quote, submit, and recovery requests, while the accepted provider +half sends only their corresponding responses. Both send and receive paths +enforce that classification before a payload can enter the shared inbox. + +Before an inbox message is acknowledged or subjected to the deeper chainstate +or PSBT firewall, the first valid identity-signed Capacity or quote claim is +atomically staged in the exact attempt or recovery record as evidence only. A +claim candidate grants no spending authority. An exact retry remains +idempotent after expiry, while a different valid signed claim creates durable +equivocation evidence. Database and persisted-local-authority failures leave +the message unacknowledged; only an unambiguously invalid remote artifact is +discarded. + +The accepted quote produces a `ClientAuthorizationManifest` that binds the +original local request and its canonical hash, requested fee mode, privacy and +selection modes, provider, offer, policy, Capacity snapshot, exact DD inputs, +recipient, amount, wallet-owned change, service-fee ceiling, expiry, and +transaction template. The provider independently creates a +`ProviderAuthorizationManifest` binding the validated intent and input-control +proofs, its locally reserved pool outpoints, wallet-owned carrier/change +scripts, DD service fee, exact DGB network fee, safety reservation, and the same +template. Manifest V2 names the reservation's exact commit key and its maximum +reserved DGB network fee. The quote attempt and that ledger row are committed +atomically; a missing or different row is never reconstructed by a retry. A +new provider signature additionally requires the currently configured safety +policy hash. A later policy change cannot strand an already durable exact +commit, whose historical non-null policy hash and SPENT ledger binding remain +verifiable. RPC, Qt, background processing, retry, and recovery all call these +same core validators. + +Before a client records a terminal result, Core verifies the complete raw +transaction, non-witness transaction, txid, wtxid, every input/output and every +witness against the trusted PSBT template. All signatures are checked with the +actual prevouts by the script interpreter. If the transaction is not already +confirmed or in the mempool, a mempool preflight must succeed. An invalid +witness or conflict remains recoverable and cannot overwrite a previous +`FINAL_COMMITTED` state. Result, attempt, and session transitions are stored +atomically. + +### Qt client fee selection + +The **Send $DD** page defaults to **Pay with my DGB**. This safe default never +selects a Paymaster or adds a DD service fee. **Automatic** first performs the +ordinary DGB funding preflight and considers a Paymaster only when suitable DGB +fee inputs are insufficient; other errors never cause a fallback. **Use a +Paymaster** requires an eligible provider. + +The normal view explains who funds the DigiByte network fee and summarizes the +recipient amount and maximum wallet outflow. Paymaster offer inspection, +provider-attempt limits, privacy profile, and provider selection are collapsed +under **Advanced Paymaster settings**. Mouse-wheel input does not change those +spin boxes or combo boxes. User-paid offers charge the displayed DD service +fee, while sponsored offers charge no DD service fee. The exact provider, +funding model, recipient amount, fee, and authorization commitment are still +confirmed before signing. + +In Automatic and Paymaster modes, **Deduct the Paymaster fee from the entered +amount** makes the entered value the exact total outflow. **Empty wallet with +Paymaster** uses the confirmed ordinary spendable DD balance and enables that +exact mode. Qt separately displays total outflow, recipient amount, provider +fee, and remaining spendable balance; the option is off for every new transfer +and is absent from direct-DGB mode. + +Automatic and Paymaster modes require a positive wallet-local client safety +policy. Qt reads the existing `getpaymasterclientsafetystatus` result and can +configure the same policy directly from the send page; this does not expose or +weaken the backend limits. A running or ambiguous collaborative transfer is +shown separately from fee selection so retry, fallback, and same-input recovery +remain visible without presenting them as ordinary fee settings. + +Qt treats Core as authoritative for both the durable session snapshot and its +allowed recovery actions. It never treats the generic `final` field alone as a +successful payment, never combines a refreshed session with an older attempt or +artifact, and never resumes signing automatically after a wallet or process +restart. `listdigidollarsendsessions` supplies a bounded wallet-local inbox for +unfinished or recovery-relevant sessions; Qt refreshes the selected session +before enabling an action. + +## Provider setup + +In Qt, the provider-only **Paymaster Network** tab is hidden by default. Enable +**Show Paymaster operator controls** under **Settings → Options → Wallet → +Expert** to configure or operate a provider. This display preference does not +disable Paymaster client payments or discovery. + +On the first visit for a wallet, Qt asks the operator to choose **Guided setup** +or **Manual expert setup**. Configuration, Safety limits, and Liquidity remain +locked until that choice is completed. Activity and recovery remains available +so a local onboarding preference can never hide durable reservations or +recovery work. Existing provider identities, policies, pool entries, or runtime +state are detected and unlock the console automatically. The choice is stored +as a wallet-scoped Qt preference; Core remains authoritative for readiness. + +The guided setup uses a theme-controlled classic dialog on Windows. It covers +current prerequisites, explicit provider-wallet selection, public identity, +user-paid/public-sponsored/restricted service models, offer limits, a finite +safety profile, policy-aware liquidity targets, and a final review. A dedicated +provider wallet is recommended, but not required. Because the wizard belongs +to the currently selected wallet view, it never silently creates or switches a +wallet: choosing another wallet closes the assistant and directs the operator +to Core's normal wallet menu before reopening Paymaster Network. The selected +wallet name is pinned throughout the review and Core rechecks descriptor, +private-key, and external-signer eligibility immediately before the first +write. The assistant opens only after provider, safety, and liquidity status +have all been read successfully. On later runs it imports the authoritative +wallet-persisted values instead of replacing them with setup defaults; every +editable page also offers an explicit **Restore defaults** action. + +Applying the reviewed plan first places an existing enabled provider safely +offline. It then creates or reuses the identity, writes a temporary compatible +safety bridge when an operating-policy transition requires one, saves the new +operating and final safety policies, saves liquidity targets and finite +maintenance limits, and rechecks the pool. Only still-missing outputs are +created, and only after a second confirmation of the exact current funding +plan. Runtime mode, autostart, and the requested enabled state are persisted +last. Existing identity and pool outputs are retained. Completed write steps +remain durable and retryable after a later failure; closing before the final +Apply confirmation writes nothing. The completion page confirms that no +additional Save buttons are required. + +A fresh guided setup defaults to the recommended `automatic` operation mode, +`autostart=false`, and an enabled provider configuration. Reopening the +assistant preserves existing runtime, autostart, and enabled choices. Therefore +an explicitly selected enabled/autostart combination may start after setup when +all readiness gates pass; otherwise starting remains an explicit Overview +action. Disabling a provider configuration always prevents a saved autostart +preference from bringing it online. Paid automatic maintenance is separately +disclosed and approved because it may create DGB-fee transactions. The +assistant remains available from Overview for later review. +Manual queue processing and provider autostart are advanced operator settings. +Manual expert setup exposes all detailed controls after an explicit risk +warning. + +When the assistant creates a new provider identity, its completion page shows +the full provider ID and asks for a new full-wallet backup. The identity private +key, policies, pool records, durable sessions and provider finance ledger are +wallet metadata; a seed phrase or descriptor export alone is not a complete +Paymaster-provider recovery. The reminder is deliberately non-blocking, but it +remains visible on Overview and Finances until `backupwallet` succeeds or the +operator explicitly acknowledges an external full-wallet backup procedure. +Material offer, safety or liquidity-policy changes recommend another backup; +ordinary payments and finance bookings do not produce repeated reminders. + +### Provider runtime modes + +Provider runtime preferences are persisted in the provider wallet: + +- `operation_mode=automatic` is the default. Once the wallet-scoped provider + runtime has been started, Core periodically handles bounded Capacity, quote, + submitted-payment, and recovery work. Each scheduler pass performs at most + one request-processing step and one submit-processing step. It uses the same + validation, authorization manifests, finite budgets, replay protection, and + atomic wallet writes as the expert RPC path. +- `operation_mode=manual` is an expert mode. Core keeps the provider reachable, + but the operator explicitly invokes the processing RPCs. A mode change is + accepted only while the provider is stopped. The stopped-state check and + runtime-settings commit share the wallet-local provider-work guard with + start/autostart, so a concurrent start cannot overtake the mode change. +- `autostart=false` is the default for current-format provider settings. + Enabling autostart permits an already configured wallet to start its provider + runtime when readiness permits after load. Older settings records are rejected + rather than migrated or overwritten. No passphrase is stored. +- The low-level `stoppaymaster` RPC serializes with a provider start and returns + `PAYMASTER_PROVIDER_BUSY` instead of falsely reporting success while another + wallet-local provider operation owns the transition. It remains an ephemeral + stop when autostart is saved as enabled. +- The Qt **Stop** action is persistent: when autostart is enabled it first + saves `autostart=false` and only then stops the runtime. The separate + **Disable provider configuration** action persists `enabled=false` and also + stops the runtime, so neither scheduler polling nor a wallet reload can undo + the operator's intent. + +An encrypted, locked provider wallet does not consume queued work or create new +signatures. The automatic service reports `waiting_for_unlock` and continues +after a later wallet unlock. With autostart enabled, other failed readiness +gates report `waiting_for_readiness`; they do not authorize spending. +Drain-only operation continues only already durable signed or committed work +and does not create new Capacity proofs or quotes. + +The provider lifecycle is deliberately staged: + +1. Create or load an eligible descriptor wallet and unlock it. +2. Create its wallet-managed BIP86 identity with + `createpaymasteridentity`. + Immediately create a full-wallet backup. Restoring that file preserves the + same provider ID and its wallet-local metadata; creating a new wallet starts + a new identity and a separate finance history. +3. While the provider is stopped, set a policy with `setpaymasterpolicy`. The + RPC holds the wallet-scoped provider-work guard across the stopped-state + check and database commit, so autostart cannot race the policy change. The absolute + `maximum_network_fee_dgb_satoshis` must be positive. + A public provider may advertise `user_paid` and `sponsored` at the same + time. User-paid transfers use the configured DD service-fee rate, while + sponsored transfers charge no DD service fee. Restricted sponsorship is a + sponsored-only invitation mode and cannot be combined with `user_paid`. +4. Set a complete finite provider loss/rate policy with + `setpaymastersafetypolicy` and inspect it with + `getpaymastersafetystatus`. A funding model that the provider advertises + requires six positive limits; six zero values disable only an unadvertised + model and never mean unlimited. The setter shares the wallet-scoped + provider-work guard with automatic servicing, so a scheduler step cannot + reserve work against the old limits while their replacement is committed. + The Qt Safety limits page explains these wallet-local spending and request + controls. Its reset buttons change only the unsaved form: user-paid limits + return to finite recommended starting values, unused sponsored models return + to safe all-zero disabled values, and quote/client sections return to their + recommended starting values. Saving is always required before a reset takes + effect in the wallet backend. Qt only assembles and displays these values; + the Core RPC remains solely responsible for validating every safety-policy + relationship and rejecting an invalid configuration. +5. Enable the wallet using `setpaymasterenabled true`. +6. Preview and explicitly execute `preparepaymasterpool`, passing the returned + `plan_id` unchanged with `execute=true`. Admission requires at + least three independent confirmed DGB slots of at least 10,000,000 sat each. + A `USER_PAID` policy additionally requires at least three confirmed admission + DD carriers. Admission and operational pool entries are separate. + The Qt Liquidity page explains the four target classes and guides a new + operator through restore/choose targets, preview, review, execution, and + confirmation. Restoring targets never moves funds. Execution is enabled only + for the unchanged target set and cryptographic plan from the matching latest + preview; Core recomputes that plan before unlocking or constructing a + transaction. `rebalancepaymasterpool` uses the same binding for the exact + entries being retired. Manual pool calls also share the provider-work guard + with automatic maintenance. Operational DGB entries below the current + advertised network-fee ceiling do not satisfy a target, and rebalancing + retains higher-value usable entries before retiring undersized ones. The default + targets are three admission and one operational DGB slot, plus three + admission and one operational DD carrier when `USER_PAID` is selected; + sponsored-only defaults use no carriers. +7. Save the ongoing targets and finite maintenance budget with + `setpaymasterliquiditypolicy`, then inspect them with + `getpaymasterliquiditystatus`. Automatic replenishment does not make a zero + budget unlimited: paid maintenance remains disabled until the operator sets + `paid_maintenance_approved=true` together with positive per-transaction, + rolling-hour, and rolling-day fee limits. +8. Keep the recommended automatic runtime, or while stopped use + `setpaymasterruntimesettings` to select manual expert operation and/or opt in + to autostart. Paymaster settings must use the current persisted format; + older development records are not migrated and must be replaced together + with the development Paymaster data before provider operation can continue. +9. Inspect `getpaymasterinfo`, `getpaymasterpoolinfo`, + `getpaymastersafetystatus`, `getpaymasterliquiditystatus`, and + `getpaymasterfinancestatus`, review the effective finite budgets, and + explicitly call `startpaymaster`. Its response + echoes the effective safety policy used for that start. A provider whose + only failed readiness gates are missing pool slots can start in a maintenance + wait state; it does not advertise or accept new work until the configured + targets are confirmed. + In automatic mode Core then services the bounded queues. In manual mode, + `processpaymasterrequests` handles at most one capacity, quote, or + recovery-provider request and `processpaymastersubmits` handles at most one + client-signed payment or recovery submission. These manual RPCs reject while + automatic servicing is active with `PAYMASTER_AUTOMATIC_SERVICE_ACTIVE`. + Both paths can sign only provider-owned inputs for an exact transaction that + passes the existing provider authorization manifest and safety policy. +10. Use `stoppaymaster` before planned maintenance or changing operation mode. + Configuration and durable commits remain in the wallet database. + +`getpaymasterinfo` includes `settings_present` so an identity-only wallet can +be distinguished from a deliberately disabled persisted provider +configuration. Qt uses this distinction when deciding whether a reopened +wizard should preserve `enabled=false` or apply the fresh-setup default. + +Provider RPCs include: + +```text +createpaymasteridentity +setpaymasterpolicy +setpaymastersafetypolicy +getpaymastersafetystatus +setpaymasterliquiditypolicy +getpaymasterliquiditystatus +withdrawpaymastercarrier +getpaymasterfinancestatus +acknowledgepaymasterproviderbackup +setpaymasterenabled +setpaymasterruntimesettings +preparepaymasterpool +rebalancepaymasterpool +getpaymasterinfo +getpaymasterpoolinfo +listpaymasterreservations +cancelpaymasterquote +processpaymasterrequests +processpaymastersubmits +startpaymaster +stoppaymaster +``` + +### Provider finances and backup + +The Qt **Finances** page is an operator dashboard, not a consensus or tax +accounting authority. Its wallet-local ledger is bound to the active chain's +genesis hash and exactly one provider ID. Ledgers belonging to different +provider identities are never merged automatically. It records: + +- confirmed user-paid service fees as native DD income; +- the actual native DGB network fee of user-paid, public-sponsored and + restricted-sponsored transfers; +- actual confirmed DGB fees for pool setup, replenishment, retirement and + carrier-excess withdrawal; and +- pending transactions separately until confirmation. + +Prepared pool value remains wallet-owned operating capital, not an expense. +The dashboard therefore reports available, reserved and pending DGB capacity, +the 1.00-DD principal of each active carrier, and carrier fee surplus separately +from income and costs. Replays are idempotent, and confirmation, reorg or +conflict changes rebuild the UTC daily totals instead of incrementing them a +second time. + +`getpaymasterfinancestatus` accepts `today`, `7d`, `30d` or `all`, and can +optionally return bounded event pages with cursor pagination. Qt requests 250 +rows per page; a complete CSV export loads every page asynchronously and writes +the destination atomically, while the interactive table remains bounded. The +detailed form also returns the latest 366 materialized UTC-day totals for the +selected period so operator interfaces do not need to load or expose transaction +IDs to draw the daily progression. Its selected-period model breakdown separates +transfer counts, DD income and DGB transfer costs for user-paid, public +sponsored and restricted sponsored operation. Native DD and DGB values are +authoritative. When a +current Oracle price exists, the RPC and Qt may show a current-price USD +estimate and its valuation time; no historical exchange rate is invented. +Accounting reconciliation derives an event only from current-format durable +wallet records that still prove its exact native amounts. It never upgrades an +older record format or estimates missing amounts; the result marks any earlier +unprovable history as partial. + +The Finances page links to the existing preview-first carrier-withdrawal, +carrier-release, DGB-retirement and liquidity-preparation controls. Preview and +execution remain distinct and retain the provider-stop, plan-ID, reservation +and finite-budget checks described below. Its CSV export contains only the +selected accounting period and native amounts (plus an optional current-price +valuation); it intentionally excludes keys, recipients, network identity data +and raw transactions. A CSV file is not a wallet backup. + +`backupwallet` is the supported complete provider backup. After the backup file +has been created, the source wallet records only the completion time, never the +destination path. Restoring the copied file intentionally presents the reminder +again so the restored installation can create its own current backup. Operators +using another full-wallet backup mechanism may call: + +```text +acknowledgepaymasterproviderbackup {"external_backup":true} +``` + +That acknowledgement does not create or validate a backup and must never be +used for seed-only or descriptor-only exports. Existing downgrade protection +through `WALLET_FLAG_PAYMASTER_AUTHORIZATION` remains unchanged. + +A complete backup necessarily includes the durable Paymaster metadata and +signed artifacts described above. Wallet key encryption alone does not imply +field-level encryption of that metadata. Store and transport backup files with +access controls appropriate for transaction history and provider-relationship +data, keep only the required generations, and securely retire obsolete copies +according to the operator's backup policy. Never use CSV finance export as a +substitute for this backup or as a source for recovery. + +A backup preserves the off-chain Paymaster records available when it was +created. A blockchain rescan does not reconstruct later signed provider results, +sponsorship-use records, or the complete provider budget/finance ledger. An older +client backup with an authorized payment may therefore reject a retry with +`PAYMASTER_INVALID_CAPACITY_DGB_CHAINSTATE` when its provider inputs are already +spent, even though the wallet rescan finds the confirmed payment. This rejection +must not be bypassed by starting a new payment with a new request ID. + +After restoring an older image, verify the original payment on chain and reconcile +missing off-chain records from the newest complete backup before resuming service. +Start the recovery node with `-paymaster=0` before loading the restored wallet, +then persist `setpaymasterruntimesettings {"autostart":false}` for that wallet. +Keep provider autostart disabled during reconciliation and never operate two +loaded copies of the same provider wallet. The older-client-backup regression +test does not establish provider-budget or sponsorship anti-rollback guarantees +for stale backups. See the [remaining security checks](digidollar-paymaster-release-gate.md#additional-security-boundary-checks-2026-09-19). + +`setpaymasterruntimesettings` accepts an object containing +`operation_mode` (`automatic` or `manual`) and/or `autostart` (boolean). + +```text +setpaymasterruntimesettings {"operation_mode":"automatic","autostart":false} +``` + +`getpaymasterinfo` and `startpaymaster` report `operation_mode`, `autostart`, +and `service_state`. In addition to `stopped`, `waiting_for_unlock`, +`waiting_for_readiness`, `active`, `manual`, `drain_only`, and `error`, the +service may report `waiting_for_maintenance_approval`, +`replenishing_liquidity`, or `waiting_for_liquidity_confirmation`. +`last_service_error`, when present, is a stable, privacy-neutral diagnostic. +`getpaymasterinfo.liquidity` contains the same target, pending, missing, +maintenance-budget, and carrier-value summary returned by +`getpaymasterliquiditystatus`. `startpaymaster` also returns the effective +finite `safety_policy` used for the operator's start review. + +Pool preparation and rebalancing are transaction-creating operations only when +their explicit execution option is set. Preview the returned plan before +executing it. Locking the wallet pauses automatic queue processing before +messages are consumed and prevents new quotes and signatures; it does not erase +configuration or prevent later recovery of an already durable final commit. + +### Automatic liquidity lifecycle + +Operational inputs are not treated as disposable after a successful payment. +When the provider atomically commits the fully validated final transaction, +Core also records its exact wallet-owned replacement outputs from the provider +authorization manifest: + +- A returned carrier becomes a `PENDING_SUCCESSOR` containing the previous + carrier principal plus the user-paid service fee. A 1.00 DD carrier that + earns 0.03 DD therefore becomes a 1.03 DD operational carrier, and can later + become 1.06 DD without a separate carrier-maintenance transaction. +- Wallet-owned DGB change becomes a `PENDING_SUCCESSOR` only when its value is + still at least the provider's required per-transfer DGB capacity. Smaller + change remains ordinary wallet value and the missing target may require paid + replenishment. + +The successor provenance is persisted with the provider commit or maintenance +operation that created it. After the required confirmation it becomes +`AVAILABLE`; a reorg moves it back to `PENDING_SUCCESSOR`, and a conflicting or +otherwise unusable output becomes `INVALIDATED`. Startup reconciliation is +idempotent and can reconstruct still-unspent successors from previously +confirmed current-format provider commits. Only active pool states protect +funds from ordinary wallet coin selection; `RELEASED` and `INVALIDATED` entries +do not. + +`ProviderLiquidityPolicy` is wallet local and controls: + +- automatic replenishment; +- admission and operational targets for DGB and DD carriers; and +- finite maximum maintenance fees per transaction, rolling hour, and rolling + day. + +The status RPC reports each class as `ready`, `pending`, +`counted_toward_target`, and `missing`. `AVAILABLE`, `RESERVED`, and +`PENDING_SUCCESSOR` count toward a target so a scheduler retry cannot create a +duplicate replacement, but only confirmed `AVAILABLE` entries are ready for a +new request. A restartable maintenance ledger stores the exact planned outputs, +transaction, reserved fee, actual fee, and monotonic accounting time. + +In a started automatic provider, Core first reconciles free successors and any +already durable maintenance transaction. It then creates at most the missing +liquidity, waits for confirmation, and only then resumes announcements and new +requests. Already authorized submissions can still be completed safely. A +locked wallet, insufficient DGB, disabled automatic replenishment, or exhausted +maintenance budget pauses new work without consuming queued requests. A +stopped provider and a provider in manual mode never create paid maintenance +transactions automatically. + +Existing providers receive suggested targets of at least three admission and +one operational DGB slot, plus corresponding carrier targets when `USER_PAID` +is enabled. Higher existing active counts are retained. Suggested maintenance +ceilings are the lowest positive ceilings of the active provider safety +classes. The operator must approve these finite limits once before the first +paid replenishment; free successor recycling does not spend the maintenance +budget. + +### Carrier excess and slot release + +Carrier service fees remain wallet-owned but stay protected while their output +is an active pool slot. `withdrawpaymastercarrier` is preview-first and offers +two distinct operations: + +```text +withdrawpaymastercarrier {"mode":"all_excess"} +withdrawpaymastercarrier {"mode":"all_excess","execute":true,"plan_id":"PLAN"} + +withdrawpaymastercarrier {"mode":"release_slot","txid":"TXID","vout":0} +withdrawpaymastercarrier {"mode":"release_slot","execute":true,"plan_id":"PLAN"} +``` + +- `all_excess` selects only confirmed, available carriers with more than 1.00 + DD. It creates fresh wallet-owned replacement carriers of exactly 1.00 DD and + combines only the excess into a fresh wallet-owned DD output. This is a DD + transaction, requires ordinary non-pool DGB fee inputs, and charges its DGB + fee to the finite maintenance budget. +- `release_slot` marks one confirmed, available operational carrier as + `RELEASED` and atomically reduces the operational-carrier target by one. It + is wallet local and costs no network fee, but the provider must be stopped. + +Execution requires the unchanged, unexpired `plan_id` from the immediately +preceding preview. Reserved, unconfirmed, authorized, spent, or invalidated +carriers cannot be withdrawn. Releasing the last carrier leaves `USER_PAID` +configured but not ready until liquidity is restored. + +A zero operational-carrier target is therefore a valid, intentional stopped +state, not a repair request. `getpaymasterinfo` reports +`PAYMASTER_LIQUIDITY_TARGETS_INCOMPLETE` and +`getpaymasterliquiditystatus` reports `waiting_for_target_configuration` until +the operator explicitly raises and saves the carrier target. Provider start and +autostart must not claim that automatic maintenance can repair this state: +maintenance only restores missing outputs *up to the saved targets*. + +Automatic-liquidity regression coverage is intentionally split by failure +surface. Provider unit tests cover target-policy coherence, explicit approval, +per-transaction and rolling maintenance ceilings, replay conflicts, release, +and monotonic accounting. Wallet-store tests cover atomic successor writes, +database failures, restart reconstruction, confirmation, reorg, conflict, and +idempotency. Functional tests exercise free carrier/DGB recycling and paid DGB +replacement after real transfers, plus paid carrier replacement with disabled +automation, missing approval, pending-confirmation restart, duplicate-scheduler +ticks, confirmation promotion, and exhausted rolling budget. The +`paymaster_pool_lifecycle` stateful fuzz target permutes pool reservations, +commits, successors, maintenance records, restarts, reorgs, conflicts, and +withdrawal plans while continuously checking durable invariants. No single +happy-path test is treated as proof of the lifecycle. + +The provider safety policy has separate classes for `user_paid`, +`public_sponsored`, and `restricted_sponsored`. Each class limits the network +fee per transaction, simultaneously reserved fee, rolling-hour and rolling-day +fees, and completed transactions per hour/day. The policy also limits active +quotes globally, per netgroup, and per wallet-HMAC-pseudonymized recipient, plus +quote requests per netgroup/minute. Raw IP addresses are not persisted as +accounting keys. + +The maximum possible DGB network fee is atomically reserved with a quote, +converted to durable spend by final commit, and released only by a safe +unsigned abort. Limits, reservations, usage, and accounting time survive +restart. When a class reaches a limit it returns +`PAYMASTER_SAFETY_LIMIT_EXHAUSTED`, stops creating quotes and announcements for +that class, and never interprets a missing/zero limit as unlimited. + +A provider with no readiness for new work may enter drain-only operation when +durable signed or committed work exists. It publishes no new capacity and +accepts no new quotes, but can finish exact authorized submits and recovery. +Locking the wallet similarly prevents new signatures without erasing durable +recovery state. + +Direct message admission uses bounded token buckets per peer, netgroup, +provider, and session, with fair queuing between sessions. Cheap size, replay, +announcement, and rate checks run before expensive signature or chainstate +validation. Capacity, submit, capability, and result replay state that affects +safety is persisted. Expired unsigned quotes/capacity reservations are released +atomically; a user- or provider-signed authorization is never unlocked merely +because its original TTL elapsed. + +Generation-bound, reference-counted RAII leases protect inbox messages across +the persistence decision. TTL pruning and both generic and specific dequeue +operations skip an active lease. An exception or failed write releases only +the lease and preserves the claim for retry; acknowledge/consume is the sole +terminal removal. + +## Fee behavior + +Provider percentage fees are limited to 10,000 basis points, in multiples of +10, and round upward to a whole DD cent: + +- 0 cents: no provider DD fee output or carrier; +- 1–99 cents: a confirmed carrier input and valid successor are required; +- 100 cents or more: a normal provider DD output is used. + +Public and restricted sponsorship charge exactly 0 DD service fee. Restricted +capabilities are bound to one concrete payment and are not globally gossiped. +Public sponsorship must still have explicit finite hourly and daily DGB budgets; +the budgets cap loss but do not provide Sybil fairness. + +For restricted operation, configure a sponsored-only policy with +`sponsorship_scope=restricted`. `startpaymaster` starts the direct provider +runtime but deliberately publishes no announcement. An authenticated sponsor +service supplies its x-only authorization key to +`createrestrictedpaymasterdescriptor`; the returned descriptor binds that key, +the provider identity, endpoint, policy, and current admission reserve. + +The sponsor signs a one-payment capability for the descriptor. The client +supplies `provider_identity_key`, `restricted_service_descriptor`, and +`sponsorship_capability` together in the seventh `senddigidollar` options object. +Restricted sessions use exactly one provider attempt. The capability plaintext +exists only in memory and on the encrypted direct connection: wallets persist +only its hash, payment binding, reservation, and consumed state. Do not pass a +capability on a command line or store it in shell history; use an authenticated +JSON-RPC client or protected application channel. + +## Recovery with an independent provider + +For an ambiguous authorization, `retry_same` may contact only the exact original +provider and artifacts. `cancel_to_self` can instead select a distinct +`USER_PAID` recovery provider through the `options` object accepted by +`resolvepaymastersession`. The recovery provider must differ from the original, +pass the full V5 Capacity handshake, and satisfy the inherited privacy profile. + +The recovery transaction reuses the reserved user inputs but returns all DD to +fresh wallet-owned scripts. The only permitted deduction is the locally bounded +recovery service fee; all DGB fee inputs belong to the recovery provider. The +client first receives an exact recovery authorization commitment and must +explicitly resubmit that commitment before its inputs are signed. Original user +inputs remain reserved until either the original transaction or the recovery +transaction is confirmed. + +Useful optional `cancel_to_self` fields are `recovery_provider_id`, +`recovery_offer_id`, `maximum_recovery_service_fee_cents`, `prepare_only`, and +`recovery_authorization_commitment`; use `help resolvepaymastersession` for the +running binary's exact schema. If neither the client has DGB nor a distinct +eligible recovery provider is reachable, delayed publication of the already +authorized original transaction remains an unavoidable on-chain risk. + +## Diagnostics + +Start with the authoritative RPC output rather than debug logs: + +```text +getpaymasterinfo +getpaymasterpoolinfo +getpaymastersafetystatus +getpaymasterliquiditystatus +getpaymasterclientsafetystatus +listpaymasters +getdigidollarsendsession +listdigidollarsendsessions +listpaymasterreservations +``` + +Production logs intentionally omit payment details, raw PSBTs, capabilities, +and key material. The provider functional test exercises this boundary with +verbose RPC and network logging and rejects known recipients, request/session +identifiers, descriptors, capabilities, PSBTs, recovery transactions, and key +material in either node's `debug.log`. Session responses expose authoritative +`session_state`, `pending_phase`, `final`, `broadcast_state`, and +`confirmation_state` fields. + +SOCKS authentication values used for stream isolation are never logged. +Connections opened for high-privacy Paymaster operation also redact the target +host and port from proxy success and failure messages. This log protection does +not reduce the sensitivity of the wallet database or its backups. + +All Paymaster clients require `-v2transport=1`; a direct Paymaster connection +never falls back to plaintext P2P v1. High privacy additionally requires an +onion endpoint, a configured onion proxy with `-proxyrandomize=1`, +`-logips=0`, `-capturemessages=0`, and exactly one provider attempt. These +requirements are checked before Core creates a payment session or reserves +wallet inputs, and are rechecked when the isolated connection is opened. + +`-paymaster=0` disables announcement discovery/relay and rejects new client or +provider operation. Paymaster operation is also unavailable before DigiDollar +activation. Unloading a provider wallet immediately removes its ephemeral +running state. Loading it again restores its durable identity, policy, pools, +recovery records, and runtime preferences. It remains stopped by default; +runtime restart after load occurs only when the operator previously enabled +`autostart` and the wallet satisfies readiness (or waits for a later unlock). + +## Automatic completion of a finite setup command + +An executed `preparepaymasterpool` command persists a finite authorization and +can now complete later through the existing wallet scheduler, including while +provider service is stopped. This exception applies only to an explicitly +approved setup, not recurring paid maintenance. Inspect `accepted` separately +from `executed`, and monitor `getpaymasterpoolinfo.preparation`. The default fee +ceiling is 0.2 DGB per setup transaction, at most two transactions per command. +See [automatic pool setup](digidollar-paymaster-pool-setup.md) for waiting states, +unsigned cancellation, fee changes, and recovery of a previously saved but +unregistered DGB transaction. 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..f3933446167 100644 --- a/doc/release-notes.md +++ b/doc/release-notes.md @@ -1,361 +1,631 @@ -26.2 Release Notes -================== - -DigiByte Core version 26.2 is now available from: - - - -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: - - +DigiByte Core version 9.26.6rc2 +============================ + +RC2 is a candidate for coordinated node and wallet testing. It replaces RC1, +which could reject a valid DigiDollar mint block during a reindex on a node +that holds a DigiDollar wallet. It is not the final v9.26.6 release. Activation +settings are listed below; installing a candidate does not change heights +already built into it. + +The release repairs identified crash and hang defects, improves DigiDollar +redemption and amount handling, and corrects wallet displays. Feather reduces +memory use by keeping less-used block header fields on disk and removing +repeated startup work. Memory use and startup time depend on the chain, +hardware, and options. See [the RAM explanation](../RAM_IMPROVE.md) for the +design and measured results. + +Saved transaction fee estimates now load correctly after a restart. The node +keeps its learned fee history instead of discarding its own saved file. + +It also adds Thaw Day. Thaw Day is a single block height, one per network, at +which a set of DigiDollar rule changes takes effect together. **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 +trigger the changes. Signet is unsupported and remains unscheduled. Installing +this software before a network's height does not activate its rules early. + +Please report problems using the issue tracker at GitHub: + + + + +Changes since RC1 +================= + +**Block validation no longer reads the wallet's script table.** The node keeps +an in-memory table that the wallet and the transaction builder fill in with the +DigiDollar amount behind each output script. The table is keyed by the script +alone, so when a DigiDollar owner address is used again, only the last amount +stays. Change from a send goes back to the address that held the spent token, +so any wallet that has ever sent DigiDollar has such an address. + +In RC1, block validation below Thaw Day read that table before the chain data. +A mainnet RC1 node holding a wallet that had minted $100 and later held $98 of +change at the same address rejected block 23,869,549, the first DigiDollar mint +block, with `bad-dd-mint-amount` while reindexing. It then marked every later +block invalid and stopped following the chain. Nodes without such a wallet +accepted the same block, so the answer depended on the local wallet rather than +on the chain. + +Block and mempool validation now read DigiDollar amounts from the chain only: +the mint or redeem record in the transaction itself, or the transaction that +created a token output. The table is left for the wallet and for unit tests. +The Thaw Day heights and rules are unchanged, and the chain that exists today is +what nodes without the wallet table already accepted, so this repairs a local +defect and does not change the rules. + +**If an RC1 node is stuck below a block it wrongly rejected**, install RC2, +start the node, and tell it to look at that block again: + +``` +digibyte-cli reconsiderblock 00000000000000052cc3d211b3d16196a3585d46474202dfa42e9f770d02e9bd +``` + +The node then validates the remaining blocks and catches up without another +full reindex. A fresh `-reindex` on RC2 also works and takes longer. Until you +have upgraded, do not start `-reindex` on an RC1 node that holds a DigiDollar +wallet. + +**A wallet message no longer fills the log once per block.** During a reindex or +a first sync the wallet logged "ReconcilePositionStates skipped while chainstate +is not ready" for every block, which produced an 8 GB log on one mainnet +reindex. The line now appears only with `-debug=digidollar`. + +**Tests.** A unit test suite writes wrong amounts into the table and checks that +mint validation, vault detection and the amount reader answer from chain data. A +functional test mints, pays the mint address again, and then reorgs and +reindexes with the wallet loaded. Both fail on RC1 and pass on RC2. + + +Read this first +=============== -To receive security and update notifications, please subscribe to: +Three things change what you can run or what your scripts must send. Read all +three before you upgrade. - +1. A pruned mainnet node cannot run at the smallest setting +----------------------------------------------------------- -How to Upgrade +A node has to keep the DigiDollar part of the chain. When a DigiDollar coin is +spent, the node reads the block that created it to find out how much it is worth +and how long it is locked. A pruned node has no transaction index, so the only +place to read that from is the block itself. Any block from the DigiDollar +activation height upward can hold one, so all of them have to stay on disk. + +On mainnet that floor is height **23,627,520**. Everything from there to the tip +is kept. Everything below it can still be deleted. + +At height 24,195,289, which was the tip on 12 September 2026, that is about +568,000 blocks. It grows by 5,760 blocks a day, because a block takes 15 +seconds. The rest of the chain, about 97 per cent of it by block count, can +still be deleted. **Pruning still works. `-prune=550` cannot be honoured.** + +How much disk that is has not been measured on a real node. As an estimate from +the average block size of the whole chain, about 1.2 kB, those 568,000 blocks +come to roughly 0.7 GB, growing by about 7 MB a day. Undo data is kept for the +same window and adds to it. Recent blocks are larger than the chain average, so +treat 0.7 GB as a floor, not an answer. + +**What to do.** Before you upgrade, set `-prune` to a few gigabytes rather than +550. Start the node, let it run for a day, then read `size_on_disk` and +`pruneheight` from `getblockchaininfo` and set the target from the real number. + +This constraint is not new in v9.26.6. It arrived with DigiDollar and is already +in v9.26.5. What is new here is that the floor is now held correctly. Before +this release, a reorg on a node whose tip was still below the floor dragged the +floor down to the height of the disconnected block and left it there until the +next restart, after which the node stopped pruning above that point. A pruned +node doing initial block download hit that on the first reorg it saw. That is +fixed. + +**If your node has already pruned DigiDollar-era blocks, it will not start.** +It stops with a message telling you to restore the missing block and undo files +or download that part of the chain again. This check is not new either, but it +is the failure an operator is most likely to meet, so it is worth saying plainly. +A node that has kept every block at or above the floor is not affected by this +missing-history check. + +**A node that cannot meet its prune target does not say so.** It keeps the +blocks it must keep, goes over the target you set, and writes nothing that tells +you why. This is known and is not fixed in this release. If your disk use is +above your target, this is the reason. + +2. Six DigiDollar commands no longer guess cents or dollars +------------------------------------------------------------- + +`senddigidollar`, `sendmanydigidollar`, `redeemdigidollar` and +`getredemptioninfo` used to work out whether a number meant cents or dollars +from the shape of the number. `10000` meant 10,000 cents, which is $100.00. +`10000.00` meant $10,000.00, which is a hundred times more. A habitual decimal +point moved a hundred times the intended amount, and the node did it without a +word. + +They now take the unit with the amount: + +- **A whole number with no unit still means cents.** `10000` is $100.00, exactly + as before. Anything already sending whole numbers of cents needs no change. +- **A number with a decimal point and no unit is refused.** Nothing is sent, + redeemed or changed. The error is: + `ambiguous amount: pass amount_unit=cents or amount_unit=dollars (an amount + with a decimal point is not accepted without a unit)` +- **`amount_unit="cents"`** requires a whole number. `"10.00"` is refused. +- **`amount_unit="dollars"`** allows at most two decimal places. `"12.34"` is + 1,234 cents. `"12"` is 1,200 cents. `"12.345"` is refused. + +The new argument goes last: + +``` +senddigidollar
[comment] [fee_rate] [selected_inputs] [amount_unit] +sendmanydigidollar [comment] [selected_inputs] [amount_unit] +redeemdigidollar [redemption_address] [fee_rate] [amount_unit] +getredemptioninfo [dd_amount] [amount_unit] +``` + +Two more take it for the amounts they filter on, rather than an amount they +send: + +``` +listdigidollarpositions [active_only] [tier_filter] [min_amount] [count] [skip] [amount_unit] +listdigidollaraddresses [include_watchonly] [min_balance] [include_empty] [amount_unit] +``` + +Both of these send $250.00: + +``` +digibyte-cli senddigidollar "" 25000 +digibyte-cli -named senddigidollar address="" amount="250.00" amount_unit="dollars" +``` + +**Anything automated against these four commands has to be checked before you +upgrade.** If it sends whole numbers of cents, it keeps working. If it sends +decimals, it stops working until you add `amount_unit="dollars"`. It fails +loudly rather than moving the wrong amount, which is the point of the change. + +Two list filters take the same argument for the same reason: `min_amount` on +`listdigidollarpositions` and `min_balance` on `listdigidollaraddresses`. A +negative value in either used to be accepted and then ignored; it is now +refused. `getredemptioninfo` now applies the same $100,000 limit that +`redeemdigidollar` applies, so asking about a redemption and doing it give the +same answer. + +`mintdigidollar` is unchanged. It has always taken whole cents only. + +3. Thaw Day activation settings +-------------------------------------------- + +Thaw Day uses one height per network: + +| Network | Height in this candidate | 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 configured | Local test setting | + +The height is the trigger. Dates are estimates that change with the block rate. +Only a private regtest chain can set a runtime height with +`-ddthawdayheight=N`. On any public network that option is a startup error. +The one height on each network selects all intended Thaw Day rule changes +together. Blocks below it keep their existing rules. + +Publish the 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. The selected height is expected on +September 18–19, before the September 21 target. Recheck the block rate during +the upgrade window. Coordinate upgrades with miners, oracles, exchanges and +full-node operators. Scheduling the height does not establish that everyone +has upgraded or that release checks have passed. + +If the upgrade window or required checks cannot be met, coordinate a replacement +release before the scheduled height to postpone or disable activation. +Operators must install the replacement before the old height. An announcement +alone cannot change an installed binary's height. See +[node operations](digidollar-operations.md) for upgrade and recovery steps. + + +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 +Shut the old version down and wait until it has fully stopped. This can take a +few minutes. Then run the installer on Windows, or copy over +`/Applications/DigiByte-Qt` on macOS, or `digibyted` and `digibyte-qt` on Linux. + +A reindex is **not** required. The wallet file format has not changed. Back up +your wallets before you upgrade, as you would for any upgrade, and keep the +backups you already have. + +If you prune, read section 1 above first and raise the setting. + +Older wallets still open. Minting a new DigiDollar needs a descriptor wallet +with private keys, HD support, a Taproot receiving descriptor and a bech32 +change descriptor. A wallet that cannot mint may still send and redeem existing +DigiDollar if it has the keys needed to sign those transactions. Wallets with +private keys disabled cannot send or redeem. The wallet now explains minting +eligibility before requesting the passphrase. + + +Thaw Day +======== + +Thaw Day is one block height per network. At and above it, three DigiDollar +rules change together: + +- **Minting uses a price taken from the chain**, not from each node's own + running volatility state. The block's committed oracle quote is compared with + the lower median of the nearest 15 eligible prices between 240 and 1,440 + blocks back. Transfers and redemptions stop using the old volatility freeze. +- **Health is measured from open vaults**, and those totals are stored in the + coin database with the rest of chain state, so they survive a restart and are + rebuilt the same way on every node. +- **A vault has one identity** that every node derives the same way. + +The height of the block being checked decides which rules apply to it. The new +rules use that block's ancestry and coin state during normal operation, reindex +and reorgs. Blocks below Thaw Day keep their existing validation rules. + +You can see the status at any time: + +``` +digibyte-cli getdigidollardeploymentinfo +``` + +The `thaw_day` object reports whether a height is scheduled, what it is, your +tip height, the next block height, and whether the rules apply at each. + +**Preparing and checking the accounting takes work.** The node normally builds +the starting vault totals when it connects the block immediately before Thaw +Day, or when it starts at that height. It walks the unspent outputs while +holding the chain lock, so RPC requests needing that lock wait. Later startups +independently check the saved vault totals again. **This cost has not been +measured on a mainnet-sized coin set.** Small regtest measurements do not predict +a mainnet startup time. + +An existing DigiDollar statistics index also checks its saved token supply +against unspent outputs when it first reaches Thaw Day and when it reopens +afterward. A new index can count directly from the chain as it catches up. +It compares totals for the same block and repairs an incorrect saved total. +This adds startup work. Normal block updates use each block's changes instead +of scanning the whole coin set again. Token supply and open-vault debt are +different totals; extra tokens burned during redemption can make them differ +without indicating corruption. + + +What changed +============ + +Money and safety +---------------- + +- **A wallet with no passphrase could replace the key that opens a vault.** + Saving a DigiDollar owner key for a vault that already had one overwrote the + old key without a word. The owner key is how a wallet reaches its collateral, + so the vault would have been left with no way to open it and the collateral + would have been stuck. Every wallet now refuses to replace an owner key with a + different one, and says so in the log. + +- **Redeeming could send a whole vault to an address nobody can spend from.** A + redemption hands the entire vault back in its first output. When the caller + gave no address for it, the builder made one up from the owner key. No wallet + watches that address and no wallet can spend from it. It was reached when the + wallet could not supply a change address and the leftover after the fee was + too small to make a change output, so the build did not stop earlier. The + redemption now stops with a plain error and builds no transaction. + +- **A mint is written to the wallet before it is sent, not after.** A node that + stopped in between used to leave coins locked in a vault with nothing in the + wallet to redeem it. Every write now reports whether it worked, and nothing is + sent if one fails. This is true from the console and from the wallet window. + +- **A missing owner key is recovered, never invented.** Redeeming a vault whose + key record has gone missing now searches the wallet's own keys, including the + unused ones it keeps ready, and accepts only a key that reproduces the vault's + own output on the chain. Five distinct errors replace the single "owner key + not found", so a locked wallet, a watch-only wallet and a genuinely missing + key can be told apart. + +- **Leftover DigiByte goes to your own wallet.** In a redemption it can no + longer follow an address you typed in, such as an exchange deposit address. If + a mint, a send or a redemption has nowhere safe to send leftover DigiByte, it + now stops with an error and builds nothing, rather than paying it to an + address nobody keeps the key for. + +- **`senddigidollar` and `redeemdigidollar` refuse an amount over $100,000** + before they select a single coin. `sendmanydigidollar` already did. The limit + applies to what you type. It never limits the burn a wallet works out for + itself when closing a vault. + +- **Redeeming works again on a wallet whose DigiByte is in many small pieces.** + The fee used to be estimated once against a fixed 400-byte guess, and the + redemption then failed when the real transaction cost more. It now measures + the transaction its candidate coins would produce and asks for more until they + cover it. If a wallet holds only coins so small that each one adds more fee + than value, the command says so and tells you to consolidate. + +- **A mint that can no longer confirm releases what it reserved.** It is + reported as expired, and the key and the record are kept, so a reorg or a late + block brings the vault back. + +- **Two mints, or two redemptions, sent at the same instant no longer pick the + same coins** and knock each other out. + +Crashes, hangs and things that would not stop +---------------------------------------------- + +- **Oracle shutdown now waits for callbacks before destroying their objects.** + It used to destroy its oracle objects as the first step of shutting down, + while a scheduler thread was still running inside one of them. That thread + then waited forever on a lock in freed + memory, and when the memory was reused instead, the node crashed. On the test + machine, 8 stops out of 30 hung on the old code. A test that stops a node + twenty times in a row failed on every attempt: three of those runs hung and + one ended in a segmentation fault. On the fixed code, 170 stops in a row all + completed. A node that hangs on shutdown leaves its coin database unwritten + and has to roll the chain forward again on the next start. + +- **A crash during a reorg is fixed.** The mempool's index update was being run + on entries belonging to the Dandelion stem pool, which wired the two pools + into each other. The next block connect then walked a broken structure and the + node died. + +- **Three ways a node could freeze are fixed.** All three were two threads + taking the same two locks in opposite order: the once-a-second Dandelion + embargo check, a peer disconnecting with stem transactions still queued, and + the inventory handler, which ran on every new connection. + +- **Two more lock-order faults are fixed.** The wallet's fallback broadcast + path, used when no Dandelion peer is available, changed the stem pool and ran + mempool acceptance with no locks at all. And on UTXO snapshot activation the + stem pool stayed on the old chain state. Both now take the same locks in the + same order as every other writer. + +- **The reported wallet lock-order faults are fixed.** Loading a wallet, + importing a descriptor or a wallet file, and `rescanblockchain` used to hang a + node that had a DigiDollar wallet, because the wallet asked the chain a + question while holding a wallet lock and the chain was waiting for the wallet. + Minting and redeeming had the same fault while checking that the chain had not + moved. + +- **Eight DigiDollar wallet commands now wait for the wallet to catch up with + the newest block.** A balance asked for straight after a block is now right, + and minting no longer fails over money that has already confirmed. + +What the wallet shows you +-------------------------- + +- **A redemption always tells you what happened.** Every message from the redeem + form now opens a dialog, with the transaction id, instead of going to a + desktop notification service that may be switched off or absent. Users were + seeing nothing at all after typing their passphrase. Every refusal from the + mint form does the same. + +- **A mint appears as a mint.** The transaction list used to show it as money + sent away and then received back, and folded the fee into the collateral + figure, so the wallet claimed more DigiByte was locked than really was. A mint + now has its own row showing the DigiDollars it created, with the collateral + and the fee on their own rows and the fee shown once. + +- **DigiByte and DigiDollar amounts have separate columns.** One column used to + hold DigiByte on some rows and dollars on others, so it could not be sorted, + added up or exported. Each column now sorts on its own number and exports + under its own heading. + +- **The transaction details window understands DigiDollar.** It shows the + amount, the collateral locked or returned, the lock period, the block the + collateral unlocks at, and the vault. Where a number is genuinely not in the + transaction it says so, instead of printing a zero that reads like a real + amount. + +- **Returned change is named correctly.** What a redemption hands back used to + be called "Redemption Change", which says the opposite of what happens. It is + now "DigiDollar change returned", with one sentence saying where it came from: + the wallet spends whole DigiDollar inputs, and if they add up to more than the + redemption burns, the extra comes back. A vault is always closed in full, + never in part. + +- **The Redeem button on a locked wallet asks for the passphrase** instead of + being greyed out so the prompt never appeared. A wallet with no private keys + is still refused, and now says why. + +- **The send form says why it will not take an amount**, and keeps the number + you typed instead of silently dropping the digit that would take it over the + limit. + +- **The transaction list always shows a confirmation count**, instead of + switching to the word "Confirmed" after five, and the details window labels + that count "Confirmations". + +- **Paying DigiDollar to your own wallet no longer writes errors to the log** on + a successful send. + +Speed and memory +---------------- + +- **A synced mainnet node keeps about 1.5 GB less in memory, and pays about two + and a half seconds more on startup for it.** Every block header the node held + carried an array of eight pointers, one per mining algorithm, recording the + last block that used it. It cost 64 bytes for every one of the chain's 24 + million headers, and two of its eight slots were for algorithms that were + never switched on. It is gone. The two places that read it now use the same + walk the difficulty rules have always used for everything else. + + The saving was measured three independent ways that agree within one per cent: + the block header record read out of each compiled binary, 208 bytes before and + 144 after; 1.56 GB less on a real synced mainnet node, which is 14 per cent of + everything that node was holding; and 62.9 bytes saved per block on the real + test network chain reindexed from the first block, against the 64 bytes + predicted. + + **The cost.** Startup takes about two and a half seconds longer on a mainnet + node, out of roughly two minutes. That is a real cost, paid on every start. + **Connecting blocks shows no measurable difference.** Three hundred blocks + were taken off and put back again, five rounds a start, two starts a build, on + a copy of the real mainnet chain; the spread from one run to the next is + twenty times larger than anything this change could contribute. + + The cost exists because the deleted array answered "which was the previous + block using this mining algorithm" in one jump, and the node now walks back + down the chain to find it. On mainnet the five algorithms are mined evenly, so + that walk averages under five steps. On the test network one algorithm can go + a long time without a block, and the same walk averages 127 steps, which is + why the test network shows a much larger slowdown than mainnet does. + + **An earlier figure was wrong and is withdrawn.** A first measurement said + startup got 3 to 4 per cent faster. A second measurement, with timing code + compiled into both builds, re-split those same runs and found the difference + was well inside the noise. Startup is slightly slower, not faster, and the two + and a half seconds above is the figure to use. + + **Difficulty is unchanged.** The walk that remains is byte for byte the code + that was already there. Before the array was deleted, the two lookups were run + side by side over every mining algorithm, every difficulty rule, every era and + the minimum-difficulty special case, and their answers were written down as + plain numbers that the tests still check. The node reports exactly the same + difficulty for all five algorithms on mainnet and on the test network, and + reaches exactly the same chain tip. Nothing on disk changed: the stored block + record never held these pointers. + +- **Routine DigiDollar and oracle log lines are quiet by default.** Three lines + were written for every transaction on every node regardless of settings. They + are gone, and routine DigiDollar and oracle messages now need + `-debug=digidollar`. Errors, warnings and startup progress are still shown + without it. One line is a known exception and still prints without the + category: a note that an owner key is already saved, written when a wallet is + paid DigiDollar at one of its own addresses. Anything that reads the log for + routine DigiDollar lines has to add the category. + +- **Coin scans use a compact, sorted copy of changed cache entries** alongside + the database records. This replaces the extra hash table used in an earlier + version of the new accounting code. The scan still copies the changed coins + so later cache reads cannot disturb it, and uses their newer values in place + of the saved versions. + +- **Mining no longer grinds at the block before Thaw Day.** The starting + accounting record is built once, when the block is really connected, instead + of once for every block template a miner asks for. + +Startup and recovery -------------------- -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 ------------------------------------------------------------ +- **Startup does not repeat oracle work it has already done**, and reuses parsed + bundles instead of parsing them again. + +- **A long DigiDollar rebuild reports progress and can be cancelled.** A scan + that is interrupted no longer publishes half-finished figures. The new + canonical recovery starts when the next block reaches Thaw Day. Existing + DigiDollar startup reconstruction still runs on DigiDollar-active chains + below that height, including when Thaw Day is not scheduled. + +- **A pruned node keeps the block history validation needs.** See section 1 + above. + +Tests, tools and documents +--------------------------- + +- `decoderawtransaction` and `getrawtransaction` work on DigiDollar transactions + on a node started with `-rpcdoccheck`, and their help lists the DigiDollar + fields they return. They could not before, so no test could decode one. +- Two inherited pruning tests now build ordinary pre-DigiDollar history first, + so they can test pruning on a chain where DigiDollar starts at the first + block. +- Seven unit tests no longer pass or fail depending on which directory the test + program was started from. +- The anchors test writes a port with its leading zero, so it no longer reports + a false failure on a machine using low port numbers. +- The operator, wallet, exchange and oracle guides are updated. +- Oracle display names come from each network's roster. Mainnet ID 0 shows + "DigiByte.Io Oracle" and ID 11 shows "Crypto Corner Shop". This is local + display text only. Oracle IDs, keys, ordering, signatures and quorum are + unchanged, and nothing about what is signed or sent over the wire changed. + + +For exchanges and custody +========================= + +- **The four amount commands are the breaking change.** See section 2. Check + every script before you upgrade. +- **Circulating supply can now report that it is unknown** instead of returning + an unreliable number. `getdigidollarstats` returns an error when historical + amounts cannot be recovered reliably. The text differs between the legacy + and Thaw Day paths. Treat this as unavailable supply. Do not treat it as zero. +- **Open vault principal is reported separately from circulating tokens.** + `getdigidollarstats` reports `open_vault_principal`, and + `selected_health_denominator` says which of the two the health figure was + computed from. At and above Thaw Day it is `open_vault_principal`; below it, + `legacy_supply`. +- Extra burns can leave fewer circulating tokens than open vault principal. With + the same collateral, that can lower health, prolong a restriction on minting, + or raise the DigiDollar needed to redeem. Thaw Day itself creates no tokens + and changes no wallet balance. +- **Leftover DigiByte from a redemption no longer follows a redemption address + you supply.** If you pass a deposit address for the returned collateral, the + change after the fee stays in the sending wallet, and the redemption stops + with an error rather than sending the change somewhere the wallet cannot + reach. + + +For miners and pools +==================== + +Nothing in this release changes difficulty or the block reward. + +The Thaw Day rule changes are gated on each network's height. Mainnet uses +24,490,000 and testnet26 uses 432,100. Signet is unsupported and remains +unscheduled. Blocks below a scheduled height retain their existing validation +rules, including during reindex. +An earlier testnet reindex reached the recorded height and hash. That result +applies to its tested build and history. The final-build mainnet reindex still +needs to be completed. + +Once the scheduled height is reached, a block that mints DigiDollar is +checked against a price taken from the chain rather than from each node's own +state. A miner still running old software after that height can build blocks the +rest of the network rejects. Plan to upgrade before the height, not after it. + + +What has been checked, and what has not +======================================= + +Each candidate needs its own verification record. That record must identify the +source commit, binary hashes, build options, tests run, skipped tests, and +unresolved findings. Results from an earlier build do not establish that RC2 +passed. Publish the completed record with the tested candidate. + +Controlled regtest coverage includes Thaw Day activation, restart, reindex, +reorgs, and separate token and vault accounting. A public testnet reindex checks +the history available at its recorded height. Mainnet Thaw Day is scheduled +at 24,490,000 and testnet26 at 432,100. Public Thaw Day activation and sustained +network observation have not been completed for this candidate. + +Before the final mainnet release, complete the final-build mainnet history +reindex, coordinated public testnet activation and observation, required platform +checks, and final independent review. A local candidate test run does not finish +those release gates. + +Passing a reindex is evidence about the tested build and the recorded history. +It does not prove how every possible future block will behave. + +RC2 record, source commit `653484decd` with the version bump on top: 3,742 unit +tests passed with none failed; 396 extended functional tests passed with 17 +skipped and none failed; the desktop wallet tests passed. The isolated Thaw Day +lab exercise `rc2-rehearsal-09` crossed Thaw Day at a lab height of 5,000, ran +every DigiDollar feature before and after it, including mints at all ten lock +tiers, transfers, every redemption path, the extra burn, the boundary reorg, a +wallet-holding node reindex, and a fresh empty node syncing from scratch, and +finished at height 5,658 with 143 recorded checks and no failures. The mainnet node +whose RC1 reindex had stopped at 23,869,548 accepted block 23,869,549 on RC2 +after `reconsiderblock`, with its DigiDollar wallet loaded, and validated every +later block to the tip at 24,222,044 with no rejection. That covers the whole +DigiDollar era of mainnet on a wallet-holding node. The blocks below the +DigiDollar activation height were validated by the same node on RC1 and were +not validated again. Fuzz targets were not rerun for RC2. -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 +DigiByte Core is supported and extensively tested on operating systems using the +Linux kernel, macOS 11.0 and newer, and Windows 7 and newer. It should work on +most other Unix-like systems but is not tested on them as often. -As well as to everyone that helped with translations on -[Transifex](https://www.transifex.com/digibyte/digibyte/). +Notes for earlier releases are under `doc/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..aaa8db15c06 --- /dev/null +++ b/doc/release-notes/release-notes-9.26.6.md @@ -0,0 +1,9 @@ +DigiByte Core version 9.26.6rc2 +============================ + +The notes for this candidate are in [doc/release-notes.md](../release-notes.md), which is where the +notes for the release being prepared live. They move here when v9.26.6 is +released. + +This file previously held a partial, mid-development version of those notes. It +was removed so that two documents about the same release cannot disagree. 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..edd0853e656 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -157,12 +157,28 @@ 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 \ digidollar/validation.h \ + paymaster/txbuilder.h \ + paymaster/client.h \ + paymaster/types.h \ + paymaster/reservation.h \ + paymaster/directory.h \ + paymaster/manager.h \ + paymaster/psbt.h \ + paymaster/recovery.h \ + paymaster/provider.h \ + paymaster/protocol.h \ + paymaster/reputation.h \ + paymaster/sponsorship.h \ + paymaster/wire.h \ + paymaster/validation.h \ external_signer.h \ flatfile.h \ headerssync.h \ @@ -218,6 +234,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 +258,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 +322,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 \ @@ -316,6 +335,7 @@ DIGIBYTE_CORE_H = \ util/hash_type.h \ util/hasher.h \ util/insert.h \ + util/int128.h \ util/macros.h \ util/message.h \ util/moneystr.h \ @@ -349,6 +369,7 @@ DIGIBYTE_CORE_H = \ wallet/context.h \ wallet/crypter.h \ wallet/db.h \ + wallet/digidollarmintcapability.h \ wallet/digidollarwallet.h \ wallet/ddcoincontrol.h \ wallet/dump.h \ @@ -359,6 +380,9 @@ DIGIBYTE_CORE_H = \ wallet/receive.h \ wallet/rpc/util.h \ wallet/rpc/wallet.h \ + wallet/rpc/paymaster.h \ + wallet/rpc/paymaster_internal.h \ + wallet/rpc/paymaster_send.h \ wallet/salvage.h \ wallet/scriptpubkeyman.h \ wallet/spend.h \ @@ -367,6 +391,11 @@ DIGIBYTE_CORE_H = \ wallet/types.h \ wallet/wallet.h \ wallet/walletdb.h \ + wallet/paymasterpsbt.h \ + wallet/paymasteridentity.h \ + wallet/paymasterprovider.h \ + wallet/paymasterstore.h \ + wallet/paymasterstore_internal.h \ wallet/wallettool.h \ wallet/walletutil.h \ walletinitinterface.h \ @@ -398,6 +427,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 +453,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 \ @@ -531,11 +562,30 @@ libdigibyte_wallet_a_SOURCES = \ wallet/rpc/transactions.cpp \ wallet/rpc/util.cpp \ wallet/rpc/wallet.cpp \ + wallet/rpc/paymaster.cpp \ + wallet/rpc/paymaster_client.cpp \ + wallet/rpc/paymaster_discovery.cpp \ + wallet/rpc/paymaster_integration.cpp \ + wallet/rpc/paymaster_send.cpp \ + wallet/rpc/paymaster_processing.cpp \ + wallet/rpc/paymaster_provider.cpp \ + wallet/rpc/paymaster_runtime.cpp \ wallet/scriptpubkeyman.cpp \ wallet/spend.cpp \ wallet/transaction.cpp \ wallet/wallet.cpp \ wallet/walletdb.cpp \ + wallet/paymasterdb.cpp \ + wallet/paymasterpsbt.cpp \ + wallet/paymasteridentity.cpp \ + wallet/paymasterprovider.cpp \ + wallet/paymasterstore.cpp \ + wallet/paymasterstore_client.cpp \ + wallet/paymasterstore_finalization.cpp \ + wallet/paymasterstore_provider.cpp \ + wallet/paymasterstore_reconciliation.cpp \ + wallet/paymasterstore_recovery.cpp \ + wallet/paymasterstore_reputation.cpp \ wallet/walletutil.cpp \ wallet/coinselection.cpp \ $(DIGIBYTE_CORE_H) @@ -649,6 +699,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,10 +795,25 @@ libdigibyte_common_a_SOURCES = \ core_read.cpp \ core_write.cpp \ deploymentinfo.cpp \ + digidollar/amount.cpp \ digidollar/digidollar.cpp \ digidollar/scripts.cpp \ digidollar/txbuilder.cpp \ digidollar/validation.cpp \ + paymaster/txbuilder.cpp \ + paymaster/client.cpp \ + paymaster/types.cpp \ + paymaster/reservation.cpp \ + paymaster/directory.cpp \ + paymaster/manager.cpp \ + paymaster/psbt.cpp \ + paymaster/recovery.cpp \ + paymaster/provider.cpp \ + paymaster/protocol.cpp \ + paymaster/reputation.cpp \ + paymaster/sponsorship.cpp \ + paymaster/wire.cpp \ + paymaster/validation.cpp \ external_signer.cpp \ init/common.cpp \ kernel/chainparams.cpp \ @@ -943,10 +1009,13 @@ digibyte_wallet_SOURCES += init/digibyte-wallet.cpp digibyte_wallet_CPPFLAGS = $(digibyte_bin_cppflags) digibyte_wallet_CXXFLAGS = $(digibyte_bin_cxxflags) digibyte_wallet_LDFLAGS = $(digibyte_bin_ldflags) +# Wallet code needs node symbols, while node RPCs call wallet Paymaster helpers. +# Rescan the wallet archive after the node archive introduces those references. digibyte_wallet_LDADD = \ $(LIBDIGIBYTE_WALLET_TOOL) \ $(LIBDIGIBYTE_WALLET) \ $(LIBDIGIBYTE_NODE) \ + $(LIBDIGIBYTE_WALLET) \ $(LIBDIGIBYTE_COMMON) \ $(LIBDIGIBYTE_UTIL) \ $(LIBUNIVALUE) \ @@ -1027,6 +1096,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.qt.include b/src/Makefile.qt.include index 82ac084ec1e..ee0ae2c7623 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -100,6 +100,7 @@ QT_MOC_CPP = \ qt/moc_digidollarreceivewidget.cpp \ qt/moc_digidollarredeemwidget.cpp \ qt/moc_digidollarsendwidget.cpp \ + qt/moc_paymastersendwidget.cpp \ qt/moc_digidollartransactionswidget.cpp DIGIBYTE_MM = \ @@ -182,7 +183,10 @@ DIGIBYTE_QT_H = \ qt/walletview.h \ qt/winshutdownmonitor.h \ qt/digidollartab.h \ + qt/paymasterconfirmation.h \ + qt/paymasterwidget.h \ qt/digidollar_qt_translate.h \ + qt/digidollarstatus.h \ qt/digidollarmintwidget.h \ qt/ddaddressbookpage.h \ qt/digidollaroverviewwidget.h \ @@ -191,6 +195,7 @@ DIGIBYTE_QT_H = \ qt/digidollarreceivewidget.h \ qt/digidollarredeemwidget.h \ qt/digidollarsendwidget.h \ + qt/paymastersendwidget.h \ qt/digidollartransactionswidget.h QT_RES_FONTS = \ @@ -307,6 +312,7 @@ DIGIBYTE_QT_WALLET_CPP = \ qt/walletview.cpp \ qt/ddaddressbookpage.cpp \ qt/digidollartab.cpp \ + qt/paymasterwidget.cpp \ qt/digidollarmintwidget.cpp \ qt/digidollaroverviewwidget.cpp \ qt/digidollarpositionswidget.cpp \ @@ -314,6 +320,7 @@ DIGIBYTE_QT_WALLET_CPP = \ qt/digidollarreceivewidget.cpp \ qt/digidollarredeemwidget.cpp \ qt/digidollarsendwidget.cpp \ + qt/paymastersendwidget.cpp \ qt/digidollartransactionswidget.cpp DIGIBYTE_QT_CPP = $(DIGIBYTE_QT_BASE_CPP) diff --git a/src/Makefile.qttest.include b/src/Makefile.qttest.include index 8408e9df6af..76079a643be 100644 --- a/src/Makefile.qttest.include +++ b/src/Makefile.qttest.include @@ -16,14 +16,23 @@ 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_paymasterwidgettests.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/paymasterwidgettests.h \ + qt/test/digidollartestutil.h \ qt/test/digidollarwave19widgettests.h \ + qt/test/ddtransactionrecordtests.h \ qt/test/optiontests.h \ qt/test/rpcnestedtests.h \ qt/test/uritests.h \ @@ -45,8 +54,13 @@ 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/paymasterwidgettests.cpp \ + qt/test/digidollartestutil.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..c3f3374cad5 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,8 +211,22 @@ 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/paymaster_txbuilder_tests.cpp \ + test/paymaster_client_tests.cpp \ + test/paymaster_types_tests.cpp \ + test/paymaster_reservation_tests.cpp \ + test/paymaster_directory_tests.cpp \ + test/paymaster_psbt_tests.cpp \ + test/paymaster_recovery_tests.cpp \ + test/paymaster_provider_tests.cpp \ + test/paymaster_protocol_tests.cpp \ + test/paymaster_reputation_tests.cpp \ + test/paymaster_sponsorship_tests.cpp \ + test/paymaster_wire_tests.cpp \ test/digidollar_validation_tests.cpp \ test/digidollar_txindex_tests.cpp \ test/digidollar_volatility_tests.cpp \ @@ -221,17 +249,24 @@ 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/denialofservice_tests.cpp \ test/deployment_burial_tests.cpp \ @@ -264,11 +299,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 \ @@ -328,14 +365,23 @@ DIGIBYTE_TESTS += \ wallet/test/rpc_util_tests.cpp \ wallet/test/scriptpubkeyman_tests.cpp \ wallet/test/walletload_tests.cpp \ + wallet/test/paymaster_wallet_load_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/paymaster_wallet_store_tests.cpp \ + wallet/test/paymaster_wallet_security_tests.cpp \ + wallet/test/paymaster_wallet_psbt_tests.cpp \ + wallet/test/paymaster_wallet_identity_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 @@ -351,7 +397,8 @@ FUZZ_WALLET_SRC = \ wallet/test/fuzz/coincontrol.cpp \ wallet/test/fuzz/coinselection.cpp \ wallet/test/fuzz/fees.cpp \ - wallet/test/fuzz/parse_iso8601.cpp + wallet/test/fuzz/parse_iso8601.cpp \ + wallet/test/fuzz/paymaster_persistence.cpp if USE_SQLITE FUZZ_WALLET_SRC += \ @@ -365,8 +412,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 +465,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 +495,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 \ @@ -480,6 +535,9 @@ test_fuzz_fuzz_SOURCES = \ test/fuzz/parse_numbers.cpp \ test/fuzz/parse_script.cpp \ test/fuzz/parse_univalue.cpp \ + test/fuzz/paymaster_stateful.cpp \ + test/fuzz/paymaster_pool_lifecycle.cpp \ + test/fuzz/paymaster_wire.cpp \ test/fuzz/partially_downloaded_block.cpp \ test/fuzz/policy_estimator.cpp \ test/fuzz/policy_estimator_io.cpp \ @@ -531,6 +589,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..ff5892d6a50 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; @@ -264,7 +292,11 @@ arith_uint256 GetBlockProofBase(const CBlockIndex& block) arith_uint256 GetBlockProof(const CBlockIndex& block) { - 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(); @@ -300,7 +332,9 @@ arith_uint256 GetBlockProof(const CBlockIndex& block) 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 +361,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..9da60435d26 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,7 +398,7 @@ 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; @@ -419,6 +440,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 +454,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/chainparamsbase.h b/src/chainparamsbase.h index 4c23e41fceb..e0f2b2a752c 100644 --- a/src/chainparamsbase.h +++ b/src/chainparamsbase.h @@ -7,6 +7,7 @@ #include +#include #include #include 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/compressor.cpp b/src/compressor.cpp index a0fa46dfb21..9401f18b818 100644 --- a/src/compressor.cpp +++ b/src/compressor.cpp @@ -180,19 +180,19 @@ bool LegacyCompressAmount(uint64_t n, uint64_t& compressed) n /= 10; e++; } - __uint128_t result; + const uint64_t max = std::numeric_limits::max(); if (e < 9) { - int d = (n % 10); + const uint64_t d = n % 10; assert(d >= 1 && d <= 9); n /= 10; - result = 1 + ((__uint128_t{n} * 9 + d - 1) * 10) + e; + if (n > (max - (d - 1)) / 9) return false; + const uint64_t mantissa = n * 9 + d - 1; + if (mantissa > (max - 1 - e) / 10) return false; + compressed = 1 + mantissa * 10 + e; } else { - result = 1 + ((__uint128_t{n} - 1) * 10) + 9; - } - if (result > std::numeric_limits::max()) { - return false; + if (n > max / 10) return false; + compressed = n * 10; } - compressed = static_cast(result); return true; } diff --git a/src/consensus/dca.cpp b/src/consensus/dca.cpp index 20d5197dba2..3333d5085ea 100644 --- a/src/consensus/dca.cpp +++ b/src/consensus/dca.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -34,14 +35,14 @@ int ResolveCanonicalHealthForDCA(int requestedHealth, bool& staleHealth) return requestedHealth; } -bool TryMultiplyInt128(__int128 a, __int128 b, __int128& result) +bool TryMultiplyInt128(util::int128_t a, util::int128_t b, util::int128_t& result) { if (a < 0 || b < 0) return false; if (a == 0 || b == 0) { result = 0; return true; } - if (a > std::numeric_limits<__int128>::max() / b) { + if (a > std::numeric_limits::max() / b) { return false; } result = a * b; @@ -87,18 +88,18 @@ int DynamicCollateralAdjustment::CalculateSystemHealth(CAmount totalCollateral, // totalCollateral is in satoshis, oraclePrice is in milli-cents per DGB // (100,000 = $1.00), and totalDD is in cents. Floor the final ratio so // health never rounds up across a DCA boundary. - __int128 numerator = 0; - if (!TryMultiplyInt128(static_cast<__int128>(totalCollateral), - static_cast<__int128>(oraclePrice), + util::int128_t numerator = 0; + if (!TryMultiplyInt128(static_cast(totalCollateral), + static_cast(oraclePrice), numerator) || !TryMultiplyInt128(numerator, 100, numerator)) { LogPrintf("DCA: Cannot calculate system health - collateral/price multiplication overflow\n"); return 0; } - __int128 denominator = static_cast<__int128>(COIN) * 1000 * - static_cast<__int128>(totalDD); - __int128 healthCalculation = numerator / denominator; + util::int128_t denominator = static_cast(COIN) * 1000 * + static_cast(totalDD); + util::int128_t healthCalculation = numerator / denominator; if (healthCalculation <= 0) { LogPrint(BCLog::DIGIDOLLAR, "DCA: Zero collateral value, system health is 0%%\n"); @@ -157,11 +158,18 @@ 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) * - static_cast<__int128>(multiplierBps); - __int128 finalRatio128 = (adjustedRatio + DCA_BPS_SCALE - 1) / DCA_BPS_SCALE; + util::int128_t adjustedRatio = static_cast(baseRatio) * + static_cast(multiplierBps); + util::int128_t finalRatio128 = (adjustedRatio + DCA_BPS_SCALE - 1) / DCA_BPS_SCALE; int finalRatio = finalRatio128 > std::numeric_limits::max() ? std::numeric_limits::max() : static_cast(finalRatio128); 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/err.cpp b/src/consensus/err.cpp index 2c432ee817c..297ca3aedd1 100644 --- a/src/consensus/err.cpp +++ b/src/consensus/err.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -119,14 +120,14 @@ CAmount EmergencyRedemptionRatio::GetRequiredDDBurn(CAmount originalDDMinted, in // Consensus-visible burn math: ceil(originalDD * 10000 / ratioBps). // Use signed 128-bit arithmetic so large valid CAmount values cannot overflow. - const __int128 numerator = static_cast<__int128>(originalDDMinted) * ERR_RATIO_NORMAL_BPS; - const __int128 required = (numerator + ratioBps - 1) / ratioBps; - const __int128 max_amount = std::numeric_limits::max(); + const util::int128_t numerator = static_cast(originalDDMinted) * ERR_RATIO_NORMAL_BPS; + const util::int128_t required = (numerator + ratioBps - 1) / ratioBps; + const util::int128_t max_amount = std::numeric_limits::max(); const CAmount requiredDD = required > max_amount ? std::numeric_limits::max() : static_cast(required); - const __int128 increaseBps = (static_cast<__int128>(requiredDD - originalDDMinted) * 10000) / originalDDMinted; + const util::int128_t increaseBps = (static_cast(requiredDD - originalDDMinted) * 10000) / originalDDMinted; LogPrint(BCLog::DIGIDOLLAR, "ERR: GetRequiredDDBurn - original: %lld, health: %d%%, ratio_bps: %d, required: %lld (%lld.%02lld%% increase)\n", static_cast(originalDDMinted), systemHealth, ratioBps, static_cast(requiredDD), @@ -377,7 +378,7 @@ bool EmergencyRedemptionRatio::ValidateERRRedemption(const CTransaction& tx, CAm return false; } - const __int128 inferredInputs = static_cast<__int128>(originalDDMinted) * tx.vin.size(); + const util::int128_t inferredInputs = static_cast(originalDDMinted) * tx.vin.size(); ddInputs = inferredInputs > std::numeric_limits::max() ? std::numeric_limits::max() : static_cast(inferredInputs); 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..095f01e5153 100644 --- a/src/consensus/volatility.cpp +++ b/src/consensus/volatility.cpp @@ -3,9 +3,12 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include +#include +#include #include #include #include +#include #include #include #include @@ -25,24 +28,24 @@ namespace Volatility { namespace { -int64_t SaturatingInt64(__int128 value) +int64_t SaturatingInt64(util::int128_t value) { - if (value > static_cast<__int128>(std::numeric_limits::max())) { + if (value > static_cast(std::numeric_limits::max())) { return std::numeric_limits::max(); } - if (value < static_cast<__int128>(std::numeric_limits::min())) { + if (value < static_cast(std::numeric_limits::min())) { return std::numeric_limits::min(); } return static_cast(value); } -uint64_t IntegerSqrtFloor(unsigned __int128 value) +uint64_t IntegerSqrtFloor(util::uint128_t value) { uint64_t low = 0; uint64_t high = std::numeric_limits::max(); while (low < high) { const uint64_t mid = low + (high - low) / 2 + 1; - const unsigned __int128 square = static_cast(mid) * mid; + const util::uint128_t square = static_cast(mid) * mid; if (square <= value) { low = mid; } else { @@ -86,6 +89,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 util::int128_t reference_price = reference.price_micro_usd; + const util::int128_t candidate_price = candidate_price_micro_usd; + const util::int128_t difference = candidate_price >= reference_price ? candidate_price - reference_price : reference_price - candidate_price; + const util::int128_t 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 +200,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 +210,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 +247,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 +519,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()); } @@ -638,20 +714,20 @@ int64_t VolatilityMonitor::CalculateStandardDeviationBps(const std::vector(value) * value; + sumSquares += static_cast(value) * value; } - const __int128 count = values.size(); - __int128 numerator = count * sumSquares - sum * sum; + const util::int128_t count = values.size(); + util::int128_t numerator = count * sumSquares - sum * sum; if (numerator <= 0) { return 0; } - const __int128 denominator = count * (count - 1); // Sample variance - const unsigned __int128 variance = static_cast(numerator / denominator); + const util::int128_t denominator = count * (count - 1); // Sample variance + const util::uint128_t variance = static_cast(numerator / denominator); const uint64_t sqrt = IntegerSqrtFloor(variance); if (sqrt > static_cast(std::numeric_limits::max())) { return std::numeric_limits::max(); @@ -686,8 +762,8 @@ int64_t CalculatePercentageChangeBps(CAmount oldPrice, CAmount newPrice) return 0; } - const __int128 delta = static_cast<__int128>(newPrice) - oldPrice; - const __int128 scaled = delta * 10000; + const util::int128_t delta = static_cast(newPrice) - oldPrice; + const util::int128_t scaled = delta * 10000; return SaturatingInt64(scaled / oldPrice); } 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/crypto/luffa.cpp b/src/crypto/luffa.cpp index 4c7b4ceb5fe..3d891e1f4a3 100644 --- a/src/crypto/luffa.cpp +++ b/src/crypto/luffa.cpp @@ -30,7 +30,9 @@ * @author Thomas Pornin */ +#if defined(__GNUC__) #pragma GCC diagnostic ignored "-Wunused-const-variable" +#endif #include #include @@ -1425,4 +1427,4 @@ sph_luffa512_addbits_and_close(void *cc, unsigned ub, unsigned n, void *dst) #ifdef __cplusplus } -#endif \ No newline at end of file +#endif diff --git a/src/dandelion.cpp b/src/dandelion.cpp index 348de21ab52..e07696f52db 100644 --- a/src/dandelion.cpp +++ b/src/dandelion.cpp @@ -36,6 +36,7 @@ bool CConnman::setLocalDandelionDestination() CNode* CConnman::getDandelionDestination(CNode* pfrom) { + if (pfrom && pfrom->IsPaymasterDirectConn()) return nullptr; LOCK(m_nodes_mutex); for (auto const& e : mDandelionRoutes) { if (pfrom == e.first) { @@ -418,6 +419,7 @@ std::vector CConnman::getAllDandelionDestinations() const void CConnman::AddDandelionDestination(CNode* pnode) { + if (!pnode || pnode->IsPaymasterDirectConn()) return; // Only process if Dandelion is enabled if (!gArgs.GetBoolArg("-dandelion", DEFAULT_DANDELION)) { LogPrint(BCLog::DANDELION, "AddDandelionDestination: Dandelion disabled, not adding peer %d\n", pnode->GetId()); @@ -469,6 +471,13 @@ void CConnman::AddDandelionDestination(CNode* pnode) } } +void CConnman::RemoveDandelionPeer(CNode* pnode) +{ + if (!pnode) return; + LOCK(m_nodes_mutex); + CloseDandelionConnections(pnode); +} + void CConnman::ThreadDandelionShuffle() { LogPrintf("ThreadDandelionShuffle: Started\n"); 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/digibyte-cli.cpp b/src/digibyte-cli.cpp index 309085766c2..d800c18d1e5 100644 --- a/src/digibyte-cli.cpp +++ b/src/digibyte-cli.cpp @@ -451,6 +451,7 @@ class NetinfoRequestHandler : public BaseRequestHandler if (conn_type == "block-relay-only") return "block"; if (conn_type == "manual" || conn_type == "feeler") return conn_type; if (conn_type == "addr-fetch") return "addr"; + if (conn_type == "paymaster") return "paymaster"; return ""; } 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..a26303ca7ac 100644 --- a/src/digidollar/digidollar.cpp +++ b/src/digidollar/digidollar.cpp @@ -2,12 +2,14 @@ // 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 +#include #include // ===================================== @@ -79,32 +81,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 util::int128_t valueInCents = (static_cast(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 util::int128_t 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 +193,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..8fe930d9649 100644 --- a/src/digidollar/health.cpp +++ b/src/digidollar/health.cpp @@ -23,9 +23,11 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -41,6 +43,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; + }; + util::int128_t 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 +596,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 +612,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 +681,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 +733,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 +770,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 +785,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 +804,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 +837,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 +869,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(); - active.ForceFlushStateToDisk(); - bool complete; - { - LOCK(::cs_main); - complete = ScanUTXOSet(&active.CoinsDB(), &active.CoinsTip(), &active.m_blockman, /*mempool=*/nullptr, &active.m_chain, &chainman.GetConsensus()); + if (callbacks.cancelled && callbacks.cancelled()) { + return {HealthScanResult::Status::CANCELLED, "DigiDollar legacy health reconstruction cancelled"}; } - 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; + active.ForceFlushStateToDisk(); + return ScanUTXOSet(&active.CoinsDB(), &active.CoinsTip(), &active.m_blockman, /*mempool=*/nullptr, + &active.m_chain, &chainman.GetConsensus(), callbacks); } // ============================================================================ @@ -811,11 +1150,9 @@ int SystemHealthMonitor::CalculateSystemHealth(CAmount ddSupply, CAmount collate return 0; // Cannot calculate without valid price } - __int128 numerator = static_cast<__int128>(collateral) * - static_cast<__int128>(price) * 100; - __int128 denominator = static_cast<__int128>(COIN) * - static_cast<__int128>(ddSupply); - __int128 health = numerator / denominator; + const util::int128_t numerator = util::int128_t{collateral} * util::int128_t{price} * 100; + const util::int128_t denominator = util::int128_t{COIN} * util::int128_t{ddSupply}; + const util::int128_t health = numerator / denominator; if (health < 0) return 0; if (health > 300) return 300; @@ -965,21 +1302,21 @@ int CalculateHealthRatio(CAmount ddAmount, CAmount dgbAmount, CAmount dgbPrice) return 0; // Cannot calculate without valid price/amount } - // Calculate DGB value in cents using __int128 to prevent overflow. + // Calculate DGB value in cents using a fixed 128-bit integer to prevent overflow. // dgbPrice is in cents (100 = $1.00 DGB price) // dgbAmount is in satoshis // Formula: (satoshis * price_cents) / COIN = value_in_cents // Then health = (value_in_cents * 100) / ddAmount // - // Using __int128 is safe here because this is a monitoring/display + // Using a fixed 128-bit integer is safe here because this is a monitoring/display // function, not consensus-critical code. The consensus equivalent - // (CalculateSystemHealth) uses a divide-first pattern, but __int128 + // (CalculateSystemHealth) uses a divide-first pattern, but 128-bit arithmetic // is simpler and handles all edge cases without precision loss. - __int128 dgbValue128 = static_cast<__int128>(dgbAmount) * static_cast<__int128>(dgbPrice); + util::int128_t dgbValue128 = util::int128_t{dgbAmount} * util::int128_t{dgbPrice}; dgbValue128 /= COIN; // Health = (Collateral Value / DD Value) * 100 - __int128 health128 = (dgbValue128 * 100) / static_cast<__int128>(ddAmount); + const util::int128_t health128 = (dgbValue128 * 100) / util::int128_t{ddAmount}; // Clamp to [0, 300] if (health128 < 0) return 0; 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..59359775ccd 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,11 +166,11 @@ 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; - } catch (const std::exception& e) { + } catch (const std::exception&) { // LogPrintf("DigiDollar: Exception creating P2TR script: %s\n", e.what()); return CScript(); } @@ -209,7 +209,7 @@ CScript CreateDigiDollarP2TR(const XOnlyPubKey& owner, CAmount ddAmount) return scriptPubKey; - } catch (const std::exception& e) { + } catch (const std::exception&) { // LogPrintf("DigiDollar: Exception creating DD P2TR script: %s\n", e.what()); return CScript(); } 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..c795dc1067c 100644 --- a/src/digidollar/txbuilder.cpp +++ b/src/digidollar/txbuilder.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -36,14 +37,92 @@ 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) { return 0; } - __int128 padded128 = (static_cast<__int128>(requiredCollateral) * 101) / 100; - if (padded128 > static_cast<__int128>(MAX_MONEY)) { + util::int128_t padded128 = (static_cast(requiredCollateral) * 101) / 100; + if (padded128 > static_cast(MAX_MONEY)) { return 0; } return static_cast(padded128); @@ -55,8 +134,8 @@ int CalculatePositionCollateralRatio(CAmount dgbLocked, CAmount ddMinted, CAmoun return 0; } - const __int128 dgbValueCents = (static_cast<__int128>(dgbLocked) * price) / COIN; - const __int128 ratio = (dgbValueCents * 100) / ddMinted; + const util::int128_t dgbValueCents = (static_cast(dgbLocked) * price) / COIN; + const util::int128_t ratio = (dgbValueCents * 100) / ddMinted; if (ratio > std::numeric_limits::max()) { return std::numeric_limits::max(); } @@ -102,7 +181,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 +260,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 +274,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 @@ -205,17 +286,17 @@ CAmount MintTxBuilder::CalculateRequiredCollateral(CAmount ddAmount, int lockDay // = 15,000,000,000,000,000 / 6310 // = 2,377,179,080,509 sats = ~23,772 DGB // - // NOTE: Using __int128 to avoid uint64 overflow. Without this, minting >$18K DD + // NOTE: Using util::int128_t to avoid uint64 overflow. Without this, minting >$18K DD // at 1000% ratio (or >$36K at 500%) causes silent overflow, producing a tiny // collateral requirement and allowing massively under-collateralized positions. - __int128 numerator = static_cast<__int128>(usdValue) * static_cast<__int128>(COIN) * - static_cast<__int128>(effectiveRatio) * 100; - __int128 denominator = static_cast<__int128>(oraclePrice); - __int128 result128 = (numerator + denominator - 1) / denominator; + util::int128_t numerator = static_cast(usdValue) * static_cast(COIN) * + static_cast(effectiveRatio) * 100; + util::int128_t denominator = static_cast(oraclePrice); + util::int128_t result128 = (numerator + denominator - 1) / denominator; // Overflow guard: cap at MAX_MONEY before casting to uint64_t. // Without this, extreme values could silently truncate to near-zero. - if (result128 > static_cast<__int128>(MAX_MONEY)) { + if (result128 > static_cast(MAX_MONEY)) { return 0; // Amount too large } uint64_t requiredCollateral = static_cast(result128); @@ -227,7 +308,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 +389,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 +416,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 +431,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 +523,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)); @@ -524,24 +598,24 @@ bool TransferTxBuilder::ValidateTransferParams(const TxBuilderTransferParams& pa for (const auto& [address, amount] : params.recipients) { // Validate address format if (!ValidateDDAddress(address)) { - LogPrintf("DigiDollar: ValidateTransferParams FAILED - Invalid address: %s\n", address); + LogPrintf("DigiDollar: ValidateTransferParams FAILED - Invalid address\n"); return false; } // Validate amount ranges if (amount <= 0) { - LogPrintf("DigiDollar: ValidateTransferParams FAILED - Non-positive amount: %d\n", amount); + LogPrintf("DigiDollar: ValidateTransferParams FAILED - Non-positive amount\n"); return false; // No zero or negative amounts } if (amount < minOutput) { - LogPrintf("DigiDollar: ValidateTransferParams FAILED - Below dust threshold: %d < %d\n", amount, minOutput); + LogPrintf("DigiDollar: ValidateTransferParams FAILED - Below dust threshold\n"); return false; // Below dust threshold } // Check maximum single transfer limit ($100,000) if (amount > 10000000) { // $100,000.00 in cents - LogPrintf("DigiDollar: ValidateTransferParams FAILED - Exceeds max transfer: %d > 10000000\n", amount); + LogPrintf("DigiDollar: ValidateTransferParams FAILED - Exceeds max transfer\n"); return false; } @@ -562,11 +636,11 @@ bool TransferTxBuilder::ValidateTransferParams(const TxBuilderTransferParams& pa // Validate fee rate if (!ValidateFeeRate(params.feeRate)) { - LogPrintf("DigiDollar: ValidateTransferParams FAILED - Invalid fee rate: %d\n", params.feeRate); + LogPrintf("DigiDollar: ValidateTransferParams FAILED - Invalid fee rate\n"); return false; } - LogPrintf("DigiDollar: ValidateTransferParams PASSED\n"); + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: ValidateTransferParams PASSED\n"); return true; } @@ -687,8 +761,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", - params.feeUtxos.size(), params.feeAmounts.size()); + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: TxBuilder - adding fee inputs\n"); CAmount totalFeeIn = 0; for (size_t i = 0; i < params.feeUtxos.size(); ++i) { @@ -696,17 +769,12 @@ 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", - i, (i < params.feeAmounts.size()) ? "feeAmounts[i]" : "GetDGBFromUTXO()", feeAmount); totalFeeIn += feeAmount; - LogPrintf("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 - adding recipient outputs\n"); for (const auto& [address, amount] : params.recipients) { - LogPrintf("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 +793,6 @@ 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); } // Add DD change output if needed — EVERY cent of DD must be accounted for. @@ -756,7 +823,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\n"); } 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,8 +835,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", - calculatedFee, MIN_DD_TX_FEE, actualFee); + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Fee calculation completed\n"); if (totalFeeIn <= 0) { result.error = "Insufficient DGB fee input: no fee inputs selected"; @@ -786,21 +852,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\n"); } } @@ -825,7 +887,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 DD transfer metadata\n"); // Final validation - ensure DD conservation // DD amounts are now stored in OP_RETURN, so sum up ddOutputAmounts instead @@ -834,8 +896,7 @@ TxBuilderResult TransferTxBuilder::BuildTransferTransaction(const TxBuilderTrans finalDDOut += amt; } - LogPrintf("DigiDollar: Conservation check - Input: %d cents, Output: %d cents\n", - totalDDIn, finalDDOut); + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Conservation check completed\n"); // DD conservation is absolute — every cent in must equal every cent out. // DD change outputs are always created (no dust exception), so strict equality holds. @@ -906,8 +967,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", - tx.vin.size(), tx.vout.size(), tx.nVersion, tx.nLockTime); + LogPrint(BCLog::DIGIDOLLAR, "DigiDollar: Transaction finalized\n"); return result; } @@ -937,6 +997,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 +1153,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 +1162,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 +1186,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 +1220,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 +1240,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 +1249,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 +1264,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 +1282,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 +1291,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 +1314,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 +1337,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 +1354,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 +1389,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 +1424,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 +1616,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 +1625,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 +1646,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 +1662,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..2cd894e5030 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 @@ -22,6 +23,7 @@ using DigiDollar::GetScriptMetadata; #include #include #include +#include #include #include @@ -50,7 +52,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 +80,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 +207,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 +237,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; @@ -422,6 +512,15 @@ static bool ExtractDDAmountFromTxRef(const CTransactionRef& prev_tx, const COutP return false; } +bool ExtractDDAmountFromTransaction(const CTransaction& tx, const COutPoint& prevout, CAmount& amount) +{ + amount = 0; + // Paymaster peers supply the creating transaction explicitly. Bind it to + // the advertised outpoint before using the unchanged upstream parser. + if (tx.GetHash() != prevout.hash) return false; + return ExtractDDAmountFromTxRef(MakeTransactionRef(tx), prevout, amount); +} + bool ExtractDDAmountFromPrevTx(const COutPoint& prevout, CAmount& amount) { amount = 0; @@ -463,7 +562,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 +573,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 +693,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 +762,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 +856,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 +882,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 +958,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; } @@ -874,15 +975,15 @@ CAmount CalculateRequiredCollateral(CAmount ddAmount, int64_t lockTime, // = (10000 * 100000000 * 150 * 100) / 6310 // = 15,000,000,000,000,000 / 6310 // = 2,377,179,080,509 sats = ~23,772 DGB - // Use __int128 to avoid uint64 overflow for large DD amounts (overflows at ~$18K@1000%) - __int128 numerator = static_cast<__int128>(ddAmount) * static_cast<__int128>(COIN) * - static_cast<__int128>(effectiveRatio) * 100; - __int128 denominator = static_cast<__int128>(ctx.oraclePriceMicroUSD); - __int128 result = (numerator + denominator - 1) / denominator; + // Use util::int128_t to avoid uint64 overflow for large DD amounts (overflows at ~$18K@1000%) + util::int128_t numerator = static_cast(ddAmount) * static_cast(COIN) * + static_cast(effectiveRatio) * 100; + util::int128_t denominator = static_cast(ctx.oraclePriceMicroUSD); + util::int128_t result = (numerator + denominator - 1) / denominator; // Fail closed when the economically required collateral is not // representable as a valid DGB amount. Capping at MAX_MONEY would accept // a mint with less collateral than the formula requires. - if (result > static_cast<__int128>(MAX_MONEY)) { + if (result > static_cast(MAX_MONEY)) { return 0; } CAmount requiredDGB = static_cast(result); @@ -896,10 +997,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); @@ -941,16 +1044,16 @@ bool ValidateCollateralRatio(CAmount dgbLocked, CAmount ddMinted, // Oracle price is in micro-USD (1,000,000 = $1.00), DD is in cents // Convert: (DGB_sats * oracle_micro_usd / COIN) = micro-USD value // Then: micro-USD / 10000 = cents - // Use __int128 to prevent overflow when dgbLocked and oraclePrice are both large - __int128 dgbValueMicroUSD128 = static_cast<__int128>(dgbLocked) * static_cast<__int128>(ctx.oraclePriceMicroUSD); + // Use util::int128_t to prevent overflow when dgbLocked and oraclePrice are both large + util::int128_t dgbValueMicroUSD128 = static_cast(dgbLocked) * static_cast(ctx.oraclePriceMicroUSD); dgbValueMicroUSD128 /= COIN; CAmount dgbValueMicroUSD = (dgbValueMicroUSD128 > std::numeric_limits::max()) ? std::numeric_limits::max() : static_cast(dgbValueMicroUSD128); CAmount dgbValueInCents = dgbValueMicroUSD / 10000; // Convert micro-USD to cents // Clamp before cast to int to avoid overflow with large ratio values - __int128 actualRatio128 = ddMinted > 0 - ? (static_cast<__int128>(dgbValueInCents) * 100) / static_cast<__int128>(ddMinted) + util::int128_t actualRatio128 = ddMinted > 0 + ? (static_cast(dgbValueInCents) * 100) / static_cast(ddMinted) : 0; if (actualRatio128 > 100000) { actualRatio128 = 100000; @@ -960,22 +1063,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 +1087,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 +1105,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 +1113,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 +1134,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 +1145,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 +1179,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 +1206,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 +1219,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 +1258,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 +1305,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 +1376,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 +1398,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 +1424,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 +1447,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 +1517,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 +1579,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 +1587,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 util::int128_t estimate = (static_cast(totalCollateral) * oraclePrice) + / (static_cast(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 +1665,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 +1683,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 +1707,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 +1759,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 +1788,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 +1929,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 +1971,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 +2026,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 +2036,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 +2047,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 +2092,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 +2112,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 +2250,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 +2268,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 +2316,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 +2349,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 +2371,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 +2396,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 +2432,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 +2501,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 +2604,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 +2628,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 +2724,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); @@ -2610,14 +2754,14 @@ bool ValidateCollateralReleaseAmount(const CTransaction& tx, // Regular DGB fee inputs are additive funding for fees/change. They must not // reduce the amount of locked collateral the redemption is required to return. - const __int128 maxAllowedDGBOutputs = static_cast<__int128>(allowedRelease) + - static_cast<__int128>(totalFeeInputs); + const util::int128_t maxAllowedDGBOutputs = static_cast(allowedRelease) + + static_cast(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); - if (static_cast<__int128>(totalDGBOutputs) > maxAllowedDGBOutputs) { + if (static_cast(totalDGBOutputs) > maxAllowedDGBOutputs) { LogPrintf("DigiDollar: Collateral release too large - outputs: %lld, allowed collateral: %lld, fee inputs: %lld, locked: %lld, ddBurned: %lld, originalDD: %lld\n", (long long)totalDGBOutputs, (long long)allowedRelease, (long long)totalFeeInputs, @@ -2638,7 +2782,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 +2800,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 +2811,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 +2868,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 +2967,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 +2995,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 +3037,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 +3107,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 +3126,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 +3188,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..85c6b80b48b 100644 --- a/src/digidollar/validation.h +++ b/src/digidollar/validation.h @@ -10,6 +10,7 @@ #include