3 Commits

Author SHA1 Message Date
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
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
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
12 changed files with 39 additions and 102 deletions

View File

@@ -3,7 +3,7 @@
.SH NAME
dragonx-cli \- manual page for dragonx-cli v1.3.0
.SH DESCRIPTION
DragonX RPC client version v1.3.0\-af7d9e230
DragonX RPC client version v1.3.0
.PP
In order to ensure you are adequately protecting your privacy when using
DragonX, please see <https://dragonx.is/security/>.

View File

@@ -3,7 +3,7 @@
.SH NAME
dragonx-tx \- manual page for dragonx-tx v1.3.0
.SH DESCRIPTION
hush\-tx utility version v1.3.0\-af7d9e230
hush\-tx utility version v1.3.0
.SS "Usage:"
.TP
hush\-tx [options] <hex\-tx> [commands]

View File

@@ -3,7 +3,7 @@
.SH NAME
dragonxd \- manual page for dragonxd v1.3.0
.SH DESCRIPTION
DragonX Daemon version v1.3.0\-af7d9e230
DragonX Daemon version v1.3.0
.PP
In order to ensure you are adequately protecting your privacy when using
DragonX, please see <https://dragonx.is/security/>.

View File

@@ -420,10 +420,6 @@ static void libevent_log_cb(int severity, const char *msg)
LogPrint("libevent", "libevent: %s\n", msg);
}
/** Cap on the combined size of an HTTP request line + headers. libevent's default is EV_SIZE_MAX,
* i.e. unbounded, and it buffers before any ACL or auth check runs. */
static const size_t MAX_HEADERS_SIZE = 8192;
bool InitHTTPServer()
{
struct evhttp* http = 0;
@@ -471,10 +467,6 @@ bool InitHTTPServer()
evhttp_set_timeout(http, GetArg("-rpcservertimeout", DEFAULT_HTTP_SERVER_TIMEOUT));
evhttp_set_max_body_size(http, MAX_SIZE);
// libevent defaults max_headers_size to EV_SIZE_MAX, so without this a single connection can
// stream an unbounded request line / header block and grow RSS ~1:1 with bytes sent, BEFORE the
// -rpcallowip ACL or auth check runs (both happen after libevent has parsed the request).
evhttp_set_max_headers_size(http, MAX_HEADERS_SIZE);
evhttp_set_gencb(http, http_request_cb, NULL);
if (!HTTPBindAddresses(http)) {

View File

@@ -8336,24 +8336,6 @@ bool SendMessages(CNode* pto, bool fSendTrickle)
// Message: addr
if (fSendTrickle)
{
// Accumulate into vAddr and send it ONCE (or in MAX_ADDR_TO_SEND-sized batches).
// This loop previously pushed the ENTIRE pto->vAddrToSend on every accepted address,
// so a single 24-byte getaddr produced N messages of N addresses each instead of one
// message of N -- ~500x the intended bandwidth on a typical addrman, all serialized
// (with per-message double-SHA256 checksums) while cs_main is held. The locally built
// vAddr was accumulated and then discarded, and the vAddr.resize(MAX_ADDR_TO_SEND) was
// a no-op standing where upstream has vAddr.clear().
const char* msg_type;
int make_flags;
if (pto->m_wants_addrv2) {
msg_type = NetMsgType::ADDRV2;
make_flags = ADDRV2_FORMAT;
} else {
msg_type = NetMsgType::ADDR;
make_flags = 0;
}
const CNetMsgMaker msgMaker(std::min(pto->nVersion, PROTOCOL_VERSION));
vector<CAddress> vAddr;
vAddr.reserve(pto->vAddrToSend.size());
BOOST_FOREACH(const CAddress& addr, pto->vAddrToSend)
@@ -8361,17 +8343,29 @@ bool SendMessages(CNode* pto, bool fSendTrickle)
if (pto->AddAddressIfNotAlreadyKnown(addr))
{
vAddr.push_back(addr);
if (vAddr.size() >= MAX_ADDR_TO_SEND)
{
pto->PushAddrMessage(msgMaker.Make(make_flags, msg_type, vAddr));
vAddr.clear();
// Should be impossible since we always check size before adding to
// vAddrToSend. Recover by trimming the vector.
vAddr.resize(MAX_ADDR_TO_SEND);
}
const char* msg_type;
int make_flags;
if (pto->m_wants_addrv2) {
msg_type = NetMsgType::ADDRV2;
make_flags = ADDRV2_FORMAT;
} else {
msg_type = NetMsgType::ADDR;
make_flags = 0;
}
pto->PushAddrMessage(CNetMsgMaker(std::min(pto->nVersion, PROTOCOL_VERSION)).Make(make_flags, msg_type, pto->vAddrToSend));
}
}
pto->vAddrToSend.clear();
if (!vAddr.empty())
pto->PushAddrMessage(msgMaker.Make(make_flags, msg_type, vAddr));
vAddr.clear();
}
CNodeState &state = *State(pto->GetId());

View File

@@ -607,11 +607,7 @@ public:
// Known checking here is only to save space from duplicates.
// SendMessages will filter it again for knowns that were added
// after addresses were pushed.
// NOTE: _addr (the address being queued), NOT addr (this peer's own address, net.h ~416).
// Testing the member made the filter constant for the connection's lifetime: once the peer's
// own address entered its addrKnown -- routine, via the remote's AdvertizeLocal -- every
// relay path to it silently no-opped until the daily addrKnown.reset().
if (_addr.IsValid() && !IsAddressKnown(_addr) && addr_format_supported) {
if (_addr.IsValid() && !IsAddressKnown(addr) && addr_format_supported) {
if (vAddrToSend.size() >= MAX_ADDR_TO_SEND) {
vAddrToSend[insecure_rand() % vAddrToSend.size()] = _addr;

View File

@@ -534,10 +534,6 @@ UniValue getblockdeltas(const UniValue& params, bool fHelp, const CPubKey& mypk)
if (fHelp || params.size() != 1)
throw runtime_error("");
// Reads mapBlockIndex / chainActive (and, below, pcoinsTip's mutable anchor cache),
// all of which are cs_main-guarded. Every sibling RPC in this file locks; this one did not.
LOCK(cs_main);
std::string strHash = params[0].get_str();
uint256 hash(uint256S(strHash));
@@ -606,19 +602,11 @@ UniValue getblockhashes(const UniValue& params, bool fHelp, const CPubKey& mypk)
std::vector<std::pair<uint256, unsigned int> > blockHashes;
{
// The lock must SPAN GetTimestampIndex: with fActiveOnly it calls blockOnchainActive() for
// every row, which reads mapBlockIndex and chainActive. The previous form was
// if (fActiveOnly)
// LOCK(cs_main);
// and LOCK() declares a scoped object, so as an unbraced substatement it was constructed
// and destroyed on that line -- the walk then ran completely unsynchronised. Taken
// unconditionally here: this RPC is explorer-only and not hot, and a conditional lock is
// exactly the shape that produced the bug.
if (fActiveOnly)
LOCK(cs_main);
if (!GetTimestampIndex(high, low, fActiveOnly, blockHashes)) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "No information available for block hashes");
}
if (!GetTimestampIndex(high, low, fActiveOnly, blockHashes)) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "No information available for block hashes");
}
UniValue result(UniValue::VARR);
@@ -889,10 +877,6 @@ UniValue getblockmerkletree(const UniValue& params, bool fHelp, const CPubKey& m
+ HelpExampleRpc("getblockmerkletree", "290000")
);
// Reads mapBlockIndex / chainActive (and, below, pcoinsTip's mutable anchor cache),
// all of which are cs_main-guarded. Every sibling RPC in this file locks; this one did not.
LOCK(cs_main);
CBlockIndex* phushblockindex;
uint256 blockRoot;
SaplingMerkleTree tree;

View File

@@ -759,25 +759,9 @@ UniValue getblocktemplate(const UniValue& params, bool fHelp, const CPubKey& myp
#ifdef ENABLE_WALLET
CReserveKey reservekey(pwalletMain);
LEAVE_CRITICAL_SECTION(cs_main);
// MUST re-enter cs_main before letting an exception escape. The enclosing LOCK(cs_main) is
// a scoped CMutexLock whose owns_lock is still true, so if CreateNewBlockWithKey throws
// (any wallet/BDB fault: disk full, EMFILE, a corrupt wallet.dat) its destructor unlocks an
// already-unlocked mutex during unwinding -> BOOST_VERIFY -> SIGABRT. Asserts cannot be
// compiled out here (main.cpp #errors on NDEBUG), so this aborts the daemon instead of
// returning the actionable error, and the abort happens inside unwinding so nothing is logged.
try {
pblocktemplate = CreateNewBlockWithKey(reservekey,pindexPrevNew->GetHeight()+1,HUSH_MAXGPUCOUNT,false);
} catch (...) {
ENTER_CRITICAL_SECTION(cs_main);
throw;
}
pblocktemplate = CreateNewBlockWithKey(reservekey,pindexPrevNew->GetHeight()+1,HUSH_MAXGPUCOUNT,false);
#else
try {
pblocktemplate = CreateNewBlockWithKey();
} catch (...) {
ENTER_CRITICAL_SECTION(cs_main);
throw;
}
pblocktemplate = CreateNewBlockWithKey();
#endif
ENTER_CRITICAL_SECTION(cs_main);
if (!pblocktemplate)

View File

@@ -22,7 +22,13 @@
// network protocol versioning
// DragonX 1.0.0 - bumped to separate from old HUSH/DragonX nodes with RandomX bug
static const int PROTOCOL_VERSION = 2000000;
// DragonX 1.3.0 - bumped for the consensus changes in this release (BLOCK_VALID_CONTEXT
// no longer overwrites the validity level, the dead CBOPRET coinbase
// price check is gone, and pow.cpp lost 157 lines). MIN_PEER_PROTO_VERSION
// is deliberately NOT raised: it currently equals this value, and raising
// it would disconnect every node still on v1.0.3 -- which is all users,
// since no release has ever been installable (unsigned archives).
static const int PROTOCOL_VERSION = 2000001;
//! initial proto version, to be increased after version/verack negotiation
static const int INIT_PROTO_VERSION = 209;
//! In this version, 'getheaders' was introduced.

View File

@@ -299,10 +299,7 @@ UniValue importprivkey(const UniValue& params, bool fHelp, const CPubKey& mypk)
bool fRescan = true;
if (params.size() > 2)
fRescan = params[2].get_bool();
// '> 3', not '== 4': with the optional 5th (secret_key) argument present the equality test
// failed and height silently stayed 0, rescanning from genesis. Every sibling RPC in this file
// already uses the '>' form.
if ( fRescan && params.size() > 3 )
if ( fRescan && params.size() == 4 )
height = params[3].get_int();

View File

@@ -1240,16 +1240,6 @@ int CWallet::SaplingWitnessMinimumHeight(const uint256& nullifier, int nWitnessH
return nMinimumHeight;
}
int CWallet::SaplingWitnessMinimumHeight(const boost::optional<uint256>& nullifier, int nWitnessHeight, int nMinimumHeight)
{
// No nullifier => an incoming-viewing-key-only note (z_importviewingkey without the full
// viewing key). Spend depth is unknowable, so treat it as unspent and keep its witness.
if (!nullifier) {
return min(nWitnessHeight, nMinimumHeight);
}
return SaplingWitnessMinimumHeight(*nullifier, nWitnessHeight, nMinimumHeight);
}
int CWallet::VerifyAndSetInitialWitness(const CBlockIndex* pindex, bool witnessOnly)
{
LOCK2(cs_main, cs_wallet);
@@ -1287,7 +1277,7 @@ int CWallet::VerifyAndSetInitialWitness(const CBlockIndex* pindex, bool witnessO
//Skip Validation when witness root has been validated
if (nd->witnessRootValidated) {
nMinimumHeight = SaplingWitnessMinimumHeight(item.second.nullifier, nd->witnessHeight, nMinimumHeight);
nMinimumHeight = SaplingWitnessMinimumHeight(*item.second.nullifier, nd->witnessHeight, nMinimumHeight);
continue;
}
@@ -1299,12 +1289,12 @@ int CWallet::VerifyAndSetInitialWitness(const CBlockIndex* pindex, bool witnessO
CBlockIndex* whIndex = chainActive[nd->witnessHeight];
if (whIndex == NULL) {
//witnessHeight strictly above the active chain (transient catch-up): cannot validate yet
nMinimumHeight = SaplingWitnessMinimumHeight(item.second.nullifier, nd->witnessHeight, nMinimumHeight);
nMinimumHeight = SaplingWitnessMinimumHeight(*item.second.nullifier, nd->witnessHeight, nMinimumHeight);
continue;
}
if (nd->witnesses.front().root() == whIndex->hashFinalSaplingRoot) {
nd->witnessRootValidated = true;
nMinimumHeight = SaplingWitnessMinimumHeight(item.second.nullifier, nd->witnessHeight, nMinimumHeight);
nMinimumHeight = SaplingWitnessMinimumHeight(*item.second.nullifier, nd->witnessHeight, nMinimumHeight);
continue;
}
//root mismatch on the active chain -> desynced; fall through to rebuild below
@@ -1316,7 +1306,7 @@ int CWallet::VerifyAndSetInitialWitness(const CBlockIndex* pindex, bool witnessO
blockRoot = pblockindex->hashFinalSaplingRoot;
if (witnessRoot == blockRoot) {
nd->witnessRootValidated = true;
nMinimumHeight = SaplingWitnessMinimumHeight(item.second.nullifier, nd->witnessHeight, nMinimumHeight);
nMinimumHeight = SaplingWitnessMinimumHeight(*item.second.nullifier, nd->witnessHeight, nMinimumHeight);
continue;
}
}
@@ -1368,7 +1358,7 @@ int CWallet::VerifyAndSetInitialWitness(const CBlockIndex* pindex, bool witnessO
}
nd->witnessHeight = pblockindex->GetHeight();
UpdateSaplingNullifierNoteMapWithTx(wtxItem.second);
nMinimumHeight = SaplingWitnessMinimumHeight(item.second.nullifier, nd->witnessHeight, nMinimumHeight);
nMinimumHeight = SaplingWitnessMinimumHeight(*item.second.nullifier, nd->witnessHeight, nMinimumHeight);
}
}
}

View File

@@ -896,12 +896,6 @@ public:
protected:
int SaplingWitnessMinimumHeight(const uint256& nullifier, int nWitnessHeight, int nMinimumHeight);
//! Overload for a note whose nullifier may be unset. A note discovered through an imported
//! INCOMING viewing key has no nullifier (computing one needs the full viewing key), so
//! dereferencing the optional aborts the daemon. Treats such a note as unspent, which is the
//! conservative direction: it keeps the witness alive rather than pruning a note we cannot
//! prove spent.
int SaplingWitnessMinimumHeight(const boost::optional<uint256>& nullifier, int nWitnessHeight, int nMinimumHeight);
/**
* pindex is the new tip being connected.