[RPC] bumpfee
This command allows a user to increase the fee on a wallet transaction T, creating a "bumper" transaction B. T must signal that it is BIP-125 replaceable. T's change output is decremented to pay the additional fee. (B will not add inputs to T.) T cannot have any descendant transactions. Once B bumps T, neither T nor B's outputs can be spent until either T or (more likely) B is mined. Includes code by @jonasschnelli and @ryanofsky
This commit is contained in:
parent
52dde66770
commit
cc0243ad32
6 changed files with 650 additions and 0 deletions
|
@ -151,6 +151,7 @@ testScripts = [
|
|||
'signmessages.py',
|
||||
'nulldummy.py',
|
||||
'import-rescan.py',
|
||||
'bumpfee.py',
|
||||
'rpcnamedargs.py',
|
||||
]
|
||||
if ENABLE_ZMQ:
|
||||
|
|
317
qa/rpc-tests/bumpfee.py
Executable file
317
qa/rpc-tests/bumpfee.py
Executable file
|
@ -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()
|
|
@ -116,6 +116,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
|
|||
{ "setnetworkactive", 0, "state" },
|
||||
{ "getmempoolancestors", 1, "verbose" },
|
||||
{ "getmempooldescendants", 1, "verbose" },
|
||||
{ "bumpfee", 1, "options" },
|
||||
// Echo with conversion (For testing only)
|
||||
{ "echojson", 0, "arg0" },
|
||||
{ "echojson", 1, "arg1" },
|
||||
|
|
|
@ -11,8 +11,10 @@
|
|||
#include "init.h"
|
||||
#include "validation.h"
|
||||
#include "net.h"
|
||||
#include "policy/policy.h"
|
||||
#include "policy/rbf.h"
|
||||
#include "rpc/server.h"
|
||||
#include "script/sign.h"
|
||||
#include "timedata.h"
|
||||
#include "util.h"
|
||||
#include "utilmoneystr.h"
|
||||
|
@ -2595,6 +2597,261 @@ UniValue fundrawtransaction(const JSONRPCRequest& request)
|
|||
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 importprivkey(const JSONRPCRequest& request);
|
||||
extern UniValue importaddress(const JSONRPCRequest& request);
|
||||
|
@ -2614,6 +2871,7 @@ static const CRPCCommand commands[] =
|
|||
{ "wallet", "addmultisigaddress", &addmultisigaddress, true, {"nrequired","keys","account"} },
|
||||
{ "wallet", "addwitnessaddress", &addwitnessaddress, true, {"address"} },
|
||||
{ "wallet", "backupwallet", &backupwallet, true, {"destination"} },
|
||||
{ "wallet", "bumpfee", &bumpfee, true, {"txid", "options"} },
|
||||
{ "wallet", "dumpprivkey", &dumpprivkey, true, {"address"} },
|
||||
{ "wallet", "dumpwallet", &dumpwallet, true, {"filename"} },
|
||||
{ "wallet", "encryptwallet", &encryptwallet, true, {"passphrase"} },
|
||||
|
|
|
@ -411,6 +411,13 @@ set<uint256> CWallet::GetConflicts(const uint256& txid) const
|
|||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
LOCK(cs_wallet);
|
||||
|
@ -1981,6 +2017,37 @@ void CWallet::AvailableCoins(vector<COutput>& vCoins, bool fOnlyConfirmed, const
|
|||
if (nDepth == 0 && !pcoin->InMempool())
|
||||
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++) {
|
||||
isminetype mine = IsMine(pcoin->tx->vout[i]);
|
||||
if (!(IsSpent(wtxid, i)) && mine != ISMINE_NO &&
|
||||
|
|
|
@ -891,6 +891,9 @@ public:
|
|||
//! Get wallet transactions that conflict with given transaction (spend same outputs)
|
||||
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)
|
||||
void Flush(bool shutdown=false);
|
||||
|
||||
|
@ -927,6 +930,9 @@ public:
|
|||
/* Mark a transaction (and it in-wallet descendants) as abandoned so its inputs may be respent. */
|
||||
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 */
|
||||
static std::string GetWalletHelpString(bool showDebug);
|
||||
|
||||
|
|
Loading…
Reference in a new issue