Commit Graph
87 Commits
Author SHA1 Message Date
DanSandClaude Opus 5 9734402d7b wallet: create new wallets from a BIP39 seed phrase by default
New wallets now get an exportable 24-word phrase instead of a random seed that
no phrase can ever reproduce. Only wallets with no seed yet are affected;
GenerateNewSeed is reachable from one place, under !HaveHDSeed(), and all three
key stores refuse to replace an existing seed.

This is safe to default on now that the storage form is backwards compatible: the
expanded 64-byte BIP39 seed is what gets stored, so a binary predating any of
this reads it and derives the same keys.

Verified on an isolated chain before flipping:
  - a new-format wallet reopened with the tagged v1.1.0 binary, which has no
    knowledge of the entropy record, listed identical addresses;
  - restoring only the 24 words into a fresh datadir recovered every address.

Note this changes what a new wallet is, not what an existing one is: the same
entropy yields a different key tree depending on which side of this commit
created the wallet. Nothing migrates, and nothing needs to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 07:01:22 +02:00
DanSandClaude Opus 5 20b2cbe830 wallet: store the expanded BIP39 seed for mnemonic wallets
Mnemonic wallets stored the 32-byte BIP39 entropy as the HD seed and relied on
CHDChain.fMnemonicSeed to tell the deriver to expand it first. That flag is
version-gated in the CHDChain serialisation, so a binary that predates it reads
the record, never consumes the trailing byte, and derives from the raw entropy --
a different key tree, silently, with no error.

Store the expanded 64-byte BIP39 seed instead, with fMnemonic = false, and keep
the entropy in its own display-only record. Derivation then reads the stored
bytes directly on every binary, old or new, so key trees are identical and no
CHDChain version bump or minversion fence is needed. The wallet format stays
readable by earlier releases rather than becoming one-way.

Addresses are unchanged: the previous format expanded the entropy on every read
and fed the same 64 bytes to Master(). This is also the form the tree already
round-trips through -- z_exportwallet dumps the expanded seed, and restoring that
hex via -hdseed installs it with fMnemonic = false.

Write order is load-bearing: seed first, entropy second. A crash between them
leaves a wallet with a seed and no phrase, which is merely inconvenient. The
reverse would leave an entropy record with no seed, and the next start would mint
a different seed while the wallet still held a phrase for the old one.

-usemnemonic still defaults to false; only explicit opt-in and -mnemonic restores
take this path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 06:39:38 +02:00
DanSandClaude Opus 5 2d7dd90c55 wallet: plumb the mnemonic entropy through CWallet
Wires the key store secret to the database records: load and store paths on
CWallet, the two ReadKeyValue arms, and the export path.

GetMnemonicPhrase now prefers the entropy record and verifies it before printing:
a phrase is only returned if expanding it reproduces the seed derivation actually
uses. It falls back to the existing fMnemonicSeed path, so wallets that store the
entropy AS the seed keep working unchanged.

IsMnemonicSeed() now means "a phrase is available" rather than "the seed is the
entropy", which is what every caller actually wants.

Still a no-op on every existing wallet: nothing creates an entropy record yet, so
GetMnemonicEntropy returns false and the old code path is taken.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 06:37:43 +02:00
DanSandClaude Opus 5 3caec548ae wallet: persist the mnemonic entropy in wallet.dat
Adds the record pair for the entropy, mirroring "hdseed"/"chdseed": a plaintext
form, an encrypted form that erases its plaintext counterpart the way
WriteCryptedKey does, and an erase.

Both new types are registered in IsKeyType so -salvagewallet preserves them.
Without that, salvage would silently drop the phrase while keeping the wallet
otherwise intact.

The records are defined but nothing writes them yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 06:30:18 +02:00
DanSandClaude Opus 5 2a7fdcc1db wallet: add an optional mnemonic-entropy secret to the key stores
Additive plumbing for storing a BIP39 entropy alongside the HD seed, mirroring
how the seed itself is handled through both key store layers: a plaintext member
on CBasicKeyStore, an encrypted pair on CCryptoKeyStore, encryption during the
unencrypted-to-encrypted conversion in EncryptKeys with the plaintext cleared,
and decryption on unlock.

RawHDSeed and CKeyingMaterial are the same secure_allocator vector type, so the
entropy passes through EncryptSecret/DecryptSecret with no adaptation.

Nothing calls this yet; there is no behaviour change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 06:23:09 +02:00
DanSandClaude Opus 5 143c33de48 wallet: erase the plaintext HD seed record when the wallet is encrypted
CWalletDB::WriteCryptedHDSeed wrote the "chdseed" record and left "hdseed" in
place, unlike WriteCryptedKey which erases "key"/"wkey" after writing "ckey".
No erase of "hdseed" existed anywhere in src/wallet/.

CDB::Rewrite does not save us: EncryptWallet calls it with pszSkip defaulted, so
it copies every surviving record verbatim into the new file. The result is that a
wallet created unencrypted and later encrypted keeps its raw HD seed in cleartext
on disk permanently, and reloads it into memory on every start.

Add CWalletDB::EraseHDSeed and call it from CWallet::SetCryptedHDSeed after the
encrypted record is written, through the same CWalletDB so it shares
EncryptWallet's transaction. Erase returns true on DB_NOTFOUND, so a wallet that
was never written in plaintext is unaffected.

The erase is deliberately best-effort and only logs on failure. A hard failure
here propagates into CCryptoKeyStore::EncryptKeys, which EncryptWallet turns into
assert(false) with half the keys encrypted in memory; a warning is strictly
better than that.

Note this path is only reachable with -developerencryptwallet, which is
experimental and off by default on this chain, so this is a latent fix rather
than a live one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 06:14:31 +02:00
DanSandClaude Opus 5 e2f88175ab wallet: default -autoshield ON only when the HD seed is known-recoverable
Autoshield moves mined coinbase into a z-address that only this wallet's HD seed
can re-derive. Defaulting that ON is only defensible where the user can actually
restore that seed.

Two cases fail that test. A seedless legacy wallet has a seed minted onto it
silently at first start, so no backup the user already holds contains it. And a
wallet seeded by an earlier build predates provenance recording, so we cannot
tell which case it was. Both are now classified as not-known-recoverable and
autoshield stays off there until the operator backs the seed up and passes
-autoshield=1. An explicit -autoshield=0/1 still wins in either direction.

Wallets this software created on an empty datadir, or restored from a
user-supplied -mnemonic/-hdseed, keep the ON default: in both cases the user
either has the phrase or supplied the seed themselves.

Verified on real wallets: a wallet carrying no origin record is classified
unknown and logs "autoshield left OFF by default: HD seed origin 4"; a wallet
created by the previous commit logs "autoshield enabled" with no
re-classification, confirming the record persists rather than being recomputed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 03:22:55 +02:00
DanSandClaude Opus 5 a494eabdce wallet: harden HD seed/chain persistence and record seed provenance
Autoshield sends mined coinbase to a seed-derived z-address, so the records that
decide how the seed derives keys become fund-safety critical. Three of them were
not treated that way.

InstallHDSeed wrote the seed record before the chain record, non-transactionally.
A crash between the two left a wallet holding a seed with no hdchain: on the next
load hdChain silently reverts to defaults, clearing fMnemonicSeed -- which
switches the derivation input from the expanded BIP39 seed to the raw entropy --
and resetting saplingAccountCounter. Write the chain first; the opposite torn
state is harmless and self-heals, because HaveHDSeed() is then false and init
installs again.

The hdchain record was read as a bare deserialise inside a catch-all with strErr
never set, and it is not a key type, so a corrupt record was downgraded to a
non-critical error and the node booted into the wrong key tree. Report it, track
whether it was read, and refuse to load a wallet that holds a seed but no
readable hdchain. Also preserve hdchain through a keys-only salvage, which would
otherwise drop it and produce exactly the state we now refuse.

GenerateNewSeed silently fell back to a random seed when BIP39 generation failed,
producing a wallet that looks mnemonic-capable but whose words can never be
exported and which no seed phrase can restore. A user who asked for -usemnemonic
now gets that or a hard failure.

Finally, record how the seed came to exist -- created on an empty wallet,
restored from -mnemonic/-hdseed, retrofitted onto a pre-existing seedless wallet,
or predating this record. A retrofitted seed is in no backup the user already
holds, so a feature that moves funds into addresses only that seed can re-derive
must not enable itself there by default. Nothing consumes this yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 03:18:35 +02:00
DanSandClaude Opus 5 dad162a89a wallet: pick the autoshield destination by seed re-derivation, in-gap only
resolveDestination() took the first spendable address in std::set order, which
orders on the raw Sapling diversifier (zcash/Address.hpp:95-98) -- uncorrelated
with anything the operator can see, and unstable across restarts as addresses
are added.

Worse, GetSaplingPaymentAddresses() also returns z_importkey/z_importwallet
addresses, and CKeyMetadata is NOT evidence of provenance: both hdKeypath and
seedFp are copied verbatim out of the import source (rpcdump.cpp:511-516 ->
wallet.cpp:5440-5441) with no verification. A crafted import can therefore claim
this wallet's seedFp and keypath m/32'/coin'/0' and capture every shielded
mining reward into a key no seed restore can reproduce. Filtering on metadata
would not have caught that.

Derive instead. A bare -mnemonic/-hdseed restore pre-derives exactly
-mnemonicsaplinggap sapling accounts from index 0 with saplingAccountCounter
reset (init.cpp:2349-2355), so the only self-recoverable destinations are the
default addresses of m/32'/<coin>'/i' for i below the gap. Walk that window from
the seed and take the lowest index the wallet holds a spending key for. Deriving
is the only authoritative test and cannot be spoofed.

When nothing in the window is held yet, derive the next account -- but only if it
will land inside the window. GenerateNewSaplingZKey does not derive at
saplingAccountCounter: its do/while skips indices already held
(wallet.cpp:150-157), so a bare counter-below-gap test is unsound. Predict the
lowest free index at or above the counter and post-verify the returned address.
If no free account remains below the gap, refuse the round and leave the coinbase
transparent -- transparent funds are still recoverable through the 1000-key
transparent gap, an unfindable note is not.

Refusing is safe: main_impl turns a false return into a clean skip, and main()
always advances nextAutoShield and clears fAutoShieldRunning, so a refused round
cannot latch the feature off.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 02:52:34 +02:00
DanSandClaude Opus 5 bb292a89cc test: merge feature/autoshield-coinbase into v1.1.0 for isolated testing
Not for release. Built solely to exercise auto-shield-coinbase on an isolated
mining chain, since the feature is a no-op on a non-mining node (the canary
seed has no balance, no z-address, no coinbase, and does not mine).

Merge is clean: util/build-win.sh auto-merged, keeping both our ARCH=default
and their -Wa,-mbig-obj. Checkpoints, verify-once guard and the version bump
all survive.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 19:24:20 -05:00
DanSandClaude Opus 5 660678f9bb build: bump version to 1.1.0
CLIENT_VERSION 1000350 -> 1010050. Goes to 1.1.0 rather than 1.0.4 because
1.0.3 is already burned and ambiguous: origin/dragonx's debian changelog
already claims 1.0.3 and the daemon bundled in the ObsidianDragon 2.0.1
installer is labelled v1.0.3-dc45e7d90, so a 1.0.4 would sort above builds
that contain less.

Bumped in configure.ac (authoritative) and in the src/clientversion.h fallback
used when HAVE_CONFIG_H is unset, which the header itself asks to be kept in
sync.

DELIBERATELY NOT BUMPED: SPROUT_VALUE_VERSION, SAPLING_VALUE_VERSION and
SAPLING_VALUE_OPTIONAL_VERSION in chain.h. Those are thresholds marking the
CLIENT_VERSION that INTRODUCED each block-index format, not "the current
version". Raising SAPLING_VALUE_OPTIONAL_VERSION to 1010050 would push every
record written by a v1.0.3 node (nVersion 1000350) into the legacy
raw-CAmount branch of the deserializer and misparse it. The stale comment
naming 1000350 as the current CLIENT_VERSION is updated; the constants stand.

1010050 >= 1000350, so this build still writes and reads the optional format.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 19:15:56 -05:00
DanSandClaude Opus 5 fbcf160478 fix(pow): only dedup RandomX when CheckBlockHeader verified it; drop fake git id
Brings onto dev the two fixes that until now existed only on release/1.0.4, so
nothing is stranded on a branch we are not shipping from.

1. GUARDED VERIFY-ONCE (main.cpp)

4e67e687d arms the RandomX dedup unconditionally whenever fCheckPOW is set. But
CheckBlockHeader returns early -- BEFORE reaching its RandomX check -- for a
block whose timestamp is >60s in the future (*futureblockp==1), and CheckBlock
deliberately continues on that path. There, hush_checkPOW is the ONLY RandomX
verification the block gets, so suppressing it leaves the block unverified.

Not a chain-acceptance hole: ConnectBlock rejects futureblock != 0, so such a
block never joins the chain. But it silently weakens DoS banning -- an invalid
future block gets rejected for its timestamp instead of for bad PoW, which is a
regression against the un-deduped behaviour it replaced.

ScopedRandomXSkip now takes an `arm` flag and CheckBlock passes fHeaderChecked,
so the dedup applies only where the header check actually completed and did the
verification. Strictly a tightening: it can only cause MORE verification than
before, never less.

2. NO FAKE GIT IDENTITY (clientversion.cpp)

A hardcoded `#define GIT_ARCHIVE 1` with GIT_COMMIT_ID "a86845f3dc", dated Feb
2018, is reached whenever build.h supplies no BUILD_DESC -- i.e. any build
without git metadata, which is exactly the tarball/CI release case. Such
binaries reported themselves as that Komodo commit regardless of content; a
build here did precisely that before this was found. The archive substitution
placeholders are kept, so a real git-archive export still works; a git-less
build now reports "-unk", which is honest and greppable.

Both syntax-clean. Rebuild and re-validation on EPYC follows; the earlier
validated binary (md5 fe83d70fec5b50c38bf65ea6c733ffa9) predates these.

release/1.0.4 is parked, not deleted -- its commit records why the v1.0.2
lineage cannot ship (block-index format incompatibility with dev-written
chainstate).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 19:15:56 -05:00
DanSandClaude Opus 5 7dc904c96f perf(sync): extend DRAGONX checkpoints to 3,226,000; build portable RandomX
The RandomX skip in RandomXValidationRequired() has never fired in production.
It skips verification below the last in-index checkpoint, but the DRAGONX
checkpoint table ended at 2,838,000 while ASSETCHAINS_RANDOMX_VALIDATION is
2,838,976 -- the window was empty by 976 blocks. Every block since the RandomX
activation has been fully verified, at ~65ms per hash on the fastest x86 core
available and ~180ms on a typical user machine.

Extends the table by 388 entries at stride 1000, from 2,839,000 to 3,226,000
(tip - ~5,600, far beyond any reorg this chain has produced; max observed depth
is 3-4). Blocks requiring a RandomX verify drop from 391,447 to ~5,500. The
same extension carries the existing script/zk-proof skip (fScriptChecks,
fExpensiveChecks) over the same range.

Checkpoint data verification, before it went anywhere near source:
  - generated on a continuously-synced node
  - all 388 hashes identical on 4 other full nodes (388/388 on each)
  - reverse-verified hash -> height, all on the active chain
  - re-extracted from the patched file and diffed against the verified set
  - 2,838 pre-existing entries unchanged; all 3,226 ascending and unique
Trailer fields computed from RPC, not util/checkpoints.pl, which greps a
rotating debug.log and assumes 1440 blk/day (DragonX is 2400).

Also switches all three build scripts from -DARCH=native to -DARCH=default.
RandomX's CMakeLists maps ARCH=native to -march=native, tuning the binary to
the build host: a Zen4 build emitted 746 AVX-512 instructions into
librandomx.a, and every seed reports avx512f=no, so that binary would SIGILL
inside RandomX fleet-wide -- and on any user CPU older than the build machine.
build-win.sh had the same flag, so shipped Windows binaries inherited it.
ARCH=default keeps -maes and per-file -mssse3/-mavx2, so the portable baseline
costs essentially nothing. Note build.sh skips cmake entirely when
src/RandomX/build/ exists, so a stale dir silently preserves the old ARCH.

Validated on an isolated datadir on an EPYC seed, bootstrap -> tip:
  - below 3,226,000 (RandomX skipped):  91.3 blk/s (22,823 blocks / 250s)
  - at/above 3,226,000 (verified):       4.7 blk/s
  ~19x at the boundary. The 4.7 blk/s baseline matches a same-day restore on
  the old binary, corroborating it independently.
  - gettxoutsetinfo at height 3,231,951 BYTE-IDENTICAL to a live node
    (hash_serialized 4885c2374ef8b84c648b97d560a57cfcc99bb979142dc89c2a5ccbc90a1f1692,
     222,635 txs/txouts, 15,826,352 bytes, total 667909.93689180)
  - synced through all 388 new checkpoints with zero rejections
  - verifychain 4 (500) and 3 (2000) both true
Binary: v1.0.3-bf3c33c53-dirty, stripped md5 fe83d70fec5b50c38bf65ea6c733ffa9

Checkpoints decay at 2,400 blocks/day; regenerating them belongs on the
release checklist.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 19:15:56 -05:00
DanSandClaude Opus 4.8 4d72e5fc30 wallet: fix sweep-scheduler wedge and unsynchronized driver mutation
The zaddr-sweep op cleared fSweepRunning/nextSweep only on the sweepComplete
success path (inside main_impl), so a cancelled or throwing sweep left
fSweepRunning stuck true. Since fSweepRunning now also gates consolidation and
the default-on autoshield, a persistently failing sweep (e.g. a corrupt-witness
note) would wedge all three background ops for the session.

Move the scheduler bookkeeping into main() so it runs on every terminal state
(success/failure/exception/cancel), guarded by op id. Preserve the intended
"keep draining every block until swept" model: on a successful-but-incomplete
round the flag stays set and nextSweep is not advanced; on completion OR on
failure/exception the flag is released and nextSweep backs off one interval, so
a failing sweep no longer retries every block or wedges the other ops.

Also fix RunSaplingSweep to take cs_wallet itself (was AssertLockHeld, a no-op
in release builds) since ChainTip does not hold it there and the driver mutates
scheduler state + enqueues -- matching RunSaplingConsolidation and
RunAutoShieldCoinbase.

sweepComplete is recorded via a new member; saplingSweepOperationId made public.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-21 18:55:16 -05:00
DanSandClaude Opus 4.8 ca730a5d98 wallet: fix consolidation-scheduler wedge (ran every block; dead mutual-exclusion)
The Sapling auto-consolidation scheduler never advanced nextConsolidation after
init and never set fConsolidationRunning, so once the tip passed the init
threshold `-consolidation` dispatched a consolidation op every block instead of
once per -consolidationinterval, and every guard that reads fConsolidationRunning
(in RunSaplingSweep, and in the new autoshield driver) was dead.

Mirror the intended scheduler model:
- RunSaplingConsolidation sets fConsolidationRunning=true before dispatch and
  self-guards with `if (fConsolidationRunning) return;`.
- The consolidation op advances nextConsolidation = consolidationInterval +
  tipHeight and clears fConsolidationRunning on every terminal state
  (success/failure/exception/cancel), guarded by op id so only the current op
  mutates scheduler state.
- saplingConsolidationOperationId moved to public so the op can read it.

Restores the documented once-per-interval cadence and makes the
sweep/consolidation/autoshield mutual-exclusion guards effective.

Note: the sweep op has the same latent wedge (fSweepRunning/nextSweep are
cleared only on the sweepComplete success path, so a cancelled or throwing
sweep leaves fSweepRunning stuck true) - left for a follow-up; this commit is
the template to port.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-21 18:28:17 -05:00
DanSandClaude Opus 4.8 2ebbbc777c wallet: add default-on auto-shield-coinbase; drain miner coinbase into a z-addr
On this ac_private=1 chain miners accumulate one transparent coinbase UTXO per
block (the only transparent output the chain permits). It had to be shielded
manually via z_shieldcoinbase, and left unshielded it is the sole persistent
metadata leak on the chain and the source of miner UTXO-fragmentation send
failures.

Add AsyncRPCOperation_autoshieldcoinbase: a periodic, default-on wallet op
driven from CWallet::ChainTip alongside sweep/consolidation, draining matured
coinbase into a wallet-owned Sapling z-address in size-bounded batches.

- Enqueue-only driver (RunAutoShieldCoinbase): takes only cs_wallet in the
  notify context; all gathering runs on the async worker under LOCK2(cs_main,
  cs_wallet).
- Dedicated op (not a reuse of z_shieldcoinbase) so it never toggles mining.
- Default-ON but conditional: silent no-op on -disablewallet, external
  -mineraddress, non-mining, or locked wallets (explicit IsLocked() guard).
- Destination is reuse-then-create; -autoshieldaddress override is
  spend-key-validated at init so funds cannot be stranded.
- Sweep-model bookkeeping: advances nextAutoShield and clears the running flag
  on every terminal state; an op-id guard stops a stale op clobbering scheduler
  state; a self-guard stops cancel/re-enqueue churn.
- Sietch-padded output shape matches manual z_shieldcoinbase txns.

Config: -autoshield (default true), -autoshieldinterval, -autoshieldaddress,
-autoshieldfee (range-validated), -autoshieldminutxos.

Incorporates fixes from an 8-angle code review: CAmount fee type with init-time
range validation, op-id-guarded flag bookkeeping, driver self-guard, and a
tipHeight-consistent NU-activation guard.

Note: the fConsolidationRunning / nextConsolidation consolidation-scheduler
wedge is a pre-existing bug, left for a separate change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-21 02:06:09 -05:00
DanSandClaude Fable 5 1c3523aac1 build(win): add -Wa,-mbig-obj to the mingw cross-compile flags
Large template/boost-heavy TUs (e.g. asyncrpcoperation.cpp) exceed the standard
PE/COFF ~32k-section limit under mingw-w64, which makes GNU ld emit
"dangerous relocation" on .pdata and crash (SIGSEGV) at link. The bigobj COFF
variant lifts that limit; this is the same flag Bitcoin Core sets for its mingw
host. Fixes the Windows daemon cross-compile.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 16:56:13 -05:00
DanS 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanS 1ed468e040 Merge remote-tracking branch 'origin/ibd-wallet-notify-fix' into dev 2026-07-07 07:43:58 +02:00
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanS f763e3f1e7 Merge ibd-sync-speedups into dev
Parallel RandomX PoW pre-verify, adaptive dbcache, UTXO snapshot, P2P/TLS sync fixes.
2026-06-28 16:11:38 -05:00
DanS f8f13f9027 Merge sapling-witness-rebuild-fix into dev
Sapling witness desync fix + parallel witness-cache rebuild + version bump to 1.0.3.
2026-06-28 16:11:38 -05:00
DanSandClaude Opus 4.8 bf1b4cffe0 Bump version to 1.0.3
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 16:04:51 -05:00
DanSandClaude Opus 4.8 82d77344d2 Fix Sapling witness desync and parallelize witness cache rebuild
Wallets upgraded across the 1.0.1->1.0.2 network transition could end up
with note witnesses stuck at a stale height, causing z_sendmany /
z_mergetoaddress to fail to build a valid spend. Root cause was a trio of
issues that let a desynced witnessHeight perpetuate instead of self-healing:

- DecrementNoteWitnesses left witnessRootValidated and the witness deque in
  an asymmetric state on the size<=1 path.
- VerifyAndSetInitialWitness blindly trusted witnessHeight instead of
  validating the cached root against the chain, so a bad height survived.
- UpdatedNoteData copied witnessHeight even when no witnesses were present.
- witnessRootValidated was uninitialized and never serialized, so a garbage
  true value could short-circuit the self-heal.

Fixes:
- Default witnessRootValidated to false (in-memory only; never serialized).
- VerifyAndSetInitialWitness now validates the cached witness root against
  the block's hashFinalSaplingRoot and reseeds on mismatch.
- Symmetric reset of witness state in DecrementNoteWitnesses.
- Guard the witnessHeight copy in UpdatedNoteData behind a non-empty
  witnesses check.
- Defensive majority-root guard in GetSaplingNoteWitnesses.

Also rewrites BuildWitnessCache to rebuild the witness cache in parallel
(per-block commitment extraction + worker pool), cutting a full repair from
~28 min to ~2 min. Tunable via -witnessbuildthreads and -witnessfastrebuild;
output verified byte-identical to the serial path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 16:03:23 -05:00
DanSandClaude Opus 4.8 1673cfb6dc IBD/sync speedups: parallel RandomX pre-verify, adaptive dbcache, P2P download fixes
- Parallel RandomX PoW pre-verification pool (CCheckQueue) run ahead of the serial
  connect; consensus-neutral (inline CheckRandomXSolution fallback still verifies
  anything not pre-verified). New -randomxverifythreads (default = -par).
- Adaptive dbcache: default sizes the UTXO/coins cache to most of RAM and shrinks
  under memory pressure, always leaving a reserve free; -dbcache pins a fixed value.
- P2P block download: bounded socket recv-drain loop (tlsmanager); frontier-block
  reassignment to break head-of-line stalls (-blockreassigntimeout); ProcessGetData
  serves a bounded batch of blocks per pass instead of one (fixes the serve-side
  one-block-per-tick throttle that caps download network-wide).
- assumeutxo: dumptxoutset RPC + LoadSnapshot machinery + AssumeutxoData chainparams.
- Signed bootstrap verification (util/bootstrap-dragonx.sh, util/sign-bootstrap.md).
- gtest: RandomX pre-verify consensus-equivalence test + UTXO-snapshot round-trip;
  revived the gtest harness (Makefile.am include fix, Makefile.gtest.include).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 12:30:10 -05:00
DanS 2f3f320d28 Handle ReadBlockFromDisk failure during IBD gracefully
During Initial Block Download, block data may not be flushed to disk
when the wallet notification thread tries to read it. Instead of
crashing with a fatal error, log a message and retry on the next cycle.
2026-03-27 14:01:50 -05:00
DanS 2b011d6ee2 fix windows build 2026-03-19 10:09:18 -05:00
DanS faa3e925cd fix windows bootstrap script, add mirror fallback 2026-03-17 18:37:40 -05:00
DanS ddd851dc11 Bump version to 1.0.2 2026-03-17 04:11:32 -05:00
DanS 752590348b Fix sapling pool persistence and add subsidy/fees to getblock RPC
Lower SPROUT_VALUE_VERSION and SAPLING_VALUE_VERSION constants in
chain.h from upstream Zcash values (1001400/1010100) to 1000000.
When DragonX was rebranded from HUSH3, CLIENT_VERSION was reset from
3.10.5 to 1.0.0, falling below these thresholds. This caused
nSaplingValue to silently skip serialization, so the sapling pool
total reset to 0 on every node restart. Explorer nodes should reindex
once after upgrading.

Add subsidy and fees fields to the getblock RPC response so explorers
can display the correct 3 DRGX block reward separately from fees,
instead of showing the combined coinbase output as the reward.
2026-03-15 16:07:16 -05:00
DanS f0cb958cac Fix fresh sync failure at diff reset height 2838976
Fresh-syncing nodes rejected the on-chain min-diff block at the
RANDOMX_VALIDATION activation height (2838976) because GetNextWorkRequired
computed the expected nBits from the preceding normal-difficulty blocks,
producing 469847994 instead of the on-chain 0x200f0f0f (HUSH_MINDIFF_NBITS).
This caused all seed nodes to be banned with "Incorrect diffbits" and the
node could never sync past that height.

Two changes:

1. GetNextWorkRequired (pow.cpp): Return nProofOfWorkLimit at the exact
   RANDOMX_VALIDATION activation height, matching the on-chain diff reset.

2. ContextualCheckBlockHeader (main.cpp): Raise DragonX daaForkHeight to
   RANDOMX_VALIDATION + 62000, covering the window where nBits was never
   validated (diff reset at 2838976 through the attack at ~2879907).

Tested by invalidating block 2838975 and reconsidering — node re-validated
through the diff reset and attack window, syncing back to tip with zero
bad-diffbits rejections.

Bump version to 1.0.1.
2026-03-12 01:25:21 -05:00
DanS 6d56ad8541 Add --linux-compat build option for Ubuntu 20.04 binaries
Build release binaries inside an Ubuntu 20.04 Docker container
to produce executables with lower GLIBC requirements, compatible
with older Linux distributions.

- Add Dockerfile.compat (Ubuntu 20.04 base, full depends rebuild)
- Add .dockerignore to exclude host build artifacts from context
- Add --linux-compat flag to build.sh with Docker build/extract/package
- Strip binaries inside container to avoid root ownership issues
2026-03-10 19:39:55 -05:00
DanS 449a00434e test scripts 2026-03-10 17:07:16 -05:00
DanS 5cda31b505 update checkpoints 2026-03-09 16:39:00 -05:00
DanS ec517f86e6 update checkpoints again 2026-03-09 16:29:55 -05:00
DanS 33e5f646a7 update checkpoints 2026-03-06 18:10:31 -06:00
DanS c1408871cc Fix Windows cross-compilation linker error and gitignore .exe artifacts 2026-03-05 05:22:44 -06:00
DanS 0a01ad8bba Fix nBits validation bypass and restore CheckProofOfWork rejection for HACs
Two critical vulnerabilities allowed an attacker to flood the DragonX chain
with minimum-difficulty blocks starting at height 2879907:

1. ContextualCheckBlockHeader only validated nBits for HUSH3 mainnet
   (gated behind `if (ishush3)`), never for HAC/smart chains. An attacker
   could submit blocks claiming any difficulty and the node accepted them.
   Add nBits validation for all non-HUSH3 smart chains, gated above
   daaForkHeight (default 450000) to maintain consensus with early chain
   history that was mined by a different binary.

2. The rebrand commit (85c8d7f7d) commented out the `return false` block
   in CheckProofOfWork that rejects blocks whose hash does not meet the
   claimed target. This made PoW validation a no-op — any hash passed.
   Restore the rejection block and add RANDOMX_VALIDATION height-gated
   logic so blocks after the activation height are always validated even
   during initial block loading.

Vulnerability #1 was inherited from the upstream hush3 codebase.
Vulnerability #2 was introduced by the DragonX rebrand.
2026-03-05 03:09:38 -06:00
DanS 85c8d7f7dd Rebrand hush3 to DragonX and share RandomX dataset across mining threads
Minimal rebrand (see compliant-rebrand branch for full rebrand):
- Rename binaries: hushd/hush-cli/hush-tx → dragonxd/dragonx-cli/dragonx-tx
- Default to DRAGONX chain params without -ac_* flags (randomx, blocktime=36, private=1)
- Update configure.ac: AC_INIT([DragonX],[1.0.0])
- Update client version string and user-agent to /DragonX:1.0.0/
- Add chainparams.cpp with DRAGONX network parameters
- Update build.sh, miner.cpp, pow.cpp for DragonX
- Add bootstrap-dragonx.sh utility script
- Update .gitignore for release directory

Share single RandomX dataset across all mining threads:
- Add RandomXDatasetManager with readers-writer lock, reducing RAM from
  ~2GB per thread to ~2GB total plus ~2MB per thread for the VM scratchpad
- Add LogProcessMemory() diagnostic helper for Linux and Windows
2026-03-04 18:42:42 -06:00
DanS d6ba1aed4e Fix RandomX validation exploit: verify nSolution contains valid RandomX hash
- Add CheckRandomXSolution() to validate RandomX PoW in nSolution field
- Add ASSETCHAINS_RANDOMX_VALIDATION activation height per chain
  (DRAGONX: 2838976, TUMIN: 1200, others: height 1)
- Add CRandomXInput serializer for deterministic RandomX hash input
- Fix CheckProofOfWork() to properly reject invalid PoW (was missing
  SMART_CHAIN_SYMBOL check, allowing bypass)
- Call CheckRandomXSolution() in hush_checkPOW and CheckBlockHeader

Without this fix, attackers could submit blocks with invalid RandomX
hashes that passed validation, as CheckProofOfWork returned early
during block loading and the nSolution field was never verified.
2026-03-03 17:28:49 -06:00