Skip to content

Commit e25b158

Browse files
sdaftuarlaanwj
authored andcommitted
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. Github-Pull: #7286 Rebased-From: eaa8d27
1 parent 64612f1 commit e25b158

File tree

5 files changed

+199
-0
lines changed

5 files changed

+199
-0
lines changed

qa/rpc-tests/listtransactions.py

+109
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,15 @@
77

88
from test_framework.test_framework import BitcoinTestFramework
99
from test_framework.util import *
10+
from test_framework.mininode import CTransaction
11+
import cStringIO
12+
import binascii
1013

14+
def txFromHex(hexstring):
15+
tx = CTransaction()
16+
f = cStringIO.StringIO(binascii.unhexlify(hexstring))
17+
tx.deserialize(f)
18+
return tx
1119

1220
def check_array_result(object_array, to_match, expected):
1321
"""
@@ -103,6 +111,107 @@ def run_test(self):
103111
{"category":"receive","amount":Decimal("0.1")},
104112
{"txid":txid, "account" : "watchonly"} )
105113

114+
self.run_rbf_opt_in_test()
115+
116+
# Check that the opt-in-rbf flag works properly, for sent and received
117+
# transactions.
118+
def run_rbf_opt_in_test(self):
119+
# Check whether a transaction signals opt-in RBF itself
120+
def is_opt_in(node, txid):
121+
rawtx = node.getrawtransaction(txid, 1)
122+
for x in rawtx["vin"]:
123+
if x["sequence"] < 0xfffffffe:
124+
return True
125+
return False
126+
127+
# Find an unconfirmed output matching a certain txid
128+
def get_unconfirmed_utxo_entry(node, txid_to_match):
129+
utxo = node.listunspent(0, 0)
130+
for i in utxo:
131+
if i["txid"] == txid_to_match:
132+
return i
133+
return None
134+
135+
# 1. Chain a few transactions that don't opt-in.
136+
txid_1 = self.nodes[0].sendtoaddress(self.nodes[1].getnewaddress(), 1)
137+
assert(not is_opt_in(self.nodes[0], txid_1))
138+
check_array_result(self.nodes[0].listtransactions(), {"txid": txid_1}, {"bip125-replaceable":"no"})
139+
sync_mempools(self.nodes)
140+
check_array_result(self.nodes[1].listtransactions(), {"txid": txid_1}, {"bip125-replaceable":"no"})
141+
142+
# Tx2 will build off txid_1, still not opting in to RBF.
143+
utxo_to_use = get_unconfirmed_utxo_entry(self.nodes[1], txid_1)
144+
145+
# Create tx2 using createrawtransaction
146+
inputs = [{"txid":utxo_to_use["txid"], "vout":utxo_to_use["vout"]}]
147+
outputs = {self.nodes[0].getnewaddress(): 0.999}
148+
tx2 = self.nodes[1].createrawtransaction(inputs, outputs)
149+
tx2_signed = self.nodes[1].signrawtransaction(tx2)["hex"]
150+
txid_2 = self.nodes[1].sendrawtransaction(tx2_signed)
151+
152+
# ...and check the result
153+
assert(not is_opt_in(self.nodes[1], txid_2))
154+
check_array_result(self.nodes[1].listtransactions(), {"txid": txid_2}, {"bip125-replaceable":"no"})
155+
sync_mempools(self.nodes)
156+
check_array_result(self.nodes[0].listtransactions(), {"txid": txid_2}, {"bip125-replaceable":"no"})
157+
158+
# Tx3 will opt-in to RBF
159+
utxo_to_use = get_unconfirmed_utxo_entry(self.nodes[0], txid_2)
160+
inputs = [{"txid": txid_2, "vout":utxo_to_use["vout"]}]
161+
outputs = {self.nodes[1].getnewaddress(): 0.998}
162+
tx3 = self.nodes[0].createrawtransaction(inputs, outputs)
163+
tx3_modified = txFromHex(tx3)
164+
tx3_modified.vin[0].nSequence = 0
165+
tx3 = binascii.hexlify(tx3_modified.serialize()).decode('utf-8')
166+
tx3_signed = self.nodes[0].signrawtransaction(tx3)['hex']
167+
txid_3 = self.nodes[0].sendrawtransaction(tx3_signed)
168+
169+
assert(is_opt_in(self.nodes[0], txid_3))
170+
check_array_result(self.nodes[0].listtransactions(), {"txid": txid_3}, {"bip125-replaceable":"yes"})
171+
sync_mempools(self.nodes)
172+
check_array_result(self.nodes[1].listtransactions(), {"txid": txid_3}, {"bip125-replaceable":"yes"})
173+
174+
# Tx4 will chain off tx3. Doesn't signal itself, but depends on one
175+
# that does.
176+
utxo_to_use = get_unconfirmed_utxo_entry(self.nodes[1], txid_3)
177+
inputs = [{"txid": txid_3, "vout":utxo_to_use["vout"]}]
178+
outputs = {self.nodes[0].getnewaddress(): 0.997}
179+
tx4 = self.nodes[1].createrawtransaction(inputs, outputs)
180+
tx4_signed = self.nodes[1].signrawtransaction(tx4)["hex"]
181+
txid_4 = self.nodes[1].sendrawtransaction(tx4_signed)
182+
183+
assert(not is_opt_in(self.nodes[1], txid_4))
184+
check_array_result(self.nodes[1].listtransactions(), {"txid": txid_4}, {"bip125-replaceable":"yes"})
185+
sync_mempools(self.nodes)
186+
check_array_result(self.nodes[0].listtransactions(), {"txid": txid_4}, {"bip125-replaceable":"yes"})
187+
188+
# Replace tx3, and check that tx4 becomes unknown
189+
tx3_b = tx3_modified
190+
tx3_b.vout[0].nValue -= 0.004*100000000 # bump the fee
191+
tx3_b = binascii.hexlify(tx3_b.serialize()).decode('utf-8')
192+
tx3_b_signed = self.nodes[0].signrawtransaction(tx3_b)['hex']
193+
txid_3b = self.nodes[0].sendrawtransaction(tx3_b_signed, True)
194+
assert(is_opt_in(self.nodes[0], txid_3b))
195+
196+
check_array_result(self.nodes[0].listtransactions(), {"txid": txid_4}, {"bip125-replaceable":"unknown"})
197+
sync_mempools(self.nodes)
198+
check_array_result(self.nodes[1].listtransactions(), {"txid": txid_4}, {"bip125-replaceable":"unknown"})
199+
200+
# Check gettransaction as well:
201+
for n in self.nodes[0:2]:
202+
assert_equal(n.gettransaction(txid_1)["bip125-replaceable"], "no")
203+
assert_equal(n.gettransaction(txid_2)["bip125-replaceable"], "no")
204+
assert_equal(n.gettransaction(txid_3)["bip125-replaceable"], "yes")
205+
assert_equal(n.gettransaction(txid_3b)["bip125-replaceable"], "yes")
206+
assert_equal(n.gettransaction(txid_4)["bip125-replaceable"], "unknown")
207+
208+
# After mining a transaction, it's no longer BIP125-replaceable
209+
self.nodes[0].generate(1)
210+
assert(txid_3b not in self.nodes[0].getrawmempool())
211+
assert_equal(self.nodes[0].gettransaction(txid_3b)["bip125-replaceable"], "no")
212+
assert_equal(self.nodes[0].gettransaction(txid_4)["bip125-replaceable"], "unknown")
213+
214+
106215
if __name__ == '__main__':
107216
ListTransactionsTest().main()
108217

src/Makefile.am

+2
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ BITCOIN_CORE_H = \
122122
noui.h \
123123
policy/fees.h \
124124
policy/policy.h \
125+
policy/rbf.h \
125126
pow.h \
126127
prevector.h \
127128
primitives/block.h \
@@ -239,6 +240,7 @@ libbitcoin_wallet_a_SOURCES = \
239240
wallet/wallet.cpp \
240241
wallet/wallet_ismine.cpp \
241242
wallet/walletdb.cpp \
243+
policy/rbf.cpp \
242244
$(BITCOIN_CORE_H)
243245

244246
# crypto primitives library

src/policy/rbf.cpp

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright (c) 2016 The Bitcoin developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#include "policy/rbf.h"
6+
7+
bool SignalsOptInRBF(const CTransaction &tx)
8+
{
9+
BOOST_FOREACH(const CTxIn &txin, tx.vin) {
10+
if (txin.nSequence < std::numeric_limits<unsigned int>::max()-1) {
11+
return true;
12+
}
13+
}
14+
return false;
15+
}
16+
17+
bool IsRBFOptIn(const CTxMemPoolEntry &entry, CTxMemPool &pool)
18+
{
19+
AssertLockHeld(pool.cs);
20+
21+
CTxMemPool::setEntries setAncestors;
22+
23+
// First check the transaction itself.
24+
if (SignalsOptInRBF(entry.GetTx())) {
25+
return true;
26+
}
27+
28+
// If this transaction is not in our mempool, then we can't be sure
29+
// we will know about all its inputs.
30+
if (!pool.exists(entry.GetTx().GetHash())) {
31+
throw std::runtime_error("Cannot determine RBF opt-in signal for non-mempool transaction\n");
32+
}
33+
34+
// If all the inputs have nSequence >= maxint-1, it still might be
35+
// signaled for RBF if any unconfirmed parents have signaled.
36+
uint64_t noLimit = std::numeric_limits<uint64_t>::max();
37+
std::string dummy;
38+
pool.CalculateMemPoolAncestors(entry, setAncestors, noLimit, noLimit, noLimit, noLimit, dummy, false);
39+
40+
BOOST_FOREACH(CTxMemPool::txiter it, setAncestors) {
41+
if (SignalsOptInRBF(it->GetTx())) {
42+
return true;
43+
}
44+
}
45+
return false;
46+
}

src/policy/rbf.h

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (c) 2016 The Bitcoin developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#ifndef BITCOIN_POLICY_RBF_H
6+
#define BITCOIN_POLICY_RBF_H
7+
8+
#include "txmempool.h"
9+
10+
// Check whether the sequence numbers on this transaction are signaling
11+
// opt-in to replace-by-fee, according to BIP 125
12+
bool SignalsOptInRBF(const CTransaction &tx);
13+
14+
// Determine whether an in-mempool transaction is signaling opt-in to RBF
15+
// according to BIP 125
16+
// This involves checking sequence numbers of the transaction, as well
17+
// as the sequence numbers of all in-mempool ancestors.
18+
bool IsRBFOptIn(const CTxMemPoolEntry &entry, CTxMemPool &pool);
19+
20+
#endif // BITCOIN_POLICY_RBF_H

src/wallet/rpcwallet.cpp

+22
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include "main.h"
1212
#include "net.h"
1313
#include "netbase.h"
14+
#include "policy/rbf.h"
1415
#include "rpcserver.h"
1516
#include "timedata.h"
1617
#include "util.h"
@@ -76,6 +77,23 @@ void WalletTxToJSON(const CWalletTx& wtx, UniValue& entry)
7677
entry.push_back(Pair("walletconflicts", conflicts));
7778
entry.push_back(Pair("time", wtx.GetTxTime()));
7879
entry.push_back(Pair("timereceived", (int64_t)wtx.nTimeReceived));
80+
81+
// Add opt-in RBF status
82+
std::string rbfStatus = "no";
83+
if (confirms <= 0) {
84+
LOCK(mempool.cs);
85+
if (!mempool.exists(hash)) {
86+
if (SignalsOptInRBF(wtx)) {
87+
rbfStatus = "yes";
88+
} else {
89+
rbfStatus = "unknown";
90+
}
91+
} else if (IsRBFOptIn(*mempool.mapTx.find(hash), mempool)) {
92+
rbfStatus = "yes";
93+
}
94+
}
95+
entry.push_back(Pair("bip125-replaceable", rbfStatus));
96+
7997
BOOST_FOREACH(const PAIRTYPE(string,string)& item, wtx.mapValue)
8098
entry.push_back(Pair(item.first, item.second));
8199
}
@@ -1439,6 +1457,8 @@ UniValue listtransactions(const UniValue& params, bool fHelp)
14391457
" \"otheraccount\": \"accountname\", (string) For the 'move' category of transactions, the account the funds came \n"
14401458
" from (for receiving funds, positive amounts), or went to (for sending funds,\n"
14411459
" negative amounts).\n"
1460+
" \"bip125-replaceable\": \"yes|no|unknown\" (string) Whether this transaction could be replaced due to BIP125 (replace-by-fee);\n"
1461+
" may be unknown for unconfirmed transactions not in the mempool\n"
14421462
" }\n"
14431463
"]\n"
14441464

@@ -1707,6 +1727,8 @@ UniValue gettransaction(const UniValue& params, bool fHelp)
17071727
" \"txid\" : \"transactionid\", (string) The transaction id.\n"
17081728
" \"time\" : ttt, (numeric) The transaction time in seconds since epoch (1 Jan 1970 GMT)\n"
17091729
" \"timereceived\" : ttt, (numeric) The time received in seconds since epoch (1 Jan 1970 GMT)\n"
1730+
" \"bip125-replaceable\": \"yes|no|unknown\" (string) Whether this transaction could be replaced due to BIP125 (replace-by-fee);\n"
1731+
" may be unknown for unconfirmed transactions not in the mempool\n"
17101732
" \"details\" : [\n"
17111733
" {\n"
17121734
" \"account\" : \"accountname\", (string) DEPRECATED. The account name involved in the transaction, can be \"\" for the default account.\n"

0 commit comments

Comments
 (0)