Skip to content

v9.26.0-rc24: Wallet RPC Correctness Release#390

Open
DigiSwarm wants to merge 850 commits intodevelopfrom
release/v9.26.0-rc24
Open

v9.26.0-rc24: Wallet RPC Correctness Release#390
DigiSwarm wants to merge 850 commits intodevelopfrom
release/v9.26.0-rc24

Conversation

@DigiSwarm
Copy link

RC24 Wallet RPC Correctness Release

This PR contains all changes since RC23, focused on replacing mock/placeholder data in DigiDollar RPC commands with real wallet queries and fixing the critical redemption fee estimation bug.

Bug Fixes

Tests

  • 1,966 C++ unit tests passing
  • All 30 DigiDollar/Oracle RPCs verified on live testnet19
  • 20+ new regression tests

See RELEASE_v9.26.0-rc24.md for full release notes.

…ove stale refs

- Fix Phase 2 consensus table: testnet is 5-of-8 (not 3-of-5),
  regtest is 4-of-7, activation heights 600/650 (not 100)
- Fix nDDActivationHeight mainnet: 22,014,720 (not 22,000,000)
- Fix oracle fetch/broadcast interval: 60 seconds (not 15)
- Remove CoinMarketCap from config and exchange sources (removed in RC15)
- Remove all RC12 references — guide is now version-neutral
- Add loadwallet to operator workflow overview
- Add jump-to table of contents with links to all sections
- Fix digibyte.conf: add testnet=1 at top, digidollar=1 under [test],
  addnode=oracle1.digibyte.io, separate optional settings
- Remove hardcoded port/rpcport (defaults are correct)
- Add clear note about testnet=1 placement
Mainnet activation height is determined by BIP9 signaling, not a
hardcoded value. Changed to 'BIP9-activated (TBD)' in the consensus
parameters table.
…ete RPC reference

Major rewrite of DIGIDOLLAR_ORACLE_SETUP.md:

- Eliminated duplicate content (upgrade/restart info was repeated 3x)
- Simplified from 628 to 500 lines while adding more content
- Three clear paths: New Setup, Upgrading, Restarting
- Complete RPC reference: all 31 oracle + digidollar + mock RPCs
  documented with usage examples
- Accurate consensus parameters verified against chainparams.cpp
- Removed stale references (CoinMarketCap, old RC versions)
- Clean table of contents with jump links
- Consolidated troubleshooting into single table
═══════════════════════════════════════════════════════════════════
WHAT IS DANDELION++ (Simple Terms)
═══════════════════════════════════════════════════════════════════

Dandelion++ is a privacy protocol for broadcasting transactions.
Instead of immediately telling every peer about a new transaction
(which makes it easy to trace who sent it), Dandelion has two phases:

  STEM PHASE: The transaction is secretly passed along a single
  chain of nodes, like whispering a secret down a line of people.
  Each node passes it to exactly one other node.

  FLUFF PHASE: At some random point, one node "fluffs" the
  transaction — broadcasting it to everyone normally. By this point,
  nobody can tell which node originally created the transaction.

Think of it like passing a note in class: you hand it to one person,
who hands it to another, who hands it to another... then at some
random point, someone reads it out loud. Nobody knows who wrote it.

═══════════════════════════════════════════════════════════════════
WHAT WENT WRONG (Simple Terms)
═══════════════════════════════════════════════════════════════════

Imagine you are passing that note in class. The bugs were:

1. SAYING IT TWICE: When you handed the note to the next person,
   you also read it out loud at the same time. Then the system that
   processes your "to-do list" of notes to pass also passed it again.
   So the next person got the same note twice every time.

2. WRONG CHECKLIST: Before passing a note, you check a list to see
   if you already passed this note. But you were checking your
   HOMEWORK checklist instead of your NOTE-PASSING checklist. The
   note was never on the homework checklist, so you passed it again
   and again forever.

3. NAME vs NICKNAME: Each note has two names — a formal name (txid)
   and a nickname (wtxid). You wrote down the nickname on your
   checklist, but kept checking the formal name. They never matched,
   so the note was never recognized as "already passed."

4. DOUBLE MESSAGE: When passing the note, you also sent an
   announcement saying "I have a note!" The next person would then
   ask "give me the note!" even though they already had it.

Result: The same transaction was being sent to the same peer hundreds
of times per second, generating 1 MB/sec of log output and wasting
bandwidth.

═══════════════════════════════════════════════════════════════════
CORRECT DANDELION TX FLOW (After Fix)
═══════════════════════════════════════════════════════════════════

Step-by-step flow when a Dandelion transaction arrives:

  Node A (sender) ──DANDELIONTX──> Our Node ──DANDELIONTX──> Node C

  1. RECEIVE: Our node receives a DANDELIONTX message from Node A
     └─ ProcessMessage(NetMsgType::DANDELIONTX)
     └─ File: src/net_processing.cpp ~line 5320

  2. ACCEPT TO STEMPOOL: Transaction is validated and stored in the
     stempool (a private holding area, separate from the mempool).
     An embargo timer is set (10-30 seconds). If the TX does not
     get confirmed via normal broadcast before the embargo expires,
     our node will "fluff" it ourselves as a safety net.
     └─ AcceptToMemoryPoolForStempool()
     └─ insertDandelionEmbargo()

  3. RELAY DECISION: RelayDandelionTransaction() is called.
     A random 10% chance decides: fluff now, or continue stem?

     ┌─ 10% FLUFF: Move TX from stempool to mempool, broadcast
     │  normally via RelayTransaction() to all peers. Done.
     │
     └─ 90% STEM: Continue passing along the secret chain.
        └─ getDandelionDestination(pfrom) picks the next node
           (deterministic per-source routing from mDandelionRoutes)
        └─ PushDandelionInventory(destination, inv) queues the
           txid for delivery in the next SendMessages() cycle

  4. QUEUE CHECK (PushDandelionInventory):
     Before queuing, checks TWO things:
     a) Is txid already in setDandelionInventoryKnown? (already sent)
     b) Is txid already in vInventoryDandelionTxToSend? (already queued)
     If either is true, SKIP — prevents duplicate queuing.
     └─ File: src/net_processing.cpp ~line 1718

  5. SEND (SendMessages):
     Next message cycle for the destination peer:
     a) Pull txid from vInventoryDandelionTxToSend
     b) Look up full TX in stempool
     c) Send full DANDELIONTX message to peer (no INV needed —
        the TX is embargoed so it cannot be fetched via GETDATA)
     d) Mark BOTH txid AND wtxid in setDandelionInventoryKnown
        (prevents re-send regardless of which hash form is used)
     e) Clear the queue
     └─ File: src/net_processing.cpp ~line 6563

  6. EMBARGO SAFETY NET (CheckDandelionEmbargoes):
     Runs every second. If embargo timer expires and the TX is
     still in the stempool (not yet in mempool), fluff it:
     move to mempool and broadcast normally.
     └─ File: src/net_processing.cpp ~line 1627

  7. ROUTE SHUFFLING (ThreadDandelionShuffle):
     Periodically reshuffles all Dandelion routes so the stem
     path changes over time (prevents long-term correlation).
     └─ File: src/dandelion.cpp

═══════════════════════════════════════════════════════════════════
WHAT WAS FIXED (Technical Detail)
═══════════════════════════════════════════════════════════════════

Bug 1 — DOUBLE-SEND in RelayDandelionTransaction
─────────────────────────────────────────────────
BEFORE: RelayDandelionTransaction() did TWO things:
  a) PushDandelionInventory(dest, inv)  → queued for SendMessages
  b) PushMessage(dest, DANDELIONTX, tx) → sent immediately
Then SendMessages processed the queue and sent it AGAIN.
Result: Every stem TX was sent twice to the same peer.

AFTER: RelayDandelionTransaction() only calls PushDandelionInventory().
SendMessages is the single path for all Dandelion TX delivery.

Bug 2 — WRONG DUPLICATE FILTER in PushDandelionInventory
────────────────────────────────────────────────────────
BEFORE: Checked m_tx_inventory_known_filter (CRollingBloomFilter
for regular TX relay). Dandelion TXs were never inserted into this
filter — they used setDandelionInventoryKnown instead. The check
always passed, so the same TX was re-queued every cycle.

AFTER: Checks setDandelionInventoryKnown (the correct Dandelion
tracking set). Also checks if already in the queue to prevent
duplicate entries within the same cycle.

Bug 3 — TXID/WTXID MISMATCH in setDandelionInventoryKnown
─────────────────────────────────────────────────────────
BEFORE: SendMessages inserted known_hash (wtxid when peer uses
wtxid relay) into setDandelionInventoryKnown. But
PushDandelionInventory queued and checked by txid. For segwit
transactions, txid != wtxid, so the "already known" check never
matched.

AFTER: SendMessages inserts BOTH the txid AND the wtxid into
setDandelionInventoryKnown. Either hash form will be caught.

Bug 4 — REDUNDANT INV WITH FULL TX
──────────────────────────────────
BEFORE: SendMessages sent both a Dandelion INV (via vInv vector)
AND the full DANDELIONTX message for the same transaction. The INV
caused the receiving peer to send GETDATA for a TX it already had.

AFTER: Only sends the full DANDELIONTX. No INV. The TX is embargoed
in the stempool and cannot be served via GETDATA anyway, so the INV
was always pointless for stem-phase transactions.

═══════════════════════════════════════════════════════════════════
LOG SPAM FIX
═══════════════════════════════════════════════════════════════════

All Dandelion hot-path log messages in SendMessages and ProcessMessage
were using LogPrintf (always logged). Changed to
LogPrint(BCLog::DANDELION) so they only appear with -debug=dandelion.
These messages fire every SendMessages cycle (~100ms) for every peer
with pending Dandelion TXs — even without the relay loop, they would
generate significant log volume on a busy node.

═══════════════════════════════════════════════════════════════════
TESTING
═══════════════════════════════════════════════════════════════════

- Added Test 4 to p2p_dandelion.py: sends a TX through Dandelion
  stem, waits 5 seconds, then checks debug.log to verify the same
  TX was not sent to the same peer more than twice. Catches the
  infinite relay loop regression.
- All 4 Dandelion functional tests pass (active probing resistance,
  loop behavior, black hole resistance, no duplicate relay)
- All P2P tests pass (p2p_tx_privacy, mempool_unbroadcast,
  wallet_resendwallettransactions, rpc_packages, mempool_limit)
- All DigiDollar tests pass (digidollar_network_relay,
  digidollar_transfer)
- C++ dandelion_tests unit tests pass
- Enabled -debug=dandelion in test args for better test coverage

═══════════════════════════════════════════════════════════════════
FILES CHANGED
═══════════════════════════════════════════════════════════════════

src/net_processing.cpp:
  - RelayDandelionTransaction(): Removed immediate PushMessage
  - PushDandelionInventory(): Check setDandelionInventoryKnown +
    queue dedup instead of m_tx_inventory_known_filter
  - SendMessages(): Insert both txid+wtxid into known set; remove
    redundant INV for stem TXs; demote LogPrintf to LogPrint
  - ProcessMessage(): Demote dandelion INV LogPrintf to LogPrint

test/functional/p2p_dandelion.py:
  - Added Test 4: no duplicate Dandelion relay regression test
  - Enabled -debug=dandelion in extra_args
Root cause: All algo initialTargets were set to >> 28 (256x harder than
powLimit). DigiShield V4's per-algo adjustment (4% per missed block) could
never bridge the 256x gap in a single retarget, so all algos were permanently
stuck at the initial difficulty. SHA256D dominated (93% of blocks) because
it's the fastest to hash on CPUs at equal difficulty.

Fixes:
- Remove explicit initialTarget for all algos except SHA256D
- SHA256D starts at >> 25 (32x harder than powLimit) to prevent CPU dominance
- All other algos (Scrypt, Skein, Qubit, Odo) default to powLimit
- DigiShield V4 retargeting remains active (matches mainnet behavior)

Testnet reset (incompatible with previous chain):
- Data directory: testnet13 -> testnet17
- P2P port: 12030 -> 12031
- Oracle endpoints updated to port 12031

Version bump to v9.26.0-rc17 with updated wallet splash image.

Compared against v8.22.1 working testnet settings to identify the issue.
v8.22.1 only set initialTarget for Odo (>> 36), all other algos used powLimit.
Replace ALWAYS_ACTIVE with real BIP9 signaling for DigiDollar deployment
on testnet. This enables proper testing of the full activation lifecycle
that will occur on mainnet.

BIP9 Activation Sequence (nMinerConfirmationWindow=200, threshold=70%):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  Window 0 │ Blocks 0-199   │ DEFINED
           │                │ Chain bootstraps, miners come online
           │                │ MTP has not yet reached nStartTime
  ─────────┼────────────────┼──────────────────────────────────────
  Window 1 │ Blocks 200-399 │ STARTED
           │                │ MTP >= genesis timestamp (nStartTime)
           │                │ Miners signal bit 23 in block version
           │                │ Need 140/200 blocks (70%) to lock in
  ─────────┼────────────────┼──────────────────────────────────────
  Window 2 │ Blocks 400-599 │ LOCKED_IN
           │                │ Threshold met in previous window
           │                │ Activation is guaranteed but delayed
           │                │ by min_activation_height=600
  ─────────┼────────────────┼──────────────────────────────────────
  Window 3 │ Blocks 600+    │ ACTIVE
           │                │ min_activation_height=600 satisfied
           │                │ DigiDollar consensus rules enforced
           │                │ Oracle system activates (height 600)

How it works:
- nStartTime set to genesis timestamp (1763932527, Nov 23 2025)
- Since genesis is in the past, MTP exceeds nStartTime at the first
  window boundary (block 200), transitioning from DEFINED to STARTED
- Miners must signal bit 23 in their block version field
- 140 out of 200 blocks (70%) must signal to achieve LOCKED_IN
- min_activation_height=600 prevents premature activation even if
  lock-in happens earlier (at block 400)
- Oracle activation (nOracleActivationHeight=600) and DigiDollar
  Phase 2 (nDigiDollarPhase2Height=600) remain aligned at block 600

This matches the mainnet activation pattern where nStartTime defines
when signaling begins and min_activation_height gates the actual
activation, ensuring all node operators have time to upgrade.

Previously: ALWAYS_ACTIVE from block 0 (no signaling tested)
Added IsDigiDollarEnabled() activation gate to 16 additional RPCs that
were previously accessible before DigiDollar activation. All DD
functionality is now 100% blocked until BIP9 status reaches ACTIVE.

RPCs gated (return 'DigiDollar is not yet activated on this network'):
  - calculatecollateralrequirement  - estimatecollateral
  - getdigidollaraddress            - getdigidollarbalance
  - listdigidollarpositions         - listdigidollartxs
  - listdigidollaraddresses         - importdigidollaraddress
  - getredemptioninfo               - getoracleprice
  - getprotectionstatus             - sendoracleprice
  - createoraclekey                 - stoporacle
  - submitoracleprice               - simulatepricevolatility

Previously gated (unchanged):
  - mintdigidollar                  - redeemdigidollar
  - senddigidollar                  - startoracle

Intentionally NOT gated (informational/config only):
  - getdigidollardeploymentinfo     - shows BIP9 status
  - getdigidollarstats              - returns zeros when inactive
  - getoracles / listoracle         - shows oracle configuration
  - validateddaddress               - address format utility
  - getdcamultiplier                - shows multiplier config
  - getalloracleprices              - shows price history

Two gate patterns used depending on RPC registration context:
  - Wallet RPCs: GetWalletForJSONRPCRequest → chain().context()
  - Server RPCs: EnsureAnyNodeContext → EnsureChainman
DigiDollar tab is now always visible in Qt regardless of BIP9 status.
Before activation, clicking the tab shows a centered status message:

  'DigiDollar is not yet active on this network.
   BIP9 Deployment Status: DEFINED/STARTED/LOCKED_IN'

Once BIP9 reaches ACTIVE, the overlay disappears and full DD
functionality (overview, send, receive, mint, redeem, vault,
transactions) becomes available.

Implementation:
- QStackedWidget switches between activation label and DD tab widget
- Timer checks IsDigiDollarEnabled() every 5 seconds
- Timer stops once activation is detected
- Status label shows current BIP9 deployment phase
- Previously: tab was hidden entirely when not ALWAYS_ACTIVE
DigiDollar tab is now always visible in Qt regardless of BIP9 status.
Before activation, clicking the tab shows a centered status message:

  'DigiDollar is not yet active on this network.
   BIP9 Deployment Status: DEFINED/STARTED/LOCKED_IN'

Once BIP9 reaches ACTIVE, the overlay disappears and full DD
functionality (overview, send, receive, mint, redeem, vault,
transactions) becomes available.

Implementation:
- QStackedWidget switches between activation label and DD tab widget
- Timer checks IsDigiDollarEnabled() every 5 seconds
- Timer stops once activation is detected
- Status label shows current BIP9 deployment phase
- Previously: tab was hidden entirely when not ALWAYS_ACTIVE
Removed explicit initialTarget for SHA256D (was >> 25, 32x harder than
powLimit). Now defaults to powLimit via InitialDifficulty() like all
other algos except on regtest.

The >> 25 target was too hard for CPU mining — cpuminer at 350 Mhash/s
had 0% acceptance rate. The v8.22.1 working testnet never set SHA256D
initialTarget either; only Odo had one.
After sending DigiDollar, the change UTXO is unconfirmed until the next
block (~15 sec). GetTotalDDBalance() only returns confirmed UTXOs, so the
send widget's validateBalance() sees 0 balance and shows a yellow warning
border on the amount field. Users cannot send DD consecutively.

Fix: Add getPendingDigiDollarBalance() (trusted unconfirmed change/mints)
to the confirmed balance in updateBalance(). This mirrors how mainnet DGB
treats trusted unconfirmed change as spendable via CachedTxIsTrusted.

No double-counting: GetTotalDDBalance() skips all depth<1 UTXOs, while
GetPendingDDBalance() only counts depth<1 trusted ones — disjoint sets.
Untrusted unconfirmed UTXOs remain excluded from both.
…uide

Comprehensive document explaining the entire DigiDollar activation
process via BIP9 version bit signaling:

- BIP9 state machine (DEFINED → STARTED → LOCKED_IN → ACTIVE)
- All deployment parameters (mainnet, testnet17, regtest)
- Complete list of 28 gated RPCs with gate patterns
- P2P oracle message gating (ORACLEPRICE, ORACLEBUNDLE, GETORACLES)
- Consensus validation gates (mempool, block, script flags)
- Qt GUI activation overlay behavior
- Miner signaling mechanics (gbt_force=true)
- Block version bit format
- Manual testing checklist
- Mainnet activation timeline
- Security considerations
- File reference table for all activation-related code
When sending DigiDollar, the transaction contains both DD token inputs
(0-value P2TR) and DGB fee inputs. The DD token inputs may not be
recognized as ISMINE_SPENDABLE by the standard wallet, causing
fAllFromMe to be false while any_from_me is true (from the DGB fee
inputs). This made the transaction fall through to the 'mixed debit'
code path, producing an 'Other' type record that displayed as '(n/a)'
in the transaction list.

Fix: Add special handling for DD_TX_TRANSFER transactions before the
fAllFromMe check (similar to existing DD_TX_REDEEM handling). This
decomposes the transaction into proper DDSend records for the DD token
outputs and a new DDSendFee record for the DGB fee portion.

Changes:
- transactionrecord.h: Add DDSendFee to TransactionRecord::Type enum
- transactionrecord.cpp: Add DD_TX_TRANSFER early-exit path in
  decomposeTransaction() that creates DDSend + DDSendFee records
- transactiontablemodel.cpp: Add 'DigiDollar Transfer Fee' display
  string, output icon, and gold color for DDSendFee type
getdigidollardeploymentinfo was gated behind IsDigiDollarEnabled() which
made it impossible to call before activation. This RPC exists specifically
to let users monitor BIP9 deployment progress (DEFINED → STARTED →
LOCKED_IN → ACTIVE). Gating it defeats its purpose — users need to see
signaling progress BEFORE activation happens.

Fix: Remove activation gate from getdigidollardeploymentinfo. It already
checks and reports the enabled/status fields correctly at any state.
All other 27 DD/Oracle RPCs remain gated.

Also fixes digidollar_activation_boundary.py test which:
1. Called getdigidollardeploymentinfo pre-activation (now works)
2. Asserted 'not yet activated' but error says 'not yet active' (fixed)

Both digidollar_activation.py and digidollar_activation_boundary.py
now pass cleanly through all BIP9 phases.
1. Transfer conservation bypass (HIGH): When all DD amount lookup methods
   failed (metadata miss + no coins view + no txLookup), the validation
   fell back to inputDD = outputDD, silently bypassing the conservation
   check. An attacker who could cause lookup failure could inflate DD
   supply via transfer transactions. Fix: Reject the transaction instead
   of assuming conservation. A consensus rule must never be soft-bypassed.

2. Collateral calculation overflow (LOW but dangerous): The __int128
   numerator divided correctly, but the cast to uint64_t silently
   truncated results exceeding 2^64. At extreme values (max DD amount +
   min oracle price + max collateral ratio), required collateral would
   wrap to near-zero, allowing mints with almost no collateral. Fix: Cap
   result at MAX_MONEY before casting.

3. Floating-point in consensus code (MEDIUM): Partial redemption
   collateral release used double division: (double)ddBurned /
   (double)originalDDMinted * (double)lockedCollateral. IEEE 754
   imprecision in consensus code is unacceptable — different platforms
   could disagree on the result, causing chain splits. Fix: Replace with
   integer math using __int128 to prevent overflow.

All 1539 unit tests pass. Updated bug8 test to expect rejection instead
of the old silent fallback behavior.
The test_mint_after_unlock test was failing because mintdigidollar on
node 1 produced a TX that hadn't relayed to node 0's mempool before
node 0 mined a block. The mined block was empty, so the DD UTXO
remained unconfirmed, causing senddigidollar to report 0 balance.

Fix: Add sync_mempools() after mint to ensure the TX is in node 0's
mempool before mining. This was a test timing issue, not a wallet bug.

All 31/31 DigiDollar functional tests now pass.
Verifies all 27 gated DigiDollar and Oracle RPCs return 'not yet active'
before BIP9 activation, while getdigidollardeploymentinfo (ungated) works
at any time. After activation, confirms key RPCs become functional.
Same overflow pattern as the consensus validation fix (370d8cb) but
in the wallet-side TxBuilder::CalculateRequiredCollateral(). The old
code cast __int128 division result directly to uint64_t, which silently
truncates if the result exceeds 2^64. The existing MAX_MONEY check came
AFTER the cast, so it could never catch the overflow.

Fix: Compare __int128 result against MAX_MONEY BEFORE casting. Return 0
(amount too large) if it exceeds MAX_MONEY.

Both consensus validation and TxBuilder now use the same safe pattern.
Same overflow pattern found in two more locations:
1. calculatecollateralrequirement RPC (line 587)
2. estimatecollateral RPC (line 2225)

Both used direct static_cast<uint64_t> on __int128 division result,
which silently truncates if result exceeds 2^64. At extreme values
(max DD amount + min oracle price + max ratio), required collateral
wraps to near-zero.

Fix: Compare __int128 result against MAX_MONEY before casting. Throw
RPC_INVALID_PARAMETER error if collateral exceeds maximum money supply.

This completes the overflow audit — all 4 collateral calculation paths
now have the safe pattern:
1. consensus validation (370d8cb)
2. TxBuilder (8a16912)
3. calculatecollateralrequirement RPC (this commit)
4. estimatecollateral RPC (this commit)
After a successful DD send, the change UTXO is pending until confirmed.
updateBalance() refreshes the available balance but did not re-run
updateAmountValidation(), leaving a stale yellow/warning border on
the amount field even after balance was sufficient.

Call updateAmountValidation() and updateSendButton() at the end of
updateBalance() so the border color tracks the current balance.

Reported-by: Bastian (Gitter)
On testnet, SHA256D is dramatically faster than other algorithms on
CPUs (~180 Mhash/s on 12 cores vs ~300 Khash/s for scrypt). Without
an explicit initialTarget, SHA256D defaults to powLimit (>> 20) which
produces instant blocks — mining thousands of blocks per minute.

Tested multiple difficulty levels empirically with cpuminer on 12 cores:
- >> 25: Too hard, 0% block acceptance rate (from previous testing)
- >> 28: Too easy, instant blocks (256x powLimit)
- >> 30: Still too fast, ~4-5 second block times (1024x powLimit)
- >> 32: Too slow, ~22 second block times (4096x powLimit)
- >> 31: Sweet spot, ~8-12 second blocks (2048x powLimit)

DigiShield real-time difficulty adjustment handles ongoing regulation
after the initial target is set, but the starting point matters for
getting the chain bootstrapped at a reasonable pace.

Note: Multi-algo activates at block 100 on testnet. Blocks 0-99 are
scrypt-only, so this setting only takes effect after block 100.
GetTotalDDBalance() and GetDDUTXOs() previously skipped ALL unconfirmed
DD UTXOs, requiring a block confirmation between consecutive sends.
This forced users to tab out and back into the DD Send tab (which
triggered updateView→updateBalance after the change confirmed).

Now trusted unconfirmed DD UTXOs (our own change/mint outputs) are
included as spendable, matching how DGB handles trusted unconfirmed
change. GetPendingDDBalance() updated to only count untrusted
unconfirmed to avoid double-counting in the UI.

Fixes: Cannot send multiple DD amounts without tabbing out and back in.
Reported-by: Bastian (Gitter)
Previously, the Qt transaction list summed ALL wallet-owned outputs
from a DigiDollar REDEEM transaction into a single 'Collateral Unlock'
line. This included both the collateral return (vout[0]) and DGB fee
change from later outputs. The combined total made it appear that the
user received MORE DGB back than they originally locked as collateral.

For example, if a user locked 242,130.75 DGB and the fee input produced
5,000 DGB in change, the wallet displayed ~247,130 DGB — confusing
testers into thinking there was a consensus bug.

Fix: Display the collateral return (vout[0]) and fee change as separate
transaction records. The collateral line shows the exact locked amount
returned, while any DGB fee change appears as a standard receive entry.

The underlying transaction was always correct — collateral and fee
change are built as separate outputs to different addresses (txbuilder
lines 1146 and 1234). This was purely a display issue.

Reported-by: Green Candle (Gitter tester)
All 1,539 C++ unit tests pass.
All 12 Qt widget tests pass.
CBlockIndex::GetAlgo() had a hardcoded check: if (nHeight < 145000)
return ALGO_SCRYPT. This used the mainnet multi-algo activation height
(145,000) for ALL networks, including testnet (activates at 100) and
regtest (activates at 100).

On testnet/regtest, this meant every block below height 145,000 was
treated as Scrypt regardless of its actual algorithm. This broke the
lastAlgoBlocks[] index — during chain load, only lastAlgoBlocks[SCRYPT]
was populated; all other algo slots remained NULL.

The cascade effect:
1. lastAlgoBlocks[QUBIT/SKEIN/SHA256D/ODO] = NULL for all blocks
2. GetLastBlockIndexForAlgoFast() returned NULL for non-Scrypt algos
3. DigiShield V4 (GetNextWorkRequiredV4) hit the early exit and
   returned InitialDifficulty() = powLimit for those algos
4. Difficulty NEVER adjusted for Qubit, Skein, SHA256D, or Odocrypt
   on any testnet/regtest chain below height 145,000

The fix removes the height check entirely. Pre-multi-algo blocks on all
networks have version bits that naturally parse as BLOCK_VERSION_SCRYPT
(algo bits = 0x0000), so the check was redundant for mainnet. The
switch statement alone correctly identifies all algorithms on all
networks at any height.

Mainnet is unaffected — pre-multi-algo blocks (version 1, 2, or
0x20000002) all have algo bits = 0x0000 = BLOCK_VERSION_SCRYPT, which
the switch handles identically to the old height check.

This bug has existed since the v8.22 -> v8.26 rebase when
CBlockIndex::GetAlgo() was added as a separate function from
CBlockHeader::GetAlgo() (which correctly parses version bits without
any height check).

Verified: all 1,539 C++ unit tests pass, 12 Qt tests pass,
feature_digibyte_multialgo_mining.py functional test passes.

Reported-by: DanGB (Gitter tester)
Co-investigated-by: Jared Tate
Add 7 new Qt tests for DigiDollar privacy/mask values support:
- privacyTabSetPrivacySlotTests: Verify DigiDollarTab has setPrivacy slot
- privacyOverviewMaskTests: Verify balance masking with # and tx list hiding
- privacySendMaskTests: Verify available balance masking
- privacyMintMaskTests: Verify DGB balance and collateral masking
- privacyRedeemMaskTests: Verify position value masking
- privacyPositionsMaskTests: Verify positions table hiding
- privacyTransactionsMaskTests: Verify transactions table hiding
- privacySignalPropagationTests: Verify privacy propagates from tab to all sub-widgets

Tests are written first (TDD approach) - they will fail until
the implementation is added in the next commit.
Implement the 'Mask Values' privacy feature for all DigiDollar Qt
widgets, matching the existing DGB OverviewPage pattern exactly.

Signal chain: DigiByteGUI → WalletView → DigiDollarTab → all sub-widgets

Changes per widget:
- DigiDollarTab: Add setPrivacy(bool) slot that relays to all sub-widgets
- DigiDollarOverviewWidget: Mask DD balance, pending, DGB collateral,
  USD value, network DD supply, network collateral. Hide recent
  transactions list when masked.
- DigiDollarSendWidget: Mask available balance, USD equivalent, fee,
  and total displays
- DigiDollarMintWidget: Mask available DGB balance, collateral value,
  oracle price, and USD equivalent
- DigiDollarRedeemWidget: Mask DD minted, DGB collateral, and
  redeemable amount values
- DigiDollarPositionsWidget: Hide positions table when masked, show
  privacy status message
- DigiDollarTransactionsWidget: Hide transactions table when masked,
  show privacy status message, skip updates while masked
- WalletView: Connect setPrivacy signal to DigiDollarTab::setPrivacy

Each widget uses a maskValue() helper that replaces digits with '#'
in formatted strings, following the same approach as
DigiByteUnits::formatWithPrivacy().

All 20 Qt tests pass (7 new privacy + 12 existing + init/cleanup).
All 1539+ C++ unit tests pass with no regressions.

Fixes privacy leak reported by DanGB and Bastian on Gitter.
JaredTate and others added 30 commits February 27, 2026 13:53
…uilds

uint64_t to int64_t brace-init is a narrowing conversion rejected by Clang
(-Wc++11-narrowing). Use static_cast instead.
Bug #8: After wallet recovery via listdescriptors/importdescriptors, the
wallet GUI showed active 'Redeem DigiDollar' buttons for positions that
were already redeemed. Reported multiple times by users.

ROOT CAUSE (two bugs):

1. ProcessDDTxForRescan relied solely on OP_RETURN parsing to detect DD
   transaction types. Full-redemption REDEEM txs (ddChange == 0) have NO
   OP_RETURN with DD metadata, making them invisible to the rescan parser.
   Positions were created during MINT processing but never marked inactive
   when the REDEEM tx was encountered.

2. ValidatePositionStates() — which cross-checks every active position
   against the actual UTXO set — only ran at wallet startup (postInitProcess),
   never after importdescriptors or rescanblockchain rescans.

FIX:

1. ProcessDDTxForRescan now uses GetDigiDollarTxType() (version field) as
   the primary tx type detection. The version field ALWAYS encodes the type
   correctly via SetDigiDollarType(). OP_RETURN parsing is retained for
   supplementary data extraction (DD change amounts).

2. ScanForWalletTransactions() now calls ScanForDDUTXOs() after any
   successful rescan completes. This runs ValidatePositionStates() which
   cross-checks all active positions against the UTXO set, catching any
   positions whose collateral was spent (redeemed).

Both fixes are defense-in-depth: Fix 1 prevents the bug, Fix 2 catches
any edge cases that Fix 1 might miss (e.g., blocks skipped by the fast
BIP158 block filter during rescan).

Includes functional test: digidollar_wallet_restore_redeem.py
These 4 p2p tests test bloom filter rejection, disconnect/ban behavior,
invalid transactions, and compact blocks — none of them test Dandelion.
Dandelion++ adds INV/GETDATA/NOTFOUND exchanges during peer handshake
that create thread scheduling races between ThreadMessageHandler and
ThreadSocketHandler on slow macOS CI runners, causing disconnect
detection timeouts.

Adding -dandelion=0 is proper test isolation: these tests should test
what they're designed to test without interference from an unrelated
feature. The dedicated p2p_dandelion.py test covers Dandelion behavior.

Also removes the macOS --exclude list from ci.yml so all p2p tests
now run on both platforms — no more skipped tests.
Port validation added during the Bitcoin Core v26.2 merge rejects
ipc:// addresses because they use filesystem paths, not host:port
format. This breaks users who rely on Unix domain sockets for ZMQ
notifications (e.g. zmqpubhashblock=ipc:///tmp/dgb.hashblock), which
worked in DigiByte v8.22.2.

Split the validation loop so ZMQ options skip port validation for
ipc:// addresses while TCP-only options (-rpcbind, -proxy, etc.)
retain strict validation. libzmq natively handles ipc:// endpoints.

Fixes: #340

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
docs: add DigiDollar documentation validation orchestrator prompt to Z_PROMPTS.md

Reusable prompt for deploying up to 5 sub-agents to validate all DigiDollar
docs against live C++ source code:
- DIGIDOLLAR_EXPLAINER.md
- DIGIDOLLAR_ARCHITECTURE.md
- DIGIDOLLAR_ORACLE_EXPLAINER.md + ORACLE_ARCHITECTURE.md
- DIGIDOLLAR_ACTIVATION_EXPLAINER.md
- DIGIDOLLAR_ORACLE_SETUP.md + docs/ guides

Each sub-agent assigned one doc + source files, produces PASS/FAIL/STALE
findings and corrected content. Orchestrator synthesizes and commits.

docs: trim validation orchestrator prompt to concise reusable format

docs: simplify validation prompt

docs: add REPO_MAP.md to validation prompt

docs: clean up validation prompt - no headers, more detail

docs: remove docs/ files from prompt - only validate specified docs

docs: simplify prompt - let agent use REPO_MAP to find source files
… code

Validated all 8 DigiDollar documentation files against actual source code.
4 files required corrections (4 others were already accurate from prior updates).

DIGIDOLLAR_ARCHITECTURE.md:
- Fix stale line references for TX output structure, ProcessDDTxForRescan,
  DeriveLockTierFromHeight, SignDDInputs, mintdigidollar/redeemdigidollar/
  getdigidollaraddress RPC locations
- Correct DeriveLockTierFromHeight thresholds to match actual 10-tier code
- Update exchange API count from 7 to 11
- Fix protection system line count from 1,470 to 3,048

DIGIDOLLAR_ORACLE_ARCHITECTURE.md:
- Remove CoinMarketCap from working exchanges diagram (paid API, broken)
- Fix price range from 10M to 100M micro-USD max ($100 not $10)
- Fix timestamp freshness check from 5 min to 1 hour
- Fix misbehavior penalties from 10-100 to 2-20 points
- Fix CheckBlock integration line from 4127 to 4313
- Correct fChecked/oracle validation ordering in CheckBlock sequence
- Fix concrete hex encoding example byte layout
- Fix ValidateBlockOracleData location from lines 802-941 to 1560-1733
- Fix testnet oracle count from 10 to 9 (5-of-9 consensus)

DIGIDOLLAR_ORACLE_EXPLAINER.md:
- Fix timestamp freshness from "5 min" to "1 hour"
- Fix coinbase scanning from vout[1]-specific to ALL outputs
- Fix price cache example from cents to micro-USD format
- Fix hex encoding: 0x62 -> 0x64 for 6500 decimal LE
- Fix section heading from "21-Byte" to "22-Byte" format
- Correct GetBestHeight() return value from 1000 to 0

REPO_MAP.md:
- Fix GetNextWorkRequired function casing: V1-V4 -> v1-v4

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fix: restore ZMQ ipc:// Unix domain socket support
… test

After disconnect_nodes(3, 4), the Dandelion stempool thread may leave the
RPC HTTP connection in Request-sent state. When stop_nodes() fires in
shutdown(), it gets CannotSendRequest on nodes 3/4 (Dandelion-enabled),
causing the macOS ARM64 CI runner to fail with leftover processes.

Add a 5s sleep post-disconnect to let Dandelion thread state drain cleanly
before the test framework's stop_nodes() is called.

Reproduces reliably on macOS 14 ARM64 GitHub Actions runners.
… test

After disconnect_nodes(3, 4), the Dandelion stempool thread may leave the
RPC HTTP connection in Request-sent state. When stop_nodes() fires in
shutdown(), it gets CannotSendRequest on nodes 3/4 (Dandelion-enabled),
causing the macOS ARM64 CI runner to fail with leftover processes.

Add a 5s sleep post-disconnect to let Dandelion thread state drain cleanly
before the test framework's stop_nodes() is called.

Reproduces reliably on macOS 14 ARM64 GitHub Actions runners.
…ion-state

fix(wallet): restore redeemed DD positions after importdescriptors
…ck state

Bug #13: listdigidollartxs returned blockheight: -1 for all transactions,
even confirmed ones.

Root cause: DDTransaction constructor defaults blockheight to -1.
GetDDTransactionHistory() recalculated confirmations dynamically from
the wallet tx but never updated blockheight or blockhash fields.

Fix: After looking up the CWalletTx, check if it has TxStateConfirmed
state. If so, extract confirmed_block_height and confirmed_block_hash.
Unconfirmed transactions retain the default -1.

This follows the same pattern Bitcoin Core uses for wallet transaction
state inspection (std::variant-based TxState).

Files changed: src/wallet/digidollarwallet.cpp (10 lines added)
Unit tests: All 3,191 pass (no regressions)
…llar

Bug #11: Users could attempt mints outside the $100 min / $100,000 max
limits with no error message. The limits existed in consensus params
(ConsensusParams::minMintAmount = 10000, maxMintAmount = 10000000) but
mintdigidollar only validated ddAmount > 0, causing confusing rejection
at broadcast time instead of clear errors at RPC time.

Fix: After parsing ddAmount, call DigiDollar::IsValidMintAmount() with
consensus params from Params().GetDigiDollarParams(). On failure, throw
RPC_INVALID_PARAMETER with human-readable message showing the limit in
both dollars and cents (e.g. 'Minimum mint amount is $100 (10000 cents)').

Uses consensus params rather than hardcoded values so limits update
automatically if consensus rules change.

Files changed: src/rpc/digidollar.cpp (18 lines added)
Unit tests: All 3,191 pass (no regressions)
…et queries

Bug #12: listdigidollaraddresses returned hardcoded mock addresses
(DDmockaddress123456789abcdef1/2/3) since RC22. The entire function
body was a stub that never queried the wallet.

Fix: Replace mock array with real queries to DigiDollarWallet:
- GetDDTimeLocks(false) to enumerate all collateral positions
- Extract DD token addresses from mint transaction outputs (vout[1])
- GetDDTransactionHistory() to capture transfer recipient addresses
- Aggregate balance, tx count, and timestamps per unique address
- Apply balance filter and watch-only filter as before

Preserves the original response format (address, label, balance,
ismine, iswatchonly, txcount, created_date, last_used) but now
returns real data from the wallet.

Known limitation: Address prefix handling currently uses regtest
format (dgbrt1 -> RD). Will need network-aware prefix mapping for
testnet (dgbt1 -> TD) and mainnet (dgb1 -> DD) before release.

Files changed: src/rpc/digidollar.cpp (126 added, 17 removed)
Unit tests: All 3,191 pass (no regressions)
- health_ratio: Now uses oracle price to compute actual collateral
  coverage percentage via (dgb_value_usd / dd_value_usd) * 100,
  using __int128 integer math to prevent overflow. Falls back to
  mock oracle on regtest.

- created_date: Estimated from unlock_height minus lock period blocks,
  converted to ISO 8601 timestamp using 15-second block time.

- unlock_date: Computed as ISO 8601 timestamp. For locked positions,
  estimated as now + blocks_remaining * 15s. For unlocked positions,
  computed retroactively.

- RPC registration: Already in wallet RPC table (wallet.cpp:965),
  the commented-out line in digidollar.cpp:4201 is the old location.

- Added regression test: digidollar_rpc_position_fields.py
Both RPCs now use a shared ScanOracleDataFromChain() helper that:
- Scans on-chain blocks, pending P2P messages (with stale filtering),
  and local runtime oracles in the same order and priority
- Uses identical scan depth (both default to 20 blocks, both accept
  a 'blocks' parameter)
- Returns consistent status strings: reporting/no_data/outlier
  (removed 'stopped' from getoracles; added outlier detection to both
  via >10% deviation from consensus price)

Changes:
- src/rpc/digidollar.cpp: Extract ScannedOracleData, OracleScanResult,
  ScanOracleDataFromChain(), GetOracleStatus() shared helpers.
  Refactor getalloracleprices() and getoracles() to use them.
  Add 'blocks' param to getoracles (2nd positional arg after active_only).
- src/rpc/client.cpp: Register getoracles blocks param for JSON conversion.
- test/functional/digidollar_oracle_consistency.py: Regression test
  verifying both RPCs return identical prices, statuses, and data.
Bug #9 (CRITICAL): Redemption fails after first redeem with 'insufficient
fee inputs'. Root cause: SelectFeeCoins was called with hardcoded
estimatedFee = 10000000 (0.1 DGB = 10M sats), but actual fee at the
current feerate of 35M sat/kB is ~14M-21M+ sats. After the first
redemption consumed larger UTXOs, remaining UTXOs couldn't cover the
underestimated fee.

Fix: Replace all hardcoded 10M sat fee estimates with proper calculation:
  estimatedFee = (vsize * feeRate) / 1000 + 50% safety margin
  Floor at 10M sats (0.1 DGB minimum)

Locations fixed:
- src/rpc/digidollar.cpp:1406 - redeemdigidollar RPC fee estimation
- src/wallet/digidollarwallet.cpp:1068 - TransferDigiDollar fee estimation
- src/wallet/digidollarwallet.cpp:2703 - EstimateRedemptionFee (FEE_RATE
  was 200K instead of 35M sat/kB, and base size was 250 instead of 350)
- src/wallet/digidollarwallet.cpp:5028 - CalculateTransactionFee
  (DEFAULT_FEE_RATE was 200K instead of 35M sat/kB)

Bug #17: Redemption fee displays incorrectly. The DDTransaction.fee field
was never populated for redemptions, always showing 0.

Fix: Set ddtx.fee = redeemResult.totalFees in both:
- src/rpc/digidollar.cpp (RPC path)
- src/wallet/digidollarwallet.cpp (wallet path)

Added 4 regression tests in digidollar_bughunt_tests.cpp:
- bug9_fee_estimation_not_hardcoded
- bug9_fee_floor_at_minimum
- bug17_estimate_redemption_fee_uses_correct_rate
- bug17_calculate_transaction_fee_uses_dd_rate

All integer math, no float/double. No consensus changes.
Also fixes variable shadowing in mintdigidollar: renamed 'params' to
'chainParams' to avoid conflict with TxBuilderMintParams 'params' in
same scope (pre-existing issue from Bug #11 commit).
… requirement

- digidollar_wallet_tests: redemption fee estimation and large tx fee now
  expect dynamic calculation (>10M sats) instead of hardcoded 10M floor
- digidollar_rpc_tests: listdigidollaraddresses now requires wallet context
  (mock data removed in Bug #12 fix), test updated to expect throw
… wallet context (Bug #12)

Registration was in non-wallet table but the RPC now needs wallet access
after mock data removal. Moved to wallet.cpp alongside other DD wallet RPCs.
Removed static qualifier so the function is externally visible.
…ition lookup

Previously returned hardcoded mock data (dd_minted=10000, dgb_return=150,
unlock_date='2024-12-31') for ALL positions including nonexistent ones.

Now properly:
- Looks up position from wallet via GetDDTimeLocks()
- Returns real dd_minted, dgb_collateral, unlock_height
- Computes dgb_return as collateral minus EstimateRedemptionFee()
- Checks ERR state for emergency redemption path and penalty
- Returns correct can_redeem (requires active + unlocked + has collateral)
- Computes unlock_date from block heights
- Returns proper error for nonexistent positions
- Moved registration to wallet RPC table for proper wallet context
SelectFeeCoins was called with nullptr for the amounts output in the
wallet's direct RedeemDigiDollar() method, leaving params.feeAmounts
empty. BuildRedemptionTransaction needs per-UTXO amounts to correctly
construct fee inputs. The RPC path already did this correctly.

Credit: JohnnyLawDGB (identified in PR #389)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants