RPC: indicate which transactions are replaceable
Add "bip125-replaceable" output field to listtransactions and gettransaction which indicates if an unconfirmed transaction, or any unconfirmed parent, is signaling opt-in RBF according to BIP 125.
This commit is contained in:
parent
f9fd4c2884
commit
eaa8d2754b
5 changed files with 199 additions and 0 deletions
|
@ -7,7 +7,15 @@
|
|||
|
||||
from test_framework.test_framework import BitcoinTestFramework
|
||||
from test_framework.util import *
|
||||
from test_framework.mininode import CTransaction
|
||||
import cStringIO
|
||||
import binascii
|
||||
|
||||
def txFromHex(hexstring):
|
||||
tx = CTransaction()
|
||||
f = cStringIO.StringIO(binascii.unhexlify(hexstring))
|
||||
tx.deserialize(f)
|
||||
return tx
|
||||
|
||||
def check_array_result(object_array, to_match, expected):
|
||||
"""
|
||||
|
@ -108,6 +116,107 @@ class ListTransactionsTest(BitcoinTestFramework):
|
|||
{"category":"receive","amount":Decimal("0.1")},
|
||||
{"txid":txid, "account" : "watchonly"} )
|
||||
|
||||
self.run_rbf_opt_in_test()
|
||||
|
||||
# Check that the opt-in-rbf flag works properly, for sent and received
|
||||
# transactions.
|
||||
def run_rbf_opt_in_test(self):
|
||||
# Check whether a transaction signals opt-in RBF itself
|
||||
def is_opt_in(node, txid):
|
||||
rawtx = node.getrawtransaction(txid, 1)
|
||||
for x in rawtx["vin"]:
|
||||
if x["sequence"] < 0xfffffffe:
|
||||
return True
|
||||
return False
|
||||
|
||||
# Find an unconfirmed output matching a certain txid
|
||||
def get_unconfirmed_utxo_entry(node, txid_to_match):
|
||||
utxo = node.listunspent(0, 0)
|
||||
for i in utxo:
|
||||
if i["txid"] == txid_to_match:
|
||||
return i
|
||||
return None
|
||||
|
||||
# 1. Chain a few transactions that don't opt-in.
|
||||
txid_1 = self.nodes[0].sendtoaddress(self.nodes[1].getnewaddress(), 1)
|
||||
assert(not is_opt_in(self.nodes[0], txid_1))
|
||||
check_array_result(self.nodes[0].listtransactions(), {"txid": txid_1}, {"bip125-replaceable":"no"})
|
||||
sync_mempools(self.nodes)
|
||||
check_array_result(self.nodes[1].listtransactions(), {"txid": txid_1}, {"bip125-replaceable":"no"})
|
||||
|
||||
# Tx2 will build off txid_1, still not opting in to RBF.
|
||||
utxo_to_use = get_unconfirmed_utxo_entry(self.nodes[1], txid_1)
|
||||
|
||||
# Create tx2 using createrawtransaction
|
||||
inputs = [{"txid":utxo_to_use["txid"], "vout":utxo_to_use["vout"]}]
|
||||
outputs = {self.nodes[0].getnewaddress(): 0.999}
|
||||
tx2 = self.nodes[1].createrawtransaction(inputs, outputs)
|
||||
tx2_signed = self.nodes[1].signrawtransaction(tx2)["hex"]
|
||||
txid_2 = self.nodes[1].sendrawtransaction(tx2_signed)
|
||||
|
||||
# ...and check the result
|
||||
assert(not is_opt_in(self.nodes[1], txid_2))
|
||||
check_array_result(self.nodes[1].listtransactions(), {"txid": txid_2}, {"bip125-replaceable":"no"})
|
||||
sync_mempools(self.nodes)
|
||||
check_array_result(self.nodes[0].listtransactions(), {"txid": txid_2}, {"bip125-replaceable":"no"})
|
||||
|
||||
# Tx3 will opt-in to RBF
|
||||
utxo_to_use = get_unconfirmed_utxo_entry(self.nodes[0], txid_2)
|
||||
inputs = [{"txid": txid_2, "vout":utxo_to_use["vout"]}]
|
||||
outputs = {self.nodes[1].getnewaddress(): 0.998}
|
||||
tx3 = self.nodes[0].createrawtransaction(inputs, outputs)
|
||||
tx3_modified = txFromHex(tx3)
|
||||
tx3_modified.vin[0].nSequence = 0
|
||||
tx3 = binascii.hexlify(tx3_modified.serialize()).decode('utf-8')
|
||||
tx3_signed = self.nodes[0].signrawtransaction(tx3)['hex']
|
||||
txid_3 = self.nodes[0].sendrawtransaction(tx3_signed)
|
||||
|
||||
assert(is_opt_in(self.nodes[0], txid_3))
|
||||
check_array_result(self.nodes[0].listtransactions(), {"txid": txid_3}, {"bip125-replaceable":"yes"})
|
||||
sync_mempools(self.nodes)
|
||||
check_array_result(self.nodes[1].listtransactions(), {"txid": txid_3}, {"bip125-replaceable":"yes"})
|
||||
|
||||
# Tx4 will chain off tx3. Doesn't signal itself, but depends on one
|
||||
# that does.
|
||||
utxo_to_use = get_unconfirmed_utxo_entry(self.nodes[1], txid_3)
|
||||
inputs = [{"txid": txid_3, "vout":utxo_to_use["vout"]}]
|
||||
outputs = {self.nodes[0].getnewaddress(): 0.997}
|
||||
tx4 = self.nodes[1].createrawtransaction(inputs, outputs)
|
||||
tx4_signed = self.nodes[1].signrawtransaction(tx4)["hex"]
|
||||
txid_4 = self.nodes[1].sendrawtransaction(tx4_signed)
|
||||
|
||||
assert(not is_opt_in(self.nodes[1], txid_4))
|
||||
check_array_result(self.nodes[1].listtransactions(), {"txid": txid_4}, {"bip125-replaceable":"yes"})
|
||||
sync_mempools(self.nodes)
|
||||
check_array_result(self.nodes[0].listtransactions(), {"txid": txid_4}, {"bip125-replaceable":"yes"})
|
||||
|
||||
# Replace tx3, and check that tx4 becomes unknown
|
||||
tx3_b = tx3_modified
|
||||
tx3_b.vout[0].nValue -= 0.004*100000000 # bump the fee
|
||||
tx3_b = binascii.hexlify(tx3_b.serialize()).decode('utf-8')
|
||||
tx3_b_signed = self.nodes[0].signrawtransaction(tx3_b)['hex']
|
||||
txid_3b = self.nodes[0].sendrawtransaction(tx3_b_signed, True)
|
||||
assert(is_opt_in(self.nodes[0], txid_3b))
|
||||
|
||||
check_array_result(self.nodes[0].listtransactions(), {"txid": txid_4}, {"bip125-replaceable":"unknown"})
|
||||
sync_mempools(self.nodes)
|
||||
check_array_result(self.nodes[1].listtransactions(), {"txid": txid_4}, {"bip125-replaceable":"unknown"})
|
||||
|
||||
# Check gettransaction as well:
|
||||
for n in self.nodes[0:2]:
|
||||
assert_equal(n.gettransaction(txid_1)["bip125-replaceable"], "no")
|
||||
assert_equal(n.gettransaction(txid_2)["bip125-replaceable"], "no")
|
||||
assert_equal(n.gettransaction(txid_3)["bip125-replaceable"], "yes")
|
||||
assert_equal(n.gettransaction(txid_3b)["bip125-replaceable"], "yes")
|
||||
assert_equal(n.gettransaction(txid_4)["bip125-replaceable"], "unknown")
|
||||
|
||||
# After mining a transaction, it's no longer BIP125-replaceable
|
||||
self.nodes[0].generate(1)
|
||||
assert(txid_3b not in self.nodes[0].getrawmempool())
|
||||
assert_equal(self.nodes[0].gettransaction(txid_3b)["bip125-replaceable"], "no")
|
||||
assert_equal(self.nodes[0].gettransaction(txid_4)["bip125-replaceable"], "unknown")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
ListTransactionsTest().main()
|
||||
|
||||
|
|
|
@ -122,6 +122,7 @@ BITCOIN_CORE_H = \
|
|||
noui.h \
|
||||
policy/fees.h \
|
||||
policy/policy.h \
|
||||
policy/rbf.h \
|
||||
pow.h \
|
||||
prevector.h \
|
||||
primitives/block.h \
|
||||
|
@ -239,6 +240,7 @@ libbitcoin_wallet_a_SOURCES = \
|
|||
wallet/wallet.cpp \
|
||||
wallet/wallet_ismine.cpp \
|
||||
wallet/walletdb.cpp \
|
||||
policy/rbf.cpp \
|
||||
$(BITCOIN_CORE_H)
|
||||
|
||||
# crypto primitives library
|
||||
|
|
46
src/policy/rbf.cpp
Normal file
46
src/policy/rbf.cpp
Normal file
|
@ -0,0 +1,46 @@
|
|||
// Copyright (c) 2016 The Bitcoin developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include "policy/rbf.h"
|
||||
|
||||
bool SignalsOptInRBF(const CTransaction &tx)
|
||||
{
|
||||
BOOST_FOREACH(const CTxIn &txin, tx.vin) {
|
||||
if (txin.nSequence < std::numeric_limits<unsigned int>::max()-1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool IsRBFOptIn(const CTxMemPoolEntry &entry, CTxMemPool &pool)
|
||||
{
|
||||
AssertLockHeld(pool.cs);
|
||||
|
||||
CTxMemPool::setEntries setAncestors;
|
||||
|
||||
// First check the transaction itself.
|
||||
if (SignalsOptInRBF(entry.GetTx())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If this transaction is not in our mempool, then we can't be sure
|
||||
// we will know about all its inputs.
|
||||
if (!pool.exists(entry.GetTx().GetHash())) {
|
||||
throw std::runtime_error("Cannot determine RBF opt-in signal for non-mempool transaction\n");
|
||||
}
|
||||
|
||||
// If all the inputs have nSequence >= maxint-1, it still might be
|
||||
// signaled for RBF if any unconfirmed parents have signaled.
|
||||
uint64_t noLimit = std::numeric_limits<uint64_t>::max();
|
||||
std::string dummy;
|
||||
pool.CalculateMemPoolAncestors(entry, setAncestors, noLimit, noLimit, noLimit, noLimit, dummy, false);
|
||||
|
||||
BOOST_FOREACH(CTxMemPool::txiter it, setAncestors) {
|
||||
if (SignalsOptInRBF(it->GetTx())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
20
src/policy/rbf.h
Normal file
20
src/policy/rbf.h
Normal file
|
@ -0,0 +1,20 @@
|
|||
// Copyright (c) 2016 The Bitcoin developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef BITCOIN_POLICY_RBF_H
|
||||
#define BITCOIN_POLICY_RBF_H
|
||||
|
||||
#include "txmempool.h"
|
||||
|
||||
// Check whether the sequence numbers on this transaction are signaling
|
||||
// opt-in to replace-by-fee, according to BIP 125
|
||||
bool SignalsOptInRBF(const CTransaction &tx);
|
||||
|
||||
// Determine whether an in-mempool transaction is signaling opt-in to RBF
|
||||
// according to BIP 125
|
||||
// This involves checking sequence numbers of the transaction, as well
|
||||
// as the sequence numbers of all in-mempool ancestors.
|
||||
bool IsRBFOptIn(const CTxMemPoolEntry &entry, CTxMemPool &pool);
|
||||
|
||||
#endif // BITCOIN_POLICY_RBF_H
|
|
@ -11,6 +11,7 @@
|
|||
#include "main.h"
|
||||
#include "net.h"
|
||||
#include "netbase.h"
|
||||
#include "policy/rbf.h"
|
||||
#include "rpcserver.h"
|
||||
#include "timedata.h"
|
||||
#include "util.h"
|
||||
|
@ -76,6 +77,23 @@ void WalletTxToJSON(const CWalletTx& wtx, UniValue& entry)
|
|||
entry.push_back(Pair("walletconflicts", conflicts));
|
||||
entry.push_back(Pair("time", wtx.GetTxTime()));
|
||||
entry.push_back(Pair("timereceived", (int64_t)wtx.nTimeReceived));
|
||||
|
||||
// Add opt-in RBF status
|
||||
std::string rbfStatus = "no";
|
||||
if (confirms <= 0) {
|
||||
LOCK(mempool.cs);
|
||||
if (!mempool.exists(hash)) {
|
||||
if (SignalsOptInRBF(wtx)) {
|
||||
rbfStatus = "yes";
|
||||
} else {
|
||||
rbfStatus = "unknown";
|
||||
}
|
||||
} else if (IsRBFOptIn(*mempool.mapTx.find(hash), mempool)) {
|
||||
rbfStatus = "yes";
|
||||
}
|
||||
}
|
||||
entry.push_back(Pair("bip125-replaceable", rbfStatus));
|
||||
|
||||
BOOST_FOREACH(const PAIRTYPE(string,string)& item, wtx.mapValue)
|
||||
entry.push_back(Pair(item.first, item.second));
|
||||
}
|
||||
|
@ -1439,6 +1457,8 @@ UniValue listtransactions(const UniValue& params, bool fHelp)
|
|||
" \"otheraccount\": \"accountname\", (string) For the 'move' category of transactions, the account the funds came \n"
|
||||
" from (for receiving funds, positive amounts), or went to (for sending funds,\n"
|
||||
" negative amounts).\n"
|
||||
" \"bip125-replaceable\": \"yes|no|unknown\" (string) Whether this transaction could be replaced due to BIP125 (replace-by-fee);\n"
|
||||
" may be unknown for unconfirmed transactions not in the mempool\n"
|
||||
" }\n"
|
||||
"]\n"
|
||||
|
||||
|
@ -1707,6 +1727,8 @@ UniValue gettransaction(const UniValue& params, bool fHelp)
|
|||
" \"txid\" : \"transactionid\", (string) The transaction id.\n"
|
||||
" \"time\" : ttt, (numeric) The transaction time in seconds since epoch (1 Jan 1970 GMT)\n"
|
||||
" \"timereceived\" : ttt, (numeric) The time received in seconds since epoch (1 Jan 1970 GMT)\n"
|
||||
" \"bip125-replaceable\": \"yes|no|unknown\" (string) Whether this transaction could be replaced due to BIP125 (replace-by-fee);\n"
|
||||
" may be unknown for unconfirmed transactions not in the mempool\n"
|
||||
" \"details\" : [\n"
|
||||
" {\n"
|
||||
" \"account\" : \"accountname\", (string) DEPRECATED. The account name involved in the transaction, can be \"\" for the default account.\n"
|
||||
|
|
Loading…
Reference in a new issue