diff --git a/qa/pull-tester/rpc-tests.py b/qa/pull-tester/rpc-tests.py index 10948bb36..8f4784836 100755 --- a/qa/pull-tester/rpc-tests.py +++ b/qa/pull-tester/rpc-tests.py @@ -154,7 +154,7 @@ testScripts = [ 'signmessages.py', # 'nulldummy.py', 'import-rescan.py', - 'harddustlimit.py', + 'dustlimits.py', 'paytxfee.py', 'feelimit.py', # While fee bumping should work in Doge, these tests depend on free transactions, which we don't support. diff --git a/qa/rpc-tests/dustlimits.py b/qa/rpc-tests/dustlimits.py new file mode 100644 index 000000000..3ced377a3 --- /dev/null +++ b/qa/rpc-tests/dustlimits.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +# Copyright (c) 2021 The Dogecoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Dust limit QA test. + +# Tests nodes with differing mempool/relay dust limits +""" + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import * +from decimal import Decimal + +class DustLimitTest(BitcoinTestFramework): + + def __init__(self): + super().__init__() + self.setup_clean_chain = True + self.num_nodes = 4 + + # set up receiving addresses outside of nodes' wallets + self.recv_1 = "n4LRQGEKcyRCXqD2MH3ompyMTJKitxu1WP" + self.recv_2 = "n1eAe5K2AQUtbmMxVzWnGAyq4hkWJdse2x" + + # seed moneys + self.seed = 100 + + def setup_nodes(self, split=False): + nodes = [] + + # 1.10.0-like node with only a soft dust limit + nodes.append(start_node(0, self.options.tmpdir, + ["-acceptnonstdtxn=0", "-dustlimit=1", "-harddustlimit=0.0", "-minrelaytxfee=1", "-debug"])) + + # 1.14.2-like node with only a hard dust limit + nodes.append(start_node(1, self.options.tmpdir, + ["-acceptnonstdtxn=0", "-dustlimit=1", "-harddustlimit=1", "-minrelaytxfee=1", "-debug"])) + + # 1.14.5-like node with a lower, different hard and soft dust limit + nodes.append(start_node(2, self.options.tmpdir, + ["-acceptnonstdtxn=0", "-dustlimit=0.01", "-harddustlimit=0.001", "-debug"])) + + # node that should accept everything + nodes.append(start_node(3, self.options.tmpdir, + ["-acceptnonstdtxn=0", "-dustlimit=0.0", "-harddustlimit=0.0", "-minrelaytxfee=0.00000001", "-debug"])) + + return nodes + + def setup_network(self, split = False): + self.nodes = self.setup_nodes() + + # connect everything to everything + for s in range(0, self.num_nodes): + for t in range(s+1, self.num_nodes): + connect_nodes_bi(self.nodes, s, t) + + self.is_network_split = False + self.sync_all() + + def run_test(self): + + # set up 10 seeded addresses for node 0-2 + addrs = [] + for i in range(3): + for _ in range(10): + addrs.append(self.nodes[i].getnewaddress()) + + # mine some blocks and prepare some coins + self.nodes[2].generate(1) + self.sync_all() + self.nodes[0].generate(101) + self.sync_all() + for addr in addrs: + self.nodes[0].sendtoaddress(addr, self.seed) + self.nodes[0].generate(1) + self.sync_all() + + # create dusty transactions + + txids = [ + self.send_dusty_tx(self.nodes[1], Decimal("1"), Decimal("1")), # goes to all + self.send_dusty_tx(self.nodes[0], Decimal("0.9"), Decimal("2")), # goes to 3/4 + self.send_dusty_tx(self.nodes[0], Decimal("0.0009"), Decimal("2")), # goes to 3/4 + self.send_dusty_tx(self.nodes[2], Decimal("0.001"), Decimal("2")), # goes to 3/4 + self.send_dusty_tx(self.nodes[2], Decimal("0.001"), Decimal("0.02")), # goes to 2/4 + ] + + # nodes do not accept dust under their hard dust limit + # no matter how much fee is paid + self.get_dust_rejection(self.nodes[2], Decimal("0.0009"), Decimal("5")) + self.get_dust_rejection(self.nodes[1], Decimal("0.9"), Decimal("5")) + + # wait 15 seconds to sync mempools + time.sleep(15) + + assert_equal(self.nodes[0].getmempoolinfo()['size'], 4) # 4 of 5 + assert_equal(self.nodes[1].getmempoolinfo()['size'], 1) # 1 of 5 + assert_equal(self.nodes[2].getmempoolinfo()['size'], 4) # 4 of 5 + assert_equal(self.nodes[3].getmempoolinfo()['size'], 5) # all + + # check each tx + i = 0 + for checktx in [[0,1,2,3], [0], [0,1,3,4], [0,1,2,3,4]]: + for idx in checktx: + assert(txids[idx] in self.nodes[i].getrawmempool()) + i += 1 + + # mining the 1 tx known to node 1 + self.nodes[1].generate(1) + sync_blocks(self.nodes) + + assert_equal(self.nodes[0].getmempoolinfo()['size'], 3) # 3 of 4 + assert_equal(self.nodes[1].getmempoolinfo()['size'], 0) # none left + assert_equal(self.nodes[2].getmempoolinfo()['size'], 3) # 3 of 4 + assert_equal(self.nodes[3].getmempoolinfo()['size'], 4) # all + + # check each tx + i = 0 + for checktx in [[1,2,3], [], [1,3,4], [1,2,3,4]]: + for idx in checktx: + assert(txids[idx] in self.nodes[i].getrawmempool()) + i += 1 + + # mine the 3 tx known to node 0 + self.nodes[0].generate(1) + sync_blocks(self.nodes) + + # now only the last tx is left + assert_equal(self.nodes[0].getmempoolinfo()['size'], 0) # none left + assert_equal(self.nodes[1].getmempoolinfo()['size'], 0) # none left + assert_equal(self.nodes[2].getmempoolinfo()['size'], 1) # all + assert_equal(self.nodes[3].getmempoolinfo()['size'], 1) # all + + # check each tx on the remaining nodes + for i in [2,3]: + assert(txids[4] in self.nodes[i].getrawmempool()) + + # after mining the last tx from node 2, all nodes should have empty mempools + self.nodes[2].generate(1) + self.sync_all() + + assert_equal(self.nodes[0].getmempoolinfo()['size'], 0) + assert_equal(self.nodes[1].getmempoolinfo()['size'], 0) + assert_equal(self.nodes[2].getmempoolinfo()['size'], 0) + assert_equal(self.nodes[3].getmempoolinfo()['size'], 0) + + print("such success. wow!") + + def create_dusty_tx(self, n, dust, fee): + minAmount = 5 * (dust + fee) + avail = n.listunspent(0, 1000, [], True, {'minimumAmount': minAmount})[0] + inputs = [ {'txid': avail['txid'], 'vout': avail['vout']} ] + outputs = { self.recv_1 : avail['amount'] - fee - dust , self.recv_2: dust } + rawtx = n.createrawtransaction(inputs, outputs) + return n.signrawtransaction(rawtx) + + def send_dusty_tx(self, n, dust, fee): + rawtx = self.create_dusty_tx(n, dust, fee) + return n.sendrawtransaction(rawtx['hex']) + + def get_dust_rejection(self, n, dust, fee): + rawtx = self.create_dusty_tx(n, dust, fee) + assert_raises_jsonrpc(-26, "dust", n.sendrawtransaction, rawtx['hex']) + +if __name__ == '__main__': + DustLimitTest().main() diff --git a/qa/rpc-tests/harddustlimit.py b/qa/rpc-tests/harddustlimit.py deleted file mode 100644 index d7b46fd94..000000000 --- a/qa/rpc-tests/harddustlimit.py +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) 2021 The Dogecoin Core developers -# Distributed under the MIT software license, see the accompanying -# file COPYING or http://www.opensource.org/licenses/mit-license.php. -"""Hard dust limit QA test. - -# Tests nodes with differing -dustlimits -""" - -from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import * -from decimal import Decimal - -class HardDustLimitTest(BitcoinTestFramework): - - def __init__(self): - super().__init__() - self.setup_clean_chain = True - self.num_nodes = 3 - - def setup_network(self, split=False): - self.nodes = [] - self.nodes.append(start_node(0, self.options.tmpdir, ["-dustlimit=0.1", "-debug"])) - self.nodes.append(start_node(1, self.options.tmpdir, ["-dustlimit=1", "-debug"])) - self.nodes.append(start_node(2, self.options.tmpdir, ["-dustlimit=0.01", "-debug"])) - - connect_nodes_bi(self.nodes,0,1) - connect_nodes_bi(self.nodes,1,2) - connect_nodes_bi(self.nodes,0,2) - - self.is_network_split=False - self.sync_all() - - def run_test(self): - - self.fee = Decimal("0.001") - self.seed = 1000 - self.coinselector = {'minimumAmount': self.fee * 10, 'maximumAmount': self.seed} - - # set up addresses - n0a1 = self.nodes[0].getnewaddress() - n0a2 = self.nodes[0].getnewaddress() - n0a3 = self.nodes[0].getnewaddress() - n1a1 = self.nodes[1].getnewaddress() - n2a1 = self.nodes[2].getnewaddress() - n2a2 = self.nodes[2].getnewaddress() - n2a3 = self.nodes[2].getnewaddress() - n2a4 = self.nodes[2].getnewaddress() - - # mine some blocks and prepare some coins - self.nodes[2].generate(1) - self.sync_all() - self.nodes[0].generate(101) - self.sync_all() - self.nodes[0].sendtoaddress(n0a1, self.seed) - self.nodes[0].sendtoaddress(n2a1, self.seed) - self.sync_all() - self.nodes[0].generate(1) - self.sync_all() - - # create dusty transactions - self.send_dusty_tx(self.nodes[2], n2a2, n0a2, Decimal("0.9")) - self.send_dusty_tx(self.nodes[2], n2a3, n0a3, Decimal("0.09")) - self.send_dusty_tx(self.nodes[0], n2a4, n1a1, Decimal("1")) - - # wait 10 seconds to sync mempools - time.sleep(10) - - assert_equal(self.nodes[2].getmempoolinfo()['size'], 3) - assert_equal(self.nodes[1].getmempoolinfo()['size'], 1) - assert_equal(self.nodes[0].getmempoolinfo()['size'], 2) - - def send_dusty_tx(self, n, addr1, addr2, dust): - avail = n.listunspent(0, 1000, [], True, self.coinselector) - inputs = [ {'txid': avail[0]['txid'], 'vout': avail[0]['vout']}] - outputs = { addr1 : avail[0]['amount'] - self.fee - dust , addr2: dust } - rawtx = n.createrawtransaction(inputs, outputs) - rawtx = n.signrawtransaction(rawtx) - n.sendrawtransaction(rawtx['hex']) - -if __name__ == '__main__': - HardDustLimitTest().main()