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>
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>
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>
RandomX PoW verification is ~84% of block-connect wall time during network IBD, and
CheckBlock was recomputing it TWICE per block: once in CheckBlockHeader and again in
hush_checkPOW (which has no CBlockIndex, so it cannot use the fRandomXVerified dedup
the parallel pre-verify pool relies on). Skip the redundant recompute inside
hush_checkPOW: CheckBlockHeader runs first in CheckBlock and rejects an invalid
solution before hush_checkPOW is reached, so the block is already verified once.
Equihash, PoW-target and notary checks in hush_checkPOW still run.
A scoped guard (ScopedRandomXSkip) SAVES and RESTORES the thread-local
fSkipRandomXValidation, so it neither clobbers the miner's own skip
(TestBlockValidity -> ConnectBlock re-entry, which would otherwise force the ~256MB
inline RandomX alloc the miner deliberately avoids) nor leaks the flag on an exception.
Measured on an isolated RandomX test chain: RandomX verifies per block 2.0 -> 1.03
(~40% faster network sync). The 2x behavior pre-exists in v1.0.2. Consensus-neutral:
RandomXPreVerify.ConsensusEquivalence gtest passes; each block is still verified
exactly once by CheckBlockHeader.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
RandomXValidationRequired skipped RandomX verification for blocks below the
static top checkpoint (GetTotalBlocksEstimate), while the fork-rejection guard
uses the in-index checkpoint (GetLastCheckpoint). Once the checkpoint list
extends above the RandomX activation height, that asymmetry opens a gap during
IBD/eclipse in which a peer with no RandomX hashpower can get SHA256-grinded,
RandomX-forged blocks accepted (CheckProofOfWork hashes the header including the
attacker-controlled nSolution). Gate the skip on GetLastCheckpoint()->GetHeight()
so a block is PoW-exempt only when provably below a checkpoint the node has
locked into its index -- the same boundary the fork guard uses. Currently inert
(top checkpoint 2838000 < activation 2838976) but becomes live at the next
checkpoint refresh; the check runs under cs_main and only ever adds verification.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Parallel RandomX PoW pre-verification pool (CCheckQueue) run ahead of the serial
connect; consensus-neutral (inline CheckRandomXSolution fallback still verifies
anything not pre-verified). New -randomxverifythreads (default = -par).
- Adaptive dbcache: default sizes the UTXO/coins cache to most of RAM and shrinks
under memory pressure, always leaving a reserve free; -dbcache pins a fixed value.
- P2P block download: bounded socket recv-drain loop (tlsmanager); frontier-block
reassignment to break head-of-line stalls (-blockreassigntimeout); ProcessGetData
serves a bounded batch of blocks per pass instead of one (fixes the serve-side
one-block-per-tick throttle that caps download network-wide).
- assumeutxo: dumptxoutset RPC + LoadSnapshot machinery + AssumeutxoData chainparams.
- Signed bootstrap verification (util/bootstrap-dragonx.sh, util/sign-bootstrap.md).
- gtest: RandomX pre-verify consensus-equivalence test + UTXO-snapshot round-trip;
revived the gtest harness (Makefile.am include fix, Makefile.gtest.include).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fresh-syncing nodes rejected the on-chain min-diff block at the
RANDOMX_VALIDATION activation height (2838976) because GetNextWorkRequired
computed the expected nBits from the preceding normal-difficulty blocks,
producing 469847994 instead of the on-chain 0x200f0f0f (HUSH_MINDIFF_NBITS).
This caused all seed nodes to be banned with "Incorrect diffbits" and the
node could never sync past that height.
Two changes:
1. GetNextWorkRequired (pow.cpp): Return nProofOfWorkLimit at the exact
RANDOMX_VALIDATION activation height, matching the on-chain diff reset.
2. ContextualCheckBlockHeader (main.cpp): Raise DragonX daaForkHeight to
RANDOMX_VALIDATION + 62000, covering the window where nBits was never
validated (diff reset at 2838976 through the attack at ~2879907).
Tested by invalidating block 2838975 and reconsidering — node re-validated
through the diff reset and attack window, syncing back to tip with zero
bad-diffbits rejections.
Bump version to 1.0.1.
Two critical vulnerabilities allowed an attacker to flood the DragonX chain
with minimum-difficulty blocks starting at height 2879907:
1. ContextualCheckBlockHeader only validated nBits for HUSH3 mainnet
(gated behind `if (ishush3)`), never for HAC/smart chains. An attacker
could submit blocks claiming any difficulty and the node accepted them.
Add nBits validation for all non-HUSH3 smart chains, gated above
daaForkHeight (default 450000) to maintain consensus with early chain
history that was mined by a different binary.
2. The rebrand commit (85c8d7f7d) commented out the `return false` block
in CheckProofOfWork that rejects blocks whose hash does not meet the
claimed target. This made PoW validation a no-op — any hash passed.
Restore the rejection block and add RANDOMX_VALIDATION height-gated
logic so blocks after the activation height are always validated even
during initial block loading.
Vulnerability #1 was inherited from the upstream hush3 codebase.
Vulnerability #2 was introduced by the DragonX rebrand.
Minimal rebrand (see compliant-rebrand branch for full rebrand):
- Rename binaries: hushd/hush-cli/hush-tx → dragonxd/dragonx-cli/dragonx-tx
- Default to DRAGONX chain params without -ac_* flags (randomx, blocktime=36, private=1)
- Update configure.ac: AC_INIT([DragonX],[1.0.0])
- Update client version string and user-agent to /DragonX:1.0.0/
- Add chainparams.cpp with DRAGONX network parameters
- Update build.sh, miner.cpp, pow.cpp for DragonX
- Add bootstrap-dragonx.sh utility script
- Update .gitignore for release directory
Share single RandomX dataset across all mining threads:
- Add RandomXDatasetManager with readers-writer lock, reducing RAM from
~2GB per thread to ~2GB total plus ~2MB per thread for the VM scratchpad
- Add LogProcessMemory() diagnostic helper for Linux and Windows
- Add CheckRandomXSolution() to validate RandomX PoW in nSolution field
- Add ASSETCHAINS_RANDOMX_VALIDATION activation height per chain
(DRAGONX: 2838976, TUMIN: 1200, others: height 1)
- Add CRandomXInput serializer for deterministic RandomX hash input
- Fix CheckProofOfWork() to properly reject invalid PoW (was missing
SMART_CHAIN_SYMBOL check, allowing bypass)
- Call CheckRandomXSolution() in hush_checkPOW and CheckBlockHeader
Without this fix, attackers could submit blocks with invalid RandomX
hashes that passed validation, as CheckProofOfWork returned early
during block loading and the nSolution field was never verified.
Some comments turned out to be wrong and some could be more helpful.
It turns out that when AveragingWindowTimespan was changed to fix a HUSH
mainnet bug long ago, that introduced a bug for HSC's that do not use
a 75s block time. Since the default is 60s that likely means all HSC's that will
be created. There were no production HSC's in use at the time of that bugfix,
so the bug went unnoticed until DRAGONX was launched. The bug then manifested
as the DRAGONX difficulty bug, which cause the difficulty to never correct down,
only up and lead to extremely long block times on DRAGONX mainnet.
This code change ensures that HUSH mainnet uses the same hardcoded AWT as it
did previously and all other HSC's will use params.AveragingWindowTimespan() ,
including DRAGONX mainnet.
This seems less dangerous than changing AveragingWindowTimespan() on HUSH mainnet.
Any projects which want to use Hush code from now on will need to be licensed as
GPLv3 or we will send the lawyers: https://www.softwarefreedom.org/
Notably, Komodo (KMD) is licensed as GPLv2 and is no longer compatible to receive
code changes, without causing legal issues. MIT projects, such as Zcash, also cannot pull
in changes from the Hush Full Node without permission from The Hush Developers,
which may in some circumstances grant an MIT license on a case-by-case basis.