diff options
Diffstat (limited to 'test/functional')
-rwxr-xr-x | test/functional/feature_anchors.py | 32 | ||||
-rwxr-xr-x | test/functional/feature_block.py | 21 | ||||
-rwxr-xr-x | test/functional/feature_nulldummy.py | 8 | ||||
-rwxr-xr-x | test/functional/feature_proxy.py | 4 | ||||
-rwxr-xr-x | test/functional/feature_rbf.py | 147 | ||||
-rwxr-xr-x | test/functional/feature_versionbits_warning.py | 4 | ||||
-rwxr-xr-x | test/functional/mempool_package_onemore.py | 43 | ||||
-rwxr-xr-x | test/functional/mempool_packages.py | 30 | ||||
-rwxr-xr-x | test/functional/p2p_compactblocks_hb.py | 97 | ||||
-rwxr-xr-x | test/functional/p2p_invalid_block.py | 19 | ||||
-rwxr-xr-x | test/functional/p2p_leak.py | 6 | ||||
-rwxr-xr-x | test/functional/rpc_packages.py | 3 | ||||
-rw-r--r-- | test/functional/test_framework/blocktools.py | 5 | ||||
-rwxr-xr-x | test/functional/test_framework/messages.py | 2 | ||||
-rwxr-xr-x | test/functional/test_framework/test_node.py | 4 | ||||
-rw-r--r-- | test/functional/test_framework/util.py | 43 | ||||
-rw-r--r-- | test/functional/test_framework/wallet.py | 15 | ||||
-rwxr-xr-x | test/functional/test_runner.py | 22 | ||||
-rwxr-xr-x | test/functional/wallet_orphanedreward.py | 62 | ||||
-rwxr-xr-x | test/functional/wallet_taproot.py | 400 |
20 files changed, 815 insertions, 152 deletions
diff --git a/test/functional/feature_anchors.py b/test/functional/feature_anchors.py index a60a723b3e..24bb02bc90 100755 --- a/test/functional/feature_anchors.py +++ b/test/functional/feature_anchors.py @@ -2,7 +2,7 @@ # Copyright (c) 2020 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. -"""Test Anchors functionality""" +"""Test block-relay-only anchors functionality""" import os @@ -10,6 +10,9 @@ from test_framework.p2p import P2PInterface from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal +INBOUND_CONNECTIONS = 5 +BLOCK_RELAY_CONNECTIONS = 2 + def check_node_connections(*, node, num_in, num_out): info = node.getnetworkinfo() @@ -25,19 +28,26 @@ class AnchorsTest(BitcoinTestFramework): self.setup_nodes() def run_test(self): - self.log.info("Add 2 block-relay-only connections to node 0") - for i in range(2): + node_anchors_path = os.path.join( + self.nodes[0].datadir, "regtest", "anchors.dat" + ) + + self.log.info("When node starts, check if anchors.dat doesn't exist") + assert not os.path.exists(node_anchors_path) + + self.log.info(f"Add {BLOCK_RELAY_CONNECTIONS} block-relay-only connections to node") + for i in range(BLOCK_RELAY_CONNECTIONS): self.log.debug(f"block-relay-only: {i}") self.nodes[0].add_outbound_p2p_connection( P2PInterface(), p2p_idx=i, connection_type="block-relay-only" ) - self.log.info("Add 5 inbound connections to node 0") - for i in range(5): + self.log.info(f"Add {INBOUND_CONNECTIONS} inbound connections to node") + for i in range(INBOUND_CONNECTIONS): self.log.debug(f"inbound: {i}") self.nodes[0].add_p2p_connection(P2PInterface()) - self.log.info("Check node 0 connections") + self.log.info("Check node connections") check_node_connections(node=self.nodes[0], num_in=5, num_out=2) # 127.0.0.1 @@ -57,14 +67,10 @@ class AnchorsTest(BitcoinTestFramework): self.log.info("Stop node 0") self.stop_node(0) - node0_anchors_path = os.path.join( - self.nodes[0].datadir, "regtest", "anchors.dat" - ) - # It should contain only the block-relay-only addresses self.log.info("Check the addresses in anchors.dat") - with open(node0_anchors_path, "rb") as file_handler: + with open(node_anchors_path, "rb") as file_handler: anchors = file_handler.read().hex() for port in block_relay_nodes_port: @@ -74,11 +80,11 @@ class AnchorsTest(BitcoinTestFramework): ip_port = ip + port assert ip_port not in anchors - self.log.info("Start node 0") + self.log.info("Start node") self.start_node(0) self.log.info("When node starts, check if anchors.dat doesn't exist anymore") - assert not os.path.exists(node0_anchors_path) + assert not os.path.exists(node_anchors_path) if __name__ == "__main__": diff --git a/test/functional/feature_block.py b/test/functional/feature_block.py index 158efb52c9..389db73bc9 100755 --- a/test/functional/feature_block.py +++ b/test/functional/feature_block.py @@ -591,6 +591,8 @@ class FullBlockTest(BitcoinTestFramework): b44.hashPrevBlock = self.tip.sha256 b44.nBits = 0x207fffff b44.vtx.append(coinbase) + tx = self.create_and_sign_transaction(out[14], 1) + b44.vtx.append(tx) b44.hashMerkleRoot = b44.calc_merkle_root() b44.solve() self.tip = b44 @@ -678,7 +680,7 @@ class FullBlockTest(BitcoinTestFramework): # Test block timestamps # -> b31 (8) -> b33 (9) -> b35 (10) -> b39 (11) -> b42 (12) -> b43 (13) -> b53 (14) -> b55 (15) # \-> b54 (15) - # + # -> b44 (14)\-> b48 () self.move_tip(43) b53 = self.next_block(53, spend=out[14]) self.send_blocks([b53], False) @@ -698,6 +700,21 @@ class FullBlockTest(BitcoinTestFramework): self.send_blocks([b55], True) self.save_spendable_output() + # The block which was previously rejected because of being "too far(3 hours)" must be accepted 2 hours later. + # The new block is only 1 hour into future now and we must reorg onto to the new longer chain. + # The new bestblock b48p is invalidated manually. + # -> b31 (8) -> b33 (9) -> b35 (10) -> b39 (11) -> b42 (12) -> b43 (13) -> b53 (14) -> b55 (15) + # \-> b54 (15) + # -> b44 (14)\-> b48 () -> b48p () + self.log.info("Accept a previously rejected future block at a later time") + node.setmocktime(int(time.time()) + 2*60*60) + self.move_tip(48) + self.block_heights[b48.sha256] = self.block_heights[b44.sha256] + 1 # b48 is a parent of b44 + b48p = self.next_block("48p") + self.send_blocks([b48, b48p], success=True) # Reorg to the longer chain + node.invalidateblock(b48p.hash) # mark b48p as invalid + node.setmocktime(0) + # Test Merkle tree malleability # # -> b42 (12) -> b43 (13) -> b53 (14) -> b55 (15) -> b57p2 (16) @@ -1308,7 +1325,7 @@ class FullBlockTest(BitcoinTestFramework): return create_tx_with_script(spend_tx, n, amount=value, script_pub_key=script) # sign a transaction, using the key we know about - # this signs input 0 in tx, which is assumed to be spending output n in spend_tx + # this signs input 0 in tx, which is assumed to be spending output 0 in spend_tx def sign_tx(self, tx, spend_tx): scriptPubKey = bytearray(spend_tx.vout[0].scriptPubKey) if (scriptPubKey[0] == OP_TRUE): # an anyone-can-spend diff --git a/test/functional/feature_nulldummy.py b/test/functional/feature_nulldummy.py index 02e387c7ee..f467626801 100755 --- a/test/functional/feature_nulldummy.py +++ b/test/functional/feature_nulldummy.py @@ -28,6 +28,7 @@ from test_framework.util import assert_equal, assert_raises_rpc_error NULLDUMMY_ERROR = "non-mandatory-script-verify-flag (Dummy CHECKMULTISIG argument must be zero)" + def trueDummy(tx): scriptSig = CScript(tx.vin[0].scriptSig) newscript = [] @@ -40,18 +41,17 @@ def trueDummy(tx): tx.vin[0].scriptSig = CScript(newscript) tx.rehash() -class NULLDUMMYTest(BitcoinTestFramework): +class NULLDUMMYTest(BitcoinTestFramework): def set_test_params(self): - # Need two nodes so GBT (getblocktemplate) doesn't complain that it's not connected. - self.num_nodes = 2 + self.num_nodes = 1 self.setup_clean_chain = True # This script tests NULLDUMMY activation, which is part of the 'segwit' deployment, so we go through # normal segwit activation here (and don't use the default always-on behaviour). self.extra_args = [[ f'-segwitheight={COINBASE_MATURITY + 5}', '-addresstype=legacy', - ]] * 2 + ]] def skip_test_if_missing_module(self): self.skip_if_no_wallet() diff --git a/test/functional/feature_proxy.py b/test/functional/feature_proxy.py index 8bee43b8ad..162814815e 100755 --- a/test/functional/feature_proxy.py +++ b/test/functional/feature_proxy.py @@ -147,13 +147,13 @@ class ProxyTest(BitcoinTestFramework): self.network_test(node, addr, network=NET_IPV6) if test_onion: - addr = "bitcoinostk4e4re.onion:8333" + addr = "pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion:8333" self.log.debug("Test: outgoing onion connection through node for address {}".format(addr)) node.addnode(addr, "onetry") cmd = proxies[2].queue.get() assert isinstance(cmd, Socks5Command) assert_equal(cmd.atyp, AddressType.DOMAINNAME) - assert_equal(cmd.addr, b"bitcoinostk4e4re.onion") + assert_equal(cmd.addr, b"pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion") assert_equal(cmd.port, 8333) if not auth: assert_equal(cmd.username, None) diff --git a/test/functional/feature_rbf.py b/test/functional/feature_rbf.py index 344db5f652..b99b64e16d 100755 --- a/test/functional/feature_rbf.py +++ b/test/functional/feature_rbf.py @@ -7,17 +7,20 @@ from decimal import Decimal from test_framework.blocktools import COINBASE_MATURITY -from test_framework.messages import COIN, COutPoint, CTransaction, CTxIn, CTxOut +from test_framework.messages import COIN, COutPoint, CTransaction, CTxIn, CTxOut, BIP125_SEQUENCE_NUMBER from test_framework.script import CScript, OP_DROP from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal, assert_raises_rpc_error, satoshi_round from test_framework.script_util import DUMMY_P2WPKH_SCRIPT, DUMMY_2_P2WPKH_SCRIPT +from test_framework.wallet import MiniWallet MAX_REPLACEMENT_LIMIT = 100 + def txToHex(tx): return tx.serialize().hex() + def make_utxo(node, amount, confirmed=True, scriptPubKey=DUMMY_P2WPKH_SCRIPT): """Create a txout with a given amount and scriptPubKey @@ -26,12 +29,12 @@ def make_utxo(node, amount, confirmed=True, scriptPubKey=DUMMY_P2WPKH_SCRIPT): confirmed - txouts created will be confirmed in the blockchain; unconfirmed otherwise. """ - fee = 1*COIN - while node.getbalance() < satoshi_round((amount + fee)/COIN): + fee = 1 * COIN + while node.getbalance() < satoshi_round((amount + fee) / COIN): node.generate(COINBASE_MATURITY) new_addr = node.getnewaddress() - txid = node.sendtoaddress(new_addr, satoshi_round((amount+fee)/COIN)) + txid = node.sendtoaddress(new_addr, satoshi_round((amount + fee) / COIN)) tx1 = node.getrawtransaction(txid, 1) txid = int(txid, 16) i, _ = next(filter(lambda vout: new_addr == vout[1]['scriptPubKey']['address'], enumerate(tx1['vout']))) @@ -78,10 +81,7 @@ class ReplaceByFeeTest(BitcoinTestFramework): self.skip_if_no_wallet() def run_test(self): - # Leave IBD - self.nodes[0].generate(1) - - make_utxo(self.nodes[0], 1*COIN) + make_utxo(self.nodes[0], 1 * COIN) # Ensure nodes are synced self.sync_all() @@ -116,11 +116,14 @@ class ReplaceByFeeTest(BitcoinTestFramework): self.log.info("Running test prioritised transactions...") self.test_prioritised_transactions() + self.log.info("Running test no inherited signaling...") + self.test_no_inherited_signaling() + self.log.info("Passed") def test_simple_doublespend(self): """Simple doublespend""" - tx0_outpoint = make_utxo(self.nodes[0], int(1.1*COIN)) + tx0_outpoint = make_utxo(self.nodes[0], int(1.1 * COIN)) # make_utxo may have generated a bunch of blocks, so we need to sync # before we can spend the coins generated, or else the resulting @@ -162,14 +165,14 @@ class ReplaceByFeeTest(BitcoinTestFramework): def test_doublespend_chain(self): """Doublespend of a long chain""" - initial_nValue = 50*COIN + initial_nValue = 50 * COIN tx0_outpoint = make_utxo(self.nodes[0], initial_nValue) prevout = tx0_outpoint remaining_value = initial_nValue chain_txids = [] - while remaining_value > 10*COIN: - remaining_value -= 1*COIN + while remaining_value > 10 * COIN: + remaining_value -= 1 * COIN tx = CTransaction() tx.vin = [CTxIn(prevout, nSequence=0)] tx.vout = [CTxOut(remaining_value, CScript([1, OP_DROP] * 15 + [1]))] @@ -202,10 +205,10 @@ class ReplaceByFeeTest(BitcoinTestFramework): def test_doublespend_tree(self): """Doublespend of a big tree of transactions""" - initial_nValue = 50*COIN + initial_nValue = 50 * COIN tx0_outpoint = make_utxo(self.nodes[0], initial_nValue) - def branch(prevout, initial_value, max_txs, tree_width=5, fee=0.0001*COIN, _total_txs=None): + def branch(prevout, initial_value, max_txs, tree_width=5, fee=0.0001 * COIN, _total_txs=None): if _total_txs is None: _total_txs = [0] if _total_txs[0] >= max_txs: @@ -236,7 +239,7 @@ class ReplaceByFeeTest(BitcoinTestFramework): _total_txs=_total_txs): yield x - fee = int(0.0001*COIN) + fee = int(0.0001 * COIN) n = MAX_REPLACEMENT_LIMIT tree_txs = list(branch(tx0_outpoint, initial_nValue, n, fee=fee)) assert_equal(len(tree_txs), n) @@ -264,8 +267,8 @@ class ReplaceByFeeTest(BitcoinTestFramework): # Try again, but with more total transactions than the "max txs # double-spent at once" anti-DoS limit. - for n in (MAX_REPLACEMENT_LIMIT+1, MAX_REPLACEMENT_LIMIT*2): - fee = int(0.0001*COIN) + for n in (MAX_REPLACEMENT_LIMIT + 1, MAX_REPLACEMENT_LIMIT * 2): + fee = int(0.0001 * COIN) tx0_outpoint = make_utxo(self.nodes[0], initial_nValue) tree_txs = list(branch(tx0_outpoint, initial_nValue, n, fee=fee)) assert_equal(len(tree_txs), n) @@ -283,7 +286,7 @@ class ReplaceByFeeTest(BitcoinTestFramework): def test_replacement_feeperkb(self): """Replacement requires fee-per-KB to be higher""" - tx0_outpoint = make_utxo(self.nodes[0], int(1.1*COIN)) + tx0_outpoint = make_utxo(self.nodes[0], int(1.1 * COIN)) tx1a = CTransaction() tx1a.vin = [CTxIn(tx0_outpoint, nSequence=0)] @@ -295,7 +298,7 @@ class ReplaceByFeeTest(BitcoinTestFramework): # rejected. tx1b = CTransaction() tx1b.vin = [CTxIn(tx0_outpoint, nSequence=0)] - tx1b.vout = [CTxOut(int(0.001*COIN), CScript([b'a'*999000]))] + tx1b.vout = [CTxOut(int(0.001 * COIN), CScript([b'a' * 999000]))] tx1b_hex = txToHex(tx1b) # This will raise an exception due to insufficient fee @@ -303,8 +306,8 @@ class ReplaceByFeeTest(BitcoinTestFramework): def test_spends_of_conflicting_outputs(self): """Replacements that spend conflicting tx outputs are rejected""" - utxo1 = make_utxo(self.nodes[0], int(1.2*COIN)) - utxo2 = make_utxo(self.nodes[0], 3*COIN) + utxo1 = make_utxo(self.nodes[0], int(1.2 * COIN)) + utxo2 = make_utxo(self.nodes[0], 3 * COIN) tx1a = CTransaction() tx1a.vin = [CTxIn(utxo1, nSequence=0)] @@ -343,8 +346,8 @@ class ReplaceByFeeTest(BitcoinTestFramework): def test_new_unconfirmed_inputs(self): """Replacements that add new unconfirmed inputs are rejected""" - confirmed_utxo = make_utxo(self.nodes[0], int(1.1*COIN)) - unconfirmed_utxo = make_utxo(self.nodes[0], int(0.1*COIN), False) + confirmed_utxo = make_utxo(self.nodes[0], int(1.1 * COIN)) + unconfirmed_utxo = make_utxo(self.nodes[0], int(0.1 * COIN), False) tx1 = CTransaction() tx1.vin = [CTxIn(confirmed_utxo)] @@ -366,13 +369,13 @@ class ReplaceByFeeTest(BitcoinTestFramework): # transactions # Start by creating a single transaction with many outputs - initial_nValue = 10*COIN + initial_nValue = 10 * COIN utxo = make_utxo(self.nodes[0], initial_nValue) - fee = int(0.0001*COIN) - split_value = int((initial_nValue-fee)/(MAX_REPLACEMENT_LIMIT+1)) + fee = int(0.0001 * COIN) + split_value = int((initial_nValue - fee) / (MAX_REPLACEMENT_LIMIT + 1)) outputs = [] - for _ in range(MAX_REPLACEMENT_LIMIT+1): + for _ in range(MAX_REPLACEMENT_LIMIT + 1): outputs.append(CTxOut(split_value, CScript([1]))) splitting_tx = CTransaction() @@ -384,7 +387,7 @@ class ReplaceByFeeTest(BitcoinTestFramework): txid = int(txid, 16) # Now spend each of those outputs individually - for i in range(MAX_REPLACEMENT_LIMIT+1): + for i in range(MAX_REPLACEMENT_LIMIT + 1): tx_i = CTransaction() tx_i.vin = [CTxIn(COutPoint(txid, i), nSequence=0)] tx_i.vout = [CTxOut(split_value - fee, DUMMY_P2WPKH_SCRIPT)] @@ -394,9 +397,9 @@ class ReplaceByFeeTest(BitcoinTestFramework): # Now create doublespend of the whole lot; should fail. # Need a big enough fee to cover all spending transactions and have # a higher fee rate - double_spend_value = (split_value-100*fee)*(MAX_REPLACEMENT_LIMIT+1) + double_spend_value = (split_value - 100 * fee) * (MAX_REPLACEMENT_LIMIT + 1) inputs = [] - for i in range(MAX_REPLACEMENT_LIMIT+1): + for i in range(MAX_REPLACEMENT_LIMIT + 1): inputs.append(CTxIn(COutPoint(txid, i), nSequence=0)) double_tx = CTransaction() double_tx.vin = inputs @@ -415,7 +418,7 @@ class ReplaceByFeeTest(BitcoinTestFramework): def test_opt_in(self): """Replacing should only work if orig tx opted in""" - tx0_outpoint = make_utxo(self.nodes[0], int(1.1*COIN)) + tx0_outpoint = make_utxo(self.nodes[0], int(1.1 * COIN)) # Create a non-opting in transaction tx1a = CTransaction() @@ -436,7 +439,7 @@ class ReplaceByFeeTest(BitcoinTestFramework): # This will raise an exception assert_raises_rpc_error(-26, "txn-mempool-conflict", self.nodes[0].sendrawtransaction, tx1b_hex, 0) - tx1_outpoint = make_utxo(self.nodes[0], int(1.1*COIN)) + tx1_outpoint = make_utxo(self.nodes[0], int(1.1 * COIN)) # Create a different non-opting in transaction tx2a = CTransaction() @@ -464,7 +467,7 @@ class ReplaceByFeeTest(BitcoinTestFramework): tx3a = CTransaction() tx3a.vin = [CTxIn(COutPoint(tx1a_txid, 0), nSequence=0xffffffff), CTxIn(COutPoint(tx2a_txid, 0), nSequence=0xfffffffd)] - tx3a.vout = [CTxOut(int(0.9*COIN), CScript([b'c'])), CTxOut(int(0.9*COIN), CScript([b'd']))] + tx3a.vout = [CTxOut(int(0.9 * COIN), CScript([b'c'])), CTxOut(int(0.9 * COIN), CScript([b'd']))] tx3a_hex = txToHex(tx3a) tx3a_txid = self.nodes[0].sendrawtransaction(tx3a_hex, 0) @@ -492,7 +495,7 @@ class ReplaceByFeeTest(BitcoinTestFramework): # correctly used by replacement logic # 1. Check that feeperkb uses modified fees - tx0_outpoint = make_utxo(self.nodes[0], int(1.1*COIN)) + tx0_outpoint = make_utxo(self.nodes[0], int(1.1 * COIN)) tx1a = CTransaction() tx1a.vin = [CTxIn(tx0_outpoint, nSequence=0)] @@ -503,14 +506,14 @@ class ReplaceByFeeTest(BitcoinTestFramework): # Higher fee, but the actual fee per KB is much lower. tx1b = CTransaction() tx1b.vin = [CTxIn(tx0_outpoint, nSequence=0)] - tx1b.vout = [CTxOut(int(0.001*COIN), CScript([b'a'*740000]))] + tx1b.vout = [CTxOut(int(0.001 * COIN), CScript([b'a' * 740000]))] tx1b_hex = txToHex(tx1b) # Verify tx1b cannot replace tx1a. assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, tx1b_hex, 0) # Use prioritisetransaction to set tx1a's fee to 0. - self.nodes[0].prioritisetransaction(txid=tx1a_txid, fee_delta=int(-0.1*COIN)) + self.nodes[0].prioritisetransaction(txid=tx1a_txid, fee_delta=int(-0.1 * COIN)) # Now tx1b should be able to replace tx1a tx1b_txid = self.nodes[0].sendrawtransaction(tx1b_hex, 0) @@ -518,7 +521,7 @@ class ReplaceByFeeTest(BitcoinTestFramework): assert tx1b_txid in self.nodes[0].getrawmempool() # 2. Check that absolute fee checks use modified fee. - tx1_outpoint = make_utxo(self.nodes[0], int(1.1*COIN)) + tx1_outpoint = make_utxo(self.nodes[0], int(1.1 * COIN)) tx2a = CTransaction() tx2a.vin = [CTxIn(tx1_outpoint, nSequence=0)] @@ -537,7 +540,7 @@ class ReplaceByFeeTest(BitcoinTestFramework): assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, tx2b_hex, 0) # Now prioritise tx2b to have a higher modified fee - self.nodes[0].prioritisetransaction(txid=tx2b.hash, fee_delta=int(0.1*COIN)) + self.nodes[0].prioritisetransaction(txid=tx2b.hash, fee_delta=int(0.1 * COIN)) # tx2b should now be accepted tx2b_txid = self.nodes[0].sendrawtransaction(tx2b_hex, 0) @@ -547,11 +550,11 @@ class ReplaceByFeeTest(BitcoinTestFramework): def test_rpc(self): us0 = self.nodes[0].listunspent()[0] ins = [us0] - outs = {self.nodes[0].getnewaddress() : Decimal(1.0000000)} + outs = {self.nodes[0].getnewaddress(): Decimal(1.0000000)} rawtx0 = self.nodes[0].createrawtransaction(ins, outs, 0, True) rawtx1 = self.nodes[0].createrawtransaction(ins, outs, 0, False) - json0 = self.nodes[0].decoderawtransaction(rawtx0) - json1 = self.nodes[0].decoderawtransaction(rawtx1) + json0 = self.nodes[0].decoderawtransaction(rawtx0) + json1 = self.nodes[0].decoderawtransaction(rawtx1) assert_equal(json0["vin"][0]["sequence"], 4294967293) assert_equal(json1["vin"][0]["sequence"], 4294967295) @@ -559,10 +562,68 @@ class ReplaceByFeeTest(BitcoinTestFramework): frawtx2a = self.nodes[0].fundrawtransaction(rawtx2, {"replaceable": True}) frawtx2b = self.nodes[0].fundrawtransaction(rawtx2, {"replaceable": False}) - json0 = self.nodes[0].decoderawtransaction(frawtx2a['hex']) - json1 = self.nodes[0].decoderawtransaction(frawtx2b['hex']) + json0 = self.nodes[0].decoderawtransaction(frawtx2a['hex']) + json1 = self.nodes[0].decoderawtransaction(frawtx2b['hex']) assert_equal(json0["vin"][0]["sequence"], 4294967293) assert_equal(json1["vin"][0]["sequence"], 4294967294) + def test_no_inherited_signaling(self): + wallet = MiniWallet(self.nodes[0]) + wallet.scan_blocks(start=76, num=1) + confirmed_utxo = wallet.get_utxo() + + # Create an explicitly opt-in parent transaction + optin_parent_tx = wallet.send_self_transfer( + from_node=self.nodes[0], + utxo_to_spend=confirmed_utxo, + sequence=BIP125_SEQUENCE_NUMBER, + fee_rate=Decimal('0.01'), + ) + assert_equal(True, self.nodes[0].getmempoolentry(optin_parent_tx['txid'])['bip125-replaceable']) + + replacement_parent_tx = wallet.create_self_transfer( + from_node=self.nodes[0], + utxo_to_spend=confirmed_utxo, + sequence=BIP125_SEQUENCE_NUMBER, + fee_rate=Decimal('0.02'), + ) + + # Test if parent tx can be replaced. + res = self.nodes[0].testmempoolaccept(rawtxs=[replacement_parent_tx['hex']])[0] + + # Parent can be replaced. + assert_equal(res['allowed'], True) + + # Create an opt-out child tx spending the opt-in parent + parent_utxo = wallet.get_utxo(txid=optin_parent_tx['txid']) + optout_child_tx = wallet.send_self_transfer( + from_node=self.nodes[0], + utxo_to_spend=parent_utxo, + sequence=0xffffffff, + fee_rate=Decimal('0.01'), + ) + + # Reports true due to inheritance + assert_equal(True, self.nodes[0].getmempoolentry(optout_child_tx['txid'])['bip125-replaceable']) + + replacement_child_tx = wallet.create_self_transfer( + from_node=self.nodes[0], + utxo_to_spend=parent_utxo, + sequence=0xffffffff, + fee_rate=Decimal('0.02'), + mempool_valid=False, + ) + + # Broadcast replacement child tx + # BIP 125 : + # 1. The original transactions signal replaceability explicitly or through inheritance as described in the above + # Summary section. + # The original transaction (`optout_child_tx`) doesn't signal RBF but its parent (`optin_parent_tx`) does. + # The replacement transaction (`replacement_child_tx`) should be able to replace the original transaction. + # See CVE-2021-31876 for further explanations. + assert_equal(True, self.nodes[0].getmempoolentry(optin_parent_tx['txid'])['bip125-replaceable']) + assert_raises_rpc_error(-26, 'txn-mempool-conflict', self.nodes[0].sendrawtransaction, replacement_child_tx["hex"], 0) + + if __name__ == '__main__': ReplaceByFeeTest().main() diff --git a/test/functional/feature_versionbits_warning.py b/test/functional/feature_versionbits_warning.py index 062a7affb5..1c9e237d78 100755 --- a/test/functional/feature_versionbits_warning.py +++ b/test/functional/feature_versionbits_warning.py @@ -21,8 +21,8 @@ VB_TOP_BITS = 0x20000000 VB_UNKNOWN_BIT = 27 # Choose a bit unassigned to any deployment VB_UNKNOWN_VERSION = VB_TOP_BITS | (1 << VB_UNKNOWN_BIT) -WARN_UNKNOWN_RULES_ACTIVE = "unknown new rules activated (versionbit {})".format(VB_UNKNOWN_BIT) -VB_PATTERN = re.compile("Warning: unknown new rules activated.*versionbit") +WARN_UNKNOWN_RULES_ACTIVE = "Unknown new rules activated (versionbit {})".format(VB_UNKNOWN_BIT) +VB_PATTERN = re.compile("Unknown new rules activated.*versionbit") class VersionBitsWarningTest(BitcoinTestFramework): def set_test_params(self): diff --git a/test/functional/mempool_package_onemore.py b/test/functional/mempool_package_onemore.py index 1e9895e621..fcd8b061fa 100755 --- a/test/functional/mempool_package_onemore.py +++ b/test/functional/mempool_package_onemore.py @@ -11,7 +11,11 @@ from decimal import Decimal from test_framework.blocktools import COINBASE_MATURITY from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import assert_equal, assert_raises_rpc_error, satoshi_round +from test_framework.util import ( + assert_equal, + assert_raises_rpc_error, + chain_transaction, +) MAX_ANCESTORS = 25 MAX_DESCENDANTS = 25 @@ -24,23 +28,6 @@ class MempoolPackagesTest(BitcoinTestFramework): def skip_test_if_missing_module(self): self.skip_if_no_wallet() - # Build a transaction that spends parent_txid:vout - # Return amount sent - def chain_transaction(self, node, parent_txids, vouts, value, fee, num_outputs): - send_value = satoshi_round((value - fee)/num_outputs) - inputs = [] - for (txid, vout) in zip(parent_txids, vouts): - inputs.append({'txid' : txid, 'vout' : vout}) - outputs = {} - for _ in range(num_outputs): - outputs[node.getnewaddress()] = send_value - rawtx = node.createrawtransaction(inputs, outputs, 0, True) - signedtx = node.signrawtransactionwithwallet(rawtx) - txid = node.sendrawtransaction(signedtx['hex']) - fulltx = node.getrawtransaction(txid, 1) - assert len(fulltx['vout']) == num_outputs # make sure we didn't generate a change output - return (txid, send_value) - def run_test(self): # Mine some blocks and have them mature. self.nodes[0].generate(COINBASE_MATURITY + 1) @@ -53,32 +40,32 @@ class MempoolPackagesTest(BitcoinTestFramework): # MAX_ANCESTORS transactions off a confirmed tx should be fine chain = [] for _ in range(4): - (txid, sent_value) = self.chain_transaction(self.nodes[0], [txid], [vout], value, fee, 2) + (txid, sent_value) = chain_transaction(self.nodes[0], [txid], [vout], value, fee, 2) vout = 0 value = sent_value chain.append([txid, value]) for _ in range(MAX_ANCESTORS - 4): - (txid, sent_value) = self.chain_transaction(self.nodes[0], [txid], [0], value, fee, 1) + (txid, sent_value) = chain_transaction(self.nodes[0], [txid], [0], value, fee, 1) value = sent_value chain.append([txid, value]) - (second_chain, second_chain_value) = self.chain_transaction(self.nodes[0], [utxo[1]['txid']], [utxo[1]['vout']], utxo[1]['amount'], fee, 1) + (second_chain, second_chain_value) = chain_transaction(self.nodes[0], [utxo[1]['txid']], [utxo[1]['vout']], utxo[1]['amount'], fee, 1) # Check mempool has MAX_ANCESTORS + 1 transactions in it assert_equal(len(self.nodes[0].getrawmempool(True)), MAX_ANCESTORS + 1) # Adding one more transaction on to the chain should fail. - assert_raises_rpc_error(-26, "too-long-mempool-chain, too many unconfirmed ancestors [limit: 25]", self.chain_transaction, self.nodes[0], [txid], [0], value, fee, 1) + assert_raises_rpc_error(-26, "too-long-mempool-chain, too many unconfirmed ancestors [limit: 25]", chain_transaction, self.nodes[0], [txid], [0], value, fee, 1) # ...even if it chains on from some point in the middle of the chain. - assert_raises_rpc_error(-26, "too-long-mempool-chain, too many descendants", self.chain_transaction, self.nodes[0], [chain[2][0]], [1], chain[2][1], fee, 1) - assert_raises_rpc_error(-26, "too-long-mempool-chain, too many descendants", self.chain_transaction, self.nodes[0], [chain[1][0]], [1], chain[1][1], fee, 1) + assert_raises_rpc_error(-26, "too-long-mempool-chain, too many descendants", chain_transaction, self.nodes[0], [chain[2][0]], [1], chain[2][1], fee, 1) + assert_raises_rpc_error(-26, "too-long-mempool-chain, too many descendants", chain_transaction, self.nodes[0], [chain[1][0]], [1], chain[1][1], fee, 1) # ...even if it chains on to two parent transactions with one in the chain. - assert_raises_rpc_error(-26, "too-long-mempool-chain, too many descendants", self.chain_transaction, self.nodes[0], [chain[0][0], second_chain], [1, 0], chain[0][1] + second_chain_value, fee, 1) + assert_raises_rpc_error(-26, "too-long-mempool-chain, too many descendants", chain_transaction, self.nodes[0], [chain[0][0], second_chain], [1, 0], chain[0][1] + second_chain_value, fee, 1) # ...especially if its > 40k weight - assert_raises_rpc_error(-26, "too-long-mempool-chain, too many descendants", self.chain_transaction, self.nodes[0], [chain[0][0]], [1], chain[0][1], fee, 350) + assert_raises_rpc_error(-26, "too-long-mempool-chain, too many descendants", chain_transaction, self.nodes[0], [chain[0][0]], [1], chain[0][1], fee, 350) # But not if it chains directly off the first transaction - (replacable_txid, replacable_orig_value) = self.chain_transaction(self.nodes[0], [chain[0][0]], [1], chain[0][1], fee, 1) + (replacable_txid, replacable_orig_value) = chain_transaction(self.nodes[0], [chain[0][0]], [1], chain[0][1], fee, 1) # and the second chain should work just fine - self.chain_transaction(self.nodes[0], [second_chain], [0], second_chain_value, fee, 1) + chain_transaction(self.nodes[0], [second_chain], [0], second_chain_value, fee, 1) # Make sure we can RBF the chain which used our carve-out rule second_tx_outputs = {self.nodes[0].getrawtransaction(replacable_txid, True)["vout"][0]['scriptPubKey']['address']: replacable_orig_value - (Decimal(1) / Decimal(100))} diff --git a/test/functional/mempool_packages.py b/test/functional/mempool_packages.py index 606717d890..5fc3ec23ae 100755 --- a/test/functional/mempool_packages.py +++ b/test/functional/mempool_packages.py @@ -13,6 +13,7 @@ from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_raises_rpc_error, + chain_transaction, satoshi_round, ) @@ -42,21 +43,6 @@ class MempoolPackagesTest(BitcoinTestFramework): def skip_test_if_missing_module(self): self.skip_if_no_wallet() - # Build a transaction that spends parent_txid:vout - # Return amount sent - def chain_transaction(self, node, parent_txid, vout, value, fee, num_outputs): - send_value = satoshi_round((value - fee)/num_outputs) - inputs = [ {'txid' : parent_txid, 'vout' : vout} ] - outputs = {} - for _ in range(num_outputs): - outputs[node.getnewaddress()] = send_value - rawtx = node.createrawtransaction(inputs, outputs) - signedtx = node.signrawtransactionwithwallet(rawtx) - txid = node.sendrawtransaction(signedtx['hex']) - fulltx = node.getrawtransaction(txid, 1) - assert len(fulltx['vout']) == num_outputs # make sure we didn't generate a change output - return (txid, send_value) - def run_test(self): # Mine some blocks and have them mature. peer_inv_store = self.nodes[0].add_p2p_connection(P2PTxInvStore()) # keep track of invs @@ -71,7 +57,7 @@ class MempoolPackagesTest(BitcoinTestFramework): chain = [] witness_chain = [] for _ in range(MAX_ANCESTORS): - (txid, sent_value) = self.chain_transaction(self.nodes[0], txid, 0, value, fee, 1) + (txid, sent_value) = chain_transaction(self.nodes[0], [txid], [0], value, fee, 1) value = sent_value chain.append(txid) # We need the wtxids to check P2P announcements @@ -189,7 +175,7 @@ class MempoolPackagesTest(BitcoinTestFramework): assert_equal(mempool[x]['descendantfees'], descendant_fees * COIN + 1000) # Adding one more transaction on to the chain should fail. - assert_raises_rpc_error(-26, "too-long-mempool-chain", self.chain_transaction, self.nodes[0], txid, vout, value, fee, 1) + assert_raises_rpc_error(-26, "too-long-mempool-chain", chain_transaction, self.nodes[0], [txid], [vout], value, fee, 1) # Check that prioritising a tx before it's added to the mempool works # First clear the mempool by mining a block. @@ -238,7 +224,7 @@ class MempoolPackagesTest(BitcoinTestFramework): transaction_package = [] tx_children = [] # First create one parent tx with 10 children - (txid, sent_value) = self.chain_transaction(self.nodes[0], txid, vout, value, fee, 10) + (txid, sent_value) = chain_transaction(self.nodes[0], [txid], [vout], value, fee, 10) parent_transaction = txid for i in range(10): transaction_package.append({'txid': txid, 'vout': i, 'amount': sent_value}) @@ -247,7 +233,7 @@ class MempoolPackagesTest(BitcoinTestFramework): chain = [] # save sent txs for the purpose of checking node1's mempool later (see below) for _ in range(MAX_DESCENDANTS - 1): utxo = transaction_package.pop(0) - (txid, sent_value) = self.chain_transaction(self.nodes[0], utxo['txid'], utxo['vout'], utxo['amount'], fee, 10) + (txid, sent_value) = chain_transaction(self.nodes[0], [utxo['txid']], [utxo['vout']], utxo['amount'], fee, 10) chain.append(txid) if utxo['txid'] is parent_transaction: tx_children.append(txid) @@ -263,7 +249,7 @@ class MempoolPackagesTest(BitcoinTestFramework): # Sending one more chained transaction will fail utxo = transaction_package.pop(0) - assert_raises_rpc_error(-26, "too-long-mempool-chain", self.chain_transaction, self.nodes[0], utxo['txid'], utxo['vout'], utxo['amount'], fee, 10) + assert_raises_rpc_error(-26, "too-long-mempool-chain", chain_transaction, self.nodes[0], [utxo['txid']], [utxo['vout']], utxo['amount'], fee, 10) # Check that node1's mempool is as expected, containing: # - txs from previous ancestor test (-> custom ancestor limit) @@ -321,13 +307,13 @@ class MempoolPackagesTest(BitcoinTestFramework): value = send_value # Create tx1 - tx1_id, _ = self.chain_transaction(self.nodes[0], tx0_id, 0, value, fee, 1) + tx1_id, _ = chain_transaction(self.nodes[0], [tx0_id], [0], value, fee, 1) # Create tx2-7 vout = 1 txid = tx0_id for _ in range(6): - (txid, sent_value) = self.chain_transaction(self.nodes[0], txid, vout, value, fee, 1) + (txid, sent_value) = chain_transaction(self.nodes[0], [txid], [vout], value, fee, 1) vout = 0 value = sent_value diff --git a/test/functional/p2p_compactblocks_hb.py b/test/functional/p2p_compactblocks_hb.py new file mode 100755 index 0000000000..a3d30a6f04 --- /dev/null +++ b/test/functional/p2p_compactblocks_hb.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +# Copyright (c) 2021 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test compact blocks HB selection logic.""" + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal + + +class CompactBlocksConnectionTest(BitcoinTestFramework): + """Test class for verifying selection of HB peer connections.""" + + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 6 + + def peer_info(self, from_node, to_node): + """Query from_node for its getpeerinfo about to_node.""" + for peerinfo in self.nodes[from_node].getpeerinfo(): + if "(testnode%i)" % to_node in peerinfo['subver']: + return peerinfo + return None + + def setup_network(self): + self.setup_nodes() + # Start network with everyone disconnected + self.sync_all() + + def relay_block_through(self, peer): + """Relay a new block through peer peer, and return HB status between 1 and [2,3,4,5].""" + self.connect_nodes(peer, 0) + self.nodes[0].generate(1) + self.sync_blocks() + self.disconnect_nodes(peer, 0) + status_to = [self.peer_info(1, i)['bip152_hb_to'] for i in range(2, 6)] + status_from = [self.peer_info(i, 1)['bip152_hb_from'] for i in range(2, 6)] + assert_equal(status_to, status_from) + return status_to + + def run_test(self): + self.log.info("Testing reserved high-bandwidth mode slot for outbound peer...") + + # Connect everyone to node 0, and mine some blocks to get all nodes out of IBD. + for i in range(1, 6): + self.connect_nodes(i, 0) + self.nodes[0].generate(2) + self.sync_blocks() + for i in range(1, 6): + self.disconnect_nodes(i, 0) + + # Construct network topology: + # - Node 0 is the block producer + # - Node 1 is the "target" node being tested + # - Nodes 2-5 are intermediaries. + # - Node 1 has an outbound connection to node 2 + # - Node 1 has inbound connections from nodes 3-5 + self.connect_nodes(3, 1) + self.connect_nodes(4, 1) + self.connect_nodes(5, 1) + self.connect_nodes(1, 2) + + # Mine blocks subsequently relaying through nodes 3,4,5 (inbound to node 1) + for nodeid in range(3, 6): + status = self.relay_block_through(nodeid) + assert_equal(status, [False, nodeid >= 3, nodeid >= 4, nodeid >= 5]) + + # And again through each. This should not change HB status. + for nodeid in range(3, 6): + status = self.relay_block_through(nodeid) + assert_equal(status, [False, True, True, True]) + + # Now relay one block through peer 2 (outbound from node 1), so it should take HB status + # from one of the inbounds. + status = self.relay_block_through(2) + assert_equal(status[0], True) + assert_equal(sum(status), 3) + + # Now relay again through nodes 3,4,5. Since 2 is outbound, it should remain HB. + for nodeid in range(3, 6): + status = self.relay_block_through(nodeid) + assert status[0] + assert status[nodeid - 2] + assert_equal(sum(status), 3) + + # Reconnect peer 2, and retry. Now the three inbounds should be HB again. + self.disconnect_nodes(1, 2) + self.connect_nodes(1, 2) + for nodeid in range(3, 6): + status = self.relay_block_through(nodeid) + assert not status[0] + assert status[nodeid - 2] + assert_equal(status, [False, True, True, True]) + + +if __name__ == '__main__': + CompactBlocksConnectionTest().main() diff --git a/test/functional/p2p_invalid_block.py b/test/functional/p2p_invalid_block.py index 483f25f48c..91666d0f08 100755 --- a/test/functional/p2p_invalid_block.py +++ b/test/functional/p2p_invalid_block.py @@ -9,8 +9,11 @@ In this test we connect to one node over p2p, and test block requests: 2) Invalid block with duplicated transaction should be re-requested. 3) Invalid block with bad coinbase value should be rejected and not re-requested. +4) Invalid block due to future timestamp is later accepted when that timestamp +becomes valid. """ import copy +import time from test_framework.blocktools import create_block, create_coinbase, create_tx_with_script from test_framework.messages import COIN @@ -18,6 +21,9 @@ from test_framework.p2p import P2PDataStore from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal +MAX_FUTURE_BLOCK_TIME = 2 * 60 * 60 + + class InvalidBlockRequestTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 @@ -133,5 +139,18 @@ class InvalidBlockRequestTest(BitcoinTestFramework): self.log.info("Test inflation by duplicating input") peer.send_blocks_and_test([block4], node, success=False, reject_reason='bad-txns-inputs-duplicate') + self.log.info("Test accepting identical block after rejecting it due to a future timestamp.") + t = int(time.time()) + node.setmocktime(t) + # Set block time +1 second past max future validity + block = create_block(tip, create_coinbase(height), t + MAX_FUTURE_BLOCK_TIME + 1) + block.hashMerkleRoot = block.calc_merkle_root() + block.solve() + # Need force_send because the block will get rejected without a getdata otherwise + peer.send_blocks_and_test([block], node, force_send=True, success=False, reject_reason='time-too-new') + node.setmocktime(t + 1) + peer.send_blocks_and_test([block], node, success=True) + + if __name__ == '__main__': InvalidBlockRequestTest().main() diff --git a/test/functional/p2p_leak.py b/test/functional/p2p_leak.py index 71d5ca92b3..f1538e8ac7 100755 --- a/test/functional/p2p_leak.py +++ b/test/functional/p2p_leak.py @@ -29,6 +29,8 @@ from test_framework.util import ( assert_greater_than_or_equal, ) +PEER_TIMEOUT = 3 + class LazyPeer(P2PInterface): def __init__(self): @@ -98,7 +100,7 @@ class P2PVersionStore(P2PInterface): class P2PLeakTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 - self.extra_args = [['-peertimeout=4']] + self.extra_args = [[f"-peertimeout={PEER_TIMEOUT}"]] def create_old_version(self, nversion): old_version_msg = msg_version() @@ -134,7 +136,7 @@ class P2PLeakTest(BitcoinTestFramework): self.nodes[0].generate(nblocks=1) # Give the node enough time to possibly leak out a message - time.sleep(5) + time.sleep(PEER_TIMEOUT + 2) # Make sure only expected messages came in assert not no_version_idle_peer.unexpected_msg diff --git a/test/functional/rpc_packages.py b/test/functional/rpc_packages.py index 3d8d81d6b8..d0848d9bd6 100755 --- a/test/functional/rpc_packages.py +++ b/test/functional/rpc_packages.py @@ -354,7 +354,8 @@ class RPCPackagesTest(BitcoinTestFramework): # This transaction is a valid BIP125 replace-by-fee assert testres_rbf_single[0]["allowed"] testres_rbf_package = self.independent_txns_testres_blank + [{ - "txid": replacement_tx.rehash(), "wtxid": replacement_tx.getwtxid(), "allowed": False, "reject-reason": "txn-mempool-conflict" + "txid": replacement_tx.rehash(), "wtxid": replacement_tx.getwtxid(), "allowed": False, + "reject-reason": "bip125-replacement-disallowed" }] self.assert_testres_equal(self.independent_txns_hex + [signed_replacement_tx["hex"]], testres_rbf_package) diff --git a/test/functional/test_framework/blocktools.py b/test/functional/test_framework/blocktools.py index e91b44e776..f35ea6c122 100644 --- a/test/functional/test_framework/blocktools.py +++ b/test/functional/test_framework/blocktools.py @@ -181,6 +181,11 @@ def create_raw_transaction(node, txid, to_address, *, amount): signed_psbt = wrpc.walletprocesspsbt(psbt) psbt = signed_psbt['psbt'] final_psbt = node.finalizepsbt(psbt) + if not final_psbt["complete"]: + node.log.info(f'final_psbt={final_psbt}') + for w in node.listwallets(): + wrpc = node.get_wallet_rpc(w) + node.log.info(f'listunspent={wrpc.listunspent()}') assert_equal(final_psbt["complete"], True) return final_psbt['hex'] diff --git a/test/functional/test_framework/messages.py b/test/functional/test_framework/messages.py index 5a9736a7a3..3d79174356 100755 --- a/test/functional/test_framework/messages.py +++ b/test/functional/test_framework/messages.py @@ -39,7 +39,7 @@ MAX_BLOOM_HASH_FUNCS = 50 COIN = 100000000 # 1 btc in satoshis MAX_MONEY = 21000000 * COIN -BIP125_SEQUENCE_NUMBER = 0xfffffffd # Sequence number that is BIP 125 opt-in and BIP 68-opt-out +BIP125_SEQUENCE_NUMBER = 0xfffffffd # Sequence number that is rbf-opt-in (BIP 125) and csv-opt-out (BIP 68) MAX_PROTOCOL_MESSAGE_LENGTH = 4000000 # Maximum length of incoming protocol messages MAX_HEADERS_RESULTS = 2000 # Number of headers sent in one getheaders result diff --git a/test/functional/test_framework/test_node.py b/test/functional/test_framework/test_node.py index c17c16f797..ba52abc7dd 100755 --- a/test/functional/test_framework/test_node.py +++ b/test/functional/test_framework/test_node.py @@ -400,14 +400,14 @@ class TestNode(): self._raise_assertion_error('Expected messages "{}" does not partially match log:\n\n{}\n\n'.format(str(expected_msgs), print_log)) @contextlib.contextmanager - def profile_with_perf(self, profile_name): + def profile_with_perf(self, profile_name: str): """ Context manager that allows easy profiling of node activity using `perf`. See `test/functional/README.md` for details on perf usage. Args: - profile_name (str): This string will be appended to the + profile_name: This string will be appended to the profile data filename generated by perf. """ subp = self._start_perf(profile_name) diff --git a/test/functional/test_framework/util.py b/test/functional/test_framework/util.py index 55166ba0ad..f354ae0075 100644 --- a/test/functional/test_framework/util.py +++ b/test/functional/test_framework/util.py @@ -20,6 +20,7 @@ import unittest from . import coverage from .authproxy import AuthServiceProxy, JSONRPCException from io import BytesIO +from typing import Callable, Optional logger = logging.getLogger("TestFramework.utils") @@ -80,7 +81,7 @@ def assert_raises_message(exc, message, fun, *args, **kwds): raise AssertionError("No exception raised") -def assert_raises_process_error(returncode, output, fun, *args, **kwds): +def assert_raises_process_error(returncode: int, output: str, fun: Callable, *args, **kwds): """Execute a process and asserts the process return code and output. Calls function `fun` with arguments `args` and `kwds`. Catches a CalledProcessError @@ -88,9 +89,9 @@ def assert_raises_process_error(returncode, output, fun, *args, **kwds): no CalledProcessError was raised or if the return code and output are not as expected. Args: - returncode (int): the process return code. - output (string): [a substring of] the process output. - fun (function): the function to call. This should execute a process. + returncode: the process return code. + output: [a substring of] the process output. + fun: the function to call. This should execute a process. args*: positional arguments for the function. kwds**: named arguments for the function. """ @@ -105,7 +106,7 @@ def assert_raises_process_error(returncode, output, fun, *args, **kwds): raise AssertionError("No exception raised") -def assert_raises_rpc_error(code, message, fun, *args, **kwds): +def assert_raises_rpc_error(code: Optional[int], message: Optional[str], fun: Callable, *args, **kwds): """Run an RPC and verify that a specific JSONRPC exception code and message is raised. Calls function `fun` with arguments `args` and `kwds`. Catches a JSONRPCException @@ -113,11 +114,11 @@ def assert_raises_rpc_error(code, message, fun, *args, **kwds): no JSONRPCException was raised or if the error code/message are not as expected. Args: - code (int), optional: the error code returned by the RPC call (defined - in src/rpc/protocol.h). Set to None if checking the error code is not required. - message (string), optional: [a substring of] the error string returned by the - RPC call. Set to None if checking the error string is not required. - fun (function): the function to call. This should be the name of an RPC. + code: the error code returned by the RPC call (defined in src/rpc/protocol.h). + Set to None if checking the error code is not required. + message: [a substring of] the error string returned by the RPC call. + Set to None if checking the error string is not required. + fun: the function to call. This should be the name of an RPC. args*: positional arguments for the function. kwds**: named arguments for the function. """ @@ -480,6 +481,28 @@ def create_confirmed_utxos(fee, node, count): return utxos +def chain_transaction(node, parent_txids, vouts, value, fee, num_outputs): + """Build and send a transaction that spends the given inputs (specified + by lists of parent_txid:vout each), with the desired total value and fee, + equally divided up to the desired number of outputs. + + Returns a tuple with the txid and the amount sent per output. + """ + send_value = satoshi_round((value - fee)/num_outputs) + inputs = [] + for (txid, vout) in zip(parent_txids, vouts): + inputs.append({'txid' : txid, 'vout' : vout}) + outputs = {} + for _ in range(num_outputs): + outputs[node.getnewaddress()] = send_value + rawtx = node.createrawtransaction(inputs, outputs, 0, True) + signedtx = node.signrawtransactionwithwallet(rawtx) + txid = node.sendrawtransaction(signedtx['hex']) + fulltx = node.getrawtransaction(txid, 1) + assert len(fulltx['vout']) == num_outputs # make sure we didn't generate a change output + return (txid, send_value) + + # Create large OP_RETURN txouts that can be appended to a transaction # to make it large (helper for constructing large transactions). def gen_return_txouts(): diff --git a/test/functional/test_framework/wallet.py b/test/functional/test_framework/wallet.py index bfb5916c37..ad58a62e28 100644 --- a/test/functional/test_framework/wallet.py +++ b/test/functional/test_framework/wallet.py @@ -6,6 +6,7 @@ from decimal import Decimal from enum import Enum +from typing import Optional from test_framework.address import ADDRESS_BCRT1_P2WSH_OP_TRUE from test_framework.key import ECKey from test_framework.messages import ( @@ -105,12 +106,12 @@ class MiniWallet: def get_address(self): return self._address - def get_utxo(self, *, txid='', mark_as_spent=True): + def get_utxo(self, *, txid: Optional[str]='', mark_as_spent=True): """ Returns a utxo and marks it as spent (pops it from the internal list) Args: - txid (string), optional: get the first utxo we find from a specific transaction + txid: get the first utxo we find from a specific transaction Note: Can be used to get the change output immediately after a send_self_transfer """ @@ -123,13 +124,13 @@ class MiniWallet: else: return self._utxos[index] - def send_self_transfer(self, *, fee_rate=Decimal("0.003"), from_node, utxo_to_spend=None, locktime=0): + def send_self_transfer(self, **kwargs): """Create and send a tx with the specified fee_rate. Fee may be exact or at most one satoshi higher than needed.""" - tx = self.create_self_transfer(fee_rate=fee_rate, from_node=from_node, utxo_to_spend=utxo_to_spend) - self.sendrawtransaction(from_node=from_node, tx_hex=tx['hex']) + tx = self.create_self_transfer(**kwargs) + self.sendrawtransaction(from_node=kwargs['from_node'], tx_hex=tx['hex']) return tx - def create_self_transfer(self, *, fee_rate=Decimal("0.003"), from_node, utxo_to_spend=None, mempool_valid=True, locktime=0): + def create_self_transfer(self, *, fee_rate=Decimal("0.003"), from_node, utxo_to_spend=None, mempool_valid=True, locktime=0, sequence=0): """Create and return a tx with the specified fee_rate. Fee may be exact or at most one satoshi higher than needed.""" self._utxos = sorted(self._utxos, key=lambda k: k['value']) utxo_to_spend = utxo_to_spend or self._utxos.pop() # Pick the largest utxo (if none provided) and hope it covers the fee @@ -139,7 +140,7 @@ class MiniWallet: assert send_value > 0 tx = CTransaction() - tx.vin = [CTxIn(COutPoint(int(utxo_to_spend['txid'], 16), utxo_to_spend['vout']))] + tx.vin = [CTxIn(COutPoint(int(utxo_to_spend['txid'], 16), utxo_to_spend['vout']), nSequence=sequence)] tx.vout = [CTxOut(int(send_value * COIN), self._scriptPubKey)] tx.nLockTime = locktime if not self._address: diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 49f269f8b4..ad1acd2e2f 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -19,6 +19,7 @@ import datetime import os import time import shutil +import signal import subprocess import sys import tempfile @@ -173,6 +174,7 @@ BASE_SCRIPTS = [ 'wallet_groups.py --legacy-wallet', 'p2p_addrv2_relay.py', 'wallet_groups.py --descriptors', + 'p2p_compactblocks_hb.py', 'p2p_disconnect_ban.py', 'rpc_decodescript.py', 'rpc_blockchain.py', @@ -261,6 +263,7 @@ BASE_SCRIPTS = [ 'wallet_send.py --legacy-wallet', 'wallet_send.py --descriptors', 'wallet_create_tx.py --descriptors', + 'wallet_taproot.py', 'p2p_fingerprint.py', 'feature_uacomment.py', 'wallet_coinbase_category.py --legacy-wallet', @@ -283,6 +286,7 @@ BASE_SCRIPTS = [ 'feature_logging.py', 'feature_anchors.py', 'feature_coinstatsindex.py', + 'wallet_orphanedreward.py', 'p2p_node_network_limited.py', 'p2p_permissions.py', 'feature_blocksdir.py', @@ -546,9 +550,11 @@ def run_tests(*, test_list, src_dir, build_dir, tmpdir, jobs=1, enable_coverage= all_passed = all(map(lambda test_result: test_result.was_successful, test_results)) and coverage_passed - # This will be a no-op unless failfast is True in which case there may be dangling - # processes which need to be killed. - job_queue.kill_and_join() + # Clean up dangling processes if any. This may only happen with --failfast option. + # Killing the process group will also terminate the current process but that is + # not an issue + if len(job_queue.jobs): + os.killpg(os.getpgid(0), signal.SIGKILL) sys.exit(not all_passed) @@ -645,16 +651,6 @@ class TestHandler: print('.', end='', flush=True) dot_count += 1 - def kill_and_join(self): - """Send SIGKILL to all jobs and block until all have ended.""" - procs = [i[2] for i in self.jobs] - - for proc in procs: - proc.kill() - - for proc in procs: - proc.wait() - class TestResult(): def __init__(self, name, status, time): diff --git a/test/functional/wallet_orphanedreward.py b/test/functional/wallet_orphanedreward.py new file mode 100755 index 0000000000..097df2cf41 --- /dev/null +++ b/test/functional/wallet_orphanedreward.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +# Copyright (c) 2020-2021 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test orphaned block rewards in the wallet.""" + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal + +class OrphanedBlockRewardTest(BitcoinTestFramework): + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 2 + + def skip_test_if_missing_module(self): + self.skip_if_no_wallet() + + def run_test(self): + # Generate some blocks and obtain some coins on node 0. We send + # some balance to node 1, which will hold it as a single coin. + self.nodes[0].generate(150) + self.nodes[0].sendtoaddress(self.nodes[1].getnewaddress(), 10) + self.nodes[0].generate(1) + + # Get a block reward with node 1 and remember the block so we can orphan + # it later. + self.sync_blocks() + blk = self.nodes[1].generate(1)[0] + self.sync_blocks() + + # Let the block reward mature and send coins including both + # the existing balance and the block reward. + self.nodes[0].generate(150) + self.sync_blocks() + assert_equal(self.nodes[1].getbalance(), 10 + 25) + txid = self.nodes[1].sendtoaddress(self.nodes[0].getnewaddress(), 30) + + # Orphan the block reward and make sure that the original coins + # from the wallet can still be spent. + self.nodes[0].invalidateblock(blk) + self.nodes[0].generate(152) + self.sync_blocks() + # Without the following abandontransaction call, the coins are + # not considered available yet. + assert_equal(self.nodes[1].getbalances()["mine"], { + "trusted": 0, + "untrusted_pending": 0, + "immature": 0, + }) + # The following abandontransaction is necessary to make the later + # lines succeed, and probably should not be needed; see + # https://github.com/bitcoin/bitcoin/issues/14148. + self.nodes[1].abandontransaction(txid) + assert_equal(self.nodes[1].getbalances()["mine"], { + "trusted": 10, + "untrusted_pending": 0, + "immature": 0, + }) + self.nodes[1].sendtoaddress(self.nodes[0].getnewaddress(), 9) + +if __name__ == '__main__': + OrphanedBlockRewardTest().main() diff --git a/test/functional/wallet_taproot.py b/test/functional/wallet_taproot.py new file mode 100755 index 0000000000..1547a90125 --- /dev/null +++ b/test/functional/wallet_taproot.py @@ -0,0 +1,400 @@ +#!/usr/bin/env python3 +# Copyright (c) 2021 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test generation and spending of P2TR addresses.""" + +import random + +from decimal import Decimal +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal +from test_framework.descriptors import descsum_create +from test_framework.script import (CScript, OP_CHECKSIG, taproot_construct) +from test_framework.segwit_addr import encode_segwit_address + +# xprvs/xpubs, and m/* derived x-only pubkeys (created using independent implementation) +KEYS = [ + { + "xprv": "tprv8ZgxMBicQKsPeNLUGrbv3b7qhUk1LQJZAGMuk9gVuKh9sd4BWGp1eMsehUni6qGb8bjkdwBxCbgNGdh2bYGACK5C5dRTaif9KBKGVnSezxV", + "xpub": "tpubD6NzVbkrYhZ4XqNGAWGWSzmxGWFwVjVTjZxh2fioKbVYi7Jx8fdbprVWsdW7mHwqjchBVas8TLZG4Xwuz4RKU4iaCqiCvoSkFCzQptqk5Y1", + "pubs": [ + "83d8ee77a0f3a32a5cea96fd1624d623b836c1e5d1ac2dcde46814b619320c18", + "a30253b018ea6fca966135bf7dd8026915427f24ccf10d4e03f7870f4128569b", + "a61e5749f2f3db9dc871d7b187e30bfd3297eea2557e9be99897ea8ff7a29a21", + "8110cf482f66dc37125e619d73075af932521724ffc7108309e88f361efe8c8a", + ] + }, + { + "xprv": "tprv8ZgxMBicQKsPe98QUPieXy5KFPVjuZNpcC9JY7K7buJEm8nWvJogK4kTda7eLjK9U4PnMNbSjEkpjDJazeBZ4rhYNYD7N6GEdaysj1AYSb5", + "xpub": "tpubD6NzVbkrYhZ4XcACN3PEwNjRpR1g4tZjBVk5pdMR2B6dbd3HYhdGVZNKofAiFZd9okBserZvv58A6tBX4pE64UpXGNTSesfUW7PpW36HuKz", + "pubs": [ + "f95886b02a84928c5c15bdca32784993105f73de27fa6ad8c1a60389b999267c", + "71522134160685eb779857033bfc84c7626f13556154653a51dd42619064e679", + "48957b4158b2c5c3f4c000f51fd2cf0fd5ff8868ebfb194256f5e9131fc74bd8", + "086dda8139b3a84944010648d2b674b70447be3ae59322c09a4907bc80be62c1", + ] + }, + { + "xprv": "tprv8ZgxMBicQKsPe3ZJmcj9aJ2EPZJYYCh6Lp3v82p75wspgaXmtDZ2RBtkAtWcGnW2VQDzMHQPBkCKMoYTqh1RfJKjv4PcmWVR7KqTpjsdboN", + "xpub": "tpubD6NzVbkrYhZ4XWb6fGPjyhgLxapUhXszv7ehQYrQWDgDX4nYWcNcbgWcM2RhYo9s2mbZcfZJ8t5LzYcr24FK79zVybsw5Qj3Rtqug8jpJMy", + "pubs": [ + "9fa5ffb68821cf559001caa0577eeea4978b29416def328a707b15e91701a2f7", + "8a104c54cd34acba60c97dd8f1f7abc89ba9587afd88dc928e91aca7b1c50d20", + "13ba6b252a4eb5ef31d39cb521724cdab19a698323f5c17093f28fb1821d052f", + "f6c2b4863fd5ba1ba09e3a890caed8b75ffbe013ebab31a06ab87cd6f72506af", + ] + }, + { + "xprv": "tprv8ZgxMBicQKsPdKziibn63Rm6aNzp7dSjDnufZMStXr71Huz7iihCRpbZZZ6Voy5HyuHCWx6foHMipzMzUq4tZrtkZ24DJwz5EeNWdsuwX5h", + "xpub": "tpubD6NzVbkrYhZ4Wo2WcFSgSqRD9QWkGxddo6WSqsVBx7uQ8QEtM7WncKDRjhFEexK119NigyCsFygA4b7sAPQxqebyFGAZ9XVV1BtcgNzbCRR", + "pubs": [ + "03a669ea926f381582ec4a000b9472ba8a17347f5fb159eddd4a07036a6718eb", + "bbf56b14b119bccafb686adec2e3d2a6b51b1626213590c3afa815d1fd36f85d", + "2994519e31bbc238a07d82f85c9832b831705d2ee4a2dbb477ecec8a3f570fe5", + "68991b5c139a4c479f8c89d6254d288c533aefc0c5b91fac6c89019c4de64988", + ] + }, + { + "xprv": "tprv8ZgxMBicQKsPen4PGtDwURYnCtVMDejyE8vVwMGhQWfVqB2FBPdekhTacDW4vmsKTsgC1wsncVqXiZdX2YFGAnKoLXYf42M78fQJFzuDYFN", + "xpub": "tpubD6NzVbkrYhZ4YF6BAXtXsqCtmv1HNyvsoSXHDsJzpnTtffH1onTEwC5SnLzCHPKPebh2i7Gxvi9kJNADcpuSmH8oM3rCYcHVtdXHjpYoKnX", + "pubs": [ + "aba457d16a8d59151c387f24d1eb887efbe24644c1ee64b261282e7baebdb247", + "c8558b7caf198e892032d91f1a48ee9bdc25462b83b4d0ac62bb7fb2a0df630e", + "8a4bcaba0e970685858d133a4d0079c8b55bbc755599e212285691eb779ce3dc", + "b0d68ada13e0d954b3921b88160d4453e9c151131c2b7c724e08f538a666ceb3", + ] + }, + { + "xprv": "tprv8ZgxMBicQKsPd91vCgRmbzA13wyip2RimYeVEkAyZvsEN5pUSB3T43SEBxPsytkxb42d64W2EiRE9CewpJQkzR8HKHLV8Uhk4dMF5yRPaTv", + "xpub": "tpubD6NzVbkrYhZ4Wc3i6L6N1Pp7cyVeyMcdLrFGXGDGzCfdCa5F4Zs3EY46N72Ws8QDEUYBVwXfDfda2UKSseSdU1fsBegJBhGCZyxkf28bkQ6", + "pubs": [ + "9b4d495b74887815a1ff623c055c6eac6b6b2e07d2a016d6526ebac71dd99744", + "8e971b781b7ce7ab742d80278f2dfe7dd330f3efd6d00047f4a2071f2e7553cb", + "b811d66739b9f07435ccda907ec5cd225355321c35e0a7c7791232f24cf10632", + "4cd27a5552c272bc80ba544e9cc6340bb906969f5e7a1510b6cef9592683fbc9", + ] + }, + { + "xprv": "tprv8ZgxMBicQKsPdEhLRxxwzTv2t18j7ruoffPeqAwVA2qXJ2P66RaMZLUWQ85SjoA7xPxdSgCB9UZ72m65qbnaLPtFTfHVP3MEmkpZk1Bv8RT", + "xpub": "tpubD6NzVbkrYhZ4Whj8KcdYPsa9T2efHC6iExzS7gynaJdv8WdripPwjq6NaH5gQJGrLmvUwHY1smhiakUosXNDTEa6qfKUQdLKV6DJBre6XvQ", + "pubs": [ + "d0c19def28bb1b39451c1a814737615983967780d223b79969ba692182c6006b", + "cb1d1b1dc62fec1894d4c3d9a1b6738e5ff9c273a64f74e9ab363095f45e9c47", + "245be588f41acfaeb9481aa132717db56ee1e23eb289729fe2b8bde8f9a00830", + "5bc4ad6d6187fa82728c85a073b428483295288f8aef5722e47305b5872f7169", + ] + }, + { + "xprv": "tprv8ZgxMBicQKsPcxbqxzcMAwQpiCD8x6qaZEJTxdKxw4w9GuMzDACTD9yhEsHGfqQcfYX4LivosLDDngTykYEp9JnTdcqY7cHqU8PpeFFKyV3", + "xpub": "tpubD6NzVbkrYhZ4WRddreGwaM4wHDj57S2V8XuFF9NGMLjY7PckqZ23PebZR1wGA4w84uX2vZphdZVsnREjij1ibYjEBTaTVQCEZCLs4xUDapx", + "pubs": [ + "065cc1b92bd99e5a3e626e8296a366b2d132688eb43aea19bc14fd8f43bf07fb", + "5b95633a7dda34578b6985e6bfd85d83ec38b7ded892a9b74a3d899c85890562", + "dc86d434b9a34495c8e845b969d51f80d19a8df03b400353ffe8036a0c22eb60", + "06c8ffde238745b29ae8a97ae533e1f3edf214bba6ec58b5e7b9451d1d61ec19", + ] + }, + { + "xprv": "tprv8ZgxMBicQKsPe6zLoU8MTTXgsdJVNBErrYGpoGwHf5VGvwUzdNc7NHeCSzkJkniCxBhZWujXjmD4HZmBBrnr3URgJjM6GxRgMmEhLdqNTWG", + "xpub": "tpubD6NzVbkrYhZ4Xa28h7nwrsBoSepRXWRmRqsc5nyb5MHfmRjmFmRhYnG4d9dC7uxixN5AfsEv1Lz3mCAuWvERyvPgKozHUVjfo8EG6foJGy7", + "pubs": [ + "d826a0a53abb6ffc60df25b9c152870578faef4b2eb5a09bdd672bbe32cdd79b", + "939365e0359ff6bc6f6404ee220714c5d4a0d1e36838b9e2081ede217674e2ba", + "4e8767edcf7d3d90258cfbbea01b784f4d2de813c4277b51279cf808bac410a2", + "d42a2c280940bfc6ede971ae72cde2e1df96c6da7dab06a132900c6751ade208", + ] + }, + { + "xprv": "tprv8ZgxMBicQKsPeB5o5oCsN2dVxM2mtJiYERQEBRc4JNwC1DFGYaEdNkmh8jJYVPU76YhkFoRoWTdh1p3yQGykG8TfDW34dKgrgSx28gswUyL", + "xpub": "tpubD6NzVbkrYhZ4Xe7aySsTmSHcXNYi3duSoj11TweMiejaqhW3Ay4DZFPZJses4sfpk4b9VHRhn8v4cKTMjugMM3hqXcqSSmRdiW8QvASXjfY", + "pubs": [ + "e360564b2e0e8d06681b6336a29d0750210e8f34afd9afb5e6fd5fe6dba26c81", + "76b4900f00a1dcce463b6d8e02b768518fce4f9ecd6679a13ad78ea1e4815ad3", + "5575556e263c8ed52e99ab02147cc05a738869afe0039911b5a60a780f4e43d2", + "593b00e2c8d4bd6dda0fd9e238888acf427bb4e128887fd5a40e0e9da78cbc01", + ] + }, + { + "xprv": "tprv8ZgxMBicQKsPfEH6jHemkGDjZRnAaKFJVGH8pQU638E6SdbX9hxit1tK2sfFPfL6KS7v8FfUKxstbfEpzSymbdfBM9Y5UkrxErF9fJaKLK3", + "xpub": "tpubD6NzVbkrYhZ4YhJtcwKN9fsr8TJ6jeSD4Zsv6vWPTQ2VH7rHn6nK4WWBCzKK7FkdVVwm3iztCU1UmStY4hX6gRbBmp9UzK9C59dQEzeXS12", + "pubs": [ + "7631cacec3343052d87ef4d0065f61dde82d7d2db0c1cc02ef61ef3c982ea763", + "c05e44a9e735d1b1bef62e2c0d886e6fb4923b2649b67828290f5cacc51c71b7", + "b33198b20701afe933226c92fd0e3d51d3f266f1113d864dbd026ae3166ef7f2", + "f99643ac3f4072ee4a949301e86963a9ca0ad57f2ef29f6b84fda037d7cac85b", + ] + }, + { + "xprv": "tprv8ZgxMBicQKsPdNWU38dT6aGxtqJR4oYS5kPpLVBcuKiiu7gqTYqMMqhUG6DP7pPahzPQu36sWSmeLCP1C4AwqcR5FX2RyRoZfd4B8pAnSdX", + "xpub": "tpubD6NzVbkrYhZ4WqYFvnJ3Vyw5TrpME8jLf3zbd1DvKbX7jbwc5wewYLKLSFRzZWV6hZj7XhsXAy7fhE5jB25DiWyNM3ztXbsXHRVCrp5BiPY", + "pubs": [ + "2258b1c3160be0864a541854eec9164a572f094f7562628281a8073bb89173a7", + "83df59d0a5c951cdd62b7ab225a62079f48d2a333a86e66c35420d101446e92e", + "2a654bf234d819055312f9ca03fad5836f9163b09cdd24d29678f694842b874a", + "aa0334ab910047387c912a21ec0dab806a47ffa38365060dbc5d47c18c6e66e7", + ] + }, + { + "xprv": "tprv8mGPkMVz5mZuJDnC2NjjAv7E9Zqa5LCgX4zawbZu5nzTtLb5kGhPwycX4H1gtW1f5ZdTKTNtQJ61hk71F2TdcQ93EFDTpUcPBr98QRji615", + "xpub": "tpubDHxRtmYEE9FaBgoyv2QKaKmLibMWEfPb6NbNE7cCW4nripqrNfWz8UEPEPbHCrakwLvwFfsqoaf4pjX4gWStp4nECRf1QwBKPkLqnY8pHbj", + "pubs": [ + "00a9da96087a72258f83b338ef7f0ea8cbbe05da5f18f091eb397d1ecbf7c3d3", + "b2749b74d51a78f5fe3ebb3a7c0ff266a468cade143dfa265c57e325177edf00", + "6b8747a6bbe4440d7386658476da51f6e49a220508a7ec77fe7bccc3e7baa916", + "4674bf4d9ebbe01bf0aceaca2472f63198655ecf2df810f8d69b38421972318e", + ] + } +] + +CHANGE_XPRV = "tprv8ZgxMBicQKsPcyDrWwiecVnTtFmfRwbfFqEfR4ZGWvq5aTTwLBWmAm5zrbMcYtb9gQNFfhRfqhhrBG37U3nhmXxEgeEPBJGHAPrHCrAd1WX" +CHANGE_XPUB = "tpubD6NzVbkrYhZ4WSFeQbPF1uSaTHHbbGnZq8qShabZwCdUQwihxaLMMFhs2kidGF2qrRKiQVqw8VoyuTHj1bZqmMXMeciaU1gBjWA1sim2zUB" + +# Point with no known discrete log. +H_POINT = "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0" + + +def key(hex_key): + """Construct an x-only pubkey from its hex representation.""" + return bytes.fromhex(hex_key) + +def pk(hex_key): + """Construct a script expression for taproot_construct for pk(hex_key).""" + return (None, CScript([bytes.fromhex(hex_key), OP_CHECKSIG])) + +def compute_taproot_address(pubkey, scripts): + """Compute the address for a taproot output with given inner key and scripts.""" + tap = taproot_construct(pubkey, scripts) + assert tap.scriptPubKey[0] == 0x51 + assert tap.scriptPubKey[1] == 0x20 + return encode_segwit_address("bcrt", 1, tap.scriptPubKey[2:]) + +class WalletTaprootTest(BitcoinTestFramework): + """Test generation and spending of P2TR address outputs.""" + + def set_test_params(self): + self.num_nodes = 3 + self.setup_clean_chain = True + self.extra_args = [['-keypool=100'], ['-keypool=100'], ["-vbparams=taproot:1:1"]] + self.supports_cli = False + + def skip_test_if_missing_module(self): + self.skip_if_no_wallet() + self.skip_if_no_sqlite() + + def setup_network(self): + self.setup_nodes() + + def init_wallet(self, i): + pass + + @staticmethod + def rand_keys(n): + ret = [] + idxes = set() + for _ in range(n): + while True: + i = random.randrange(len(KEYS)) + if not i in idxes: + break + idxes.add(i) + ret.append(KEYS[i]) + return ret + + @staticmethod + def make_desc(pattern, privmap, keys, pub_only = False): + pat = pattern.replace("$H", H_POINT) + for i in range(len(privmap)): + if privmap[i] and not pub_only: + pat = pat.replace("$%i" % (i + 1), keys[i]['xprv']) + else: + pat = pat.replace("$%i" % (i + 1), keys[i]['xpub']) + return descsum_create(pat) + + @staticmethod + def make_addr(treefn, keys, i): + args = [] + for j in range(len(keys)): + args.append(keys[j]['pubs'][i]) + return compute_taproot_address(*treefn(*args)) + + def do_test_addr(self, comment, pattern, privmap, treefn, keys): + self.log.info("Testing %s address derivation" % comment) + desc = self.make_desc(pattern, privmap, keys, False) + desc_pub = self.make_desc(pattern, privmap, keys, True) + assert_equal(self.nodes[0].getdescriptorinfo(desc)['descriptor'], desc_pub) + result = self.addr_gen.importdescriptors([{"desc": desc_pub, "active": True, "timestamp": "now"}]) + assert(result[0]['success']) + for i in range(4): + addr_g = self.addr_gen.getnewaddress(address_type='bech32') + if treefn is not None: + addr_r = self.make_addr(treefn, keys, i) + assert_equal(addr_g, addr_r) + + # tr descriptors cannot be imported when Taproot is not active + result = self.privs_tr_enabled.importdescriptors([{"desc": desc, "timestamp": "now"}]) + assert(result[0]["success"]) + result = self.pubs_tr_enabled.importdescriptors([{"desc": desc_pub, "timestamp": "now"}]) + assert(result[0]["success"]) + if desc.startswith("tr"): + result = self.privs_tr_disabled.importdescriptors([{"desc": desc, "timestamp": "now"}]) + assert(not result[0]["success"]) + assert_equal(result[0]["error"]["code"], -4) + assert_equal(result[0]["error"]["message"], "Cannot import tr() descriptor when Taproot is not active") + result = self.pubs_tr_disabled.importdescriptors([{"desc": desc_pub, "timestamp": "now"}]) + assert(not result[0]["success"]) + assert_equal(result[0]["error"]["code"], -4) + assert_equal(result[0]["error"]["message"], "Cannot import tr() descriptor when Taproot is not active") + + def do_test_sendtoaddress(self, comment, pattern, privmap, treefn, keys_pay, keys_change): + self.log.info("Testing %s through sendtoaddress" % comment) + desc_pay = self.make_desc(pattern, privmap, keys_pay) + desc_change = self.make_desc(pattern, privmap, keys_change) + desc_pay_pub = self.make_desc(pattern, privmap, keys_pay, True) + desc_change_pub = self.make_desc(pattern, privmap, keys_change, True) + assert_equal(self.nodes[0].getdescriptorinfo(desc_pay)['descriptor'], desc_pay_pub) + assert_equal(self.nodes[0].getdescriptorinfo(desc_change)['descriptor'], desc_change_pub) + result = self.rpc_online.importdescriptors([{"desc": desc_pay, "active": True, "timestamp": "now"}]) + assert(result[0]['success']) + result = self.rpc_online.importdescriptors([{"desc": desc_change, "active": True, "timestamp": "now", "internal": True}]) + assert(result[0]['success']) + for i in range(4): + addr_g = self.rpc_online.getnewaddress(address_type='bech32') + if treefn is not None: + addr_r = self.make_addr(treefn, keys_pay, i) + assert_equal(addr_g, addr_r) + boring_balance = int(self.boring.getbalance() * 100000000) + to_amnt = random.randrange(1000000, boring_balance) + self.boring.sendtoaddress(address=addr_g, amount=Decimal(to_amnt) / 100000000, subtractfeefromamount=True) + self.nodes[0].generatetoaddress(1, self.boring.getnewaddress()) + test_balance = int(self.rpc_online.getbalance() * 100000000) + ret_amnt = random.randrange(100000, test_balance) + res = self.rpc_online.sendtoaddress(address=self.boring.getnewaddress(), amount=Decimal(ret_amnt) / 100000000, subtractfeefromamount=True) + self.nodes[0].generatetoaddress(1, self.boring.getnewaddress()) + assert(self.rpc_online.gettransaction(res)["confirmations"] > 0) + + def do_test_psbt(self, comment, pattern, privmap, treefn, keys_pay, keys_change): + self.log.info("Testing %s through PSBT" % comment) + desc_pay = self.make_desc(pattern, privmap, keys_pay, False) + desc_change = self.make_desc(pattern, privmap, keys_change, False) + desc_pay_pub = self.make_desc(pattern, privmap, keys_pay, True) + desc_change_pub = self.make_desc(pattern, privmap, keys_change, True) + assert_equal(self.nodes[0].getdescriptorinfo(desc_pay)['descriptor'], desc_pay_pub) + assert_equal(self.nodes[0].getdescriptorinfo(desc_change)['descriptor'], desc_change_pub) + result = self.psbt_online.importdescriptors([{"desc": desc_pay_pub, "active": True, "timestamp": "now"}]) + assert(result[0]['success']) + result = self.psbt_online.importdescriptors([{"desc": desc_change_pub, "active": True, "timestamp": "now", "internal": True}]) + assert(result[0]['success']) + result = self.psbt_offline.importdescriptors([{"desc": desc_pay, "active": True, "timestamp": "now"}]) + assert(result[0]['success']) + result = self.psbt_offline.importdescriptors([{"desc": desc_change, "active": True, "timestamp": "now", "internal": True}]) + assert(result[0]['success']) + for i in range(4): + addr_g = self.psbt_online.getnewaddress(address_type='bech32') + if treefn is not None: + addr_r = self.make_addr(treefn, keys_pay, i) + assert_equal(addr_g, addr_r) + boring_balance = int(self.boring.getbalance() * 100000000) + to_amnt = random.randrange(1000000, boring_balance) + self.boring.sendtoaddress(address=addr_g, amount=Decimal(to_amnt) / 100000000, subtractfeefromamount=True) + self.nodes[0].generatetoaddress(1, self.boring.getnewaddress()) + test_balance = int(self.psbt_online.getbalance() * 100000000) + ret_amnt = random.randrange(100000, test_balance) + psbt = self.psbt_online.walletcreatefundedpsbt([], [{self.boring.getnewaddress(): Decimal(ret_amnt) / 100000000}], None, {"subtractFeeFromOutputs":[0]})['psbt'] + res = self.psbt_offline.walletprocesspsbt(psbt) + assert(res['complete']) + rawtx = self.nodes[0].finalizepsbt(res['psbt'])['hex'] + txid = self.nodes[0].sendrawtransaction(rawtx) + self.nodes[0].generatetoaddress(1, self.boring.getnewaddress()) + assert(self.psbt_online.gettransaction(txid)['confirmations'] > 0) + + def do_test(self, comment, pattern, privmap, treefn, nkeys): + keys = self.rand_keys(nkeys * 4) + self.do_test_addr(comment, pattern, privmap, treefn, keys[0:nkeys]) + self.do_test_sendtoaddress(comment, pattern, privmap, treefn, keys[0:nkeys], keys[nkeys:2*nkeys]) + self.do_test_psbt(comment, pattern, privmap, treefn, keys[2*nkeys:3*nkeys], keys[3*nkeys:4*nkeys]) + + def run_test(self): + self.log.info("Creating wallets...") + self.nodes[0].createwallet(wallet_name="privs_tr_enabled", descriptors=True, blank=True) + self.privs_tr_enabled = self.nodes[0].get_wallet_rpc("privs_tr_enabled") + self.nodes[2].createwallet(wallet_name="privs_tr_disabled", descriptors=True, blank=True) + self.privs_tr_disabled=self.nodes[2].get_wallet_rpc("privs_tr_disabled") + self.nodes[0].createwallet(wallet_name="pubs_tr_enabled", descriptors=True, blank=True, disable_private_keys=True) + self.pubs_tr_enabled = self.nodes[0].get_wallet_rpc("pubs_tr_enabled") + self.nodes[2].createwallet(wallet_name="pubs_tr_disabled", descriptors=True, blank=True, disable_private_keys=True) + self.pubs_tr_disabled=self.nodes[2].get_wallet_rpc("pubs_tr_disabled") + self.nodes[0].createwallet(wallet_name="boring") + self.nodes[0].createwallet(wallet_name="addr_gen", descriptors=True, disable_private_keys=True, blank=True) + self.nodes[0].createwallet(wallet_name="rpc_online", descriptors=True, blank=True) + self.nodes[0].createwallet(wallet_name="psbt_online", descriptors=True, disable_private_keys=True, blank=True) + self.nodes[1].createwallet(wallet_name="psbt_offline", descriptors=True, blank=True) + self.boring = self.nodes[0].get_wallet_rpc("boring") + self.addr_gen = self.nodes[0].get_wallet_rpc("addr_gen") + self.rpc_online = self.nodes[0].get_wallet_rpc("rpc_online") + self.psbt_online = self.nodes[0].get_wallet_rpc("psbt_online") + self.psbt_offline = self.nodes[1].get_wallet_rpc("psbt_offline") + + self.log.info("Mining blocks...") + gen_addr = self.boring.getnewaddress() + self.nodes[0].generatetoaddress(101, gen_addr) + + self.do_test( + "tr(XPRV)", + "tr($1/*)", + [True], + lambda k1: (key(k1), []), + 1 + ) + self.do_test( + "tr(H,XPRV)", + "tr($H,pk($1/*))", + [True], + lambda k1: (key(H_POINT), [pk(k1)]), + 1 + ) + self.do_test( + "wpkh(XPRV)", + "wpkh($1/*)", + [True], + None, + 1 + ) + self.do_test( + "tr(XPRV,{H,{H,XPUB}})", + "tr($1/*,{pk($H),{pk($H),pk($2/*)}})", + [True, False], + lambda k1, k2: (key(k1), [pk(H_POINT), [pk(H_POINT), pk(k2)]]), + 2 + ) + self.do_test( + "wsh(multi(1,XPRV,XPUB))", + "wsh(multi(1,$1/*,$2/*))", + [True, False], + None, + 2 + ) + self.do_test( + "tr(XPUB,{{H,{H,XPUB}},{H,{H,{H,XPRV}}}})", + "tr($1/*,{{pk($H),{pk($H),pk($2/*)}},{pk($H),{pk($H),{pk($H),pk($3/*)}}}})", + [False, False, True], + lambda k1, k2, k3: (key(k1), [[pk(H_POINT), [pk(H_POINT), pk(k2)]], [pk(H_POINT), [pk(H_POINT), [pk(H_POINT), pk(k3)]]]]), + 3 + ) + + self.log.info("Sending everything back...") + + txid = self.rpc_online.sendtoaddress(address=self.boring.getnewaddress(), amount=self.rpc_online.getbalance(), subtractfeefromamount=True) + self.nodes[0].generatetoaddress(1, self.boring.getnewaddress()) + assert(self.rpc_online.gettransaction(txid)["confirmations"] > 0) + + psbt = self.psbt_online.walletcreatefundedpsbt([], [{self.boring.getnewaddress(): self.psbt_online.getbalance()}], None, {"subtractFeeFromOutputs": [0]})['psbt'] + res = self.psbt_offline.walletprocesspsbt(psbt) + assert(res['complete']) + rawtx = self.nodes[0].finalizepsbt(res['psbt'])['hex'] + txid = self.nodes[0].sendrawtransaction(rawtx) + self.nodes[0].generatetoaddress(1, self.boring.getnewaddress()) + assert(self.psbt_online.gettransaction(txid)['confirmations'] > 0) + +if __name__ == '__main__': + WalletTaprootTest().main() |