Browse Source

Merge #8456: [RPC] Simplified bumpfee command.

cc0243a [RPC] bumpfee (mrbandrews)
52dde66 [wallet] Add include_unsafe argument to listunspent RPC (Russell Yanofsky)
766e8a4 [wallet] Add IsAllFromMe: true if all inputs are from wallet (Suhas Daftuar)
0.14
Wladimir J. van der Laan 8 years ago
parent
commit
2ef52d3cf1
No known key found for this signature in database
GPG Key ID: 74810B012346C9A6
  1. 1
      qa/pull-tester/rpc-tests.py
  2. 317
      qa/rpc-tests/bumpfee.py
  3. 1
      src/rpc/client.cpp
  4. 14
      src/rpc/server.cpp
  5. 5
      src/rpc/server.h
  6. 289
      src/wallet/rpcwallet.cpp
  7. 90
      src/wallet/wallet.cpp
  8. 12
      src/wallet/wallet.h

1
qa/pull-tester/rpc-tests.py

@ -151,6 +151,7 @@ testScripts = [
'signmessages.py', 'signmessages.py',
'nulldummy.py', 'nulldummy.py',
'import-rescan.py', 'import-rescan.py',
'bumpfee.py',
'rpcnamedargs.py', 'rpcnamedargs.py',
] ]
if ENABLE_ZMQ: if ENABLE_ZMQ:

317
qa/rpc-tests/bumpfee.py

@ -0,0 +1,317 @@
#!/usr/bin/env python3
# Copyright (c) 2016 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
from segwit import send_to_witness
from test_framework.test_framework import BitcoinTestFramework
from test_framework import blocktools
from test_framework.mininode import CTransaction
from test_framework.util import *
from test_framework.util import *
import io
import time
# Sequence number that is BIP 125 opt-in and BIP 68-compliant
BIP125_SEQUENCE_NUMBER = 0xfffffffd
WALLET_PASSPHRASE = "test"
WALLET_PASSPHRASE_TIMEOUT = 3600
class BumpFeeTest(BitcoinTestFramework):
def __init__(self):
super().__init__()
self.num_nodes = 2
self.setup_clean_chain = True
def setup_network(self, split=False):
extra_args = [["-debug", "-prematurewitness", "-walletprematurewitness", "-walletrbf={}".format(i)]
for i in range(self.num_nodes)]
self.nodes = start_nodes(self.num_nodes, self.options.tmpdir, extra_args)
# Encrypt wallet for test_locked_wallet_fails test
self.nodes[1].encryptwallet(WALLET_PASSPHRASE)
bitcoind_processes[1].wait()
self.nodes[1] = start_node(1, self.options.tmpdir, extra_args[1])
self.nodes[1].walletpassphrase(WALLET_PASSPHRASE, WALLET_PASSPHRASE_TIMEOUT)
connect_nodes_bi(self.nodes, 0, 1)
self.is_network_split = False
self.sync_all()
def run_test(self):
peer_node, rbf_node = self.nodes
rbf_node_address = rbf_node.getnewaddress()
# fund rbf node with 10 coins of 0.001 btc (100,000 satoshis)
print("Mining blocks...")
peer_node.generate(110)
self.sync_all()
for i in range(25):
peer_node.sendtoaddress(rbf_node_address, 0.001)
self.sync_all()
peer_node.generate(1)
self.sync_all()
assert_equal(rbf_node.getbalance(), Decimal("0.025"))
print("Running tests")
dest_address = peer_node.getnewaddress()
test_small_output_fails(rbf_node, dest_address)
test_dust_to_fee(rbf_node, dest_address)
test_simple_bumpfee_succeeds(rbf_node, peer_node, dest_address)
test_segwit_bumpfee_succeeds(rbf_node, dest_address)
test_nonrbf_bumpfee_fails(peer_node, dest_address)
test_notmine_bumpfee_fails(rbf_node, peer_node, dest_address)
test_bumpfee_with_descendant_fails(rbf_node, rbf_node_address, dest_address)
test_settxfee(rbf_node, dest_address)
test_rebumping(rbf_node, dest_address)
test_rebumping_not_replaceable(rbf_node, dest_address)
test_unconfirmed_not_spendable(rbf_node, rbf_node_address)
test_locked_wallet_fails(rbf_node, dest_address)
print("Success")
def test_simple_bumpfee_succeeds(rbf_node, peer_node, dest_address):
rbfid = create_fund_sign_send(rbf_node, {dest_address: 0.00090000})
rbftx = rbf_node.gettransaction(rbfid)
sync_mempools((rbf_node, peer_node))
assert rbfid in rbf_node.getrawmempool() and rbfid in peer_node.getrawmempool()
bumped_tx = rbf_node.bumpfee(rbfid)
assert bumped_tx["fee"] - abs(rbftx["fee"]) > 0
# check that bumped_tx propogates, original tx was evicted and has a wallet conflict
sync_mempools((rbf_node, peer_node))
assert bumped_tx["txid"] in rbf_node.getrawmempool()
assert bumped_tx["txid"] in peer_node.getrawmempool()
assert rbfid not in rbf_node.getrawmempool()
assert rbfid not in peer_node.getrawmempool()
oldwtx = rbf_node.gettransaction(rbfid)
assert len(oldwtx["walletconflicts"]) > 0
# check wallet transaction replaces and replaced_by values
bumpedwtx = rbf_node.gettransaction(bumped_tx["txid"])
assert_equal(oldwtx["replaced_by_txid"], bumped_tx["txid"])
assert_equal(bumpedwtx["replaces_txid"], rbfid)
def test_segwit_bumpfee_succeeds(rbf_node, dest_address):
# Create a transaction with segwit output, then create an RBF transaction
# which spends it, and make sure bumpfee can be called on it.
segwit_in = next(u for u in rbf_node.listunspent() if u["amount"] == Decimal("0.001"))
segwit_out = rbf_node.validateaddress(rbf_node.getnewaddress())
rbf_node.addwitnessaddress(segwit_out["address"])
segwitid = send_to_witness(
version=0,
node=rbf_node,
utxo=segwit_in,
pubkey=segwit_out["pubkey"],
encode_p2sh=False,
amount=Decimal("0.0009"),
sign=True)
rbfraw = rbf_node.createrawtransaction([{
'txid': segwitid,
'vout': 0,
"sequence": BIP125_SEQUENCE_NUMBER
}], {dest_address: Decimal("0.0005"),
get_change_address(rbf_node): Decimal("0.0003")})
rbfsigned = rbf_node.signrawtransaction(rbfraw)
rbfid = rbf_node.sendrawtransaction(rbfsigned["hex"])
assert rbfid in rbf_node.getrawmempool()
bumped_tx = rbf_node.bumpfee(rbfid)
assert bumped_tx["txid"] in rbf_node.getrawmempool()
assert rbfid not in rbf_node.getrawmempool()
def test_nonrbf_bumpfee_fails(peer_node, dest_address):
# cannot replace a non RBF transaction (from node which did not enable RBF)
not_rbfid = create_fund_sign_send(peer_node, {dest_address: 0.00090000})
assert_raises_message(JSONRPCException, "not BIP 125 replaceable", peer_node.bumpfee, not_rbfid)
def test_notmine_bumpfee_fails(rbf_node, peer_node, dest_address):
# cannot bump fee unless the tx has only inputs that we own.
# here, the rbftx has a peer_node coin and then adds a rbf_node input
# Note that this test depends upon the RPC code checking input ownership prior to change outputs
# (since it can't use fundrawtransaction, it lacks a proper change output)
utxos = [node.listunspent()[-1] for node in (rbf_node, peer_node)]
inputs = [{
"txid": utxo["txid"],
"vout": utxo["vout"],
"address": utxo["address"],
"sequence": BIP125_SEQUENCE_NUMBER
} for utxo in utxos]
output_val = sum(utxo["amount"] for utxo in utxos) - Decimal("0.001")
rawtx = rbf_node.createrawtransaction(inputs, {dest_address: output_val})
signedtx = rbf_node.signrawtransaction(rawtx)
signedtx = peer_node.signrawtransaction(signedtx["hex"])
rbfid = rbf_node.sendrawtransaction(signedtx["hex"])
assert_raises_message(JSONRPCException, "Transaction contains inputs that don't belong to this wallet",
rbf_node.bumpfee, rbfid)
def test_bumpfee_with_descendant_fails(rbf_node, rbf_node_address, dest_address):
# cannot bump fee if the transaction has a descendant
# parent is send-to-self, so we don't have to check which output is change when creating the child tx
parent_id = create_fund_sign_send(rbf_node, {rbf_node_address: 0.00050000})
tx = rbf_node.createrawtransaction([{"txid": parent_id, "vout": 0}], {dest_address: 0.00020000})
tx = rbf_node.signrawtransaction(tx)
txid = rbf_node.sendrawtransaction(tx["hex"])
assert_raises_message(JSONRPCException, "Transaction has descendants in the wallet", rbf_node.bumpfee, parent_id)
def test_small_output_fails(rbf_node, dest_address):
# cannot bump fee with a too-small output
rbfid = spend_one_input(rbf_node,
Decimal("0.00100000"),
{dest_address: 0.00080000,
get_change_address(rbf_node): Decimal("0.00010000")})
rbf_node.bumpfee(rbfid, {"totalFee": 20000})
rbfid = spend_one_input(rbf_node,
Decimal("0.00100000"),
{dest_address: 0.00080000,
get_change_address(rbf_node): Decimal("0.00010000")})
assert_raises_message(JSONRPCException, "Change output is too small", rbf_node.bumpfee, rbfid, {"totalFee": 20001})
def test_dust_to_fee(rbf_node, dest_address):
# check that if output is reduced to dust, it will be converted to fee
# the bumped tx sets fee=9900, but it converts to 10,000
rbfid = spend_one_input(rbf_node,
Decimal("0.00100000"),
{dest_address: 0.00080000,
get_change_address(rbf_node): Decimal("0.00010000")})
fulltx = rbf_node.getrawtransaction(rbfid, 1)
bumped_tx = rbf_node.bumpfee(rbfid, {"totalFee": 19900})
full_bumped_tx = rbf_node.getrawtransaction(bumped_tx["txid"], 1)
assert_equal(bumped_tx["fee"], Decimal("0.00020000"))
assert_equal(len(fulltx["vout"]), 2)
assert_equal(len(full_bumped_tx["vout"]), 1) #change output is eliminated
def test_settxfee(rbf_node, dest_address):
# check that bumpfee reacts correctly to the use of settxfee (paytxfee)
# increase feerate by 2.5x, test that fee increased at least 2x
rbf_node.settxfee(Decimal("0.00001000"))
rbfid = create_fund_sign_send(rbf_node, {dest_address: 0.00090000})
rbftx = rbf_node.gettransaction(rbfid)
rbf_node.settxfee(Decimal("0.00002500"))
bumped_tx = rbf_node.bumpfee(rbfid)
assert bumped_tx["fee"] > 2 * abs(rbftx["fee"])
rbf_node.settxfee(Decimal("0.00000000")) # unset paytxfee
def test_rebumping(rbf_node, dest_address):
# check that re-bumping the original tx fails, but bumping the bumper succeeds
rbf_node.settxfee(Decimal("0.00001000"))
rbfid = create_fund_sign_send(rbf_node, {dest_address: 0.00090000})
bumped = rbf_node.bumpfee(rbfid, {"totalFee": 1000})
assert_raises_message(JSONRPCException, "already bumped", rbf_node.bumpfee, rbfid, {"totalFee": 2000})
rbf_node.bumpfee(bumped["txid"], {"totalFee": 2000})
def test_rebumping_not_replaceable(rbf_node, dest_address):
# check that re-bumping a non-replaceable bump tx fails
rbfid = create_fund_sign_send(rbf_node, {dest_address: 0.00090000})
bumped = rbf_node.bumpfee(rbfid, {"totalFee": 10000, "replaceable": False})
assert_raises_message(JSONRPCException, "Transaction is not BIP 125 replaceable", rbf_node.bumpfee, bumped["txid"],
{"totalFee": 20000})
def test_unconfirmed_not_spendable(rbf_node, rbf_node_address):
# check that unconfirmed outputs from bumped transactions are not spendable
rbfid = create_fund_sign_send(rbf_node, {rbf_node_address: 0.00090000})
rbftx = rbf_node.gettransaction(rbfid)["hex"]
assert rbfid in rbf_node.getrawmempool()
bumpid = rbf_node.bumpfee(rbfid)["txid"]
assert bumpid in rbf_node.getrawmempool()
assert rbfid not in rbf_node.getrawmempool()
# check that outputs from the bump transaction are not spendable
# due to the replaces_txid check in CWallet::AvailableCoins
assert_equal([t for t in rbf_node.listunspent(minconf=0, include_unsafe=False) if t["txid"] == bumpid], [])
# submit a block with the rbf tx to clear the bump tx out of the mempool,
# then call abandon to make sure the wallet doesn't attempt to resubmit the
# bump tx, then invalidate the block so the rbf tx will be put back in the
# mempool. this makes it possible to check whether the rbf tx outputs are
# spendable before the rbf tx is confirmed.
block = submit_block_with_tx(rbf_node, rbftx)
rbf_node.abandontransaction(bumpid)
rbf_node.invalidateblock(block.hash)
assert bumpid not in rbf_node.getrawmempool()
assert rbfid in rbf_node.getrawmempool()
# check that outputs from the rbf tx are not spendable before the
# transaction is confirmed, due to the replaced_by_txid check in
# CWallet::AvailableCoins
assert_equal([t for t in rbf_node.listunspent(minconf=0, include_unsafe=False) if t["txid"] == rbfid], [])
# check that the main output from the rbf tx is spendable after confirmed
rbf_node.generate(1)
assert_equal(
sum(1 for t in rbf_node.listunspent(minconf=0, include_unsafe=False)
if t["txid"] == rbfid and t["address"] == rbf_node_address and t["spendable"]), 1)
def test_locked_wallet_fails(rbf_node, dest_address):
rbfid = create_fund_sign_send(rbf_node, {dest_address: 0.00090000})
rbf_node.walletlock()
assert_raises_message(JSONRPCException, "Please enter the wallet passphrase with walletpassphrase first.",
rbf_node.bumpfee, rbfid)
def create_fund_sign_send(node, outputs):
rawtx = node.createrawtransaction([], outputs)
fundtx = node.fundrawtransaction(rawtx)
signedtx = node.signrawtransaction(fundtx["hex"])
txid = node.sendrawtransaction(signedtx["hex"])
return txid
def spend_one_input(node, input_amount, outputs):
input = dict(sequence=BIP125_SEQUENCE_NUMBER, **next(u for u in node.listunspent() if u["amount"] == input_amount))
rawtx = node.createrawtransaction([input], outputs)
signedtx = node.signrawtransaction(rawtx)
txid = node.sendrawtransaction(signedtx["hex"])
return txid
def get_change_address(node):
"""Get a wallet change address.
There is no wallet RPC to access unused change addresses, so this creates a
dummy transaction, calls fundrawtransaction to give add an input and change
output, then returns the change address."""
dest_address = node.getnewaddress()
dest_amount = Decimal("0.00012345")
rawtx = node.createrawtransaction([], {dest_address: dest_amount})
fundtx = node.fundrawtransaction(rawtx)
info = node.decoderawtransaction(fundtx["hex"])
return next(address for out in info["vout"]
if out["value"] != dest_amount for address in out["scriptPubKey"]["addresses"])
def submit_block_with_tx(node, tx):
ctx = CTransaction()
ctx.deserialize(io.BytesIO(hex_str_to_bytes(tx)))
tip = node.getbestblockhash()
height = node.getblockcount() + 1
block_time = node.getblockheader(tip)["mediantime"] + 1
block = blocktools.create_block(int(tip, 16), blocktools.create_coinbase(height), block_time)
block.vtx.append(ctx)
block.rehash()
block.hashMerkleRoot = block.calc_merkle_root()
block.solve()
error = node.submitblock(bytes_to_hex_str(block.serialize(True)))
if error is not None:
raise Exception(error)
return block
if __name__ == "__main__":
BumpFeeTest().main()

1
src/rpc/client.cpp

@ -117,6 +117,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "setnetworkactive", 0, "state" }, { "setnetworkactive", 0, "state" },
{ "getmempoolancestors", 1, "verbose" }, { "getmempoolancestors", 1, "verbose" },
{ "getmempooldescendants", 1, "verbose" }, { "getmempooldescendants", 1, "verbose" },
{ "bumpfee", 1, "options" },
// Echo with conversion (For testing only) // Echo with conversion (For testing only)
{ "echojson", 0, "arg0" }, { "echojson", 0, "arg0" },
{ "echojson", 1, "arg1" }, { "echojson", 1, "arg1" },

14
src/rpc/server.cpp

@ -79,16 +79,20 @@ void RPCTypeCheck(const UniValue& params,
break; break;
const UniValue& v = params[i]; const UniValue& v = params[i];
if (!((v.type() == t) || (fAllowNull && (v.isNull())))) if (!(fAllowNull && v.isNull())) {
{ RPCTypeCheckArgument(v, t);
string err = strprintf("Expected type %s, got %s",
uvTypeName(t), uvTypeName(v.type()));
throw JSONRPCError(RPC_TYPE_ERROR, err);
} }
i++; i++;
} }
} }
void RPCTypeCheckArgument(const UniValue& value, UniValue::VType typeExpected)
{
if (value.type() != typeExpected) {
throw JSONRPCError(RPC_TYPE_ERROR, strprintf("Expected type %s, got %s", uvTypeName(typeExpected), uvTypeName(value.type())));
}
}
void RPCTypeCheckObj(const UniValue& o, void RPCTypeCheckObj(const UniValue& o,
const map<string, UniValueType>& typesExpected, const map<string, UniValueType>& typesExpected,
bool fAllowNull, bool fAllowNull,

5
src/rpc/server.h

@ -78,6 +78,11 @@ bool RPCIsInWarmup(std::string *statusOut);
void RPCTypeCheck(const UniValue& params, void RPCTypeCheck(const UniValue& params,
const std::list<UniValue::VType>& typesExpected, bool fAllowNull=false); const std::list<UniValue::VType>& typesExpected, bool fAllowNull=false);
/**
* Type-check one argument; throws JSONRPCError if wrong type given.
*/
void RPCTypeCheckArgument(const UniValue& value, UniValue::VType typeExpected);
/* /*
Check for expected keys/value types in an Object. Check for expected keys/value types in an Object.
*/ */

289
src/wallet/rpcwallet.cpp

@ -11,8 +11,10 @@
#include "init.h" #include "init.h"
#include "validation.h" #include "validation.h"
#include "net.h" #include "net.h"
#include "policy/policy.h"
#include "policy/rbf.h" #include "policy/rbf.h"
#include "rpc/server.h" #include "rpc/server.h"
#include "script/sign.h"
#include "timedata.h" #include "timedata.h"
#include "util.h" #include "util.h"
#include "utilmoneystr.h" #include "utilmoneystr.h"
@ -2364,9 +2366,9 @@ UniValue listunspent(const JSONRPCRequest& request)
if (!EnsureWalletIsAvailable(request.fHelp)) if (!EnsureWalletIsAvailable(request.fHelp))
return NullUniValue; return NullUniValue;
if (request.fHelp || request.params.size() > 3) if (request.fHelp || request.params.size() > 4)
throw runtime_error( throw runtime_error(
"listunspent ( minconf maxconf [\"addresses\",...] )\n" "listunspent ( minconf maxconf [\"addresses\",...] [include_unsafe] )\n"
"\nReturns array of unspent transaction outputs\n" "\nReturns array of unspent transaction outputs\n"
"with between minconf and maxconf (inclusive) confirmations.\n" "with between minconf and maxconf (inclusive) confirmations.\n"
"Optionally filter to only include txouts paid to specified addresses.\n" "Optionally filter to only include txouts paid to specified addresses.\n"
@ -2378,6 +2380,10 @@ UniValue listunspent(const JSONRPCRequest& request)
" \"address\" (string) bitcoin address\n" " \"address\" (string) bitcoin address\n"
" ,...\n" " ,...\n"
" ]\n" " ]\n"
"4. include_unsafe (bool, optional, default=true) Include outputs that are not safe to spend\n"
" because they come from unconfirmed untrusted transactions or unconfirmed\n"
" replacement transactions (cases where we are less sure that a conflicting\n"
" transaction won't be mined).\n"
"\nResult\n" "\nResult\n"
"[ (array of json object)\n" "[ (array of json object)\n"
" {\n" " {\n"
@ -2401,18 +2407,21 @@ UniValue listunspent(const JSONRPCRequest& request)
+ HelpExampleRpc("listunspent", "6, 9999999 \"[\\\"1PGFqEzfmQch1gKD3ra4k18PNj3tTUUSqg\\\",\\\"1LtvqCaApEdUGFkpKMM4MstjcaL4dKg8SP\\\"]\"") + HelpExampleRpc("listunspent", "6, 9999999 \"[\\\"1PGFqEzfmQch1gKD3ra4k18PNj3tTUUSqg\\\",\\\"1LtvqCaApEdUGFkpKMM4MstjcaL4dKg8SP\\\"]\"")
); );
RPCTypeCheck(request.params, boost::assign::list_of(UniValue::VNUM)(UniValue::VNUM)(UniValue::VARR));
int nMinDepth = 1; int nMinDepth = 1;
if (request.params.size() > 0) if (request.params.size() > 0 && !request.params[0].isNull()) {
RPCTypeCheckArgument(request.params[0], UniValue::VNUM);
nMinDepth = request.params[0].get_int(); nMinDepth = request.params[0].get_int();
}
int nMaxDepth = 9999999; int nMaxDepth = 9999999;
if (request.params.size() > 1) if (request.params.size() > 1 && !request.params[1].isNull()) {
RPCTypeCheckArgument(request.params[1], UniValue::VNUM);
nMaxDepth = request.params[1].get_int(); nMaxDepth = request.params[1].get_int();
}
set<CBitcoinAddress> setAddress; set<CBitcoinAddress> setAddress;
if (request.params.size() > 2) { if (request.params.size() > 2 && !request.params[2].isNull()) {
RPCTypeCheckArgument(request.params[2], UniValue::VARR);
UniValue inputs = request.params[2].get_array(); UniValue inputs = request.params[2].get_array();
for (unsigned int idx = 0; idx < inputs.size(); idx++) { for (unsigned int idx = 0; idx < inputs.size(); idx++) {
const UniValue& input = inputs[idx]; const UniValue& input = inputs[idx];
@ -2425,11 +2434,17 @@ UniValue listunspent(const JSONRPCRequest& request)
} }
} }
bool include_unsafe = true;
if (request.params.size() > 3 && !request.params[3].isNull()) {
RPCTypeCheckArgument(request.params[3], UniValue::VBOOL);
include_unsafe = request.params[3].get_bool();
}
UniValue results(UniValue::VARR); UniValue results(UniValue::VARR);
vector<COutput> vecOutputs; vector<COutput> vecOutputs;
assert(pwalletMain != NULL); assert(pwalletMain != NULL);
LOCK2(cs_main, pwalletMain->cs_wallet); LOCK2(cs_main, pwalletMain->cs_wallet);
pwalletMain->AvailableCoins(vecOutputs, false, NULL, true); pwalletMain->AvailableCoins(vecOutputs, !include_unsafe, NULL, true);
BOOST_FOREACH(const COutput& out, vecOutputs) { BOOST_FOREACH(const COutput& out, vecOutputs) {
if (out.nDepth < nMinDepth || out.nDepth > nMaxDepth) if (out.nDepth < nMinDepth || out.nDepth > nMaxDepth)
continue; continue;
@ -2619,6 +2634,261 @@ UniValue fundrawtransaction(const JSONRPCRequest& request)
return result; return result;
} }
UniValue bumpfee(const JSONRPCRequest& request)
{
if (!EnsureWalletIsAvailable(request.fHelp)) {
return NullUniValue;
}
if (request.fHelp || request.params.size() < 1 || request.params.size() > 2) {
throw runtime_error(
"bumpfee \"txid\" ( options ) \n"
"\nBumps the fee of an opt-in-RBF transaction T, replacing it with a new transaction B.\n"
"An opt-in RBF transaction with the given txid must be in the wallet.\n"
"The command will pay the additional fee by decreasing (or perhaps removing) its change output.\n"
"If the change output is not big enough to cover the increased fee, the command will currently fail\n"
"instead of adding new inputs to compensate. (A future implementation could improve this.)\n"
"The command will fail if the wallet or mempool contains a transaction that spends one of T's outputs.\n"
"By default, the new fee will be calculated automatically using estimatefee.\n"
"The user can specify a confirmation target for estimatefee.\n"
"Alternatively, the user can specify totalFee, or use RPC setpaytxfee to set a higher fee rate.\n"
"At a minimum, the new fee rate must be high enough to pay a new relay fee (relay fee amount returned\n"
"by getnetworkinfo RPC) and to enter the node's mempool.\n"
"\nArguments:\n"
"1. txid (string, required) The txid to be bumped\n"
"2. options (object, optional)\n"
" {\n"
" \"confTarget\" (numeric, optional) Confirmation target (in blocks)\n"
" \"totalFee\" (numeric, optional) Total fee (NOT feerate) to pay, in satoshis.\n"
" In rare cases, the actual fee paid might be slightly higher than the specified\n"
" totalFee if the tx change output has to be removed because it is too close to\n"
" the dust threshold.\n"
" \"replaceable\" (boolean, optional, default true) Whether the new transaction should still be\n"
" marked bip-125 replaceable. If true, the sequence numbers in the transaction will\n"
" be left unchanged from the original. If false, any input sequence numbers in the\n"
" original transaction that were less than 0xfffffffe will be increased to 0xfffffffe\n"
" so the new transaction will not be explicitly bip-125 replaceable (though it may\n"
" still be replacable in practice, for example if it has unconfirmed ancestors which\n"
" are replaceable).\n"
" }\n"
"\nResult:\n"
"{\n"
" \"txid\": \"value\", (string) The id of the new transaction\n"
" \"oldfee\": n, (numeric) Fee of the replaced transaction\n"
" \"fee\": n, (numeric) Fee of the new transaction\n"
"}\n"
"\nExamples:\n"
"\nBump the fee, get the new transaction\'s txid\n" +
HelpExampleCli("bumpfee", "<txid>"));
}
RPCTypeCheck(request.params, boost::assign::list_of(UniValue::VSTR)(UniValue::VOBJ));
uint256 hash;
hash.SetHex(request.params[0].get_str());
// retrieve the original tx from the wallet
LOCK2(cs_main, pwalletMain->cs_wallet);
EnsureWalletIsUnlocked();
if (!pwalletMain->mapWallet.count(hash)) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid or non-wallet transaction id");
}
CWalletTx& wtx = pwalletMain->mapWallet[hash];
if (pwalletMain->HasWalletSpend(hash)) {
throw JSONRPCError(RPC_MISC_ERROR, "Transaction has descendants in the wallet");
}
{
LOCK(mempool.cs);
auto it = mempool.mapTx.find(hash);
if (it != mempool.mapTx.end() && it->GetCountWithDescendants() > 1) {
throw JSONRPCError(RPC_MISC_ERROR, "Transaction has descendants in the mempool");
}
}
if (wtx.GetDepthInMainChain() != 0) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Transaction has been mined, or is conflicted with a mined transaction");
}
if (!SignalsOptInRBF(wtx)) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Transaction is not BIP 125 replaceable");
}
if (wtx.mapValue.count("replaced_by_txid")) {
throw JSONRPCError(RPC_INVALID_REQUEST, strprintf("Cannot bump transaction %s which was already bumped by transaction %s", hash.ToString(), wtx.mapValue.at("replaced_by_txid")));
}
// check that original tx consists entirely of our inputs
// if not, we can't bump the fee, because the wallet has no way of knowing the value of the other inputs (thus the fee)
if (!pwalletMain->IsAllFromMe(wtx, ISMINE_SPENDABLE)) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Transaction contains inputs that don't belong to this wallet");
}
// figure out which output was change
// if there was no change output or multiple change outputs, fail
int nOutput = -1;
for (size_t i = 0; i < wtx.tx->vout.size(); ++i) {
if (pwalletMain->IsChange(wtx.tx->vout[i])) {
if (nOutput != -1) {
throw JSONRPCError(RPC_MISC_ERROR, "Transaction has multiple change outputs");
}
nOutput = i;
}
}
if (nOutput == -1) {
throw JSONRPCError(RPC_MISC_ERROR, "Transaction does not have a change output");
}
// optional parameters
bool specifiedConfirmTarget = false;
int newConfirmTarget = nTxConfirmTarget;
CAmount totalFee = 0;
bool replaceable = true;
if (request.params.size() > 1) {
UniValue options = request.params[1];
RPCTypeCheckObj(options,
{
{"confTarget", UniValueType(UniValue::VNUM)},
{"totalFee", UniValueType(UniValue::VNUM)},
{"replaceable", UniValueType(UniValue::VBOOL)},
},
true, true);
if (options.exists("confTarget") && options.exists("totalFee")) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "confTarget and totalFee options should not both be set. Please provide either a confirmation target for fee estimation or an explicit total fee for the transaction.");
} else if (options.exists("confTarget")) {
specifiedConfirmTarget = true;
newConfirmTarget = options["confTarget"].get_int();
if (newConfirmTarget <= 0) { // upper-bound will be checked by estimatefee/smartfee
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid confTarget (cannot be <= 0)");
}
} else if (options.exists("totalFee")) {
totalFee = options["totalFee"].get_int64();
if (totalFee <= 0) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid totalFee (cannot be <= 0)");
} else if (totalFee > maxTxFee) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid totalFee (cannot be higher than maxTxFee)");
}
}
if (options.exists("replaceable")) {
replaceable = options["replaceable"].get_bool();
}
}
// signature sizes can vary by a byte, so add 1 for each input when calculating the new fee
int64_t txSize = GetVirtualTransactionSize(*(wtx.tx));
const int64_t maxNewTxSize = txSize + wtx.tx->vin.size();
// calculate the old fee and fee-rate
CAmount nOldFee = wtx.GetDebit(ISMINE_SPENDABLE) - wtx.tx->GetValueOut();
CFeeRate nOldFeeRate(nOldFee, txSize);
CAmount nNewFee;
CFeeRate nNewFeeRate;
if (totalFee > 0) {
CAmount minTotalFee = nOldFeeRate.GetFee(maxNewTxSize) + minRelayTxFee.GetFee(maxNewTxSize);
if (totalFee < minTotalFee) {
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid totalFee, must be at least %s (oldFee %s + relayFee %s)", FormatMoney(minTotalFee), nOldFeeRate.GetFee(maxNewTxSize), minRelayTxFee.GetFee(maxNewTxSize)));
}
nNewFee = totalFee;
nNewFeeRate = CFeeRate(totalFee, maxNewTxSize);
} else {
// use the user-defined payTxFee if possible, otherwise use smartfee / fallbackfee
if (!specifiedConfirmTarget && payTxFee.GetFeePerK() != 0) {
nNewFeeRate = payTxFee;
} else {
nNewFeeRate = mempool.estimateSmartFee(newConfirmTarget);
}
if (nNewFeeRate.GetFeePerK() == 0) {
nNewFeeRate = CWallet::fallbackFee;
}
// new fee rate must be at least old rate + minimum relay rate
if (nNewFeeRate.GetFeePerK() < nOldFeeRate.GetFeePerK() + ::minRelayTxFee.GetFeePerK()) {
nNewFeeRate = CFeeRate(nOldFeeRate.GetFeePerK() + ::minRelayTxFee.GetFeePerK());
}
nNewFee = nNewFeeRate.GetFee(maxNewTxSize);
}
// check that fee rate is higher than mempool's minimum fee
// (no point in bumping fee if we know that the new tx won't be accepted to the mempool)
// This may occur if the user set TotalFee or paytxfee too low, if fallbackfee is too low, or, perhaps,
// in a rare situation where the mempool minimum fee increased significantly since the fee estimation just a
// moment earlier. In this case, we report an error to the user, who may use totalFee to make an adjustment.
CFeeRate minMempoolFeeRate = mempool.GetMinFee(GetArg("-maxmempool", DEFAULT_MAX_MEMPOOL_SIZE) * 1000000);
if (nNewFeeRate.GetFeePerK() < minMempoolFeeRate.GetFeePerK()) {
throw JSONRPCError(RPC_MISC_ERROR, strprintf("New fee rate (%s) is less than the minimum fee rate (%s) to get into the mempool. totalFee value should to be at least %s or settxfee value should be at least %s to add transaction.", FormatMoney(nNewFeeRate.GetFeePerK()), FormatMoney(minMempoolFeeRate.GetFeePerK()), FormatMoney(minMempoolFeeRate.GetFee(maxNewTxSize)), FormatMoney(minMempoolFeeRate.GetFeePerK())));
}
// Now modify the output to increase the fee.
// If the output is not large enough to pay the fee, fail.
CAmount nDelta = nNewFee - nOldFee;
assert(nDelta > 0);
CMutableTransaction tx(*(wtx.tx));
CTxOut* poutput = &(tx.vout[nOutput]);
if (poutput->nValue < nDelta) {
throw JSONRPCError(RPC_MISC_ERROR, "Change output is too small to bump the fee");
}
// If the output would become dust, discard it (converting the dust to fee)
poutput->nValue -= nDelta;
if (poutput->nValue <= poutput->GetDustThreshold(::minRelayTxFee)) {
LogPrint("rpc", "Bumping fee and discarding dust output\n");
nNewFee += poutput->nValue;
tx.vout.erase(tx.vout.begin() + nOutput);
}
// Mark new tx not replaceable, if requested.
if (!replaceable) {
for (auto& input : tx.vin) {
if (input.nSequence < 0xfffffffe) input.nSequence = 0xfffffffe;
}
}
// sign the new tx
CTransaction txNewConst(tx);
int nIn = 0;
for (auto& input : tx.vin) {
std::map<uint256, CWalletTx>::const_iterator mi = pwalletMain->mapWallet.find(input.prevout.hash);
assert(mi != pwalletMain->mapWallet.end() && input.prevout.n < mi->second.tx->vout.size());
const CScript& scriptPubKey = mi->second.tx->vout[input.prevout.n].scriptPubKey;
const CAmount& amount = mi->second.tx->vout[input.prevout.n].nValue;
SignatureData sigdata;
if (!ProduceSignature(TransactionSignatureCreator(pwalletMain, &txNewConst, nIn, amount, SIGHASH_ALL), scriptPubKey, sigdata)) {
throw JSONRPCError(RPC_WALLET_ERROR, "Can't sign transaction.");
}
UpdateTransaction(tx, nIn, sigdata);
nIn++;
}
// commit/broadcast the tx
CReserveKey reservekey(pwalletMain);
CWalletTx wtxBumped(pwalletMain, MakeTransactionRef(std::move(tx)));
wtxBumped.mapValue["replaces_txid"] = hash.ToString();
CValidationState state;
if (!pwalletMain->CommitTransaction(wtxBumped, reservekey, g_connman.get(), state) || !state.IsValid()) {
throw JSONRPCError(RPC_WALLET_ERROR, strprintf("Error: The transaction was rejected! Reason given: %s", state.GetRejectReason()));
}
// mark the original tx as bumped
if (!pwalletMain->MarkReplaced(wtx.GetHash(), wtxBumped.GetHash())) {
// TODO: see if JSON-RPC has a standard way of returning a response
// along with an exception. It would be good to return information about
// wtxBumped to the caller even if marking the original transaction
// replaced does not succeed for some reason.
throw JSONRPCError(RPC_WALLET_ERROR, "Error: Created new bumpfee transaction but could not mark the original transaction as replaced.");
}
UniValue result(UniValue::VOBJ);
result.push_back(Pair("txid", wtxBumped.GetHash().GetHex()));
result.push_back(Pair("oldfee", ValueFromAmount(nOldFee)));
result.push_back(Pair("fee", ValueFromAmount(nNewFee)));
return result;
}
extern UniValue dumpprivkey(const JSONRPCRequest& request); // in rpcdump.cpp extern UniValue dumpprivkey(const JSONRPCRequest& request); // in rpcdump.cpp
extern UniValue importprivkey(const JSONRPCRequest& request); extern UniValue importprivkey(const JSONRPCRequest& request);
extern UniValue importaddress(const JSONRPCRequest& request); extern UniValue importaddress(const JSONRPCRequest& request);
@ -2638,6 +2908,7 @@ static const CRPCCommand commands[] =
{ "wallet", "addmultisigaddress", &addmultisigaddress, true, {"nrequired","keys","account"} }, { "wallet", "addmultisigaddress", &addmultisigaddress, true, {"nrequired","keys","account"} },
{ "wallet", "addwitnessaddress", &addwitnessaddress, true, {"address"} }, { "wallet", "addwitnessaddress", &addwitnessaddress, true, {"address"} },
{ "wallet", "backupwallet", &backupwallet, true, {"destination"} }, { "wallet", "backupwallet", &backupwallet, true, {"destination"} },
{ "wallet", "bumpfee", &bumpfee, true, {"txid", "options"} },
{ "wallet", "dumpprivkey", &dumpprivkey, true, {"address"} }, { "wallet", "dumpprivkey", &dumpprivkey, true, {"address"} },
{ "wallet", "dumpwallet", &dumpwallet, true, {"filename"} }, { "wallet", "dumpwallet", &dumpwallet, true, {"filename"} },
{ "wallet", "encryptwallet", &encryptwallet, true, {"passphrase"} }, { "wallet", "encryptwallet", &encryptwallet, true, {"passphrase"} },
@ -2666,7 +2937,7 @@ static const CRPCCommand commands[] =
{ "wallet", "listreceivedbyaddress", &listreceivedbyaddress, false, {"minconf","include_empty","include_watchonly"} }, { "wallet", "listreceivedbyaddress", &listreceivedbyaddress, false, {"minconf","include_empty","include_watchonly"} },
{ "wallet", "listsinceblock", &listsinceblock, false, {"blockhash","target_confirmations","include_watchonly"} }, { "wallet", "listsinceblock", &listsinceblock, false, {"blockhash","target_confirmations","include_watchonly"} },
{ "wallet", "listtransactions", &listtransactions, false, {"account","count","skip","include_watchonly"} }, { "wallet", "listtransactions", &listtransactions, false, {"account","count","skip","include_watchonly"} },
{ "wallet", "listunspent", &listunspent, false, {"minconf","maxconf","addresses"} }, { "wallet", "listunspent", &listunspent, false, {"minconf","maxconf","addresses","include_unsafe"} },
{ "wallet", "lockunspent", &lockunspent, true, {"unlock","transactions"} }, { "wallet", "lockunspent", &lockunspent, true, {"unlock","transactions"} },
{ "wallet", "move", &movecmd, false, {"fromaccount","toaccount","amount","minconf","comment"} }, { "wallet", "move", &movecmd, false, {"fromaccount","toaccount","amount","minconf","comment"} },
{ "wallet", "sendfrom", &sendfrom, false, {"fromaccount","toaddress","amount","minconf","comment","comment_to"} }, { "wallet", "sendfrom", &sendfrom, false, {"fromaccount","toaddress","amount","minconf","comment","comment_to"} },

90
src/wallet/wallet.cpp

@ -411,6 +411,13 @@ set<uint256> CWallet::GetConflicts(const uint256& txid) const
return result; return result;
} }
bool CWallet::HasWalletSpend(const uint256& txid) const
{
AssertLockHeld(cs_wallet);
auto iter = mapTxSpends.lower_bound(COutPoint(txid, 0));
return (iter != mapTxSpends.end() && iter->first.hash == txid);
}
void CWallet::Flush(bool shutdown) void CWallet::Flush(bool shutdown)
{ {
bitdb.Flush(shutdown); bitdb.Flush(shutdown);
@ -826,6 +833,35 @@ void CWallet::MarkDirty()
} }
} }
bool CWallet::MarkReplaced(const uint256& originalHash, const uint256& newHash)
{
LOCK(cs_wallet);
auto mi = mapWallet.find(originalHash);
// There is a bug if MarkReplaced is not called on an existing wallet transaction.
assert(mi != mapWallet.end());
CWalletTx& wtx = (*mi).second;
// Ensure for now that we're not overwriting data
assert(wtx.mapValue.count("replaced_by_txid") == 0);
wtx.mapValue["replaced_by_txid"] = newHash.ToString();
CWalletDB walletdb(strWalletFile, "r+");
bool success = true;
if (!walletdb.WriteTx(wtx)) {
LogPrintf("%s: Updating walletdb tx %s failed", __func__, wtx.GetHash().ToString());
success = false;
}
NotifyTransactionChanged(this, originalHash, CT_UPDATED);
return success;
}
bool CWallet::AddToWallet(const CWalletTx& wtxIn, bool fFlushOnClose) bool CWallet::AddToWallet(const CWalletTx& wtxIn, bool fFlushOnClose)
{ {
LOCK(cs_wallet); LOCK(cs_wallet);
@ -1154,6 +1190,8 @@ isminetype CWallet::IsMine(const CTxIn &txin) const
return ISMINE_NO; return ISMINE_NO;
} }
// Note that this function doesn't distinguish between a 0-valued input,
// and a not-"is mine" (according to the filter) input.
CAmount CWallet::GetDebit(const CTxIn &txin, const isminefilter& filter) const CAmount CWallet::GetDebit(const CTxIn &txin, const isminefilter& filter) const
{ {
{ {
@ -1236,6 +1274,27 @@ CAmount CWallet::GetDebit(const CTransaction& tx, const isminefilter& filter) co
return nDebit; return nDebit;
} }
bool CWallet::IsAllFromMe(const CTransaction& tx, const isminefilter& filter) const
{
LOCK(cs_wallet);
BOOST_FOREACH(const CTxIn& txin, tx.vin)
{
auto mi = mapWallet.find(txin.prevout.hash);
if (mi == mapWallet.end())
return false; // any unknown inputs can't be from us
const CWalletTx& prev = (*mi).second;
if (txin.prevout.n >= prev.tx->vout.size())
return false; // invalid input!
if (!(IsMine(prev.tx->vout[txin.prevout.n]) & filter))
return false;
}
return true;
}
CAmount CWallet::GetCredit(const CTransaction& tx, const isminefilter& filter) const CAmount CWallet::GetCredit(const CTransaction& tx, const isminefilter& filter) const
{ {
CAmount nCredit = 0; CAmount nCredit = 0;
@ -1958,6 +2017,37 @@ void CWallet::AvailableCoins(vector<COutput>& vCoins, bool fOnlyConfirmed, const
if (nDepth == 0 && !pcoin->InMempool()) if (nDepth == 0 && !pcoin->InMempool())
continue; continue;
// We should not consider coins from transactions that are replacing
// other transactions.
//
// Example: There is a transaction A which is replaced by bumpfee
// transaction B. In this case, we want to prevent creation of
// a transaction B' which spends an output of B.
//
// Reason: If transaction A were initially confirmed, transactions B
// and B' would no longer be valid, so the user would have to create
// a new transaction C to replace B'. However, in the case of a
// one-block reorg, transactions B' and C might BOTH be accepted,
// when the user only wanted one of them. Specifically, there could
// be a 1-block reorg away from the chain where transactions A and C
// were accepted to another chain where B, B', and C were all
// accepted.
if (nDepth == 0 && fOnlyConfirmed && pcoin->mapValue.count("replaces_txid")) {
continue;
}
// Similarly, we should not consider coins from transactions that
// have been replaced. In the example above, we would want to prevent
// creation of a transaction A' spending an output of A, because if
// transaction B were initially confirmed, conflicting with A and
// A', we wouldn't want to the user to create a transaction D
// intending to replace A', but potentially resulting in a scenario
// where A, A', and D could all be accepted (instead of just B and
// D, or just A and A' like the user would want).
if (nDepth == 0 && fOnlyConfirmed && pcoin->mapValue.count("replaced_by_txid")) {
continue;
}
for (unsigned int i = 0; i < pcoin->tx->vout.size(); i++) { for (unsigned int i = 0; i < pcoin->tx->vout.size(); i++) {
isminetype mine = IsMine(pcoin->tx->vout[i]); isminetype mine = IsMine(pcoin->tx->vout[i]);
if (!(IsSpent(wtxid, i)) && mine != ISMINE_NO && if (!(IsSpent(wtxid, i)) && mine != ISMINE_NO &&

12
src/wallet/wallet.h

@ -825,6 +825,10 @@ public:
std::set<CTxDestination> GetAccountAddresses(const std::string& strAccount) const; std::set<CTxDestination> GetAccountAddresses(const std::string& strAccount) const;
isminetype IsMine(const CTxIn& txin) const; isminetype IsMine(const CTxIn& txin) const;
/**
* Returns amount of debit if the input matches the
* filter, otherwise returns 0
*/
CAmount GetDebit(const CTxIn& txin, const isminefilter& filter) const; CAmount GetDebit(const CTxIn& txin, const isminefilter& filter) const;
isminetype IsMine(const CTxOut& txout) const; isminetype IsMine(const CTxOut& txout) const;
CAmount GetCredit(const CTxOut& txout, const isminefilter& filter) const; CAmount GetCredit(const CTxOut& txout, const isminefilter& filter) const;
@ -834,6 +838,8 @@ public:
/** should probably be renamed to IsRelevantToMe */ /** should probably be renamed to IsRelevantToMe */
bool IsFromMe(const CTransaction& tx) const; bool IsFromMe(const CTransaction& tx) const;
CAmount GetDebit(const CTransaction& tx, const isminefilter& filter) const; CAmount GetDebit(const CTransaction& tx, const isminefilter& filter) const;
/** Returns whether all of the inputs match the filter */
bool IsAllFromMe(const CTransaction& tx, const isminefilter& filter) const;
CAmount GetCredit(const CTransaction& tx, const isminefilter& filter) const; CAmount GetCredit(const CTransaction& tx, const isminefilter& filter) const;
CAmount GetChange(const CTransaction& tx) const; CAmount GetChange(const CTransaction& tx) const;
void SetBestChain(const CBlockLocator& loc); void SetBestChain(const CBlockLocator& loc);
@ -885,6 +891,9 @@ public:
//! Get wallet transactions that conflict with given transaction (spend same outputs) //! Get wallet transactions that conflict with given transaction (spend same outputs)
std::set<uint256> GetConflicts(const uint256& txid) const; std::set<uint256> GetConflicts(const uint256& txid) const;
//! Check if a given transaction has any of its outputs spent by another transaction in the wallet
bool HasWalletSpend(const uint256& txid) const;
//! Flush wallet (bitdb flush) //! Flush wallet (bitdb flush)
void Flush(bool shutdown=false); void Flush(bool shutdown=false);
@ -921,6 +930,9 @@ public:
/* Mark a transaction (and it in-wallet descendants) as abandoned so its inputs may be respent. */ /* Mark a transaction (and it in-wallet descendants) as abandoned so its inputs may be respent. */
bool AbandonTransaction(const uint256& hashTx); bool AbandonTransaction(const uint256& hashTx);
/** Mark a transaction as replaced by another transaction (e.g., BIP 125). */
bool MarkReplaced(const uint256& originalHash, const uint256& newHash);
/* Returns the wallets help message */ /* Returns the wallets help message */
static std::string GetWalletHelpString(bool showDebug); static std::string GetWalletHelpString(bool showDebug);

Loading…
Cancel
Save