Commit Graph

32091 Commits

Author SHA1 Message Date
d0657d38b4 Merge dev into dragonx: DragonX rebrand + security/consensus hardening + IBD speedups
# Conflicts:
#	contrib/init/dragonxd.conf
#	contrib/init/dragonxd.init
#	contrib/init/dragonxd.openrc
#	contrib/init/dragonxd.openrcconf
#	contrib/init/dragonxd.service
2026-07-21 18:58:33 -05:00
8976e020e9 packaging: don't fail the .deb build when optional lintian is absent
build-debian-package.sh warns at startup that lintian is optional, but then
called `lintian -i ...` unconditionally at the end. Under `set -e` that aborted
with exit 127 on hosts without lintian — after the .deb had already been built.
Guard the call with `command -v lintian` so the build exits 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 16:04:08 -05:00
9c715f68eb depends: fix libsodium build breaking on git.savannah.gnu.org 502
libsodium's autogen.sh fetches config.sub/config.guess from git.savannah.gnu.org
gitweb, which is frequently down (currently returns 502). curl saved the HTML
error page over config.sub, so libsodium's configure died with
"cannot run /bin/bash ./build-aux/config.sub" and the whole build failed.

autoreconf -ivf (run earlier in autogen.sh) already installs valid config.sub/
config.guess from the build host, so set DO_NOT_UPDATE_CONFIG_SCRIPTS=1 (the
script's own opt-out) to skip the fragile download. Validated: the full build
now completes and produces working dragonxd/dragonx-cli/dragonx-tx.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 16:02:04 -05:00
e3247d946e cleanup: rebrand residual user-facing "HUSH" strings to DragonX
Sweeps the leftover coin-name strings in RPC help text, RPC output, and log
messages that the currency-unit change didn't cover:

- RPC help: "mining reward amount in HUSH" -> DRAGONX (mining.cpp x2);
  "at least minbal HUSH" -> DRAGONX (rpcwallet.cpp); "the HUSH address" /
  "(string) HUSH address" -> DragonX (rawtransaction.cpp)
- RPC output: the SMART_CHAIN_SYMBOL[0]==0 ? "HUSH" : SYMBOL coin-name fallback
  (crosschain/misc/mining/blockchain) -> "DRAGONX"; the notarizations JSON key
  make_pair("HUSH", ...) -> "DRAGONX"
- Logs: "HUSH blocktime changing", "stopping HUSH HTTP/REST/RPC",
  "HUSH raw magic=" -> DragonX

Left untouched (verified): the 82 "HUSH3"/ishush3 chain-symbol consensus checks;
hush_globals.h CURRENCIES[] price-oracle basket (internal lookup, dead feature
on DragonX); hush.h notarization debug printf; a commented-out cout in main.cpp.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 15:37:57 -05:00
ffb753057a cleanup: rebrand currency unit, depends mirrors, seeds; drop Hush-history files
Follow-up to the doc rebrand, addressing the previously out-of-scope legacy:

- Currency unit: strCurrencyUnits (chainparams.cpp) and CURRENCY_UNIT
  (amount.cpp) "HUSH" -> "DRAGONX". Both are display-only (RPC help + metrics);
  no logic comparisons, verified.
- depends mirrors: libsodium/boost/utfcpp fetched from git.hush.is/attachments;
  repointed to canonical upstream (GitHub release / archives.boost.io / GitHub
  tag) with the existing sha256 hashes verified to match those sources.
- Seeds: nodes_main.txt now lists the five node[1-5].dragonx.is IPs (DNS-resolved)
  instead of Hush nodes; regenerated src/chainparamsseeds.h (was compiling Hush
  seed IPs as the fixed fallback); generate-seeds.py header now says DragonX;
  hush_seed_nodes.txt updated to DragonX seeds.
- Deleted Hush-history / wrong-for-DragonX files: contrib/snapshot/ (block-500000
  Hush airdrop, ~10MB), notary_seeds.txt (Hush notaries; DragonX isn't notarized),
  and the Hush emission scripts hush_supply, hush_supply_old, hush_halvings,
  hush_block_subsidy_per_halving (hardcode Hush's 340000/12.5 economics).

Kept: hush_scanner (engine invoked by dragonx_scanner) and the "The Hush
developers" copyright headers (lineage credit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 14:13:31 -05:00
46693a355a docs: rebrand documentation, packaging, and helper scripts to DragonX
The docs/packaging were largely un-rebranded Hush3 content, with several
docs stating facts that are wrong for DragonX. This rewrites them against
the verified DragonX source state.

Corrections (not just branding):
- PoW: RandomX (CPU), not Equihash/ASIC — README, overview.md, randomx.md
- Privacy: private from genesis (ac_private=1, Sapling@height1), not "as of
  block 340000" — overview.md, payment-api.md
- Removed the false "coinbase must be shielded" consensus claim
  (shield-coinbase.md, payment-api.md); coinbase is directly spendable
- Fixed default fee 0.0001 (was 0.0010000, 10x); stratum port 22769 (was 19031)
- datadir ~/.hush/DRAGONX, DRAGONX.conf, dragonxd/dragonx-cli/dragonx-tx,
  git.dragonx.is throughout; branch model dev->dragonx
- Softened the inherited dPoW reorg claim (no live DragonX notary infra)

Packaging: fix build-debian-package.sh + gen-manpages.sh to use the dragonx
binaries/manpages; rename bash-completions to dragonx*; drop hush-arrakis-chain
from the package. Keep /usr/share/hush (hardcoded in the binary for params).

Also: README links/logo, ObsidianDragon + SilentDragonXAndroid wallets,
networking/init/dev-process/contrib/util rebrand, and leftover helper scripts.
Delete legacy duplicates (hushd.* init/service, HUSH3.conf examples,
OLD_WALLETS.md, hsc.md) and rename hush-uri.bat -> dragonx-uri.bat.

Out of scope (noted, not changed): historical changelog/copyright, the Hush
mainnet airdrop snapshot, seed data files, depends/ source mirrors, and the
in-code strCurrencyUnits="HUSH".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 13:59:06 -05:00
bf3c33c53a revert(net): remove header-accept RandomX check; keep nMinimumChainWork + #8
An adversarial re-review found the header-accept RandomX check (b9fdc7981 +
7e9b2c661 header-PoW + d2124a303 defer) to be a persistent source of
consensus-liveness bugs: it derives the RandomX key from the ACTIVE chain
(hush_chainactive), the wrong branch for reorg/side-branch/catch-up headers, so
it repeatedly false-rejected validly-mined headers and DoS(100)-hard-banned
honest peers (IBD-tail catch-up and deep-reorg cases); the defer fix and an
extend-tip fix each addressed one case while leaving/creating others (an
extend-tip variant re-opened an unbounded post-IBD side-branch flood). It only
mitigated a low-harm resource DoS -- forged headers bloat mapBlockIndex memory/
disk but are never SELECTED (nMinimumChainWork) and the full RandomX + target
check still runs at block-connect. Revert to fCheckPOW=0 at header-accept
(original behavior). A comment in AcceptBlockHeader records that any re-attempt
must derive the key from the header's OWN ancestry (pindexPrev->GetAncestor),
never the active chain.

Also hardens two issues the same review found:
- #8 IBD header cap now bounds against the VALIDATED chainActive.Height()
  (attacker-hard) instead of pindexBestHeader, which a forward-extending flood
  advanced in lockstep, defeating the cap.
- opreturn_burn only emits a change output above the dust threshold; a sub-dust
  change made the returned tx non-standard/unrelayable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 20:24:45 -05:00
5951ee118a fix(net): cap per-peer headers during IBD (header-flood DoS)
Audit #8. The HEADERS handler accepted unbounded headers per peer with no
cumulative cap; during IBD (fCheckPOW=0) a peer could flood cost-free PoW-less
headers into mapBlockIndex/leveldb (never selected -- nMinimumChainWork gates
that -- but still memory/disk growth). Add a per-peer nHeadersProcessed counter
in CNodeState; while IsInitialBlockDownload(), if one peer exceeds
2*max(pindexBestHeader height, checkpoint height) + 200000 headers,
Misbehaving(100) and drop it. The cap is ~2x the chain length, so honest sync
never approaches it; inert post-IBD (the RandomX header check handles forged
headers there).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 18:54:35 -05:00
d2124a3038 fix(pow): defer RandomX header check when key block not yet connected
Follow-on to the post-IBD header-PoW verification (b9fdc7981 + 7e9b2c661).
CheckRandomXSolution derives the RandomX key from the block at
keyHeight = ((height-lag)/interval)*interval, looked up on the ACTIVE chain
(hush_chainactive), so that block must be CONNECTED. When a post-IBD node's
block tip lags the header tip by more than ~one RandomX interval -- the normal
IBD tail, or any node catching up -- the key block is not connected yet, so
GetRandomXKey returns empty. The old code returned an error, making
CheckBlockHeader DoS(100)-ban the honest peer that sent a perfectly valid tip
header we simply could not verify yet.

Observed live: a node finishing a mainnet reindex banned the pool box + seeds
and stalled ~2000 blocks short of the tip. Fix: on an empty key, DEFER (return
true) instead of error -- the header is fully RandomX-verified at block-connect,
where the key block is always connected (blocks connect in order,
keyHeight <= height-lag < the connected tip). Flood protection is preserved for
synced nodes (key present -> real check) and bounded during catch-up by the
per-peer IBD header cap + nMinimumChainWork.

Validated on the live 3.14M-block chain: the affected node caught up the full
~2135-block gap to the tip with zero peer bans (was stalled + banned before).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 18:54:35 -05:00
b5050d06c0 fix(wallet): opreturn_burn return change + widen txfee to CAmount
#10 (HIGH) opreturn_burn selected UTXOs for nAmount+txfee but pushed only the
burn vout and returned - so the entire selected-input surplus was silently paid
as miner fee (e.g. a 500-coin UTXO burning 10 lost ~490). Push a change output
for (inputs - nAmount - txfee). Also widen the int32_t txfee (which truncated
large CAmount fees) to CAmount and MoneyRange-validate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 11:21:28 -05:00
a520441e3a fix(rpc): guard z_validateaddress against null pwalletMain under -disablewallet
#11 (HIGH) z_validateaddress locked LOCK2(cs_main, pwalletMain->cs_wallet) with
no availability guard; under -disablewallet pwalletMain is NULL, so the member
deref SIGSEGVs the daemon (execute() only catches std::exception). Use the
null-safe LOCK2 idiom already used by sibling RPCs so validation still works
without a wallet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 11:21:28 -05:00
11704e6023 fix(nspv): add missing length lower-bounds before request/vopret reads
nSPV handlers (gated behind non-default -nspv_msg) read request[1]/vopret[1]
before confirming the peer sent >=2 bytes:

#6 (MEDIUM) NSPV_UTXOS/NSPV_TXIDS evaluated request[1] whenever len<69 (incl
len==1); the 4351d5b73 value-clamp left this lower bound open. The TXIDS/MEMPOOL
else-branch debug prints also read request[1] unconditionally. Add len>=2 guards
/ drop request[1] from the prints.

#7 (LOW) NSPV_MEMPOOL_CCEVALCODE read vopret[1] on a possibly-1-byte vector.
Guard with vopret.size()>=2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 11:21:28 -05:00
7e9b2c6615 fix(net): verify RandomX at correct height in AcceptBlockHeader + cap locator
header-pow: AcceptBlockHeader passed the caller's reused *ppindex (and a height
derived from it, ==0 for a new header) to CheckBlockHeader instead of the
header's own local pindex + real height. Post-IBD this made
RandomXValidationRequired(0) false, so CheckRandomXSolution returned true WITHOUT
verifying (and the fRandomXVerified short-circuit could fire on an unverified
header) - silently defeating the header-flood PoW gate from b9fdc7981. Resolve
pindexPrev up-front, pass real height (parent+1) and the local (NULL) pindex so
the post-IBD RandomX check actually runs; IBD stays fast (fCheckPOW=0).
Stability-tested: 303 valid headers accepted across a 4-node RandomX net,
0 false rejects / bans.

#9 (MEDIUM) GETBLOCKS/GETHEADERS deserialized an unbounded CBlockLocator.vHave
(~130k hashes) and scanned it linearly under cs_main with no ban - a
message-thread liveness DoS. Add MAX_LOCATOR_SZ=101 + Misbehaving, matching the
adjacent vInv/headers caps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 11:21:28 -05:00
fc06a43dd7 fix(consensus): bound OP_RETURN opretlen + clamp notary pubkeys array
Defensive-audit findings (adversarially verified + fleet stability-tested):

#4 (CRITICAL) hush_voutupdate trusted an attacker-decoded OP_RETURN length
(opretlen, up to 65535 via OP_PUSHDATA2) with no check against the real script
length, driving up to ~64KB out-of-bounds reads through hush_stateupdate ->
hush_eventadd_opreturn -> hush_kvupdate (persisted to disk, leaked via kvsearch
RPC, reliable crash on block connect). Reject any opret claiming more bytes than
remain in the script, at the single taint source.

#5 (HIGH) notary-ratification loop did memcpy(pubkeys[numvalid++],..) into a
fixed uint8_t[64][33] with no bound; >64 crafted vouts smashed the stack. Clamp
numvalid < 64.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 11:21:28 -05:00
7e99311210 fix(net): enforce nMinimumChainWork in IBD (eclipse / low-work fake-chain protection)
nMinimumChainWork was defined in chainparams but never checked, and IsInitialBlockDownload
decided "synced" from tip timestamp/height alone -- so an eclipsed or bootstrapping node
could be fed a cheap low-work fake chain with recent timestamps and trust it. Reset the
stale mainnet floor (0x281b32ff3198a1 was ABOVE the live chain, would have bricked mainnet)
to the real chainwork at height ~3,100,000, and hold a node in IBD until its tip reaches the
floor. Gated to the DRAGONX symbol so ephemeral assetchains from the same binary are not
trapped in IBD; the check can only keep a node in IBD, never force it out (no false-sync risk).
Complements the header-flood fix (b9fdc7981): that stops invalid-PoW headers off the real tip;
this stops valid-but-cheap fake chains from a fake genesis.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 17:21:12 -05:00
14e3fb6708 fix(wallet): reserve miner fee during z_sendmany note selection
The Sapling note-selection loop stopped once total_value >= nTotalOut, ignoring
the miner fee, so a wallet with notes covering the amount but not amount+fee
selected too few notes and failed later with a spurious "insufficient funds".
Reserve the fee (default or user-supplied) in the selection target.

Leto eb4fc52273.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 16:06:39 -05:00
b9fdc79818 fix(net): verify PoW at header-accept once synced (header-flood DoS)
AcceptBlockHeader called CheckBlockHeader with fCheckPOW=0, so a synced node
stored any well-formed PoW-less header off the tip into mapBlockIndex without
bound (memory/disk DoS). nMinimumChainWork is defined but unenforced and would
not stop tip-siblings anyway (they inherit the tip's chain work). Verify PoW at
header-accept time when not in IBD: forged headers now fail RandomX and the peer
is DoS-banned. IBD keeps fCheckPOW=0 for fast header sync; the full-block
RandomX/target check at connect is unchanged, so no valid header is rejected
(not a consensus-rule change).

fCheckPOW=0 call site is Leto (6a30b40415).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 16:06:39 -05:00
4351d5b733 fix(nspv/wallet): bound nSPV request buffers + fix uninitialized fee / null-deref
hush_nSPV_fullnode.h: bound the REMOTERPC method strcpy and json memcpy to their
fixed buffers (method[64], json[11000]); add lower-length and memcpy-source bounds
to the UTXOS/TXIDS coinaddr[64] copies and the MEMPOOL handler. These paths
deserialize attacker-controlled request bytes -> stack overflow / OOB read. The
nSPV server is opt-in via -nspv_msg (off by default; DragonX uses lightwalletd).

rpc/blockchain.cpp: getchaintxstats null-checks pwalletMain (crash under -disablewallet).
wallet/rpcwallet.cpp: z_sendmany initializes nFee to the default miners fee (was read
uninitialized when no fee param supplied).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 15:56:09 -05:00
4a0a334649 fix(consensus): guard NULL pindex deref in hush_validate_chain (crash DoS)
hush_validate_chain() enters its body when hush_getblockindex(srchash) returns
NULL (via || short-circuit) -- srchash comes from an attacker-controlled
notarization OP_RETURN -- then a debug fprintf dereferenced the NULL pindex.
A block carrying one crafted OP_RETURN tx crashed every synced node on connect,
and crash-looped on restart. Guard the deref: pindex ? GetHeight() : -1.

Introduced by Leto commit 4988ce6f2 ("much debug such wow", 2022).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 15:56:09 -05:00
d52550a6fc fix(consensus): reject Sprout JoinSplits (unverified proof + vpub_new inflation vector)
Sprout JoinSplit proofs/sigs/nullifiers/anchors are never verified (the verifier
arg to CheckTransaction is unused), yet vpub_new is counted as transparent
value-in -- a forged all-zero JoinSplit mints arbitrary value from nothing.
Reproduced on an isolated ac_private=1 chain: 500,000 minted into a z-addr,
accepted + mined + verifychain=true.

Reject any non-coinbase tx carrying a JoinSplit in ContextualCheckTransaction
(covers both mempool acceptance and ConnectBlock). DragonX is Sapling-only from
genesis with zero JoinSplits in its history (mainnet supply audit), so this is
inert on all legitimate traffic and never invalidates a historical block.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 08:10:14 -05:00
d159e72086 feat: chain-level Sapling turnstile (reject blocks that would drive the pool negative)
Belt-and-suspenders inflation/counterfeiting guard on top of the per-tx Sapling
binding signature: ConnectBlock rejects any block whose cumulative Sapling value
pool would go negative (bad-sapling-value-pool-negative) -- a block can never
deshield more value than was ever shielded.

Enforced ONLY when the pool is reliably tracked from genesis (pprev's
nChainSaplingValue is engaged), so it can never false-reject a valid block or
split the chain on nodes that don't track the pool -- those stay dormant.

To make "not reliably tracked" propagate safely, nSaplingValue becomes a
boost::optional<CAmount> (was a plain CAmount). A version-gated dual-read in
CDiskBlockIndex reads records written before SAPLING_VALUE_OPTIONAL_VERSION
(1000350 = v1.0.3) as the legacy raw 8-byte CAmount but DISCARDS the value
(reads boost::none). Records written at >= 1000350 use the optional format and
persist, so from-genesis and reindexed v1.0.3 nodes are durably active across
restarts.

Tested on a 5-node RandomX fleet: old-format DB loads dormant (0 corruption);
from-genesis stays active with correct pool accumulation and 0 false-rejects
across shield/deshield cycles; dormant/active/reindexed nodes converge; a crafted
counterfeit block is rejected (guard fires, no crash); active state persists
across restart (verified at CLIENT_VERSION 1000351 with gate 1000350).

*** MANDATORY UPGRADE STEP (v1.0.3 dev/test nodes) ***
CLIENT_VERSION stays 1000350, and pre-turnstile v1.0.3 builds ALSO stamped
records at 1000350 but in the old plain-8-byte format. Those records now route to
the OPTIONAL read branch and MISPARSE: LoadBlockIndexDB throws and the node
ABORTS on startup (looks like block-DB corruption). Therefore any node that ran an
earlier v1.0.3 (1000350) build MUST have its block data wiped or be -reindexed
before running this build -- do NOT upgrade a 1000350 datadir in place.
Production mainnet (v1.0.2 = CLIENT_VERSION 1000250) is UNAFFECTED: those records
take the legacy branch and read correctly (dormant until reindex). v1.0.3 is
unreleased, so only dev/test datadirs are affected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 01:14:46 +02:00
adf2bacdbd feat: fee-ordered mempool eviction (TrimToSize/Expire) + displacement on admission
This fork never ported Bitcoin's mempool size-limiting: CTxMemPool had no TrimToSize/
Expire and LimitMempoolSize was commented out. An earlier commit added a blunt
DynamicMemoryUsage admission cap that bounded memory but bluntly REJECTED new txs when
full -- so a high-fee tx could not push out a low-fee one. This implements proper
fee-ordered eviction using the per-tx feerate index that already exists (mapTx index 1,
CompareTxMemPoolEntryByFee), with no new index and no descendant-tracking port.

- CTxMemPool::TrimToSize(sizelimit, pvNoSpendsRemaining): while DynamicMemoryUsage() is
  over the limit, evict the lowest-feerate tx (the tail of the feerate index) and its
  in-mempool descendants (recursive remove), re-deriving the tail each iteration.
  Terminates (pool strictly shrinks) and cleans every secondary index via remove().
- CTxMemPool::Expire(time): age-based sweep (entry time older than `time`), for
  LimitMempoolSize's -mempoolexpiry.
- LimitMempoolSize re-enabled (Expire + TrimToSize) and called from ConnectTip on every
  block connect. (No pcoinsTip->Uncache -- CCoinsViewCache has none in this fork; it is
  only a UTXO-cache perf hint.)
- AcceptToMemoryPool now ADDS the tx then TrimToSizes: a higher-fee tx displaces
  lower-fee ones; if this tx was itself the lowest-feerate (evicted), it is rejected
  ("mempool full"). Replaces the blunt reject-when-full cap.
- DEFAULT_MEMPOOL_EXPIRY 1 -> 72 hours (age-Expire is now live; 1h was too aggressive).

Known simplification (documented in code): per-tx feerate, not descendant-aggregate
(CPFP) scoring, and no rollingMinimumFeeRate anti-thrash. Adversarially reviewed
(termination, iterator safety, recursive-lock safety, index cleanup all confirmed) and
runtime-tested on the fleet: pool stays bounded under a 1600-tx flood, verifychain ok,
no hang/crash.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 09:01:06 +02:00
1ec3dbfee3 docs: note BIP39 cross-wallet restore parity is mainnet-only in -mnemonic help
The 24-word seed restores the same wallet in SilentDragonXLite only on mainnet;
testnet/regtest derive a different HD coin_type (per BIP44), so a phrase does not
round-trip across wallets there. Document that in the -mnemonic help so it is not
mistaken for a bug.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 08:10:35 +02:00
ddf6a35680 Rebrand cleanups: getpeerinfo help example + 1.0.3 debian changelog entry
net.cpp: getpeerinfo help address example 18030->21768 and 'Hush server'->'DragonX server'. debian/changelog: prepend 1.0.3 release entry summarizing IBD speedups, witness fix, bulk streaming, seed phrases, assumeutxo removal. NOTE net.cpp change needs a daemon rebuild to surface in runtime RPC help. Staged on 176; not pushed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 08:07:23 +02:00
9cb6424799 Fix dragonx-cli -rpcport help default: 18030 (hush) -> 21769 (DragonX)
The -rpcport help string in bitcoin-cli.cpp hardcoded hush's 18030; the actual default (BaseParams().RPCPort()) is DragonX's 21769, so this was misleading help text only (the CLI already connects to 21769). Set to 21769 and regenerated doc/man/dragonx-cli.1 from the rebuilt binary. NOTE: a separate hush 18030 leftover remains in src/rpc/net.cpp:357 (getpeerinfo help example address) - daemon RPC help, out of scope here. Staged on 176; not pushed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 08:07:23 +02:00
1c6f64b87e Harvest DragonX packaging + legal artifacts from compliant-rebrand
Legal: correct GPLv3 LICENSE (fixes garbled 'GENERAL GENERAL'), AUTHORS DragonX attribution, COPYING. Packaging: man pages REGENERATED from the 1.0.3 binaries via help2man (dragonxd/dragonx-cli/dragonx-tx.1 -> v1.0.3, correct dates), wired into doc/man/Makefile.am (dist_man1_MANS), orphaned hush*.1 removed. Init/openrc/systemd scripts, Debian packaging (control/changelog/copyright rebranded hush->dragonx + install stubs), example confs taken from origin/compliant-rebrand (c05134e77). REMAINING follow-ups: (1) debian/changelog still tops at 1.0.0 - add a 1.0.3 entry; (2) dragonx-cli --help hardcodes rpcport default 18030 (hush) - fix the HelpMessage string in source then regen. Staged on 176 for review; not pushed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 08:07:23 +02:00
a568ab628e tune: raise adaptive dbcache ceiling to 64 GiB on 64-bit hosts
nMaxDbCache capped the adaptive UTXO/db cache (and a manual -dbcache) at 16 GiB, so the
help's "uses most of free RAM" was false above ~20 GB of RAM. Raise the 64-bit ceiling to
64 GiB. The adaptive controller + its RAM reserve still bound actual usage and shrink under
memory pressure, and small hosts are unaffected -- the ceiling only binds once RAM-minus-
reserve exceeds it. The coins cache grows lazily to the target, so nothing is pre-allocated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 06:59:25 +02:00
693d2290e0 fix: release RandomX pre-verify cache at shutdown; tune verify-threads help + -maxblocksintransit ceiling
Three small cleanups to the parallel RandomX pre-verify + P2P-window features:

- Call RandomXValidatorShutdown() in Shutdown() to release the ~256MB shared RandomX
  verify cache. It was allocated on first use but never freed, leaking at every exit.
  Safe here: threadGroup.interrupt_all() (earlier in Shutdown) stops the pre-verify
  worker, and the release takes g_rxvMutex so it can't race a mid-flight verify.
- Clarify the -randomxverifythreads help: the pool only helps NETWORK sync, not reindex
  (reindex runs with a window of 1, so the pool does nothing there).
- Clamp -maxblocksintransit to the real BLOCK_DOWNLOAD_WINDOW (1024) ceiling instead of a
  misleading 4096. Values above the window are a silent no-op (FindNextBlocksToDownload
  never fetches beyond pindexLastCommonBlock + BLOCK_DOWNLOAD_WINDOW); log when clamping.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 06:59:25 +02:00
389c8c7383 fix: cap mempool memory usage (-maxmempool) to bound an OOM DoS
This fork never ported Bitcoin's fee-ordered mempool eviction: CTxMemPool has no
TrimToSize/Expire, and LimitMempoolSize's body + call site are both commented out and
reference an undefined DEFAULT_MAX_MEMPOOL_SIZE. So the mempool had no total-size
ceiling. Together with the just-restored removeExpired() and near-free tx admission, a
peer could flood transactions to exhaust every node's memory (incl. pool/payout nodes).

Add a simple admission cap in AcceptToMemoryPool: once the pool exceeds -maxmempool it
refuses new admissions with DoS(0) (no ban -- a full pool isn't the peer's fault). This
is not fee-ordered eviction (that needs the absent TrimToSize machinery) but it bounds
the footprint; removeExpired() already evicts unmineable expired txs on each block
connect. New DEFAULT_MAX_MEMPOOL_SIZE=300 (MB, Bitcoin's default) is far above DragonX's
normal mempool, so normal operation is unaffected. Reviewed: bytes-vs-bytes comparison,
read under LOCK(pool.cs) on a recursive mutex (no deadlock); only the reorg re-add path
and new sends route through it, and only at 300MB.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 04:58:39 +02:00
4e3c0f8f6f fix: apply real DoS score for invalid headers (Misbehaving nDoS/nDoS was always 1)
In the HEADERS handler, an invalid header scored Misbehaving(id, nDoS/nDoS). Because
the call is guarded by `if (nDoS > 0 ...)`, nDoS/nDoS is always exactly 1, so every
invalid header cost a fixed 1 misbehavior point regardless of severity -- it took
~banscore (default 101) invalid headers to ban a peer instead of 1, effectively
disarming the ban backstop against header spam. The two sibling call sites in the
same handler (tx-accept, block-accept) already pass nDoS directly.

Pass the real nDoS so a genuinely-invalid header (e.g. bad-diffbits, DoS 100) bans in
one message. Cannot over-ban honest peers: every DoS>0 header path is genuinely
invalid consensus, and benign/racy headers (unconnectable prevblock, future block,
clock-skew) either score DoS 0 or never reach Misbehaving (double-guarded by
IsInvalid + nDoS>0 + futureblock==0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 04:29:54 +02:00
2e54d9fb4d fix: restore removeExpired() mempool scan (was a no-op -> unbounded mempool DoS)
CTxMemPool::removeExpired() declared `transactionsToRemove` and looped over it
without ever populating it -- the mapTx scan that collects expired txs had been
dropped, so it evicted nothing. Expired txs (past nExpiryHeight) can never be mined
yet were never removed, so a peer could wedge them into every node's mempool
permanently at ~zero cost (never mined -> never pay a fee), growing the mempool
without bound: a memory-exhaustion DoS against every node (incl. pool/payout nodes).

Restore the upstream Zcash/Komodo scan: iterate mapTx, collect txs failing
IsExpiredTx(tx, tipHeight) into a separate list, then remove() them (collect-then-
remove avoids iterator invalidation; recursive=true also evicts the now-unmineable
descendants). Also drops an unused CBlockIndex* local.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 04:29:54 +02:00
4238da9bea fix: cap indexed-node block-tree dbcache so adaptive dbcache feeds the UTXO set
With -addressindex/-spentindex, nBlockTreeDBCache was set to 3/4 of nTotalCache. That
rule was sized for the old fixed 512 MiB dbcache default (~384 MiB), but adaptive
dbcache now makes nTotalCache multi-GB, so on indexed pool/explorer nodes ~3/4 of
several GB was diverted to the block-index LevelDB read cache -- far more than it can
use -- while starving the in-memory UTXO set that actually speeds IBD, and that chunk
is not shrinkable by the memory-pressure controller.

Measured on an 8 GiB box with -addressindex: 4420 MiB block-index cache + 1097 MiB
UTXO set, vs 3859 MiB UTXO on a plain node. Cap the index cache at 1 GiB (ample for
the index read cache; tunable) so the adaptive budget flows to the coins cache.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 01:26:30 +02:00
4e67e687d7 perf: verify each block's RandomX solution once, not twice, during sync
RandomX PoW verification is ~84% of block-connect wall time during network IBD, and
CheckBlock was recomputing it TWICE per block: once in CheckBlockHeader and again in
hush_checkPOW (which has no CBlockIndex, so it cannot use the fRandomXVerified dedup
the parallel pre-verify pool relies on). Skip the redundant recompute inside
hush_checkPOW: CheckBlockHeader runs first in CheckBlock and rejects an invalid
solution before hush_checkPOW is reached, so the block is already verified once.
Equihash, PoW-target and notary checks in hush_checkPOW still run.

A scoped guard (ScopedRandomXSkip) SAVES and RESTORES the thread-local
fSkipRandomXValidation, so it neither clobbers the miner's own skip
(TestBlockValidity -> ConnectBlock re-entry, which would otherwise force the ~256MB
inline RandomX alloc the miner deliberately avoids) nor leaks the flag on an exception.

Measured on an isolated RandomX test chain: RandomX verifies per block 2.0 -> 1.03
(~40% faster network sync). The 2x behavior pre-exists in v1.0.2. Consensus-neutral:
RandomXPreVerify.ConsensusEquivalence gtest passes; each block is still verified
exactly once by CheckBlockHeader.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 01:26:30 +02:00
762e25294f fix: guard BuildWitnessCache against an off-active-chain pindex (heap overflow)
BuildWitnessCache sizes its blockCms buffer from pindex->GetHeight() but the
Phase-1 loop walks the active chain (chainActive.Next), terminating only on
pbi==pindex. If a reorg moved pindex off the active chain while the notify
thread lagged (cs_main is released between per-block ChainTip calls) and the new
active tip is taller, the loop never reaches pindex and, once past pindex's
height, writes blockCms[h-startHeight] out of bounds -- a heap overflow.
Rebuilding witnesses for an abandoned block is meaningless anyway, so bail early
when pindex is not on the active chain; cs_main is held for the whole function,
so the check cannot race the loop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 05:48:39 +02:00
3bb4eb3a5a fix: harden ThreadNotifyWallets IBD read-retry (crash + abandoned disconnects)
Two defects in the IBD ReadBlockFromDisk retry path of ThreadNotifyWallets:

- The connect-loop rebuild indexed recentlyConflicted.first.at(pindex), which
  throws std::out_of_range for a block whose conflict entry was drained in an
  earlier retry cycle. Uncaught under cs_main in the notify boost thread, that
  aborts the node and crash-loops. Use operator[] (empty-list default), i.e.
  best-effort conflict notifications, instead of throwing.

- The disconnect-loop IBD break fell through into the connect loop, which
  advanced pindexLastTip to the new tip and permanently abandoned the pending
  disconnect notifications (leaving wallet witness/anchor state desynced from
  the chain). Add a flag so the break skips the connect loop this cycle and
  truly retries the disconnect on the next one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 05:48:39 +02:00
7914fca0f2 fix: gate the RandomX PoW-verification skip on the in-index checkpoint
RandomXValidationRequired skipped RandomX verification for blocks below the
static top checkpoint (GetTotalBlocksEstimate), while the fork-rejection guard
uses the in-index checkpoint (GetLastCheckpoint). Once the checkpoint list
extends above the RandomX activation height, that asymmetry opens a gap during
IBD/eclipse in which a peer with no RandomX hashpower can get SHA256-grinded,
RandomX-forged blocks accepted (CheckProofOfWork hashes the header including the
attacker-controlled nSolution). Gate the skip on GetLastCheckpoint()->GetHeight()
so a block is PoW-exempt only when provably below a checkpoint the node has
locked into its index -- the same boundary the fork guard uses. Currently inert
(top checkpoint 2838000 < activation 2838976) but becomes live at the next
checkpoint refresh; the check runs under cs_main and only ever adds verification.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 05:48:39 +02:00
810bd6712f fix: make nCoinCacheUsage std::atomic to close an adaptive-dbcache data race
The scheduled AdjustCoinCacheForMemoryPressure task writes nCoinCacheUsage from
the scheduler thread holding no lock, while cs_main-holding threads
(FlushStateToDisk, VerifyDB) read it -- an unsynchronized read/write of a
non-atomic size_t (C++ UB). Make it std::atomic<size_t>; correct the comment
that incorrectly described the access as lock-free/race-free.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 05:48:39 +02:00
bf5b066a8d fix: evaluate wolfSSL_pending() under cs_hSocket in the recv-drain loop
The bulk-streaming recv-drain loop called wolfSSL_pending(pnode->ssl) after
releasing cs_hSocket, racing with SocketSendData (wolfSSL_write) and
CloseSocketDisconnect (wolfSSL_free) on the same TLS session -- a data race and
potential use-after-free on any TLS peer. Capture the pending-byte count inside
the cs_hSocket-locked block and use the captured value for the drain decision.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 05:48:39 +02:00
19e1ce6f00 fix: self-heal a corrupt/torn notarizations DB instead of aborting startup
A torn or corrupt notarizations (dPoW) leveldb -- a 0-byte log left by a torn
snapshot, or a corrupt MANIFEST -- threw at open and was caught by the block-DB
load try/catch, aborting startup with a misleading Error-opening-block-database
message and forcing a full resync. The notarizations DB is non-essential and
node-regenerable, so on open failure move it aside (notarizations.corrupt,
preserving the data in case the error was transient) and regenerate a fresh one;
if the fresh recreate also fails it still propagates as fatal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 05:48:39 +02:00
dc45e7d904 Harden ProcessGetData: log+disconnect instead of asserting on block-read failure
The legacy getdata block-serving path asserted whenever ReadBlockFromDisk failed for a block we had advertised (BLOCK_HAVE_DATA). A single transient I/O error or on-disk corruption, triggerable by any peer getdata, crashed the whole node (observed once during bulk-serve load testing on 176). Now log the failure and disconnect that peer so it can re-fetch from another node, matching the bulk GETBLOCKSTREAM serve path which already fails gracefully. Build-verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 08:08:34 +02:00
1ed468e040 Merge remote-tracking branch 'origin/ibd-wallet-notify-fix' into dev 2026-07-07 07:43:58 +02:00
53b1fe332b Rebrand cleanups: getpeerinfo help example + 1.0.3 debian changelog entry
net.cpp: getpeerinfo help address example 18030->21768 and 'Hush server'->'DragonX server'. debian/changelog: prepend 1.0.3 release entry summarizing IBD speedups, witness fix, bulk streaming, seed phrases, assumeutxo removal. NOTE net.cpp change needs a daemon rebuild to surface in runtime RPC help. Staged on 176; not pushed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 06:35:46 +02:00
9c6ccbb726 Fix dragonx-cli -rpcport help default: 18030 (hush) -> 21769 (DragonX)
The -rpcport help string in bitcoin-cli.cpp hardcoded hush's 18030; the actual default (BaseParams().RPCPort()) is DragonX's 21769, so this was misleading help text only (the CLI already connects to 21769). Set to 21769 and regenerated doc/man/dragonx-cli.1 from the rebuilt binary. NOTE: a separate hush 18030 leftover remains in src/rpc/net.cpp:357 (getpeerinfo help example address) - daemon RPC help, out of scope here. Staged on 176; not pushed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 06:35:46 +02:00
34432e5848 Harvest DragonX packaging + legal artifacts from compliant-rebrand
Legal: correct GPLv3 LICENSE (fixes garbled 'GENERAL GENERAL'), AUTHORS DragonX attribution, COPYING. Packaging: man pages REGENERATED from the 1.0.3 binaries via help2man (dragonxd/dragonx-cli/dragonx-tx.1 -> v1.0.3, correct dates), wired into doc/man/Makefile.am (dist_man1_MANS), orphaned hush*.1 removed. Init/openrc/systemd scripts, Debian packaging (control/changelog/copyright rebranded hush->dragonx + install stubs), example confs taken from origin/compliant-rebrand (c05134e77). REMAINING follow-ups: (1) debian/changelog still tops at 1.0.0 - add a 1.0.3 entry; (2) dragonx-cli --help hardcodes rpcport default 18030 (hush) - fix the HelpMessage string in source then regen. Staged on 176 for review; not pushed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 06:35:46 +02:00
4caf2fc68f Add BIP39 seed phrases (SilentDragonXLite-compatible) and HD transparent keys
Derive transparent (t-addr) keys from the HD seed and add BIP39 mnemonic seed
phrases that are byte-for-byte compatible with SilentDragonXLite, so the same
24 words recover the same shielded and transparent addresses in either wallet.

HD transparent keys:
- Derive t-keys from the seed at m/44'/coin'/0'/0/i (were random CKeys).
- CHDChain gains a version-gated transparent counter; existing wallets load
  unchanged. GenerateNewKey routes through DeriveNewChildKey when enabled
  (-hdtransparent, default on).
- Restore from a seed hex via -hdseed with gap-limit pre-derivation; birthday
  pinned to genesis so the rescan is not clipped.

BIP39 seed phrases:
- Wire the vendored trezor BIP39 lib (src/crypto/bip39) into the build, fix its
  BIP39_WORDS guard, and disable the insecure mnemonic cache.
- Match SDXLite exactly: English wordlist, empty passphrase, PBKDF2 64-byte
  seed, coin type 141, ZIP-32 m/32'/141'/i' and BIP44 m/44'/141'/0'/0/i. Store
  the 32-byte entropy and expand to the 64-byte seed on demand.
- Restore via -mnemonic, create via -usemnemonic, reveal via z_exportmnemonic.

Verified by gtests including a known-answer BIP39 seed vector and z/t address
derivation checks (src/gtest/test_hdtransparent.cpp, test_mnemonic_compat.cpp).
Docs in doc/hd-transparent-keys.md and doc/seed-phrase.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 01:57:18 -05:00
84aefb5475 Remove assumeutxo / UTXO-snapshot feature
Removes the dumptxoutset RPC, -loadutxosnapshot / -loadutxosnapshotunsafe,
the CCoinsViewDB Dump/LoadSnapshot machinery + CUTXOSnapshotHeader, the
AssumeutxoData chainparams anchor, the LoadSnapshotChainstate activation +
reorg-below-H guard, the persisted assumeutxo-height flag, and the gtest.

Rationale: it duplicated the existing bootstrap (same skip-the-genesis-grind
fast-sync, no speed advantage), its only real edge was a trust model we don't
need for this chain, and it was inert anyway (no published snapshot hash in
chainparams). The -loadutxosnapshot load path adopted an external UTXO set and
bypassed genesis validation, so removing it also drops that attack surface.
Builds clean (no dangling references); the kept IBD speedups (RandomX
pre-verify, adaptive dbcache, tlsmanager) are untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 16:42:37 -05:00
1f2b109d95 Add opt-in bulk block streaming (-bulkblocksync)
A single getblockstrm request makes a peer stream a contiguous range of old
blocks back-to-back as ordinary BLOCK messages, amortizing the per-block
round-trip over the whole range instead of the MAX_BLOCKS_IN_TRANSIT_PER_PEER
window. This targets the bandwidth-delay-product ceiling that dominates IBD
from few/high-latency peers below the checkpoint.

Design (off by default; negotiated via a NODE_BULKBLOCKS service bit; the
default getdata IBD path is untouched when disabled):
- protocol: NODE_BULKBLOCKS service bit + getblockstrm/blockstream messages.
- requester: in SendMessages, after FindNextBlocksToDownload, when the first
  needed block is >= BULK_TIP_MARGIN (5000) below the network tip and the peer
  advertises the bit and we are in IBD, request a contiguous range (<=128
  blocks) instead of per-block getdata; mark the range in-flight.
- server: stream the range (caps 128 blocks / 8 MiB; reads outside cs_main;
  per-peer flood throttle), then a trailing blockstream header with the actual
  count sent. Self-suppresses while the server itself is in IBD.
- received blocks ride the existing BLOCK -> ProcessNewBlock path (fully
  validated; checkpoints below 2.84M still apply); the trailing header
  reconciles partial deliveries and the range is freed on a 90s timeout, so a
  partial/withheld/refused batch falls back to the normal path (no leak, no
  permanent gap, no disconnect). In-flight tracking is by literal hash, so a
  reorg cannot orphan range entries.

Hardened against the issues found in two adversarial review passes (drain vs
timeout, partial reconciliation, ownership-guarded frees, one-shot header,
reorg-proof helpers, cs_main hold). Validated end-to-end between two local
v1.0.3 nodes (128/128 and partial serves; height advanced; no errors).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 21:22:54 -05:00
78ea2aac5b Add -maxblocksintransit: tunable per-peer block-download window
The per-peer in-flight block window (MAX_BLOCKS_IN_TRANSIT_PER_PEER) was a
hardcoded 16. On a single, high-latency peer during IBD the transfer is
bandwidth-delay-product bound (window / RTT), so with tiny sub-checkpoint
blocks the window, not bandwidth, is the ceiling — measured ~4x throughput
going 16 -> 64 on a 350ms-RTT peer. Make it a runtime flag (default 16,
clamped 1..4096), logged at startup. No behavior change at the default.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 21:22:32 -05:00
2419ed7bf7 Fix flaky build: make version-probe pipes SIGPIPE-safe
util/build.sh runs with `set -eu -o pipefail`. `eval "$MAKE" --version | head -n2`
(and the analogous `as --version | head`) can race: head closes the pipe after N
lines, make/as catch SIGPIPE and exit non-zero, pipefail propagates the failure,
and errexit aborts the build before any compilation. Append `|| true` so these
purely-informational version prints can never fail the build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 21:21:29 -05:00
a9b1b4085f Fix automake -lcurl portability lint in Makefile
LIBBITCOIN_SERVER was fed into both EXTRA_LIBRARIES (a list of buildable
library files) and several _LDADD link lines. Embedding the -lcurl linker
flag inside it made automake reject it in the EXTRA_LIBRARIES context
("'-lcurl' is not a standard library name"). Make LIBBITCOIN_SERVER a pure
file and route -lcurl through its own LIBCURL variable, added to the
dragonxd, hush-gtest, and test_bitcoin link lines after libbitcoin_server.a
(whose objects reference curl symbols) so static link order stays correct.

Verified with a clean Windows cross-build (-DCURL_STATICLIB) and a native
Linux build: both link cleanly and the automake lint is gone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 16:58:31 -05:00