Compare commits

...

6 commits

Author SHA1 Message Date
Matias Furszyfer
50286edf1d
Merge 8eb2736de4 into 66aa6a47bd 2025-01-08 20:43:22 +01:00
glozow
66aa6a47bd
Merge bitcoin/bitcoin#30391: BlockAssembler: return selected packages virtual size and fee
Some checks are pending
CI / test each commit (push) Waiting to run
CI / macOS 14 native, arm64, no depends, sqlite only, gui (push) Waiting to run
CI / macOS 14 native, arm64, fuzz (push) Waiting to run
CI / Win64 native, VS 2022 (push) Waiting to run
CI / Win64 native fuzz, VS 2022 (push) Waiting to run
CI / ASan + LSan + UBSan + integer, no depends, USDT (push) Waiting to run
7c123c08dd  miner: add package feerate vector to CBlockTemplate (ismaelsadeeq)

Pull request description:

  This PR enables `BlockAssembler` to add all selected packages' fee and virtual size to a vector, and then return the vector as a member of `CBlockTemplate` struct.

  This PR is the first step in the https://github.com/bitcoin/bitcoin/issues/30392 project.

  The packages' vsize and fee are used in #30157 to select a percentile fee rate of the top block in the mempool.

ACKs for top commit:
  rkrux:
    tACK 7c123c08dd
  ryanofsky:
    Code review ACK 7c123c08dd. Changes since last review are rebasing due to a test conflict, giving the new field a better name and description, resolving the test conflict, and renaming a lot of test variables. The actual code change is still one-line change.
  glozow:
    reACK 7c123c08dd

Tree-SHA512: 767b0b3d4273cf1589fd2068d729a66c7414c0f9574b15989fbe293f8c85cd6c641dd783cde55bfabab32cd047d7d8a071d6897b06ed4295c0d071e588de0861
2025-01-08 13:01:23 -05:00
ismaelsadeeq
7c123c08dd
miner: add package feerate vector to CBlockTemplate
- The package feerates are ordered by the sequence in which
  packages are selected for inclusion in the block template.

- The commit also tests this new behaviour.

Co-authored-by: willcl-ark <will@256k1.dev>
2025-01-07 15:29:17 -05:00
furszy
8eb2736de4
wallet: migration, don't create spendable wallet from a watch-only legacy wallet
Currently, the migration process creates a brand-new descriptor wallet with no
connection to the user's legacy wallet when the legacy wallet lacks key material
and contains only watch-only scripts. This behavior is not aligned with user
expectations. If the legacy wallet contains only watch-only scripts, the migration
process should only generate a watch-only wallet instead.

This commit implements this behavior.
2024-12-11 13:16:03 -05:00
furszy
788bb48f67
wallet: introduce method to return all db created files 2024-12-11 13:10:58 -05:00
furszy
81da08929f
refactor: remove sqlite dir path back-and-forth conversion 2024-12-11 13:10:58 -05:00
12 changed files with 106 additions and 16 deletions

View file

@ -421,6 +421,7 @@ void BlockAssembler::addPackageTxs(int& nPackagesSelected, int& nDescendantsUpda
}
++nPackagesSelected;
pblocktemplate->m_package_feerates.emplace_back(packageFees, static_cast<int32_t>(packageSize));
// Update transactions that depend on each of these
nDescendantsUpdated += UpdatePackagesForAdded(mempool, ancestors, mapModifiedTx);

View file

@ -10,6 +10,7 @@
#include <policy/policy.h>
#include <primitives/block.h>
#include <txmempool.h>
#include <util/feefrac.h>
#include <memory>
#include <optional>
@ -39,6 +40,9 @@ struct CBlockTemplate
std::vector<CAmount> vTxFees;
std::vector<int64_t> vTxSigOpsCost;
std::vector<unsigned char> vchCoinbaseCommitment;
/* A vector of package fee rates, ordered by the sequence in which
* packages are selected for inclusion in the block template.*/
std::vector<FeeFrac> m_package_feerates;
};
// Container for tracking updates to ancestor feerate as we include (parent)

View file

@ -16,6 +16,7 @@
#include <txmempool.h>
#include <uint256.h>
#include <util/check.h>
#include <util/feefrac.h>
#include <util/strencodings.h>
#include <util/time.h>
#include <util/translation.h>
@ -25,6 +26,7 @@
#include <test/util/setup_common.h>
#include <memory>
#include <vector>
#include <boost/test/unit_test.hpp>
@ -123,19 +125,22 @@ void MinerTestingSetup::TestPackageSelection(const CScript& scriptPubKey, const
tx.vout[0].nValue = 5000000000LL - 1000;
// This tx has a low fee: 1000 satoshis
Txid hashParentTx = tx.GetHash(); // save this txid for later use
AddToMempool(tx_mempool, entry.Fee(1000).Time(Now<NodeSeconds>()).SpendsCoinbase(true).FromTx(tx));
const auto parent_tx{entry.Fee(1000).Time(Now<NodeSeconds>()).SpendsCoinbase(true).FromTx(tx)};
AddToMempool(tx_mempool, parent_tx);
// This tx has a medium fee: 10000 satoshis
tx.vin[0].prevout.hash = txFirst[1]->GetHash();
tx.vout[0].nValue = 5000000000LL - 10000;
Txid hashMediumFeeTx = tx.GetHash();
AddToMempool(tx_mempool, entry.Fee(10000).Time(Now<NodeSeconds>()).SpendsCoinbase(true).FromTx(tx));
const auto medium_fee_tx{entry.Fee(10000).Time(Now<NodeSeconds>()).SpendsCoinbase(true).FromTx(tx)};
AddToMempool(tx_mempool, medium_fee_tx);
// This tx has a high fee, but depends on the first transaction
tx.vin[0].prevout.hash = hashParentTx;
tx.vout[0].nValue = 5000000000LL - 1000 - 50000; // 50k satoshi fee
Txid hashHighFeeTx = tx.GetHash();
AddToMempool(tx_mempool, entry.Fee(50000).Time(Now<NodeSeconds>()).SpendsCoinbase(false).FromTx(tx));
const auto high_fee_tx{entry.Fee(50000).Time(Now<NodeSeconds>()).SpendsCoinbase(false).FromTx(tx)};
AddToMempool(tx_mempool, high_fee_tx);
std::unique_ptr<BlockTemplate> block_template = mining->createNewBlock(options);
BOOST_REQUIRE(block_template);
@ -145,6 +150,21 @@ void MinerTestingSetup::TestPackageSelection(const CScript& scriptPubKey, const
BOOST_CHECK(block.vtx[2]->GetHash() == hashHighFeeTx);
BOOST_CHECK(block.vtx[3]->GetHash() == hashMediumFeeTx);
// Test the inclusion of package feerates in the block template and ensure they are sequential.
const auto block_package_feerates = BlockAssembler{m_node.chainman->ActiveChainstate(), &tx_mempool, options}.CreateNewBlock()->m_package_feerates;
BOOST_CHECK(block_package_feerates.size() == 2);
// parent_tx and high_fee_tx are added to the block as a package.
const auto combined_txs_fee = parent_tx.GetFee() + high_fee_tx.GetFee();
const auto combined_txs_size = parent_tx.GetTxSize() + high_fee_tx.GetTxSize();
FeeFrac package_feefrac{combined_txs_fee, combined_txs_size};
// The package should be added first.
BOOST_CHECK(block_package_feerates[0] == package_feefrac);
// The medium_fee_tx should be added next.
FeeFrac medium_tx_feefrac{medium_fee_tx.GetFee(), medium_fee_tx.GetTxSize()};
BOOST_CHECK(block_package_feerates[1] == medium_tx_feefrac);
// Test that a package below the block min tx fee doesn't get included
tx.vin[0].prevout.hash = hashHighFeeTx;
tx.vout[0].nValue = 5000000000LL - 1000 - 50000; // 0 fee

View file

@ -131,6 +131,8 @@ public:
/** Return path to main database filename */
std::string Filename() override { return fs::PathToString(env->Directory() / m_filename); }
/** Return paths to all database created files */
std::vector<fs::path> Files() override { return {env->Directory() / m_filename}; }
std::string Format() override { return "bdb"; }
/**

View file

@ -170,6 +170,9 @@ public:
/** Return path to main database file for logs and error messages. */
virtual std::string Filename() = 0;
/** Return paths to all database created files */
virtual std::vector<fs::path> Files() = 0;
virtual std::string Format() = 0;
std::atomic<unsigned int> nUpdateCounter;

View file

@ -65,6 +65,7 @@ public:
/** Return path to main database file for logs and error messages. */
std::string Filename() override { return fs::PathToString(m_filepath); }
std::vector<fs::path> Files() override { return {m_filepath}; }
std::string Format() override { return "bdb_ro"; }

View file

@ -63,6 +63,7 @@ public:
void IncrementUpdateCounter() override { ++nUpdateCounter; }
void ReloadDbEnv() override {}
std::string Filename() override { return "dummy"; }
std::vector<fs::path> Files() override { return {}; }
std::string Format() override { return "dummy"; }
std::unique_ptr<DatabaseBatch> MakeBatch(bool flush_on_close = true) override { return std::make_unique<DummyBatch>(); }
};

View file

@ -112,12 +112,12 @@ Mutex SQLiteDatabase::g_sqlite_mutex;
int SQLiteDatabase::g_sqlite_count = 0;
SQLiteDatabase::SQLiteDatabase(const fs::path& dir_path, const fs::path& file_path, const DatabaseOptions& options, bool mock)
: WalletDatabase(), m_mock(mock), m_dir_path(fs::PathToString(dir_path)), m_file_path(fs::PathToString(file_path)), m_write_semaphore(1), m_use_unsafe_sync(options.use_unsafe_sync)
: WalletDatabase(), m_mock(mock), m_dir_path(dir_path), m_file_path(fs::PathToString(file_path)), m_write_semaphore(1), m_use_unsafe_sync(options.use_unsafe_sync)
{
{
LOCK(g_sqlite_mutex);
LogPrintf("Using SQLite Version %s\n", SQLiteDatabaseVersion());
LogPrintf("Using wallet %s\n", m_dir_path);
LogPrintf("Using wallet %s\n", fs::PathToString(m_dir_path));
if (++g_sqlite_count == 1) {
// Setup logging
@ -253,7 +253,7 @@ void SQLiteDatabase::Open()
if (m_db == nullptr) {
if (!m_mock) {
TryCreateDirectories(fs::PathFromString(m_dir_path));
TryCreateDirectories(m_dir_path);
}
int ret = sqlite3_open_v2(m_file_path.c_str(), &m_db, flags, nullptr);
if (ret != SQLITE_OK) {

View file

@ -105,7 +105,7 @@ class SQLiteDatabase : public WalletDatabase
private:
const bool m_mock{false};
const std::string m_dir_path;
const fs::path m_dir_path;
const std::string m_file_path;
@ -166,6 +166,15 @@ public:
void IncrementUpdateCounter() override { ++nUpdateCounter; }
std::string Filename() override { return m_file_path; }
/** Return paths to all database created files */
std::vector<fs::path> Files() override
{
std::vector<fs::path> files;
files.emplace_back(m_dir_path / fs::PathFromString(m_file_path));
files.emplace_back(m_dir_path / "database");
files.emplace_back(m_dir_path / "db.log");
return files;
}
std::string Format() override { return "sqlite"; }
/** Make a SQLiteBatch connected to this database */

View file

@ -122,6 +122,7 @@ public:
void ReloadDbEnv() override {}
std::string Filename() override { return "mockable"; }
std::vector<fs::path> Files() override { return {}; }
std::string Format() override { return "mock"; }
std::unique_ptr<DatabaseBatch> MakeBatch(bool flush_on_close = true) override { return std::make_unique<MockableBatch>(m_records, m_pass); }
};

View file

@ -4078,6 +4078,9 @@ util::Result<void> CWallet::ApplyMigrationData(WalletBatch& local_wallet_batch,
return util::Error{Untranslated(STR_INTERNAL_BUG("Error: Legacy wallet data missing"))};
}
// When the legacy wallet has no spendable scripts, it will be replaced by the watch-only/solvable wallets.
bool empty_local_wallet = data.desc_spkms.empty() && !data.master_key.key.IsValid();
// Get all invalid or non-watched scripts that will not be migrated
std::set<CTxDestination> not_migrated_dests;
for (const auto& script : legacy_spkm->GetNotMineScriptPubKeys()) {
@ -4109,9 +4112,9 @@ util::Result<void> CWallet::ApplyMigrationData(WalletBatch& local_wallet_batch,
m_external_spk_managers.clear();
m_internal_spk_managers.clear();
// Setup new descriptors
// Setup new descriptors (only if we are migrating any key material)
SetWalletFlagWithDB(local_wallet_batch, WALLET_FLAG_DESCRIPTORS);
if (!IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
if (!empty_local_wallet && !IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
// Use the existing master key if we have it
if (data.master_key.key.IsValid()) {
SetupDescriptorScriptPubKeyMans(local_wallet_batch, data.master_key);
@ -4261,6 +4264,14 @@ util::Result<void> CWallet::ApplyMigrationData(WalletBatch& local_wallet_batch,
}
}
// If there was no key material in the main wallet, there should be no records on it anymore.
// This wallet will be discarded at the end of the process. Only wallets that contain the
// migrated records will be presented to the user.
if (empty_local_wallet) {
if (!m_address_book.empty()) return util::Error{_("Error: Not all address book records were migrated")};
if (!mapWallet.empty()) return util::Error{_("Error: Not all transaction records were migrated")};
}
return {}; // all good
}
@ -4269,7 +4280,7 @@ bool CWallet::CanGrindR() const
return !IsWalletFlagSet(WALLET_FLAG_EXTERNAL_SIGNER);
}
bool DoMigration(CWallet& wallet, WalletContext& context, bilingual_str& error, MigrationResult& res) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet)
bool DoMigration(CWallet& wallet, WalletContext& context, bilingual_str& error, MigrationResult& res, bool& empty_local_wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet)
{
AssertLockHeld(wallet.cs_wallet);
@ -4378,6 +4389,11 @@ bool DoMigration(CWallet& wallet, WalletContext& context, bilingual_str& error,
return false;
}
wallet.WalletLogPrintf("Wallet migration complete.\n");
// When the legacy wallet had no spendable scripts, the local wallet will contain no information after migration.
// In such case, we notify the caller to not add it to the result. The user is not expecting to have a spendable
// wallet.
empty_local_wallet = data->desc_spkms.empty() && !data->master_key.key.IsValid();
return true;
});
}
@ -4487,6 +4503,14 @@ util::Result<MigrationResult> MigrateLegacyToDescriptor(const std::string& walle
}
}
// Indicates whether the current wallet is empty after migration.
// Notes:
// When non-empty: the local wallet becomes the main spendable wallet.
// When empty: The local wallet is excluded from the result, as the
// user does not expect a spendable wallet after migrating
// only watch-only scripts.
bool empty_local_wallet = false;
{
LOCK(local_wallet->cs_wallet);
// First change to using SQLite
@ -4495,7 +4519,7 @@ util::Result<MigrationResult> MigrateLegacyToDescriptor(const std::string& walle
// Do the migration of keys and scripts for non-blank wallets, and cleanup if it fails
success = local_wallet->IsWalletFlagSet(WALLET_FLAG_BLANK_WALLET);
if (!success) {
success = DoMigration(*local_wallet, context, error, res);
success = DoMigration(*local_wallet, context, error, res, empty_local_wallet);
} else {
// Make sure that descriptors flag is actually set
local_wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
@ -4509,20 +4533,32 @@ util::Result<MigrationResult> MigrateLegacyToDescriptor(const std::string& walle
std::set<fs::path> wallet_dirs;
if (success) {
// Migration successful, unload all wallets locally, then reload them.
// Reload the main wallet
wallet_dirs.insert(fs::PathFromString(local_wallet->GetDatabase().Filename()).parent_path());
success = reload_wallet(local_wallet);
res.wallet = local_wallet;
res.wallet_name = wallet_name;
// Reload and set the main spendable wallet only if needed
if (!empty_local_wallet) {
success = reload_wallet(local_wallet);
res.wallet = local_wallet;
res.wallet_name = wallet_name;
} else {
// At this point, the main wallet is empty after migration, meaning it has no records.
// Therefore, we can safely remove it.
std::vector<fs::path> paths_to_remove = local_wallet->GetDatabase().Files();
local_wallet.reset();
for (const auto& path_to_remove : paths_to_remove) fs::remove_all(path_to_remove);
}
if (success && res.watchonly_wallet) {
// Reload watchonly
wallet_dirs.insert(fs::PathFromString(res.watchonly_wallet->GetDatabase().Filename()).parent_path());
success = reload_wallet(res.watchonly_wallet);
// When no spendable wallet is created, set the wallet name to the watch-only wallet name
if (res.wallet_name.empty()) res.wallet_name = res.watchonly_wallet->GetName();
}
if (success && res.solvables_wallet) {
// Reload solvables
wallet_dirs.insert(fs::PathFromString(res.solvables_wallet->GetDatabase().Filename()).parent_path());
success = reload_wallet(res.solvables_wallet);
// When no spendable neither watch-only wallets are created, set the wallet name to the solvables-only wallet name
if (res.wallet_name.empty()) res.wallet_name = res.solvables_wallet->GetName();
}
}
if (!success) {

View file

@ -3,7 +3,7 @@
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Test Migrating a wallet from legacy to descriptor."""
import os.path
import random
import shutil
import struct
@ -111,6 +111,8 @@ class WalletMigrationTest(BitcoinTestFramework):
# Migrate, checking that rescan does not occur
with self.master_node.assert_debug_log(expected_msgs=[], unexpected_msgs=["Rescanning"]):
migrate_info = self.master_node.migratewallet(wallet_name=wallet_name, **kwargs)
# Always verify backup path exist after migration
assert os.path.exists(migrate_info['backup_path'])
return migrate_info, self.master_node.get_wallet_rpc(wallet_name)
def test_basic(self):
@ -1018,6 +1020,12 @@ class WalletMigrationTest(BitcoinTestFramework):
res, _ = self.migrate_and_get_rpc("bare_p2pk")
wo_wallet = self.master_node.get_wallet_rpc(res['watchonly_name'])
assert_equal(wo_wallet.listdescriptors()['descriptors'][0]['desc'], descsum_create(f'pk({pubkey.hex()})'))
# Ensure that migrating a wallet with watch-only scripts does not create a spendable wallet.
assert_equal('bare_p2pk_watchonly', res['wallet_name'])
assert "bare_p2pk" not in self.master_node.listwallets()
assert "bare_p2pk" not in [w["name"] for w in self.master_node.listwalletdir()["wallets"]]
wo_wallet.unloadwallet()
def test_manual_keys_import(self):
@ -1058,6 +1066,10 @@ class WalletMigrationTest(BitcoinTestFramework):
# Verify all expected descriptors were migrated
migrated_desc = [item['desc'] for item in wo_wallet.listdescriptors()['descriptors']]
assert_equal(expected_descs, migrated_desc)
# Ensure that migrating a wallet with watch-only scripts does not create a spendable wallet.
assert_equal('import_pubkeys_watchonly', res['wallet_name'])
assert "import_pubkeys" not in self.master_node.listwallets()
assert "import_pubkeys" not in [w["name"] for w in self.master_node.listwalletdir()["wallets"]]
wo_wallet.unloadwallet()
def run_test(self):