diff --git a/qa/pull-tester/rpc-tests.py b/qa/pull-tester/rpc-tests.py index 796e5a3a5..f620f1388 100755 --- a/qa/pull-tester/rpc-tests.py +++ b/qa/pull-tester/rpc-tests.py @@ -81,6 +81,7 @@ testScripts = [ 'receivedby.py', 'mempool_resurrect_test.py', 'txn_doublespend.py --mineblock', + 'txn_doublespendrelay.py', 'txn_clone.py', 'getchaintips.py', 'rawtransactions.py', diff --git a/qa/rpc-tests/txn_doublespendrelay.py b/qa/rpc-tests/txn_doublespendrelay.py new file mode 100644 index 000000000..f349c53c0 --- /dev/null +++ b/qa/rpc-tests/txn_doublespendrelay.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python + +# +# Test double-spend-relay and notification code +# + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import * +from decimal import Decimal + +class DoubleSpendRelay(BitcoinTestFramework): + + # + # Create a 4-node network; roles for the nodes are: + # [0] : transaction creator + # [1] : respend sender + # [2] : relay node + # [3] : receiver, should detect/notify of double-spends + # + # Node connectivity is: + # [0,1] <--> [2] <--> [3] + # + def setup_network(self): + self.is_network_split = False + self.nodes = [] + for i in range(0,4): + self.nodes.append(start_node(i, self.options.tmpdir)) + connect_nodes(self.nodes[0], 2) + connect_nodes(self.nodes[1], 2) + connect_nodes(self.nodes[3], 2) + return self.nodes + + def run_test(self): + fee = Decimal("0.01") + + nodes = self.nodes + # Test 1: First spend + # shutdown nodes[1] so it is not aware of the first spend + # and will be willing to broadcast a respend + stop_node(nodes[1], 1) + # First spend: nodes[0] -> nodes[3] + amount = Decimal("144") # We rely on this requiring 3 50-BTC inputs + (total_in, tx1_inputs) = gather_inputs(nodes[0], amount+fee) + change_outputs = make_change(nodes[0], total_in, amount, fee) + outputs = dict(change_outputs) + outputs[nodes[3].getnewaddress()] = amount + signed = nodes[0].signrawtransaction(nodes[0].createrawtransaction(tx1_inputs, outputs)) + txid1 = nodes[0].sendrawtransaction(signed["hex"], True) + sync_mempools([nodes[0], nodes[3]]) + + txid1_info = nodes[3].gettransaction(txid1) + assert_equal(txid1_info["respendsobserved"], []) + + # Test 2: Is double-spend of tx1_inputs[0] relayed? + # Restart nodes[1] + nodes[1] = start_node(1, self.options.tmpdir) + connect_nodes(nodes[1], 2) + # Second spend: nodes[0] -> nodes[0] + amount = Decimal("40") + total_in = Decimal("50") + inputs2 = [tx1_inputs[0]] + change_outputs = make_change(nodes[0], total_in, amount, fee) + outputs = dict(change_outputs) + outputs[nodes[0].getnewaddress()] = amount + signed = nodes[0].signrawtransaction(nodes[0].createrawtransaction(inputs2, outputs)) + txid2 = nodes[1].sendrawtransaction(signed["hex"], True) + # Wait until txid2 is relayed to nodes[3] (but don't wait forever): + # Note we can't use sync_mempools, because the respend isn't added to + # the mempool. + for i in range(1,7): + txid1_info = nodes[3].gettransaction(txid1) + if txid1_info["respendsobserved"] != []: + break + time.sleep(0.1 * i**2) # geometric back-off + assert_equal(txid1_info["respendsobserved"], [txid2]) + + # Test 3: Is triple-spend of tx1_inputs[0] not relayed? + # Clear node1 mempool + stop_node(nodes[1], 1) + nodes[1] = start_node(1, self.options.tmpdir) + connect_nodes(nodes[1], 2) + # Third spend: nodes[0] -> nodes[0] + outputs = dict(change_outputs) + outputs[nodes[0].getnewaddress()] = amount + signed = nodes[0].signrawtransaction(nodes[0].createrawtransaction(inputs2, outputs)) + txid3 = nodes[1].sendrawtransaction(signed["hex"], True) + # Ensure txid3 not relayed to nodes[3]: + time.sleep(9.1) + txid1_info = nodes[3].gettransaction(txid1) + assert_equal(txid1_info["respendsobserved"], [txid2]) + + # Test 4: Is double-spend of tx1_inputs[1] relayed when triple-spend of tx1_inputs[0] precedes it? + # Clear node1 mempool + stop_node(nodes[1], 1) + nodes[1] = start_node(1, self.options.tmpdir) + connect_nodes(nodes[1], 2) + # Inputs are third spend, second spend + amount = Decimal("89") + total_in = Decimal("100") + inputs4 = [tx1_inputs[0],tx1_inputs[1]] + change_outputs = make_change(nodes[0], total_in, amount, fee) + outputs = dict(change_outputs) + outputs[nodes[0].getnewaddress()] = amount + signed = nodes[0].signrawtransaction(nodes[0].createrawtransaction(inputs4, outputs)) + txid4 = nodes[1].sendrawtransaction(signed["hex"], True) + # Wait until txid4 is relayed to nodes[3] (but don't wait forever): + for i in range(1,7): + txid1_info = nodes[3].gettransaction(txid1) + if txid1_info["respendsobserved"] != [txid2]: + break + time.sleep(0.1 * i**2) # geometric back-off + assert_equal(sorted(txid1_info["respendsobserved"]), sorted([txid2,txid4])) + + # Test 5: Is double-spend of tx1_inputs[2] relayed when triple-spend of tx1_inputs[0] follows it? + # Clear node1 mempool + stop_node(nodes[1], 1) + nodes[1] = start_node(1, self.options.tmpdir) + connect_nodes(nodes[1], 2) + # Inputs are second spend, third spend + amount = Decimal("88") + total_in = Decimal("100") + inputs5 = [tx1_inputs[2],tx1_inputs[0]] + change_outputs = make_change(nodes[0], total_in, amount, fee) + outputs = dict(change_outputs) + outputs[nodes[0].getnewaddress()] = amount + signed = nodes[0].signrawtransaction(nodes[0].createrawtransaction(inputs5, outputs)) + txid5 = nodes[1].sendrawtransaction(signed["hex"], True) + # Wait until txid5 is relayed to nodes[3] (but don't wait forever): + for i in range(1,7): + txid1_info = nodes[3].gettransaction(txid1) + if sorted(txid1_info["respendsobserved"]) != sorted([txid2,txid4]): + break + time.sleep(0.1 * i**2) # geometric back-off + assert_equal(sorted(txid1_info["respendsobserved"]), sorted([txid2,txid4,txid5])) + +if __name__ == '__main__': + DoubleSpendRelay().main() + diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 11b054125..2a68252c5 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -94,6 +94,10 @@ void WalletTxToJSON(const CWalletTx& wtx, UniValue& entry) BOOST_FOREACH(const uint256& conflict, wtx.GetConflicts()) conflicts.push_back(conflict.GetHex()); entry.push_back(Pair("walletconflicts", conflicts)); + UniValue respends; + BOOST_FOREACH(const uint256& respend, wtx.GetConflicts()) + respends.push_back(respend.GetHex()); + entry.push_back(Pair("respendsobserved", respends)); entry.push_back(Pair("time", wtx.GetTxTime())); entry.push_back(Pair("timereceived", (int64_t)wtx.nTimeReceived)); @@ -1394,6 +1398,12 @@ UniValue listtransactions(const UniValue& params, bool fHelp) " category of transactions.\n" " \"blocktime\": xxx, (numeric) The block time in seconds since epoch (1 Jan 1970 GMT).\n" " \"txid\": \"transactionid\", (string) The transaction id. Available for 'send' and 'receive' category of transactions.\n" + " \"walletconflicts\" : [\n" + " \"conflictid\", (string) Ids of transactions, including equivalent clones, that re-spend a txid input.\n" + " ],\n" + " \"respendsobserved\" : [\n" + " \"respendid\", (string) Ids of transactions, NOT equivalent clones, that re-spend a txid input. \"Double-spends.\"\n" + " ],\n" " \"time\": xxx, (numeric) The transaction time in seconds since epoch (midnight Jan 1 1970 GMT).\n" " \"timereceived\": xxx, (numeric) The time received in seconds since epoch (midnight Jan 1 1970 GMT). Available \n" " for 'send' and 'receive' category of transactions.\n" @@ -1588,6 +1598,12 @@ UniValue listsinceblock(const UniValue& params, bool fHelp) " \"blockindex\": n, (numeric) The block index containing the transaction. Available for 'send' and 'receive' category of transactions.\n" " \"blocktime\": xxx, (numeric) The block time in seconds since epoch (1 Jan 1970 GMT).\n" " \"txid\": \"transactionid\", (string) The transaction id. Available for 'send' and 'receive' category of transactions.\n" + " \"walletconflicts\" : [\n" + " \"conflictid\", (string) Ids of transactions, including equivalent clones, that re-spend a txid input.\n" + " ],\n" + " \"respendsobserved\" : [\n" + " \"respendid\", (string) Ids of transactions, NOT equivalent clones, that re-spend a txid input. \"Double-spends.\"\n" + " ],\n" " \"time\": xxx, (numeric) The transaction time in seconds since epoch (Jan 1 1970 GMT).\n" " \"timereceived\": xxx, (numeric) The time received in seconds since epoch (Jan 1 1970 GMT). Available for 'send' and 'receive' category of transactions.\n" " \"comment\": \"...\", (string) If a comment is associated with the transaction.\n" @@ -1672,6 +1688,12 @@ UniValue gettransaction(const UniValue& params, bool fHelp) " \"blockindex\" : xx, (numeric) The block index\n" " \"blocktime\" : ttt, (numeric) The time in seconds since epoch (1 Jan 1970 GMT)\n" " \"txid\" : \"transactionid\", (string) The transaction id.\n" + " \"walletconflicts\" : [\n" + " \"conflictid\", (string) Ids of transactions, including equivalent clones, that re-spend a txid input.\n" + " ],\n" + " \"respendsobserved\" : [\n" + " \"respendid\", (string) Ids of transactions, NOT equivalent clones, that re-spend a txid input. \"Double-spends.\"\n" + " ],\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" diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index bd8b19330..457780de8 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -1205,6 +1205,15 @@ bool CWallet::AddToWallet(const CWalletTx& wtxIn, bool fFromLoadWallet, CWalletD boost::thread t(runCommand, strCmd); // thread runs free } + // external respend notify + std::string strCmdRespend = GetArg("-respendnotify", ""); + if (!strCmdRespend.empty()) + { + boost::replace_all(strCmd, "%s", wtxIn.GetHash().GetHex()); + boost::replace_all(strCmd, "%t", hash.GetHex()); + boost::thread t(runCommand, strCmd); // thread runs free + } + } return true; } @@ -3667,6 +3676,7 @@ std::string CWallet::GetWalletHelpString(bool showDebug) strUsage += HelpMessageOpt("-wallet=", _("Specify wallet file (within data directory)") + " " + strprintf(_("(default: %s)"), DEFAULT_WALLET_DAT)); strUsage += HelpMessageOpt("-walletbroadcast", _("Make the wallet broadcast transactions") + " " + strprintf(_("(default: %u)"), DEFAULT_WALLETBROADCAST)); strUsage += HelpMessageOpt("-walletnotify=", _("Execute command when a wallet transaction changes (%s in cmd is replaced by TxID)")); + strUsage += HelpMessageOpt("-respendnotify=", _("Execute command when a network tx respends wallet tx input (%s=respend TxID, %t=wallet TxID)")); strUsage += HelpMessageOpt("-zapwallettxes=", _("Delete all wallet transactions and only recover those parts of the blockchain through -rescan on startup") + " " + _("(1 = keep tx meta data e.g. account owner and payment request information, 2 = drop tx meta data)"));