mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-01-11 20:32:35 -03:00
Merge #7222: RPC: Indicate which transactions are signaling opt-in RBF
eaa8d27
RPC: indicate which transactions are replaceable (Suhas Daftuar)
This commit is contained in:
commit
82429d0861
5 changed files with 199 additions and 0 deletions
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -108,6 +116,107 @@ class ListTransactionsTest(BitcoinTestFramework):
|
||||||
{"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()
|
||||||
|
|
||||||
|
|
|
@ -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 \
|
||||||
|
@ -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
|
||||||
|
|
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 "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"
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue