Merge 9152 via sweepprivkeys

This commit is contained in:
Luke Dashjr 2025-03-05 03:27:08 +00:00
commit 647dcf1fe7
11 changed files with 291 additions and 4 deletions

View File

@ -10,6 +10,7 @@
#include <primitives/transaction.h> // For CTransactionRef
#include <util/result.h>
#include <any>
#include <functional>
#include <memory>
#include <optional>
@ -399,6 +400,8 @@ class ChainClient
public:
virtual ~ChainClient() = default;
virtual void assignContextHACK(std::any&) {};
//! Register rpcs.
virtual void registerRpcs() = 0;

View File

@ -77,6 +77,7 @@ const QStringList historyFilter = QStringList()
<< "sethdseed"
<< "signmessagewithprivkey"
<< "signrawtransactionwithkey"
<< "sweepprivkeys"
<< "walletpassphrase"
<< "walletpassphrasechange"
<< "encryptwallet";

View File

@ -3,6 +3,10 @@
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#if defined(HAVE_CONFIG_H)
#include <config/bitcoin-config.h>
#endif
#include <rpc/blockchain.h>
#include <blockfilter.h>
@ -23,6 +27,7 @@
#include <index/blockfilterindex.h>
#include <index/coinstatsindex.h>
#include <kernel/coinstats.h>
#include <key_io.h>
#include <logging/timer.h>
#include <net.h>
#include <net_processing.h>
@ -32,6 +37,7 @@
#include <node/utxo_snapshot.h>
#include <node/warnings.h>
#include <primitives/transaction.h>
#include <policy/settings.h>
#include <rpc/server.h>
#include <rpc/server_util.h>
#include <rpc/util.h>
@ -48,6 +54,17 @@
#include <util/strencodings.h>
#include <util/string.h>
#include <util/syserror.h>
#include <validation.h>
#ifdef ENABLE_WALLET
#include <interfaces/wallet.h>
#include <wallet/coincontrol.h>
#include <wallet/fees.h>
#include <wallet/rpc/util.h>
#include <wallet/types.h>
#include <wallet/wallet.h>
#endif
#include <util/translation.h>
#include <validation.h>
#include <validationinterface.h>
@ -515,6 +532,171 @@ static RPCHelpMan getblockhash()
};
}
#ifdef ENABLE_WALLET
bool FindScriptPubKey(std::atomic<int>& scan_progress, const std::atomic<bool>& should_abort, int64_t& count, CCoinsViewCursor* cursor, const std::set<CScript>& needles, std::map<COutPoint, Coin>& out_results, std::function<void()>& interruption_point);
static RPCHelpMan sweepprivkeys()
{
return RPCHelpMan{"sweepprivkeys",
"\nSends bitcoins controlled by private key to specified destinations.\n",
{
{"options", RPCArg::Type::OBJ_NAMED_PARAMS, RPCArg::Optional::NO, "",
{
{"privkeys", RPCArg::Type::ARR, RPCArg::Optional::NO, "An array of WIF private key(s)",
{
{"privkey", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, ""},
},
},
{"label", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "Label for received bitcoins"},
},
RPCArgOptions{.oneline_description="options"}},
},
RPCResult{RPCResult::Type::STR_HEX, "", "The transaction id."},
RPCExamples{""},
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
{
NodeContext& node = EnsureAnyNodeContext(request.context);
JSONRPCRequest wallet_req = request;
CHECK_NONFATAL(node.wallet_loader && node.wallet_loader->context());
node.wallet_loader->assignContextHACK(wallet_req.context);
std::shared_ptr<wallet::CWallet> const wallet = wallet::GetWalletForJSONRPCRequest(wallet_req);
if (!wallet) return NullUniValue;
wallet::CWallet* const pwallet = wallet.get();
// NOTE: It isn't safe to sweep-and-send in a single action, since this would leave the send missing from the transaction history
// Parse options
std::set<CScript> needles;
wallet::CCoinControl coin_control;
FillableSigningProvider temp_keystore;
CMutableTransaction tx;
std::string label;
CAmount total_in = 0;
for (const std::string& optname : request.params[0].getKeys()) {
const UniValue& optval = request.params[0][optname];
if (optname == "privkeys") {
const UniValue& privkeys_a = optval.get_array();
for (size_t privkey_i = 0; privkey_i < privkeys_a.size(); ++privkey_i) {
const UniValue& privkey_wif = privkeys_a[privkey_i];
std::string wif_secret = privkey_wif.get_str();
CKey key = DecodeSecret(wif_secret);
if (!key.IsValid()) throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid private key encoding");
CPubKey pubkey = key.GetPubKey();
CHECK_NONFATAL(key.VerifyPubKey(pubkey));
temp_keystore.AddKey(key);
CKeyID address = pubkey.GetID();
CScript script = GetScriptForDestination(PKHash(address));
if (!script.empty()) {
needles.insert(script);
}
script = GetScriptForRawPubKey(pubkey);
if (!script.empty()) {
needles.insert(script);
}
}
} else if (optname == "label") {
label = wallet::LabelFromValue(optval.get_str());
} else {
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Unrecognised option '%s'", optname));
}
}
std::unique_ptr<wallet::ReserveDestination> reservedest;
CTxDestination dest;
{
LOCK(pwallet->cs_wallet);
// Reserve the key we will be using
reservedest.reset(new wallet::ReserveDestination(pwallet, pwallet->TransactionChangeType(pwallet->m_default_change_type, std::vector<wallet::CRecipient>())));
auto op_dest = reservedest->GetReservedDestination(false);
if (!op_dest) {
throw JSONRPCError(RPC_WALLET_KEYPOOL_RAN_OUT, util::ErrorString(op_dest).original);
}
dest = *op_dest;
}
// Scan UTXO set for inputs
std::vector<CTxOut> input_txos;
{
// Collect all possible inputs
std::map<COutPoint, Coin> coins;
{
std::unique_ptr<CCoinsViewCursor> pcursor;
{
ChainstateManager& chainman = EnsureAnyChainman(request.context);
LOCK(cs_main);
if (node.mempool) {
node.mempool->FindScriptPubKey(needles, coins);
}
Chainstate& active_chainstate = chainman.ActiveChainstate();
active_chainstate.ForceFlushStateToDisk();
pcursor = std::unique_ptr<CCoinsViewCursor>(active_chainstate.CoinsDB().Cursor());
CHECK_NONFATAL(pcursor);
}
std::atomic<int> scan_progress;
const std::atomic<bool> should_abort{false};
int64_t count;
if (!FindScriptPubKey(scan_progress, should_abort, count, pcursor.get(), needles, coins, node.rpc_interruption_point)) {
throw JSONRPCError(RPC_MISC_ERROR, "UTXO FindScriptPubKey failed");
}
}
// Add them as inputs to the transaction, and count the total value
for (auto& it : coins) {
const COutPoint& outpoint = it.first;
const Coin& coin = it.second;
const CTxOut& txo = coin.out;
tx.vin.emplace_back(outpoint.hash, outpoint.n);
input_txos.push_back(txo);
total_in += txo.nValue;
}
}
if (total_in == 0) {
throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, "No value to sweep");
}
tx.vout.emplace_back(total_in, GetScriptForDestination(dest));
while (true) {
if (IsDust(tx.vout[0], pwallet->chain().relayDustFee())) {
throw JSONRPCError(RPC_VERIFY_REJECTED, "Swept value would be dust");
}
for (size_t input_index = 0; input_index < tx.vin.size(); ++input_index) {
SignatureData empty;
if (!SignSignature(temp_keystore, input_txos[input_index].scriptPubKey, tx, input_index, input_txos[input_index].nValue, SIGHASH_ALL, empty)) {
throw JSONRPCError(RPC_MISC_ERROR, "Failed to sign");
}
}
int64_t tx_vsize = GetVirtualTransactionSize(CTransaction(tx));
CAmount fee_needed = GetMinimumFee(*wallet, tx_vsize, coin_control, nullptr /* FeeCalculation */);
const CAmount total_out = tx.vout[0].nValue;
if (fee_needed <= total_in - total_out) {
break;
}
tx.vout[0].nValue = total_in - fee_needed;
}
CTransactionRef final_tx(MakeTransactionRef(std::move(tx)));
pwallet->SetAddressBook(dest, label, wallet::AddressPurpose::RECEIVE);
std::string err_string;
const node::TransactionError err = BroadcastTransaction(node, final_tx, err_string, pwallet->m_default_max_tx_fee, true /* relay */, true /* wait_callback */);
if (node::TransactionError::OK != err) {
pwallet->DelAddressBook(dest);
throw JSONRPCTransactionError(err, err_string);
}
reservedest->KeepDestination();
return final_tx->GetHash().GetHex();
},
};
}
#endif // ENABLE_WALLET
static RPCHelpMan getblockheader()
{
return RPCHelpMan{"getblockheader",
@ -2249,7 +2431,6 @@ static RPCHelpMan getblockstats()
};
}
namespace {
//! Search for a given set of pubkey scripts
bool FindScriptPubKey(std::atomic<int>& scan_progress, const std::atomic<bool>& should_abort, int64_t& count, CCoinsViewCursor* cursor, const std::set<CScript>& needles, std::map<COutPoint, Coin>& out_results, std::function<void()>& interruption_point)
{
@ -2279,7 +2460,6 @@ bool FindScriptPubKey(std::atomic<int>& scan_progress, const std::atomic<bool>&
scan_progress = 100;
return true;
}
} // namespace
/** RAII object to prevent concurrency issue when scanning the txout set */
static std::atomic<int> g_scan_progress;
@ -3621,6 +3801,10 @@ void RegisterBlockchainRPCCommands(CRPCTable& t)
{"hidden", &waitforblockheight},
{"hidden", &syncwithvalidationinterfacequeue},
{"hidden", &getblocklocations},
#ifdef ENABLE_WALLET
{"wallet", &sweepprivkeys},
#endif
};
for (const auto& c : commands) {
t.appendCommand(c.name, &c);

View File

@ -100,6 +100,8 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "getdescriptoractivity", 0, "blockhashes" },
{ "getdescriptoractivity", 1, "scanobjects" },
{ "getdescriptoractivity", 2, "include_mempool" },
{ "sweepprivkeys", 0, "options" },
{ "sweepprivkeys", 0, "privkeys" },
{ "scantxoutset", 1, "scanobjects" },
{ "dumptxoutset", 1, "format" },
{ "dumptxoutset", 2, "show_header" },

View File

@ -184,6 +184,7 @@ const std::vector<std::string> RPC_COMMANDS_SAFE_FOR_FUZZING{
"submitblock",
"submitheader",
"submitpackage",
"sweepprivkeys",
"syncwithvalidationinterfacequeue",
"testmempoolaccept",
"uptime",

View File

@ -49,7 +49,9 @@ struct CoinsViewOptions {
int simulate_crash_ratio = 0;
};
/** CCoinsView backed by the coin database (chainstate/) */
/** CCoinsView backed by the coin database (chainstate/)
* Cursor requires FlushStateToDisk for consistency.
*/
class CCoinsViewDB final : public CCoinsView
{
protected:

View File

@ -808,6 +808,21 @@ std::vector<CTxMemPool::indexed_transaction_set::const_iterator> CTxMemPool::Get
return iters;
}
void CTxMemPool::FindScriptPubKey(const std::set<CScript>& needles, std::map<COutPoint, Coin>& out_results) {
LOCK(cs);
for (const CTxMemPoolEntry& entry : mapTx) {
const CTransaction& tx = entry.GetTx();
const Txid& hash = tx.GetHash();
for (size_t txo_index = tx.vout.size(); txo_index > 0; ) {
--txo_index;
const CTxOut& txo = tx.vout[txo_index];
if (needles.count(txo.scriptPubKey)) {
out_results.emplace(COutPoint(hash, txo_index), Coin(txo, MEMPOOL_HEIGHT, false));
}
}
}
}
static TxMempoolInfo GetInfo(CTxMemPool::indexed_transaction_set::const_iterator it) {
return TxMempoolInfo{it->GetSharedTx(), it->GetTime(), it->GetFee(), it->GetTxSize(), it->GetModifiedFee() - it->GetFee()};
}

View File

@ -41,6 +41,7 @@
#include <vector>
class CChain;
class CScript;
class ValidationSignals;
struct bilingual_str;
@ -691,6 +692,8 @@ public:
std::vector<CTxMemPoolEntryRef> entryAll() const EXCLUSIVE_LOCKS_REQUIRED(cs);
std::vector<TxMempoolInfo> infoAll() const;
void FindScriptPubKey(const std::set<CScript>& needles, std::map<COutPoint, Coin>& out_results);
size_t DynamicMemoryUsage() const;
/** Adds a transaction to the unbroadcast set */
@ -834,6 +837,9 @@ public:
* It also allows you to sign a double-spend directly in
* signrawtransactionwithkey and signrawtransactionwithwallet,
* as long as the conflicting transaction is not yet confirmed.
*
* Its Cursor also doesn't work. In general, it is broken as a CCoinsView
* implementation outside of a few use cases.
*/
class CCoinsViewMemPool : public CCoinsViewBacked
{

View File

@ -616,13 +616,18 @@ public:
}
~WalletLoaderImpl() override { UnloadWallets(m_context); }
//! HACK to workaround libc++ bugs (assigning from other locations such as sweepprivkeys breaks std::any_cast type checking); see also https://github.com/llvm/llvm-project/issues/55684
void assignContextHACK(std::any& a) override
{
a = &m_context;
}
//! ChainClient methods
void registerRpcs() override
{
for (const CRPCCommand& command : GetWalletRPCCommands()) {
m_rpc_commands.emplace_back(command.category, command.name, [this, &command](const JSONRPCRequest& request, UniValue& result, bool last_handler) {
JSONRPCRequest wallet_request = request;
wallet_request.context = &m_context;
assignContextHACK(wallet_request.context);
return command.actor(wallet_request, result, last_handler);
}, command.argNames, command.unique_id);
m_rpc_handlers.emplace_back(m_context.chain->handleRpc(m_rpc_commands.back()));

View File

@ -185,6 +185,7 @@ BASE_SCRIPTS = [
'interface_bitcoin_cli.py --descriptors',
'feature_bind_extra.py',
'mempool_resurrect.py',
'wallet_sweepprivkeys.py',
'wallet_txn_doublespend.py --mineblock',
'tool_cli_bash_completion.py',
'tool_wallet.py --legacy-wallet',

View File

@ -0,0 +1,67 @@
#!/usr/bin/env python3
# Copyright (c) 2014-2022 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Test the sweepprivkeys RPC."""
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import assert_equal, assert_fee_amount
class SweepPrivKeysTest(BitcoinTestFramework):
def add_options(self, parser):
self.add_wallet_options(parser)
def set_test_params(self):
self.num_nodes = 2
def check_balance(self, delta, txid):
node = self.nodes[0]
new_balances = node.getbalances()['mine']
new_balance = new_balances['trusted'] + new_balances['untrusted_pending']
balance_change = new_balance - self.balance
actual_fee = delta - balance_change
tx_vsize = node.getrawtransaction(txid, True)['vsize']
assert_fee_amount(actual_fee, tx_vsize, self.tx_feerate)
self.balance = new_balance
def skip_test_if_missing_module(self):
self.skip_if_no_wallet()
def run_test(self):
node = self.nodes[0]
miner = self.nodes[1]
keys = (
('mkckmmfVv89sW1HUjyRuydGhwFmSaYtRvG', '92YkaycAxLPUqbbV78V9nNngKLnyVd9T8uZuZAzQnc26dJSP4fm'),
('mw8s1FS2Vr7GwQF8bnDVUQHQZq5qWqz5kq', '93VijJgAYnVUGXAfxYhbMHVGVwQUEXK1YnPvcCod3x1RLbzUhXe'),
)
# This test is not meant to test fee estimation and we'd like
# to be sure all txs are sent at a consistent desired feerate
self.tx_feerate = self.nodes[0].getnetworkinfo()['relayfee'] * 2
node.settxfee(self.tx_feerate)
self.generate(miner, 120)
self.balance = node.getbalance('*', 0)
txid = node.sendtoaddress(keys[0][0], 10)
self.check_balance(-10, txid)
# Sweep from mempool
txid = node.sweepprivkeys({'privkeys': (keys[0][1],), 'label': 'test 1'})
assert_equal(node.listtransactions()[-1]['label'], 'test 1')
self.check_balance(10, txid)
txid = node.sendtoaddress(keys[1][0], 5)
self.check_balance(-5, txid)
self.sync_all()
self.generate(miner, 4)
assert_equal(self.balance, node.getbalance('*', 1))
# Sweep from blockchain
txid = node.sweepprivkeys({'privkeys': (keys[1][1],), 'label': 'test 2'})
assert_equal(node.listtransactions()[-1]['label'], 'test 2')
self.check_balance(5, txid)
if __name__ == '__main__':
SweepPrivKeysTest(__file__).main()