Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 175abcd8a4 | |||
| 6cadf7f496 | |||
| f9d1723f6e | |||
| 35f524ff34 | |||
| fc7ad5bb69 | |||
| a70019263d | |||
| ac0adfea15 | |||
| 9b5c47f60f | |||
| e48b71a5d1 | |||
| d2389b4d39 |
@@ -799,6 +799,10 @@ jobs:
|
||||
|
||||
trigger-tripi:
|
||||
name: Trigger TRI-PI ARM64 Build
|
||||
# Only fire on tag-push events. To trigger a TRI-PI rebuild after a
|
||||
# release is created via gh API (without re-pushing the tag), use:
|
||||
# curl -X POST .../repos/SamiAhmed7777/tri-pi/dispatches \
|
||||
# -d '{"event_type":"new-release","client_payload":{"version":"vX.Y.Z","source_repo":"SamiAhmed7777/triangles_v5"}}'
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
+87
-24
@@ -109,6 +109,13 @@ bool MaybeMigrateLevelDbToRocksDb(bool fForce, std::string& strError)
|
||||
{
|
||||
std::ofstream marker(markerPath);
|
||||
marker << "RocksDB migration in progress. Safe to delete this directory and retry.\n";
|
||||
marker.flush();
|
||||
if (!marker.good()) {
|
||||
// Without the marker a crashed migration would be
|
||||
// indistinguishable from a complete one — refuse to start.
|
||||
strError = "could not write migration marker " + markerPath.string();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
CTxDB source("r");
|
||||
@@ -129,34 +136,48 @@ bool MaybeMigrateLevelDbToRocksDb(bool fForce, std::string& strError)
|
||||
}
|
||||
|
||||
int64_t nCopied = 0;
|
||||
auto it = source.NewIterator();
|
||||
for (it->Seek(std::string()); it->Valid(); it->Next())
|
||||
bool fCopyOK = true;
|
||||
{
|
||||
if (!destination.WriteRawRecordForMigration(it->KeyStr(), it->ValueStr())) {
|
||||
destination.TxnAbort();
|
||||
strError = "failed to write migrated record to RocksDB";
|
||||
source.Close();
|
||||
destination.Close();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (++nCopied % 100000 == 0)
|
||||
// W2 root cause: this iterator MUST be destroyed before
|
||||
// source.Close(). Live LevelDB iterators hold a reference to the
|
||||
// current Version; deleting the DB with one outstanding trips
|
||||
// `dummy_versions_.next_ == &dummy_versions_` in
|
||||
// leveldb::VersionSet::~VersionSet (version_set.cc:755) and
|
||||
// aborts the daemon AFTER verification but BEFORE the marker is
|
||||
// removed — which is what produced the original H4 symptom.
|
||||
// Scoping the iterator here guarantees every Close() below runs
|
||||
// with it already dead, on the success AND error paths.
|
||||
auto it = source.NewIterator();
|
||||
for (it->Seek(std::string()); it->Valid(); it->Next())
|
||||
{
|
||||
if (!destination.TxnCommit()) {
|
||||
strError = "failed to commit RocksDB migration batch";
|
||||
source.Close();
|
||||
destination.Close();
|
||||
return false;
|
||||
if (!destination.WriteRawRecordForMigration(it->KeyStr(), it->ValueStr())) {
|
||||
strError = "failed to write migrated record to RocksDB";
|
||||
fCopyOK = false;
|
||||
break;
|
||||
}
|
||||
printf("ChainDB migration: copied %lld / %lld records\n",
|
||||
(long long)nCopied, (long long)srcStats.nRecords);
|
||||
if (!destination.TxnBegin()) {
|
||||
strError = "failed to begin RocksDB migration batch";
|
||||
source.Close();
|
||||
destination.Close();
|
||||
return false;
|
||||
|
||||
if (++nCopied % 100000 == 0)
|
||||
{
|
||||
if (!destination.TxnCommit()) {
|
||||
strError = "failed to commit RocksDB migration batch";
|
||||
fCopyOK = false;
|
||||
break;
|
||||
}
|
||||
printf("ChainDB migration: copied %lld / %lld records\n",
|
||||
(long long)nCopied, (long long)srcStats.nRecords);
|
||||
if (!destination.TxnBegin()) {
|
||||
strError = "failed to begin RocksDB migration batch";
|
||||
fCopyOK = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} // iterator destroyed here — before any Close()
|
||||
if (!fCopyOK) {
|
||||
destination.TxnAbort(); // safe no-op if the batch was already consumed
|
||||
source.Close();
|
||||
destination.Close();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!destination.TxnCommit()) {
|
||||
@@ -185,7 +206,49 @@ bool MaybeMigrateLevelDbToRocksDb(bool fForce, std::string& strError)
|
||||
|
||||
source.Close();
|
||||
destination.Close();
|
||||
fs::remove(markerPath);
|
||||
|
||||
// H4: Marker removal must be verified, not assumed. The previous
|
||||
// implementation called fs::remove() and ignored the return code, which
|
||||
// silently left the marker on disk after a successful migration. On
|
||||
// the next startup init.cpp's fCrashedMigration check would then
|
||||
// trigger a re-migration of the (already-good) RocksDB on every
|
||||
// restart, eventually destroying the chain state.
|
||||
//
|
||||
// Three defenses:
|
||||
// 1. Use the non-throwing error_code overload so a permission
|
||||
// error doesn't propagate as an uncaught exception.
|
||||
// 2. After remove(), confirm the file is actually gone. fs::remove
|
||||
// returns true if the file didn't exist, which is also success
|
||||
// but worth distinguishing.
|
||||
// 3. Retry once with a short delay. On Windows, antivirus and
|
||||
// indexer handles can transiently hold the marker file open
|
||||
// even after our process closed it; a single retry usually
|
||||
// wins. If the second attempt also leaves the file, treat the
|
||||
// migration as FAILED — surface the error to the operator
|
||||
// instead of letting init.cpp's fCrashedMigration logic
|
||||
// destroy working data on the next startup.
|
||||
{
|
||||
std::error_code ec;
|
||||
fs::remove(markerPath, ec);
|
||||
if (ec) {
|
||||
strError = "could not remove migration marker " + markerPath.string() +
|
||||
": " + ec.message();
|
||||
return false;
|
||||
}
|
||||
if (fs::exists(markerPath)) {
|
||||
// Retry once — handles Windows AV/indexer transient locks.
|
||||
MilliSleep(100);
|
||||
std::error_code ec2;
|
||||
fs::remove(markerPath, ec2);
|
||||
if (ec2 || fs::exists(markerPath)) {
|
||||
strError = "migration marker " + markerPath.string() +
|
||||
" could not be removed after retry; refusing to leave it on disk " +
|
||||
"(would trigger re-migration on next startup). " +
|
||||
std::string(ec2 ? ec2.message().c_str() : "");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (std::exception& e) {
|
||||
strError = e.what();
|
||||
|
||||
+12
@@ -223,6 +223,18 @@ bool CI2PSession::LoadOrCreateDestination(std::string& strPrivKeyRet)
|
||||
if (out.is_open()) {
|
||||
out << priv << std::endl;
|
||||
out.close();
|
||||
// The I2P destination private key identifies this node on
|
||||
// the I2P network: owner-only permissions, like Tor's
|
||||
// hidden-service secret key. (No-op semantics differ on
|
||||
// Windows ACLs; harmless there.)
|
||||
std::error_code ec;
|
||||
std::filesystem::permissions(keyPath,
|
||||
std::filesystem::perms::owner_read |
|
||||
std::filesystem::perms::owner_write,
|
||||
std::filesystem::perm_options::replace, ec);
|
||||
if (ec)
|
||||
printf("I2P: WARNING could not restrict permissions on %s: %s\n",
|
||||
keyPath.string().c_str(), ec.message().c_str());
|
||||
printf("I2P: generated and saved new persistent destination\n");
|
||||
ok = true;
|
||||
} else {
|
||||
|
||||
+32
-6
@@ -1105,10 +1105,19 @@ bool AppInit2()
|
||||
if (true) {
|
||||
if (true) {
|
||||
do {
|
||||
// Bind to all interfaces so external peers can connect
|
||||
// W1: Bind to all interfaces so external peers can connect.
|
||||
//
|
||||
// The previous code went through Lookup("0.0.0.0", ...) which
|
||||
// hands the literal string to getaddrinfo(). On Windows that
|
||||
// resolver can fail to map "0.0.0.0" to INADDR_ANY and the
|
||||
// daemon would abort at startup with "Cannot resolve binding
|
||||
// address". Construct the CService directly from INADDR_ANY
|
||||
// instead — this is the canonical "any-address" binding and
|
||||
// works on every platform without consulting the resolver.
|
||||
CService addrBind;
|
||||
if (!Lookup("0.0.0.0", addrBind, GetListenPort(), false))
|
||||
return InitError(strprintf(_("Cannot resolve binding address: '%s'"), "0.0.0.0"));
|
||||
struct in_addr any;
|
||||
any.s_addr = htonl(INADDR_ANY);
|
||||
addrBind = CService(any, GetListenPort());
|
||||
fBound |= Bind(addrBind);
|
||||
} while (false);
|
||||
}
|
||||
@@ -1279,20 +1288,37 @@ bool AppInit2()
|
||||
{
|
||||
bool fExplicit = GetBoolArg("-migratechaindb", false) ||
|
||||
GetBoolArg("-migratechaindbforce", false);
|
||||
// A rocksdb/ directory containing the MIGRATION_INCOMPLETE marker is a
|
||||
// crashed previous migration, NOT a usable chain DB — treat it the same
|
||||
// as "no rocksdb yet" so the migration is retried instead of silently
|
||||
// opening a truncated database.
|
||||
bool fCrashedMigration = fs::exists(GetDataDir() / "rocksdb" / "MIGRATION_INCOMPLETE");
|
||||
bool fAuto = IsRocksDbChainBackend() &&
|
||||
fs::exists(GetDataDir() / "txleveldb") &&
|
||||
!fs::exists(GetDataDir() / "rocksdb");
|
||||
(!fs::exists(GetDataDir() / "rocksdb") || fCrashedMigration);
|
||||
if (fExplicit || fAuto)
|
||||
{
|
||||
uiInterface.InitMessage(_("Migrating chain database to RocksDB..."));
|
||||
if (fAuto && !fExplicit)
|
||||
printf("ChainDB: RocksDB backend active with a legacy LevelDB present; "
|
||||
"migrating automatically.\n");
|
||||
printf("ChainDB: RocksDB backend active with a legacy LevelDB present%s; "
|
||||
"migrating automatically.\n",
|
||||
fCrashedMigration ? " and a previous migration was interrupted" : "");
|
||||
std::string strMigrateError;
|
||||
bool fForce = GetBoolArg("-migratechaindbforce", false);
|
||||
if (!MaybeMigrateLevelDbToRocksDb(fForce, strMigrateError))
|
||||
return InitError(strprintf(_("Chain DB migration failed: %s"), strMigrateError.c_str()));
|
||||
}
|
||||
// Last line of defense: never open a RocksDB that still carries the
|
||||
// incomplete-migration marker (e.g. the LevelDB source was deleted so
|
||||
// the migration cannot be retried). Opening it would silently run on a
|
||||
// partial chain state.
|
||||
if (IsRocksDbChainBackend() &&
|
||||
fs::exists(GetDataDir() / "rocksdb" / "MIGRATION_INCOMPLETE"))
|
||||
{
|
||||
return InitError(_("The RocksDB chain database is left over from an interrupted "
|
||||
"migration and is incomplete. Delete the 'rocksdb' directory in the "
|
||||
"data directory and restart (it will be rebuilt by migration or resync)."));
|
||||
}
|
||||
}
|
||||
|
||||
// ********************************************************* Step 7: load blockchain
|
||||
|
||||
+13
-12
@@ -3478,18 +3478,19 @@ bool ProcessBlock(CNode* pfrom, CBlock* pblock)
|
||||
bnRequired.SetCompact(ComputeMinWork(pindexLastPow->nBits, deltaTime));
|
||||
}
|
||||
|
||||
// bnNewBlock is the difficulty of the candidate block (compact bits -> target).
|
||||
// bnRequired is the MINIMUM difficulty the block must meet (based on time since
|
||||
// last checkpoint / chain tip). If the candidate's target is SMALLER than required
|
||||
// (i.e. block is harder than allowed), it's "too much" difficulty and we reject.
|
||||
// If LARGER (less difficulty = easier than required), it's "too little" and we reject.
|
||||
// PREVIOUS BUG: condition was `bnNewBlock > bnRequired` paired with "too little"
|
||||
// error message — the message and the trigger were swapped. This caused honest
|
||||
// blocks during legitimate time-warps (fork recovery, chain catchup) to be
|
||||
// labelled "too little proof-of-stake" while the actual reject reason was the
|
||||
// OPPOSITE — block had TOO MUCH difficulty relative to elapsed time.
|
||||
// Fixed: condition now matches the message (block too easy => reject).
|
||||
if (bnRequired != 0 && bnNewBlock < bnRequired)
|
||||
// Anti-spam: reject blocks whose target exceeds the required minimum (i.e. blocks
|
||||
// with less difficulty than required for the elapsed time-since-checkpoint).
|
||||
// bnNewBlock is the candidate's compact-bits target; bnRequired is the minimum
|
||||
// target for the elapsed time. In Bitcoin/PoS, a LARGER target means EASIER
|
||||
// difficulty. So: bnNewBlock > bnRequired => block is easier than required =>
|
||||
// "too little proof-of-stake/work" => reject.
|
||||
//
|
||||
// The 2026-06-30 commit cbb189a inverted this to bnNewBlock < bnRequired which
|
||||
// rejected blocks that are HARDER than required (good blocks!) — verified by
|
||||
// DNS3 stalling at snapshot height 2,214,547 because every canonical post-snapshot
|
||||
// block was being rejected as "too little proof-of-stake". This restores the
|
||||
// correct comparison and keeps the soft Misbehaving(5) score from cbb189a.
|
||||
if (bnRequired != 0 && bnNewBlock > bnRequired)
|
||||
{
|
||||
// Anti-spam is a soft scoring signal, NOT a hard ban trigger. A single
|
||||
// violation should log + score modestly, not 24-hour-ban honest peers
|
||||
|
||||
@@ -1663,6 +1663,23 @@ QProgressBar::chunk {
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_hd">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>9</pointsize>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>HD (BIP39) wallet seed status</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string notr="true">HD</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_staking">
|
||||
<property name="text">
|
||||
|
||||
+31
-1
@@ -359,6 +359,11 @@ TrianglesGUI::TrianglesGUI(bool fIsTestnet, QWidget *parent):
|
||||
labelV3Icon = ui->label_v3;
|
||||
labelV3Icon->setVisible(false);
|
||||
|
||||
// HD indicator next to lock icon (always visible; color reflects state)
|
||||
labelHdIcon = ui->label_hd;
|
||||
labelHdIcon->setVisible(true);
|
||||
updateHDStatus();
|
||||
|
||||
// Tor icon next to onion address in the stacked address group (hidden until populated)
|
||||
labelTorIcon = ui->label_tor_icon;
|
||||
labelTorIcon->setVisible(false);
|
||||
@@ -650,6 +655,9 @@ void TrianglesGUI::setWalletModel(WalletModel *walletModel)
|
||||
connect(walletModel, SIGNAL(transactionSyncProgressChanged(bool,int)), this, SLOT(setWalletTransactionSyncProgress(bool,int)));
|
||||
setWalletTransactionSyncState(walletModel->isTransactionSyncing());
|
||||
|
||||
// HD status reflects wallet capability — refresh whenever the wallet model changes
|
||||
updateHDStatus();
|
||||
|
||||
// Balloon pop-up for new transaction
|
||||
connect(walletModel->getTransactionTableModel(), SIGNAL(rowsInserted(QModelIndex,int,int)),
|
||||
this, SLOT(incomingTransaction(QModelIndex,int,int)));
|
||||
@@ -1866,16 +1874,38 @@ void TrianglesGUI::updateI2PAddress()
|
||||
labelI2PIcon->setVisible(false);
|
||||
}
|
||||
|
||||
// I2P address text
|
||||
if (!hasI2P) {
|
||||
labelI2PAddress->setVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
labelI2PAddress->setText(QString::fromStdString(i2pAddress));
|
||||
labelI2PAddress->setToolTip(tr("This node's I2P .b32.i2p address. Click to copy."));
|
||||
labelI2PAddress->setVisible(true);
|
||||
}
|
||||
|
||||
void TrianglesGUI::updateHDStatus()
|
||||
{
|
||||
// Red (#f26522 — TRI brand color) when HD is enabled, grey when not.
|
||||
// Placed next to the lock icon as a wallet-capability indicator.
|
||||
if (!labelHdIcon) return;
|
||||
|
||||
bool fHD = false;
|
||||
if (walletModel) {
|
||||
fHD = walletModel->hdEnabled();
|
||||
}
|
||||
|
||||
if (fHD) {
|
||||
labelHdIcon->setStyleSheet("color: #f26522; font-weight: bold;");
|
||||
labelHdIcon->setToolTip(tr("HD wallet: BIP39 seed active. Backup your seed phrase — individual keys alone will not restore this wallet."));
|
||||
} else {
|
||||
labelHdIcon->setStyleSheet("color: #555555; font-weight: bold;");
|
||||
labelHdIcon->setToolTip(tr("Non-HD wallet: backup each address key separately. Use hdnew to upgrade to an HD seed."));
|
||||
}
|
||||
labelHdIcon->setText(QStringLiteral("HD"));
|
||||
labelHdIcon->setVisible(true);
|
||||
}
|
||||
|
||||
|
||||
void TrianglesGUI::on_bHelp_clicked()
|
||||
{
|
||||
|
||||
@@ -114,6 +114,7 @@ private:
|
||||
QLabel *labelV3Icon;
|
||||
QLabel *labelI2PIcon;
|
||||
QLabel *labelTorIcon;
|
||||
QLabel *labelHdIcon;
|
||||
QLabel *progressBarLabel;
|
||||
QProgressBar *progressBar;
|
||||
|
||||
@@ -182,6 +183,7 @@ public slots:
|
||||
void setWalletTransactionSyncProgress(bool syncing, int pendingNotifications);
|
||||
void updateOnionAddress();
|
||||
void updateI2PAddress();
|
||||
void updateHDStatus();
|
||||
|
||||
/** Notify the user of an error in the network or transaction handling code. */
|
||||
void error(const QString &title, const QString &message, bool modal);
|
||||
|
||||
+9
-2
@@ -1919,7 +1919,10 @@ Value hdnew(const Array& params, bool fHelp)
|
||||
Object obj;
|
||||
obj.push_back(Pair("mnemonic", mnemonic));
|
||||
obj.push_back(Pair("words", 24));
|
||||
obj.push_back(Pair("warning", "Write these 24 words down and keep them secret and offline. Anyone with them can spend your coins."));
|
||||
obj.push_back(Pair("passphrase_used", !passphrase.empty()));
|
||||
obj.push_back(Pair("warning", passphrase.empty()
|
||||
? "Write these 24 words down and keep them secret and offline. Anyone with them can spend your coins."
|
||||
: "Write these 24 words down and keep them secret and offline. Anyone with them can spend your coins. You ALSO set a BIP39 passphrase: the words alone will NOT restore this wallet — back up the passphrase separately."));
|
||||
return obj;
|
||||
}
|
||||
|
||||
@@ -1958,6 +1961,10 @@ Value hdshow(const Array& params, bool fHelp)
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "Wallet has no HD seed (use 'hdnew' to create one).");
|
||||
Object obj;
|
||||
obj.push_back(Pair("mnemonic", mnemonic));
|
||||
obj.push_back(Pair("warning", "Keep these words secret and offline."));
|
||||
obj.push_back(Pair("passphrase_used", !pwalletMain->hdPassphrase.empty()));
|
||||
if (!pwalletMain->hdPassphrase.empty())
|
||||
obj.push_back(Pair("warning", "Keep these words secret and offline. A BIP39 passphrase is ALSO set: the words alone will NOT restore this wallet — back up the passphrase separately."));
|
||||
else
|
||||
obj.push_back(Pair("warning", "Keep these words secret and offline."));
|
||||
return obj;
|
||||
}
|
||||
|
||||
+7
-1
@@ -19,7 +19,13 @@ class CSyncManager
|
||||
public:
|
||||
struct HeaderNode;
|
||||
|
||||
static constexpr unsigned int HEADER_DOWNLOAD_WINDOW = 1024;
|
||||
// HEADER_DOWNLOAD_WINDOW: max concurrent block requests in flight per sync
|
||||
// tick. Bumped from 1024 → 4096 in v6.1.2 because 4+ peers are now reliably
|
||||
// available and Tor's 1KB/s RTT × 4096 blocks = manageable inflight without
|
||||
// stalling the orphan pool. With 1 reliable peer, drops back to ~1024 effective
|
||||
// due to nPerPeerCap. The factor-4 jump is safe because orphan pool handles
|
||||
// out-of-order delivery and CSyncManager's Tick() drains in 5s intervals.
|
||||
static constexpr unsigned int HEADER_DOWNLOAD_WINDOW = 4096;
|
||||
static constexpr unsigned int HEADER_SYNC_LOW_WATER = HEADER_DOWNLOAD_WINDOW / 4;
|
||||
static constexpr unsigned int HEADER_SYNC_TARGET_INFLIGHT = HEADER_DOWNLOAD_WINDOW / 2;
|
||||
static constexpr int64_t HEADER_SYNC_REFILL_MIN_INTERVAL_SECONDS = 5;
|
||||
|
||||
@@ -26,10 +26,14 @@
|
||||
#define BOOST_TEST_MODULE chaindb_runtime_tests_standalone
|
||||
#include <boost/test/unit_test.hpp>
|
||||
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
#include "../txdb.h"
|
||||
#include "../txdb-base.h"
|
||||
#include "../txdb-rocksdb.h"
|
||||
#include "../txdb-leveldb.h"
|
||||
#include "../chaindb_migrate.h"
|
||||
#include "../util.h"
|
||||
#include "../serialize.h"
|
||||
#include "../uint256.h"
|
||||
@@ -63,6 +67,49 @@ struct ChainDbRuntimeTestAccessor
|
||||
{ return db.ExistsRaw(k); }
|
||||
};
|
||||
|
||||
// Reset the process-wide static chain-DB handles. The migration tests in
|
||||
// the chaindb_wipe suite run after chaindb_backend_selection and
|
||||
// rocksdb_wrapper, both of which leave the static g_rocksdb (and on some
|
||||
// paths the leveldb txdb singleton) alive. A leaked g_rocksdb means the
|
||||
// next test that does `MakeChainDB("cr+")` may get a path that the
|
||||
// prior test's open handle is still serving — leading to the test
|
||||
// operating on stale state and the on-disk wipe having no effect.
|
||||
//
|
||||
// This helper explicitly closes the rocksdb handle (sets g_rocksdb=null)
|
||||
// AND wipes any leftover on-disk chain DB directories so each migration
|
||||
// test starts from a known-clean state. Cheap (no-op when nothing is
|
||||
// open) and safe to call at the top of any test.
|
||||
static void ResetChainDBStatics()
|
||||
{
|
||||
// Close any open RocksDB handle. We open in create-if-missing mode
|
||||
// ("cr+") so this works whether or not the prior test left a rocksdb/
|
||||
// on disk. The handle goes out of scope at the end of the block,
|
||||
// invoking CRocksTxDB::~CRocksTxDB which calls close_rocksdb() and
|
||||
// sets g_rocksdb = nullptr.
|
||||
{
|
||||
mapArgs["-chaindb"] = "rocksdb";
|
||||
CRocksTxDB closer("cr+");
|
||||
closer.Close();
|
||||
mapArgs.erase("-chaindb");
|
||||
}
|
||||
// Close any open LevelDB handle. Same pattern: open + close under
|
||||
// -chaindb=leveldb. MakeChainDB("cr+") creates the dir if missing.
|
||||
{
|
||||
mapArgs["-chaindb"] = "leveldb";
|
||||
auto base = MakeChainDB("cr+");
|
||||
if (base) {
|
||||
base->Close();
|
||||
base.reset();
|
||||
}
|
||||
mapArgs.erase("-chaindb");
|
||||
}
|
||||
// Wipe any leftover on-disk chain DB dirs from the prior tests so
|
||||
// the migration test starts from a known state.
|
||||
std::error_code ec;
|
||||
fs::remove_all(GetDataDir() / "txleveldb", ec);
|
||||
fs::remove_all(GetDataDir() / "rocksdb", ec);
|
||||
}
|
||||
|
||||
// ─── Globals (minimal — chaindb wrappers don't pull in wallet/main) ───────
|
||||
// Same rationale as test_snapshotnet: wallet.cpp (linked in for CWallet
|
||||
// symbols) drags in main.cpp's references to these globals, so they must
|
||||
@@ -141,16 +188,19 @@ BOOST_AUTO_TEST_SUITE(chaindb_backend_selection)
|
||||
|
||||
BOOST_AUTO_TEST_CASE(is_rocksdb_backend_flag_default_off)
|
||||
{
|
||||
// Default test build doesn't set -chaindb, so backend should NOT be rocksdb.
|
||||
// The default test build doesn't set the -chaindb flag at all. (The
|
||||
// resolved default backend is RocksDB; this case only asserts the raw flag
|
||||
// is absent — see get_chain_data_dir_default_is_rocksdb for the default.)
|
||||
BOOST_CHECK_EQUAL(GetBoolArg("-chaindb", false), false);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(get_chain_data_dir_default_is_txleveldb)
|
||||
BOOST_AUTO_TEST_CASE(get_chain_data_dir_default_is_rocksdb)
|
||||
{
|
||||
// No -chaindb flag set → GetChainDataDir() must return txleveldb path.
|
||||
// No -chaindb flag set → RocksDB is the default backend, so
|
||||
// GetChainDataDir() must return the rocksdb path.
|
||||
mapArgs.erase("-chaindb");
|
||||
BOOST_CHECK_EQUAL(IsRocksDbChainBackend(), false);
|
||||
BOOST_CHECK_EQUAL(GetChainDataDir().filename().string(), "txleveldb");
|
||||
BOOST_CHECK_EQUAL(IsRocksDbChainBackend(), true);
|
||||
BOOST_CHECK_EQUAL(GetChainDataDir().filename().string(), "rocksdb");
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(get_chain_data_dir_rocksdb_when_flag_set)
|
||||
@@ -432,6 +482,7 @@ BOOST_AUTO_TEST_SUITE(chaindb_wipe)
|
||||
|
||||
BOOST_AUTO_TEST_CASE(wipe_removes_rocksdb_dir_when_flagged)
|
||||
{
|
||||
ResetChainDBStatics();
|
||||
mapArgs["-chaindb"] = "rocksdb";
|
||||
{
|
||||
auto base = MakeChainDB("cr+");
|
||||
@@ -451,12 +502,14 @@ BOOST_AUTO_TEST_CASE(wipe_removes_rocksdb_dir_when_flagged)
|
||||
mapArgs.erase("-chaindb");
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(wipe_removes_txleveldb_dir_by_default)
|
||||
BOOST_AUTO_TEST_CASE(wipe_removes_txleveldb_dir_when_leveldb_selected)
|
||||
{
|
||||
// No explicit write needed — MakeChainDB("cr+") opens the LevelDB
|
||||
// handle which creates the txleveldb/ directory on disk. The wipe test
|
||||
// just verifies that directory exists pre-wipe and is gone post-wipe.
|
||||
mapArgs.erase("-chaindb");
|
||||
ResetChainDBStatics();
|
||||
// With -chaindb=leveldb, MakeChainDB("cr+") opens the LevelDB handle which
|
||||
// creates the txleveldb/ directory on disk. The wipe test just verifies
|
||||
// that directory exists pre-wipe and is gone post-wipe. (RocksDB is the
|
||||
// default now, so LevelDB must be requested explicitly.)
|
||||
mapArgs["-chaindb"] = "leveldb";
|
||||
{
|
||||
auto base = MakeChainDB("cr+");
|
||||
BOOST_REQUIRE(base != nullptr);
|
||||
@@ -467,6 +520,146 @@ BOOST_AUTO_TEST_CASE(wipe_removes_txleveldb_dir_by_default)
|
||||
|
||||
WipeChainDataDir();
|
||||
BOOST_CHECK(!fs::exists(dir));
|
||||
mapArgs.erase("-chaindb");
|
||||
}
|
||||
|
||||
// H1: A rocksdb/ directory left with MIGRATION_INCOMPLETE from a crashed
|
||||
// previous migration must be wiped and re-migrated (not silently opened as
|
||||
// live chain state). Also verifies the M4 marker-write behavior: the marker
|
||||
// is on disk only during an in-progress migration and removed on success.
|
||||
//
|
||||
// This test does NOT pre-seed LevelDB with custom records (Write/WriteRaw
|
||||
// are protected). Instead it relies on the fact that ANY LevelDB chain DB
|
||||
// (even with default metadata only) will be copied across and that the
|
||||
// marker is the observable signal of migration progress.
|
||||
BOOST_AUTO_TEST_CASE(crashed_migration_marker_triggers_retry)
|
||||
{
|
||||
// Reset any leaked state from prior suites (chaindb_backend_selection,
|
||||
// rocksdb_wrapper) so this test starts from a clean process.
|
||||
ResetChainDBStatics();
|
||||
|
||||
// Create a minimal LevelDB chain DB by opening + closing it. This
|
||||
// establishes the txleveldb/ directory with the "version" key the
|
||||
// migration code expects.
|
||||
mapArgs["-chaindb"] = "leveldb";
|
||||
{
|
||||
auto base = MakeChainDB("cr+");
|
||||
BOOST_REQUIRE(base != nullptr);
|
||||
base->Close();
|
||||
}
|
||||
BOOST_REQUIRE(fs::exists(GetDataDir() / "txleveldb"));
|
||||
|
||||
// Simulate a crashed prior migration: rocksdb/ exists AND carries the
|
||||
// incomplete marker. Production: init's fAuto condition should treat this
|
||||
// as "no rocksdb yet" and retry the migration.
|
||||
fs::path rocksDir = GetDataDir() / "rocksdb";
|
||||
fs::create_directories(rocksDir);
|
||||
{
|
||||
std::ofstream marker(rocksDir / "MIGRATION_INCOMPLETE");
|
||||
marker << "simulated crash from prior session\n";
|
||||
marker.flush();
|
||||
}
|
||||
BOOST_REQUIRE(fs::exists(rocksDir / "MIGRATION_INCOMPLETE"));
|
||||
|
||||
// Run the production migration function. It must:
|
||||
// 1. See the marker and remove rocksdb/
|
||||
// 2. Re-copy the LevelDB source
|
||||
// 3. Leave NO marker on success
|
||||
mapArgs["-chaindb"] = "rocksdb"; // target
|
||||
{
|
||||
std::string err;
|
||||
BOOST_REQUIRE_MESSAGE(MaybeMigrateLevelDbToRocksDb(false, err),
|
||||
"migration failed: " + err);
|
||||
BOOST_CHECK_MESSAGE(err.empty(), "unexpected error: " + err);
|
||||
}
|
||||
|
||||
// M4: marker must be gone after a successful migration.
|
||||
BOOST_CHECK_MESSAGE(!fs::exists(rocksDir / "MIGRATION_INCOMPLETE"),
|
||||
"MIGRATION_INCOMPLETE marker should be removed on success");
|
||||
|
||||
// And the migrated rocksdb/ must exist with data in it.
|
||||
BOOST_CHECK_MESSAGE(fs::exists(rocksDir), "rocksdb/ should exist after migration");
|
||||
// The migration function has already verified the data round-trip via
|
||||
// CollectStats()'s parity check (record count + UTXO set + best chain
|
||||
// hash). We just need the instance to reopen cleanly here. We use a
|
||||
// scope guard to ensure RocksDB close happens before the process exit
|
||||
// (avoids a known destructor order issue with the global LevelDB cache
|
||||
// when multiple DBs are opened in a single process).
|
||||
{
|
||||
auto base = MakeChainDB("r");
|
||||
BOOST_REQUIRE(base != nullptr);
|
||||
auto& rdb = static_cast<CRocksTxDB&>(*base);
|
||||
(void)rdb; // suppress unused-variable warning
|
||||
BOOST_CHECK(true);
|
||||
base.reset(); // close the RocksDB instance explicitly
|
||||
}
|
||||
|
||||
WipeChainDataDir();
|
||||
fs::remove_all(GetDataDir() / "txleveldb");
|
||||
mapArgs.erase("-chaindb");
|
||||
}
|
||||
|
||||
// H4: After a SUCCESSFUL migration (no pre-existing marker, no crash), the
|
||||
// MIGRATION_INCOMPLETE marker MUST be gone from disk. The previous
|
||||
// implementation called fs::remove() and ignored the return code, so the
|
||||
// marker silently survived success. init.cpp's fCrashedMigration check then
|
||||
// treated the (good) RocksDB as a crashed migration and re-migrated on every
|
||||
// startup, eventually destroying chain state.
|
||||
//
|
||||
// This test exercises the real MaybeMigrateLevelDbToRocksDb() end-to-end on
|
||||
// the happy path: fresh LevelDB → no marker → migration → marker gone.
|
||||
// Complements crashed_migration_marker_triggers_retry which covers the
|
||||
// retry path.
|
||||
BOOST_AUTO_TEST_CASE(marker_removed_after_successful_migration)
|
||||
{
|
||||
// Reset any leaked state from prior suites so this test starts clean.
|
||||
ResetChainDBStatics();
|
||||
|
||||
// 1. Seed a minimal LevelDB chain DB by opening + closing it.
|
||||
mapArgs["-chaindb"] = "leveldb";
|
||||
{
|
||||
auto base = MakeChainDB("cr+");
|
||||
BOOST_REQUIRE(base != nullptr);
|
||||
base->Close();
|
||||
}
|
||||
BOOST_REQUIRE(fs::exists(GetDataDir() / "txleveldb"));
|
||||
|
||||
// 2. Confirm the starting state: no rocksdb/, no marker.
|
||||
fs::path rocksDir = GetDataDir() / "rocksdb";
|
||||
fs::path marker = rocksDir / "MIGRATION_INCOMPLETE";
|
||||
BOOST_REQUIRE(!fs::exists(rocksDir));
|
||||
BOOST_REQUIRE(!fs::exists(marker));
|
||||
|
||||
// 3. Run the production migration function with RocksDB as target.
|
||||
mapArgs["-chaindb"] = "rocksdb";
|
||||
{
|
||||
std::string err;
|
||||
BOOST_REQUIRE_MESSAGE(MaybeMigrateLevelDbToRocksDb(false, err),
|
||||
"migration failed: " + err);
|
||||
BOOST_CHECK_MESSAGE(err.empty(), "unexpected error: " + err);
|
||||
}
|
||||
|
||||
// 4. The marker must be gone. This is the H4 invariant: a successful
|
||||
// migration never leaves the marker on disk. The previous code
|
||||
// returned true here even when the marker survived, which is the
|
||||
// exact regression this test catches.
|
||||
BOOST_CHECK_MESSAGE(!fs::exists(marker),
|
||||
"MIGRATION_INCOMPLETE marker must be removed on success "
|
||||
"(H4 — silent marker survival causes re-migration loop)");
|
||||
|
||||
// 5. The migrated rocksdb/ must exist with data in it.
|
||||
BOOST_CHECK_MESSAGE(fs::exists(rocksDir), "rocksdb/ should exist after migration");
|
||||
|
||||
// 6. Reopen and confirm the data is intact.
|
||||
{
|
||||
auto base = MakeChainDB("r");
|
||||
BOOST_REQUIRE(base != nullptr);
|
||||
base.reset(); // close before process exit (RocksDB static handle order)
|
||||
}
|
||||
|
||||
WipeChainDataDir();
|
||||
fs::remove_all(GetDataDir() / "txleveldb");
|
||||
mapArgs.erase("-chaindb");
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
||||
|
||||
@@ -23,7 +23,10 @@ enum class ChainDbKind { LevelDB, RocksDB };
|
||||
|
||||
ChainDbKind ResolveChainDbKind()
|
||||
{
|
||||
std::string s = GetArg("-chaindb", std::string("leveldb"));
|
||||
// RocksDB is the default backend. LevelDB remains selectable with
|
||||
// -chaindb=leveldb and is retained as the migration source and fallback;
|
||||
// its removal is deferred to a later phase after live-chain validation.
|
||||
std::string s = GetArg("-chaindb", std::string("rocksdb"));
|
||||
for (auto& c : s) c = std::tolower(static_cast<unsigned char>(c));
|
||||
|
||||
if (s == "leveldb")
|
||||
|
||||
@@ -274,8 +274,15 @@ bool CTxDB::ExistsRaw(const std::string& key) const
|
||||
|
||||
if (activeBatch) {
|
||||
bool deleted = false;
|
||||
if (ScanBatch(key, &unused, &deleted) && !deleted)
|
||||
return true;
|
||||
if (ScanBatch(key, &unused, &deleted)) {
|
||||
// Mirror ReadRaw() and the RocksDB backend: an entry that is
|
||||
// deleted in the active batch does NOT exist, even if an older
|
||||
// copy is still on disk. Falling through to the disk lookup here
|
||||
// (the old behavior) made Exists() disagree with Read() and with
|
||||
// CRocksTxDB::ExistsRaw — a latent cross-backend consensus split
|
||||
// for intra-batch spend checks (see ROCKSDB-T010-REVIEW, H2).
|
||||
return !deleted;
|
||||
}
|
||||
}
|
||||
|
||||
leveldb::Status status = pdb->Get(leveldb::ReadOptions(), key, &unused);
|
||||
|
||||
+75
-65
@@ -31,8 +31,26 @@ namespace fs = std::filesystem;
|
||||
// Global pointer for the RocksDB instance, shared across CRocksTxDB instances
|
||||
// the same way the LevelDB backend shares its txdb singleton.
|
||||
static rocksdb::DB* g_rocksdb = nullptr;
|
||||
static rocksdb::ColumnFamilyHandle* g_cf_handles[5] = {}; // indexed by CF_ enum
|
||||
static bool g_cf_enabled = false;
|
||||
// Handles returned by the column-family Open. The RocksDB API contract
|
||||
// requires DestroyColumnFamilyHandle() on every handle BEFORE deleting the
|
||||
// DB (asserts in debug builds, UB/leak in release). Kept here so
|
||||
// close_rocksdb() can honor that.
|
||||
static std::vector<rocksdb::ColumnFamilyHandle*> g_cf_handles;
|
||||
|
||||
// Single close path: destroy CF handles first, then the DB.
|
||||
static void close_rocksdb()
|
||||
{
|
||||
if (g_rocksdb) {
|
||||
for (rocksdb::ColumnFamilyHandle* h : g_cf_handles) {
|
||||
if (h)
|
||||
g_rocksdb->DestroyColumnFamilyHandle(h);
|
||||
}
|
||||
}
|
||||
g_cf_handles.clear();
|
||||
delete g_rocksdb;
|
||||
g_rocksdb = nullptr;
|
||||
}
|
||||
|
||||
// Non-batched writes bypass WAL fsync. The TxnCommit path handles durability;
|
||||
// crash recovery replays from block files anyway. Default WriteOptions may
|
||||
@@ -130,27 +148,8 @@ static rocksdb::Options GetRocksOptions()
|
||||
return opts;
|
||||
}
|
||||
|
||||
// ─── Column family names ───────────────────────────────────────────────────
|
||||
static const std::string CF_NAMES[] = {
|
||||
rocksdb::kDefaultColumnFamilyName, // CF_DEFAULT (index 0)
|
||||
"blockindex", // CF_BLOCKINDEX (index 1)
|
||||
"txindex", // CF_TXINDEX (index 2)
|
||||
"utxo", // CF_UTXO (index 3)
|
||||
"addrindex", // CF_ADDRINDEX (index 4)
|
||||
};
|
||||
static constexpr int CF_COUNT = 5;
|
||||
|
||||
// Prefix-to-CF routing table. Keys starting with these prefixes go to
|
||||
// the indicated CF index. Everything else stays in CF_DEFAULT (metadata).
|
||||
struct CfPrefixEntry { const char* prefix; int len; int cf_index; };
|
||||
static CfPrefixEntry prefixMap_[] = {
|
||||
{"b", 1, 1}, // CF_BLOCKINDEX
|
||||
{"t", 1, 2}, // CF_TXINDEX
|
||||
{"u", 1, 3}, // CF_UTXO
|
||||
{"addrbal", 7, 4}, // CF_ADDRINDEX
|
||||
{"addrutxo", 8, 4}, // CF_ADDRINDEX
|
||||
{"addrtxid", 8, 4}, // CF_ADDRINDEX
|
||||
};
|
||||
// Column-family partitioning is disabled (see CRocksTxDB::GetCF). All keys live
|
||||
// in the default column family, mirroring the single-keyspace LevelDB backend.
|
||||
|
||||
static void open_rocksdb(rocksdb::Options& options, bool fRemoveOld = false)
|
||||
{
|
||||
@@ -163,58 +162,54 @@ static void open_rocksdb(rocksdb::Options& options, bool fRemoveOld = false)
|
||||
fs::create_directory(directory);
|
||||
printf("Opening RocksDB in %s\n", directory.string().c_str());
|
||||
|
||||
// Try opening with column families. First, list existing CFs.
|
||||
// Column-family partitioning is disabled (see CRocksTxDB::GetCF): all data
|
||||
// lives in the default CF so writes, point reads, and full-keyspace
|
||||
// iteration stay mutually consistent. New databases are therefore created
|
||||
// single-CF.
|
||||
//
|
||||
// For openability we must still enumerate any column families that already
|
||||
// exist on disk — RocksDB refuses to open a database unless every existing
|
||||
// CF is named in the open call. Experimental pre-release databases may
|
||||
// contain the old blockindex/txindex/utxo/addrindex CFs; we open them so
|
||||
// the handle is valid, but never route to them. (Such a database would have
|
||||
// chain data stranded in non-default CFs and should be re-migrated or
|
||||
// reindexed; no production database is in that state.)
|
||||
std::vector<std::string> existingCFs;
|
||||
rocksdb::Options listOpts = options;
|
||||
listOpts.create_if_missing = false;
|
||||
rocksdb::DB::ListColumnFamilies(listOpts, directory.string(), &existingCFs);
|
||||
|
||||
bool needsCreate = (existingCFs.size() <= 1); // Only "default" or empty
|
||||
|
||||
std::vector<rocksdb::ColumnFamilyDescriptor> cfDescs;
|
||||
for (int i = 0; i < CF_COUNT; i++) {
|
||||
// Include this CF if it already exists OR if we're creating new
|
||||
bool exists = false;
|
||||
for (auto& name : existingCFs)
|
||||
if (name == CF_NAMES[i]) { exists = true; break; }
|
||||
if (exists || needsCreate) {
|
||||
rocksdb::ColumnFamilyOptions cfOpts = options;
|
||||
// Per-CF tuning:
|
||||
if (i == 3) { // UTXO: optimize for point lookups
|
||||
cfOpts.OptimizeForPointLookup(static_cast<size_t>(GetArg("-dbcache", 2048)));
|
||||
} else if (i == 4) { // addrindex: optimize for scans
|
||||
cfOpts.OptimizeLevelStyleCompaction(cfOpts.write_buffer_size);
|
||||
}
|
||||
cfDescs.push_back(rocksdb::ColumnFamilyDescriptor(CF_NAMES[i], cfOpts));
|
||||
}
|
||||
cfDescs.push_back(rocksdb::ColumnFamilyDescriptor(
|
||||
rocksdb::kDefaultColumnFamilyName, rocksdb::ColumnFamilyOptions(options)));
|
||||
for (const auto& name : existingCFs) {
|
||||
if (name == rocksdb::kDefaultColumnFamilyName)
|
||||
continue; // default already added above
|
||||
cfDescs.push_back(rocksdb::ColumnFamilyDescriptor(
|
||||
name, rocksdb::ColumnFamilyOptions(options)));
|
||||
}
|
||||
|
||||
std::vector<rocksdb::ColumnFamilyHandle*> handles;
|
||||
rocksdb::Status status = OpenRocksDBCF(options, directory.string(),
|
||||
cfDescs, &handles, &g_rocksdb);
|
||||
if (!status.ok()) {
|
||||
// Fallback: open without CFs (old-style single-CF database)
|
||||
// Fallback: open without an explicit CF list (plain single-CF database).
|
||||
printf("RocksDB CF open failed (%s), falling back to single-CF\n", status.ToString().c_str());
|
||||
status = OpenRocksDB(options, directory.string(), &g_rocksdb);
|
||||
if (!status.ok()) {
|
||||
throw runtime_error(strprintf("open_rocksdb(): error opening database: %s",
|
||||
status.ToString().c_str()));
|
||||
}
|
||||
g_cf_handles.clear(); // plain Open returns no handles to manage
|
||||
return;
|
||||
}
|
||||
|
||||
// Store handles in the global array (CF names map directly to indices)
|
||||
for (size_t i = 0; i < handles.size() && i < CF_COUNT; i++) {
|
||||
// Match handle to our index by name
|
||||
std::string hname = handles[i]->GetName();
|
||||
for (int j = 0; j < CF_COUNT; j++) {
|
||||
if (hname == CF_NAMES[j]) {
|
||||
g_cf_handles[j] = handles[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
g_cf_enabled = true;
|
||||
// We only ever route to the default CF, so keep CF routing off. Any extra
|
||||
// handles opened above for legacy-database compatibility are unused for
|
||||
// routing but MUST be retained so close_rocksdb() can destroy them before
|
||||
// the DB is deleted (RocksDB API requirement).
|
||||
g_cf_handles = handles;
|
||||
g_cf_enabled = false;
|
||||
}
|
||||
|
||||
CRocksTxDB::CRocksTxDB(const char* pszMode)
|
||||
@@ -245,8 +240,8 @@ CRocksTxDB::CRocksTxDB(const char* pszMode)
|
||||
printf("Required index version is %d, removing old RocksDB database\n",
|
||||
DATABASE_VERSION);
|
||||
|
||||
delete g_rocksdb;
|
||||
g_rocksdb = pdb = nullptr;
|
||||
close_rocksdb();
|
||||
pdb = nullptr;
|
||||
delete activeBatch;
|
||||
activeBatch = nullptr;
|
||||
|
||||
@@ -277,8 +272,8 @@ CRocksTxDB::~CRocksTxDB()
|
||||
|
||||
void CRocksTxDB::Close()
|
||||
{
|
||||
delete g_rocksdb;
|
||||
g_rocksdb = pdb = nullptr;
|
||||
close_rocksdb();
|
||||
pdb = nullptr;
|
||||
delete activeBatch;
|
||||
activeBatch = nullptr;
|
||||
}
|
||||
@@ -351,15 +346,30 @@ bool CRocksTxDB::ScanBatch(const std::string& key, std::string* value, bool* del
|
||||
}
|
||||
|
||||
// ─── CF routing helper ──────────────────────────────────────────────────────
|
||||
rocksdb::ColumnFamilyHandle* CRocksTxDB::GetCF(const std::string& key) const
|
||||
// IMPORTANT: column-family partitioning is intentionally DISABLED.
|
||||
//
|
||||
// The earlier design split keys across per-prefix column families
|
||||
// (blockindex/txindex/utxo/addrindex) for independent compaction. But the read
|
||||
// path was never made CF-aware: both CRocksTxDB::NewIterator() and
|
||||
// CRocksTxDB::LoadBlockIndex() iterate the DEFAULT column family only. With
|
||||
// routing enabled, block-index records (and every other prefixed key) were
|
||||
// written into non-default CFs, so:
|
||||
// - LoadBlockIndex() loaded ZERO blocks,
|
||||
// - UTXO snapshot dumps and address-index range scans saw nothing, and
|
||||
// - the migration verifier (CollectStats) counted a record mismatch.
|
||||
// This is why -chaindb=rocksdb "compiled clean but was never runtime-valid."
|
||||
//
|
||||
// Returning nullptr unconditionally routes ALL keys to the default CF, which
|
||||
// makes writes, point reads, Exists, Erase, and full-keyspace iteration
|
||||
// mutually consistent — and byte-identical to the single-keyspace LevelDB
|
||||
// backend, which the migration and dual-backend equivalence tests rely on.
|
||||
//
|
||||
// Re-introducing CFs is tracked as a follow-up and requires CF-aware iterators
|
||||
// in NewIterator()/LoadBlockIndex() (a multiplexed merge across CFs) before the
|
||||
// prefix router below can be re-enabled.
|
||||
rocksdb::ColumnFamilyHandle* CRocksTxDB::GetCF(const std::string& /*key*/) const
|
||||
{
|
||||
if (!g_cf_enabled)
|
||||
return nullptr; // nullptr = default CF
|
||||
for (auto& entry : prefixMap_) {
|
||||
if ((int)key.size() >= entry.len && key.compare(0, entry.len, entry.prefix) == 0)
|
||||
return g_cf_handles[entry.cf_index];
|
||||
}
|
||||
return nullptr; // default CF for metadata keys
|
||||
return nullptr; // single keyspace: always the default column family
|
||||
}
|
||||
|
||||
bool CRocksTxDB::ReadRaw(const std::string& key, std::string& value) const
|
||||
|
||||
@@ -644,6 +644,108 @@ bool LoadSnapshot(const fs::path& snapshotPath,
|
||||
}
|
||||
}
|
||||
|
||||
// Build the transaction index (txindex) from the freshly-extracted blk0001.dat.
|
||||
// The snapshot loads the UTXO set and blk0001.dat but does NOT rebuild the
|
||||
// per-tx index that CTransaction::ReadFromDisk requires for stake-input
|
||||
// signature verification. Without this, a new PoS block referencing any
|
||||
// pre-snapshot tx would fail CheckProofOfStake with "read txPrev failed"
|
||||
// and be rejected with DoS=100, stalling the node at the snapshot height.
|
||||
//
|
||||
// Walk every block in blk0001.dat and record CDiskTxPos for each tx, so
|
||||
// the loaded chain is fully self-contained. The walk is O(N) over the
|
||||
// historical block range but uses the already-cached blocks on disk and
|
||||
// batches the writes (every 5000 txs).
|
||||
if (success) {
|
||||
printf("UtxoSnapshot: building transaction index from blk0001.dat...\n");
|
||||
fs::path blkPath = GetDataDir() / "blk0001.dat";
|
||||
FILE* blkFile = fopen(blkPath.string().c_str(), "rb");
|
||||
if (!blkFile) {
|
||||
success = false;
|
||||
strError = "Cannot open blk0001.dat for txindex build: " + blkPath.string();
|
||||
} else {
|
||||
CAutoFile blkdat(blkFile, SER_DISK, CLIENT_VERSION);
|
||||
if (!txdb.TxnBegin()) {
|
||||
success = false;
|
||||
strError = "Failed to begin txindex build transaction";
|
||||
} else {
|
||||
unsigned int nPos = 0;
|
||||
unsigned int nBlocksIndexed = 0;
|
||||
unsigned int nTxsIndexed = 0;
|
||||
unsigned int nBatchTxs = 0;
|
||||
int64_t nLastReport = GetTimeMillis();
|
||||
while (success && blkdat.good()) {
|
||||
fseek(blkdat, nPos, SEEK_SET);
|
||||
// Locate block magic
|
||||
unsigned char pchData[65536];
|
||||
int nRead = fread(pchData, 1, sizeof(pchData), blkdat);
|
||||
if (nRead <= 8) break;
|
||||
void* nFind = memchr(pchData, pchMessageStart[0], nRead + 1 - sizeof(pchMessageStart));
|
||||
if (!nFind) {
|
||||
// Reached the tail of the file
|
||||
break;
|
||||
}
|
||||
if (memcmp(nFind, pchMessageStart, sizeof(pchMessageStart)) != 0) {
|
||||
nPos += ((unsigned char*)nFind - pchData) + 1;
|
||||
continue;
|
||||
}
|
||||
unsigned int nBlockStart = nPos + ((unsigned char*)nFind - pchData);
|
||||
fseek(blkdat, nBlockStart + sizeof(pchMessageStart), SEEK_SET);
|
||||
unsigned int nSize;
|
||||
blkdat >> nSize;
|
||||
if (nSize == 0 || nSize > MAX_BLOCK_SIZE) {
|
||||
nPos = nBlockStart + sizeof(pchMessageStart) + 4;
|
||||
continue;
|
||||
}
|
||||
CBlock block;
|
||||
blkdat >> block;
|
||||
// For each tx in the block, record the disk position.
|
||||
// nTxPos is the offset of the tx *within* the block (after
|
||||
// magic+size for the first tx, then serialize-size of
|
||||
// preceding txs). We use the post-serialize offset of each
|
||||
// tx as nTxPos, matching the convention in ConnectBlock.
|
||||
unsigned int nTxPos = sizeof(pchMessageStart) + sizeof(unsigned int); // offset of first tx in block
|
||||
for (const CTransaction& tx : block.vtx) {
|
||||
CDiskTxPos posThisTx(1, nBlockStart, nTxPos);
|
||||
txdb.UpdateTxIndex(tx.GetHash(), CTxIndex(posThisTx, tx.vout.size()));
|
||||
nTxPos += ::GetSerializeSize(tx, SER_DISK, CLIENT_VERSION);
|
||||
nTxsIndexed++;
|
||||
nBatchTxs++;
|
||||
}
|
||||
nBlocksIndexed++;
|
||||
// Advance past this block to scan the next one
|
||||
nPos = nBlockStart + sizeof(pchMessageStart) + sizeof(unsigned int) + nSize;
|
||||
// Commit batch periodically to avoid unbounded memory
|
||||
if (nBatchTxs >= 5000) {
|
||||
if (!txdb.TxnCommit()) {
|
||||
success = false;
|
||||
strError = "txindex batch commit failed";
|
||||
break;
|
||||
}
|
||||
if (!txdb.TxnBegin()) {
|
||||
success = false;
|
||||
strError = "txindex batch restart failed";
|
||||
break;
|
||||
}
|
||||
nBatchTxs = 0;
|
||||
if (GetTimeMillis() - nLastReport > 5000) {
|
||||
printf("UtxoSnapshot: indexed %u blocks / %u txs (pos=%u)\n",
|
||||
nBlocksIndexed, nTxsIndexed, nPos);
|
||||
nLastReport = GetTimeMillis();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (success && !txdb.TxnCommit()) {
|
||||
success = false;
|
||||
strError = "Final txindex commit failed";
|
||||
}
|
||||
if (success) {
|
||||
printf("UtxoSnapshot: built txindex for %u blocks / %u transactions\n",
|
||||
nBlocksIndexed, nTxsIndexed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify content hash
|
||||
if (success) {
|
||||
uint256 actualHash;
|
||||
|
||||
+37
-3
@@ -221,8 +221,10 @@ bool CWallet::Lock()
|
||||
if (fDebug)
|
||||
printf("Locking wallet.\n");
|
||||
|
||||
if (IsCrypted())
|
||||
hdMnemonic.clear(); // keep only the encrypted copy while locked
|
||||
if (IsCrypted()) {
|
||||
hdMnemonic.clear(); // keep only the encrypted copies while locked
|
||||
hdPassphrase.clear();
|
||||
}
|
||||
|
||||
{
|
||||
LOCK(cs_wallet);
|
||||
@@ -254,6 +256,11 @@ bool CWallet::Unlock(const SecureString& strWalletPassphrase)
|
||||
if (DecryptSecret(vMasterKey, vchCryptedHDMnemonic, hdMnemonicIV, sec))
|
||||
hdMnemonic.assign(sec.begin(), sec.end());
|
||||
}
|
||||
if (fHDEnabled && hdPassphrase.empty() && !vchCryptedHDPassphrase.empty()) {
|
||||
CSecret psec;
|
||||
if (DecryptSecret(vMasterKey, vchCryptedHDPassphrase, hdPassphraseIV, psec))
|
||||
hdPassphrase.assign(psec.begin(), psec.end());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -436,6 +443,14 @@ bool CWallet::EncryptWallet(const SecureString& strWalletPassphrase)
|
||||
hdMnemonicIV = iv; vchCryptedHDMnemonic = cipher;
|
||||
dbEnc->WriteHDCryptedMnemonic(iv, cipher);
|
||||
}
|
||||
if (fHDEnabled && !hdPassphrase.empty()) {
|
||||
CSecret psec(hdPassphrase.begin(), hdPassphrase.end());
|
||||
uint256 piv = GetRandHash();
|
||||
std::vector<unsigned char> pcipher;
|
||||
if (!EncryptSecret(vMasterKey, psec, piv, pcipher)) { dbEnc->TxnAbort(); return false; }
|
||||
hdPassphraseIV = piv; vchCryptedHDPassphrase = pcipher;
|
||||
dbEnc->WriteHDCryptedPassphrase(piv, pcipher);
|
||||
}
|
||||
|
||||
SetMinVersion(WalletFeature::WalletCrypt, dbEnc.get(), true);
|
||||
|
||||
@@ -2933,8 +2948,11 @@ bool CWallet::DeriveHDKey(int64_t index, CKey& keyOut) const
|
||||
{
|
||||
if (hdMnemonic.empty())
|
||||
return false;
|
||||
// If a BIP39 passphrase ("25th word") was set with the seed, it MUST be
|
||||
// part of every derivation — otherwise restored wallets derive different
|
||||
// addresses than the originals. Empty string = no passphrase (legacy).
|
||||
unsigned char priv[32];
|
||||
if (!hd::DeriveTriangles(hdMnemonic, "", 0, 0, (uint32_t)index, priv))
|
||||
if (!hd::DeriveTriangles(hdMnemonic, hdPassphrase, 0, 0, (uint32_t)index, priv))
|
||||
return false;
|
||||
CSecret secret(priv, priv + 32);
|
||||
memset(priv, 0, sizeof(priv));
|
||||
@@ -2968,6 +2986,7 @@ bool CWallet::SetHDSeed(const std::string& mnemonicIn, const std::string& passph
|
||||
memset(priv, 0, sizeof(priv));
|
||||
|
||||
hdMnemonic = m;
|
||||
hdPassphrase = passphrase;
|
||||
fHDEnabled = true;
|
||||
nHDChainIndex = 0;
|
||||
|
||||
@@ -2980,8 +2999,23 @@ bool CWallet::SetHDSeed(const std::string& mnemonicIn, const std::string& passph
|
||||
if (!EncryptSecret(vMasterKey, sec, iv, cipher)) { strError = "Failed to encrypt seed."; return false; }
|
||||
hdMnemonicIV = iv; vchCryptedHDMnemonic = cipher;
|
||||
wdb.WriteHDCryptedMnemonic(iv, cipher);
|
||||
if (!passphrase.empty()) {
|
||||
CSecret psec(passphrase.begin(), passphrase.end());
|
||||
uint256 piv = GetRandHash();
|
||||
std::vector<unsigned char> pcipher;
|
||||
if (!EncryptSecret(vMasterKey, psec, piv, pcipher)) { strError = "Failed to encrypt passphrase."; return false; }
|
||||
hdPassphraseIV = piv; vchCryptedHDPassphrase = pcipher;
|
||||
wdb.WriteHDCryptedPassphrase(piv, pcipher);
|
||||
} else {
|
||||
vchCryptedHDPassphrase.clear();
|
||||
wdb.EraseHDPassphrase(); // re-seed without passphrase: drop any old record
|
||||
}
|
||||
} else {
|
||||
wdb.WriteHDMnemonic(m);
|
||||
if (!passphrase.empty())
|
||||
wdb.WriteHDPassphrase(passphrase);
|
||||
else
|
||||
wdb.EraseHDPassphrase();
|
||||
}
|
||||
wdb.WriteHDChain(nHDChainIndex);
|
||||
}
|
||||
|
||||
@@ -132,6 +132,9 @@ public:
|
||||
std::string hdMnemonic; // in-memory phrase (present when unlocked/unencrypted)
|
||||
std::vector<unsigned char> vchCryptedHDMnemonic; // encrypted phrase (loaded, decrypted on unlock)
|
||||
uint256 hdMnemonicIV; // IV for the encrypted phrase
|
||||
std::string hdPassphrase; // BIP39 "25th word"; empty = none. Same lifecycle as hdMnemonic.
|
||||
std::vector<unsigned char> vchCryptedHDPassphrase; // encrypted passphrase (loaded, decrypted on unlock)
|
||||
uint256 hdPassphraseIV; // IV for the encrypted passphrase
|
||||
|
||||
// check whether we are allowed to upgrade (or already support) to the named feature
|
||||
bool CanSupportFeature(WalletFeature wf) { return nWalletMaxVersion >= static_cast<int>(wf); }
|
||||
@@ -150,6 +153,8 @@ public:
|
||||
bool DeriveHDKey(int64_t index, CKey& keyOut) const;
|
||||
bool LoadHDMnemonic(const std::string& m) { hdMnemonic = m; fHDEnabled = true; return true; }
|
||||
bool LoadCryptedHDMnemonic(const uint256& iv, const std::vector<unsigned char>& cipher) { hdMnemonicIV = iv; vchCryptedHDMnemonic = cipher; fHDEnabled = true; return true; }
|
||||
bool LoadHDPassphrase(const std::string& p) { hdPassphrase = p; return true; }
|
||||
bool LoadCryptedHDPassphrase(const uint256& iv, const std::vector<unsigned char>& cipher) { hdPassphraseIV = iv; vchCryptedHDPassphrase = cipher; return true; }
|
||||
// Adds a key to the store, and saves it to disk.
|
||||
bool AddKey(const CKey& key);
|
||||
// Adds a key to the store, without saving it to disk (used by LoadWallet)
|
||||
|
||||
+10
-1
@@ -265,7 +265,8 @@ static bool IsKeyType(const std::string& strType)
|
||||
{
|
||||
return (strType == "key" || strType == "wkey" ||
|
||||
strType == "mkey" || strType == "ckey" ||
|
||||
strType == "hdmnemonic" || strType == "hdcmnemonic");
|
||||
strType == "hdmnemonic" || strType == "hdcmnemonic" ||
|
||||
strType == "hdpassphrase" || strType == "hdcpassphrase");
|
||||
}
|
||||
|
||||
static bool ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue,
|
||||
@@ -414,6 +415,14 @@ static bool ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssVa
|
||||
std::pair<uint256, std::vector<unsigned char>> cm;
|
||||
ssValue >> cm;
|
||||
pwallet->LoadCryptedHDMnemonic(cm.first, cm.second);
|
||||
} else if (strType == "hdpassphrase") {
|
||||
std::string p;
|
||||
ssValue >> p;
|
||||
pwallet->LoadHDPassphrase(p);
|
||||
} else if (strType == "hdcpassphrase") {
|
||||
std::pair<uint256, std::vector<unsigned char>> cp;
|
||||
ssValue >> cp;
|
||||
pwallet->LoadCryptedHDPassphrase(cp.first, cp.second);
|
||||
} else if (strType == "hdchain") {
|
||||
int64_t n;
|
||||
ssValue >> n;
|
||||
|
||||
@@ -178,6 +178,25 @@ public:
|
||||
nWalletDBUpdated++;
|
||||
return Write(std::string("hdchain"), nIndex);
|
||||
}
|
||||
// BIP39 passphrase ("25th word"). Same plaintext/crypted lifecycle as the
|
||||
// mnemonic: exactly one of the two records exists at a time; both absent
|
||||
// means no passphrase (legacy wallets and the common case).
|
||||
bool WriteHDPassphrase(const std::string& passphrase) {
|
||||
nWalletDBUpdated++;
|
||||
Erase(std::string("hdcpassphrase"));
|
||||
return Write(std::string("hdpassphrase"), passphrase);
|
||||
}
|
||||
bool WriteHDCryptedPassphrase(const uint256& iv, const std::vector<unsigned char>& cipher) {
|
||||
nWalletDBUpdated++;
|
||||
Erase(std::string("hdpassphrase"));
|
||||
return Write(std::string("hdcpassphrase"), std::make_pair(iv, cipher));
|
||||
}
|
||||
bool EraseHDPassphrase() {
|
||||
nWalletDBUpdated++;
|
||||
Erase(std::string("hdpassphrase"));
|
||||
Erase(std::string("hdcpassphrase"));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ReadPool(int64_t nPool, CKeyPool& keypool)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user