mirror of
https://github.com/Retropex/bitcoin.git
synced 2025-05-12 19:20:42 +02:00
Merge 9152 via sweepprivkeys
This commit is contained in:
commit
647dcf1fe7
@ -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;
|
||||
|
||||
|
@ -77,6 +77,7 @@ const QStringList historyFilter = QStringList()
|
||||
<< "sethdseed"
|
||||
<< "signmessagewithprivkey"
|
||||
<< "signrawtransactionwithkey"
|
||||
<< "sweepprivkeys"
|
||||
<< "walletpassphrase"
|
||||
<< "walletpassphrasechange"
|
||||
<< "encryptwallet";
|
||||
|
@ -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);
|
||||
|
@ -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" },
|
||||
|
@ -184,6 +184,7 @@ const std::vector<std::string> RPC_COMMANDS_SAFE_FOR_FUZZING{
|
||||
"submitblock",
|
||||
"submitheader",
|
||||
"submitpackage",
|
||||
"sweepprivkeys",
|
||||
"syncwithvalidationinterfacequeue",
|
||||
"testmempoolaccept",
|
||||
"uptime",
|
||||
|
@ -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:
|
||||
|
@ -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()};
|
||||
}
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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()));
|
||||
|
@ -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',
|
||||
|
67
test/functional/wallet_sweepprivkeys.py
Executable file
67
test/functional/wallet_sweepprivkeys.py
Executable 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()
|
Loading…
Reference in New Issue
Block a user