100 Commits
Author SHA1 Message Date
DanSandClaude Opus 5 da191e447d Merge branch 'dev' for the v1.3.0 release
72 commits since 1c3523aac (2026-07-23). Highlights:

  consensus  BLOCK_VALID_CONTEXT no longer overwrites the block validity level
             (6d282db21); dead CBOPRET coinbase price check removed (267e6f7ad);
             PROTOCOL_VERSION 2000000 -> 2000001, MIN_PEER_PROTO_VERSION held.
  sync       DRAGONX checkpoints extended to 3,226,000 -- past the RandomX
             activation at 2,838,976, so the skip-below-checkpoint path finally
             fires (7dc904c96).
  wallet     interrupted rescans no longer hide funds and no longer latch
             fAbortRescan (a6b6f80db); rescan progress is checkpointed when a scan
             is interrupted (c1040028e); salvaged wallets open in a degraded mode
             with hdchain-mismatch detection (5634aed75).
  stratum    stratummine gated behind an explicit flag and hardened (db42091ce);
             low-difficulty shares rejected before spending a RandomX hash
             (fa16e740b).
  release    version bumped to v1.3.0 (af7d9e230) and doc/release-process.md
             corrected -- it previously omitted signing entirely, which is why
             v1.1.0 and v1.2.0 were tagged but never became installable releases.

Soak before merge: ~86 node-hours on c1040028e across all seven seeds, plus the
primary -- the pool's block-template source -- running it in production with the
pool accepting shares. Fleet consensus verified at a common height throughout.

Not included, deliberately: branch audit-fixes-20260901 (two commits, both marked
UNTESTED) stays parked. The ported rpc-tests have never been run green and are not
a gate for this release.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FU87LdsJZiZkfq1eXubpeo
2026-09-01 19:09:04 -05:00
DanSandClaude Opus 5 fc26e2e0a0 doc: stamp the man pages with the release version, not a build sha
The man pages were regenerated at e2e10f6ef from a binary built at af7d9e230 and
carry "v1.3.0-af7d9e230". The release will be tagged v1.3.0, so the shipped man
pages would contradict the tag they ship under -- the same provenance gap af7d9e230
itself was written to close.

A faithful regeneration cannot fix this before the tag exists: gen-manpages.sh reads
--version from the binary, genbuild.sh derives that from `git describe`, and that
only prints exactly "v1.3.0" once the annotated tag is in place. Regenerating after
tagging would mean committing on top of the tag and moving it. doc/release-process.md
anticipates this and documents hardcoding the version instead.

Content is untouched and needs no regeneration: all 150 daemon options in the man
page match `dragonxd --help` exactly, including the five -autoshield* flags.

Not changed: dragonx-tx.1 still describes itself as "hush-tx utility". That string
comes from the binary's own --help, so editing the man page would make it diverge
from what the tool actually prints. It is a binary-level branding fix, not a doc one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FU87LdsJZiZkfq1eXubpeo
2026-09-01 19:06:54 -05:00
DanSandClaude Opus 5 1d2d5f32d0 version: bump PROTOCOL_VERSION for the v1.3.0 consensus changes
doc/release-process.md requires PROTOCOL_VERSION to increase by 1 for any release
carrying a consensus change. This one does: 6d282db21 stops BLOCK_VALID_CONTEXT
overwriting the block validity level, 267e6f7ad drops the dead CBOPRET price
validation from the coinbase check, and pow.cpp is 157 lines lighter. The value
has not moved since 85c8d7f7d in March.

MIN_PEER_PROTO_VERSION stays at 2000000. It currently equals PROTOCOL_VERSION, so
raising it in step would disconnect every peer still announcing 2000000 -- which is
every user, because no DragonX release has ever been installable: the wallet's
updater pins an ed25519 key and sets kDaemonRequireSignature = true, and no release
has ever shipped a .sig. Bumping only the advertised version lets 1.3.0 nodes be
identified on the wire without partitioning the network on release day.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FU87LdsJZiZkfq1eXubpeo
2026-09-01 19:06:54 -05:00
DanSandClaude Opus 5 c1040028e4 wallet: checkpoint rescan progress when the scan is interrupted
An aborted or shutdown-interrupted rescan discarded all of its progress: nothing
advanced the on-disk best-block locator, so the next run started over. On a large
shielded wallet that is a multi-minute witness rebuild repeated from scratch.

Writes the locator ONCE, at the interrupt, rather than periodically through the
scan. That delivers the whole stated benefit -- "resume where it stopped" -- at
the cost of exactly one SetBestChain per interrupted scan. A periodic checkpoint
would additionally survive SIGKILL and power loss, a much weaker requirement, and
it is the part that carries all the cost: SetBestChainINTERNAL is O(whole wallet)
regardless of progress, so on the reported 5.3k-tx / 7.5k-note wallet a checkpoint
every 2500 blocks would plausibly cost more than the rebuild it was meant to save.
If someone later demonstrates a need for crash resilience mid-scan, add it then,
with a measured interval and fFlush=false -- not before.

Two guards, both necessary:

  - CONTIGUITY. The RPC entry points (rescan / importprivkey / z_importkey /
    z_importviewingkey) take a caller-supplied start height validated only against
    chainActive.Height(), never against the wallet's own persisted locator. A scan
    beginning above that locator must not checkpoint at all, or it would record
    the skipped range as scanned and hide any funds in it. Logged once when it
    applies, so an operator can see why an interrupt did not persist.

  - MONOTONICITY. A checkpoint may only advance the locator, never move it back.

The locator points at the last FULLY PROCESSED block -- the parent of the block we
were about to scan -- so resume restarts one block early. CChain::GetLocator pushes
its argument first and FindForkInGlobalIndex returns that same block, so resume
begins AT it; the one-block overlap is deliberate and idempotent, since AddToWallet
only takes its merge path when the transaction is already present.

No witness work is done at the interrupt, which answers the two "//TODO: should we
update witness caches?" comments this replaces: witnesses are re-derived from each
note's own witnessHeight independently of the locator, and witnessRootValidated is
never serialized, so every note is revalidated against hashFinalSaplingRoot on the
next start. A mid-scan checkpoint does capture notes at mixed witnessHeights -- the
in-loop BuildWitnessCache(pindex, true) returns before the extension phase and only
seeds new notes at their own transaction's height -- but that state is recoverable:
the post-loop BuildWitnessCache(tip, false) levels every note, and it now occurs at
most once per scan instead of hundreds of times.

Uses SetBestChainNoFlush so the checkpoint does not trigger a full BDB
txn_checkpoint over the whole cache, and reports failure rather than silently
leaving the operator to discover the replay.

Depends on the fLastRescanCompleted gate in the previous commit: without it
init.cpp would overwrite this checkpoint with a tip locator immediately after the
scan returns.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FU87LdsJZiZkfq1eXubpeo
2026-08-31 20:54:37 -05:00
DanSandClaude Opus 5 a6b6f80db0 wallet: stop an interrupted rescan from hiding funds, and un-latch fAbortRescan
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
2026-08-31 20:52:40 -05:00
DanSandClaude Opus 5 3aac75e94f cli: report the port actually dialled, not a stale global
dragonx-cli resolves -rpcport correctly -- CallRPC does
  int port = GetArg("-rpcport", BaseParams().RPCPort());
and dials that port. But both connection-failure messages printed
ASSETCHAINS_RPCPORT instead, a separate global defined at the top of
bitcoin-cli.cpp, initialised to the mainnet default 21769, and never assigned
anywhere in the CLI.

So every failure claimed port 21769 regardless of what was asked for:

  $ dragonx-cli -rpcport=21799 getblockcount
  error: couldn't connect to server at port 21769

That reads as "your -rpcport was ignored", which is a much more alarming and
much more misleading diagnosis than "nothing is listening yet". It cost an hour
of debugging on a node that was simply still loading a 15 GB txindex, and led to
the wrong conclusion that the CLI could be talking to the production daemon when
it was not.

Both sites now print the local `port`. Verified against the previous binary:

  new:  -rpcport=21799 -> "couldn't connect to server at port 21799"
        -rpcport=59999 -> "couldn't connect to server at port 59999"
  old:  both            -> "couldn't connect to server at port 21769"

and the CLI still reaches a live daemon on a non-default port.

ASSETCHAINS_RPCPORT is left defined; it is referenced by other translation units.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FU87LdsJZiZkfq1eXubpeo
2026-08-31 17:35:52 -05:00
DanSandClaude Opus 5 5634aed750 wallet: open salvaged wallets in a degraded mode, and detect a mismatched hdchain
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
2026-08-31 13:29:43 -05:00
DanSandClaude Opus 5 db42091ce3 rpc/net: gate and harden stratummine, keep regtest off the live network, init nSPV filter
stratummine (rpc/mining.cpp), a test-only reference miner, was registered
unconditionally behind a bare #ifndef WIN32 with okSafeMode=true, so it shipped
as a live RPC on every non-Windows release build. Four fixes:

  - Gated behind an explicit, default-off -stratummine, with regtest exempt so
    qa/rpc-tests can still drive it. Deliberately NOT gated on fExperimentalMode:
    that defaults to TRUE (init.cpp:1195), so such a gate is a no-op -- an error
    made and caught while testing this change.
  - mining.notify's three hash fields went straight into uint256(ParseHex(...)).
    uint256's vector ctor asserts on a wrong size (uint256.cpp:30) and NDEBUG is
    defined nowhere in this build, so that assert is live in release; ParseHex
    also truncates silently at the first non-hex character. A short or garbled
    field therefore ABORTED THE DAEMON. Added StratumHex256(), which requires 64
    hex chars and a 32-byte result, and all three fields are validated before any
    is committed so a bad job is rejected rather than half-applied.
  - processLine ran inside the window where the RandomX cache, the VM and the
    socket are live, all released on the normal path only, and every get_str()
    throws on a type mismatch -- so a malformed message leaked 256 MB and the fd.
    Body wrapped in try/catch: ignore the line, keep mining.
  - Caller-supplied timeout clamped to [1, 3600]; it was unbounded, pinning an
    RPC worker and the cache indefinitely. okSafeMode -> false.

Verified against a hostile stratum server on regtest:
  - 4 malformed mining.notify payloads (short hex, non-hex, wrong JSON type,
    31 bytes) -> all rejected, daemon alive, 0 assertions. The 31-byte case is
    the one that previously hit the uint256 assert.
  - valid job first (so RandomX actually allocates) then garbage mid-mine ->
    RSS delta +2.2 MB, i.e. cache and VM released, not the ~256 MB a leak leaves.
  - regtest exemption confirmed: stratummine runs past the gate there.
  The non-regtest refusal path is by code reading only -- a testnet node on this
  host collides with the production daemon's RPC port, so it was not exercised.

hush_utils.h: stop injecting the mainnet node1-node10.dragonx.is addnode seeds
when -regtest or -testnet is set. regtest reuses mainnet's network magic, so a
supposedly isolated node was handshaking production peers and pulling their
headers into its own index. Gated at the injection site only -- isdragonx itself
must stay true, because it also selects ac_private, ac_algo, blocktime and the
reward/halving schedule (an earlier version of this patch gated isdragonx itself
and silently turned ac_private off on regtest). hush_args() runs between
ParseParameters() and ReadConfigFile() (bitcoind.cpp:115/144/158), so this sees a
command-line -regtest, as qa/rpc-tests uses, but not a config-file regtest=1.

hush_nSPV_fullnode.h: initialize `filter` at both sites. It was assigned only on
the len-11 request form; the other two passed uninitialized stack memory to
NSPV_getaddressutxos/NSPV_getaddresstxids, remotely reachable since
HUSH_NSPV_FULLNODE is on by default.

getblocktemplate_proposals.py still passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FU87LdsJZiZkfq1eXubpeo
2026-08-31 02:38:38 -05:00
DanSandClaude Opus 5 6d282db216 consensus: stop BLOCK_VALID_CONTEXT overwriting the block validity level
BLOCK_VALID_CONTEXT was 6. The validity levels above it are sequential VALUES
packed into a 3-bit field, not independent bits, so BLOCK_VALID_MASK is
1|2|3|4|5 == 7 and the value 6 sat entirely inside it. `pindex->nStatus |=
BLOCK_VALID_CONTEXT` (main.cpp:5616, and :3285) therefore did not set a flag --
it overwrote the validity level.

Consequences, all long-standing:
  - A header-only block raised to BLOCK_VALID_TREE(2) became 2|6 == 6, which
    reads as >= BLOCK_VALID_CHAIN(4) and >= BLOCK_VALID_SCRIPTS(5). A block
    merely written to disk reported full script validity.
  - Every later RaiseValidity() silently no-opped, because 6 >= every level.
    ConnectBlock's RaiseValidity(BLOCK_VALID_SCRIPTS) was a permanent no-op.
  - CheckBlockIndex's "CHAIN valid implies all parents are CHAIN valid" invariant
    was violated whenever a stored block sat above a still-header-only ancestor,
    i.e. ordinary out-of-order parallel block download. fDefaultConsistencyChecks
    is true only for regtest, so the abort was regtest-only -- but the garbled
    index is written identically on mainnet, where only the detection is off.

Not a v1.3.0 regression: introduced upstream in Komodo fa309e5b0 (2019-04-02),
inherited via Hush, and byte-identical in v1.0.3, which the production network
runs today. Validity was only ever INFLATED, never deflated, so no valid block
was rejected and no invalid block skipped validation -- ConnectBlock's CheckBlock
and full script/proof verification always ran. The user-visible effects were
misreports: getchaintips labelling never-connected forks "valid-fork",
submitblock answering "duplicate" for unvalidated blocks, and ProcessGetData
serving them. The material cost was to QA: multi-node regtest tests aborted the
syncing node at random, making the rpc-test suite unusable.

Moves the flag to 512, the next free bit above BLOCK_IN_TMPFILE(256), and adds
static_asserts that every nStatus flag is disjoint from BLOCK_VALID_MASK so this
cannot be reintroduced silently. nStatus is serialized as VARINT, so the wider
value needs no format change.

Verified under gdb on regtest. A connected block's nStatus:
  before   0x1e  validity field 6            (measured on the previous binary)
  after   0x21d  validity field 5 = SCRIPTS, context bit set
wallet_sapling.py, which aborted the syncing node deterministically, now runs to
completion with no assert.

No migration: the original level is unrecoverable from a polluted entry (1|6,
3|6 and 5|6 all give 7; 2|6 and 4|6 both give 6), and the only safe guess is
downward, which would risk revalidation work on a 3.25M-block index for a
cosmetic gain. Legacy entries simply have the flag bit clear, so they re-run the
contextual check they previously skipped -- more checking, not less -- and
resolve on any reindex.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FU87LdsJZiZkfq1eXubpeo
2026-08-31 02:12:17 -05:00
DanSandClaude Opus 5 c5fde12485 qa: port 12 wallet/mining tests to python3 and fix three framework blockers
Ports qa/rpc-tests from 6 python3 files to 18. Ran every ported test against
the freshly built v1.3.0 dragonxd. Results, honestly:

  PASS (1)            getblocktemplate_proposals.py
  NOT APPLICABLE (7)  wallet.py, walletbackup.py, wallet_protectcoinbase.py,
                      wallet_listnotes.py, wallet_mergetoaddress.py,
                      getblocktemplate.py, wallet_shieldcoinbase.py
  BLOCKED (4)         wallet_sapling, wallet_nullifiers, wallet_persistence,
                      wallet_treestate

The "not applicable" seven are inherited Zcash/Hush-era tests that exercise
features DragonX deliberately removed. They assume transparent t->t value
transfer, but ASSETCHAINS_PRIVATE=1 (hush_utils.h:1826) makes sendtoaddress
and sendmany consensus-refuse; they assume Sprout joinsplits, which are gone
from the RPC layer entirely; and they hardcode Bitcoin economics (10 coin/block,
100-block maturity) against DragonX's 3 DRGX and COINBASE_MATURITY=1. They are
ported and left in place rather than deleted, but they cannot pass on this chain
without being rewritten around z_shieldcoinbase/autoshield.

Three framework fixes in test_framework/util.py, each of which broke every
multi-node test:
  - initialize_chain() passed -connect=0, and init.cpp soft-sets -listen=0 when
    -connect is present, so cache node0 never opened its p2p port and nodes 1-3
    could never sync to it -- initialize_chain() hung forever in sync_blocks().
    Now passes -listen=1 -bind=127.0.0.1 -dnsseed=0 explicitly (an explicit arg
    beats SoftSetBoolArg) while keeping the cache nodes off the public network.
  - cache cleanup removed files from <datadir> when dragonxd writes them one
    level deeper into the net-specific <datadir>/regtest.
  - set_node_times() did print("..." + t) with t an int -> TypeError.
  - default binary paths corrected to src/dragonxd and src/dragonx-cli.

The four BLOCKED tests are blocked by a daemon assert, not by the port; see the
follow-up commit/report on BLOCK_VALID_CONTEXT.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FU87LdsJZiZkfq1eXubpeo
2026-08-31 01:53:43 -05:00
DanSandClaude Opus 5 e2e10f6ef8 doc: regenerate man pages from the v1.3.0 binaries
Previously the version strings were restamped by hand in af7d9e230 because
help2man and a built binary were both unavailable. Regenerated properly via
util/gen-manpages.sh against freshly built v1.3.0 binaries, which also picks
up two options that were never documented:

  -sietch-min-zouts=<n>   decoy Sapling outputs added to each z_sendmany
  -stratumtarget=<hex>    pool share target, for solo/low-difficulty mining

NOTE: the version lines read "v1.3.0-af7d9e230" because `git describe` has
no annotated tag to find yet. Once v1.3.0 is tagged annotated, these must be
regenerated once more so they read a clean "v1.3.0" -- that step stays open
on the release checklist.

Generated on seed 176 (glibc 2.35 runs the binaries; help2man is installed
there and not on the primary), with an isolated HOME so nothing touched the
node's datadir. The reindex running on that box was not disturbed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FU87LdsJZiZkfq1eXubpeo
2026-08-31 00:23:56 -05:00
DanSandClaude Opus 5 af7d9e2300 release: bump to v1.3.0 and document the signing step that was never performed
The tree stamped 1.2.0 in both configure.ac and src/clientversion.h, but
v1.2.0 is an annotated tag already pushed at fad05d3ab and dev is 27
commits past it. Both trees therefore produced CLIENT_VERSION 1020050 and
announced an identical /DragonX:1.2.0/ subversion, so a released binary
would have been indistinguishable from the tag on the wire, in
getnetworkinfo, and to the wallet's in-app updater -- destroying the only
provenance check users have: build the tag, compare the binary.

Bump MINOR rather than REVISION: the delta since v1.2.0 adds a subsystem
(RandomX stratum) and a new RPC (stratummine).

  configure.ac, src/clientversion.h  1.2.0 -> 1.3.0 (CLIENT_VERSION 1030050)
  doc/man/*.1                        version strings restamped
  contrib/debian/changelog           1.3.0 entry for the 27 commits
  src/chain.h                        stale "CLIENT_VERSION is 1010050" comment

Man page *content* still needs a real regeneration: util/gen-manpages.sh
requires help2man and built 1.3.0 binaries, so it belongs in the release
build, where it will also pick up the new stratum options.

doc/release-process.md is why v1.1.0 and v1.2.0 were tagged but never
became releases. Followed verbatim it produced a release the wallet
refuses to install:

  - it directed releases to a branch named `dragonx`, which does not
    exist; releases are cut on `master`
  - it never once mentioned signing, yet the updater pins an ed25519 key
    and sets kDaemonRequireSignature = true, so an unsigned release is
    refused outright and every user silently stays on their old daemon
  - it did not require the release tag to be annotated, and genbuild.sh
    calls `git describe` without --tags, so a lightweight tag stamps the
    build `v<older-tag>-<sha>` instead of the release version -- which is
    exactly what happened to v1.0.0 through v1.0.3
  - it referenced util/build-debian-package-ARM.sh, which is not in tree

Adds the signing and checksum-table steps, the annotated-tag requirement
with a `git describe` verification, and the rule that a new version must
exceed every existing tag including unpublished ones.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FU87LdsJZiZkfq1eXubpeo
2026-08-30 22:48:52 -05:00
DanS 4cc7e0491a qa: finish the python3 port far enough to build the shared test chain
Follow-up to 5e0a70683, which got start_node() working. Three more
defects sat behind it:

- initialize_chain() builds the 4-node cache with its own daemon
  invocation, which 5e0a70683 did not touch. It therefore still omitted
  -regtest (so those cache nodes ran on MAINNET) and -asmap (so they
  refused to start at all). Every test that uses the cache -- which is
  most of the wallet suite -- died there.

- reindex.py and getblocktemplate_longpoll.py each carried a single
  python2 print statement, which is the whole reason they would not even
  parse under python3. Shebangs updated to match.

The suite still does not pass: initialize_chain hits a remaining py2
str+int concatenation, and getblocktemplate.py reaches a real test
assertion. Both are beyond this commit, but the harness now gets far
enough to start nodes, answer RPC and begin building the shared chain,
which it could not do before.
2026-08-30 21:29:08 -05:00
DanS 5e0a706839 qa: repair the rpc-test harness so it can start a DragonX node at all
The integration suite has never run against DragonX. Six independent
defects stacked up, each only visible once the previous was fixed:

1. test_framework used python2 implicit relative imports
   ("from authproxy import ..."), removed in python3, so every test died
   at import. Made explicit relative imports.

2. It wrote ZZZ.conf -- a Komodo assetchain convention -- while DragonX
   reads DRAGONX.conf. The daemon therefore never saw the generated
   config, fell back to mainnet defaults and tried to bind RPC 21769,
   which on a seed node is already held by the real node.

3. start_node() was hard-wired for the -ac_name=ZZZ assetchain tests: it
   took the RPC port from extra_args[3], passed extra_args[0] as argv[0]
   of the CLI, and only wrote a config when extra_args[0] matched. Any
   test that passes no extra args crashed on len(None). The generic path
   now takes the port from rpc_port(i) -- the same helper
   initialize_datadir() already used -- and drives the CLI with -datadir.

4. dragonxd refuses to start without an asmap file, which no test datadir
   had. initialize_datadir() now provisions one.

5. -asmap relative paths resolve against the NET-SPECIFIC datadir, so a
   copy in <datadir> is never found. Pass an absolute path.

6. Worst: -regtest was only ever set as "regtest=1" in the conf file,
   which DragonX ignores. Every "regtest" node therefore ran on MAINNET:
   real genesis, real seeds, real peers. An observed run synced 196,180
   live blocks and 679MB into /tmp before the test timed out. -regtest is
   now passed as a command-line flag, with -connect=0 so an isolated
   regtest node stays off the public network.

With these, nodes start, RPC answers, and tests run to a real result.
They do not all pass yet -- getblocktemplate.py reaches an assertion --
but that is now a test outcome rather than a harness failure.
2026-08-30 19:44:27 -05:00
DanSandClaude Opus 4.8 60d66022f6 regtest: default -checkpoints off so an isolated node leaves IBD
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
2026-08-30 17:26:21 -05:00
DanSandClaude Opus 4.8 3b2aa866aa rpc: honor a command-line rpcpassword across restarts; fix dead -rpcusername key
hush_configfile(), on any restart where the auto-generated DRAGONX.conf
already exists, hard-assigned mapArgs["-rpcpassword"] from the conf and
wrote the username to mapArgs["-rpcusername"] -- a key nothing reads
(InitRPCAuthentication in httprpc.cpp and bitcoin-cli.cpp both read
"-rpcuser"). The hard assignment silently overwrote a -rpcpassword passed
on the command line, so after the first run the effective RPC credentials
became {cmdline-user}:{conf-password}, matching neither the command-line
pair the operator passed nor the full conf pair. Automation or external
clients that connect with the known command-line password broke on every
restart.

Use SoftSetArg for both, so a command-line (or explicitly configured)
value wins and the conf-derived credential is only a fallback. Verified on
regtest: after a restart an external client with the command-line password
gets HTTP 200 and the conf's random password gets 401; the conf-only
operator path (no command-line creds) still authenticates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGhWvdBSgt6UxxHANr7gfN
2026-08-30 16:32:20 -05:00
DanSandClaude Opus 4.8 2d6359ea74 gtest: add job_id parser regression test for the 63-char abort fix
Covers b3e81f1ed: a 63-character mining.submit job_id containing any
non-hex byte must never complete to a 32-byte vector, so the EWBF
completion loop skips it instead of constructing uint256() (which
asserts vch.size()==32 and would abort the daemon from an
unauthenticated client).

Five cases pin the invariant using the real ParseHex/uint256 primitives:
all-spaces, a non-hex byte mid-string, and a non-hex byte at the end
never yield 32 bytes; a genuine 63-hex job_id completes to exactly 32
bytes for all 16 digits; plus ParseHex odd/even-length anchors. Wired
gtest/test_stratum_jobid.cpp into Makefile.gtest.include (5 tests, all
pass; full hush-gtest suite now 18/18).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGhWvdBSgt6UxxHANr7gfN
2026-08-30 16:24:55 -05:00
DanS fa16e740b6 stratum: reject low-diff shares before spending a RandomX hash on them
SubmitBlock validated in the wrong order. After two O(1) length checks it
went straight to CheckRandomXSolution() -- a full ~65ms
randomx_calculate_hash -- and only afterwards tested the share target. So
32 arbitrary bytes from any peer bought a RandomX hash, and the cost was
paid in three bad places at once: on the shared HTTP/RPC libevent thread
(stratum uses EventBase(), the same base ThreadHTTP dispatches, and RPC
replies are posted back onto it), inside the read loop that holds
cs_stratum so BlockWatcher cannot push new work to real miners, and
holding the global cs_randomx_validator that block validation also takes.

One connection writing submit lines pins that thread indefinitely.

Move the share-target test above the RandomX verify. CBlockHeader::
GetHash() is SerializeHash over the header including nSolution, so
meeting the target still requires real SHA256d grinding -- an attacker
now pays for the hash instead of the node. Semantics are unchanged,
including that an empty local_diff parses to zero and still rejects; only
the position moved, and the diagnostics are recomputed locally since the
old message used variables declared further down.

This does not make the submit path cheap, only bounded: at the default
share target the grind is small, so a submit rate limit and moving
SubmitBlock off the event loop are both still wanted.

Also add the authorization check every sibling handler has and
mining.submit lacked. Being explicit about what that is worth:
mining.authorize validates no credential, so the gate is parity and
handshake-ordering, not authentication. The reorder above is the part
that actually bounds an unknown peer.
2026-08-28 23:35:06 -05:00
DanS 5f40c8ede0 stratum: stop paying every miner's blocks to whoever asked for work first
CreateNewBlock() builds the stratum template with an OP_FALSE placeholder
in the coinbase, and CustomizeWork() substituted the miner's own payout
address only while that placeholder was still intact. GetWorkUnit() then
wrote the customized coinbase straight back into the shared template:

    current_work.GetBlock().vtx[0] = cb;
    current_work.GetBlock().hashMerkleRoot = ...BuildMerkleTree();

which consumed the placeholder for everyone. The first client to request
work after a tip change therefore captured the template. Every later
client on that job got a mining.notify whose merkle root already
committed to the first client's coinbase, CustomizeWork() was a no-op for
them on submit, and SubmitBlock() read the shared root back -- so a block
found by miner B was accepted paying miner A. It was silent: the daemon
logged "GOT BLOCK!!! by <B>" while the coinbase paid A.

With untrusted miners that is a reward-theft primitive, and it is cheap:
mining.authorize sets m_send_work, so re-sending it in a loop wins the
race after every tip.

Leave the template pristine and derive each client's header from a local
copy, in GetWorkUnit for the notify and again in SubmitBlock from the
coinbase CustomizeWork() just produced for that client. This is the
refactor the TODO removed here was asking for.

CustomizeWork() now stamps the payout script unconditionally, so a
coinbase that somehow arrives already customized can never be inherited
by another miner, and rejects an invalid address rather than silently
building a coinbase that pays no one.

Single-miner behaviour is unchanged, which is why this survived: it is
only observable with two miners on one template.
2026-08-28 23:31:47 -05:00
DanS b3e81f1eda stratum: do not abort the daemon on a malformed 63-character job_id
The "EWBF 31 bytes job_id fix" in stratum_mining_submit completes a
63-character job_id with each hex digit in turn and feeds the result
straight to uint256(). ParseHex() stops at the first non-hex character
and returns a shorter vector without signalling an error, and
base_blob(const std::vector<unsigned char>&) asserts vch.size() == 32.

So a single mining.submit whose job_id is any 63-character string
containing a non-hex byte -- 63 spaces will do -- aborts the node.
asserts are live in release builds here: -DNDEBUG appears only in
leveldb's own makefile, never in configure.ac, and the shipped binary
still carries the assertion string. The dispatch loop catches UniValue
and std::exception; abort() goes through both.

Size-check each candidate before constructing, as ParseUInt256() a few
hundred lines up already does for the ordinary path. If none of the 16
completions parse, ret stays null, misses work_templates, and the
handler returns false exactly as it does for any unknown job.

Not gated on IsHex(job_id_str): 63 is odd and IsHex() requires an even
length, so that test would disable the EWBF path this code exists for.
2026-08-28 23:30:24 -05:00
DanS d05302d450 build: stamp container builds with the real version instead of "-unk"
.dockerignore excludes .git, so util/genbuild.sh finds no repository inside
the container and emits "// No build information available", which
clientversion.cpp renders as the "-unk" suffix. Every binary produced by
./build.sh --linux-compat therefore self-reports "v1.2.0-unk" and cannot be
traced to a commit -- including release artifacts, since this build path is
part of the v1.2.0 tag.

Pre-generating src/obj/build.h does not survive (genbuild rewrites it when the
content differs), and simply un-ignoring .git does not help a linked worktree,
whose .git is a file pointing outside the build context.

So build.sh computes the version on the host, mirroring genbuild.sh rule for
rule -- the nearest tag only when HEAD is that tag and the tree is clean,
otherwise v<VERSION>-<short sha> with a -dirty suffix -- and passes it through
a BUILD_DESC build-arg that Dockerfile.compat exports as DRAGONX_BUILD_DESC.
genbuild.sh honours that variable when set and is otherwise untouched; with
git metadata present it emits a byte-identical build.h.

Every added git call is guarded with || true because build.sh runs under
set -eu -o pipefail: a source tarball, a host without git, or a branch whose
only reachable tags are lightweight (v1.0.1-v1.0.3 are lightweight; v1.1.0 is
the first annotated one) would otherwise abort the build with no diagnostic.
Those cases now degrade to the previous "-unk" behaviour with a warning.

A direct "docker build -f Dockerfile.compat" passes no BUILD_DESC and still
produces -unk; the Dockerfile now says so loudly rather than silently.
2026-08-29 01:39:24 +02:00
DanSandClaude Opus 4.8 2232868d9f stratum: RandomX pool mining support + reference miner (stratummine)
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>
2026-08-27 20:24:13 -05:00
DanSandClaude Opus 4.8 1745ee4e63 net: fix -connect never dialing its targets (empty-addr IsValid gate)
ConnectNode() rejected the connection on !addrConnect.IsValid() (and
!IsReachable(addrConnect)) BEFORE the pszDest branch. But -connect (and
-addnode host:port / "addnode <host> onetry") reaches ConnectNode with an
empty placeholder addrConnect and the real target in pszDest, resolved
later by ConnectSocketByName(). An empty CAddress is invalid, so every
-connect attempt returned NULL before a socket was ever opened — the peer
logs "trying connection <host>" then "ConnectNode FAILED" and never dials.
(Introduced upstream with BIP155/addrv2; -connect went unused because
normal operation connects via addrman with real, valid addresses.)

Guard both early-return checks with `if (!pszDest)` so they apply only when
dialing addrConnect directly. Connect-by-name now falls through to
ConnectSocketByName() as intended. Direct-address connections (pszDest==NULL,
the addrman path) are unchanged.

Validated: two nodes on one host, B started with `-connect=<A>` exclusively
(no -addnode) now connects to A over TLS, syncs A's chain, and stays
isolated (0 other peers) — previously B connected to nothing. Clean build,
verifychain 4 0 = true.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-27 19:08:36 -05:00
DanSandClaude Opus 4.8 11766ec61b init: fix Windows build regression — fs::path::c_str() into LogPrint
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>
2026-08-27 17:04:36 -05:00
DanSandClaude Opus 4.8 004808403f cleanup: dedup addrman Select_ table walk and pow-limit-by-algo selection
Two behavior-preserving refactors flagged by the Phase-6 hygiene scoping:

* addrman: CAddrMan::Select_ contained two ~40-line copies of the same
  bucket-table random walk, differing only in the table (vvTried/vvNew),
  its bucket count, and a log label. Extract the shared loop into
  SelectFromTable_(vvTable, nBucketCount, tableName); Select_ now just
  dispatches to it. Verbatim move — clean compile proves self-containment.

* pow: the "Equihash uses powLimit, everything else uses powAlternate"
  selection was copy-pasted as an if/else into GetNextWorkRequired,
  CalculateNextWorkRequired, and lwmaCalculateNextWorkRequired. Extract
  into PowLimitForAlgo(params). The CheckProofOfWork site (line ~892) is
  left as-is: it has an extra `height <= 1` genesis special-case and is
  NOT the same selection. On DragonX (RandomX) this always returns
  powAlternate, exactly as before.

Validated: full build of dragonxd/cli/tx, isolated self-mine to height
336, verifychain 4 0 -> true (exercises PowLimitForAlgo on every block).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-27 16:51:16 -05:00
DanSandClaude Opus 4.8 e55c67b0eb wallet: extract ParseSendManyRecipients from z_sendmany
Second slice of the z_sendmany split. The ~60-line loop that parses and
validates the "outputs" array — rejecting unknown keys, bad addresses, memos
on taddrs, oversize memos and negative amounts, and sorting recipients into
taddr/zaddr lists while accumulating nTotalOut — is lifted verbatim into
ParseSendManyRecipients(outputs, branchId, taddrRecipients, zaddrRecipients,
nTotalOut).

Verbatim extract-method (the loop text was moved unchanged, not retyped); the
helper reads only outputs + branchId and writes the three by-reference outputs
the loop already produced, which the clean compile proves self-contained (a
reference to any other z_sendmany local would fail to link). Built clean;
verifychain=true.

Together with the SelectAnyZaddrSource slice, z_sendmany is now ~135 lines
shorter, with source-address resolution and recipient parsing separated out.
Remaining: note selection, Sietch padding, and tx assembly (later slices,
best validated against a synced node since z_sendmany's linkability guard
blocks the send path on a peerless node).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-27 16:28:46 -05:00
DanSandClaude Opus 4.8 4a2e531cb6 wallet: extract SelectAnyZaddrSource from z_sendmany
First slice of the z_sendmany monolith split (Phase-6 refactor). The
`fromaddress == "z"` case ("spend from any zaddr") was ~76 lines inline: it
gathers the wallet's Sapling notes, sums balances per zaddr, and picks a
random zaddr whose confirmed balance covers total outputs + fee. Lift it
verbatim into a helper `SelectAnyZaddrSource(outputs, params)` that returns
the chosen zaddr (or throws the same JSONRPCError), and call it from
z_sendmany.

Behavior-preserving by construction: the block was moved unchanged, the only
edit being `fromaddress = vPotentialAddresses[...]` -> `return ...` with the
call site doing `fromaddress = SelectAnyZaddrSource(outputs, params)`, so
fromaddress receives the identical value (or the identical throw propagates).
The helper takes only outputs/params + globals, which the clean compile
proves (a reference to any z_sendmany local would not link). Built clean;
verifychain=true.

Note: the runtime happy path could not be exercised on an isolated node —
z_sendmany's private-chain "still syncing" linkability guard fires before the
fromaddress resolution when there are no peers — but a verbatim extract-method
does not require it. z_sendmany is now ~76 lines shorter; further extraction
(recipient parsing, fee/tx assembly) remains for follow-up slices.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-27 16:21:49 -05:00
DanSandClaude Opus 4.8 c92e69a13f miner: fix cs_main/mempool.cs lock leak on the isStake error paths
CreateNewBlock takes cs_main and mempool.cs unconditionally via
ENTER_CRITICAL_SECTION, and releases them on every return path. Two of those
LEAVE pairs — the notary-pay failure and the TestBlockValidity failure — were
wrapped in `if (!isStake) { LEAVE; LEAVE; }` but still `return(0)`
afterwards, so when isStake is true the function returned with both locks
still held: a lock leak that deadlocks the next cs_main acquirer. The success
and timelock return paths already release unconditionally, so the guard was
simply wrong.

Make both LEAVE pairs unconditional to match the ENTER. Behavior-identical on
DragonX (a RandomX chain where staking/LWMAPOS is off, so isStake is always
false and the LEAVE already ran), and correct for both isStake values.

Verified: a node self-mined thousands of blocks across two mining threads
(each block ENTER/LEAVEs the locks in CreateNewBlock) with height rising
continuously and verifychain=true — a leak would have deadlocked immediately.

The fuller RAII conversion (replace the manual ENTER/LEAVE with a scoped
LOCK2 for exception safety) remains a worthwhile follow-up, but is a larger
change to cs_main handling best done under its own review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-27 16:05:10 -05:00
DanSandClaude Opus 4.8 81f803948c hygiene: Phase 6 (partial) — fix 7 verified bugs + quick-wins + dedup
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>
2026-08-27 15:55:00 -05:00
DanSandClaude Opus 4.8 817c6b2d0e hygiene: Phase 5 — correct stale comments + name safe magic numbers
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>
2026-08-27 15:19:22 -05:00
DanSandClaude Opus 4.8 cf15b0a399 hygiene: Phase 4 — route unconditional debug output through LogPrint
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>
2026-08-27 15:02:01 -05:00
DanSandClaude Opus 4.8 267e6f7ad5 consensus: drop the dead CBOPRET price-validation from the coinbase check
Step 0 of the scoped HAC/HUSH3-drop work (the zero-risk, self-contained
piece). ContextualCheckCoinbaseTransaction's only action was calling
hush_opretvalidate() for CBOPRET price-oracle validation, gated on
ASSETCHAINS_CBOPRET. On DragonX that global is always 0 (default 0, only
ever set by -ac_cbopret, which DragonX never passes), so the branch is dead
and the function already returns true for every DragonX coinbase.

Remove the dead branch; the function is now unconditionally valid at this
stage, which is behavior-identical on DragonX. Verified: a node self-mines
and `verifychain` re-validates the whole chain (every coinbase re-checked
through this function) = true. hush_opretvalidate (hush_gateway.h) is now
unreferenced; its removal is part of the larger hush_gateway cleanup, tracked
with the rest of the HAC/HUSH3-drop follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-27 14:14:41 -05:00
DanSandClaude Opus 4.8 0de30bbbd1 hygiene: Phase 3 follow-up — sweep commented-out debug cruft
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>
2026-08-27 13:25:17 -05:00
DanSandClaude Opus 4.8 b7060c7de0 hygiene: Phase 3 — dead-code excision (~7,100 lines)
Third phase of the code-hygiene remediation: remove provably-dead code.
Each in-file deletion was adversarially verified (a per-finding investigator
plus a skeptic that greps the tree to refute deletability); the sanity grep
confirms zero dangling references, and the tree builds and self-mines clean.

Removed wholesale:
- src/cc/dapps/ (9 files, ~6,939 lines): a git-tracked but never-built Komodo
  DEX / z-migration tool (hushdex.c, zmigrate.c, cJSON.c, ...). Nothing in
  Makefile.am/configure.ac references it.

In-file dead code:
- pow.cpp: the `#ifdef original_algo` oldRT_CST_RST function (the macro is
  never defined in source or build flags) and two `if ( 0 )` debug blocks.
- walletdb.cpp: the "orphaned staking transaction" cleanup path (deadTxns) —
  DragonX is RandomX PoW with no staking, so the guard is always false and the
  block never runs; also drop the now-unused static/extern decls.
- cc/eval.h: the ProcessCC / Eval::ImportCoin / ImportPayout / DisputePayout
  declarations that have no definition anywhere in the tree.
- rpc/crosschain.cpp: the crosschainproof stub RPC (returned {} unconditionally)
  and its registrations in rpc/server.cpp, rpc/server.h, rpc/client.cpp.
- coins.cpp: the commented-out `//TODO: delete` Sprout PushAnchor template.
- wallet.cpp: the permanently-zero KMD `interest2` term in CreateTransaction.
- rpc/net.cpp: hush_longestchain's always-zero `n` var (num > (n>>1) => num>0)
  and its `if ( 0 )` debug branch.
- hush_nSPV.h / hush_nSPV_fullnode.h: three `if ( 0 && ... )` dead debug branches.
- net.cpp, crypter.h, saplingconsolidation.cpp (dup set_error_code),
  shieldcoinbase.cpp (`donation < 0` on a uint8_t), cclib.cpp (unused
  FAUCET2SIZE): single-line dead-code fixes.

Deliberately NOT touched (verification refuted the audit's "dead on DragonX"
premise): the ~2,250-line HUSH3 checkpoint block, the miner notary/timelock
paths, and hush_gateway.h — all reachable at runtime via -ac_name / -ac_*
args (the inherited Komodo assetchain model), and hush_gateway's
hush_opretvalidate is called from ConnectBlock (consensus). Dropping those
requires a deliberate decision to remove HAC/HUSH3 mode, tracked separately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-27 13:20:51 -05:00
DanSandClaude Opus 4.8 f76382f8e7 Merge fix/windows-cross-build into the v1.2.0 line
Brings the two mingw cross-compile build-system fixes (link-group via
AC_SUBST(LINK_GROUP_*), librustzcash staged-name normalization) onto the
v1.2.0 line that now carries Phases 1-2 of the code-hygiene remediation.
The two sides are disjoint (build-system files vs. src/wallet/rpc), so
this is a clean integration; the merged tree is re-verified to build for
Windows (Phase 1/2 source changes had only been built for Linux before).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-27 12:34:15 -05:00
DanSandClaude Opus 4.8 aff6101987 hygiene: Phase 2 — DragonX-authored cleanup (13 findings)
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>
2026-08-27 12:24:04 -05:00
DanSandClaude Opus 4.8 798eccc624 hygiene: fix seven latent defects from the v1.2.0 code audit
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>
2026-08-27 11:35:38 -05:00
DanSandClaude Opus 4.8 ac95106abe build(win): make the mingw cross-compile link and find librustzcash
Three things broke the x86_64-w64-mingw32 build; two are real and fixed
here (the third was a stale-object contamination from building Linux and
Windows in the same tree, resolved by a clean rebuild — not a code fix).

1. Single-pass mingw ld could not resolve the cross-references DragonX
   added between the internal static archives (libbitcoin_util/common
   objects pulling in UniValue; util<->common mutual deps). GNU ld on
   Linux re-scans archives so it never surfaced; ld64 on macOS rejects
   the grouping flag outright. Bracket each binary's _LDADD in
   -Wl,--start-group/--end-group, delivered via AC_SUBST(LINK_GROUP_*)
   so automake does not reject the linker flag inside _LDADD, and left
   empty on every non-Windows target.

2. The Rust build emits the mingw archive as rustzcash.lib, but the
   link line asks for -lrustzcash, i.e. librustzcash.a. Normalize the
   staged filename to librustzcash.a for every host (a no-op on
   Linux/macOS, where the basename was already librustzcash.a).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-27 03:56:23 -05:00
DanSandClaude Opus 4.8 02b4d03fc6 wallet: low-severity polish from the dev/v1.2.0 review
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
2026-08-26 22:10:35 -05:00
DanSandClaude Opus 5 fad05d3ab4 doc: regenerate manpages for v1.2.0
They still described v1.0.3-4caf2fc68, so none of the options added
since -- -autoshield and its four companions, -mnemonic,
-mnemonicsaplinggap -- appeared anywhere in them.

Generated from a binary built at a clean tree on an annotated v1.2.0
tag, which is what makes util/genbuild.sh emit BUILD_DESC "v1.2.0"
rather than a version with a commit suffix. Note that genbuild.sh uses
`git describe --abbrev=0`, which ignores lightweight tags; v1.0.0
through v1.0.3 were lightweight, which is why those releases all
reported themselves as v1.0.x-<sha>. Tag releases annotated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FU87LdsJZiZkfq1eXubpeo
2026-08-25 18:42:18 -05:00
DanSandClaude Opus 5 5d3f7e520c packaging: correct the debian changelog and the manpage file list
- dragonx.manpages listed DEBIAN/manpages/*.1, a path nothing creates,
  so dh_installman would fail on it. Point it at doc/man/*.1, which is
  where util/gen-manpages.sh writes and what doc/man/Makefile.am ships.
- Restore the 1.0.1 and 1.0.2 stanzas, reconstructed from the commits
  each tag actually contains. The file jumped 1.0.3 -> 1.0.0.
- The 1.1.0 and 1.0.0 trailers named weekdays that do not match their
  dates ("Thu, 21 Aug 2026" is a Friday; "Mon, 03 Mar 2026" is a
  Tuesday). Replace both with the real v1.1.0 and v1.0.0 tag dates,
  which fixes the weekday and the disagreement with the tag at once.
- Record the 1.2.0 peer-discovery work in its stanza.
- Drop doc/man/hushd.html and doc/man/hush-cli.html: stale Hush-branded
  pages for binaries this tree no longer builds, referenced by nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FU87LdsJZiZkfq1eXubpeo
2026-08-25 18:36:03 -05:00
DanSandClaude Opus 5 092a608fd9 chainparams: do not let other smart chains inherit DragonX's seeds
A smart chain builds its params by copying a base network and
overriding pieces, but nothing ever touched vSeeds/vFixedSeeds. That is
how DRAGONX ran on Hush's seeds -- seed1.hush.is and friends, long
since gone from DNS -- for as long as it did, and it means any other
assetchain started from this binary now inherits DragonX's.

Seeds are per-chain by nature: an address serving one chain is useless
to another, and dialling it is at best wasted effort and at worst a
peer speaking a different protocol. DRAGONX keeps the seeds configured
in CMainParams, which are its own; every other chain starts empty and
relies on -addnode/-connect, which an assetchain operator has to
configure regardless.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FU87LdsJZiZkfq1eXubpeo
2026-08-25 18:36:03 -05:00
DanSandClaude Opus 5 3dd667b127 net: add two seed nodes and reserve names for three more
node6.dragonx.is (13.140.58.251) and node7.dragonx.is (5.104.83.100)
are new full nodes in regions the existing five did not cover. Both go
into the compiled-in fixed-seed list and into the DRAGONX -addnode set.

node8 through node10 are reserved names with no DNS records yet. A
hostname that does not resolve is harmless on this path --
ThreadOpenAddedConnections simply fails to open the connection and
retries on its normal cycle -- and reserving the names in the binary
means a future seed can be brought into the default addnode set by
creating a single DNS record, with no release and no waiting for users
to upgrade. seed.dragonx.is already gives the DNS-seed path that
property; this extends it to the addnode path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FU87LdsJZiZkfq1eXubpeo
2026-08-25 18:36:03 -05:00
DanSandClaude Opus 5 dde6cd810f net: seed from a round-robin DNS record instead of hardcoded hosts
seed.dragonx.is is now an A-record set over the five seed nodes (DNS-only, TTL 300,
created in Cloudflare alongside this change). One lookup returns all of them, and
the set can change -- a node added, a node retired -- with a DNS edit rather than a
release.

That is the actual point. Before this the network's entry points were hardcoded
into the binary twice over: here in vSeeds, and again in the -addnode injection in
hush_utils.h. Adding a sixth node meant shipping a new version and waiting for
users to upgrade.

node1 and node5 stay as static fallbacks against the round-robin record being
mistyped or deleted. They resolve to the same hosts, so that is insurance against a
DNS mistake rather than real redundancy.

Verified end to end on a fresh datadir (empty addrman, real node untouched, and
crucially with NO custom -port -- see below):

  before:  0 addresses found from DNS seeds, 0 handshakes, 1 block (genesis)
  after:   7 addresses found, connection attempts to all five seeds on :21768,
           3 version handshakes, 3296 blocks connected and syncing

The earlier run of this test appeared to fail with 0 handshakes. That was the
harness, not the code: -port overrides ASSETCHAINS_P2PPORT, and net.cpp builds
DNS-seeded addresses as CAddress(CService(ip, ASSETCHAINS_P2PPORT)), so a test node
with a custom port dials every seeded peer on its own port and reaches nothing.
That is the mechanism behind the long-standing "never use a custom -port on a test
node" rule; use -listen=0 instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 00:22:56 +02:00
DanSandClaude Opus 5 499f02a905 net: repair DragonX peer discovery, broken three separate ways
A DRAGONX node had no working peer discovery. Both mechanisms were broken, and a
third bug hid the fact.

1. DNS seeds were Hush's, and all three are dead. chainparams_commandline() sets an
   assetchain's port, magic, blocktime, upgrade heights and checkpoints but never
   touches vSeeds or vFixedSeeds, so DRAGONX silently inherited CMainParams':
   seed1.hush.is, seed2.hush.is and dns.leto.net. None of the three has an A record
   any more -- verified against 1.1.1.1 and 8.8.8.8, with google.com and
   node1..node5.dragonx.is resolving fine from the same host as a control. Replaced
   with the five DragonX node hostnames, which do resolve and do listen.

2. Every fixed seed carried port 0. contrib/seeds/generate-seeds.py documents its
   input as <ip>:<port>, but contrib/seeds/nodes_main.txt held bare IPs, so
   parse_spec() took the port as empty and emitted 0x00,0x00 for all five entries.
   The fixed-seed fallback -- which exists precisely for when DNS seeding yields
   nothing -- was therefore handing out unconnectable addresses. Added the port to
   nodes_main.txt and regenerated; entries now end 0x55,0x08 (21768).

3. ThreadDNSAddressSeed never incremented `found`, so "%d addresses found from DNS
   seeds" printed 0 unconditionally, whether seeding worked or not. That is almost
   certainly why nobody noticed the seeds had gone dead: the one diagnostic that
   would have shown it was hardcoded to say zero.

Verified on a fresh datadir (empty addrman, separate ports, real node untouched):
DNS seeding now reports "5 addresses found from DNS seeds" where it previously
reported 0, and the fixed-seed path adds 5 entries carrying the correct port.

Note on scope: the five hostnames are single-A-record hosts, so each contributes one
address rather than the spread a real seeder returns. A dedicated DNS seeder, or
simply a round-robin A record over the seed set, would be the proper fix and needs
only a DNS change rather than a release. This restores a working discovery path;
it does not make it a good one.

Also corrected the generated header's #endif comment, which said
HUSH_CHAINPARAMSSEEDS_H while the guard is DRAGONX_CHAINPARAMSSEEDS_H.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 00:11:55 +02:00
DanSandClaude Opus 5 4dc57e80b1 build: bump version to 1.2.0
dev and the v1.1.0 tag were version-indistinguishable: both reported
CLIENT_VERSION 1010050, subversion "/DragonX:1.1.0/" and IS_RELEASE=true, because
660678f9b was the last commit to touch a version file and it predates the tag. The
twenty commits since were therefore invisible to every channel a client can query,
and the in-app updater compares exactly those. Only git-describe distinguished
them, and that degrades to "-unk" on a tarball build.

A minor bump rather than a patch: since v1.1.0 the tree gained auto-shield-coinbase
(a new feature, on by default where the seed is known-recoverable), BIP39 seed
phrases as the default for new wallets, the z_autoshieldstatus RPC, and three new
wallet.dat record types. Understating that as 1.1.1 would hide an on-disk format
change from the one place users look.

The published v1.1.0 tag is left where it is. Re-pointing a tag that is already on
the remote breaks anyone who fetched it.

The wallet feature version deliberately stays at FEATURE_LATEST = 60000. The new
records are additive and older binaries skip unknown types harmlessly, while
bumping it would make them refuse the wallet outright with DB_TOO_NEW. The one real
incompatibility, a truncated hdchain record, is self-healing as of a0ccb4be1, so
refusing to load would be strictly worse for the user than what happens today.

Verified: configure.ac and clientversion.h agree; CLIENT_VERSION 1010050 -> 1020050;
build.sh derives 1.2.0; bitcoin-config.h carries CLIENT_VERSION_MINOR 2 after a
reconfigure run with the depends CONFIG_SITE; a full rebuild of src succeeds with 0
errors and both binaries report v1.2.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 21:52:51 +02:00
DanSandClaude Opus 5 04ac7c1186 doc: how to build release binaries in containers, for several glibc floors
Binaries built on Ubuntu 22.04 require GLIBC_2.34 and GLIBCXX_3.4.30 and will not
start on Ubuntu 20.04 -- which is four of our five seeds, and an unknown share of
users. The binary the fleet actually runs today needs only GLIBC_2.29, so it was
built somewhere older; seed 176 has since been upgraded to 22.04 and now produces
binaries it is the only seed able to run.

--linux-compat and Dockerfile.compat already solved this (6d56ad854) but were
undocumented outside the build script and pinned to one base image. Parameterise
the base via ARG BASE_IMAGE (default unchanged, so --linux-compat behaves exactly
as before) and document the whole path.

doc/build-containers.md is written to be executed by a person or an agent starting
from a machine with nothing installed: why the glibc direction matters, with the
measured numbers; what already exists in the repo so nobody writes a second build
system; prerequisites and honest cost (~15GB, 4GB RAM, 1-2h per base because
depends/ builds boost, BDB, wolfssl and rust from source); one-target and
multi-target recipes; which base to pick and why 20.04 is the recommended floor
while 18.04 needs verifying (GCC 7 against -std=c++17); a mandatory verification
step with the exact objdump/readelf commands and the expected ceilings; and the
traps.

The traps are the part worth having written down: ETXTBSY when installing over a
running daemon (cp fails even after the process exits -- stage and rename, then
sha256-verify before starting); never touching configure.ac in a configured tree,
because the mtime alone triggers a reconfigure that dies on libdb_cxx; never
blind-touching a path that may not exist, which silently creates stray empty files;
RandomX needing ARCH=default or it emits AVX-512 that SIGILLs the fleet; build-win.sh
silently discarding every argument; and macOS being uncontainerisable because
depends/ has no darwin cross path at all.

Also records that full static linking is NOT the answer here: the daemon resolves
node1..node5.dragonx.is via getaddrinfo, and static glibc pushes that through NSS,
which dlopens libnss_dns at runtime and reintroduces the dependency it was meant to
remove.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 21:28:12 +02:00
DanSandClaude Opus 5 65130c3120 async/wallet: close the loose ends around automated operations
Six small defects, all found by an audit of the automated-operation path and all
verified present before changing anything.

AsyncRPCQueue::addOperation returned void and silently dropped the operation when
the queue was closed or finishing. Every caller assumed success: the three
schedulers left their running flag set with nothing in flight (which then blocks
every later round), and z_sendmany / z_shieldcoinbase / z_mergetoaddress returned
an opid for work that would never run -- z_shieldcoinbase and z_mergetoaddress
having already locked their selected coins in the constructor. It now returns
bool; the schedulers release the flag and log, and the three RPCs raise an error
instead of handing back an opid. Coin locks are memory-only, so a refusal at
shutdown reclaims them with the process; the lie about success was the defect.

Nothing ever removed finished automated operations from the queue's map.
popOperationForId is reached only from z_getoperationresult, so on a node running
autoshield every 25 blocks the map grew by one entry per round forever. The
schedulers now pop the operation they just cancelled. The worker already handles
a missing id ("cannot find operation in map, may have been removed",
asyncrpcqueue.cpp), and it releases lock_ before calling main(), so popping under
cs_wallet introduces no lock cycle.

The autoshield operation built its transaction against targetHeight_, the
enqueue-time height, while SetExpiryHeight and the network-upgrade straddle guard
both used tipHeight. Since the builder's height selects the consensus branch id,
the guard was checking a height the transaction was not signed against -- it could
not prevent the failure it exists to prevent. Now tipHeight throughout.

cancel() in the sweep, consolidation and autoshield operations set CANCELLED
unconditionally, dropping the base class's guard entirely. The schedulers cancel
the previous operation when they enqueue the next, so a round that had already
SUCCEEDED got its result relabelled as cancelled. Restored a narrower guard: still
cancellable while READY or EXECUTING (the base class refuses the latter, which
would defeat cancellation here), but a terminal state is left alone.

CommitAutomatedTx dumped the whole transaction to stderr on every commit,
duplicating the LogPrintf that CommitTransaction does one call later. ToString()
emits a line per input, so with the 400-input autoshield cap that was tens of KB
of stderr per round. Removed.

Also corrected a comment that credited the immature-coinbase exclusion to
fOnlySpendable (the argument is fOnlyConfirmed; the exclusion is unconditional in
AvailableCoins), and noted that AUTOSHIELD_CTXIN_P2SH_SIZE is a byte size that
merely happens to share the value 400 with the input cap.

Verified on an isolated regtest chain, 8/8: nine consecutive autoshield rounds
succeed after the builder-height change; the operation map stays at 1 entry across
all nine (it grew one per round before); stderr totals 1608 bytes for the whole
run with zero CommitAutomatedTx dumps, while debug.log still records all nine
commits via CommitTransaction; no round is relabelled cancelled; funds shield
correctly and no coin locks leak.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 19:18:30 +02:00
DanSandClaude Opus 5 698bcf9574 build: derive the release version from configure.ac
build.sh hardcoded VERSION="1.0.3" while configure.ac had been at 1.1.0 since
660678f9b. package_release() uses it to name the output directory, so
`./build.sh --all-release` from dev would have emitted
release/dragonx-1.0.3-<platform>/ containing binaries that report 1.1.0 --
mislabelled artifacts, from the one place where the label is what users see.

Read the four _CLIENT_VERSION_* defines out of configure.ac instead, applying the
same suffix rule its _CLIENT_VERSION_SUFFIX m4 uses (build < 25 -> beta, < 50 ->
rc, == 50 -> plain, > 50 -> point release), and abort if any of them cannot be
parsed rather than naming a release directory after an empty string.

SCRIPT_DIR moves above the version block because the lookup needs it.

Verified: derives 1.1.0 from the current tree, and a deliberately unparseable
configure.ac makes it exit 1 with a message instead of guessing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 18:56:18 +02:00
DanSandClaude Opus 5 a0ccb4be1d wallet: repair a truncated hdchain record instead of refusing the wallet
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>
2026-08-25 18:18:38 +02:00
DanSandClaude Opus 5 358011bd54 wallet: resolve the autoshield destination at startup
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>
2026-08-25 17:41:32 +02:00
DanSandClaude Opus 5 7a62fc4877 wallet: break the sweep/consolidation/autoshield deadlock
A successful-but-incomplete sweep round deliberately returns with fSweepRunning
still set and nextSweep unadvanced, as a "keep draining next block" baton. Every
early return in RunSaplingSweep, though, leaves that baton set without
re-dispatching -- and RunSaplingConsolidation, which is gated on fSweepRunning,
then returns without advancing nextConsolidation. So the "consolidation is
within 5 blocks" blackout at the top of RunSaplingSweep never lifts: sweep waits
on consolidation, consolidation waits on sweep, and neither runs again.

That much is pre-existing. What is new is that autoshield now shares the gate --
RunAutoShieldCoinbase returns early on fSweepRunning || fConsolidationRunning --
so a wedged sweep silently disables coinbase shielding too, with
z_autoshieldstatus reporting autoshield true, running false, and no reason.

Only honour the baton while a sweep operation is genuinely in flight: if the
operation for saplingSweepOperationId is absent or has reached a terminal state,
drop the stale flag and let the checks below decide afresh. The drain model is
unchanged -- nextSweep is still unadvanced, so the next block re-dispatches.

Also report the deferral in z_autoshieldstatus, so mutual exclusion with sweep
or consolidation reads as a deferral rather than an unexplained idle.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 07:59:13 +02:00
DanSandClaude Opus 5 9a8f17b2c8 wallet: bound autoshield rounds and lock their inputs
Two defects in a single autoshield round, both of the quiet kind.

The size estimate reserved a flat 2000 bytes for "header + sietch outputs", but
every autoshield tx carries three Sapling OutputDescriptions -- the change note
plus the two Sietch dummies -- and the real fixed cost is ~2937 bytes. Measured
across seven live mainnet coinbase shields: 113.5 bytes per input and 2936.6
+/- 0.8 bytes fixed, of which 3 * 948 = 2844 is the output descriptions. The
estimate was therefore short by ~937 bytes before a single input was counted.

Inputs are charged AUTOSHIELD_CTXIN_DUST_SIZE = 148, which is conservative for
the default P2PK coinbase but exact for P2PKH, so with a P2PKH coinbase a
backlog of 1332..1337 utxos passed the estimate and built a tx over
MAX_TX_SIZE_AFTER_SAPLING. CommitTransaction calls AddToWallet before
AcceptToMemoryPool, so a rejected oversize tx leaves its inputs reading as spent.

z_shieldcoinbase caps a manual shield at SHIELD_COINBASE_DEFAULT_LIMIT = 50
utxos; autoshield dropped that cap and relied on the byte estimate alone.
Restore one -- AUTOSHIELD_MAX_INPUTS = 400 -- so the byte arithmetic is no longer
the only thing between a large backlog and an oversize transaction. The
remainder is shielded on the next round.

Second, the proof build deliberately runs without cs_wallet so wallet RPCs are
not stalled, which leaves a multi-second window in which a concurrent
z_shieldcoinbase or z_sendmany can re-select the same coinbase outputs.
AvailableCoins already honours IsLockedCoin and z_shieldcoinbase already
brackets its selection with LockCoin/UnlockCoin; autoshield made zero LockCoin
calls. Take the locks under cs_wallet at selection time and release them via
RAII, since several early returns sit between selection and commit and a leaked
lock would exclude those coins from every future round.

Verified on an isolated regtest chain with a 540-utxo backlog:
  round 1 logged "reached per-round input cap (400)" and committed exactly 400
    inputs in a 48351-byte tx (estimate 62300, limit 200000)
  round 2 took the remaining 151; backlog drained 540 -> 0
  listlockunspent showed 400 coins locked mid-round and 0 afterwards
  48351 bytes for 400 inputs implies 2937 bytes of fixed overhead, agreeing
    with the mainnet measurement to 14 bytes

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 07:59:13 +02:00
DanSandClaude Opus 5 1ec6590fb7 wallet: add z_autoshieldstatus
Auto-shielding could silently decline to run with no way to ask why. The
destination it resolves was equally invisible: the only evidence was a LogPrintf
emitted once per round, so an operator wanting to know where their mined coinbase
was going had to grep debug.log.

z_autoshieldstatus reports the enable state, whether a round is in flight, the
next height, interval, fee, minimum utxos, and the resolved destination -- plus
the HD seed provenance in both numeric and readable form, whether the seed is
phrase-recoverable, and a disabled_reason explaining why it is off when it is.

That last field is the point. "autoshield": false on its own does not distinguish
an operator who passed -autoshield=0 from a wallet whose seed provenance is not
known-recoverable, and those need different responses.

Mirrors z_sweepstatus in shape and registration.

Verified on all three branches:
  fresh wallet    -> autoshield true, origin 1 "created on an empty wallet",
                     seed_recoverable true, disabled_reason ""
  -autoshield=0   -> disabled_reason "disabled by -autoshield=0"
  upgraded wallet -> autoshield false, origin 4 "predates provenance recording",
                     seed_recoverable false, disabled_reason "HD seed origin is
                     not known-recoverable; back the seed up and pass -autoshield=1"

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 06:04:43 +02:00
DanSandClaude Opus 5 92e6c7008d wallet: define CHDChain's static constants out of line
hush-gtest failed to link with "undefined reference to
CHDChain::VERSION_HD_MNEMONIC". The version constants are static const int with
in-class initialisers and no definition anywhere, so any ODR use needs one --
and gtest's EXPECT_*/ASSERT_* macros take their arguments by const reference,
which is exactly that. test_mnemonic_compat.cpp:161 passes VERSION_HD_MNEMONIC
to EXPECT_LT.

dragonxd links either way, because nothing in the daemon binds these to a
reference; only the test target exposed it, and the test target was never built
on the branch that introduced the test.

Define all four rather than only the one that failed: VERSION_HD_BASE,
VERSION_HD_TRANSPARENT and CURRENT_VERSION carry the identical latent fault, and
the next EXPECT_EQ against any of them would hit the same wall. Fixing the test
instead would have hidden the problem rather than removed it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 18:58:24 +02:00
DanSandClaude Opus 5 733df964ec Merge autoshield-safety into dev: auto-shield coinbase + BIP39 by default
Brings the auto-shield-coinbase feature and the wallet seed work onto the release
line so both are exercised from dev rather than a side branch.

From upstream: auto-shield of matured coinbase into a wallet z-address, plus
fixes to two pre-existing scheduler wedges (consolidation dispatching every block
instead of once per interval, and a failing sweep stranding its running flag).

On top of that:
  * the auto-shield destination is chosen by re-deriving m/32'/coin'/i' from the
    seed and taking the lowest in-gap account the wallet holds a key for, rather
    than the first entry in std::set order. Key metadata is NOT evidence of
    provenance -- z_importwallet copies hdKeypath and seedFp verbatim from the
    import file -- so a crafted import could otherwise claim account 0 and
    capture every shielded reward;
  * HD seed/chain persistence is hardened: the chain record is written before the
    seed, a corrupt hdchain is loud rather than silently reverting derivation to
    the raw entropy, hdchain survives -salvagewallet, and the silent BIP39 ->
    random seed fallback is gone;
  * seed provenance is recorded, and -autoshield defaults ON only where that
    provenance says the seed is recoverable;
  * mnemonic wallets now store the EXPANDED 64-byte BIP39 seed with the 32-byte
    entropy in a separate display-only record. Derivation reads stored bytes
    directly on every binary, so key trees are identical and no CHDChain version
    bump or minversion fence is needed -- the format stays readable by earlier
    releases;
  * new wallets are created from a BIP39 phrase by default;
  * the plaintext hdseed record is erased when a wallet is encrypted. It was
    previously left behind, and CDB::Rewrite copies surviving records verbatim,
    so the unencrypted seed persisted on disk forever.

TWO DEFAULTS CHANGE for new wallets: auto-shielding (where the seed provenance is
known) and BIP39 seed generation. Existing wallets are untouched -- seed
generation is reachable only when a wallet has none, and all three key stores
refuse to replace an existing seed.

Testing state, stated plainly: every commit built clean and the branch was
verified on an isolated chain -- destination selection provably ignores an
imported foreign key, both provenance branches behave, a new-format wallet
reopened by a pre-change binary lists identical addresses, and restoring only the
24 words recovers the wallet. NOT yet tested: any of this against a funded wallet
on mainnet, or a soak of these defaults on a real node.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 11:53:32 -05:00
DanSandClaude Opus 5 d12e7dc99d Merge origin/master into dev: restore the mingw -Wa,-mbig-obj flag
dev branched from the release line before 1c3523aac and so lost -Wa,-mbig-obj
from util/build-win.sh. Without it the Windows cross-compile fails at link: large
template/boost-heavy translation units exceed the PE/COFF ~32k-section limit, and
GNU ld emits "dangerous relocation" on .pdata and crashes. v1.1.0 therefore could
not produce a win64 binary at all.

The merge brings only that one file back. util/build-win.sh is the only file both
sides touched, and the two edits are ~20 lines apart, so it auto-merges keeping
both fixes: -Wa,-mbig-obj on the configure line and -DARCH=default on the cmake
line. Verified after merging that dev's own work is intact -- the extended
checkpoint table, the guarded RandomX dedup, the 1.1.0 version bump, and
ARCH=default in the other two build scripts.

No C++ changed; the only delta from the pre-merge dev is the shell script.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 10:42:29 -05:00
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