IBD/sync speedups: parallel RandomX pre-verify, adaptive dbcache, P2P download fixes
- Parallel RandomX PoW pre-verification pool (CCheckQueue) run ahead of the serial connect; consensus-neutral (inline CheckRandomXSolution fallback still verifies anything not pre-verified). New -randomxverifythreads (default = -par). - Adaptive dbcache: default sizes the UTXO/coins cache to most of RAM and shrinks under memory pressure, always leaving a reserve free; -dbcache pins a fixed value. - P2P block download: bounded socket recv-drain loop (tlsmanager); frontier-block reassignment to break head-of-line stalls (-blockreassigntimeout); ProcessGetData serves a bounded batch of blocks per pass instead of one (fixes the serve-side one-block-per-tick throttle that caps download network-wide). - assumeutxo: dumptxoutset RPC + LoadSnapshot machinery + AssumeutxoData chainparams. - Signed bootstrap verification (util/bootstrap-dragonx.sh, util/sign-bootstrap.md). - gtest: RandomX pre-verify consensus-equivalence test + UTXO-snapshot round-trip; revived the gtest harness (Makefile.am include fix, Makefile.gtest.include). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
15
README.md
15
README.md
@@ -27,6 +27,21 @@ the entire history of Hush transactions; depending on the speed of your
|
|||||||
computer and network connection, it will likely take a few hours at least, but
|
computer and network connection, it will likely take a few hours at least, but
|
||||||
some people report full nodes syncing in less than 1.5 hours.
|
some people report full nodes syncing in less than 1.5 hours.
|
||||||
|
|
||||||
|
# Fastest way to sync (bootstrap)
|
||||||
|
|
||||||
|
The quickest way to get a fully-synced node is the signed bootstrap snapshot, which
|
||||||
|
installs a pre-built blockchain so you skip re-validating the whole chain from genesis:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Stop dragonxd first if it is running, then:
|
||||||
|
./util/bootstrap-dragonx.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The script preserves your `wallet.dat` and `DRAGONX.conf`, verifies the download's
|
||||||
|
checksums and (once a release key is published) its cryptographic signature, then starts
|
||||||
|
you near the chain tip. If you prefer to sync from the network instead, a larger
|
||||||
|
`-dbcache` (e.g. `-dbcache=2048`) noticeably speeds up the initial block download.
|
||||||
|
|
||||||
# Banned by GitHub
|
# Banned by GitHub
|
||||||
|
|
||||||
In working on this release, Duke Leto was suspended from Github, which gave Hush developers
|
In working on this release, Duke Leto was suspended from Github, which gave Hush developers
|
||||||
|
|||||||
@@ -685,5 +685,5 @@ endif
|
|||||||
if ENABLE_TESTS
|
if ENABLE_TESTS
|
||||||
#include Makefile.test-hush.include
|
#include Makefile.test-hush.include
|
||||||
#include Makefile.test.include
|
#include Makefile.test.include
|
||||||
#include Makefile.gtest.include
|
include Makefile.gtest.include
|
||||||
endif
|
endif
|
||||||
|
|||||||
@@ -4,65 +4,59 @@ TESTS += hush-gtest
|
|||||||
bin_PROGRAMS += hush-gtest
|
bin_PROGRAMS += hush-gtest
|
||||||
|
|
||||||
# tool for generating our public parameters
|
# tool for generating our public parameters
|
||||||
|
# NOTE: the original test list used an invalid automake form (comment after a trailing
|
||||||
|
# backslash, and `zcash_gtest_SOURCES +=` with no prior `=`), which is why the whole
|
||||||
|
# gtest harness was disabled via a `#include`. Minimal valid set: the harness + the
|
||||||
|
# UTXO-snapshot round-trip test. Re-add other gtest sources here as they are revived.
|
||||||
hush_gtest_SOURCES = \
|
hush_gtest_SOURCES = \
|
||||||
gtest/main.cpp \
|
gtest/main.cpp \
|
||||||
gtest/utils.cpp \
|
gtest/utils.cpp \
|
||||||
gtest/test_checktransaction.cpp \
|
gtest/test_utxosnapshot.cpp \
|
||||||
gtest/json_test_vectors.cpp \
|
gtest/test_randomx_preverify.cpp
|
||||||
gtest/json_test_vectors.h \
|
|
||||||
gtest/test_wallet_zkeys.cpp \
|
|
||||||
# These tests are order-dependent, because they
|
|
||||||
# depend on global state (see #1539)
|
|
||||||
if ENABLE_WALLET
|
|
||||||
zcash_gtest_SOURCES += \
|
|
||||||
wallet/gtest/test_wallet_zkeys.cpp
|
|
||||||
endif
|
|
||||||
zcash_gtest_SOURCES += \
|
|
||||||
gtest/test_tautology.cpp \
|
|
||||||
gtest/test_deprecation.cpp \
|
|
||||||
gtest/test_equihash.cpp \
|
|
||||||
gtest/test_httprpc.cpp \
|
|
||||||
gtest/test_keys.cpp \
|
|
||||||
gtest/test_keystore.cpp \
|
|
||||||
gtest/test_noteencryption.cpp \
|
|
||||||
gtest/test_mempool.cpp \
|
|
||||||
gtest/test_merkletree.cpp \
|
|
||||||
gtest/test_metrics.cpp \
|
|
||||||
gtest/test_miner.cpp \
|
|
||||||
gtest/test_pow.cpp \
|
|
||||||
gtest/test_random.cpp \
|
|
||||||
gtest/test_rpc.cpp \
|
|
||||||
gtest/test_sapling_note.cpp \
|
|
||||||
gtest/test_transaction.cpp \
|
|
||||||
gtest/test_transaction_builder.cpp \
|
|
||||||
gtest/test_upgrades.cpp \
|
|
||||||
gtest/test_validation.cpp \
|
|
||||||
gtest/test_circuit.cpp \
|
|
||||||
gtest/test_txid.cpp \
|
|
||||||
gtest/test_libzcash_utils.cpp \
|
|
||||||
gtest/test_proofs.cpp \
|
|
||||||
gtest/test_pedersen_hash.cpp \
|
|
||||||
gtest/test_checkblock.cpp \
|
|
||||||
gtest/test_zip32.cpp
|
|
||||||
if ENABLE_WALLET
|
|
||||||
zcash_gtest_SOURCES += \
|
|
||||||
wallet/gtest/test_wallet.cpp
|
|
||||||
endif
|
|
||||||
|
|
||||||
hush_gtest_CPPFLAGS = $(AM_CPPFLAGS) -DMULTICORE -fopenmp -DBINARY_OUTPUT -DCURVE_ALT_BN128 -DSTATIC $(BITCOIN_INCLUDES)
|
hush_gtest_CPPFLAGS = $(AM_CPPFLAGS) -DMULTICORE -fopenmp -DBINARY_OUTPUT -DCURVE_ALT_BN128 -DSTATIC $(BITCOIN_INCLUDES)
|
||||||
hush_gtest_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS)
|
hush_gtest_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS)
|
||||||
|
|
||||||
hush_gtest_LDADD = -lgtest -lgmock $(LIBBITCOIN_SERVER) $(LIBBITCOIN_CLI) $(LIBBITCOIN_COMMON) $(LIBBITCOIN_UTIL) $(LIBBITCOIN_CRYPTO) $(LIBBITCOIN_UNIVALUE) $(LIBLEVELDB) $(LIBMEMENV) \
|
# Mirror dragonxd_LDADD's working library set/order (the old list used a non-existent
|
||||||
$(BOOST_LIBS) $(BOOST_UNIT_TEST_FRAMEWORK_LIB) $(LIBSECP256K1)
|
# $(LIBBITCOIN_UNIVALUE) so univalue was never linked, and omitted LIBHUSH/LIBRANDOMX/libcc).
|
||||||
|
hush_gtest_LDADD = -lgtest -lgmock \
|
||||||
|
$(LIBBITCOIN_SERVER) \
|
||||||
|
$(LIBBITCOIN_COMMON) \
|
||||||
|
$(LIBUNIVALUE) \
|
||||||
|
$(LIBBITCOIN_UTIL) \
|
||||||
|
$(LIBBITCOIN_CRYPTO) \
|
||||||
|
$(LIBZCASH) \
|
||||||
|
$(LIBHUSH) \
|
||||||
|
$(LIBLEVELDB) \
|
||||||
|
$(LIBMEMENV) \
|
||||||
|
$(LIBSECP256K1) \
|
||||||
|
$(LIBRANDOMX)
|
||||||
if ENABLE_WALLET
|
if ENABLE_WALLET
|
||||||
hush_gtest_LDADD += $(LIBBITCOIN_WALLET)
|
hush_gtest_LDADD += $(LIBBITCOIN_WALLET)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
hush_gtest_LDADD += $(LIBZCASH_CONSENSUS) $(BDB_LIBS) $(SSL_LIBS) $(CRYPTO_LIBS) $(EVENT_PTHREADS_LIBS) $(EVENT_LIBS) $(LIBZCASH) $(LIBZCASH_LIBS)
|
hush_gtest_LDADD += \
|
||||||
|
$(BOOST_LIBS) \
|
||||||
|
$(BOOST_UNIT_TEST_FRAMEWORK_LIB) \
|
||||||
|
$(BDB_LIBS) \
|
||||||
|
$(SSL_LIBS) \
|
||||||
|
$(CRYPTO_LIBS) \
|
||||||
|
$(EVENT_PTHREADS_LIBS) \
|
||||||
|
$(EVENT_LIBS) \
|
||||||
|
$(LIBBITCOIN_CRYPTO) \
|
||||||
|
$(LIBZCASH_LIBS)
|
||||||
|
|
||||||
hush_gtest_LDFLAGS = $(RELDFLAGS) $(AM_LDFLAGS) $(LIBTOOL_APP_LDFLAGS) -static
|
if TARGET_DARWIN
|
||||||
|
hush_gtest_LDADD += libcc.dylib $(LIBSECP256K1)
|
||||||
|
endif
|
||||||
|
if TARGET_WINDOWS
|
||||||
|
hush_gtest_LDADD += libcc.dll $(LIBSECP256K1)
|
||||||
|
endif
|
||||||
|
if TARGET_LINUX
|
||||||
|
hush_gtest_LDADD += libcc.so $(LIBSECP256K1)
|
||||||
|
endif
|
||||||
|
|
||||||
hush_gtest_LDFLAGS = $(RELDFLAGS) $(AM_LDFLAGS) $(LIBTOOL_APP_LDFLAGS) -static
|
hush_gtest_LDFLAGS = $(RELDFLAGS) $(AM_LDFLAGS) $(LIBTOOL_APP_LDFLAGS)
|
||||||
|
|
||||||
hush-gtest-expected-failures: hush-gtest FORCE
|
hush-gtest-expected-failures: hush-gtest FORCE
|
||||||
./hush-gtest --gtest_filter=*DISABLED_* --gtest_also_run_disabled_tests
|
./hush-gtest --gtest_filter=*DISABLED_* --gtest_also_run_disabled_tests
|
||||||
|
|||||||
12
src/chain.h
12
src/chain.h
@@ -399,7 +399,16 @@ public:
|
|||||||
|
|
||||||
//! (memory only) Sequential id assigned to distinguish order in which blocks are received.
|
//! (memory only) Sequential id assigned to distinguish order in which blocks are received.
|
||||||
uint32_t nSequenceId;
|
uint32_t nSequenceId;
|
||||||
|
|
||||||
|
//! (memory only) Set true once this block's RandomX PoW has been verified by the parallel
|
||||||
|
//! pre-verification pool, letting the inline check in CheckBlockHeader skip the recompute.
|
||||||
|
//! Written by exactly one pre-verify worker (1:1 with the block) and read by the connect
|
||||||
|
//! thread only AFTER the pool barrier (CCheckQueue::Wait provides the happens-before), so a
|
||||||
|
//! plain bool is race-free here. NOT serialized — a pure optimization hint; the inline
|
||||||
|
//! CheckRandomXSolution remains the consensus authority. (Plain bool, not std::atomic, so
|
||||||
|
//! CBlockIndex stays copyable for CDiskBlockIndex's `CBlockIndex(*pindex)` construction.)
|
||||||
|
bool fRandomXVerified;
|
||||||
|
|
||||||
void SetNull()
|
void SetNull()
|
||||||
{
|
{
|
||||||
phashBlock = NULL;
|
phashBlock = NULL;
|
||||||
@@ -414,6 +423,7 @@ public:
|
|||||||
chainPower = CChainPower();
|
chainPower = CChainPower();
|
||||||
nTx = 0;
|
nTx = 0;
|
||||||
nChainTx = 0;
|
nChainTx = 0;
|
||||||
|
fRandomXVerified = false;
|
||||||
|
|
||||||
// Shieldex Index chain stats
|
// Shieldex Index chain stats
|
||||||
nChainPayments = 0;
|
nChainPayments = 0;
|
||||||
|
|||||||
@@ -69,6 +69,17 @@ public:
|
|||||||
double fTransactionsPerDay;
|
double fTransactionsPerDay;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Trusted UTXO-snapshot (assumeutxo-style) anchor. When `hash` is set, a node loading a
|
||||||
|
* snapshot via -loadutxosnapshot must produce exactly this content hash at this height,
|
||||||
|
* otherwise the snapshot is refused. Null hash = not configured (loading requires the
|
||||||
|
* explicit -loadutxosnapshotunsafe override, e.g. for regtest/testing). Mirrors the
|
||||||
|
* hardcoded-checkpoint trust model. */
|
||||||
|
struct AssumeutxoData {
|
||||||
|
int height;
|
||||||
|
uint256 hash;
|
||||||
|
bool IsNull() const { return hash.IsNull(); }
|
||||||
|
};
|
||||||
|
|
||||||
enum Bech32Type {
|
enum Bech32Type {
|
||||||
SAPLING_PAYMENT_ADDRESS,
|
SAPLING_PAYMENT_ADDRESS,
|
||||||
SAPLING_FULL_VIEWING_KEY,
|
SAPLING_FULL_VIEWING_KEY,
|
||||||
@@ -105,6 +116,7 @@ public:
|
|||||||
const std::string& Bech32HRP(Bech32Type type) const { return bech32HRPs[type]; }
|
const std::string& Bech32HRP(Bech32Type type) const { return bech32HRPs[type]; }
|
||||||
const std::vector<uint8_t>& FixedSeeds() const { return vFixedSeeds; }
|
const std::vector<uint8_t>& FixedSeeds() const { return vFixedSeeds; }
|
||||||
const CCheckpointData& Checkpoints() const { return checkpointData; }
|
const CCheckpointData& Checkpoints() const { return checkpointData; }
|
||||||
|
const AssumeutxoData& Assumeutxo() const { return assumeutxoData; }
|
||||||
/** Return the founder's reward address and script for a given block height */
|
/** Return the founder's reward address and script for a given block height */
|
||||||
std::string GetFoundersRewardAddressAtHeight(int height) const;
|
std::string GetFoundersRewardAddressAtHeight(int height) const;
|
||||||
CScript GetFoundersRewardScriptAtHeight(int height) const;
|
CScript GetFoundersRewardScriptAtHeight(int height) const;
|
||||||
@@ -144,6 +156,7 @@ protected:
|
|||||||
bool fMineBlocksOnDemand = false;
|
bool fMineBlocksOnDemand = false;
|
||||||
bool fTestnetToBeDeprecatedFieldRPC = false;
|
bool fTestnetToBeDeprecatedFieldRPC = false;
|
||||||
CCheckpointData checkpointData;
|
CCheckpointData checkpointData;
|
||||||
|
AssumeutxoData assumeutxoData; // null by default; set per-network in chainparams.cpp once a snapshot hash is published
|
||||||
std::vector<std::string> vFoundersRewardAddress;
|
std::vector<std::string> vFoundersRewardAddress;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
172
src/gtest/test_randomx_preverify.cpp
Normal file
172
src/gtest/test_randomx_preverify.cpp
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
// Copyright (c) 2024-2026 The DragonX developers
|
||||||
|
// Distributed under the GPLv3 software license, see the accompanying
|
||||||
|
// file COPYING or https://www.gnu.org/licenses/gpl-3.0.en.html
|
||||||
|
//
|
||||||
|
// Consensus-equivalence test for the parallel RandomX pre-verification pool. The pool is purely an
|
||||||
|
// optimization: a block's transient fRandomXVerified flag (set by CRandomXCheck on a real hash
|
||||||
|
// match) only lets CheckBlockHeader SKIP the inline recompute. So for every block the pool's
|
||||||
|
// outcome must equal the inline CheckRandomXSolution outcome — `(preVerified || inline) == inline`.
|
||||||
|
// We exercise a valid solution, a corrupted solution, and confirm the pool never "succeeds" on a
|
||||||
|
// block the inline check would reject.
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "arith_uint256.h"
|
||||||
|
#include "chain.h"
|
||||||
|
#include "chainparams.h"
|
||||||
|
#include "pow.h"
|
||||||
|
#include "primitives/block.h"
|
||||||
|
#include "RandomX/src/randomx.h"
|
||||||
|
#include "hush_defs.h"
|
||||||
|
#include "util.h"
|
||||||
|
#include <boost/thread.hpp>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
extern int32_t HUSH_LOADINGBLOCKS;
|
||||||
|
extern bool fCheckpointsEnabled;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// Compute the correct RandomX solution for a header using a standalone reference light VM, via the
|
||||||
|
// SAME key + input helpers the validator uses (so the bytes/key match exactly).
|
||||||
|
void ReferenceRandomXHash(const CBlockHeader& hdr, const std::string& key, unsigned char out[RANDOMX_HASH_SIZE])
|
||||||
|
{
|
||||||
|
std::vector<unsigned char> in = GetRandomXInput(hdr);
|
||||||
|
randomx_flags flags = randomx_get_flags();
|
||||||
|
randomx_cache* c = randomx_alloc_cache(flags);
|
||||||
|
ASSERT_NE(c, nullptr);
|
||||||
|
randomx_init_cache(c, key.data(), key.size());
|
||||||
|
randomx_vm* vm = randomx_create_vm(flags, c, nullptr);
|
||||||
|
ASSERT_NE(vm, nullptr);
|
||||||
|
randomx_calculate_hash(vm, in.data(), in.size(), out);
|
||||||
|
randomx_destroy_vm(vm);
|
||||||
|
randomx_release_cache(c);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST(RandomXPreVerify, ConsensusEquivalence)
|
||||||
|
{
|
||||||
|
// Force RandomX validation to actually run at low heights in the test harness.
|
||||||
|
uint32_t savedAlgo = ASSETCHAINS_ALGO, savedRx = ASSETCHAINS_RANDOMX;
|
||||||
|
int32_t savedVal = ASSETCHAINS_RANDOMX_VALIDATION, savedLoad = HUSH_LOADINGBLOCKS;
|
||||||
|
bool savedCkpt = fCheckpointsEnabled;
|
||||||
|
ASSETCHAINS_RANDOMX = 2; // a distinct nonzero algo id
|
||||||
|
ASSETCHAINS_ALGO = ASSETCHAINS_RANDOMX;
|
||||||
|
ASSETCHAINS_RANDOMX_VALIDATION = 1; // enforce from height 1
|
||||||
|
HUSH_LOADINGBLOCKS = 0; // not in initial-load (else RandomX skipped)
|
||||||
|
fCheckpointsEnabled = false; // avoid the below-checkpoint skip
|
||||||
|
|
||||||
|
const int32_t height = 10; // < interval+lag -> the chain-params initial key (no chainActive needed)
|
||||||
|
|
||||||
|
CBlockHeader hdr;
|
||||||
|
hdr.nVersion = 4;
|
||||||
|
hdr.hashPrevBlock = uint256S("0x0000000000000000000000000000000000000000000000000000000000000001");
|
||||||
|
hdr.hashMerkleRoot = uint256S("0x0000000000000000000000000000000000000000000000000000000000000002");
|
||||||
|
hdr.hashFinalSaplingRoot = uint256S("0x0000000000000000000000000000000000000000000000000000000000000003");
|
||||||
|
hdr.nTime = 1700000000;
|
||||||
|
hdr.nBits = 0x200f0f0f;
|
||||||
|
hdr.nNonce = uint256S("0x0000000000000000000000000000000000000000000000000000000000000004");
|
||||||
|
|
||||||
|
std::string key = GetRandomXKey(height);
|
||||||
|
ASSERT_FALSE(key.empty());
|
||||||
|
|
||||||
|
unsigned char good[RANDOMX_HASH_SIZE];
|
||||||
|
ReferenceRandomXHash(hdr, key, good);
|
||||||
|
|
||||||
|
// Run the pool path synchronously on this thread (CRandomXCheck creates its own thread_local VM).
|
||||||
|
auto poolVerifies = [&](const CBlockHeader& h) -> bool {
|
||||||
|
RandomXValidatorPrepareKey(key); // load the shared cache with this key
|
||||||
|
bool slot = false;
|
||||||
|
CRandomXCheck chk(key, GetRandomXInput(h), h.nSolution.data(), &slot);
|
||||||
|
chk();
|
||||||
|
return slot;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Case 1 — valid solution: both inline and pool accept; equivalence holds.
|
||||||
|
hdr.nSolution.assign(good, good + RANDOMX_HASH_SIZE);
|
||||||
|
EXPECT_TRUE(CheckRandomXSolution(&hdr, height));
|
||||||
|
EXPECT_TRUE(poolVerifies(hdr));
|
||||||
|
EXPECT_EQ(poolVerifies(hdr) || CheckRandomXSolution(&hdr, height), CheckRandomXSolution(&hdr, height));
|
||||||
|
|
||||||
|
// Case 2 — corrupted solution: both reject; the pool must NOT set verified.
|
||||||
|
{
|
||||||
|
CBlockHeader bad = hdr;
|
||||||
|
bad.nSolution[0] ^= 0xff;
|
||||||
|
EXPECT_FALSE(CheckRandomXSolution(&bad, height));
|
||||||
|
EXPECT_FALSE(poolVerifies(bad));
|
||||||
|
EXPECT_EQ(poolVerifies(bad) || CheckRandomXSolution(&bad, height), CheckRandomXSolution(&bad, height));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 3 — a verified flag on the block lets CheckBlockHeader skip, but verified is only ever set
|
||||||
|
// by a real hash match, so it can never mask an invalid block. (Pool returns false for the bad
|
||||||
|
// block above, so its fRandomXVerified stays false and the inline path rejects it at connect.)
|
||||||
|
|
||||||
|
ASSETCHAINS_ALGO = savedAlgo; ASSETCHAINS_RANDOMX = savedRx;
|
||||||
|
ASSETCHAINS_RANDOMX_VALIDATION = savedVal; HUSH_LOADINGBLOCKS = savedLoad;
|
||||||
|
fCheckpointsEnabled = savedCkpt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A/B: serial inline verification (single VM) vs the parallel pool (worker threads). Directly
|
||||||
|
// measures the speedup the pool delivers. We don't care about validity here (mismatched solutions
|
||||||
|
// still cost a full hash), only wall-clock. parallel must beat serial whenever >1 core is used.
|
||||||
|
TEST(RandomXPreVerify, ParallelSpeedup)
|
||||||
|
{
|
||||||
|
uint32_t savedAlgo = ASSETCHAINS_ALGO, savedRx = ASSETCHAINS_RANDOMX;
|
||||||
|
int32_t savedVal = ASSETCHAINS_RANDOMX_VALIDATION, savedLoad = HUSH_LOADINGBLOCKS;
|
||||||
|
bool savedCkpt = fCheckpointsEnabled;
|
||||||
|
ASSETCHAINS_RANDOMX = 2; ASSETCHAINS_ALGO = ASSETCHAINS_RANDOMX;
|
||||||
|
ASSETCHAINS_RANDOMX_VALIDATION = 1; HUSH_LOADINGBLOCKS = 0; fCheckpointsEnabled = false;
|
||||||
|
|
||||||
|
const int32_t height = 10;
|
||||||
|
std::string key = GetRandomXKey(height);
|
||||||
|
ASSERT_FALSE(key.empty());
|
||||||
|
ASSERT_TRUE(RandomXValidatorPrepareKey(key));
|
||||||
|
|
||||||
|
const int M = 16; // blocks to verify in the window
|
||||||
|
std::vector<CBlockHeader> hdrs(M);
|
||||||
|
for (int i = 0; i < M; i++) {
|
||||||
|
hdrs[i].nVersion = 4;
|
||||||
|
hdrs[i].nTime = 1700000000 + i;
|
||||||
|
hdrs[i].nBits = 0x200f0f0f;
|
||||||
|
hdrs[i].nNonce = ArithToUint256(arith_uint256(i + 1)); // distinct inputs
|
||||||
|
hdrs[i].nSolution.assign(RANDOMX_HASH_SIZE, 0); // arbitrary; we time the hash
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serial baseline: inline single-VM verification (each call hashes, then mismatches -> false).
|
||||||
|
int64_t t0 = GetTimeMicros();
|
||||||
|
for (int i = 0; i < M; i++) CheckRandomXSolution(&hdrs[i], height);
|
||||||
|
int64_t serialUs = GetTimeMicros() - t0;
|
||||||
|
|
||||||
|
// Parallel: spawn K-1 workers + the master (this thread) joining via Wait().
|
||||||
|
int K = std::min(8, std::max(2, (int)boost::thread::hardware_concurrency()));
|
||||||
|
boost::thread_group workers;
|
||||||
|
for (int i = 0; i < K - 1; i++) workers.create_thread(&ThreadRandomXVerify);
|
||||||
|
|
||||||
|
std::unique_ptr<bool[]> slots(new bool[M]());
|
||||||
|
std::vector<CRandomXCheck> checks;
|
||||||
|
checks.reserve(M);
|
||||||
|
for (int i = 0; i < M; i++)
|
||||||
|
checks.push_back(CRandomXCheck(key, GetRandomXInput(hdrs[i]), hdrs[i].nSolution.data(), &slots[i]));
|
||||||
|
|
||||||
|
int64_t t1 = GetTimeMicros();
|
||||||
|
{
|
||||||
|
CCheckQueueControl<CRandomXCheck> control(&rxCheckQueue);
|
||||||
|
control.Add(checks);
|
||||||
|
control.Wait();
|
||||||
|
}
|
||||||
|
int64_t parallelUs = GetTimeMicros() - t1;
|
||||||
|
|
||||||
|
workers.interrupt_all();
|
||||||
|
workers.join_all();
|
||||||
|
|
||||||
|
printf("[ RandomX A/B ] %d blocks: serial(1 VM)=%ldms, parallel(%d threads)=%ldms, speedup=%.1fx\n",
|
||||||
|
M, (long)(serialUs / 1000), K, (long)(parallelUs / 1000),
|
||||||
|
(double)serialUs / (double)std::max<int64_t>(1, parallelUs));
|
||||||
|
|
||||||
|
EXPECT_LT(parallelUs, serialUs); // parallel must be faster than serial on a multi-core box
|
||||||
|
|
||||||
|
ASSETCHAINS_ALGO = savedAlgo; ASSETCHAINS_RANDOMX = savedRx;
|
||||||
|
ASSETCHAINS_RANDOMX_VALIDATION = savedVal; HUSH_LOADINGBLOCKS = savedLoad;
|
||||||
|
fCheckpointsEnabled = savedCkpt;
|
||||||
|
}
|
||||||
203
src/gtest/test_utxosnapshot.cpp
Normal file
203
src/gtest/test_utxosnapshot.cpp
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
// Copyright (c) 2024-2026 The DragonX developers
|
||||||
|
// Distributed under the GPLv3 software license, see the accompanying
|
||||||
|
// file COPYING or https://www.gnu.org/licenses/gpl-3.0.en.html
|
||||||
|
//
|
||||||
|
// Round-trip tests for the trusted UTXO snapshot (assumeutxo-style) dump/load core
|
||||||
|
// (CCoinsViewDB::DumpSnapshot / LoadSnapshot). This exercises the highest-risk part of
|
||||||
|
// the feature in isolation: that coins, Sapling commitment trees, the nullifier set, the
|
||||||
|
// best block and the best Sapling anchor survive a serialize -> hash -> deserialize cycle
|
||||||
|
// exactly, and that integrity/trust verification rejects tampered or wrong-hash snapshots.
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <boost/filesystem.hpp>
|
||||||
|
|
||||||
|
#include "chainparams.h"
|
||||||
|
#include "coins.h"
|
||||||
|
#include "txdb.h"
|
||||||
|
#include "script/script.h"
|
||||||
|
#include "uint256.h"
|
||||||
|
#include "zcash/IncrementalMerkleTree.hpp"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Populate an in-memory chainstate DB directly via BatchWrite (mirrors how blocks persist
|
||||||
|
// coins/anchors/nullifiers), so DumpSnapshot has a realistic mixed state to serialize.
|
||||||
|
void PopulateChainstate(CCoinsViewDB &db, const uint256 &bestBlock,
|
||||||
|
uint256 &anchorRootOut, const uint256 &nullifierIn)
|
||||||
|
{
|
||||||
|
// One unspent transparent output.
|
||||||
|
CCoinsMap mapCoins;
|
||||||
|
{
|
||||||
|
uint256 txid = uint256S("0xaa00000000000000000000000000000000000000000000000000000000000001");
|
||||||
|
CCoinsCacheEntry &e = mapCoins[txid];
|
||||||
|
e.coins.fCoinBase = false;
|
||||||
|
e.coins.nVersion = 1;
|
||||||
|
e.coins.nHeight = 100;
|
||||||
|
e.coins.vout.resize(1);
|
||||||
|
e.coins.vout[0].nValue = 12345;
|
||||||
|
e.coins.vout[0].scriptPubKey = CScript() << OP_TRUE;
|
||||||
|
e.flags = CCoinsCacheEntry::DIRTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// One Sapling commitment tree (anchor), keyed by its root.
|
||||||
|
SaplingMerkleTree tree;
|
||||||
|
tree.append(uint256S("0xbb00000000000000000000000000000000000000000000000000000000000002"));
|
||||||
|
anchorRootOut = tree.root();
|
||||||
|
CAnchorsSaplingMap mapSaplingAnchors;
|
||||||
|
{
|
||||||
|
CAnchorsSaplingCacheEntry &e = mapSaplingAnchors[anchorRootOut];
|
||||||
|
e.entered = true;
|
||||||
|
e.tree = tree;
|
||||||
|
e.flags = CAnchorsSaplingCacheEntry::DIRTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// One spent Sapling nullifier.
|
||||||
|
CNullifiersMap mapSaplingNullifiers;
|
||||||
|
{
|
||||||
|
CNullifiersCacheEntry &e = mapSaplingNullifiers[nullifierIn];
|
||||||
|
e.entered = true;
|
||||||
|
e.flags = CNullifiersCacheEntry::DIRTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
CAnchorsSproutMap mapSproutAnchors; // empty
|
||||||
|
CNullifiersMap mapSproutNullifiers; // empty
|
||||||
|
ASSERT_TRUE(db.BatchWrite(mapCoins, bestBlock, uint256(), anchorRootOut,
|
||||||
|
mapSproutAnchors, mapSaplingAnchors, mapSproutNullifiers, mapSaplingNullifiers));
|
||||||
|
}
|
||||||
|
|
||||||
|
CUTXOSnapshotHeader MakeHeader(const uint256 &bestBlock, const uint256 &bestAnchor)
|
||||||
|
{
|
||||||
|
CUTXOSnapshotHeader h;
|
||||||
|
h.nMagic = UTXO_SNAPSHOT_MAGIC;
|
||||||
|
h.nVersion = UTXO_SNAPSHOT_VERSION;
|
||||||
|
memcpy(&h.nNetworkMagic, Params().MessageStart(), 4);
|
||||||
|
h.baseBlockHash = bestBlock;
|
||||||
|
h.nHeight = 100;
|
||||||
|
h.nChainTx = 1;
|
||||||
|
h.fHasChainSaplingValue = 1;
|
||||||
|
h.nChainSaplingValue = 999;
|
||||||
|
h.bestSaplingAnchor = bestAnchor;
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST(UTXOSnapshot, RoundTripPreservesChainstate)
|
||||||
|
{
|
||||||
|
SelectParams(CBaseChainParams::REGTEST);
|
||||||
|
|
||||||
|
const uint256 bestBlock = uint256S("0xff00000000000000000000000000000000000000000000000000000000000009");
|
||||||
|
const uint256 nullifier = uint256S("0xcc00000000000000000000000000000000000000000000000000000000000003");
|
||||||
|
|
||||||
|
CCoinsViewDB src(1 << 20, true); // in-memory
|
||||||
|
uint256 anchorRoot;
|
||||||
|
PopulateChainstate(src, bestBlock, anchorRoot, nullifier);
|
||||||
|
|
||||||
|
boost::filesystem::path path = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path();
|
||||||
|
|
||||||
|
CUTXOSnapshotHeader header = MakeHeader(bestBlock, anchorRoot);
|
||||||
|
uint256 dumpHash; std::string err;
|
||||||
|
ASSERT_TRUE(src.DumpSnapshot(path.string(), header, dumpHash, err)) << err;
|
||||||
|
EXPECT_EQ(header.nCoins, 1u);
|
||||||
|
EXPECT_EQ(header.nSaplingAnchors, 1u);
|
||||||
|
EXPECT_EQ(header.nSaplingNullifiers, 1u);
|
||||||
|
|
||||||
|
// Load into a fresh in-memory DB (integrity check only, no trust hash).
|
||||||
|
CCoinsViewDB dst(1 << 20, true);
|
||||||
|
CUTXOSnapshotHeader loadedHeader; uint256 loadHash;
|
||||||
|
ASSERT_TRUE(dst.LoadSnapshot(path.string(), uint256(), /*fRequireExpected=*/false, loadedHeader, loadHash, err)) << err;
|
||||||
|
|
||||||
|
// Hash is deterministic across dump and load.
|
||||||
|
EXPECT_EQ(dumpHash, loadHash);
|
||||||
|
EXPECT_EQ(loadedHeader.nHeight, 100);
|
||||||
|
EXPECT_EQ(loadedHeader.baseBlockHash, bestBlock);
|
||||||
|
|
||||||
|
// Best block round-trips.
|
||||||
|
EXPECT_EQ(dst.GetBestBlock(), bestBlock);
|
||||||
|
|
||||||
|
// Coins round-trip: the stored UTXO must come back intact. (We check the specific coin
|
||||||
|
// directly rather than via GetStats(), which dereferences mapBlockIndex for the best block
|
||||||
|
// — not populated in this pure unit test.) The full-content equivalence is already proven
|
||||||
|
// by dumpHash == loadHash above.
|
||||||
|
const uint256 txid = uint256S("0xaa00000000000000000000000000000000000000000000000000000000000001");
|
||||||
|
CCoins c1, c2;
|
||||||
|
ASSERT_TRUE(src.GetCoins(txid, c1));
|
||||||
|
ASSERT_TRUE(dst.GetCoins(txid, c2));
|
||||||
|
ASSERT_EQ(c2.vout.size(), 1u);
|
||||||
|
EXPECT_EQ(c2.vout[0].nValue, c1.vout[0].nValue);
|
||||||
|
EXPECT_TRUE(c2.vout[0].scriptPubKey == c1.vout[0].scriptPubKey);
|
||||||
|
|
||||||
|
// Sapling anchor (commitment tree) round-trips byte-exactly: the recovered tree's root
|
||||||
|
// must equal the key it was stored under (this is the invariant ConnectBlock relies on).
|
||||||
|
SaplingMerkleTree recovered;
|
||||||
|
ASSERT_TRUE(dst.GetSaplingAnchorAt(anchorRoot, recovered));
|
||||||
|
EXPECT_EQ(recovered.root(), anchorRoot);
|
||||||
|
EXPECT_EQ(dst.GetBestAnchor(SAPLING), anchorRoot);
|
||||||
|
|
||||||
|
// Nullifier set round-trips.
|
||||||
|
EXPECT_TRUE(dst.GetNullifier(nullifier, SAPLING));
|
||||||
|
EXPECT_FALSE(dst.GetNullifier(uint256S("0xdead"), SAPLING));
|
||||||
|
|
||||||
|
boost::filesystem::remove(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UTXOSnapshot, RejectsTrustHashMismatch)
|
||||||
|
{
|
||||||
|
SelectParams(CBaseChainParams::REGTEST);
|
||||||
|
const uint256 bestBlock = uint256S("0xff0000000000000000000000000000000000000000000000000000000000000a");
|
||||||
|
const uint256 nullifier = uint256S("0xcc0000000000000000000000000000000000000000000000000000000000000b");
|
||||||
|
|
||||||
|
CCoinsViewDB src(1 << 20, true);
|
||||||
|
uint256 anchorRoot;
|
||||||
|
PopulateChainstate(src, bestBlock, anchorRoot, nullifier);
|
||||||
|
|
||||||
|
boost::filesystem::path path = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path();
|
||||||
|
CUTXOSnapshotHeader header = MakeHeader(bestBlock, anchorRoot);
|
||||||
|
uint256 dumpHash; std::string err;
|
||||||
|
ASSERT_TRUE(src.DumpSnapshot(path.string(), header, dumpHash, err)) << err;
|
||||||
|
|
||||||
|
// A wrong "trusted" hash must be refused.
|
||||||
|
CCoinsViewDB dst(1 << 20, true);
|
||||||
|
CUTXOSnapshotHeader h2; uint256 hh;
|
||||||
|
uint256 wrong = uint256S("0x1234");
|
||||||
|
EXPECT_FALSE(dst.LoadSnapshot(path.string(), wrong, /*fRequireExpected=*/true, h2, hh, err));
|
||||||
|
// The correct hash must pass.
|
||||||
|
EXPECT_TRUE(dst.LoadSnapshot(path.string(), dumpHash, /*fRequireExpected=*/true, h2, hh, err)) << err;
|
||||||
|
|
||||||
|
boost::filesystem::remove(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UTXOSnapshot, RejectsCorruptedFile)
|
||||||
|
{
|
||||||
|
SelectParams(CBaseChainParams::REGTEST);
|
||||||
|
const uint256 bestBlock = uint256S("0xff0000000000000000000000000000000000000000000000000000000000000c");
|
||||||
|
const uint256 nullifier = uint256S("0xcc0000000000000000000000000000000000000000000000000000000000000d");
|
||||||
|
|
||||||
|
CCoinsViewDB src(1 << 20, true);
|
||||||
|
uint256 anchorRoot;
|
||||||
|
PopulateChainstate(src, bestBlock, anchorRoot, nullifier);
|
||||||
|
|
||||||
|
boost::filesystem::path path = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path();
|
||||||
|
CUTXOSnapshotHeader header = MakeHeader(bestBlock, anchorRoot);
|
||||||
|
uint256 dumpHash; std::string err;
|
||||||
|
ASSERT_TRUE(src.DumpSnapshot(path.string(), header, dumpHash, err)) << err;
|
||||||
|
|
||||||
|
// Flip a byte near the end (inside the coins/anchor payload, before the trailing hash).
|
||||||
|
{
|
||||||
|
boost::filesystem::fstream f(path, std::ios::in | std::ios::out | std::ios::binary);
|
||||||
|
f.seekg(0, std::ios::end);
|
||||||
|
std::streamoff sz = f.tellg();
|
||||||
|
ASSERT_GT(sz, 40);
|
||||||
|
f.seekg(sz - 40);
|
||||||
|
char c; f.read(&c, 1);
|
||||||
|
f.seekp(sz - 40);
|
||||||
|
c = (char)(c ^ 0xff);
|
||||||
|
f.write(&c, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
CCoinsViewDB dst(1 << 20, true);
|
||||||
|
CUTXOSnapshotHeader h2; uint256 hh;
|
||||||
|
EXPECT_FALSE(dst.LoadSnapshot(path.string(), uint256(), /*fRequireExpected=*/false, h2, hh, err));
|
||||||
|
|
||||||
|
boost::filesystem::remove(path);
|
||||||
|
}
|
||||||
@@ -580,70 +580,98 @@ int TLSManager::threadSocketHandler(CNode* pnode, fd_set& fdsetRecv, fd_set& fds
|
|||||||
char pchBuf[0x10000];
|
char pchBuf[0x10000];
|
||||||
bool bIsSSL = false;
|
bool bIsSSL = false;
|
||||||
int nBytes = 0, nRet = 0;
|
int nBytes = 0, nRet = 0;
|
||||||
|
// Drain the socket in a bounded loop rather than one read per select pass: a single
|
||||||
|
// 64K read per pass underfills high-bandwidth/high-latency links. Cap the reads per
|
||||||
|
// pass and honor the receive-flood back-pressure so one peer can neither exhaust
|
||||||
|
// memory nor starve other peers within this pass.
|
||||||
|
int nDrainReads = 0;
|
||||||
|
const int MAX_DRAIN_READS = 16; // up to ~1 MiB per peer per pass (fairness across peers)
|
||||||
|
// Pre-read back-pressure: gate on the flood ceiling BEFORE each read so the per-peer
|
||||||
|
// recv buffer high-water stays at ReceiveFloodSize()+one read (matching the select()
|
||||||
|
// FD_SET gate), and track bytes locally to avoid the O(n) GetTotalRecvSize() per pass.
|
||||||
|
const int64_t nRecvBase = (int64_t)pnode->GetTotalRecvSize();
|
||||||
|
int64_t nPassBytes = 0;
|
||||||
|
bool fKeepReading = true;
|
||||||
|
while (fKeepReading) {
|
||||||
|
if (nRecvBase + nPassBytes > (int64_t)ReceiveFloodSize())
|
||||||
|
break;
|
||||||
|
{
|
||||||
|
LOCK(pnode->cs_hSocket);
|
||||||
|
|
||||||
{
|
if (pnode->hSocket == INVALID_SOCKET) {
|
||||||
LOCK(pnode->cs_hSocket);
|
LogPrint("tls", "Receive: connection with %s is already closed\n", pnode->addr.ToString());
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
if (pnode->hSocket == INVALID_SOCKET) {
|
bIsSSL = (pnode->ssl != NULL);
|
||||||
LogPrint("tls", "Receive: connection with %s is already closed\n", pnode->addr.ToString());
|
|
||||||
return -1;
|
if (bIsSSL) {
|
||||||
|
wolfSSL_ERR_clear_error(); // clear the error queue, otherwise we may be reading an old error that occurred previously in the current thread
|
||||||
|
nBytes = wolfSSL_read(pnode->ssl, pchBuf, sizeof(pchBuf));
|
||||||
|
nRet = wolfSSL_get_error(pnode->ssl, nBytes);
|
||||||
|
} else {
|
||||||
|
nBytes = recv(pnode->hSocket, pchBuf, sizeof(pchBuf), MSG_DONTWAIT);
|
||||||
|
nRet = WSAGetLastError();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bIsSSL = (pnode->ssl != NULL);
|
if (nBytes > 0) {
|
||||||
|
if (!pnode->ReceiveMsgBytes(pchBuf, nBytes)) {
|
||||||
if (bIsSSL) {
|
|
||||||
wolfSSL_ERR_clear_error(); // clear the error queue, otherwise we may be reading an old error that occurred previously in the current thread
|
|
||||||
nBytes = wolfSSL_read(pnode->ssl, pchBuf, sizeof(pchBuf));
|
|
||||||
nRet = wolfSSL_get_error(pnode->ssl, nBytes);
|
|
||||||
} else {
|
|
||||||
nBytes = recv(pnode->hSocket, pchBuf, sizeof(pchBuf), MSG_DONTWAIT);
|
|
||||||
nRet = WSAGetLastError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nBytes > 0) {
|
|
||||||
if (!pnode->ReceiveMsgBytes(pchBuf, nBytes))
|
|
||||||
pnode->CloseSocketDisconnect();
|
|
||||||
pnode->nLastRecv = GetTime();
|
|
||||||
pnode->nRecvBytes += nBytes;
|
|
||||||
pnode->RecordBytesRecv(nBytes);
|
|
||||||
} else if (nBytes == 0) {
|
|
||||||
|
|
||||||
if (bIsSSL) {
|
|
||||||
unsigned long error = ERR_get_error();
|
|
||||||
const char* error_str = ERR_error_string(error, NULL);
|
|
||||||
LogPrint("tls", "TLS: WARNING: %s: %s():%d - SSL_read err: %s\n",
|
|
||||||
__FILE__, __func__, __LINE__, error_str);
|
|
||||||
}
|
|
||||||
// socket closed gracefully (peer disconnected)
|
|
||||||
if (!pnode->fDisconnect)
|
|
||||||
LogPrint("tls", "socket closed (%s)\n", pnode->addr.ToString());
|
|
||||||
pnode->CloseSocketDisconnect();
|
|
||||||
|
|
||||||
} else if (nBytes < 0) {
|
|
||||||
// error
|
|
||||||
if (bIsSSL) {
|
|
||||||
if (nRet != WOLFSSL_ERROR_WANT_READ && nRet != WOLFSSL_ERROR_WANT_WRITE)
|
|
||||||
{
|
|
||||||
if (!pnode->fDisconnect)
|
|
||||||
LogPrintf("TLS: ERROR: SSL_read %s\n", ERR_error_string(nRet, NULL));
|
|
||||||
pnode->CloseSocketDisconnect();
|
pnode->CloseSocketDisconnect();
|
||||||
|
fKeepReading = false;
|
||||||
|
}
|
||||||
|
pnode->nLastRecv = GetTime();
|
||||||
|
pnode->nRecvBytes += nBytes;
|
||||||
|
pnode->RecordBytesRecv(nBytes);
|
||||||
|
nPassBytes += nBytes;
|
||||||
|
// Keep draining only while the socket likely has more data (we filled the
|
||||||
|
// buffer, or TLS has buffered decrypted bytes) and within the per-pass cap.
|
||||||
|
// The flood ceiling is enforced pre-read at the top of the loop.
|
||||||
|
if (fKeepReading) {
|
||||||
|
bool fMore = (nBytes == (int)sizeof(pchBuf)) || (bIsSSL && wolfSSL_pending(pnode->ssl) > 0);
|
||||||
|
if (!fMore || ++nDrainReads >= MAX_DRAIN_READS)
|
||||||
|
fKeepReading = false;
|
||||||
|
}
|
||||||
|
} else if (nBytes == 0) {
|
||||||
|
|
||||||
|
if (bIsSSL) {
|
||||||
unsigned long error = ERR_get_error();
|
unsigned long error = ERR_get_error();
|
||||||
const char* error_str = ERR_error_string(error, NULL);
|
const char* error_str = ERR_error_string(error, NULL);
|
||||||
LogPrint("tls", "TLS: WARNING: %s: %s():%d - SSL_read - code[0x%x], err: %s\n",
|
LogPrint("tls", "TLS: WARNING: %s: %s():%d - SSL_read err: %s\n",
|
||||||
__FILE__, __func__, __LINE__, nRet, error_str);
|
__FILE__, __func__, __LINE__, error_str);
|
||||||
|
}
|
||||||
|
// socket closed gracefully (peer disconnected)
|
||||||
|
if (!pnode->fDisconnect)
|
||||||
|
LogPrint("tls", "socket closed (%s)\n", pnode->addr.ToString());
|
||||||
|
pnode->CloseSocketDisconnect();
|
||||||
|
fKeepReading = false;
|
||||||
|
|
||||||
|
} else if (nBytes < 0) {
|
||||||
|
// error
|
||||||
|
if (bIsSSL) {
|
||||||
|
if (nRet != WOLFSSL_ERROR_WANT_READ && nRet != WOLFSSL_ERROR_WANT_WRITE)
|
||||||
|
{
|
||||||
|
if (!pnode->fDisconnect)
|
||||||
|
LogPrintf("TLS: ERROR: SSL_read %s\n", ERR_error_string(nRet, NULL));
|
||||||
|
pnode->CloseSocketDisconnect();
|
||||||
|
|
||||||
|
unsigned long error = ERR_get_error();
|
||||||
|
const char* error_str = ERR_error_string(error, NULL);
|
||||||
|
LogPrint("tls", "TLS: WARNING: %s: %s():%d - SSL_read - code[0x%x], err: %s\n",
|
||||||
|
__FILE__, __func__, __LINE__, nRet, error_str);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// preventive measure from exhausting CPU usage
|
||||||
|
MilliSleep(1); // 1 msec
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// preventive measure from exhausting CPU usage
|
if (nRet != WSAEWOULDBLOCK && nRet != WSAEMSGSIZE && nRet != WSAEINTR && nRet != WSAEINPROGRESS) {
|
||||||
MilliSleep(1); // 1 msec
|
if (!pnode->fDisconnect)
|
||||||
}
|
LogPrintf("TLS: ERROR: socket recv %s\n", NetworkErrorString(nRet));
|
||||||
} else {
|
pnode->CloseSocketDisconnect();
|
||||||
if (nRet != WSAEWOULDBLOCK && nRet != WSAEMSGSIZE && nRet != WSAEINTR && nRet != WSAEINPROGRESS) {
|
}
|
||||||
if (!pnode->fDisconnect)
|
|
||||||
LogPrintf("TLS: ERROR: socket recv %s\n", NetworkErrorString(nRet));
|
|
||||||
pnode->CloseSocketDisconnect();
|
|
||||||
}
|
}
|
||||||
|
fKeepReading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
191
src/init.cpp
191
src/init.cpp
@@ -43,6 +43,7 @@
|
|||||||
#endif
|
#endif
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "metrics.h"
|
#include "metrics.h"
|
||||||
|
#include "pow.h"
|
||||||
#include "miner.h"
|
#include "miner.h"
|
||||||
#include "net.h"
|
#include "net.h"
|
||||||
#include "rpc/server.h"
|
#include "rpc/server.h"
|
||||||
@@ -176,7 +177,7 @@ public:
|
|||||||
// Writes do not need similar protection, as failure to write is handled by the caller.
|
// Writes do not need similar protection, as failure to write is handled by the caller.
|
||||||
};
|
};
|
||||||
|
|
||||||
static CCoinsViewDB *pcoinsdbview = NULL;
|
CCoinsViewDB *pcoinsdbview = NULL; // global (declared extern in main.h) for UTXO-snapshot dump/load
|
||||||
static CCoinsViewErrorCatcher *pcoinscatcher = NULL;
|
static CCoinsViewErrorCatcher *pcoinscatcher = NULL;
|
||||||
static boost::scoped_ptr<ECCVerifyHandle> globalVerifyHandle;
|
static boost::scoped_ptr<ECCVerifyHandle> globalVerifyHandle;
|
||||||
|
|
||||||
@@ -387,14 +388,17 @@ std::string HelpMessage(HelpMessageMode mode)
|
|||||||
}
|
}
|
||||||
strUsage += HelpMessageOpt("-datadir=<dir>", _("Specify data directory (this path cannot use '~')"));
|
strUsage += HelpMessageOpt("-datadir=<dir>", _("Specify data directory (this path cannot use '~')"));
|
||||||
strUsage += HelpMessageOpt("-exportdir=<dir>", _("Specify directory to be used when exporting data"));
|
strUsage += HelpMessageOpt("-exportdir=<dir>", _("Specify directory to be used when exporting data"));
|
||||||
strUsage += HelpMessageOpt("-dbcache=<n>", strprintf(_("Set database cache size in megabytes (%d to %d, default: %d)"), nMinDbCache, nMaxDbCache, nDefaultDbCache));
|
strUsage += HelpMessageOpt("-dbcache=<n>", strprintf(_("Set database cache size in megabytes (%d to %d). Default: adaptive - uses most free RAM to speed up initial block download (far fewer UTXO flushes to disk) and automatically shrinks if other applications need memory, always leaving a reserve free. Setting a fixed value disables adaptive sizing."), nMinDbCache, nMaxDbCache));
|
||||||
strUsage += HelpMessageOpt("-loadblock=<file>", _("Imports blocks from external blk000??.dat file") + " " + _("on startup"));
|
strUsage += HelpMessageOpt("-loadblock=<file>", _("Imports blocks from external blk000??.dat file") + " " + _("on startup"));
|
||||||
|
strUsage += HelpMessageOpt("-loadutxosnapshot=<file>", _("On a fresh node (empty chainstate), load a trusted UTXO snapshot produced by 'dumptxoutset' and fast-forward the tip to its height, skipping replay of earlier blocks. Block headers up to that height must already be present (e.g. via header sync or bootstrap). Blocks above the snapshot are still fully validated."));
|
||||||
|
strUsage += HelpMessageOpt("-loadutxosnapshotunsafe", _("Allow -loadutxosnapshot even when no trusted snapshot hash is hardcoded for this network (verifies file integrity only, not authenticity). Testing/regtest only."));
|
||||||
strUsage += HelpMessageOpt("-maxdebugfilesize=<n>", strprintf(_("Set the max size of the debug.log file (default: %u)"), 15));
|
strUsage += HelpMessageOpt("-maxdebugfilesize=<n>", strprintf(_("Set the max size of the debug.log file (default: %u)"), 15));
|
||||||
strUsage += HelpMessageOpt("-maxorphantx=<n>", strprintf(_("Keep at most <n> unconnectable transactions in memory (default: %u)"), DEFAULT_MAX_ORPHAN_TRANSACTIONS));
|
strUsage += HelpMessageOpt("-maxorphantx=<n>", strprintf(_("Keep at most <n> unconnectable transactions in memory (default: %u)"), DEFAULT_MAX_ORPHAN_TRANSACTIONS));
|
||||||
strUsage += HelpMessageOpt("-maxreorg=<n>", _("Specify the maximum length of a blockchain re-organization"));
|
strUsage += HelpMessageOpt("-maxreorg=<n>", _("Specify the maximum length of a blockchain re-organization"));
|
||||||
strUsage += HelpMessageOpt("-mempooltxinputlimit=<n>", _("[DEPRECATED/IGNORED] Set the maximum number of transparent inputs in a transaction that the mempool will accept (default: 0 = no limit applied)"));
|
strUsage += HelpMessageOpt("-mempooltxinputlimit=<n>", _("[DEPRECATED/IGNORED] Set the maximum number of transparent inputs in a transaction that the mempool will accept (default: 0 = no limit applied)"));
|
||||||
strUsage += HelpMessageOpt("-par=<n>", strprintf(_("Set the number of script verification threads (%u to %d, 0 = auto, <0 = leave that many cores free, default: %d)"),
|
strUsage += HelpMessageOpt("-par=<n>", strprintf(_("Set the number of script verification threads (%u to %d, 0 = auto, <0 = leave that many cores free, default: %d)"),
|
||||||
-(int)boost::thread::hardware_concurrency(), MAX_SCRIPTCHECK_THREADS, DEFAULT_SCRIPTCHECK_THREADS));
|
-(int)boost::thread::hardware_concurrency(), MAX_SCRIPTCHECK_THREADS, DEFAULT_SCRIPTCHECK_THREADS));
|
||||||
|
strUsage += HelpMessageOpt("-randomxverifythreads=<n>", strprintf(_("Number of threads for parallel RandomX PoW pre-verification of post-checkpoint blocks during sync (0 = inline only, max %d, default: same as -par)"), MAX_SCRIPTCHECK_THREADS));
|
||||||
#ifndef _WIN32
|
#ifndef _WIN32
|
||||||
strUsage += HelpMessageOpt("-pid=<file>", strprintf(_("Specify pid file (default: %s)"), "hushd.pid"));
|
strUsage += HelpMessageOpt("-pid=<file>", strprintf(_("Specify pid file (default: %s)"), "hushd.pid"));
|
||||||
#endif
|
#endif
|
||||||
@@ -987,6 +991,123 @@ bool AppInitServers(boost::thread_group& threadGroup)
|
|||||||
*/
|
*/
|
||||||
extern int32_t HUSH_REWIND;
|
extern int32_t HUSH_REWIND;
|
||||||
|
|
||||||
|
// --- Adaptive coins-cache sizing -------------------------------------------------------------
|
||||||
|
// The in-memory UTXO/coins cache (nCoinCacheUsage) is the biggest lever on IBD speed: a bigger
|
||||||
|
// cache means far fewer chainstate flushes to disk. We size it to use most of RAM, but a scheduled
|
||||||
|
// background task (AdjustCoinCacheForMemoryPressure, registered in AppInit2) shrinks the target when
|
||||||
|
// free system memory runs low — e.g. the user opens other apps — and grows it back when memory frees
|
||||||
|
// up, always leaving a reserve free for the rest of the system. The existing per-block flush
|
||||||
|
// (FlushStateToDisk, FLUSH_STATE_IF_NEEDED, which fires when cacheSize > nCoinCacheUsage) enforces
|
||||||
|
// whatever target is current, so the task only moves the threshold: it never touches cs_main or the
|
||||||
|
// flush path. NOTE: the coins cache is application heap, not OS file cache — "freeing" it means an
|
||||||
|
// early flush that clears the map; on Linux the allocator returns the pages, on Windows the heap
|
||||||
|
// returns them best-effort (RSS may lag), but either way the node stops growing past the target.
|
||||||
|
// windows.h / <unistd.h> arrive via compat.h (net.h). Memory helpers return 0 if undeterminable.
|
||||||
|
static int64_t GetPhysicalMemoryMB()
|
||||||
|
{
|
||||||
|
#ifdef WIN32
|
||||||
|
MEMORYSTATUSEX status;
|
||||||
|
status.dwLength = sizeof(status);
|
||||||
|
if (GlobalMemoryStatusEx(&status))
|
||||||
|
return (int64_t)(status.ullTotalPhys / (1024 * 1024));
|
||||||
|
return 0;
|
||||||
|
#elif defined(_SC_PHYS_PAGES) && defined(_SC_PAGESIZE)
|
||||||
|
long pages = sysconf(_SC_PHYS_PAGES);
|
||||||
|
long pageSize = sysconf(_SC_PAGESIZE);
|
||||||
|
if (pages > 0 && pageSize > 0)
|
||||||
|
return (int64_t)((int64_t)pages * (int64_t)pageSize / (1024 * 1024));
|
||||||
|
return 0;
|
||||||
|
#else
|
||||||
|
return 0;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// Currently-available (allocatable) physical RAM in MiB. On Linux uses MemAvailable (counts
|
||||||
|
// reclaimable page cache), falling back to truly-free pages.
|
||||||
|
static int64_t GetAvailableMemoryMB()
|
||||||
|
{
|
||||||
|
#ifdef WIN32
|
||||||
|
MEMORYSTATUSEX status;
|
||||||
|
status.dwLength = sizeof(status);
|
||||||
|
if (GlobalMemoryStatusEx(&status))
|
||||||
|
return (int64_t)(status.ullAvailPhys / (1024 * 1024));
|
||||||
|
return 0;
|
||||||
|
#else
|
||||||
|
FILE* f = fopen("/proc/meminfo", "r");
|
||||||
|
if (f) {
|
||||||
|
char line[256];
|
||||||
|
long long availKB = -1;
|
||||||
|
while (fgets(line, sizeof(line), f)) {
|
||||||
|
if (sscanf(line, "MemAvailable: %lld kB", &availKB) == 1)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
fclose(f);
|
||||||
|
if (availKB >= 0)
|
||||||
|
return (int64_t)(availKB / 1024);
|
||||||
|
}
|
||||||
|
#if defined(_SC_AVPHYS_PAGES) && defined(_SC_PAGESIZE)
|
||||||
|
long pages = sysconf(_SC_AVPHYS_PAGES);
|
||||||
|
long pageSize = sysconf(_SC_PAGESIZE);
|
||||||
|
if (pages > 0 && pageSize > 0)
|
||||||
|
return (int64_t)((int64_t)pages * (int64_t)pageSize / (1024 * 1024));
|
||||||
|
#endif
|
||||||
|
return 0;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// RAM (MiB) to always keep free for the OS and other applications: 20% of total, at least 2 GiB.
|
||||||
|
static int64_t GetMemoryReserveMB()
|
||||||
|
{
|
||||||
|
int64_t ramMB = GetPhysicalMemoryMB();
|
||||||
|
int64_t reserve = (ramMB > 0) ? ramMB / 5 : 2048; // 20%
|
||||||
|
if (reserve < 2048) reserve = 2048;
|
||||||
|
return reserve;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Startup -dbcache default: use most of RAM (total minus the reserve), clamped to
|
||||||
|
// [nDefaultDbCache, nMaxDbCache] MiB. Falls back to the fixed default if RAM can't be detected.
|
||||||
|
static int64_t GetDefaultDbCacheMB()
|
||||||
|
{
|
||||||
|
int64_t ramMB = GetPhysicalMemoryMB();
|
||||||
|
if (ramMB <= 0)
|
||||||
|
return nDefaultDbCache;
|
||||||
|
int64_t cacheMB = ramMB - GetMemoryReserveMB();
|
||||||
|
if (cacheMB < nDefaultDbCache) cacheMB = nDefaultDbCache;
|
||||||
|
if (cacheMB > nMaxDbCache) cacheMB = nMaxDbCache;
|
||||||
|
return cacheMB;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ceiling (bytes) the adaptive task may grow the coins cache back up to (the startup nCoinCacheUsage).
|
||||||
|
static size_t g_nMaxCoinCacheUsage = 0;
|
||||||
|
static const int64_t g_nMinCoinCacheMB = 256; // never thrash below this working set
|
||||||
|
|
||||||
|
// Scheduled task: nudge nCoinCacheUsage toward "use all RAM except the reserve". If free RAM is below
|
||||||
|
// the reserve we shrink the target (the next per-block flush releases the excess); if there is spare
|
||||||
|
// RAM we grow it back toward the startup ceiling. Lock-free: it only reads system memory and writes
|
||||||
|
// the aligned size_t threshold that the flush path reads.
|
||||||
|
static void AdjustCoinCacheForMemoryPressure()
|
||||||
|
{
|
||||||
|
if (g_nMaxCoinCacheUsage == 0)
|
||||||
|
return; // adaptive sizing disabled (user pinned -dbcache) or RAM undetectable
|
||||||
|
int64_t availMB = GetAvailableMemoryMB();
|
||||||
|
if (availMB <= 0)
|
||||||
|
return; // can't measure pressure; leave the target untouched
|
||||||
|
int64_t reserveMB = GetMemoryReserveMB();
|
||||||
|
// Error term: free RAM beyond the reserve. >0 => spare, grow; <0 => pressure, shrink.
|
||||||
|
int64_t errMB = availMB - reserveMB;
|
||||||
|
// Deadband: ignore small fluctuations so the target settles instead of oscillating.
|
||||||
|
if (errMB > -256 && errMB < 256)
|
||||||
|
return;
|
||||||
|
int64_t curTargetMB = (int64_t)(nCoinCacheUsage >> 20);
|
||||||
|
// Damped proportional step (gain 1/4) toward "free RAM == reserve"; the clamps bound it and the
|
||||||
|
// per-block flush (FLUSH_STATE_IF_NEEDED) enforces a lowered target within ~one block during IBD.
|
||||||
|
int64_t newTargetMB = curTargetMB + errMB / 4;
|
||||||
|
int64_t ceilMB = (int64_t)(g_nMaxCoinCacheUsage >> 20);
|
||||||
|
if (newTargetMB > ceilMB) newTargetMB = ceilMB;
|
||||||
|
if (newTargetMB < g_nMinCoinCacheMB) newTargetMB = g_nMinCoinCacheMB;
|
||||||
|
nCoinCacheUsage = (size_t)(newTargetMB << 20);
|
||||||
|
}
|
||||||
|
|
||||||
bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
|
bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
|
||||||
{
|
{
|
||||||
//fprintf(stderr,"%s start\n", __FUNCTION__);
|
//fprintf(stderr,"%s start\n", __FUNCTION__);
|
||||||
@@ -1309,6 +1430,15 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
|
|||||||
else if (nScriptCheckThreads > MAX_SCRIPTCHECK_THREADS)
|
else if (nScriptCheckThreads > MAX_SCRIPTCHECK_THREADS)
|
||||||
nScriptCheckThreads = MAX_SCRIPTCHECK_THREADS;
|
nScriptCheckThreads = MAX_SCRIPTCHECK_THREADS;
|
||||||
|
|
||||||
|
// Parallel RandomX pre-verification threads (speeds up post-checkpoint sync). Defaults to the
|
||||||
|
// script-check thread count — RandomX pre-verify and script checks do not run simultaneously
|
||||||
|
// within a single connect, so they can share the same budget. 0 disables (inline-only).
|
||||||
|
nRandomXVerifyThreads = GetArg("-randomxverifythreads", nScriptCheckThreads);
|
||||||
|
if (nRandomXVerifyThreads < 0)
|
||||||
|
nRandomXVerifyThreads = 0;
|
||||||
|
else if (nRandomXVerifyThreads > MAX_SCRIPTCHECK_THREADS)
|
||||||
|
nRandomXVerifyThreads = MAX_SCRIPTCHECK_THREADS;
|
||||||
|
|
||||||
fServer = GetBoolArg("-server", false);
|
fServer = GetBoolArg("-server", false);
|
||||||
//fprintf(stderr,"%s tik6\n", __FUNCTION__);
|
//fprintf(stderr,"%s tik6\n", __FUNCTION__);
|
||||||
|
|
||||||
@@ -1545,6 +1675,14 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
|
|||||||
threadGroup.create_thread(&ThreadScriptCheck);
|
threadGroup.create_thread(&ThreadScriptCheck);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Spawn the parallel RandomX pre-verification worker pool (the connect thread joins as the Nth
|
||||||
|
// worker via CCheckQueueControl::Wait, so spawn N-1 here, mirroring ThreadScriptCheck).
|
||||||
|
if (ASSETCHAINS_ALGO == ASSETCHAINS_RANDOMX && nRandomXVerifyThreads > 0) {
|
||||||
|
LogPrintf("Using %u threads for parallel RandomX pre-verification\n", nRandomXVerifyThreads);
|
||||||
|
for (int i = 0; i < nRandomXVerifyThreads - 1; i++)
|
||||||
|
threadGroup.create_thread(&ThreadRandomXVerify);
|
||||||
|
}
|
||||||
|
|
||||||
//fprintf(stderr,"%s tik13\n", __FUNCTION__);
|
//fprintf(stderr,"%s tik13\n", __FUNCTION__);
|
||||||
|
|
||||||
// Start the lightweight task scheduler thread
|
// Start the lightweight task scheduler thread
|
||||||
@@ -1840,7 +1978,7 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
|
|||||||
LogPrintf("* Compression is %s\n", dbCompression ? "enabled" : "disabled");
|
LogPrintf("* Compression is %s\n", dbCompression ? "enabled" : "disabled");
|
||||||
|
|
||||||
// cache size calculations
|
// cache size calculations
|
||||||
int64_t nTotalCache = (GetArg("-dbcache", nDefaultDbCache) << 20);
|
int64_t nTotalCache = (GetArg("-dbcache", GetDefaultDbCacheMB()) << 20);
|
||||||
nTotalCache = std::max(nTotalCache, nMinDbCache << 20); // total cache cannot be less than nMinDbCache
|
nTotalCache = std::max(nTotalCache, nMinDbCache << 20); // total cache cannot be less than nMinDbCache
|
||||||
nTotalCache = std::min(nTotalCache, nMaxDbCache << 20); // total cache cannot be greated than nMaxDbcache
|
nTotalCache = std::min(nTotalCache, nMaxDbCache << 20); // total cache cannot be greated than nMaxDbcache
|
||||||
int64_t nBlockTreeDBCache = nTotalCache / 8;
|
int64_t nBlockTreeDBCache = nTotalCache / 8;
|
||||||
@@ -1857,6 +1995,14 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
|
|||||||
int64_t nCoinDBCache = std::min(nTotalCache / 2, (nTotalCache / 4) + (1 << 23)); // use 25%-50% of the remainder for disk cache
|
int64_t nCoinDBCache = std::min(nTotalCache / 2, (nTotalCache / 4) + (1 << 23)); // use 25%-50% of the remainder for disk cache
|
||||||
nTotalCache -= nCoinDBCache;
|
nTotalCache -= nCoinDBCache;
|
||||||
nCoinCacheUsage = nTotalCache; // the rest goes to in-memory cache
|
nCoinCacheUsage = nTotalCache; // the rest goes to in-memory cache
|
||||||
|
// Adaptive sizing: unless the user pinned -dbcache, grow/shrink the coins cache with free system
|
||||||
|
// memory (AdjustCoinCacheForMemoryPressure), using the startup size as the ceiling.
|
||||||
|
if (!mapArgs.count("-dbcache")) {
|
||||||
|
g_nMaxCoinCacheUsage = nCoinCacheUsage;
|
||||||
|
scheduler.scheduleEvery(&AdjustCoinCacheForMemoryPressure, 5);
|
||||||
|
LogPrintf("* Adaptive dbcache enabled: ceiling %.0fMiB, keeping >= %lldMiB RAM free for the system\n",
|
||||||
|
nCoinCacheUsage * (1.0 / 1024 / 1024), (long long)GetMemoryReserveMB());
|
||||||
|
}
|
||||||
LogPrintf("Cache configuration:\n");
|
LogPrintf("Cache configuration:\n");
|
||||||
LogPrintf("* Max cache setting possible %.1fMiB\n", nMaxDbCache);
|
LogPrintf("* Max cache setting possible %.1fMiB\n", nMaxDbCache);
|
||||||
LogPrintf("* Using %.1fMiB for block index database\n", nBlockTreeDBCache * (1.0 / 1024 / 1024));
|
LogPrintf("* Using %.1fMiB for block index database\n", nBlockTreeDBCache * (1.0 / 1024 / 1024));
|
||||||
@@ -1938,6 +2084,45 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
|
|||||||
strLoadError = _("Error initializing block database");
|
strLoadError = _("Error initializing block database");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Trusted UTXO snapshot fast-sync (assumeutxo-style). If -loadutxosnapshot is given
|
||||||
|
// and the chainstate is still empty, load the verified snapshot and fast-forward the
|
||||||
|
// tip to height H; blocks above H then sync with full PoW/script/Sapling validation.
|
||||||
|
{
|
||||||
|
std::string snapPath = GetArg("-loadutxosnapshot", "");
|
||||||
|
if (!snapPath.empty()) {
|
||||||
|
if (!pcoinsdbview->GetBestBlock().IsNull()) {
|
||||||
|
LogPrintf("%s: -loadutxosnapshot ignored, chainstate is not empty\n", __func__);
|
||||||
|
} else {
|
||||||
|
const CChainParams::AssumeutxoData& au = chainparams.Assumeutxo();
|
||||||
|
bool unsafe = GetBoolArg("-loadutxosnapshotunsafe", false);
|
||||||
|
if (au.IsNull() && !unsafe) {
|
||||||
|
strLoadError = _("-loadutxosnapshot: no trusted snapshot hash is configured for this network; refusing (use -loadutxosnapshotunsafe for testing only)");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
CUTXOSnapshotHeader hdr; uint256 gotHash; std::string snapErr;
|
||||||
|
bool requireExpected = !au.IsNull() && !unsafe;
|
||||||
|
if (!pcoinsdbview->LoadSnapshot(snapPath, au.hash, requireExpected, hdr, gotHash, snapErr)) {
|
||||||
|
strLoadError = strprintf(_("Failed to load UTXO snapshot: %s"), snapErr);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!au.IsNull() && hdr.nHeight != au.height) {
|
||||||
|
strLoadError = _("UTXO snapshot height does not match the trusted value for this network");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
pcoinsTip->SetBestBlock(hdr.baseBlockHash); // refresh cache view of the freshly-written chainstate
|
||||||
|
std::string fixErr;
|
||||||
|
if (!LoadSnapshotChainstate(hdr, fixErr)) {
|
||||||
|
strLoadError = strprintf(_("Failed to activate UTXO snapshot tip: %s"), fixErr);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
pblocktree->WriteAssumeutxoHeight(hdr.nHeight); // persist reorg-below-H guard across restarts
|
||||||
|
LogPrintf("%s: loaded trusted UTXO snapshot at height %d (hash %s); syncing forward with full validation\n",
|
||||||
|
__func__, hdr.nHeight, gotHash.GetHex());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
HUSH_LOADINGBLOCKS = 0;
|
HUSH_LOADINGBLOCKS = 0;
|
||||||
// Check for changed -txindex state
|
// Check for changed -txindex state
|
||||||
if (fTxIndex != GetBoolArg("-txindex", true)) {
|
if (fTxIndex != GetBoolArg("-txindex", true)) {
|
||||||
|
|||||||
138
src/main.cpp
138
src/main.cpp
@@ -85,10 +85,12 @@ void hush_pricesupdate(int32_t height,CBlock *pblock);
|
|||||||
BlockMap mapBlockIndex;
|
BlockMap mapBlockIndex;
|
||||||
CChain chainActive;
|
CChain chainActive;
|
||||||
CBlockIndex *pindexBestHeader = NULL;
|
CBlockIndex *pindexBestHeader = NULL;
|
||||||
|
int nAssumeutxoSnapshotHeight = -1; // height H of a loaded UTXO snapshot; reorgs below H are refused (-1 = none)
|
||||||
static int64_t nTimeBestReceived = 0;
|
static int64_t nTimeBestReceived = 0;
|
||||||
CWaitableCriticalSection csBestBlock;
|
CWaitableCriticalSection csBestBlock;
|
||||||
CConditionVariable cvBlockChange;
|
CConditionVariable cvBlockChange;
|
||||||
int nScriptCheckThreads = 0;
|
int nScriptCheckThreads = 0;
|
||||||
|
int nRandomXVerifyThreads = 0; // parallel RandomX pre-verification worker count (0 = inline only)
|
||||||
bool fExperimentalMode = true;
|
bool fExperimentalMode = true;
|
||||||
bool fImporting = false;
|
bool fImporting = false;
|
||||||
bool fReindex = false;
|
bool fReindex = false;
|
||||||
@@ -485,7 +487,7 @@ namespace {
|
|||||||
|
|
||||||
/** Update pindexLastCommonBlock and add not-in-flight missing successors to vBlocks, until it has
|
/** Update pindexLastCommonBlock and add not-in-flight missing successors to vBlocks, until it has
|
||||||
* at most count entries. */
|
* at most count entries. */
|
||||||
void FindNextBlocksToDownload(NodeId nodeid, unsigned int count, std::vector<CBlockIndex*>& vBlocks, NodeId& nodeStaller) {
|
void FindNextBlocksToDownload(NodeId nodeid, unsigned int count, std::vector<CBlockIndex*>& vBlocks, NodeId& nodeStaller, CBlockIndex** pFrontierStuck = NULL) {
|
||||||
if (count == 0)
|
if (count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -562,8 +564,9 @@ namespace {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if (waitingfor == -1) {
|
} else if (waitingfor == -1) {
|
||||||
// This is the first already-in-flight block.
|
// This is the first already-in-flight block (the download frontier).
|
||||||
waitingfor = mapBlocksInFlight[pindex->GetBlockHash()].first;
|
waitingfor = mapBlocksInFlight[pindex->GetBlockHash()].first;
|
||||||
|
if (pFrontierStuck) *pFrontierStuck = pindex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4140,6 +4143,45 @@ static void PruneBlockIndexCandidates() {
|
|||||||
assert(!setBlockIndexCandidates.empty());
|
assert(!setBlockIndexCandidates.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Activate a trusted UTXO snapshot (assumeutxo-style) as the chain tip WITHOUT replaying blocks
|
||||||
|
// 0..H. The chainstate has already been populated by CCoinsViewDB::LoadSnapshot(); here we mark the
|
||||||
|
// snapshot's base block (height H) as fully validated and set it as the active tip. Blocks above H
|
||||||
|
// then connect normally with full PoW + script + Sapling-proof validation. Requires that the block
|
||||||
|
// HEADERS for height H are already present in mapBlockIndex (from prior header sync or bootstrap).
|
||||||
|
// NOTE: below-H blocks have no body/undo data, so reorgs below H are impossible (see Stage D guard).
|
||||||
|
bool LoadSnapshotChainstate(const CUTXOSnapshotHeader& header, std::string& strError)
|
||||||
|
{
|
||||||
|
LOCK(cs_main);
|
||||||
|
BlockMap::iterator it = mapBlockIndex.find(header.baseBlockHash);
|
||||||
|
if (it == mapBlockIndex.end() || it->second == NULL) {
|
||||||
|
strError = "block header for the snapshot height is not present; sync headers (or use the bootstrap) before loading a UTXO snapshot";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
CBlockIndex* pindexH = it->second;
|
||||||
|
if (pindexH->GetHeight() != header.nHeight) {
|
||||||
|
strError = "snapshot base block height does not match its header index";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only nChainTx is consensus-relevant for tip selection; nTx must merely be non-zero so the
|
||||||
|
// (nChainTx != 0) candidate-eligibility checks hold. Ancestors legitimately have nTx==0 here
|
||||||
|
// because we never received their bodies — this is the assumeutxo trust assumption.
|
||||||
|
if (pindexH->nTx == 0)
|
||||||
|
pindexH->nTx = (header.nChainTx > 0 ? (unsigned int)header.nChainTx : 1);
|
||||||
|
pindexH->nChainTx = (unsigned int)header.nChainTx;
|
||||||
|
if (header.fHasChainSaplingValue)
|
||||||
|
pindexH->nChainSaplingValue = header.nChainSaplingValue;
|
||||||
|
|
||||||
|
pindexH->RaiseValidity(BLOCK_VALID_SCRIPTS);
|
||||||
|
nAssumeutxoSnapshotHeight = pindexH->GetHeight(); // arm the reorg-below-H guard (Stage D)
|
||||||
|
setBlockIndexCandidates.insert(pindexH);
|
||||||
|
chainActive.SetTip(pindexH);
|
||||||
|
if (pindexBestHeader == NULL || pindexBestHeader->GetHeight() < pindexH->GetHeight())
|
||||||
|
pindexBestHeader = pindexH;
|
||||||
|
PruneBlockIndexCandidates();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Try to make some progress towards making pindexMostWork the active block.
|
* Try to make some progress towards making pindexMostWork the active block.
|
||||||
* pblock is either NULL or a pointer to a CBlock corresponding to pindexMostWork.
|
* pblock is either NULL or a pointer to a CBlock corresponding to pindexMostWork.
|
||||||
@@ -4188,6 +4230,16 @@ static bool ActivateBestChainStep(bool fSkipdpow, CValidationState &state, CBloc
|
|||||||
return state.DoS(100, error("ActivateBestChainStep(): pindexOldTip->GetHeight().%d > notarizedht %d && pindexFork->GetHeight().%d is < notarizedht %d, so ignore it",(int32_t)pindexOldTip->GetHeight(),notarizedht,(int32_t)pindexFork->GetHeight(),notarizedht),
|
return state.DoS(100, error("ActivateBestChainStep(): pindexOldTip->GetHeight().%d > notarizedht %d && pindexFork->GetHeight().%d is < notarizedht %d, so ignore it",(int32_t)pindexOldTip->GetHeight(),notarizedht,(int32_t)pindexFork->GetHeight(),notarizedht),
|
||||||
REJECT_INVALID, "past-notarized-height");
|
REJECT_INVALID, "past-notarized-height");
|
||||||
}
|
}
|
||||||
|
// Refuse reorgs whose fork point is below a loaded UTXO snapshot height (Stage D): the node has
|
||||||
|
// no block/undo data for 0..H, so disconnecting below H is impossible. Belt-and-suspenders on top
|
||||||
|
// of checkpoint fork-rejection (H sits at/below the last hardcoded checkpoint).
|
||||||
|
if ( nAssumeutxoSnapshotHeight >= 0 && pindexFork != 0 && pindexFork->GetHeight() < nAssumeutxoSnapshotHeight )
|
||||||
|
{
|
||||||
|
return state.DoS(100, error("ActivateBestChainStep(): reorg fork height %d is below the loaded UTXO snapshot height %d; refusing",
|
||||||
|
(int32_t)pindexFork->GetHeight(), nAssumeutxoSnapshotHeight),
|
||||||
|
REJECT_INVALID, "below-assumeutxo-snapshot");
|
||||||
|
}
|
||||||
|
|
||||||
// - On ChainDB initialization, pindexOldTip will be null, so there are no removable blocks.
|
// - On ChainDB initialization, pindexOldTip will be null, so there are no removable blocks.
|
||||||
// - If pindexMostWork is in a chain that doesn't have the same genesis block as our chain,
|
// - If pindexMostWork is in a chain that doesn't have the same genesis block as our chain,
|
||||||
// then pindexFork will be null, and we would need to remove the entire chain including
|
// then pindexFork will be null, and we would need to remove the entire chain including
|
||||||
@@ -4258,6 +4310,36 @@ static bool ActivateBestChainStep(bool fSkipdpow, CValidationState &state, CBloc
|
|||||||
}
|
}
|
||||||
nHeight = nTargetHeight;
|
nHeight = nTargetHeight;
|
||||||
|
|
||||||
|
// Parallel RandomX pre-verification (Stage 4): verify this about-to-be-connected window's
|
||||||
|
// PoW on the worker pool BEFORE the serial connect, so ConnectBlock rarely pays the
|
||||||
|
// ~tens-of-ms light-mode hash. Pure optimization — CheckBlockHeader's inline
|
||||||
|
// CheckRandomXSolution still verifies anything not pre-verified, so consensus is unchanged.
|
||||||
|
// We hold cs_main; key derivation + the disk reads happen here on the main thread, and the
|
||||||
|
// pool workers receive only value-type work items (no cs_main, no chainstate pointers).
|
||||||
|
if (nRandomXVerifyThreads > 0 && rxCheckQueue.IsIdle()) {
|
||||||
|
std::map<std::string, std::vector<CRandomXCheck> > rxGroups; // grouped by RandomX key
|
||||||
|
BOOST_FOREACH(CBlockIndex *pidx, vpindexToConnect) {
|
||||||
|
if (pidx->fRandomXVerified || !RandomXValidationRequired(pidx->GetHeight()))
|
||||||
|
continue;
|
||||||
|
std::string rxKey = GetRandomXKey(pidx->GetHeight());
|
||||||
|
if (rxKey.empty())
|
||||||
|
continue; // can't derive key -> inline fallback
|
||||||
|
CBlock blk;
|
||||||
|
if (!ReadBlockFromDisk(blk, pidx, false))
|
||||||
|
continue; // -> inline fallback
|
||||||
|
if (blk.nSolution.size() != 32) // RANDOMX_HASH_SIZE; wrong size -> inline (will error)
|
||||||
|
continue;
|
||||||
|
rxGroups[rxKey].push_back(CRandomXCheck(rxKey, GetRandomXInput(blk), blk.nSolution.data(), &pidx->fRandomXVerified));
|
||||||
|
}
|
||||||
|
for (std::map<std::string, std::vector<CRandomXCheck> >::iterator it = rxGroups.begin(); it != rxGroups.end(); ++it) {
|
||||||
|
if (!RandomXValidatorPrepareKey(it->first))
|
||||||
|
break; // cache alloc failed -> leave the rest for the inline fallback
|
||||||
|
CCheckQueueControl<CRandomXCheck> control(&rxCheckQueue);
|
||||||
|
control.Add(it->second);
|
||||||
|
control.Wait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Connect new blocks.
|
// Connect new blocks.
|
||||||
BOOST_REVERSE_FOREACH(CBlockIndex *pindexConnect, vpindexToConnect) {
|
BOOST_REVERSE_FOREACH(CBlockIndex *pindexConnect, vpindexToConnect) {
|
||||||
if (!ConnectTip(state, pindexConnect, pindexConnect == pindexMostWork ? pblock : NULL)) {
|
if (!ConnectTip(state, pindexConnect, pindexConnect == pindexMostWork ? pblock : NULL)) {
|
||||||
@@ -4993,7 +5075,11 @@ bool CheckBlockHeader(int32_t *futureblockp,int32_t height,CBlockIndex *pindex,
|
|||||||
{
|
{
|
||||||
if ( !CheckEquihashSolution(&blockhdr, Params()) )
|
if ( !CheckEquihashSolution(&blockhdr, Params()) )
|
||||||
return state.DoS(100, error("CheckBlockHeader(): Equihash solution invalid"),REJECT_INVALID, "invalid-solution");
|
return state.DoS(100, error("CheckBlockHeader(): Equihash solution invalid"),REJECT_INVALID, "invalid-solution");
|
||||||
if ( !CheckRandomXSolution(&blockhdr, height) )
|
// Skip the inline RandomX recompute only if the parallel pre-verify pool already verified
|
||||||
|
// THIS block (fRandomXVerified set 1:1 on a real hash match). Every other case — pool miss,
|
||||||
|
// straggler, disabled pool, or any pindex==NULL caller (TestBlockValidity/VerifyDB/header
|
||||||
|
// accept) — falls through to the inline check, so consensus is unchanged.
|
||||||
|
if ( !(pindex && pindex->fRandomXVerified) && !CheckRandomXSolution(&blockhdr, height) )
|
||||||
return state.DoS(100, error("CheckBlockHeader(): RandomX solution invalid"),REJECT_INVALID, "invalid-randomx-solution");
|
return state.DoS(100, error("CheckBlockHeader(): RandomX solution invalid"),REJECT_INVALID, "invalid-randomx-solution");
|
||||||
}
|
}
|
||||||
// Check proof of work matches claimed amount
|
// Check proof of work matches claimed amount
|
||||||
@@ -5957,6 +6043,15 @@ bool static LoadBlockIndexDB()
|
|||||||
pblocktree->ReadReindexing(fReindexing);
|
pblocktree->ReadReindexing(fReindexing);
|
||||||
fReindex |= fReindexing;
|
fReindex |= fReindexing;
|
||||||
|
|
||||||
|
// Restore the loaded-UTXO-snapshot height so the reorg-below-H guard survives restarts.
|
||||||
|
{
|
||||||
|
int snapHeight = -1;
|
||||||
|
if (pblocktree->ReadAssumeutxoHeight(snapHeight) && snapHeight >= 0) {
|
||||||
|
nAssumeutxoSnapshotHeight = snapHeight;
|
||||||
|
LogPrintf("%s: loaded-from-UTXO-snapshot height is %d; reorgs below it are refused\n", __func__, snapHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check whether we have a transaction index
|
// Check whether we have a transaction index
|
||||||
pblocktree->ReadFlag("txindex", fTxIndex);
|
pblocktree->ReadFlag("txindex", fTxIndex);
|
||||||
LogPrintf("%s: transaction index %s\n", __func__, fTxIndex ? "enabled" : "disabled");
|
LogPrintf("%s: transaction index %s\n", __func__, fTxIndex ? "enabled" : "disabled");
|
||||||
@@ -6746,6 +6841,13 @@ void static ProcessGetData(CNode* pfrom)
|
|||||||
std::deque<CInv>::iterator it = pfrom->vRecvGetData.begin();
|
std::deque<CInv>::iterator it = pfrom->vRecvGetData.begin();
|
||||||
|
|
||||||
vector<CInv> vNotFound;
|
vector<CInv> vNotFound;
|
||||||
|
// Serve up to this many blocks per ProcessGetData pass. The old code broke after a SINGLE block,
|
||||||
|
// so a 16-block getdata was dribbled out one block per message-handler tick (~100ms), throttling
|
||||||
|
// block download for every peer fetching from us. Bound the per-pass work (cs_main is held while
|
||||||
|
// reading blocks from disk); any remainder is served on the next pass (the message handler keeps
|
||||||
|
// fSleep=false while vRecvGetData is non-empty, so there is no 100ms park between passes).
|
||||||
|
const unsigned int nMaxBlocksServedPerPass = 16;
|
||||||
|
unsigned int nBlocksServed = 0;
|
||||||
|
|
||||||
LOCK(cs_main);
|
LOCK(cs_main);
|
||||||
|
|
||||||
@@ -6863,7 +6965,10 @@ void static ProcessGetData(CNode* pfrom)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inv.type == MSG_BLOCK || inv.type == MSG_FILTERED_BLOCK)
|
// Serve a bounded batch of blocks per pass rather than one (see nMaxBlocksServedPerPass
|
||||||
|
// above). The send-buffer gate at the top of the loop still pauses us if the buffer fills;
|
||||||
|
// this counter bounds the cs_main hold for a (possibly malicious) large getdata.
|
||||||
|
if ((inv.type == MSG_BLOCK || inv.type == MSG_FILTERED_BLOCK) && ++nBlocksServed >= nMaxBlocksServedPerPass)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8145,12 +8250,35 @@ bool SendMessages(CNode* pto, bool fSendTrickle)
|
|||||||
if (!pto->fDisconnect && !pto->fClient && (fFetch || !IsInitialBlockDownload()) && state.nBlocksInFlight < MAX_BLOCKS_IN_TRANSIT_PER_PEER) {
|
if (!pto->fDisconnect && !pto->fClient && (fFetch || !IsInitialBlockDownload()) && state.nBlocksInFlight < MAX_BLOCKS_IN_TRANSIT_PER_PEER) {
|
||||||
vector<CBlockIndex*> vToDownload;
|
vector<CBlockIndex*> vToDownload;
|
||||||
NodeId staller = -1;
|
NodeId staller = -1;
|
||||||
FindNextBlocksToDownload(pto->GetId(), MAX_BLOCKS_IN_TRANSIT_PER_PEER - state.nBlocksInFlight, vToDownload, staller);
|
CBlockIndex *pFrontierStuck = NULL;
|
||||||
|
FindNextBlocksToDownload(pto->GetId(), MAX_BLOCKS_IN_TRANSIT_PER_PEER - state.nBlocksInFlight, vToDownload, staller, &pFrontierStuck);
|
||||||
BOOST_FOREACH(CBlockIndex *pindex, vToDownload) {
|
BOOST_FOREACH(CBlockIndex *pindex, vToDownload) {
|
||||||
vGetData.push_back(CInv(MSG_BLOCK, pindex->GetBlockHash()));
|
vGetData.push_back(CInv(MSG_BLOCK, pindex->GetBlockHash()));
|
||||||
MarkBlockAsInFlight(pto->GetId(), pindex->GetBlockHash(), consensusParams, pindex);
|
MarkBlockAsInFlight(pto->GetId(), pindex->GetBlockHash(), consensusParams, pindex);
|
||||||
LogPrint("net", "Requesting block %s (%d) peer=%d\n", pindex->GetBlockHash().ToString(), pindex->GetHeight(), pto->id);
|
LogPrint("net", "Requesting block %s (%d) peer=%d\n", pindex->GetBlockHash().ToString(), pindex->GetHeight(), pto->id);
|
||||||
}
|
}
|
||||||
|
// Frontier reassignment: when this peer has nothing new to fetch because the next-needed
|
||||||
|
// (frontier) block is in flight from another, slow peer and has been stuck beyond a short
|
||||||
|
// threshold, re-request it from THIS (responsive) peer instead of waiting out the long
|
||||||
|
// (~72s) timeout or disconnecting the slow peer. This breaks the head-of-line stall that
|
||||||
|
// throttles IBD when downloading from few, distant peers. Trustless: the block is still
|
||||||
|
// fully validated on arrival - we only change which peer serves it. -blockreassigntimeout
|
||||||
|
// = seconds (0 disables; default 5).
|
||||||
|
static const int64_t nReassignUs = GetArg("-blockreassigntimeout", 5) * 1000000LL;
|
||||||
|
if (nReassignUs > 0 && vToDownload.empty() && pFrontierStuck != NULL &&
|
||||||
|
staller != -1 && staller != pto->GetId()) {
|
||||||
|
map<uint256, pair<NodeId, list<QueuedBlock>::iterator> >::iterator itF =
|
||||||
|
mapBlocksInFlight.find(pFrontierStuck->GetBlockHash());
|
||||||
|
if (itF != mapBlocksInFlight.end() && itF->second.first == staller &&
|
||||||
|
itF->second.second->nTime < nNow - nReassignUs) {
|
||||||
|
uint256 hReassign = pFrontierStuck->GetBlockHash();
|
||||||
|
LogPrint("net", "Reassigning stalled frontier block %s (%d) from peer=%d to peer=%d\n",
|
||||||
|
hReassign.ToString(), pFrontierStuck->GetHeight(), staller, pto->id);
|
||||||
|
MarkBlockAsReceived(hReassign); // free from slow peer (no disconnect)
|
||||||
|
vGetData.push_back(CInv(MSG_BLOCK, hReassign));
|
||||||
|
MarkBlockAsInFlight(pto->GetId(), hReassign, consensusParams, pFrontierStuck); // re-request from this peer
|
||||||
|
}
|
||||||
|
}
|
||||||
if (state.nBlocksInFlight == 0 && staller != -1) {
|
if (state.nBlocksInFlight == 0 && staller != -1) {
|
||||||
if (State(staller)->nStallingSince == 0) {
|
if (State(staller)->nStallingSince == 0) {
|
||||||
State(staller)->nStallingSince = nNow;
|
State(staller)->nStallingSince = nNow;
|
||||||
|
|||||||
20
src/main.h
20
src/main.h
@@ -100,7 +100,11 @@ static const int MAX_BLOCKS_IN_TRANSIT_PER_PEER = 16;
|
|||||||
/** Timeout in seconds during which a peer must stall block download progress before being disconnected. */
|
/** Timeout in seconds during which a peer must stall block download progress before being disconnected. */
|
||||||
static const unsigned int BLOCK_STALLING_TIMEOUT = 2;
|
static const unsigned int BLOCK_STALLING_TIMEOUT = 2;
|
||||||
/** Number of headers sent in one getheaders result. We rely on the assumption that if a peer sends
|
/** Number of headers sent in one getheaders result. We rely on the assumption that if a peer sends
|
||||||
* less than this number, we reached its tip. Changing this value is a protocol upgrade. */
|
* less than this number, we reached its tip. Changing this value is a protocol upgrade: the
|
||||||
|
* continuation logic (main.cpp, "nCount == MAX_HEADERS_RESULTS") and the serve-side limit must
|
||||||
|
* match across the network, so a single node raising it unilaterally would mis-detect a stock
|
||||||
|
* peer's 160-header reply as "tip reached" and stall header sync. Raise only as a coordinated
|
||||||
|
* network upgrade (with a protocol-version bump). */
|
||||||
static const unsigned int MAX_HEADERS_RESULTS = 160;
|
static const unsigned int MAX_HEADERS_RESULTS = 160;
|
||||||
/** Size of the "block download window": how far ahead of our current height do we fetch?
|
/** Size of the "block download window": how far ahead of our current height do we fetch?
|
||||||
* Larger windows tolerate larger download speed differences between peer, but increase the potential
|
* Larger windows tolerate larger download speed differences between peer, but increase the potential
|
||||||
@@ -155,6 +159,7 @@ extern bool fExperimentalMode;
|
|||||||
extern bool fImporting;
|
extern bool fImporting;
|
||||||
extern bool fReindex;
|
extern bool fReindex;
|
||||||
extern int nScriptCheckThreads;
|
extern int nScriptCheckThreads;
|
||||||
|
extern int nRandomXVerifyThreads;
|
||||||
extern bool fTxIndex;
|
extern bool fTxIndex;
|
||||||
extern bool fZindex;
|
extern bool fZindex;
|
||||||
extern bool fIsBareMultisigStd;
|
extern bool fIsBareMultisigStd;
|
||||||
@@ -930,6 +935,19 @@ extern CChain chainActive;
|
|||||||
/** Global variable that points to the active CCoinsView (protected by cs_main) */
|
/** Global variable that points to the active CCoinsView (protected by cs_main) */
|
||||||
extern CCoinsViewCache *pcoinsTip;
|
extern CCoinsViewCache *pcoinsTip;
|
||||||
|
|
||||||
|
/** Global variable that points to the coins database (chainstate/, protected by cs_main).
|
||||||
|
* Exposed for the UTXO-snapshot (assumeutxo-style) dump/load paths. */
|
||||||
|
class CCoinsViewDB;
|
||||||
|
extern CCoinsViewDB *pcoinsdbview;
|
||||||
|
|
||||||
|
/** Activate a trusted UTXO snapshot (already written to the chainstate DB by LoadSnapshot) as the
|
||||||
|
* chain tip at its height H, without replaying blocks 0..H. Headers for H must already exist. */
|
||||||
|
struct CUTXOSnapshotHeader;
|
||||||
|
bool LoadSnapshotChainstate(const CUTXOSnapshotHeader& header, std::string& strError);
|
||||||
|
/** Height H of a loaded UTXO snapshot (assumeutxo). Reorgs whose fork point is below H are refused
|
||||||
|
* because the node has no block/undo data for 0..H. -1 means no snapshot is in effect. */
|
||||||
|
extern int nAssumeutxoSnapshotHeight;
|
||||||
|
|
||||||
/** Global variable that points to the active block tree (protected by cs_main) */
|
/** Global variable that points to the active block tree (protected by cs_main) */
|
||||||
extern CBlockTreeDB *pblocktree;
|
extern CBlockTreeDB *pblocktree;
|
||||||
|
|
||||||
|
|||||||
217
src/pow.cpp
217
src/pow.cpp
@@ -18,6 +18,7 @@
|
|||||||
* *
|
* *
|
||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
#include "pow.h"
|
#include "pow.h"
|
||||||
|
#include "checkpoints.h"
|
||||||
#include "consensus/upgrades.h"
|
#include "consensus/upgrades.h"
|
||||||
#include "arith_uint256.h"
|
#include "arith_uint256.h"
|
||||||
#include "chain.h"
|
#include "chain.h"
|
||||||
@@ -30,6 +31,8 @@
|
|||||||
#include "sodium.h"
|
#include "sodium.h"
|
||||||
#include "RandomX/src/randomx.h"
|
#include "RandomX/src/randomx.h"
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
#include <boost/thread/shared_mutex.hpp>
|
||||||
|
#include <boost/thread/locks.hpp>
|
||||||
|
|
||||||
#ifdef ENABLE_RUST
|
#ifdef ENABLE_RUST
|
||||||
#include "librustzcash.h"
|
#include "librustzcash.h"
|
||||||
@@ -704,6 +707,7 @@ static std::mutex cs_randomx_validator;
|
|||||||
static randomx_cache *s_rxCache = nullptr;
|
static randomx_cache *s_rxCache = nullptr;
|
||||||
static randomx_vm *s_rxVM = nullptr;
|
static randomx_vm *s_rxVM = nullptr;
|
||||||
static std::string s_rxCurrentKey; // tracks current key to avoid re-init
|
static std::string s_rxCurrentKey; // tracks current key to avoid re-init
|
||||||
|
static int64_t nTimeRandomX = 0; // cumulative RandomX validation time (us), reported under -debug=bench
|
||||||
|
|
||||||
// Thread-local flag: skip CheckRandomXSolution when the miner is validating its own block
|
// Thread-local flag: skip CheckRandomXSolution when the miner is validating its own block
|
||||||
// The miner already computed the correct RandomX hash — re-verifying with a separate
|
// The miner already computed the correct RandomX hash — re-verifying with a separate
|
||||||
@@ -714,26 +718,70 @@ void SetSkipRandomXValidation(bool skip) { fSkipRandomXValidation = skip; }
|
|||||||
|
|
||||||
CBlockIndex *hush_chainactive(int32_t height);
|
CBlockIndex *hush_chainactive(int32_t height);
|
||||||
|
|
||||||
bool CheckRandomXSolution(const CBlockHeader *pblock, int32_t height)
|
// Centralized predicate: does a block at this height actually require a RandomX hash check?
|
||||||
|
// Shared by CheckRandomXSolution (inline path) and the parallel pre-verify pool so the two can
|
||||||
|
// never drift. Returns false when the recompute is unnecessary:
|
||||||
|
// - non-RandomX chain, or RandomX validation disabled (activation height < 0)
|
||||||
|
// - below the RandomX activation height (those blocks used Equihash, validated elsewhere)
|
||||||
|
// - during initial on-disk block loading / reindex (HUSH_LOADINGBLOCKS)
|
||||||
|
// - below the last hardcoded checkpoint (chain pinned by checkpoint hash + linkage + work)
|
||||||
|
// Deliberately does NOT consider the thread-local fSkipRandomXValidation (miner self-check) — that
|
||||||
|
// is a property of the calling thread, handled only in the inline CheckRandomXSolution below.
|
||||||
|
bool RandomXValidationRequired(int32_t height)
|
||||||
{
|
{
|
||||||
// Only applies to RandomX chains
|
|
||||||
if (ASSETCHAINS_ALGO != ASSETCHAINS_RANDOMX)
|
if (ASSETCHAINS_ALGO != ASSETCHAINS_RANDOMX)
|
||||||
return true;
|
return false;
|
||||||
|
|
||||||
// Disabled if activation height is negative
|
|
||||||
if (ASSETCHAINS_RANDOMX_VALIDATION < 0)
|
if (ASSETCHAINS_RANDOMX_VALIDATION < 0)
|
||||||
return true;
|
return false;
|
||||||
|
|
||||||
// Not yet at activation height
|
|
||||||
if (height < ASSETCHAINS_RANDOMX_VALIDATION)
|
if (height < ASSETCHAINS_RANDOMX_VALIDATION)
|
||||||
return true;
|
return false;
|
||||||
|
|
||||||
// Do not affect initial block loading
|
|
||||||
extern int32_t HUSH_LOADINGBLOCKS;
|
extern int32_t HUSH_LOADINGBLOCKS;
|
||||||
if (HUSH_LOADINGBLOCKS != 0)
|
if (HUSH_LOADINGBLOCKS != 0)
|
||||||
|
return false;
|
||||||
|
extern bool fCheckpointsEnabled;
|
||||||
|
if (fCheckpointsEnabled && height < Checkpoints::GetTotalBlocksEstimate(Params().Checkpoints()))
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize the RandomX hash input: the block header without nSolution (but with nNonce). Used by
|
||||||
|
// both the inline CheckRandomXSolution and the parallel pre-verify pool, so the bytes are identical.
|
||||||
|
std::vector<unsigned char> GetRandomXInput(const CBlockHeader& block)
|
||||||
|
{
|
||||||
|
CRandomXInput rxInput(block);
|
||||||
|
CDataStream ss(SER_NETWORK, PROTOCOL_VERSION);
|
||||||
|
ss << rxInput;
|
||||||
|
return std::vector<unsigned char>(ss.begin(), ss.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Derive the RandomX key string for a block at `height`. Below interval+lag it is the chain-params
|
||||||
|
// initial key; otherwise the block hash at the key-rotation height. MUST be called under cs_main
|
||||||
|
// (reads chainActive via hush_chainactive). Returns empty if the key-height block is unavailable.
|
||||||
|
std::string GetRandomXKey(int32_t height)
|
||||||
|
{
|
||||||
|
static int randomxInterval = GetRandomXInterval();
|
||||||
|
static int randomxBlockLag = GetRandomXBlockLag();
|
||||||
|
if (height < randomxInterval + randomxBlockLag) {
|
||||||
|
char initialKey[82];
|
||||||
|
snprintf(initialKey, 81, "%08x%s%08x", ASSETCHAINS_MAGIC, SMART_CHAIN_SYMBOL, ASSETCHAINS_RPCPORT);
|
||||||
|
return std::string(initialKey, strlen(initialKey));
|
||||||
|
}
|
||||||
|
int keyHeight = ((height - randomxBlockLag) / randomxInterval) * randomxInterval;
|
||||||
|
CBlockIndex *pKeyIndex = hush_chainactive(keyHeight);
|
||||||
|
if (pKeyIndex == nullptr)
|
||||||
|
return std::string();
|
||||||
|
uint256 blockKey = pKeyIndex->GetBlockHash();
|
||||||
|
return std::string((const char*)&blockKey, sizeof(blockKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CheckRandomXSolution(const CBlockHeader *pblock, int32_t height)
|
||||||
|
{
|
||||||
|
// Centralized height gate (shared with the parallel pre-verify pool, Stage 0).
|
||||||
|
if (!RandomXValidationRequired(height))
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
// Skip when miner is validating its own block via TestBlockValidity
|
// Skip when the miner is validating its own freshly-mined block via TestBlockValidity
|
||||||
|
// (thread-local; never set on the connect thread or the pre-verify worker threads).
|
||||||
if (fSkipRandomXValidation)
|
if (fSkipRandomXValidation)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
@@ -743,47 +791,44 @@ bool CheckRandomXSolution(const CBlockHeader *pblock, int32_t height)
|
|||||||
pblock->nSolution.size(), RANDOMX_HASH_SIZE, height);
|
pblock->nSolution.size(), RANDOMX_HASH_SIZE, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
static int randomxInterval = GetRandomXInterval();
|
// Derive the key (shared helper) and serialize the input (identical bytes to the pool path).
|
||||||
static int randomxBlockLag = GetRandomXBlockLag();
|
std::string rxKey = GetRandomXKey(height);
|
||||||
|
if (rxKey.empty())
|
||||||
// Determine the correct RandomX key for this height
|
return error("CheckRandomXSolution(): cannot derive RandomX key for height %d", height);
|
||||||
char initialKey[82];
|
std::vector<unsigned char> ssInput = GetRandomXInput(*pblock);
|
||||||
snprintf(initialKey, 81, "%08x%s%08x", ASSETCHAINS_MAGIC, SMART_CHAIN_SYMBOL, ASSETCHAINS_RPCPORT);
|
|
||||||
|
|
||||||
std::string rxKey;
|
|
||||||
if (height < randomxInterval + randomxBlockLag) {
|
|
||||||
// Use initial key derived from chain params
|
|
||||||
rxKey = std::string(initialKey, strlen(initialKey));
|
|
||||||
} else {
|
|
||||||
// Use block hash at the key height
|
|
||||||
int keyHeight = ((height - randomxBlockLag) / randomxInterval) * randomxInterval;
|
|
||||||
CBlockIndex *pKeyIndex = hush_chainactive(keyHeight);
|
|
||||||
if (pKeyIndex == nullptr) {
|
|
||||||
return error("CheckRandomXSolution(): cannot get block index at key height %d for block %d", keyHeight, height);
|
|
||||||
}
|
|
||||||
uint256 blockKey = pKeyIndex->GetBlockHash();
|
|
||||||
rxKey = std::string((const char*)&blockKey, sizeof(blockKey));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Serialize the block header without nSolution (but with nNonce) as RandomX input
|
|
||||||
CRandomXInput rxInput(*pblock);
|
|
||||||
CDataStream ss(SER_NETWORK, PROTOCOL_VERSION);
|
|
||||||
ss << rxInput;
|
|
||||||
|
|
||||||
char computedHash[RANDOMX_HASH_SIZE];
|
char computedHash[RANDOMX_HASH_SIZE];
|
||||||
|
|
||||||
|
// Measurement (Track 1): isolate RandomX verification cost during IBD. The
|
||||||
|
// expensive parts are the per-key cache (re)init (~every GetRandomXInterval()
|
||||||
|
// blocks) and the hash computation itself; both happen under the lock below.
|
||||||
|
int64_t nTimeRxStart = GetTimeMicros();
|
||||||
|
bool fKeyInit = false;
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(cs_randomx_validator);
|
std::lock_guard<std::mutex> lock(cs_randomx_validator);
|
||||||
|
|
||||||
// Initialize cache + VM if needed, or re-init if key changed
|
// Initialize cache + VM if needed, or re-init if key changed
|
||||||
if (s_rxCache == nullptr) {
|
if (s_rxCache == nullptr) {
|
||||||
randomx_flags flags = randomx_get_flags();
|
randomx_flags flags = randomx_get_flags();
|
||||||
s_rxCache = randomx_alloc_cache(flags);
|
// Try large pages for the 256MB validator cache: fewer TLB misses → ~15-30% faster
|
||||||
|
// light-mode validation where the OS has hugepages configured. Falls back transparently
|
||||||
|
// when unavailable, exactly as the miner does (miner.cpp:1097). Page size does not affect
|
||||||
|
// the computed hash, so this is consensus-neutral.
|
||||||
|
bool fLargePages = true;
|
||||||
|
s_rxCache = randomx_alloc_cache(flags | RANDOMX_FLAG_LARGE_PAGES);
|
||||||
|
if (s_rxCache == nullptr) {
|
||||||
|
fLargePages = false;
|
||||||
|
s_rxCache = randomx_alloc_cache(flags);
|
||||||
|
}
|
||||||
if (s_rxCache == nullptr) {
|
if (s_rxCache == nullptr) {
|
||||||
return error("CheckRandomXSolution(): failed to allocate RandomX cache");
|
return error("CheckRandomXSolution(): failed to allocate RandomX cache");
|
||||||
}
|
}
|
||||||
|
// Confirm the fast paths are active (JIT off would be ~9x slower; see randomx-benchmark).
|
||||||
|
LogPrint("bench", "CheckRandomXSolution: RandomX flags=0x%x JIT=%d HARD_AES=%d largePages=%d\n",
|
||||||
|
(unsigned int)flags, !!(flags & RANDOMX_FLAG_JIT), !!(flags & RANDOMX_FLAG_HARD_AES), (int)fLargePages);
|
||||||
randomx_init_cache(s_rxCache, rxKey.data(), rxKey.size());
|
randomx_init_cache(s_rxCache, rxKey.data(), rxKey.size());
|
||||||
s_rxCurrentKey = rxKey;
|
s_rxCurrentKey = rxKey;
|
||||||
|
fKeyInit = true;
|
||||||
s_rxVM = randomx_create_vm(flags, s_rxCache, nullptr);
|
s_rxVM = randomx_create_vm(flags, s_rxCache, nullptr);
|
||||||
if (s_rxVM == nullptr) {
|
if (s_rxVM == nullptr) {
|
||||||
randomx_release_cache(s_rxCache);
|
randomx_release_cache(s_rxCache);
|
||||||
@@ -793,11 +838,17 @@ bool CheckRandomXSolution(const CBlockHeader *pblock, int32_t height)
|
|||||||
} else if (s_rxCurrentKey != rxKey) {
|
} else if (s_rxCurrentKey != rxKey) {
|
||||||
randomx_init_cache(s_rxCache, rxKey.data(), rxKey.size());
|
randomx_init_cache(s_rxCache, rxKey.data(), rxKey.size());
|
||||||
s_rxCurrentKey = rxKey;
|
s_rxCurrentKey = rxKey;
|
||||||
|
fKeyInit = true;
|
||||||
randomx_vm_set_cache(s_rxVM, s_rxCache);
|
randomx_vm_set_cache(s_rxVM, s_rxCache);
|
||||||
}
|
}
|
||||||
|
|
||||||
randomx_calculate_hash(s_rxVM, &ss[0], ss.size(), computedHash);
|
randomx_calculate_hash(s_rxVM, ssInput.data(), ssInput.size(), computedHash);
|
||||||
}
|
}
|
||||||
|
int64_t nTimeRxEnd = GetTimeMicros();
|
||||||
|
nTimeRandomX += nTimeRxEnd - nTimeRxStart;
|
||||||
|
LogPrint("bench", " - RandomX verify ht=%d: %.2fms%s [%.2fs]\n",
|
||||||
|
height, (nTimeRxEnd - nTimeRxStart) * 0.001,
|
||||||
|
fKeyInit ? " (key-init)" : "", nTimeRandomX * 0.000001);
|
||||||
|
|
||||||
// Compare computed hash against nSolution
|
// Compare computed hash against nSolution
|
||||||
if (memcmp(computedHash, pblock->nSolution.data(), RANDOMX_HASH_SIZE) != 0) {
|
if (memcmp(computedHash, pblock->nSolution.data(), RANDOMX_HASH_SIZE) != 0) {
|
||||||
@@ -814,7 +865,7 @@ bool CheckRandomXSolution(const CBlockHeader *pblock, int32_t height)
|
|||||||
fprintf(stderr, " computed : %s\n", computedHex.c_str());
|
fprintf(stderr, " computed : %s\n", computedHex.c_str());
|
||||||
fprintf(stderr, " nSolution: %s\n", solutionHex.c_str());
|
fprintf(stderr, " nSolution: %s\n", solutionHex.c_str());
|
||||||
fprintf(stderr, " rxKey size=%lu, input size=%lu, nNonce=%s\n",
|
fprintf(stderr, " rxKey size=%lu, input size=%lu, nNonce=%s\n",
|
||||||
rxKey.size(), ss.size(), pblock->nNonce.ToString().c_str());
|
rxKey.size(), ssInput.size(), pblock->nNonce.ToString().c_str());
|
||||||
fprintf(stderr, " nSolution.size()=%lu, RANDOMX_HASH_SIZE=%d\n",
|
fprintf(stderr, " nSolution.size()=%lu, RANDOMX_HASH_SIZE=%d\n",
|
||||||
pblock->nSolution.size(), RANDOMX_HASH_SIZE);
|
pblock->nSolution.size(), RANDOMX_HASH_SIZE);
|
||||||
// Also log to debug.log
|
// Also log to debug.log
|
||||||
@@ -822,7 +873,7 @@ bool CheckRandomXSolution(const CBlockHeader *pblock, int32_t height)
|
|||||||
LogPrintf(" computed : %s\n", computedHex);
|
LogPrintf(" computed : %s\n", computedHex);
|
||||||
LogPrintf(" nSolution: %s\n", solutionHex);
|
LogPrintf(" nSolution: %s\n", solutionHex);
|
||||||
LogPrintf(" rxKey size=%lu, input size=%lu, nNonce=%s\n",
|
LogPrintf(" rxKey size=%lu, input size=%lu, nNonce=%s\n",
|
||||||
rxKey.size(), ss.size(), pblock->nNonce.ToString());
|
rxKey.size(), ssInput.size(), pblock->nNonce.ToString());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -830,6 +881,88 @@ bool CheckRandomXSolution(const CBlockHeader *pblock, int32_t height)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================================
|
||||||
|
// Parallel RandomX pre-verification pool (Stage 2).
|
||||||
|
// One shared light-mode cache (holding a single key at a time) + per-thread VMs, mirroring the
|
||||||
|
// miner's RandomXDatasetManager pattern (miner.cpp). The connect thread (ActivateBestChainStep)
|
||||||
|
// loads the cache key for a same-key group of about-to-be-connected blocks, dispatches them to
|
||||||
|
// this pool, and barrier-waits; each worker hashes on its own VM (sharing the read-only cache)
|
||||||
|
// and, on a match, sets the block's transient fRandomXVerified flag so the inline check in
|
||||||
|
// CheckBlockHeader can be skipped. The inline path remains the consensus authority for anything
|
||||||
|
// not pre-verified, so the pool can only ever flip false->true on a real hash match.
|
||||||
|
static boost::shared_mutex g_rxvMutex; // shared = hashing; exclusive = cache (re)init
|
||||||
|
static randomx_cache* g_rxvCache = nullptr; // shared, read-only during hashing
|
||||||
|
static std::string g_rxvKey; // key currently loaded into g_rxvCache
|
||||||
|
static randomx_flags g_rxvFlags;
|
||||||
|
static thread_local randomx_vm* tls_rxvVM = nullptr;
|
||||||
|
static thread_local std::string tls_rxvVMKey;
|
||||||
|
|
||||||
|
CCheckQueue<CRandomXCheck> rxCheckQueue(1); // batch size 1: each item is ~tens of ms
|
||||||
|
|
||||||
|
bool RandomXValidatorPrepareKey(const std::string& rxKey)
|
||||||
|
{
|
||||||
|
boost::unique_lock<boost::shared_mutex> lock(g_rxvMutex);
|
||||||
|
if (g_rxvCache == nullptr) {
|
||||||
|
g_rxvFlags = randomx_get_flags();
|
||||||
|
g_rxvCache = randomx_alloc_cache(g_rxvFlags | RANDOMX_FLAG_LARGE_PAGES);
|
||||||
|
if (g_rxvCache == nullptr)
|
||||||
|
g_rxvCache = randomx_alloc_cache(g_rxvFlags);
|
||||||
|
if (g_rxvCache == nullptr) {
|
||||||
|
LogPrintf("RandomXValidatorPrepareKey: cache alloc failed; parallel pre-verify disabled\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
randomx_init_cache(g_rxvCache, rxKey.data(), rxKey.size());
|
||||||
|
g_rxvKey = rxKey;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (g_rxvKey != rxKey) {
|
||||||
|
randomx_init_cache(g_rxvCache, rxKey.data(), rxKey.size());
|
||||||
|
g_rxvKey = rxKey;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CRandomXCheck::operator()()
|
||||||
|
{
|
||||||
|
boost::shared_lock<boost::shared_mutex> lock(g_rxvMutex);
|
||||||
|
// The connect thread set the shared cache to one key before dispatching this group. If this
|
||||||
|
// item's key doesn't match (e.g. a key-rotation straggler) or the cache is unavailable, skip it
|
||||||
|
// and leave *presult false — the inline CheckRandomXSolution will verify it.
|
||||||
|
if (g_rxvCache == nullptr || g_rxvKey != rxKey)
|
||||||
|
return true;
|
||||||
|
if (tls_rxvVM == nullptr) {
|
||||||
|
tls_rxvVM = randomx_create_vm(g_rxvFlags, g_rxvCache, nullptr);
|
||||||
|
if (tls_rxvVM == nullptr)
|
||||||
|
return true; // cannot verify here -> inline fallback
|
||||||
|
tls_rxvVMKey = g_rxvKey;
|
||||||
|
} else if (tls_rxvVMKey != g_rxvKey) {
|
||||||
|
// Cache was re-initialized to a new key since this VM last ran; rebind.
|
||||||
|
randomx_vm_set_cache(tls_rxvVM, g_rxvCache);
|
||||||
|
tls_rxvVMKey = g_rxvKey;
|
||||||
|
}
|
||||||
|
unsigned char h[RANDOMX_HASH_SIZE];
|
||||||
|
randomx_calculate_hash(tls_rxvVM, input.data(), input.size(), h);
|
||||||
|
if (memcmp(h, expected, RANDOMX_HASH_SIZE) == 0 && presult != nullptr)
|
||||||
|
*presult = true;
|
||||||
|
return true; // ALWAYS true: never short-circuit the queue; per-block result is in *presult
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThreadRandomXVerify()
|
||||||
|
{
|
||||||
|
RenameThread("hush-rxverify");
|
||||||
|
rxCheckQueue.Thread();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RandomXValidatorShutdown()
|
||||||
|
{
|
||||||
|
boost::unique_lock<boost::shared_mutex> lock(g_rxvMutex);
|
||||||
|
// Per-thread VMs are intentionally leaked (process exiting); release the shared cache.
|
||||||
|
if (g_rxvCache != nullptr) {
|
||||||
|
randomx_release_cache(g_rxvCache);
|
||||||
|
g_rxvCache = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int32_t hush_chosennotary(int32_t *notaryidp,int32_t height,uint8_t *pubkey33,uint32_t timestamp);
|
int32_t hush_chosennotary(int32_t *notaryidp,int32_t height,uint8_t *pubkey33,uint32_t timestamp);
|
||||||
int32_t hush_currentheight();
|
int32_t hush_currentheight();
|
||||||
void hush_index2pubkey33(uint8_t *pubkey33,CBlockIndex *pindex,int32_t height);
|
void hush_index2pubkey33(uint8_t *pubkey33,CBlockIndex *pindex,int32_t height);
|
||||||
|
|||||||
54
src/pow.h
54
src/pow.h
@@ -21,8 +21,13 @@
|
|||||||
#define HUSH_POW_H
|
#define HUSH_POW_H
|
||||||
|
|
||||||
#include "chain.h"
|
#include "chain.h"
|
||||||
|
#include "checkqueue.h"
|
||||||
#include "consensus/params.h"
|
#include "consensus/params.h"
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
class CBlockHeader;
|
class CBlockHeader;
|
||||||
class CBlockIndex;
|
class CBlockIndex;
|
||||||
@@ -41,6 +46,55 @@ bool CheckEquihashSolution(const CBlockHeader *pblock, const CChainParams&);
|
|||||||
/** Check whether a block header contains a valid RandomX solution */
|
/** Check whether a block header contains a valid RandomX solution */
|
||||||
bool CheckRandomXSolution(const CBlockHeader *pblock, int32_t height);
|
bool CheckRandomXSolution(const CBlockHeader *pblock, int32_t height);
|
||||||
|
|
||||||
|
/** Whether a block at this height requires a RandomX hash check (shared gate used by both the
|
||||||
|
* inline CheckRandomXSolution and the parallel pre-verification pool). */
|
||||||
|
bool RandomXValidationRequired(int32_t height);
|
||||||
|
|
||||||
|
/** Serialize the RandomX hash input (block header without nSolution) — identical bytes to the
|
||||||
|
* inline CheckRandomXSolution path, so the parallel pool computes the same hash. */
|
||||||
|
std::vector<unsigned char> GetRandomXInput(const CBlockHeader& block);
|
||||||
|
|
||||||
|
/** Derive the RandomX key string for a block at `height`. MUST be called under cs_main (reads
|
||||||
|
* chainActive). Returns empty string if the key-height block is unavailable. */
|
||||||
|
std::string GetRandomXKey(int32_t height);
|
||||||
|
|
||||||
|
/** A single RandomX pre-verification work item for the parallel validator pool. Pure value type
|
||||||
|
* (no chainstate pointers) so workers need no cs_main. On a hash match it sets *presult=true; on
|
||||||
|
* any failure it leaves *presult untouched — the inline CheckRandomXSolution remains the
|
||||||
|
* consensus authority and re-verifies anything not pre-verified. operator() ALWAYS returns true,
|
||||||
|
* so one block's failure never short-circuits the rest of the CCheckQueue batch. */
|
||||||
|
class CRandomXCheck
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
std::string rxKey; // RandomX key for this block's height
|
||||||
|
std::vector<unsigned char> input; // serialized CRandomXInput(header)
|
||||||
|
unsigned char expected[32]; // block.nSolution (claimed RandomX hash)
|
||||||
|
bool* presult; // -> pindex->fRandomXVerified (set true only on a hash match)
|
||||||
|
public:
|
||||||
|
CRandomXCheck() : presult(nullptr) { memset(expected, 0, sizeof(expected)); }
|
||||||
|
CRandomXCheck(const std::string& keyIn, std::vector<unsigned char> inputIn,
|
||||||
|
const unsigned char* expectedIn, bool* presultIn)
|
||||||
|
: rxKey(keyIn), input(std::move(inputIn)), presult(presultIn)
|
||||||
|
{ memcpy(expected, expectedIn, sizeof(expected)); }
|
||||||
|
bool operator()();
|
||||||
|
void swap(CRandomXCheck& c) {
|
||||||
|
rxKey.swap(c.rxKey);
|
||||||
|
input.swap(c.input);
|
||||||
|
std::swap(presult, c.presult);
|
||||||
|
for (int i = 0; i < 32; i++) std::swap(expected[i], c.expected[i]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** The RandomX pre-verification check queue (parallel pool). */
|
||||||
|
extern CCheckQueue<CRandomXCheck> rxCheckQueue;
|
||||||
|
/** Worker entry point (spawn N at startup, mirrors ThreadScriptCheck). */
|
||||||
|
void ThreadRandomXVerify();
|
||||||
|
/** Load `rxKey` into the shared validator cache (alloc on first use); call before dispatching a
|
||||||
|
* same-key group of checks. Returns false on allocation failure. */
|
||||||
|
bool RandomXValidatorPrepareKey(const std::string& rxKey);
|
||||||
|
/** Release the shared validator cache at shutdown. */
|
||||||
|
void RandomXValidatorShutdown();
|
||||||
|
|
||||||
/** Set thread-local flag to skip RandomX validation (used by miner during TestBlockValidity) */
|
/** Set thread-local flag to skip RandomX validation (used by miner during TestBlockValidity) */
|
||||||
void SetSkipRandomXValidation(bool skip);
|
void SetSkipRandomXValidation(bool skip);
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,9 @@
|
|||||||
#include "rpc/server.h"
|
#include "rpc/server.h"
|
||||||
#include "streams.h"
|
#include "streams.h"
|
||||||
#include "sync.h"
|
#include "sync.h"
|
||||||
|
#include "txdb.h"
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
|
#include <boost/filesystem.hpp>
|
||||||
#include "script/script.h"
|
#include "script/script.h"
|
||||||
#include "script/script_error.h"
|
#include "script/script_error.h"
|
||||||
#include "script/sign.h"
|
#include "script/sign.h"
|
||||||
@@ -860,6 +862,77 @@ UniValue gettxoutsetinfo(const UniValue& params, bool fHelp, const CPubKey& mypk
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UniValue dumptxoutset(const UniValue& params, bool fHelp, const CPubKey& mypk)
|
||||||
|
{
|
||||||
|
if (fHelp || params.size() != 1)
|
||||||
|
throw runtime_error(
|
||||||
|
"dumptxoutset \"path\"\n"
|
||||||
|
"\nWrite a trusted snapshot of the current chainstate (UTXO set + Sapling commitment\n"
|
||||||
|
"trees, nullifier set and pool value) to disk. The snapshot can be loaded by a fresh\n"
|
||||||
|
"node with -loadutxosnapshot=<file> to skip replaying the chain from genesis.\n"
|
||||||
|
"\nThis is intended to be run at a final/checkpoint height; the node must be fully synced.\n"
|
||||||
|
"\nArguments:\n"
|
||||||
|
"1. \"path\" (string, required) path to write the snapshot file (must not already exist)\n"
|
||||||
|
"\nResult:\n"
|
||||||
|
"{\n"
|
||||||
|
" \"height\": n, (numeric) snapshot height H\n"
|
||||||
|
" \"base_hash\": \"hex\", (string) block hash at height H\n"
|
||||||
|
" \"snapshot_hash\": \"hex\", (string) content hash to hardcode for verification\n"
|
||||||
|
" \"coins\": n, (numeric) number of UTXO records\n"
|
||||||
|
" \"sapling_anchors\": n, (numeric) number of Sapling anchor records\n"
|
||||||
|
" \"sapling_nullifiers\": n, (numeric) number of Sapling nullifier records\n"
|
||||||
|
" \"path\": \"...\" (string) the file written\n"
|
||||||
|
"}\n"
|
||||||
|
"\nExamples:\n"
|
||||||
|
+ HelpExampleCli("dumptxoutset", "/path/to/dragonx-utxo.dat")
|
||||||
|
+ HelpExampleRpc("dumptxoutset", "\"/path/to/dragonx-utxo.dat\"")
|
||||||
|
);
|
||||||
|
|
||||||
|
boost::filesystem::path path = boost::filesystem::absolute(params[0].get_str());
|
||||||
|
if (boost::filesystem::exists(path))
|
||||||
|
throw JSONRPCError(RPC_INVALID_PARAMETER, "path already exists, refusing to overwrite: " + path.string());
|
||||||
|
|
||||||
|
LOCK(cs_main);
|
||||||
|
|
||||||
|
if (pcoinsdbview == nullptr || pcoinsTip == nullptr)
|
||||||
|
throw JSONRPCError(RPC_INTERNAL_ERROR, "chainstate not available");
|
||||||
|
|
||||||
|
// Flush so the on-disk chainstate matches the in-memory tip before we iterate it.
|
||||||
|
FlushStateToDisk();
|
||||||
|
|
||||||
|
CBlockIndex *tip = chainActive.Tip();
|
||||||
|
if (tip == nullptr)
|
||||||
|
throw JSONRPCError(RPC_INTERNAL_ERROR, "no chain tip");
|
||||||
|
|
||||||
|
CUTXOSnapshotHeader header;
|
||||||
|
header.nMagic = UTXO_SNAPSHOT_MAGIC;
|
||||||
|
header.nVersion = UTXO_SNAPSHOT_VERSION;
|
||||||
|
memcpy(&header.nNetworkMagic, Params().MessageStart(), 4);
|
||||||
|
header.baseBlockHash = tip->GetBlockHash();
|
||||||
|
header.nHeight = tip->GetHeight();
|
||||||
|
header.nChainTx = tip->nChainTx;
|
||||||
|
if (tip->nChainSaplingValue) {
|
||||||
|
header.fHasChainSaplingValue = 1;
|
||||||
|
header.nChainSaplingValue = *tip->nChainSaplingValue;
|
||||||
|
}
|
||||||
|
header.bestSaplingAnchor = pcoinsdbview->GetBestAnchor(SAPLING);
|
||||||
|
|
||||||
|
uint256 snapshotHash;
|
||||||
|
std::string strError;
|
||||||
|
if (!pcoinsdbview->DumpSnapshot(path.string(), header, snapshotHash, strError))
|
||||||
|
throw JSONRPCError(RPC_INTERNAL_ERROR, "dumptxoutset failed: " + strError);
|
||||||
|
|
||||||
|
UniValue ret(UniValue::VOBJ);
|
||||||
|
ret.push_back(Pair("height", (int64_t)header.nHeight));
|
||||||
|
ret.push_back(Pair("base_hash", header.baseBlockHash.GetHex()));
|
||||||
|
ret.push_back(Pair("snapshot_hash", snapshotHash.GetHex()));
|
||||||
|
ret.push_back(Pair("coins", (int64_t)header.nCoins));
|
||||||
|
ret.push_back(Pair("sapling_anchors", (int64_t)header.nSaplingAnchors));
|
||||||
|
ret.push_back(Pair("sapling_nullifiers", (int64_t)header.nSaplingNullifiers));
|
||||||
|
ret.push_back(Pair("path", path.string()));
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
UniValue getblockmerkletree(const UniValue& params, bool fHelp, const CPubKey& mypk)
|
UniValue getblockmerkletree(const UniValue& params, bool fHelp, const CPubKey& mypk)
|
||||||
{
|
{
|
||||||
if (fHelp || params.size() != 1 )
|
if (fHelp || params.size() != 1 )
|
||||||
@@ -1851,6 +1924,7 @@ static const CRPCCommand commands[] =
|
|||||||
{ "blockchain", "getrawmempool", &getrawmempool, true },
|
{ "blockchain", "getrawmempool", &getrawmempool, true },
|
||||||
{ "blockchain", "gettxout", &gettxout, true },
|
{ "blockchain", "gettxout", &gettxout, true },
|
||||||
{ "blockchain", "gettxoutsetinfo", &gettxoutsetinfo, true },
|
{ "blockchain", "gettxoutsetinfo", &gettxoutsetinfo, true },
|
||||||
|
{ "blockchain", "dumptxoutset", &dumptxoutset, true },
|
||||||
{ "blockchain", "verifychain", &verifychain, true },
|
{ "blockchain", "verifychain", &verifychain, true },
|
||||||
|
|
||||||
/* Not shown in help */
|
/* Not shown in help */
|
||||||
|
|||||||
237
src/txdb.cpp
237
src/txdb.cpp
@@ -21,9 +21,11 @@
|
|||||||
|
|
||||||
#include "txdb.h"
|
#include "txdb.h"
|
||||||
#include "chainparams.h"
|
#include "chainparams.h"
|
||||||
|
#include "clientversion.h"
|
||||||
#include "hash.h"
|
#include "hash.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "pow.h"
|
#include "pow.h"
|
||||||
|
#include "streams.h"
|
||||||
#include "uint256.h"
|
#include "uint256.h"
|
||||||
#include "core_io.h"
|
#include "core_io.h"
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
@@ -269,6 +271,233 @@ bool CCoinsViewDB::GetStats(CCoinsStats &stats) const {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper: count entries in the coins DB whose key prefix matches `prefix`.
|
||||||
|
// LevelDB returns keys in sorted order, so iteration is deterministic across nodes.
|
||||||
|
static uint64_t CountByPrefix(CDBWrapper &db, char prefix)
|
||||||
|
{
|
||||||
|
boost::scoped_ptr<CDBIterator> pcursor(db.NewIterator());
|
||||||
|
uint64_t n = 0;
|
||||||
|
for (pcursor->Seek(prefix); pcursor->Valid(); pcursor->Next()) {
|
||||||
|
boost::this_thread::interruption_point();
|
||||||
|
std::pair<char, uint256> key;
|
||||||
|
if (pcursor->GetKey(key) && key.first == prefix) n++;
|
||||||
|
else break;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CCoinsViewDB::DumpSnapshot(const std::string &path, CUTXOSnapshotHeader &header, uint256 &hashRet, std::string &strError) const
|
||||||
|
{
|
||||||
|
CDBWrapper *pdb = const_cast<CDBWrapper*>(&db);
|
||||||
|
|
||||||
|
// Counting pass (caller holds cs_main and has flushed, so the set is stable).
|
||||||
|
header.nCoins = CountByPrefix(*pdb, DB_COINS);
|
||||||
|
header.nSaplingAnchors = CountByPrefix(*pdb, DB_SAPLING_ANCHOR);
|
||||||
|
header.nSaplingNullifiers = CountByPrefix(*pdb, DB_SAPLING_NULLIFIER);
|
||||||
|
|
||||||
|
FILE *f = fopen(path.c_str(), "wb");
|
||||||
|
if (f == nullptr) { strError = "cannot open snapshot file for writing: " + path; return false; }
|
||||||
|
CAutoFile fileout(f, SER_DISK, CLIENT_VERSION);
|
||||||
|
|
||||||
|
// The content hash is computed over the same logical object stream the loader will
|
||||||
|
// reconstruct, so producer and consumer agree regardless of on-disk encoding.
|
||||||
|
CHashWriter hasher(SER_GETHASH, PROTOCOL_VERSION);
|
||||||
|
|
||||||
|
fileout << header;
|
||||||
|
hasher << header;
|
||||||
|
|
||||||
|
// Coins ('c')
|
||||||
|
{
|
||||||
|
boost::scoped_ptr<CDBIterator> pcursor(pdb->NewIterator());
|
||||||
|
uint64_t n = 0;
|
||||||
|
for (pcursor->Seek(DB_COINS); pcursor->Valid(); pcursor->Next()) {
|
||||||
|
boost::this_thread::interruption_point();
|
||||||
|
std::pair<char, uint256> key;
|
||||||
|
CCoins coins;
|
||||||
|
if (pcursor->GetKey(key) && key.first == DB_COINS) {
|
||||||
|
if (!pcursor->GetValue(coins)) { strError = "failed reading coins record"; return false; }
|
||||||
|
fileout << key.second; hasher << key.second;
|
||||||
|
fileout << coins; hasher << coins;
|
||||||
|
n++;
|
||||||
|
} else break;
|
||||||
|
}
|
||||||
|
if (n != header.nCoins) { strError = "coin count changed during dump"; return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sapling anchors ('Z') — the commitment trees referenced by spends above H.
|
||||||
|
{
|
||||||
|
boost::scoped_ptr<CDBIterator> pcursor(pdb->NewIterator());
|
||||||
|
uint64_t n = 0;
|
||||||
|
for (pcursor->Seek(DB_SAPLING_ANCHOR); pcursor->Valid(); pcursor->Next()) {
|
||||||
|
boost::this_thread::interruption_point();
|
||||||
|
std::pair<char, uint256> key;
|
||||||
|
SaplingMerkleTree tree;
|
||||||
|
if (pcursor->GetKey(key) && key.first == DB_SAPLING_ANCHOR) {
|
||||||
|
if (!pcursor->GetValue(tree)) { strError = "failed reading sapling anchor"; return false; }
|
||||||
|
fileout << key.second; hasher << key.second;
|
||||||
|
fileout << tree; hasher << tree;
|
||||||
|
n++;
|
||||||
|
} else break;
|
||||||
|
}
|
||||||
|
if (n != header.nSaplingAnchors) { strError = "sapling anchor count changed during dump"; return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sapling nullifiers ('S') — spent markers; value is always true, so only the key matters.
|
||||||
|
{
|
||||||
|
boost::scoped_ptr<CDBIterator> pcursor(pdb->NewIterator());
|
||||||
|
uint64_t n = 0;
|
||||||
|
for (pcursor->Seek(DB_SAPLING_NULLIFIER); pcursor->Valid(); pcursor->Next()) {
|
||||||
|
boost::this_thread::interruption_point();
|
||||||
|
std::pair<char, uint256> key;
|
||||||
|
if (pcursor->GetKey(key) && key.first == DB_SAPLING_NULLIFIER) {
|
||||||
|
fileout << key.second; hasher << key.second;
|
||||||
|
n++;
|
||||||
|
} else break;
|
||||||
|
}
|
||||||
|
if (n != header.nSaplingNullifiers) { strError = "sapling nullifier count changed during dump"; return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
hashRet = hasher.GetHash();
|
||||||
|
fileout << hashRet; // trailing content hash (not fed into the hasher)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CCoinsViewDB::LoadSnapshot(const std::string &path, const uint256 &expectedHash, bool fRequireExpected,
|
||||||
|
CUTXOSnapshotHeader &headerRet, uint256 &hashRet, std::string &strError)
|
||||||
|
{
|
||||||
|
uint32_t netmagic = 0;
|
||||||
|
memcpy(&netmagic, Params().MessageStart(), 4);
|
||||||
|
|
||||||
|
// ---- Pass 1: read + verify integrity (and the trusted hash) WITHOUT writing to the DB ----
|
||||||
|
CUTXOSnapshotHeader header;
|
||||||
|
uint256 computed;
|
||||||
|
{
|
||||||
|
FILE *f = fopen(path.c_str(), "rb");
|
||||||
|
if (f == nullptr) { strError = "cannot open snapshot file: " + path; return false; }
|
||||||
|
CAutoFile filein(f, SER_DISK, CLIENT_VERSION);
|
||||||
|
CHashWriter hasher(SER_GETHASH, PROTOCOL_VERSION);
|
||||||
|
try {
|
||||||
|
filein >> header; hasher << header;
|
||||||
|
if (header.nMagic != UTXO_SNAPSHOT_MAGIC) { strError = "not a DragonX UTXO snapshot (bad magic)"; return false; }
|
||||||
|
if (header.nVersion != UTXO_SNAPSHOT_VERSION) { strError = "unsupported snapshot version"; return false; }
|
||||||
|
if (header.nNetworkMagic != netmagic) { strError = "snapshot is for a different network"; return false; }
|
||||||
|
|
||||||
|
for (uint64_t i = 0; i < header.nCoins; i++) {
|
||||||
|
boost::this_thread::interruption_point();
|
||||||
|
uint256 txid; CCoins coins;
|
||||||
|
filein >> txid; filein >> coins;
|
||||||
|
hasher << txid; hasher << coins;
|
||||||
|
}
|
||||||
|
for (uint64_t i = 0; i < header.nSaplingAnchors; i++) {
|
||||||
|
boost::this_thread::interruption_point();
|
||||||
|
uint256 root; SaplingMerkleTree tree;
|
||||||
|
filein >> root; filein >> tree;
|
||||||
|
hasher << root; hasher << tree;
|
||||||
|
}
|
||||||
|
for (uint64_t i = 0; i < header.nSaplingNullifiers; i++) {
|
||||||
|
boost::this_thread::interruption_point();
|
||||||
|
uint256 nf;
|
||||||
|
filein >> nf;
|
||||||
|
hasher << nf;
|
||||||
|
}
|
||||||
|
uint256 stored;
|
||||||
|
filein >> stored;
|
||||||
|
computed = hasher.GetHash();
|
||||||
|
if (computed != stored) { strError = "snapshot content hash mismatch (corrupt or truncated)"; return false; }
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
strError = std::string("error reading snapshot: ") + e.what();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fRequireExpected && computed != expectedHash) {
|
||||||
|
strError = "snapshot hash does not match the trusted value hardcoded for this network";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
hashRet = computed;
|
||||||
|
headerRet = header;
|
||||||
|
|
||||||
|
// ---- Pass 2: apply to the (empty) chainstate DB in bounded batches ----
|
||||||
|
const size_t CHUNK = 100000;
|
||||||
|
CCoinsMap mapCoins;
|
||||||
|
CAnchorsSproutMap mapSproutAnchors; // unused on this chain, always empty
|
||||||
|
CAnchorsSaplingMap mapSaplingAnchors;
|
||||||
|
CNullifiersMap mapSproutNullifiers; // unused, always empty
|
||||||
|
CNullifiersMap mapSaplingNullifiers;
|
||||||
|
{
|
||||||
|
FILE *f = fopen(path.c_str(), "rb");
|
||||||
|
if (f == nullptr) { strError = "cannot reopen snapshot file: " + path; return false; }
|
||||||
|
CAutoFile filein(f, SER_DISK, CLIENT_VERSION);
|
||||||
|
try {
|
||||||
|
CUTXOSnapshotHeader hdr2;
|
||||||
|
filein >> hdr2; // header already validated in pass 1
|
||||||
|
|
||||||
|
for (uint64_t i = 0; i < header.nCoins; i++) {
|
||||||
|
boost::this_thread::interruption_point();
|
||||||
|
uint256 txid; CCoins coins;
|
||||||
|
filein >> txid; filein >> coins;
|
||||||
|
CCoinsCacheEntry &e = mapCoins[txid];
|
||||||
|
e.coins = coins;
|
||||||
|
e.flags = CCoinsCacheEntry::DIRTY;
|
||||||
|
if (mapCoins.size() >= CHUNK) {
|
||||||
|
if (!BatchWrite(mapCoins, uint256(), uint256(), uint256(), mapSproutAnchors, mapSaplingAnchors, mapSproutNullifiers, mapSaplingNullifiers))
|
||||||
|
{ strError = "batch write failed (coins)"; return false; }
|
||||||
|
mapCoins.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!mapCoins.empty()) {
|
||||||
|
if (!BatchWrite(mapCoins, uint256(), uint256(), uint256(), mapSproutAnchors, mapSaplingAnchors, mapSproutNullifiers, mapSaplingNullifiers))
|
||||||
|
{ strError = "batch write failed (coins remainder)"; return false; }
|
||||||
|
mapCoins.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint64_t i = 0; i < header.nSaplingAnchors; i++) {
|
||||||
|
boost::this_thread::interruption_point();
|
||||||
|
uint256 root; SaplingMerkleTree tree;
|
||||||
|
filein >> root; filein >> tree;
|
||||||
|
CAnchorsSaplingCacheEntry &e = mapSaplingAnchors[root];
|
||||||
|
e.entered = true;
|
||||||
|
e.tree = tree;
|
||||||
|
e.flags = CAnchorsSaplingCacheEntry::DIRTY;
|
||||||
|
if (mapSaplingAnchors.size() >= CHUNK) {
|
||||||
|
if (!BatchWrite(mapCoins, uint256(), uint256(), uint256(), mapSproutAnchors, mapSaplingAnchors, mapSproutNullifiers, mapSaplingNullifiers))
|
||||||
|
{ strError = "batch write failed (anchors)"; return false; }
|
||||||
|
mapSaplingAnchors.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!mapSaplingAnchors.empty()) {
|
||||||
|
if (!BatchWrite(mapCoins, uint256(), uint256(), uint256(), mapSproutAnchors, mapSaplingAnchors, mapSproutNullifiers, mapSaplingNullifiers))
|
||||||
|
{ strError = "batch write failed (anchors remainder)"; return false; }
|
||||||
|
mapSaplingAnchors.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint64_t i = 0; i < header.nSaplingNullifiers; i++) {
|
||||||
|
boost::this_thread::interruption_point();
|
||||||
|
uint256 nf;
|
||||||
|
filein >> nf;
|
||||||
|
CNullifiersCacheEntry &e = mapSaplingNullifiers[nf];
|
||||||
|
e.entered = true;
|
||||||
|
e.flags = CNullifiersCacheEntry::DIRTY;
|
||||||
|
if (mapSaplingNullifiers.size() >= CHUNK) {
|
||||||
|
if (!BatchWrite(mapCoins, uint256(), uint256(), uint256(), mapSproutAnchors, mapSaplingAnchors, mapSproutNullifiers, mapSaplingNullifiers))
|
||||||
|
{ strError = "batch write failed (nullifiers)"; return false; }
|
||||||
|
mapSaplingNullifiers.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
strError = std::string("error applying snapshot: ") + e.what();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final write: flush any remaining nullifiers AND set the best-block / best-sapling-anchor
|
||||||
|
// pointers, so GetBestBlock()==H and GetBestAnchor(SAPLING) resolve after load.
|
||||||
|
if (!BatchWrite(mapCoins, header.baseBlockHash, uint256(), header.bestSaplingAnchor,
|
||||||
|
mapSproutAnchors, mapSaplingAnchors, mapSproutNullifiers, mapSaplingNullifiers))
|
||||||
|
{ strError = "final batch write failed"; return false; }
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool CBlockTreeDB::WriteBatchSync(const std::vector<std::pair<int, const CBlockFileInfo*> >& fileInfo, int nLastFile, const std::vector<CBlockIndex*>& blockinfo) {
|
bool CBlockTreeDB::WriteBatchSync(const std::vector<std::pair<int, const CBlockFileInfo*> >& fileInfo, int nLastFile, const std::vector<CBlockIndex*>& blockinfo) {
|
||||||
CDBBatch batch(*this);
|
CDBBatch batch(*this);
|
||||||
if (fDebug)
|
if (fDebug)
|
||||||
@@ -655,6 +884,14 @@ bool CBlockTreeDB::ReadFlag(const std::string &name, bool &fValue) const {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool CBlockTreeDB::WriteAssumeutxoHeight(int nHeight) {
|
||||||
|
return Write(std::make_pair(DB_FLAG, std::string("assumeutxoheight")), nHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CBlockTreeDB::ReadAssumeutxoHeight(int &nHeight) const {
|
||||||
|
return Read(std::make_pair(DB_FLAG, std::string("assumeutxoheight")), nHeight);
|
||||||
|
}
|
||||||
|
|
||||||
void hush_index2pubkey33(uint8_t *pubkey33,CBlockIndex *pindex,int32_t height);
|
void hush_index2pubkey33(uint8_t *pubkey33,CBlockIndex *pindex,int32_t height);
|
||||||
|
|
||||||
bool CBlockTreeDB::blockOnchainActive(const uint256 &hash) {
|
bool CBlockTreeDB::blockOnchainActive(const uint256 &hash) {
|
||||||
|
|||||||
71
src/txdb.h
71
src/txdb.h
@@ -56,6 +56,61 @@ static const int64_t nMaxDbCache = sizeof(void*) > 4 ? 16384 : 1024;
|
|||||||
//! min. -dbcache in (MiB)
|
//! min. -dbcache in (MiB)
|
||||||
static const int64_t nMinDbCache = 4;
|
static const int64_t nMinDbCache = 4;
|
||||||
|
|
||||||
|
/** Magic + version for the trusted UTXO-snapshot (assumeutxo-style) file format. */
|
||||||
|
static const uint32_t UTXO_SNAPSHOT_MAGIC = 0x58535844; // 'DXSX'
|
||||||
|
static const uint8_t UTXO_SNAPSHOT_VERSION = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Header of a trusted chainstate snapshot taken at a final height H. On this private
|
||||||
|
* chain the chainstate is more than transparent UTXOs, so the snapshot also carries the
|
||||||
|
* Sapling commitment trees, the nullifier set, the best Sapling anchor and the pool value.
|
||||||
|
*
|
||||||
|
* File layout: [CUTXOSnapshotHeader]
|
||||||
|
* nCoins × (uint256 txid, CCoins)
|
||||||
|
* nSaplingAnchors × (uint256 root, SaplingMerkleTree)
|
||||||
|
* nSaplingNullifiers × (uint256 nullifier)
|
||||||
|
* uint256 contentHash // hash over everything above (NOT itself)
|
||||||
|
*/
|
||||||
|
struct CUTXOSnapshotHeader
|
||||||
|
{
|
||||||
|
uint32_t nMagic;
|
||||||
|
uint8_t nVersion;
|
||||||
|
uint32_t nNetworkMagic; // Params().MessageStart() as uint32 — prevents cross-network use
|
||||||
|
uint256 baseBlockHash; // hash of block H (the snapshot tip)
|
||||||
|
int32_t nHeight; // H
|
||||||
|
uint64_t nChainTx; // cumulative tx count at H (needed for tip fix-up)
|
||||||
|
uint8_t fHasChainSaplingValue;
|
||||||
|
int64_t nChainSaplingValue; // cumulative Sapling pool value at H (valid iff fHasChainSaplingValue)
|
||||||
|
uint256 bestSaplingAnchor; // best Sapling anchor root at H
|
||||||
|
uint64_t nCoins;
|
||||||
|
uint64_t nSaplingAnchors;
|
||||||
|
uint64_t nSaplingNullifiers;
|
||||||
|
|
||||||
|
CUTXOSnapshotHeader() { SetNull(); }
|
||||||
|
void SetNull() {
|
||||||
|
nMagic = 0; nVersion = 0; nNetworkMagic = 0; baseBlockHash.SetNull();
|
||||||
|
nHeight = 0; nChainTx = 0; fHasChainSaplingValue = 0; nChainSaplingValue = 0;
|
||||||
|
bestSaplingAnchor.SetNull(); nCoins = 0; nSaplingAnchors = 0; nSaplingNullifiers = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ADD_SERIALIZE_METHODS;
|
||||||
|
template <typename Stream, typename Operation>
|
||||||
|
inline void SerializationOp(Stream& s, Operation ser_action) {
|
||||||
|
READWRITE(nMagic);
|
||||||
|
READWRITE(nVersion);
|
||||||
|
READWRITE(nNetworkMagic);
|
||||||
|
READWRITE(baseBlockHash);
|
||||||
|
READWRITE(nHeight);
|
||||||
|
READWRITE(nChainTx);
|
||||||
|
READWRITE(fHasChainSaplingValue);
|
||||||
|
READWRITE(nChainSaplingValue);
|
||||||
|
READWRITE(bestSaplingAnchor);
|
||||||
|
READWRITE(nCoins);
|
||||||
|
READWRITE(nSaplingAnchors);
|
||||||
|
READWRITE(nSaplingNullifiers);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/** CCoinsView backed by the coin database (chainstate/) */
|
/** CCoinsView backed by the coin database (chainstate/) */
|
||||||
class CCoinsViewDB : public CCoinsView
|
class CCoinsViewDB : public CCoinsView
|
||||||
{
|
{
|
||||||
@@ -81,6 +136,19 @@ public:
|
|||||||
CNullifiersMap &mapSproutNullifiers,
|
CNullifiersMap &mapSproutNullifiers,
|
||||||
CNullifiersMap &mapSaplingNullifiers);
|
CNullifiersMap &mapSaplingNullifiers);
|
||||||
bool GetStats(CCoinsStats &stats) const;
|
bool GetStats(CCoinsStats &stats) const;
|
||||||
|
|
||||||
|
//! Stream the full chainstate at the current tip into a snapshot file (assumeutxo-style
|
||||||
|
//! producer). Caller fills the metadata fields of `header` (height, baseBlockHash, nChainTx,
|
||||||
|
//! pool value, bestSaplingAnchor); this fills the counts, writes the file, and returns the
|
||||||
|
//! content hash. Caller must hold cs_main and have flushed the cache to disk first.
|
||||||
|
bool DumpSnapshot(const std::string &path, CUTXOSnapshotHeader &header, uint256 &hashRet, std::string &strError) const;
|
||||||
|
|
||||||
|
//! Load a snapshot file produced by DumpSnapshot into the (empty) chainstate DB. Two passes:
|
||||||
|
//! pass 1 reads everything and verifies the internal content hash (and, if fRequireExpected,
|
||||||
|
//! that it equals expectedHash) WITHOUT touching the DB; pass 2 writes coins/anchors/nullifiers
|
||||||
|
//! plus the best-block / best-sapling-anchor pointers. Returns the header + computed hash.
|
||||||
|
bool LoadSnapshot(const std::string &path, const uint256 &expectedHash, bool fRequireExpected,
|
||||||
|
CUTXOSnapshotHeader &headerRet, uint256 &hashRet, std::string &strError);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Access to the block database (blocks/index/) */
|
/** Access to the block database (blocks/index/) */
|
||||||
@@ -117,6 +185,9 @@ public:
|
|||||||
bool ReadTimestampBlockIndex(const uint256 &hash, unsigned int &logicalTS) const;
|
bool ReadTimestampBlockIndex(const uint256 &hash, unsigned int &logicalTS) const;
|
||||||
bool WriteFlag(const std::string &name, bool fValue);
|
bool WriteFlag(const std::string &name, bool fValue);
|
||||||
bool ReadFlag(const std::string &name, bool &fValue) const;
|
bool ReadFlag(const std::string &name, bool &fValue) const;
|
||||||
|
//! Persist/restore the height of a loaded UTXO snapshot so the reorg-below-H guard survives restarts.
|
||||||
|
bool WriteAssumeutxoHeight(int nHeight);
|
||||||
|
bool ReadAssumeutxoHeight(int &nHeight) const;
|
||||||
bool LoadBlockIndexGuts();
|
bool LoadBlockIndexGuts();
|
||||||
bool blockOnchainActive(const uint256 &hash);
|
bool blockOnchainActive(const uint256 &hash);
|
||||||
UniValue Snapshot(int top);
|
UniValue Snapshot(int top);
|
||||||
|
|||||||
@@ -13,6 +13,20 @@ BOOTSTRAP_FALLBACK_URL="https://bootstrap2.dragonx.is"
|
|||||||
BOOTSTRAP_FILE="DRAGONX.zip"
|
BOOTSTRAP_FILE="DRAGONX.zip"
|
||||||
CHAIN_NAME="DRAGONX"
|
CHAIN_NAME="DRAGONX"
|
||||||
|
|
||||||
|
# DragonX bootstrap signing public key (PEM, openssl-compatible).
|
||||||
|
# WHY: the .md5/.sha256 files are served from the same host as the archive, so they
|
||||||
|
# only detect transmission corruption — a compromised bootstrap server could publish a
|
||||||
|
# malicious archive with matching checksums. A detached signature verified against THIS
|
||||||
|
# embedded public key (shipped in the repo, not downloaded) closes that gap: a bad server
|
||||||
|
# cannot forge a signature without the maintainer's offline private key.
|
||||||
|
#
|
||||||
|
# ROLLOUT: until the maintainer embeds a real key here and publishes DRAGONX.zip.sig,
|
||||||
|
# this stays as the placeholder and signature enforcement is skipped (with a loud warning),
|
||||||
|
# so existing users are unaffected. Once a real key is pasted in, an unsigned/invalid
|
||||||
|
# bootstrap is refused (fail-closed). See util/sign-bootstrap.md for the signing procedure.
|
||||||
|
BOOTSTRAP_PUBKEY_PLACEHOLDER="REPLACE_WITH_DRAGONX_BOOTSTRAP_PUBLIC_KEY_PEM"
|
||||||
|
BOOTSTRAP_PUBKEY="$BOOTSTRAP_PUBKEY_PLACEHOLDER"
|
||||||
|
|
||||||
# Determine data directory
|
# Determine data directory
|
||||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||||
DATADIR="$HOME/Library/Application Support/Hush/$CHAIN_NAME"
|
DATADIR="$HOME/Library/Application Support/Hush/$CHAIN_NAME"
|
||||||
@@ -139,6 +153,7 @@ download_from() {
|
|||||||
local outfile="$DATADIR/$BOOTSTRAP_FILE"
|
local outfile="$DATADIR/$BOOTSTRAP_FILE"
|
||||||
local md5file="$DATADIR/${BOOTSTRAP_FILE}.md5"
|
local md5file="$DATADIR/${BOOTSTRAP_FILE}.md5"
|
||||||
local sha256file="$DATADIR/${BOOTSTRAP_FILE}.sha256"
|
local sha256file="$DATADIR/${BOOTSTRAP_FILE}.sha256"
|
||||||
|
local sigfile="$DATADIR/${BOOTSTRAP_FILE}.sig"
|
||||||
|
|
||||||
info "Downloading bootstrap from $base_url ..."
|
info "Downloading bootstrap from $base_url ..."
|
||||||
info "This may take a while depending on your connection speed."
|
info "This may take a while depending on your connection speed."
|
||||||
@@ -149,14 +164,50 @@ download_from() {
|
|||||||
info "Downloading checksums..."
|
info "Downloading checksums..."
|
||||||
download_file "$base_url/${BOOTSTRAP_FILE}.md5" "$md5file" || return 1
|
download_file "$base_url/${BOOTSTRAP_FILE}.md5" "$md5file" || return 1
|
||||||
download_file "$base_url/${BOOTSTRAP_FILE}.sha256" "$sha256file" || return 1
|
download_file "$base_url/${BOOTSTRAP_FILE}.sha256" "$sha256file" || return 1
|
||||||
|
# Detached signature is optional during rollout (non-fatal if absent); enforcement
|
||||||
|
# is decided in verify_signature() based on whether a real public key is embedded.
|
||||||
|
rm -f "$sigfile"
|
||||||
|
download_file "$base_url/${BOOTSTRAP_FILE}.sig" "$sigfile" || warn "No signature file at $base_url (${BOOTSTRAP_FILE}.sig)"
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Verify the detached signature of the archive against the embedded release public key.
|
||||||
|
# Fail-closed once a real key is configured; skip (with warning) while the placeholder is in place.
|
||||||
|
verify_signature() {
|
||||||
|
local archive="$1"
|
||||||
|
local sigfile="$2"
|
||||||
|
|
||||||
|
if [[ "$BOOTSTRAP_PUBKEY" == "$BOOTSTRAP_PUBKEY_PLACEHOLDER" ]]; then
|
||||||
|
warn "Bootstrap signature verification is not yet configured (no maintainer key embedded)."
|
||||||
|
warn "Relying on TLS + checksum integrity only. See util/sign-bootstrap.md."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v openssl &>/dev/null; then
|
||||||
|
error "openssl is required to verify the bootstrap signature but was not found. Install openssl and retry."
|
||||||
|
fi
|
||||||
|
if [[ ! -s "$sigfile" ]]; then
|
||||||
|
error "Bootstrap signature (${BOOTSTRAP_FILE}.sig) is missing; refusing to use an unsigned bootstrap."
|
||||||
|
fi
|
||||||
|
|
||||||
|
local pubfile
|
||||||
|
pubfile=$(mktemp)
|
||||||
|
printf '%s\n' "$BOOTSTRAP_PUBKEY" > "$pubfile"
|
||||||
|
if openssl dgst -sha256 -verify "$pubfile" -signature "$sigfile" "$archive" >&2; then
|
||||||
|
rm -f "$pubfile"
|
||||||
|
info "Bootstrap signature verified against embedded DragonX release key."
|
||||||
|
else
|
||||||
|
rm -f "$pubfile"
|
||||||
|
error "Bootstrap signature verification FAILED — the archive is NOT signed by the DragonX release key. Aborting; do not use this bootstrap."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# Download the bootstrap and verify checksums
|
# Download the bootstrap and verify checksums
|
||||||
download_bootstrap() {
|
download_bootstrap() {
|
||||||
local outfile="$DATADIR/$BOOTSTRAP_FILE"
|
local outfile="$DATADIR/$BOOTSTRAP_FILE"
|
||||||
local md5file="$DATADIR/${BOOTSTRAP_FILE}.md5"
|
local md5file="$DATADIR/${BOOTSTRAP_FILE}.md5"
|
||||||
local sha256file="$DATADIR/${BOOTSTRAP_FILE}.sha256"
|
local sha256file="$DATADIR/${BOOTSTRAP_FILE}.sha256"
|
||||||
|
local sigfile="$DATADIR/${BOOTSTRAP_FILE}.sig"
|
||||||
|
|
||||||
if ! download_from "$BOOTSTRAP_BASE_URL"; then
|
if ! download_from "$BOOTSTRAP_BASE_URL"; then
|
||||||
warn "Primary download failed, trying fallback $BOOTSTRAP_FALLBACK_URL ..."
|
warn "Primary download failed, trying fallback $BOOTSTRAP_FALLBACK_URL ..."
|
||||||
@@ -187,8 +238,11 @@ download_bootstrap() {
|
|||||||
warn "sha256sum not found, skipping SHA256 verification."
|
warn "sha256sum not found, skipping SHA256 verification."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Clean up checksum files
|
# Verify the cryptographic signature (fail-closed once a release key is embedded).
|
||||||
rm -f "$md5file" "$sha256file"
|
verify_signature "$outfile" "$sigfile"
|
||||||
|
|
||||||
|
# Clean up checksum + signature files
|
||||||
|
rm -f "$md5file" "$sha256file" "$sigfile"
|
||||||
|
|
||||||
echo "$outfile"
|
echo "$outfile"
|
||||||
}
|
}
|
||||||
|
|||||||
56
util/sign-bootstrap.md
Normal file
56
util/sign-bootstrap.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# Signing the DragonX bootstrap archive
|
||||||
|
|
||||||
|
`util/bootstrap-dragonx.sh` verifies a detached signature of `DRAGONX.zip` against a
|
||||||
|
public key **embedded in the script** (`BOOTSTRAP_PUBKEY`). Because the key ships in the
|
||||||
|
repo/binary and is not downloaded from the bootstrap server, a compromised bootstrap host
|
||||||
|
cannot forge a valid signature — unlike the `.md5`/`.sha256` files, which are served from
|
||||||
|
the same host and only detect corruption.
|
||||||
|
|
||||||
|
Until a real key is embedded, `BOOTSTRAP_PUBKEY` is the placeholder and the script skips
|
||||||
|
signature enforcement (with a warning), so existing users are unaffected. Once a real key
|
||||||
|
is pasted in, an unsigned or invalid bootstrap is **refused**.
|
||||||
|
|
||||||
|
## One-time: create the signing keypair (offline)
|
||||||
|
|
||||||
|
Keep the private key OFFLINE (air-gapped if possible). Ed25519 or RSA-4096 both work with
|
||||||
|
the `openssl dgst -sha256 -verify` check the script uses; RSA-4096 maximizes compatibility:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Private key — keep secret, never publish
|
||||||
|
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 -out dragonx-bootstrap.key
|
||||||
|
# Public key — paste into bootstrap-dragonx.sh
|
||||||
|
openssl pkey -in dragonx-bootstrap.key -pubout -out dragonx-bootstrap.pub
|
||||||
|
cat dragonx-bootstrap.pub
|
||||||
|
```
|
||||||
|
|
||||||
|
Paste the full PEM (including the `-----BEGIN/END PUBLIC KEY-----` lines) into
|
||||||
|
`BOOTSTRAP_PUBKEY` in `util/bootstrap-dragonx.sh`, e.g.:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
BOOTSTRAP_PUBKEY="$(cat <<'PEM'
|
||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
... base64 ...
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
|
PEM
|
||||||
|
)"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Each release: sign the archive and publish the signature
|
||||||
|
|
||||||
|
```sh
|
||||||
|
openssl dgst -sha256 -sign dragonx-bootstrap.key -out DRAGONX.zip.sig DRAGONX.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
Upload `DRAGONX.zip.sig` next to `DRAGONX.zip` (and its `.md5`/`.sha256`) on every
|
||||||
|
bootstrap host (`bootstrap.dragonx.is`, `bootstrap2.dragonx.is`). Verify locally first:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
openssl dgst -sha256 -verify dragonx-bootstrap.pub -signature DRAGONX.zip.sig DRAGONX.zip
|
||||||
|
# -> "Verified OK"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rotating the key
|
||||||
|
|
||||||
|
Embed the new public key in the script, sign future archives with the new private key, and
|
||||||
|
release a new client version. Old clients keep trusting the old key; coordinate the cutover
|
||||||
|
with a release so users upgrade before the old key is retired.
|
||||||
Reference in New Issue
Block a user