v9.26.0-rc24: Wallet RPC Correctness Release#390
Open
v9.26.0-rc24: Wallet RPC Correctness Release#390
Conversation
…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
…byte into feature/digidollar-v1
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.
…byte into feature/digidollar-v1
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.
…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.
…byte into feature/digidollar-v1
…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.
… 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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
See
RELEASE_v9.26.0-rc24.mdfor full release notes.