Skip to content

Commit

Permalink
RPC: indicate which transactions are replaceable
Browse files Browse the repository at this point in the history
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
  • Loading branch information
sdaftuar authored and laanwj committed Jan 20, 2016
1 parent 64612f1 commit e25b158
Show file tree
Hide file tree
Showing 5 changed files with 199 additions and 0 deletions.
109 changes: 109 additions & 0 deletions qa/rpc-tests/listtransactions.py
Expand Up @@ -7,7 +7,15 @@


from test_framework.test_framework import BitcoinTestFramework from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import * 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): def check_array_result(object_array, to_match, expected):
""" """
Expand Down Expand Up @@ -103,6 +111,107 @@ def run_test(self):
{"category":"receive","amount":Decimal("0.1")}, {"category":"receive","amount":Decimal("0.1")},
{"txid":txid, "account" : "watchonly"} ) {"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__': if __name__ == '__main__':
ListTransactionsTest().main() ListTransactionsTest().main()


2 changes: 2 additions & 0 deletions src/Makefile.am
Expand Up @@ -122,6 +122,7 @@ BITCOIN_CORE_H = \
noui.h \ noui.h \
policy/fees.h \ policy/fees.h \
policy/policy.h \ policy/policy.h \
policy/rbf.h \
pow.h \ pow.h \
prevector.h \ prevector.h \
primitives/block.h \ primitives/block.h \
Expand Down Expand Up @@ -239,6 +240,7 @@ libbitcoin_wallet_a_SOURCES = \
wallet/wallet.cpp \ wallet/wallet.cpp \
wallet/wallet_ismine.cpp \ wallet/wallet_ismine.cpp \
wallet/walletdb.cpp \ wallet/walletdb.cpp \
policy/rbf.cpp \
$(BITCOIN_CORE_H) $(BITCOIN_CORE_H)


# crypto primitives library # crypto primitives library
Expand Down
46 changes: 46 additions & 0 deletions src/policy/rbf.cpp
@@ -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 changes: 20 additions & 0 deletions src/policy/rbf.h
@@ -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
22 changes: 22 additions & 0 deletions src/wallet/rpcwallet.cpp
Expand Up @@ -11,6 +11,7 @@
#include "main.h" #include "main.h"
#include "net.h" #include "net.h"
#include "netbase.h" #include "netbase.h"
#include "policy/rbf.h"
#include "rpcserver.h" #include "rpcserver.h"
#include "timedata.h" #include "timedata.h"
#include "util.h" #include "util.h"
Expand Down Expand Up @@ -76,6 +77,23 @@ void WalletTxToJSON(const CWalletTx& wtx, UniValue& entry)
entry.push_back(Pair("walletconflicts", conflicts)); entry.push_back(Pair("walletconflicts", conflicts));
entry.push_back(Pair("time", wtx.GetTxTime())); entry.push_back(Pair("time", wtx.GetTxTime()));
entry.push_back(Pair("timereceived", (int64_t)wtx.nTimeReceived)); 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) BOOST_FOREACH(const PAIRTYPE(string,string)& item, wtx.mapValue)
entry.push_back(Pair(item.first, item.second)); entry.push_back(Pair(item.first, item.second));
} }
Expand Down Expand Up @@ -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" " \"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" " from (for receiving funds, positive amounts), or went to (for sending funds,\n"
" negative amounts).\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"
"]\n" "]\n"


Expand Down Expand Up @@ -1707,6 +1727,8 @@ UniValue gettransaction(const UniValue& params, bool fHelp)
" \"txid\" : \"transactionid\", (string) The transaction id.\n" " \"txid\" : \"transactionid\", (string) The transaction id.\n"
" \"time\" : ttt, (numeric) The transaction time in seconds since epoch (1 Jan 1970 GMT)\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" " \"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" " \"details\" : [\n"
" {\n" " {\n"
" \"account\" : \"accountname\", (string) DEPRECATED. The account name involved in the transaction, can be \"\" for the default account.\n" " \"account\" : \"accountname\", (string) DEPRECATED. The account name involved in the transaction, can be \"\" for the default account.\n"
Expand Down

0 comments on commit e25b158

Please sign in to comment.