Two independent fund-visibility bugs on the rescan path, both pre-existing.
1. AN INTERRUPTED RESCAN RECORDED ITSELF AS COMPLETE.
ScanForWalletTransactions returns a bare `int ret` (a found-tx count) on both its
abort and shutdown bail-outs, so the caller could not tell "finished" from
"stopped at block H". init.cpp then ran, unconditionally:
pwalletMain->ScanForWalletTransactions(pindexRescan, true);
pwalletMain->SetBestChain(chainActive.GetLocator()); // TIP locator
An interrupted scan therefore stamped the wallet as scanned all the way to the
chain tip. On the next start, the `chainActive.Tip() != pindexRescan` guard just
above sees no work to do and skips the rescan entirely, so every transaction in
the never-scanned range stays out of mapWallet -- permanently invisible to
getbalance and unspendable. That is exactly the case `-rescan` exists for: a key
import, right after ClearNoteWitnessCache has run.
Adds CWallet::fLastRescanCompleted, set false at scan entry and true only on the
normal exit, and gates the init.cpp locator write on it. Deliberately NOT
overloading the int return, which callers already use as a tx count.
2. fAbortRescan WAS NEVER RESET.
wallet.h declares it, AbortRescan() sets it true, and nothing in src/ ever sets
it false. The abortrescan RPC is live. After one call, for the remaining lifetime
of the process:
- every ScanForWalletTransactions returns immediately, so re-running an import
silently no-ops while the RPC still reports success;
- BuildWitnessCache bails on the same flag on EVERY call, including the routine
per-block extension from ChainTip.
Witness heights then diverge across notes while the chain advances, and
GetSaplingNoteWitnesses elects the majority root as the anchor and returns
boost::none for every note that disagrees. Those notes still show in the balance
but cannot be spent. Cleared at scan entry and consumed in the abort branch.
Also in BuildWitnessCache's abort branch: stop clearing fRescanning. A witness
rebuild is not a rescan and must not touch a flag ScanForWalletTransactions owns.
3. Both bail-out log lines had two format specifiers and one argument:
LogPrintf("%s: Rescan aborted at block %d\n", pwalletMain->rescanHeight);
tinyformat's "too many conversion specifiers" guard is disabled in this tree
(tinyformat.h: `if(*fmt != '\0' && 0 ) // disabled due to complaints`), so this
does not throw -- verified by compiling the exact call against this tree's
tinyformat.h. It emits "3841207: Rescan aborted at block " with the height in the
__func__ slot, the real height dropped, and the trailing newline swallowed so the
next log line concatenates onto it. On the one path an operator has to diagnose
an interrupted rescan, the only record was corrupt. Fixed at both sites, and the
state updates moved above the log call.
4. Makes SetBestChainINTERNAL report whether the atomic write committed, so a
checkpointing caller can tell (six failure paths returned void). Adds
SetBestChainNoFlush, which opens the wallet DB with fFlushOnClose=false: the
default ctor makes ~CDB run a full BDB txn_checkpoint over the whole cache, which
is fine hourly but not from inside a scan -- the same flush wallet.cpp already
documents avoiding elsewhere "for performance reasons". Nothing calls it yet.
5. SetBestChainINTERNAL took each CWalletTx by value, deep-copying every note's
witness deque (WITNESS_CACHE_SIZE entries) per transaction per call, purely to
serialize it. Now by const reference.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FU87LdsJZiZkfq1eXubpeo
A wallet.dat repaired by v1.0.3-or-earlier -salvagewallet could not be opened by
this build at all. Those builds' IsKeyType has no "hdchain" case, so salvage
dropped the record; LoadWallet then returned DB_CORRUPT and told the user to
restore from a seed phrase. There is no such recovery path: the node aborts
before the RPC server exists, -usemnemonic defaulted to 0 in v1.0.3 so many of
these wallets never had a phrase, and SetHDSeedFromMnemonic refuses a non-empty
wallet, so "move wallet.dat aside" discards every non-HD key salvage preserved.
The guard itself was right to refuse to DERIVE -- a missing hdchain falls back to
SetNull defaults, clearing fMnemonicSeed and switching derivation from the
64-byte BIP39 seed to the raw 32-byte entropy, an entirely different key tree.
It was wrong to refuse to OPEN. Split the two:
- "hdchain" record ABSENT (the salvage signature, tracked by a new
wss.fHDChainSeen set before any parse attempt) -> open DEGRADED.
- "hdchain" record PRESENT but unreadable -> still DB_CORRUPT; that signals
wider file damage.
Degraded means the wallet spends, receives and rescans normally but refuses to
derive any new HD key. Gated at the single choke point GetHDSeedForDerivation
rather than at the two generators, so z_getnewaddress/sendmany/shieldcoinbase
raise a clean error and autoshield skips. hdChain.seedFp is deliberately left
null, which keeps IsHDTransparentEnabled false so getnewaddress falls back to the
legacy random-key path -- gating it instead would break TopUpKeyPool, change
addresses and block templates. -rescan is soft-set because an old salvage also
dropped defaultkey and bestblock, which would otherwise look like a first run and
skip the rescan, leaving a permanently zero balance.
NEW DETECTOR, covering strictly more damage than the guard beside it. A wallet
salvaged by v1.0.3 and then USED on v1.0.3 persists a SetNull-derived hdchain
carrying seedFp=null. On this build fHDChainRead is then true, the guard never
fires, the node starts perfectly clean -- and derives into the wrong key tree
forever, silently. That state is unforgeable by any legitimate writer, since
InstallHDSeed always stores seedFp = seed.Fingerprint(), so compare the loaded
chain's seedFp against the fingerprint taken from the hdseed/chdseed record KEY
(which works while an encrypted wallet is locked) and degrade on mismatch.
Also:
- init.cpp: abort in the DB_CORRUPT branch itself, as DB_NEED_REWRITE already
does. Falling through ran several hundred more lines against a wallet just
declared corrupt, including SetHDSeedOrigin(), which WRITES to it. Error text
no longer advises -mnemonic (wrong twice over, see above).
- rpcdump.cpp: z_exportwallet discarded GetHDSeedForDerivation's return and
emitted the line regardless, so a failure wrote a blank seed next to a
legitimate-looking BLAKE2b-of-empty fingerprint -- a backup that looks valid
and restores nothing.
- Corrected the guard's comment: the saplingAccountCounter half was overstated.
Both generators loop while(Have...Key(...)), so a low counter walks forward
past existing accounts rather than colliding with them.
qa/r4-salvaged-wallet-harness.sh builds the victim wallets with the on-disk
v1.0.1 release binary and asserts the behaviour end to end. It REFUSES to run
outside a network namespace and re-checks getconnectioncount==0, because those
old binaries predate the regtest seed-injection fix and would otherwise dial the
live network. 11/11 pass:
C healthy wallet opens and is NOT flagged (no false positives)
A salvaged wallet opens, flagged, persists no hdchain, z_getnewaddress refused,
getnewaddress still works, z_exportwallet emits no bogus HDSeed line
B poisoned wallet opens and the seedFp detector fires
Note the harness asserts "no hdchain synthesised", NOT "wallet.dat unchanged":
a control run showed normal startup rewrites wallet.dat for healthy wallets too.
This is the access/detection half. Proving fMnemonicSeed by re-deriving against
held keys, and repairing the record, is deliberately left out: it requires a
write, and the affected population is unmeasured. dev's own salvage already
preserves hdchain, so the class is closed going forward.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FU87LdsJZiZkfq1eXubpeo
chainparams_commandline() applies the DRAGONX mainnet checkpoint set to
every SMART_CHAIN_SYMBOL=="DRAGONX" network, -regtest included, so an
isolated regtest node (height ~hundreds) sits far below the top checkpoint
(~3.2M). IsInitialBlockDownload() latches true whenever fCheckpointsEnabled
&& height < GetTotalBlocksEstimate(), so regtest was permanently in IBD --
disabling every ChainTip auto-op that gates on !IBD (autoshield, z_sweep,
consolidation) and the below-checkpoint script-check skip, and forcing
every regtest test to pass -checkpoints=0 by hand.
Default -checkpoints to false on regtest (still overridable with
-checkpoints=1). Verified: a fresh regtest node logs "Leaving
InitialBlockDownload" after a few blocks with no flag, and -checkpoints=1
keeps it in IBD as before.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGhWvdBSgt6UxxHANr7gfN
The stratum server was hardcoded for legacy Equihash (1347-byte solution,
sol.begin()+3 offset, CheckEquihashSolution) and was off-by-default with a
"do not use on RandomX" warning. DragonX is RandomX, so external pool mining
was impossible. This wires RandomX end-to-end.
Server (stratum.cpp), branched on ASSETCHAINS_ALGO == ASSETCHAINS_RANDOMX:
* GetWorkUnit sets StratumWork.nHeight and, for RandomX, sends the per-height
RandomX key via a new mining.set_randomx_key message (a miner cannot derive
it without the chain). The legacy mining.notify format is unchanged.
* SubmitBlock/stratum_mining_submit accept a 32-byte solution (== the RandomX
hash, used as nSolution verbatim) and validate it with CheckRandomXSolution
instead of CheckEquihashSolution. Target check (GetHash() < target) and the
nNonce = extranonce1||extranonce2 assembly are shared with the equihash path.
* -stratumtarget=<hex> overrides the pool share target (default diff-1); lets a
solo/low-difficulty test miner accept easy shares.
* GetWorkUnit's IsInitialBlockDownload guard is bypassed under -testnode=1 so an
isolated low-work test chain can serve work.
Reference miner: `stratummine "host" port ("address" timeout)` RPC (rpc/mining.cpp,
POSIX-only). A minimal stratum client that subscribes/authorizes, receives work +
the RandomX key, varies nNonce, hashes with RandomX via GetRandomXInput (byte-
identical to CheckRandomXSolution) and submits a 32-byte solution. Off-the-shelf
Equihash/Monero miners can't speak DragonX's 256-bit-nNonce Zcash header, so this
is the reference implementation. Added to the rpc client arg-conversion table.
Validated: loopback (chain 0->4, accepted every time, verifychain=true) AND a real
2-box LAN run (Linux miner -> Mac stratum server, 3 blocks accepted, verifychain=true).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 4 (cf15b0a39) converted the asmap-file-location debug output from
fprintf(stderr,...) to LogPrint. On Windows, boost::filesystem::path::c_str()
returns const wchar_t*, which tinyformat rejects at compile time
(is_wchar<const wchar_t*> has no tinyformat_wchar_is_not_supported member) —
breaking the mingw cross-build at init.o. The old fprintf accepted wchar_t*
silently (and printed garbage on Windows), so the latent bug only surfaced
once the call became type-checked.
Route all 11 asmap_path.c_str() calls through .string().c_str() so the
argument is a narrow std::string on every platform, matching the existing
correct idiom at pathLockFile.string().c_str() (line ~1662). No behavior
change on POSIX (path::c_str() is already char* there).
Caught by the cross-platform build phase of the 3-box test bring-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
From the Phase-6 structural scoping: land the verified behavioral bugs and
the safe quick-wins/dedups now; the large refactors (monolith splits, ~180
header globals) and consensus-adjacent items stay deferred. Built clean;
self-mined; verifychain=true.
BUGS (verified by reading the code):
- net.cpp CNode::Ban: a braceless `if (subNet.Match(...))` left
`pnode->fDisconnect = true;` OUTSIDE the guard, so banning any one subnet
marked EVERY connected peer for disconnect (dropped the whole peer set).
Wrapped the two statements in braces. (LIVE, high severity.)
- rpcdump.cpp importwallet: the `!fGood -> throw "Error adding some keys"`
check was trapped inside the `if (fRescan)` block, so importwallet with
rescan=false silently reported success when key import failed. Hoisted the
check before the rescan branch and cleaned the garbled braces/indentation.
- wallet.cpp CommitTransaction: ignored AddToWallet()'s return, so a failed
disk-persist of a just-signed spend was swallowed while the tx broadcast.
Now logs a hard error on failure.
- hush_nSPV_fullnode.h: the UTXOS branch declared `uint8_t filter` while the
twin TXIDS branch uses `uint32_t filter`; dragon_rwnum switches on
sizeof(filter), so the utxos path parsed only 1 of 4 wire filter bytes.
Widened to uint32_t.
- asyncrpcoperation_sweep.cpp: LogPrintf("%s ... %s", one-arg) read a missing
vararg; added the __func__ argument.
- rpcdump.cpp importprivkey: inner `auto secret_key` shadowed the outer
uint8_t and changed the type into DecodeCustomSecret; dropped the shadow.
- rpcdump.cpp getrescaninfo: char[8] + sprintf("%.4f") overflows when the
ratio >= 10.0 (transient reorg); widened to char[16] + snprintf.
QUICK WINS: removed the duplicate DRAGON_MAXSCRIPTSIZE #define; pinned the
dead HUSH3-branch NOTARISATION_SCAN_LIMIT_BLOCKS to 1440; fixed init typos
(fRequestShutdown, RPC warmup).
DEDUP: extracted the 19-line try/catch error-mapping block — copy-pasted
identically into all six async operations — into
AsyncRPCOperation::set_error_from_current_exception(), so the mapping is
edited in one place. Behavior-identical (verified all six blocks were byte-
identical first).
Deferred (endorsed by the scoping, better as their own PRs): addrman Select_
dedup, the pow.cpp powLimit helper (consensus file), the miner CreateNewBlock
lock-asymmetry, the wallet monolith splits, and the ~180-global / consensus-
retarget / Komodo-heritage work.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fifth phase of the code-hygiene remediation, done per-file (18 files) via an
18-agent Workflow with strict guardrails. No consensus or security VALUES
changed (verified: pow/hush_utils/hush_bitcoind/net.h/crypter/stratum have
zero non-comment numeric changes); built clean, self-mined, verifychain=true.
Totals: 57 stale comments corrected, 46 user-facing branding fixes, 6
behavior-preserving named constants, 4 stratum warnings, 77 items deliberately
left (consensus/security values, copyright headers).
- Stale comments -> DragonX: corrected HUSH3/HUSH/Komodo/Equihash/Arrakis
leftovers that misdescribed the active chain (pow AWT/lwma notes, txdb
"Equihash solution" -> RandomX, wallet CLTV/overwinter/Sapling@1 notes,
util datadir provenance, crypter's stale mapSproutSpendingKeys invariant).
Replaced unprofessional/editorializing comments (profane TODOs, "developers
are elite") with neutral technical notes, preserving the real design info.
- User-facing branding: dumpwallet header "created by Hush" -> "DragonX";
RPC help text hushd/hush-cli/hushprivkey/hushaddress/Agama -> dragonx*;
rebranded provably-dead "HUSH3" symbol-fallback literals to DRAGONX.
- Named constants (same values, non-consensus/non-security): addrman
peer-selection factors (kChanceFactorGrowth/kChanceScale/deprioritize),
sietch MIN_ZOUTS, an init notarization-DB cache size.
- Left the security macros (ECC/TFM_TIMING_RESISTANT=420, comment-clarified
only), the consensus subsidy/commission literals + 128/129 TRANSITION
(comment only), and copyright headers untouched.
- Added a prominent WARNING that the stratum server is Equihash-era and NOT
updated for DragonX's RandomX PoW (flagged, not rewritten).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fourth phase of the code-hygiene remediation, addressing the ~1,082 stray
fprintf(stderr)/printf debug calls so consensus and hot paths stop spewing
to stderr/stdout. Done per-file (18 files) with conservative rules; the tree
builds clean, self-mines, and `verifychain` re-validates the whole chain
(pow/txdb/coins/miner paths) with -debug=1 enabling every converted line —
zero tinyformat/format-arg exceptions.
Net across 18 files: 174 commented-out debug lines deleted, 173 unconditional
live prints converted to LogPrint("<cat>",...)/LogPrintf (net/mining/pow/nspv/
zrpc categories, format strings + args preserved exactly), 40 pure-noise or
sensitive prints deleted, and 194 calls DELIBERATELY LEFT (already behind
fDebug/fZdebug guards, or genuine startup/fatal-error output that must reach
the console before logging init).
Notable:
- Deleted sensitive success-path dumps (nSPV SIG_TXHASH + full tx input/
output/change amounts; kvupdate privkey/pubkey hex) that were writing key
and amount material straight to stderr/stdout.
- Removed the raw 32-byte target hex dumps in the zawy adaptive-PoW helpers
and the legacy one-shot `if(height==340000)` HUSH artifact in pow.cpp.
- Converted per-tx relay + ban/banlist (net), per-setgenerate MININGTHREADS
(rpc/mining), signrawtransaction TXPOW, and per-message nSPV traces.
- Left format/arg-mismatched lines untouched (flagged) to avoid introducing
tinyformat runtime throws.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remove ~116 commented-out debug/dead-code lines the audit flagged as noise
(findings F6/F7/F15/F22): the Komodo "%s tikN" step-tracer comments and other
commented-out fprintf/printf/LogPrintf/std::cerr calls threaded through
AppInit2 (init.cpp), CreateNewBlock and the miner loops (miner.cpp), plus a
few commented-out dead-code fragments (sendmany SetLockTime/nLockTime, the
CCtx CC_vinselect random-pick block and AddNormalinputsLocal remote-mypk
redirect, miner adaptive-PoW assignments).
Comments only — no compiled behavior changes; the tree builds clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Second phase of the code-hygiene remediation, covering the items the
DragonX team introduced or the rebrand missed. No consensus behavior
changes; validated on a local self-mining node.
Log cruft:
- chainparams: route the startup ">>>>>>>>>>" banner and the port line
through LogPrintf/LogPrint("net") instead of fprintf(stderr).
- pow: drop the fprintf(stderr) hash-mismatch dump that duplicated the
LogPrintf copy verbatim.
- miner: gate the RandomXDatasetManager per-alloc/per-VM address dumps and
MemDiag /proc reads behind LogPrint("randomx"); keep a one-line
"allocated shared dataset (N GB)" summary at default verbosity.
Named constants (single source of truth in wallet.h):
- DEFAULT_AUTOSHIELD_FEE/INTERVAL, MIN_AUTOSHIELD_INTERVAL,
DEFAULT_AUTOSHIELD_MIN_UTXOS, AUTOSHIELD_MIN/MAX_FEE for the autoshield
option parsing and help text (were bare 10000/25/5 literals repeated
across init.cpp and wallet.h).
- AUTO_OP_TARGET_HEIGHT_OFFSET replaces the three copy-pasted
`blockHeight + 5` async-op scheduling offsets.
- AUTO_OP_EXPIRY_DELTA replaces the three per-file *_EXPIRY_DELTA=15
constants (sweep/consolidation/autoshield) with one shared value.
- Move DEFAULT_AUTOSHIELD_FEE out of the op header into wallet.h so it no
longer collides when both headers are included.
Error surfacing:
- init: report clamped/out-of-range -autoshieldinterval/-autoshieldfee via
InitWarning() (surfaces to GUI/log) instead of fprintf(stderr).
Rebrand / dead foreign-chain code (approved removals):
- server: drop the HUSH3 special-cases in stop() and HelpExampleCli; the
cli example now shows "dragonx-cli" instead of "hush-cli".
- getinfo (misc): remove the stale dPoW notarization block (notarized,
prevMoMheight, notarizedhash, notarizedtxid, notarizedtxid_height,
HUSHnotarized_height, notarized_confirms) — DragonX is a private chain
from genesis with no active dPoW. Also fixes the hardcoded "HUSH3" that
made getinfo query a foreign chain's notarization.
- delete the dead Komodo notary RPCs getera/getdragonjson/
getnotarysendmany/geterablockheights (getera returned 0;
getnotarysendmany was marked "this is broke") and their registrations.
- remove the dead ASSETCHAINS_EQUIHASH reporting branches in getinfo and
getmininginfo — DragonX is RandomX-only.
chainparams: document why the upstream Equihash params and the literal
Bitcoin genesis are retained under RandomX (do not "fix" them).
Deferred: the hdSeedOrigin int->string switch in z_autoshieldstatus
(no shared helper exists to reuse; low value).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 1 of the code-hygiene remediation. Each is a genuine defect, not
style. The two consensus functions are touched only by provably
behavior-preserving dead-code removal and a comment.
- miner: initialize the anti-spin loop counter (was `int i;` -- the loop
condition read an indeterminate value, UB) and break once the block
time advances past the median, which is what the comment intended.
- consensus/upgrades: drop a stray printf() on the NetworkUpgradeState
path; the following assert already documents the invariant.
- txdb: CBlockTreeDB::Snapshot2's outer catch treated ANY exception as
normal end-of-iteration and built a snapshot from partial data. Fail
instead, matching the inner catch the author marked consensus-relevant
("we need to exit here if so for consensus code!"). iter->Valid()
already handles genuine end-of-iteration.
- wallet/rpcwallet + init: -sietch-min-zouts used a "--" key that the arg
parser (which normalizes --foo to -foo) can never match, so the Sietch
decoy floor was silently stuck at the default. Use the single-dash key
so the knob works, and document it in -help.
- hush_bitcoind + hush_utils: remove four unreachable duplicate `else if`
branches from hush_commission()/hush_block_subsidy() (a second
`height < 23860000` and a second `height < 27220000` in each). Proven
identical across 49,591 heights. NB: the dead values hint at an intended
clean halving schedule that was never wired up; the DEPLOYED schedule
(two double-steps) is preserved exactly. Changing it is a future
consensus decision, not this cleanup.
- hush_bitcoind: replace the "likely a bug" halving TODO with an accurate
note -- INTERVAL is only consumed by a debug fprintf, so the > vs >=
boundary at HALVING1 has no consensus effect.
- git rm two committed macOS build artifacts (cc/customcc.dylib and
libcc.dylib) and add the missing .dylib .gitignore rules.
Built clean on Linux; an isolated node self-mined genesis->5113 exercising
the miner and consensus-subsidy paths, and getsnapshot returned normally.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up nits surfaced by the multi-agent review of the dragonx..dev delta;
none are correctness/consensus bugs, all are defensive/consistency tidy-ups.
Builds clean; the diff was reviewed across concurrency, scheduler, and
tx-building lenses.
- wallet: default CWallet::fAutoShieldEnabled to false. init.cpp always
recomputes it (ON only for CREATED/RESTORED seed provenance) before any
ChainTip round, so this is behaviour-neutral in the normal path and stops a
CWallet that skips that init from auto-enabling for provenance the gate would
reject.
- wallet: clamp a loaded hdSeedOrigin to UNKNOWN when out of enum range, so a
corrupt/hand-edited wallet.dat cannot claim a known-recoverable seed and flip
autoshield ON.
- wallet: key the sweep and consolidation ops' NU-straddle guard, transaction
builder height, and expiry off execution-time tipHeight instead of the stale
enqueue-time targetHeight_ -- matching the autoshield op (65130c312) so the
builder's consensus-branch selection agrees with the height the tx is signed
for. (Sweep previously built at targetHeight_ but expired at the live tip.)
- wallet: on the sweep NU-straddle skip, set sweepComplete_ so the round backs
nextSweep off one interval instead of re-dispatching a fresh sweep op every
block through the activation window.
- init: clamp -autoshieldinterval below 5 up to the documented minimum of 5,
rather than silently resetting it to the default 25.
- chainparams: make the ClearSeeds guard an exact "DRAGONX" match instead of a
7-char prefix, so DRAGONX-prefixed assetchains (e.g. DRAGONX2) no longer
inherit DragonX's seeds.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UtvyqQQSqR64DNEjUEuTmb
A build predating VERSION_HD_TRANSPARENT rewrites the hdchain record with only
its four base fields while leaving nVersion at whatever it read. Our version-gated
reads then run off the end of the stream, ReadKeyValue turns the throw into
fHDChainRead=false, and LoadWallet escalates that to DB_CORRUPT. Verified against
the real v1.0.2-2b011d6ee release binary: a dev wallet, opened once by v1.0.2 and
given a single new sapling address, came back to
Error reading wallet database: hdchain record is corrupt
Error loading wallet.dat: HD seed present but the hdchain record is missing or corrupt
Error loading wallet.dat: Wallet corrupted
Nothing is actually lost there -- the same test showed the seed phrase restoring
the full balance, the autoshield destination, and even the address v1.0.2 had
generated -- but the user is shown "Wallet corrupted" with no hint of that.
Recover instead, for a v1 or v2 record. Everything derivation depends on is either
in the four-field prefix or in the separate hdseed record, and the trailing
counters are self-healing: DeriveNewChildKey and GenerateNewSaplingZKey both skip
indices whose key the wallet already holds, so restarting a counter at 0 re-walks
past existing keys rather than reissuing them. Read the prefix from an untouched
copy of the stream, default the missing tail, log it, and rewrite the record in
full form so the next load is clean.
A record claiming nVersion >= VERSION_HD_MNEMONIC still fails loud: that flag
selects the derivation input, so guessing it wrong yields a different key tree in
silence. No wallet this code has written can be in that state -- every
InstallHDSeed call site passes fMnemonic=false -- so the branch is defensive only.
Also say what to do about it. Both the log line and the init error now name the
remedy (move wallet.dat aside, restart with -mnemonic and -rescan) rather than
stopping at "Wallet corrupted".
Verified on regtest against the real v1.0.2 binary, 14/14: the round-trip that
previously ended in "Wallet corrupted" now loads, logs the repair and the rewrite,
keeps the balance, the autoshield destination and v1.0.2's own address, still
issues distinct fresh t-addresses after the counter reset, needs no repair on the
second load, and remains fully recoverable from the seed phrase.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pwalletMain->autoShieldAddress was only ever written by a round running in the
current process, so on any node without an explicit -autoshieldaddress,
z_autoshieldstatus reported an empty destination from startup until the first
round fired. disabled_reason was empty on that path too (rpcwallet.cpp), so the
RPC showed autoshield true, running false, no address and no explanation -- the
exact silent state z_autoshieldstatus was added to eliminate. On a restored
wallet, which pre-derives the whole -mnemonicsaplinggap window and therefore
always has an in-gap account to pick, the answer was known at startup and simply
not computed.
Split the read-only half of resolveDestination into a free function shared with
init: the configured override if set, else the lowest in-gap account
m/32'/coin'/i' the wallet already holds. init calls it once when autoshield is
enabled and no explicit address was given.
It deliberately does not generate a key. Deriving a fresh sapling account as a
side effect of populating a status field would mutate the wallet to make an RPC
prettier, so step 3 of resolveDestination -- the generation path, which must stay
inside the operation where an unlocked wallet is already established -- is left
where it was. A brand-new wallet holds nothing in-gap, so the field stays empty
there and disabled_reason now says why instead of being blank.
Behaviour is otherwise unchanged: same derivation, same lowest-index-wins rule,
same refusal to trust CKeyMetadata, same caching for the life of the process.
Verified on an isolated regtest chain, 12/12:
fresh wallet -> address "", reason "no destination resolved yet; one will
be derived from the HD seed on the first round"
after one round -> address set, reason empty
after RESTART -> address visible with NO block mined since (height 12 both
sides), and z_listaddresses still holds exactly 1 address,
so init derived nothing
-autoshieldaddress-> still overrides the derived destination, and the round
shields into it
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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
This commit removes the CLI option -opretmintxfee, makes it always
enabled, changes the required fee rate and changes which OP_RETURNS
get "amnesty" and do not need to pay this fee rate.