From d6b9efae9a1c9c4a992c43b8006bb5ef87eea202 Mon Sep 17 00:00:00 2001 From: Luke Dashjr Date: Sun, 13 Nov 2016 20:19:33 +0000 Subject: [PATCH 1/6] Wallet/RPC: Abstract common WIF privkey parsing into ParseWIFPrivKey function --- src/rpc/misc.cpp | 9 ++----- src/rpc/rawtransaction.cpp | 9 ++----- src/rpc/server.cpp | 18 ++++++++++++++ src/rpc/server.h | 5 ++++ src/wallet/rpcdump.cpp | 60 ++++++++++++---------------------------------- 5 files changed, 42 insertions(+), 59 deletions(-) diff --git a/src/rpc/misc.cpp b/src/rpc/misc.cpp index efff4a99aef..056dca39bbc 100644 --- a/src/rpc/misc.cpp +++ b/src/rpc/misc.cpp @@ -443,13 +443,8 @@ UniValue signmessagewithprivkey(const JSONRPCRequest& request) std::string strPrivkey = request.params[0].get_str(); std::string strMessage = request.params[1].get_str(); - CBitcoinSecret vchSecret; - bool fGood = vchSecret.SetString(strPrivkey); - if (!fGood) - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid private key"); - CKey key = vchSecret.GetKey(); - if (!key.IsValid()) - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Private key outside allowed range"); + CKey key; + ParseWIFPrivKey(strPrivkey, key, nullptr); CHashWriter ss(SER_GETHASH, 0); ss << strMessageMagic; diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index 81005118541..af1be50e57f 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -740,13 +740,8 @@ UniValue signrawtransaction(const JSONRPCRequest& request) UniValue keys = request.params[2].get_array(); for (unsigned int idx = 0; idx < keys.size(); idx++) { UniValue k = keys[idx]; - CBitcoinSecret vchSecret; - bool fGood = vchSecret.SetString(k.get_str()); - if (!fGood) - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid private key"); - CKey key = vchSecret.GetKey(); - if (!key.IsValid()) - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Private key outside allowed range"); + CKey key; + ParseWIFPrivKey(k.get_str(), key, nullptr); tempKeystore.AddKey(key); } } diff --git a/src/rpc/server.cpp b/src/rpc/server.cpp index 9ad8d228faa..2d186e6a0f5 100644 --- a/src/rpc/server.cpp +++ b/src/rpc/server.cpp @@ -154,6 +154,24 @@ std::vector ParseHexO(const UniValue& o, std::string strKey) return ParseHexV(find_value(o, strKey), strKey); } +void ParseWIFPrivKey(const std::string wif_secret, CKey& key, CPubKey* pubkey) +{ + CBitcoinSecret secret; + if (!secret.SetString(wif_secret)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid private key encoding"); + } + + key = secret.GetKey(); + if (!key.IsValid()) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Private key outside allowed range"); + } + + if (pubkey) { + *pubkey = key.GetPubKey(); + assert(key.VerifyPubKey(*pubkey)); + } +} + /** * Note: This interface may still be subject to change. */ diff --git a/src/rpc/server.h b/src/rpc/server.h index dd6f7632456..42f89c5cfa7 100644 --- a/src/rpc/server.h +++ b/src/rpc/server.h @@ -184,7 +184,12 @@ extern uint256 ParseHashO(const UniValue& o, std::string strKey); extern std::vector ParseHexV(const UniValue& v, std::string strName); extern std::vector ParseHexO(const UniValue& o, std::string strKey); +class CKey; +class CPubKey; + extern CAmount AmountFromValue(const UniValue& value); +void ParseWIFPrivKey(const std::string wif_secret, CKey&, CPubKey*); + extern std::string HelpExampleCli(const std::string& methodname, const std::string& args); extern std::string HelpExampleRpc(const std::string& methodname, const std::string& args); diff --git a/src/wallet/rpcdump.cpp b/src/wallet/rpcdump.cpp index 67c6d9ec646..b568bc79e1e 100644 --- a/src/wallet/rpcdump.cpp +++ b/src/wallet/rpcdump.cpp @@ -117,16 +117,9 @@ UniValue importprivkey(const JSONRPCRequest& request) if (fRescan && fPruneMode) throw JSONRPCError(RPC_WALLET_ERROR, "Rescan is disabled in pruned mode"); - CBitcoinSecret vchSecret; - bool fGood = vchSecret.SetString(strSecret); - - if (!fGood) throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid private key encoding"); - - CKey key = vchSecret.GetKey(); - if (!key.IsValid()) throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Private key outside allowed range"); - - CPubKey pubkey = key.GetPubKey(); - assert(key.VerifyPubKey(pubkey)); + CKey key; + CPubKey pubkey; + ParseWIFPrivKey(strSecret, key, &pubkey); CKeyID vchAddress = pubkey.GetID(); { pwallet->MarkDirty(); @@ -496,12 +489,13 @@ UniValue importwallet(const JSONRPCRequest& request) boost::split(vstr, line, boost::is_any_of(" ")); if (vstr.size() < 2) continue; - CBitcoinSecret vchSecret; - if (!vchSecret.SetString(vstr[0])) + CKey key; + CPubKey pubkey; + try { + ParseWIFPrivKey(vstr[0], key, &pubkey); + } catch (...) { continue; - CKey key = vchSecret.GetKey(); - CPubKey pubkey = key.GetPubKey(); - assert(key.VerifyPubKey(pubkey)); + } CKeyID keyid = pubkey.GetID(); if (pwallet->HaveKey(keyid)) { LogPrintf("Skipping import of %s (key already present)\n", CBitcoinAddress(keyid).ToString()); @@ -801,21 +795,9 @@ UniValue ProcessImport(CWallet * const pwallet, const UniValue& data, const int6 for (size_t i = 0; i < keys.size(); i++) { const std::string& privkey = keys[i].get_str(); - CBitcoinSecret vchSecret; - bool fGood = vchSecret.SetString(privkey); - - if (!fGood) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid private key encoding"); - } - - CKey key = vchSecret.GetKey(); - - if (!key.IsValid()) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Private key outside allowed range"); - } - - CPubKey pubkey = key.GetPubKey(); - assert(key.VerifyPubKey(pubkey)); + CKey key; + CPubKey pubkey; + ParseWIFPrivKey(privkey, key, &pubkey); CKeyID vchAddress = pubkey.GetID(); pwallet->MarkDirty(); @@ -909,21 +891,9 @@ UniValue ProcessImport(CWallet * const pwallet, const UniValue& data, const int6 if (keys.size()) { const std::string& strPrivkey = keys[0].get_str(); - // Checks. - CBitcoinSecret vchSecret; - bool fGood = vchSecret.SetString(strPrivkey); - - if (!fGood) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid private key encoding"); - } - - CKey key = vchSecret.GetKey(); - if (!key.IsValid()) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Private key outside allowed range"); - } - - CPubKey pubKey = key.GetPubKey(); - assert(key.VerifyPubKey(pubKey)); + CKey key; + CPubKey pubKey; + ParseWIFPrivKey(strPrivkey, key, &pubKey); CBitcoinAddress pubKeyAddress = CBitcoinAddress(pubKey.GetID()); From 1aac0fa8fe186a63d139e8fe95327a7912b57d15 Mon Sep 17 00:00:00 2001 From: Luke Dashjr Date: Wed, 28 Dec 2016 18:58:20 +0000 Subject: [PATCH 2/6] Document limitations of CCoinsView::Cursor implementations --- src/txdb.h | 4 +++- src/txmempool.h | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/txdb.h b/src/txdb.h index adcbc73380d..7e60201fdd3 100644 --- a/src/txdb.h +++ b/src/txdb.h @@ -63,7 +63,9 @@ struct CDiskTxPos : public CDiskBlockPos } }; -/** CCoinsView backed by the coin database (chainstate/) */ +/** CCoinsView backed by the coin database (chainstate/) + * Cursor requires FlushStateToDisk for consistency. + */ class CCoinsViewDB : public CCoinsView { protected: diff --git a/src/txmempool.h b/src/txmempool.h index 6723ea8e6ca..77bb0aaa98c 100644 --- a/src/txmempool.h +++ b/src/txmempool.h @@ -676,6 +676,9 @@ class CTxMemPool * dependency graph are checked directly in AcceptToMemoryPool. * It also allows you to sign a double-spend directly in signrawtransaction, * 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 { From f2f3792f48c101075728b228050dc75c74d42edc Mon Sep 17 00:00:00 2001 From: Luke Dashjr Date: Sun, 13 Nov 2016 19:46:45 +0000 Subject: [PATCH 3/6] {CCoinsView,CTxMemPool}::FindScriptPubKey to search the UTXO set --- src/coins.cpp | 26 ++++++++++++++++++++++++++ src/coins.h | 6 ++++++ src/txmempool.cpp | 14 ++++++++++++++ src/txmempool.h | 4 ++++ 4 files changed, 50 insertions(+) diff --git a/src/coins.cpp b/src/coins.cpp index e30bda930ae..857961459c2 100644 --- a/src/coins.cpp +++ b/src/coins.cpp @@ -10,6 +10,10 @@ #include +#include +#include +#include + bool CCoinsView::GetCoin(const COutPoint &outpoint, Coin &coin) const { return false; } uint256 CCoinsView::GetBestBlock() const { return uint256(); } std::vector CCoinsView::GetHeadBlocks() const { return std::vector(); } @@ -22,6 +26,28 @@ bool CCoinsView::HaveCoin(const COutPoint &outpoint) const return GetCoin(outpoint, coin); } +void CCoinsView::FindScriptPubKey(CCoinsViewCursor& cursor, const std::set& needles, std::map& out_results) { + for ( ; cursor.Valid(); cursor.Next()) { + COutPoint outpoint; + Coin coin; + if (!cursor.GetKey(outpoint)) { + throw std::runtime_error(std::string(__func__) + ": GetKey failed"); + } else if (!cursor.GetValue(coin)) { + throw std::runtime_error(std::string(__func__) + ": GetValue failed"); + } + if (!needles.count(coin.out.scriptPubKey)) { + continue; + } + out_results.emplace(outpoint, coin); + } +} + +void CCoinsView::FindScriptPubKey(const std::set& needles, std::map& out_results) { + std::unique_ptr pcursor(Cursor()); + FindScriptPubKey(*pcursor, needles, out_results); +} + + CCoinsViewBacked::CCoinsViewBacked(CCoinsView *viewIn) : base(viewIn) { } bool CCoinsViewBacked::GetCoin(const COutPoint &outpoint, Coin &coin) const { return base->GetCoin(outpoint, coin); } bool CCoinsViewBacked::HaveCoin(const COutPoint &outpoint) const { return base->HaveCoin(outpoint); } diff --git a/src/coins.h b/src/coins.h index efb5ce869c3..dd0d21a4bac 100644 --- a/src/coins.h +++ b/src/coins.h @@ -17,6 +17,8 @@ #include #include +#include +#include #include /** @@ -163,6 +165,10 @@ class CCoinsView //! the old block hash, in that order. virtual std::vector GetHeadBlocks() const; + //! Search for a given set of pubkey scripts + static void FindScriptPubKey(CCoinsViewCursor& cursor, const std::set& needles, std::map& out_results); + void FindScriptPubKey(const std::set& needles, std::map& out_results); + //! Do a bulk modification (multiple Coin changes + BestBlock change). //! The passed mapCoins can be modified. virtual bool BatchWrite(CCoinsMap &mapCoins, const uint256 &hashBlock); diff --git a/src/txmempool.cpp b/src/txmempool.cpp index 4a81055231b..5d7a100a63d 100644 --- a/src/txmempool.cpp +++ b/src/txmempool.cpp @@ -835,6 +835,20 @@ TxMempoolInfo CTxMemPool::info(const uint256& hash) const return GetInfo(i); } +void CTxMemPool::FindScriptPubKey(const std::set& needles, std::map& out_results) { + LOCK(cs); + for (const CTxMemPoolEntry& entry : mapTx) { + const CTransaction& tx = entry.GetTx(); + const uint256& hash = tx.GetHash(); + for (size_t txo_index = tx.vout.size(); txo_index-- > 0; ) { + const CTxOut& txo = tx.vout[txo_index]; + if (needles.count(txo.scriptPubKey)) { + out_results.emplace(COutPoint(hash, txo_index), Coin(txo, MEMPOOL_HEIGHT, false)); + } + } + } +} + void CTxMemPool::PrioritiseTransaction(const uint256& hash, const CAmount& nFeeDelta) { { diff --git a/src/txmempool.h b/src/txmempool.h index 77bb0aaa98c..59dc2b4f119 100644 --- a/src/txmempool.h +++ b/src/txmempool.h @@ -6,6 +6,7 @@ #ifndef BITCOIN_TXMEMPOOL_H #define BITCOIN_TXMEMPOOL_H +#include #include #include #include @@ -29,6 +30,7 @@ #include class CBlockIndex; +class CScript; /** Fake height value used in Coin to signify they are only in the memory pool (since 0.8) */ static const uint32_t MEMPOOL_HEIGHT = 0x7FFFFFFF; @@ -622,6 +624,8 @@ class CTxMemPool TxMempoolInfo info(const uint256& hash) const; std::vector infoAll() const; + void FindScriptPubKey(const std::set& needles, std::map& out_results); + size_t DynamicMemoryUsage() const; boost::signals2::signal NotifyEntryAdded; From 2670b2fb4766372e6abe6e76cf3c08f1577f49ea Mon Sep 17 00:00:00 2001 From: Luke Dashjr Date: Sun, 13 Nov 2016 22:05:20 +0000 Subject: [PATCH 4/6] Wallet/RPC: sweepprivkeys method to scan UTXO set and send to local wallet --- src/rpc/client.cpp | 1 + src/wallet/rpcwallet.cpp | 154 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+) diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 41794537824..a6f604d1a7e 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -74,6 +74,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "sendmany", 4, "subtractfeefrom" }, { "sendmany", 5 , "replaceable" }, { "sendmany", 6 , "conf_target" }, + { "sweepprivkeys", 0, "options" }, { "addmultisigaddress", 0, "nrequired" }, { "addmultisigaddress", 1, "keys" }, { "createmultisig", 0, "nrequired" }, diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index a6176c3485e..565647048e1 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -20,6 +20,7 @@ #include "rpc/server.h" #include "script/sign.h" #include "timedata.h" +#include "txdb.h" #include "util.h" #include "utilmoneystr.h" #include "wallet/coincontrol.h" @@ -1064,6 +1065,158 @@ UniValue sendmany(const JSONRPCRequest& request) return wtx.GetHash().GetHex(); } +UniValue sweepprivkeys(const JSONRPCRequest& request) +{ + CWallet * const pwallet = GetWalletForJSONRPCRequest(request); + if (!EnsureWalletIsAvailable(pwallet, request.fHelp)) { + return NullUniValue; + } + + if (request.fHelp || request.params.size() != 1) + throw std::runtime_error( + "sweepprivkeys {\"privkeys\": [\"bitcoinprivkey\",...], other options}\n" + "\nSends bitcoins controlled by private key to specified destinations.\n" + "\nOptions:\n" + " \"privkeys\":[\"bitcoinprivkey\",...] (array of strings, required) An array of WIF private key(s)\n" + " \"label\":\"actuallabelname\" (string, optional) Label for received bitcoins\n" + ); + + // NOTE: It isn't safe to sweep-and-send in a single action, since this would leave the send missing from the transaction history + + RPCTypeCheck(request.params, {UniValue::VOBJ}); + + // Parse options + std::set needles; + CCoinControl coin_control; + CBasicKeyStore tempKeystore; + 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; + CPubKey pubkey; + ParseWIFPrivKey(wif_secret, key, &pubkey); + + tempKeystore.AddKey(key); + CKeyID address = pubkey.GetID(); + CScript script = GetScriptForDestination(address); + if (!script.empty()) { + needles.insert(script); + } + script = GetScriptForRawPubKey(pubkey); + if (!script.empty()) { + needles.insert(script); + } + } + } else if (optname == "label") { + label = AccountFromValue(optval.get_str()); + } else { + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Unrecognised option '%s'", optname)); + } + } + + // Ensure keypool is filled if possible + { + LOCK2(cs_main, pwallet->cs_wallet); + + if (!pwallet->IsLocked()) { + pwallet->TopUpKeyPool(); + } + } + + // Reserve the key we will be using + CReserveKey reservekey(pwallet); + CPubKey pubkey; + if (!reservekey.GetReservedKey(pubkey)) { + throw JSONRPCError(RPC_WALLET_KEYPOOL_RAN_OUT, "Error: Keypool ran out, please call keypoolrefill first"); + } + + // Scan UTXO set for inputs + std::vector input_txos; + { + // Collect all possible inputs + std::map coins; + { + LOCK(cs_main); + mempool.FindScriptPubKey(needles, coins); + FlushStateToDisk(); + pcoinsdbview->FindScriptPubKey(needles, coins); + } + + // 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"); + } + + CKeyID keyID = pubkey.GetID(); + CTxDestination txdest = CBitcoinAddress(keyID).Get(); + + tx.vout.emplace_back(total_in, GetScriptForDestination(txdest)); + + while (true) { + if (IsDust(tx.vout[0], ::minRelayTxFee)) { + throw JSONRPCError(RPC_VERIFY_REJECTED, "Swept value would be dust"); + } + for (size_t input_index = 0; input_index < tx.vin.size(); ++input_index) { + SignatureData sigdata; + if (!ProduceSignature(MutableTransactionSignatureCreator(&tempKeystore, &tx, input_index, input_txos[input_index].nValue, SIGHASH_ALL), input_txos[input_index].scriptPubKey, sigdata)) { + throw JSONRPCError(RPC_MISC_ERROR, "Failed to sign"); + } + UpdateTransaction(tx, input_index, sigdata); + } + int64_t tx_vsize = GetVirtualTransactionSize(tx); + CAmount fee_needed = pwallet->GetMinimumFee(tx_vsize, coin_control, ::mempool, ::feeEstimator, nullptr); + 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(keyID, label, "receive"); + + CValidationState state; + bool rv; + { + LOCK(cs_main); + rv = AcceptToMemoryPool(mempool, state, final_tx, true, NULL, NULL, false, maxTxFee); + } + if (!rv) { + pwallet->DelAddressBook(keyID); + if (state.IsInvalid()) { + throw JSONRPCError(RPC_TRANSACTION_REJECTED, strprintf("%i: %s", state.GetRejectCode(), state.GetRejectReason())); + } else { + throw JSONRPCError(RPC_TRANSACTION_ERROR, state.GetRejectReason()); + } + } + reservekey.KeepKey(); + + CInv inv(MSG_TX, final_tx->GetHash()); + g_connman->ForEachNode([&inv](CNode* pnode) + { + pnode->PushInventory(inv); + }); + + return final_tx->GetHash().GetHex(); +} + // Defined in rpc/misc.cpp extern CScript _createmultisig_redeemScript(CWallet * const pwallet, const UniValue& params); @@ -3189,6 +3342,7 @@ static const CRPCCommand commands[] = { "wallet", "sendfrom", &sendfrom, false, {"fromaccount","toaddress","amount","minconf","comment","comment_to"} }, { "wallet", "sendmany", &sendmany, false, {"fromaccount","amounts","minconf","comment","subtractfeefrom","replaceable","conf_target","estimate_mode"} }, { "wallet", "sendtoaddress", &sendtoaddress, false, {"address","amount","comment","comment_to","subtractfeefromamount","replaceable","conf_target","estimate_mode"} }, + { "wallet", "sweepprivkeys", &sweepprivkeys, false, {"options"} }, { "wallet", "setaccount", &setaccount, true, {"address","account"} }, { "wallet", "settxfee", &settxfee, true, {"amount"} }, { "wallet", "signmessage", &signmessage, true, {"address","message"} }, From d067b178570f4758d6ad23b35e61e2e6b351786f Mon Sep 17 00:00:00 2001 From: Luke Dashjr Date: Thu, 29 Dec 2016 13:22:03 +0000 Subject: [PATCH 5/6] GUI/RPCConsole: Include sweepprivkeys in history sensitive-command filter --- src/qt/rpcconsole.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/qt/rpcconsole.cpp b/src/qt/rpcconsole.cpp index 3590a98efac..2886b74fb92 100644 --- a/src/qt/rpcconsole.cpp +++ b/src/qt/rpcconsole.cpp @@ -71,6 +71,7 @@ const QStringList historyFilter = QStringList() << "importmulti" << "signmessagewithprivkey" << "signrawtransaction" + << "sweepprivkeys" << "walletpassphrase" << "walletpassphrasechange" << "encryptwallet"; From f0d612bd193cc642cf259e1f4f026a6d60927b90 Mon Sep 17 00:00:00 2001 From: Luke Dashjr Date: Sat, 19 Aug 2017 03:54:38 +0000 Subject: [PATCH 6/6] QA: Functional test for sweepprivkeys --- test/functional/sweepprivkeys.py | 67 ++++++++++++++++++++++++++++++++++++++++ test/functional/test_runner.py | 1 + 2 files changed, 68 insertions(+) create mode 100755 test/functional/sweepprivkeys.py diff --git a/test/functional/sweepprivkeys.py b/test/functional/sweepprivkeys.py new file mode 100755 index 00000000000..40c748b9e4f --- /dev/null +++ b/test/functional/sweepprivkeys.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +# Copyright (c) 2014-2017 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.""" + +import decimal +import math + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal, assert_fee_amount + +class SweepPrivKeysTest(BitcoinTestFramework): + def __init__(self): + super().__init__() + self.num_nodes = 2 + + def check_balance(self, delta, txid): + node = self.nodes[0] + new_balance = node.getbalance('*', 0) + 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 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) + + miner.generate(120) + self.sync_all() + self.balance = node.getbalance('*', 0) + + txid = node.sendtoaddress(keys[0][0], 10) + self.check_balance(-10, txid) + + # Sweep from mempool + import json + 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() + miner.generate(4) + self.sync_all() + 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().main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 93f180555d2..12a8a136e4a 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -82,6 +82,7 @@ 'keypool-topup.py', 'zmq_test.py', 'mempool_resurrect_test.py', + 'sweepprivkeys.py', 'txn_doublespend.py --mineblock', 'txn_clone.py', 'getchaintips.py',