diff --git a/test/functional/abc-get-invalid-block.py b/test/functional/abc-get-invalid-block.py index ffb3df72e..4a6396a98 100755 --- a/test/functional/abc-get-invalid-block.py +++ b/test/functional/abc-get-invalid-block.py @@ -1,76 +1,76 @@ #!/usr/bin/env python3 # Copyright (c) 2019 The Bitcoin developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test requesting invalid blocks behaves safely.""" from test_framework.messages import ( + MSG_BLOCK, + MSG_CMPCT_BLOCK, CInv, msg_getblocks, msg_getdata, msg_getheaders, - MSG_BLOCK, - MSG_CMPCT_BLOCK, ) -from test_framework.p2p import p2p_lock, P2PInterface +from test_framework.p2p import P2PInterface, p2p_lock from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal class GetInvalidBlockTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 def run_test(self): node = self.nodes[0] node.add_p2p_connection(P2PInterface()) chaintip = node.getbestblockhash() # Mine some blocks and invalidate them blocks = node.generatetoaddress( 3, node.get_deterministic_priv_key().address) assert_equal(blocks[-1], node.getbestblockhash()) node.invalidateblock(blocks[0]) assert_equal(chaintip, node.getbestblockhash()) # Clear any old messages with p2p_lock: node.p2p.last_message.pop("block", None) node.p2p.last_message.pop("cmpctblock", None) node.p2p.last_message.pop("headers", None) # Requests for the invalidated block and it's decendants should fail. # Not doing so is a potential DoS vector. for b in blocks: block_hash = int(b, 16) # Currently, the implementation for getblocks skips blocks which # are not on the currently active chain. This is the only logged # indication of such. with node.assert_debug_log(expected_msgs=["getblocks -1 to"]): msg = msg_getblocks() msg.locator.vHave = [block_hash] node.p2p.send_message(msg) node.p2p.sync_with_ping() with node.assert_debug_log(expected_msgs=["ignoring request from peer=0 for old block that isn't in the main chain"]): msg = msg_getdata() msg.inv.append(CInv(MSG_BLOCK, block_hash)) node.p2p.send_message(msg) node.p2p.sync_with_ping() with node.assert_debug_log(expected_msgs=["ignoring request from peer=0 for old block that isn't in the main chain"]): msg = msg_getdata() msg.inv.append(CInv(MSG_CMPCT_BLOCK, block_hash)) node.p2p.send_message(msg) node.p2p.sync_with_ping() with node.assert_debug_log(expected_msgs=["ignoring request from peer=0 for old block header that isn't in the main chain"]): msg = msg_getheaders() msg.hashstop = block_hash node.p2p.send_message(msg) node.p2p.sync_with_ping() if __name__ == '__main__': GetInvalidBlockTest().main() diff --git a/test/functional/abc-invalid-chains.py b/test/functional/abc-invalid-chains.py index cac0f6689..6aac3b52b 100755 --- a/test/functional/abc-invalid-chains.py +++ b/test/functional/abc-invalid-chains.py @@ -1,158 +1,155 @@ #!/usr/bin/env python3 # Copyright (c) 2019 The Bitcoin developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-ilncense.php. import time -from test_framework.test_framework import BitcoinTestFramework +from test_framework.blocktools import create_block, create_coinbase from test_framework.p2p import P2PDataStore +from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal -from test_framework.blocktools import ( - create_block, - create_coinbase, -) class InvalidChainsTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 self.setup_clean_chain = True self.tip = None self.blocks = {} self.block_heights = {} self.extra_args = [["-whitelist=noban@127.0.0.1"]] def next_block(self, number): if self.tip is None: base_block_hash = self.genesis_hash block_time = int(time.time()) + 1 else: base_block_hash = self.tip.sha256 block_time = self.tip.nTime + 1 height = self.block_heights[base_block_hash] + 1 coinbase = create_coinbase(height) coinbase.rehash() block = create_block(base_block_hash, coinbase, block_time) block.solve() self.tip = block self.block_heights[block.sha256] = height assert number not in self.blocks self.blocks[number] = block return block def run_test(self): node = self.nodes[0] node.add_p2p_connection(P2PDataStore()) self.genesis_hash = int(node.getbestblockhash(), 16) self.block_heights[self.genesis_hash] = 0 # move the tip back to a previous block def tip(number): self.tip = self.blocks[number] # shorthand for functions block = self.next_block # Reference for blocks mined in this test: # # 11 21 -- 221 - 222 # / / / # 0 - 1 - 2 - 22 - 23 - 24 - 25 # \ # -- 12 - 13 - 14 # \ # -- 15 - 16 - 17 - 18 # Generate some valid blocks node.p2p.send_blocks_and_test([block(0), block(1), block(2)], node) # Explicitly invalidate blocks 1 and 2 # See below for why we do this node.invalidateblock(self.blocks[1].hash) assert_equal(self.blocks[0].hash, node.getbestblockhash()) node.invalidateblock(self.blocks[2].hash) assert_equal(self.blocks[0].hash, node.getbestblockhash()) # Mining on top of blocks 1 or 2 is rejected tip(1) node.p2p.send_blocks_and_test( [block(11)], node, success=False, force_send=True, reject_reason='bad-prevblk') tip(2) node.p2p.send_blocks_and_test( [block(21)], node, success=False, force_send=True, reject_reason='bad-prevblk') # Reconsider block 2 to remove invalid status from *both* 1 and 2 # The goal is to test that block 1 is not retaining any internal state # that prevents us from accepting blocks building on top of block 1 node.reconsiderblock(self.blocks[2].hash) assert_equal(self.blocks[2].hash, node.getbestblockhash()) # Mining on the block 1 chain should be accepted # (needs to mine two blocks because less-work chains are not processed) tip(1) node.p2p.send_blocks_and_test([block(12), block(13)], node) # Mining on the block 2 chain should still be accepted # (needs to mine two blocks because less-work chains are not processed) tip(2) node.p2p.send_blocks_and_test([block(22), block(221)], node) # Mine more blocks from block 22 to be longest chain tip(22) node.p2p.send_blocks_and_test([block(23), block(24)], node) # Sanity checks assert_equal(self.blocks[24].hash, node.getbestblockhash()) assert any(self.blocks[221].hash == chaintip["hash"] for chaintip in node.getchaintips()) # Invalidating the block 2 chain should reject new blocks on that chain node.invalidateblock(self.blocks[2].hash) assert_equal(self.blocks[13].hash, node.getbestblockhash()) # Mining on the block 2 chain should be rejected tip(24) node.p2p.send_blocks_and_test( [block(25)], node, success=False, force_send=True, reject_reason='bad-prevblk') # Continued mining on the block 1 chain is still ok tip(13) node.p2p.send_blocks_and_test([block(14)], node) # Mining on a once-valid chain forking from block 2's longest chain, # which is now invalid, should also be rejected. tip(221) node.p2p.send_blocks_and_test( [block(222)], node, success=False, force_send=True, reject_reason='bad-prevblk') self.log.info( "Make sure that reconsidering a block behaves correctly when cousin chains (neither ancestors nor descendants) become available as a result") # Reorg out 14 with four blocks. tip(13) node.p2p.send_blocks_and_test( [block(15), block(16), block(17), block(18)], node) # Invalidate 17 (so 18 now has failed parent) node.invalidateblock(self.blocks[17].hash) assert_equal(self.blocks[16].hash, node.getbestblockhash()) # Invalidate 13 (so 14 and 15 and 16 now also have failed parent) node.invalidateblock(self.blocks[13].hash) assert_equal(self.blocks[12].hash, node.getbestblockhash()) # Reconsider 14, which should reconsider 13 and remove failed parent # from our cousins 15 and 16 as well. Even though we reconsidered # 14, we end up on a different chain because 15/16 have more work. # (But, this shouldn't undo our invalidation of 17) node.reconsiderblock(self.blocks[14].hash) assert_equal(self.blocks[16].hash, node.getbestblockhash()) if __name__ == '__main__': InvalidChainsTest().main() diff --git a/test/functional/abc-invalid-message.py b/test/functional/abc-invalid-message.py index cf187ca0c..3dba9109f 100755 --- a/test/functional/abc-invalid-message.py +++ b/test/functional/abc-invalid-message.py @@ -1,100 +1,97 @@ #!/usr/bin/env python3 # Copyright (c) 2019 The Bitcoin developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """ABC Invalid Message Test Test that invalid messages get rejected and/or ban the sender as expected for each case. """ import struct from test_framework.messages import NODE_NETWORK, msg_version -from test_framework.p2p import ( - msg_ping, - P2PInterface, -) +from test_framework.p2p import P2PInterface, msg_ping from test_framework.test_framework import BitcoinTestFramework def msg_bad_checksum(connection, original_message): message_data = bytearray(connection.build_message(original_message)) data = original_message.serialize() i = 0 i += len(connection.magic_bytes) i += 12 i += len(struct.pack(" 0: assert_equal(len(block.vtx), tx_count) # Do PoW, which is cheap on regnet block.solve() self.tip = block self.block_heights[block.sha256] = height assert number not in self.blocks self.blocks[number] = block return block def run_test(self): node = self.nodes[0] node.add_p2p_connection(P2PDataStore()) self.genesis_hash = int(node.getbestblockhash(), 16) self.block_heights[self.genesis_hash] = 0 spendable_outputs = [] # save the current tip so it can be spent by a later block def save_spendable_output(): spendable_outputs.append(self.tip) # get an output that we previously marked as spendable def get_spendable_output(): return PreviousSpendableOutput(spendable_outputs.pop(0).vtx[0], 0) # move the tip back to a previous block def tip(number): self.tip = self.blocks[number] # update block state def update_block(block_number): block = self.blocks[block_number] old_sha256 = block.sha256 block.hashMerkleRoot = block.calc_merkle_root() block.solve() # Update the internal state just like in next_block self.tip = block if block.sha256 != old_sha256: self.block_heights[block.sha256] = self.block_heights[old_sha256] del self.block_heights[old_sha256] self.blocks[block_number] = block return block # shorthand for functions block = self.next_block # Create a new block block(0) save_spendable_output() node.p2p.send_blocks_and_test([self.tip], node) # Now we need that block to mature so we can spend the coinbase. maturity_blocks = [] for i in range(99): block(5000 + i) maturity_blocks.append(self.tip) save_spendable_output() node.p2p.send_blocks_and_test(maturity_blocks, node) # collect spendable outputs now to avoid cluttering the code later on out = [] for i in range(100): out.append(get_spendable_output()) # Let's build some blocks and test them. for i in range(17): n = i + 1 node.p2p.send_blocks_and_test([block(n)], node) node.p2p.send_blocks_and_test([block(5556)], node) # Block with regular ordering are now rejected. node.p2p.send_blocks_and_test([block( 5557, out[17], tx_count=16)], node, success=False, reject_reason='tx-ordering') # Rewind bad block. tip(5556) # After we activate the Nov 15, 2018 HF, transaction order is enforced. def ordered_block(block_number, spend): b = block(block_number, spend=spend, tx_count=16) make_conform_to_ctor(b) update_block(block_number) return b # Now that the fork activated, we need to order transaction per txid. node.p2p.send_blocks_and_test([ordered_block(4445, out[17])], node) node.p2p.send_blocks_and_test([ordered_block(4446, out[18])], node) # Generate a block with a duplicated transaction. double_tx_block = ordered_block(4447, out[19]) assert_equal(len(double_tx_block.vtx), 16) double_tx_block.vtx = double_tx_block.vtx[:8] + \ [double_tx_block.vtx[8]] + double_tx_block.vtx[8:] update_block(4447) node.p2p.send_blocks_and_test( [self.tip], node, success=False, reject_reason='bad-txns-duplicate') # Rewind bad block. tip(4446) # Check over two blocks. proper_block = ordered_block(4448, out[20]) node.p2p.send_blocks_and_test([self.tip], node) replay_tx_block = ordered_block(4449, out[21]) assert_equal(len(replay_tx_block.vtx), 16) replay_tx_block.vtx.append(proper_block.vtx[5]) replay_tx_block.vtx = [replay_tx_block.vtx[0]] + \ sorted(replay_tx_block.vtx[1:], key=lambda tx: tx.get_id()) update_block(4449) node.p2p.send_blocks_and_test( [self.tip], node, success=False, reject_reason='bad-txns-BIP30') if __name__ == '__main__': TransactionOrderingTest().main() diff --git a/test/functional/abc_feature_minerfund.py b/test/functional/abc_feature_minerfund.py index 12357059a..747669679 100755 --- a/test/functional/abc_feature_minerfund.py +++ b/test/functional/abc_feature_minerfund.py @@ -1,96 +1,90 @@ #!/usr/bin/env python3 # Copyright (c) 2020 The Bitcoin developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. -from test_framework.blocktools import ( - create_block, - create_coinbase, -) +from decimal import Decimal + +from test_framework.blocktools import create_block, create_coinbase from test_framework.messages import ToHex from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import ( - assert_equal, - assert_greater_than_or_equal, -) - -from decimal import Decimal +from test_framework.util import assert_equal, assert_greater_than_or_equal AXION_ACTIVATION_TIME = 2000000600 MINER_FUND_RATIO = 8 MINER_FUND_ADDR = 'ecregtest:pqnqv9lt7e5vjyp0w88zf2af0l92l8rxdgz0wv9ltl' class MinerFundTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 self.extra_args = [[ '-enableminerfund', '-axionactivationtime={}'.format(AXION_ACTIVATION_TIME), ]] def run_test(self): node = self.nodes[0] address = node.get_deterministic_priv_key().address self.log.info('Create some history') for _ in range(0, 50): node.generatetoaddress(1, address) node = self.nodes[0] address = node.get_deterministic_priv_key().address # Move MTP forward to axion activation node.setmocktime(AXION_ACTIVATION_TIME) node.generatetoaddress(6, address) assert_equal( node.getblockchaininfo()['mediantime'], AXION_ACTIVATION_TIME) # Let's remember the hash of this block for later use. fork_block_hash = int(node.getbestblockhash(), 16) def get_best_coinbase(): return node.getblock(node.getbestblockhash(), 2)['tx'][0] # No money goes to the fund. coinbase = get_best_coinbase() assert_equal(len(coinbase['vout']), 1) block_reward = coinbase['vout'][0]['value'] # First block with the new rules. node.generatetoaddress(1, address) # Now we send part of the coinbase to the fund. coinbase = get_best_coinbase() assert_equal(len(coinbase['vout']), 2) assert_equal( coinbase['vout'][1]['scriptPubKey']['addresses'][0], MINER_FUND_ADDR) total = Decimal() for o in coinbase['vout']: total += o['value'] assert_equal(total, block_reward) assert_greater_than_or_equal( coinbase['vout'][1]['value'], (MINER_FUND_RATIO * total) / 100) # Invalidate top block, submit a custom block that do not send anything # to the fund and check it is rejected. node.invalidateblock(node.getbestblockhash()) block_height = node.getblockcount() + 1 block = create_block( fork_block_hash, create_coinbase(block_height), AXION_ACTIVATION_TIME + 1, version=4) block.solve() assert_equal(node.submitblock(ToHex(block)), 'bad-cb-minerfund') if __name__ == '__main__': MinerFundTest().main() diff --git a/test/functional/abc_mining_basic.py b/test/functional/abc_mining_basic.py index 47e3ae524..d605aeeb2 100755 --- a/test/functional/abc_mining_basic.py +++ b/test/functional/abc_mining_basic.py @@ -1,159 +1,157 @@ #!/usr/bin/env python3 # Copyright (c) 2020 The Bitcoin developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """ Tests for Bitcoin ABC mining RPCs """ +from decimal import Decimal + from test_framework.cdefs import ( BLOCK_MAXBYTES_MAXSIGCHECKS_RATIO, DEFAULT_MAX_BLOCK_SIZE, ) -from test_framework.messages import ( - XEC, -) +from test_framework.messages import XEC from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_greater_than_or_equal, connect_nodes, ) -from decimal import Decimal - AXION_ACTIVATION_TIME = 2000000600 MINER_FUND_ADDR = 'ecregtest:pqnqv9lt7e5vjyp0w88zf2af0l92l8rxdgz0wv9ltl' MINER_FUND_LEGACY_ADDR = '2MviGxxFciGeWTgkUgYgjqehWt18c4ZsShd' class AbcMiningRPCTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 3 self.extra_args = [[], [ '-enableminerfund', '-axionactivationtime={}'.format(AXION_ACTIVATION_TIME), ], [ '-enableminerfund', '-usecashaddr=0', '-axionactivationtime={}'.format(AXION_ACTIVATION_TIME), ]] def setup_network(self): self.setup_nodes() # Connect node0 to all other nodes so getblocktemplate will return results # (getblocktemplate has a sanity check that ensures it's connected to a network) # Since the other nodes are mining blocks "in the future" compared to node0, # node0 will not broadcast blocks between the other nodes. for n in range(1, len(self.nodes)): connect_nodes(self.nodes[0], self.nodes[n]) def run_for_node(self, node, expectedMinerFundAddress): address = node.get_deterministic_priv_key().address # Assert the results of getblocktemplate have expected values. Keys not # in 'expected' are not checked. def assert_getblocktemplate(expected): # Always test these values in addition to those passed in expected = {**expected, **{ 'sigoplimit': DEFAULT_MAX_BLOCK_SIZE // BLOCK_MAXBYTES_MAXSIGCHECKS_RATIO, }} blockTemplate = node.getblocktemplate() for key, value in expected.items(): assert_equal(blockTemplate[key], value) # Move block time to just before axion activation node.setmocktime(AXION_ACTIVATION_TIME) node.generatetoaddress(5, address) # Before axion activation, the miner fund list is empty assert_getblocktemplate({ 'coinbasetxn': { 'minerfund': { 'addresses': [], 'minimumvalue': 0, }, }, }) # Move MTP forward to axion activation node.generatetoaddress(1, address) assert_equal( node.getblockchaininfo()['mediantime'], AXION_ACTIVATION_TIME) def get_best_coinbase(): return node.getblock(node.getbestblockhash(), 2)['tx'][0] coinbase = get_best_coinbase() assert_equal(len(coinbase['vout']), 1) block_reward = coinbase['vout'][0]['value'] # We don't need to test all fields in getblocktemplate since many of # them are covered in mining_basic.py assert_equal(node.getmempoolinfo()['size'], 0) assert_getblocktemplate({ 'coinbasetxn': { # We expect to start seeing the miner fund addresses since the # next block will start enforcing them. 'minerfund': { 'addresses': [expectedMinerFundAddress], 'minimumvalue': block_reward * 8 // 100 * XEC, }, }, # Although the coinbase value need not necessarily be the same as # the last block due to halvings and fees, we know this to be true # since we are not crossing a halving boundary and there are no # transactions in the mempool. 'coinbasevalue': block_reward * XEC, 'mintime': AXION_ACTIVATION_TIME + 1, }) # First block with the new rules node.generatetoaddress(1, address) # We expect the coinbase to have multiple outputs now coinbase = get_best_coinbase() assert_greater_than_or_equal(len(coinbase['vout']), 2) total = Decimal() for o in coinbase['vout']: total += o['value'] assert_equal(total, block_reward) assert_getblocktemplate({ 'coinbasetxn': { 'minerfund': { 'addresses': [expectedMinerFundAddress], 'minimumvalue': block_reward * 8 // 100 * XEC, }, }, # Again, we assume the coinbase value is the same as prior blocks. 'coinbasevalue': block_reward * XEC, 'mintime': AXION_ACTIVATION_TIME + 1, }) # Move MTP forward node.setmocktime(AXION_ACTIVATION_TIME + 1) node.generatetoaddress(6, address) assert_getblocktemplate({ 'coinbasetxn': { 'minerfund': { 'addresses': [expectedMinerFundAddress], 'minimumvalue': block_reward * 8 // 100 * XEC, }, }, 'coinbasevalue': block_reward * XEC, 'mintime': AXION_ACTIVATION_TIME + 2, }) def run_test(self): # node0 is for connectivity only and is not mined on (see # setup_network) self.run_for_node(self.nodes[1], MINER_FUND_ADDR) self.run_for_node(self.nodes[2], MINER_FUND_LEGACY_ADDR) if __name__ == '__main__': AbcMiningRPCTest().main() diff --git a/test/functional/abc_p2p_avalanche_peer_discovery.py b/test/functional/abc_p2p_avalanche_peer_discovery.py index bdd07aa57..b511ac827 100755 --- a/test/functional/abc_p2p_avalanche_peer_discovery.py +++ b/test/functional/abc_p2p_avalanche_peer_discovery.py @@ -1,262 +1,259 @@ #!/usr/bin/env python3 # Copyright (c) 2020-2021 The Bitcoin developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the peer discovery behavior of avalanche nodes. This includes tests for the service flag, avahello handshake and proof exchange. """ import time from test_framework.address import ADDRESS_ECREG_UNSPENDABLE from test_framework.avatools import ( - get_ava_p2p_interface, create_coinbase_stakes, + get_ava_p2p_interface, get_proof_ids, ) -from test_framework.key import ( - ECKey, - ECPubKey, -) -from test_framework.p2p import p2p_lock +from test_framework.key import ECKey, ECPubKey from test_framework.messages import ( + MSG_AVA_PROOF, + NODE_AVALANCHE, + NODE_NETWORK, AvalancheProof, CInv, FromHex, - MSG_AVA_PROOF, msg_getdata, - NODE_AVALANCHE, - NODE_NETWORK, ) +from test_framework.p2p import p2p_lock from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal from test_framework.wallet_util import bytes_to_wif UNCONDITIONAL_RELAY_DELAY = 2 * 60 class AvalancheTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 self.extra_args = [['-enableavalanche=1', '-enableavalanchepeerdiscovery=1']] self.supports_cli = False def run_test(self): node = self.nodes[0] # duplicate the deterministic sig test from src/test/key_tests.cpp privkey = ECKey() privkey.set(bytes.fromhex( "12b004fff7f4b69ef8650e767f18f11ede158148b425660723b9f9a66e61f747"), True) wif_privkey = bytes_to_wif(privkey.get_bytes()) self.log.info( "Check the node is signalling the avalanche service bit only if there is a proof.") assert_equal( int(node.getnetworkinfo()['localservices'], 16) & NODE_AVALANCHE, 0) # Create stakes by mining blocks addrkey0 = node.get_deterministic_priv_key() blockhashes = node.generatetoaddress(2, addrkey0.address) stakes = create_coinbase_stakes(node, [blockhashes[0]], addrkey0.key) proof_sequence = 11 proof_expiration = 12 proof = node.buildavalancheproof( proof_sequence, proof_expiration, wif_privkey, stakes) # Restart the node self.restart_node(0, self.extra_args[0] + [ "-avaproof={}".format(proof), "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", ]) assert_equal( int(node.getnetworkinfo()['localservices'], 16) & NODE_AVALANCHE, NODE_AVALANCHE) def check_avahello(args): # Restart the node with the given args self.restart_node(0, self.extra_args[0] + args) peer = get_ava_p2p_interface(node) avahello = peer.wait_for_avahello().hello avakey = ECPubKey() avakey.set(bytes.fromhex(node.getavalanchekey())) assert avakey.verify_schnorr( avahello.sig, avahello.get_sighash(peer)) self.log.info( "Test the avahello signature with a generated delegation") check_avahello([ "-avaproof={}".format(proof), "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN" ]) master_key = ECKey() master_key.generate() limited_id = FromHex(AvalancheProof(), proof).limited_proofid delegation = node.delegateavalancheproof( f"{limited_id:0{64}x}", bytes_to_wif(privkey.get_bytes()), master_key.get_pubkey().get_bytes().hex(), ) self.log.info("Test the avahello signature with a supplied delegation") check_avahello([ "-avaproof={}".format(proof), "-avadelegation={}".format(delegation), "-avamasterkey={}".format(bytes_to_wif(master_key.get_bytes())), ]) stakes = create_coinbase_stakes(node, [blockhashes[1]], addrkey0.key) interface_proof_hex = node.buildavalancheproof( proof_sequence, proof_expiration, wif_privkey, stakes) limited_id = FromHex( AvalancheProof(), interface_proof_hex).limited_proofid # delegate delegated_key = ECKey() delegated_key.generate() interface_delegation_hex = node.delegateavalancheproof( f"{limited_id:0{64}x}", bytes_to_wif(privkey.get_bytes()), delegated_key.get_pubkey().get_bytes().hex(), None) self.log.info("Test that wrong avahello signature causes a ban") bad_interface = get_ava_p2p_interface(node) wrong_key = ECKey() wrong_key.generate() with node.assert_debug_log( ["Misbehaving", "peer=1 (0 -> 100) BAN THRESHOLD EXCEEDED: invalid-avahello-signature"]): bad_interface.send_avahello(interface_delegation_hex, wrong_key) bad_interface.wait_for_disconnect() self.log.info( 'Check that receiving a valid avahello triggers a proof getdata request') good_interface = get_ava_p2p_interface(node) proofid = good_interface.send_avahello( interface_delegation_hex, delegated_key) def getdata_found(peer, proofid): with p2p_lock: return good_interface.last_message.get( "getdata") and good_interface.last_message["getdata"].inv[-1].hash == proofid self.wait_until(lambda: getdata_found(good_interface, proofid)) self.log.info('Check that we can download the proof from our peer') node_proofid = FromHex(AvalancheProof(), proof).proofid def wait_for_proof_validation(): # Connect some blocks to trigger the proof verification node.generate(1) self.wait_until(lambda: node_proofid in get_proof_ids(node)) wait_for_proof_validation() getdata = msg_getdata([CInv(MSG_AVA_PROOF, node_proofid)]) self.log.info( "Proof has been inv'ed recently, check it can be requested") good_interface.send_message(getdata) def proof_received(peer): with p2p_lock: return peer.last_message.get( "avaproof") and peer.last_message["avaproof"].proof.proofid == node_proofid self.wait_until(lambda: proof_received(good_interface)) # Restart the node self.restart_node(0, self.extra_args[0] + [ "-avaproof={}".format(proof), "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", ]) wait_for_proof_validation() self.log.info( "The proof has not been announced, it cannot be requested") peer = get_ava_p2p_interface(node, services=NODE_NETWORK) peer.send_message(getdata) # Give enough time for the node to answer. Since we cannot check for a # non-event this is the best we can do time.sleep(2) assert not proof_received(peer) self.log.info("The proof is known for long enough to be requested") current_time = int(time.time()) node.setmocktime(current_time + UNCONDITIONAL_RELAY_DELAY) peer.send_message(getdata) self.wait_until(lambda: proof_received(peer)) # Restart the node self.restart_node(0, self.extra_args[0] + [ "-avaproof={}".format(proof), "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", ]) wait_for_proof_validation() # The only peer is the node itself assert_equal(len(node.getavalanchepeerinfo()), 1) assert_equal(node.getavalanchepeerinfo()[0]["proof"], proof) peer = get_ava_p2p_interface(node) peer_proofid = peer.send_avahello( interface_delegation_hex, delegated_key) self.wait_until(lambda: getdata_found(peer, peer_proofid)) assert peer_proofid not in get_proof_ids(node) self.log.info( "Check that the peer gets added as an avalanche node as soon as the node knows about the proof") node.sendavalancheproof(interface_proof_hex) def has_node_count(count): peerinfo = node.getavalanchepeerinfo() return (len(peerinfo) == 2 and peerinfo[-1]["proof"] == interface_proof_hex and peerinfo[-1]["nodecount"] == count) self.wait_until(lambda: has_node_count(1)) self.log.info( "Check that the peer gets added immediately if the proof is already known") # Connect another peer using the same proof peer_proof_known = get_ava_p2p_interface(node) peer_proof_known.send_avahello(interface_delegation_hex, delegated_key) self.wait_until(lambda: has_node_count(2)) self.log.info("Invalidate the proof and check the nodes are removed") tip = node.getbestblockhash() # Invalidate the block with the proof utxo node.invalidateblock(blockhashes[1]) # Change the address to make sure we don't generate a block identical # to the one we just invalidated. Can be generate(1) after D9694 or # D9697 is landed. forked_tip = node.generatetoaddress(1, ADDRESS_ECREG_UNSPENDABLE)[0] self.wait_until(lambda: node.getbestblockhash() == forked_tip) self.wait_until(lambda: len(node.getavalanchepeerinfo()) == 1) assert peer_proofid not in get_proof_ids(node) self.log.info("Reorg back and check the nodes are added back") node.invalidateblock(forked_tip) node.reconsiderblock(tip) self.wait_until(lambda: has_node_count(2), timeout=2) if __name__ == '__main__': AvalancheTest().main() diff --git a/test/functional/abc_p2p_avalanche_voting.py b/test/functional/abc_p2p_avalanche_voting.py index 08d892a92..937258b2d 100755 --- a/test/functional/abc_p2p_avalanche_voting.py +++ b/test/functional/abc_p2p_avalanche_voting.py @@ -1,240 +1,237 @@ #!/usr/bin/env python3 # Copyright (c) 2020-2021 The Bitcoin developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the resolution of forks via avalanche.""" import random from test_framework.avatools import ( - get_ava_p2p_interface, create_coinbase_stakes, + get_ava_p2p_interface, ) -from test_framework.key import ( - ECKey, - ECPubKey, -) +from test_framework.key import ECKey, ECPubKey from test_framework.messages import AvalancheVote from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal from test_framework.wallet_util import bytes_to_wif BLOCK_ACCEPTED = 0 BLOCK_INVALID = 1 BLOCK_PARKED = 2 BLOCK_FORK = 3 BLOCK_UNKNOWN = -1 BLOCK_MISSING = -2 BLOCK_PENDING = -3 QUORUM_NODE_COUNT = 16 class AvalancheTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 2 self.extra_args = [ ['-enableavalanche=1', '-avacooldown=0'], ['-enableavalanche=1', '-avacooldown=0', '-noparkdeepreorg', '-maxreorgdepth=-1']] self.supports_cli = False self.rpc_timeout = 120 def run_test(self): node = self.nodes[0] # Build a fake quorum of nodes. def get_quorum(): return [get_ava_p2p_interface(node) for _ in range(0, QUORUM_NODE_COUNT)] # Pick on node from the quorum for polling. quorum = get_quorum() poll_node = quorum[0] # Generate many block and poll for them. addrkey0 = node.get_deterministic_priv_key() blockhashes = node.generatetoaddress(100, addrkey0.address) # Use the first coinbase to create a stake stakes = create_coinbase_stakes(node, [blockhashes[0]], addrkey0.key) fork_node = self.nodes[1] # Make sure the fork node has synced the blocks self.sync_blocks([node, fork_node]) # Get the key so we can verify signatures. avakey = ECPubKey() avakey.set(bytes.fromhex(node.getavalanchekey())) self.log.info("Poll for the chain tip...") best_block_hash = int(node.getbestblockhash(), 16) poll_node.send_poll([best_block_hash]) def assert_response(expected): response = poll_node.wait_for_avaresponse() r = response.response assert_equal(r.cooldown, 0) # Verify signature. assert avakey.verify_schnorr(response.sig, r.get_hash()) votes = r.votes assert_equal(len(votes), len(expected)) for i in range(0, len(votes)): assert_equal(repr(votes[i]), repr(expected[i])) assert_response([AvalancheVote(BLOCK_ACCEPTED, best_block_hash)]) self.log.info("Poll for a selection of blocks...") various_block_hashes = [ int(node.getblockhash(0), 16), int(node.getblockhash(1), 16), int(node.getblockhash(10), 16), int(node.getblockhash(25), 16), int(node.getblockhash(42), 16), int(node.getblockhash(96), 16), int(node.getblockhash(99), 16), int(node.getblockhash(100), 16), ] poll_node.send_poll(various_block_hashes) assert_response([AvalancheVote(BLOCK_ACCEPTED, h) for h in various_block_hashes]) self.log.info( "Poll for a selection of blocks, but some are now invalid...") invalidated_block = node.getblockhash(76) node.invalidateblock(invalidated_block) # We need to send the coin to a new address in order to make sure we do # not regenerate the same block. node.generatetoaddress( 26, 'ecregtest:pqv2r67sgz3qumufap3h2uuj0zfmnzuv8v38gtrh5v') node.reconsiderblock(invalidated_block) poll_node.send_poll(various_block_hashes) assert_response([AvalancheVote(BLOCK_ACCEPTED, h) for h in various_block_hashes[:5]] + [AvalancheVote(BLOCK_FORK, h) for h in various_block_hashes[-3:]]) self.log.info("Poll for unknown blocks...") various_block_hashes = [ int(node.getblockhash(0), 16), int(node.getblockhash(25), 16), int(node.getblockhash(42), 16), various_block_hashes[5], various_block_hashes[6], various_block_hashes[7], random.randrange(1 << 255, (1 << 256) - 1), random.randrange(1 << 255, (1 << 256) - 1), random.randrange(1 << 255, (1 << 256) - 1), ] poll_node.send_poll(various_block_hashes) assert_response([AvalancheVote(BLOCK_ACCEPTED, h) for h in various_block_hashes[:3]] + [AvalancheVote(BLOCK_FORK, h) for h in various_block_hashes[3:6]] + [AvalancheVote(BLOCK_UNKNOWN, h) for h in various_block_hashes[-3:]]) self.log.info("Trigger polling from the node...") # duplicate the deterministic sig test from src/test/key_tests.cpp privkey = ECKey() privkey.set(bytes.fromhex( "12b004fff7f4b69ef8650e767f18f11ede158148b425660723b9f9a66e61f747"), True) proof_sequence = 11 proof_expiration = 12 proof = node.buildavalancheproof( proof_sequence, proof_expiration, bytes_to_wif( privkey.get_bytes()), stakes) # Activate the quorum. for n in quorum: success = node.addavalanchenode( n.nodeid, privkey.get_pubkey().get_bytes().hex(), proof) assert success is True def can_find_block_in_poll(hash, resp=BLOCK_ACCEPTED): found_hash = False for n in quorum: poll = n.get_avapoll_if_available() # That node has not received a poll if poll is None: continue # We got a poll, check for the hash and repond votes = [] for inv in poll.invs: # Vote yes to everything r = BLOCK_ACCEPTED # Look for what we expect if inv.hash == hash: r = resp found_hash = True votes.append(AvalancheVote(r, inv.hash)) n.send_avaresponse(poll.round, votes, privkey) return found_hash # Now that we have a peer, we should start polling for the tip. hash_tip = int(node.getbestblockhash(), 16) self.wait_until(lambda: can_find_block_in_poll(hash_tip), timeout=5) # Make sure the fork node has synced the blocks self.sync_blocks([node, fork_node]) # Create a fork 2 blocks deep. This should trigger polling. fork_node.invalidateblock(fork_node.getblockhash(100)) fork_address = fork_node.get_deterministic_priv_key().address fork_node.generatetoaddress(2, fork_address) # Because the new tip is a deep reorg, the node will not accept it # right away, but poll for it. def parked_block(blockhash): for tip in node.getchaintips(): if tip["hash"] == blockhash: assert tip["status"] != "active" return tip["status"] == "parked" return False fork_tip = fork_node.getbestblockhash() self.wait_until(lambda: parked_block(fork_tip)) self.log.info("Answer all polls to finalize...") hash_to_find = int(fork_tip, 16) def has_accepted_new_tip(): can_find_block_in_poll(hash_to_find) return node.getbestblockhash() == fork_tip # Because everybody answers yes, the node will accept that block. self.wait_until(has_accepted_new_tip, timeout=15) assert_equal(node.getbestblockhash(), fork_tip) self.log.info("Answer all polls to park...") node.generate(1) tip_to_park = node.getbestblockhash() hash_to_find = int(tip_to_park, 16) assert(tip_to_park != fork_tip) def has_parked_new_tip(): can_find_block_in_poll(hash_to_find, BLOCK_PARKED) return node.getbestblockhash() == fork_tip # Because everybody answers no, the node will park that block. self.wait_until(has_parked_new_tip, timeout=15) assert_equal(node.getbestblockhash(), fork_tip) self.log.info( "Check the node is discouraging unexpected avaresponses.") with node.assert_debug_log( ['Misbehaving', 'peer=1 (0 -> 2): unexpected-ava-response']): # unknown voting round poll_node.send_avaresponse( round=2**32 - 1, votes=[], privkey=privkey) if __name__ == '__main__': AvalancheTest().main() diff --git a/test/functional/abc_p2p_compactblocks.py b/test/functional/abc_p2p_compactblocks.py index ac1f968a7..2d730b7a3 100755 --- a/test/functional/abc_p2p_compactblocks.py +++ b/test/functional/abc_p2p_compactblocks.py @@ -1,360 +1,356 @@ #!/usr/bin/env python3 # Copyright (c) 2015-2016 The Bitcoin Core developers # Copyright (c) 2017 The Bitcoin developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """ This test checks simple acceptance of bigger blocks via p2p. It is derived from the much more complex p2p-fullblocktest. The intention is that small tests can be derived from this one, or this one can be extended, to cover the checks done for bigger blocks (e.g. sigops limits). """ -from collections import deque import random import time +from collections import deque from test_framework.blocktools import ( create_block, create_coinbase, create_tx_with_script, make_conform_to_ctor, ) from test_framework.cdefs import ONE_MEGABYTE from test_framework.messages import ( COutPoint, CTransaction, CTxIn, CTxOut, HeaderAndShortIDs, msg_cmpctblock, msg_sendcmpct, ser_compact_size, ) -from test_framework.p2p import ( - p2p_lock, - P2PDataStore, - P2PInterface, -) -from test_framework.script import CScript, OP_RETURN, OP_TRUE +from test_framework.p2p import P2PDataStore, P2PInterface, p2p_lock +from test_framework.script import OP_RETURN, OP_TRUE, CScript from test_framework.test_framework import BitcoinTestFramework from test_framework.txtools import pad_tx from test_framework.util import assert_equal class PreviousSpendableOutput(): def __init__(self, tx=CTransaction(), n=-1): self.tx = tx # the output we're spending self.n = n # TestP2PConn: A peer we use to send messages to bitcoind, and store responses. class TestP2PConn(P2PInterface): def __init__(self): self.last_sendcmpct = None self.last_cmpctblock = None self.last_getheaders = None self.last_headers = None super().__init__() def on_sendcmpct(self, message): self.last_sendcmpct = message def on_cmpctblock(self, message): self.last_cmpctblock = message self.last_cmpctblock.header_and_shortids.header.calc_sha256() def on_getheaders(self, message): self.last_getheaders = message def on_headers(self, message): self.last_headers = message for x in self.last_headers.headers: x.calc_sha256() def clear_block_data(self): with p2p_lock: self.last_sendcmpct = None self.last_cmpctblock = None class FullBlockTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 self.setup_clean_chain = True self.block_heights = {} self.tip = None self.blocks = {} self.excessive_block_size = 16 * ONE_MEGABYTE self.extra_args = [['-whitelist=noban@127.0.0.1', '-limitancestorcount=999999', '-limitancestorsize=999999', '-limitdescendantcount=999999', '-limitdescendantsize=999999', '-maxmempool=99999', '-excessiveblocksize={}'.format( self.excessive_block_size), '-acceptnonstdtxn=1']] # UBSAN will cause this test to timeout without this. self.rpc_timeout = 180 def add_options(self, parser): super().add_options(parser) parser.add_argument( "--runbarelyexpensive", dest="runbarelyexpensive", default=True) def add_transactions_to_block(self, block, tx_list): [tx.rehash() for tx in tx_list] block.vtx.extend(tx_list) # this is a little handier to use than the version in blocktools.py def create_tx(self, spend_tx, n, value, script=CScript([OP_TRUE])): tx = create_tx_with_script(spend_tx, n, b"", value, script) return tx def next_block(self, number, spend=None, script=CScript( [OP_TRUE]), block_size=0, extra_txns=0): if self.tip is None: base_block_hash = self.genesis_hash block_time = int(time.time()) + 1 else: base_block_hash = self.tip.sha256 block_time = self.tip.nTime + 1 # First create the coinbase height = self.block_heights[base_block_hash] + 1 coinbase = create_coinbase(height) coinbase.rehash() if spend is None: # We need to have something to spend to fill the block. assert_equal(block_size, 0) block = create_block(base_block_hash, coinbase, block_time) else: # all but one satoshi to fees coinbase.vout[0].nValue += spend.tx.vout[spend.n].nValue - 1 coinbase.rehash() block = create_block(base_block_hash, coinbase, block_time) # Make sure we have plenty enough to spend going forward. spendable_outputs = deque([spend]) def get_base_transaction(): # Create the new transaction tx = CTransaction() # Spend from one of the spendable outputs spend = spendable_outputs.popleft() tx.vin.append(CTxIn(COutPoint(spend.tx.sha256, spend.n))) # Add spendable outputs for i in range(4): tx.vout.append(CTxOut(0, CScript([OP_TRUE]))) spendable_outputs.append(PreviousSpendableOutput(tx, i)) pad_tx(tx) return tx tx = get_base_transaction() # Make it the same format as transaction added for padding and save the size. # It's missing the padding output, so we add a constant to account # for it. tx.rehash() # If a specific script is required, add it. if script is not None: tx.vout.append(CTxOut(1, script)) # Put some random data into the first transaction of the chain to # randomize ids. tx.vout.append( CTxOut(0, CScript([random.randint(0, 256), OP_RETURN]))) # Add the transaction to the block self.add_transactions_to_block(block, [tx]) # Add transaction until we reach the expected transaction count for _ in range(extra_txns): self.add_transactions_to_block(block, [get_base_transaction()]) # If we have a block size requirement, just fill # the block until we get there current_block_size = len(block.serialize()) overage_bytes = 0 while current_block_size < block_size: # We will add a new transaction. That means the size of # the field enumerating how many transaction go in the block # may change. current_block_size -= len(ser_compact_size(len(block.vtx))) current_block_size += len(ser_compact_size(len(block.vtx) + 1)) # Add padding to fill the block. left_to_fill = block_size - current_block_size # Don't go over the 1 mb limit for a txn if left_to_fill > 500000: # Make sure we eat up non-divisible by 100 amounts quickly # Also keep transaction less than 1 MB left_to_fill = 500000 + left_to_fill % 100 # Create the new transaction tx = get_base_transaction() pad_tx(tx, left_to_fill - overage_bytes) if len(tx.serialize()) + current_block_size > block_size: # Our padding was too big try again overage_bytes += 1 continue # Add the tx to the list of transactions to be included # in the block. self.add_transactions_to_block(block, [tx]) current_block_size += len(tx.serialize()) # Now that we added a bunch of transaction, we need to recompute # the merkle root. make_conform_to_ctor(block) block.hashMerkleRoot = block.calc_merkle_root() # Check that the block size is what's expected if block_size > 0: assert_equal(len(block.serialize()), block_size) # Do PoW, which is cheap on regnet block.solve() self.tip = block self.block_heights[block.sha256] = height assert number not in self.blocks self.blocks[number] = block return block def run_test(self): node = self.nodes[0] default_p2p = node.add_p2p_connection(P2PDataStore()) test_p2p = node.add_p2p_connection(TestP2PConn()) self.genesis_hash = int(node.getbestblockhash(), 16) self.block_heights[self.genesis_hash] = 0 spendable_outputs = [] # save the current tip so it can be spent by a later block def save_spendable_output(): spendable_outputs.append(self.tip) # get an output that we previously marked as spendable def get_spendable_output(): return PreviousSpendableOutput(spendable_outputs.pop(0).vtx[0], 0) # move the tip back to a previous block def tip(number): self.tip = self.blocks[number] # shorthand for functions block = self.next_block # Create a new block block(0) save_spendable_output() default_p2p.send_blocks_and_test([self.tip], node) # Now we need that block to mature so we can spend the coinbase. maturity_blocks = [] for i in range(99): block(5000 + i) maturity_blocks.append(self.tip) save_spendable_output() # Get to one block of the May 15, 2018 HF activation for i in range(6): block(5100 + i) maturity_blocks.append(self.tip) # Send it all to the node at once. default_p2p.send_blocks_and_test(maturity_blocks, node) # collect spendable outputs now to avoid cluttering the code later on out = [] for i in range(100): out.append(get_spendable_output()) # Check that compact block also work for big blocks # Wait for SENDCMPCT def received_sendcmpct(): return (test_p2p.last_sendcmpct is not None) self.wait_until(received_sendcmpct, timeout=30) sendcmpct = msg_sendcmpct() sendcmpct.version = 1 sendcmpct.announce = True test_p2p.send_and_ping(sendcmpct) # Exchange headers def received_getheaders(): return (test_p2p.last_getheaders is not None) self.wait_until(received_getheaders, timeout=30) # Return the favor test_p2p.send_message(test_p2p.last_getheaders) # Wait for the header list def received_headers(): return (test_p2p.last_headers is not None) self.wait_until(received_headers, timeout=30) # It's like we know about the same headers ! test_p2p.send_message(test_p2p.last_headers) # Send a block b1 = block(1, spend=out[0], block_size=ONE_MEGABYTE + 1) default_p2p.send_blocks_and_test([self.tip], node) # Checks the node to forward it via compact block def received_block(): return (test_p2p.last_cmpctblock is not None) self.wait_until(received_block, timeout=30) # Was it our block ? cmpctblk_header = test_p2p.last_cmpctblock.header_and_shortids.header cmpctblk_header.calc_sha256() assert cmpctblk_header.sha256 == b1.sha256 # Send a large block with numerous transactions. test_p2p.clear_block_data() b2 = block(2, spend=out[1], extra_txns=70000, block_size=self.excessive_block_size - 1000) default_p2p.send_blocks_and_test([self.tip], node) # Checks the node forwards it via compact block self.wait_until(received_block, timeout=30) # Was it our block ? cmpctblk_header = test_p2p.last_cmpctblock.header_and_shortids.header cmpctblk_header.calc_sha256() assert cmpctblk_header.sha256 == b2.sha256 # In order to avoid having to resend a ton of transactions, we invalidate # b2, which will send all its transactions in the mempool. Note that this # assumes reorgs will insert low-fee transactions back into the # mempool. node.invalidateblock(node.getbestblockhash()) # Let's send a compact block and see if the node accepts it. # Let's modify b2 and use it so that we can reuse the mempool. tx = b2.vtx[0] tx.vout.append(CTxOut(0, CScript([random.randint(0, 256), OP_RETURN]))) tx.rehash() b2.vtx[0] = tx b2.hashMerkleRoot = b2.calc_merkle_root() b2.solve() # Now we create the compact block and send it comp_block = HeaderAndShortIDs() comp_block.initialize_from_block(b2) test_p2p.send_and_ping(msg_cmpctblock(comp_block.to_p2p())) # Check that compact block is received properly assert int(node.getbestblockhash(), 16) == b2.sha256 if __name__ == '__main__': FullBlockTest().main() diff --git a/test/functional/abc_p2p_fullblocktest.py b/test/functional/abc_p2p_fullblocktest.py index e661e0f5c..fd5ca3ed5 100755 --- a/test/functional/abc_p2p_fullblocktest.py +++ b/test/functional/abc_p2p_fullblocktest.py @@ -1,272 +1,266 @@ #!/usr/bin/env python3 # Copyright (c) 2015-2016 The Bitcoin Core developers # Copyright (c) 2017 The Bitcoin developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """ This test checks simple acceptance of bigger blocks via p2p. It is derived from the much more complex p2p-fullblocktest. The intention is that small tests can be derived from this one, or this one can be extended, to cover the checks done for bigger blocks (e.g. sigops limits). """ -from collections import deque import random import time +from collections import deque from test_framework.blocktools import ( create_block, create_coinbase, create_tx_with_script, make_conform_to_ctor, ) -from test_framework.cdefs import ( - ONE_MEGABYTE, -) +from test_framework.cdefs import ONE_MEGABYTE from test_framework.messages import ( COutPoint, CTransaction, CTxIn, CTxOut, - ser_compact_size, ToHex, + ser_compact_size, ) from test_framework.p2p import P2PDataStore -from test_framework.script import ( - CScript, - OP_RETURN, - OP_TRUE, -) +from test_framework.script import OP_RETURN, OP_TRUE, CScript from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal class PreviousSpendableOutput(): def __init__(self, tx=CTransaction(), n=-1): self.tx = tx # the output we're spending self.n = n class FullBlockTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 self.setup_clean_chain = True self.block_heights = {} self.tip = None self.blocks = {} self.excessive_block_size = 100 * ONE_MEGABYTE self.extra_args = [['-whitelist=noban@127.0.0.1', "-excessiveblocksize={}".format(self.excessive_block_size)]] self.supports_cli = False # The default timeout is not enough when submitting large blocks with # TSAN enabled self.rpc_timeout = 360 def add_options(self, parser): super().add_options(parser) parser.add_argument( "--runbarelyexpensive", dest="runbarelyexpensive", default=True) def add_transactions_to_block(self, block, tx_list): [tx.rehash() for tx in tx_list] block.vtx.extend(tx_list) # this is a little handier to use than the version in blocktools.py def create_tx(self, spend, value, script=CScript([OP_TRUE])): tx = create_tx_with_script(spend.tx, spend.n, b"", value, script) return tx def next_block(self, number, spend=None, script=CScript([OP_TRUE]), block_size=0): if self.tip is None: base_block_hash = self.genesis_hash block_time = int(time.time()) + 1 else: base_block_hash = self.tip.sha256 block_time = self.tip.nTime + 1 # First create the coinbase height = self.block_heights[base_block_hash] + 1 coinbase = create_coinbase(height) coinbase.rehash() if spend is None: # We need to have something to spend to fill the block. assert_equal(block_size, 0) block = create_block(base_block_hash, coinbase, block_time) else: # all but one satoshi to fees coinbase.vout[0].nValue += spend.tx.vout[spend.n].nValue - 1 coinbase.rehash() block = create_block(base_block_hash, coinbase, block_time) # Make sure we have plenty engough to spend going forward. spendable_outputs = deque([spend]) def get_base_transaction(): # Create the new transaction tx = CTransaction() # Spend from one of the spendable outputs spend = spendable_outputs.popleft() tx.vin.append(CTxIn(COutPoint(spend.tx.sha256, spend.n))) # Add spendable outputs for i in range(4): tx.vout.append(CTxOut(0, CScript([OP_TRUE]))) spendable_outputs.append(PreviousSpendableOutput(tx, i)) return tx tx = get_base_transaction() # Make it the same format as transaction added for padding and save the size. # It's missing the padding output, so we add a constant to account # for it. tx.rehash() base_tx_size = len(tx.serialize()) + 18 # If a specific script is required, add it. if script is not None: tx.vout.append(CTxOut(1, script)) # Put some random data into the first transaction of the chain to # randomize ids. tx.vout.append( CTxOut(0, CScript([random.randint(0, 256), OP_RETURN]))) # Add the transaction to the block self.add_transactions_to_block(block, [tx]) # If we have a block size requirement, just fill # the block until we get there current_block_size = len(block.serialize()) while current_block_size < block_size: # We will add a new transaction. That means the size of # the field enumerating how many transaction go in the block # may change. current_block_size -= len(ser_compact_size(len(block.vtx))) current_block_size += len(ser_compact_size(len(block.vtx) + 1)) # Create the new transaction tx = get_base_transaction() # Add padding to fill the block. script_length = block_size - current_block_size - base_tx_size if script_length > 510000: if script_length < 1000000: # Make sure we don't find ourselves in a position where we # need to generate a transaction smaller than what we # expected. script_length = script_length // 2 else: script_length = 500000 script_pad_len = script_length script_output = CScript([b'\x00' * script_pad_len]) tx.vout.append(CTxOut(0, script_output)) # Add the tx to the list of transactions to be included # in the block. self.add_transactions_to_block(block, [tx]) current_block_size += len(tx.serialize()) # Now that we added a bunch of transaction, we need to recompute # the merkle root. make_conform_to_ctor(block) block.hashMerkleRoot = block.calc_merkle_root() # Check that the block size is what's expected if block_size > 0: assert_equal(len(block.serialize()), block_size) # Do PoW, which is cheap on regnet block.solve() self.tip = block self.block_heights[block.sha256] = height assert number not in self.blocks self.blocks[number] = block return block def run_test(self): node = self.nodes[0] node.add_p2p_connection(P2PDataStore()) self.genesis_hash = int(node.getbestblockhash(), 16) self.block_heights[self.genesis_hash] = 0 spendable_outputs = [] # save the current tip so it can be spent by a later block def save_spendable_output(): spendable_outputs.append(self.tip) # get an output that we previously marked as spendable def get_spendable_output(): return PreviousSpendableOutput(spendable_outputs.pop(0).vtx[0], 0) # move the tip back to a previous block def tip(number): self.tip = self.blocks[number] # adds transactions to the block and updates state def update_block(block_number, new_transactions): block = self.blocks[block_number] self.add_transactions_to_block(block, new_transactions) old_sha256 = block.sha256 make_conform_to_ctor(block) block.hashMerkleRoot = block.calc_merkle_root() block.solve() # Update the internal state just like in next_block self.tip = block if block.sha256 != old_sha256: self.block_heights[ block.sha256] = self.block_heights[old_sha256] del self.block_heights[old_sha256] self.blocks[block_number] = block return block # shorthand for functions block = self.next_block # Create a new block block(0) save_spendable_output() node.p2p.send_blocks_and_test([self.tip], node) # Now we need that block to mature so we can spend the coinbase. maturity_blocks = [] for i in range(99): block(5000 + i) maturity_blocks.append(self.tip) save_spendable_output() node.p2p.send_blocks_and_test(maturity_blocks, node) # collect spendable outputs now to avoid cluttering the code later on out = [] for i in range(100): out.append(get_spendable_output()) # Let's build some blocks and test them. for i in range(16): n = i + 1 block(n, spend=out[i], block_size=n * ONE_MEGABYTE) node.p2p.send_blocks_and_test([self.tip], node) # block of maximal size block(17, spend=out[16], block_size=self.excessive_block_size) node.p2p.send_blocks_and_test([self.tip], node) # Reject oversized blocks with bad-blk-length error block(18, spend=out[17], block_size=self.excessive_block_size + 1) node.p2p.send_blocks_and_test( [self.tip], node, success=False, reject_reason='bad-blk-length') # Rewind bad block. tip(17) # Submit a very large block via RPC large_block = block( 33, spend=out[17], block_size=self.excessive_block_size) assert_equal(node.submitblock(ToHex(large_block)), None) if __name__ == '__main__': FullBlockTest().main() diff --git a/test/functional/abc_p2p_proof_inventory.py b/test/functional/abc_p2p_proof_inventory.py index 25dd7aee2..a41446632 100644 --- a/test/functional/abc_p2p_proof_inventory.py +++ b/test/functional/abc_p2p_proof_inventory.py @@ -1,302 +1,295 @@ #!/usr/bin/env python3 # Copyright (c) 2021 The Bitcoin developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """ Test proof inventory relaying """ -from test_framework.avatools import ( - gen_proof, - get_proof_ids, - wait_for_proof, -) +import time + from test_framework.address import ADDRESS_ECREG_UNSPENDABLE +from test_framework.avatools import gen_proof, get_proof_ids, wait_for_proof from test_framework.key import ECKey from test_framework.messages import ( + MSG_AVA_PROOF, + MSG_TYPE_MASK, AvalancheProof, CInv, FromHex, - MSG_AVA_PROOF, - MSG_TYPE_MASK, msg_avaproof, msg_getdata, ) -from test_framework.p2p import ( - P2PInterface, - p2p_lock, -) +from test_framework.p2p import P2PInterface, p2p_lock from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_greater_than, connect_nodes, ) from test_framework.wallet_util import bytes_to_wif -import time - # Broadcast reattempt occurs every 10 to 15 minutes MAX_INITIAL_BROADCAST_DELAY = 15 * 60 # Delay to allow the node to respond to getdata requests UNCONDITIONAL_RELAY_DELAY = 2 * 60 class ProofInvStoreP2PInterface(P2PInterface): def __init__(self): super().__init__() self.proof_invs_counter = 0 def on_inv(self, message): for i in message.inv: if i.type & MSG_TYPE_MASK == MSG_AVA_PROOF: self.proof_invs_counter += 1 class ProofInventoryTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 5 self.extra_args = [['-enableavalanche=1', '-avacooldown=0']] * self.num_nodes def test_send_proof_inv(self): self.log.info("Test sending a proof to our peers") node = self.nodes[0] for i in range(10): node.add_p2p_connection(ProofInvStoreP2PInterface()) _, proof = gen_proof(node) assert node.sendavalancheproof(proof.serialize().hex()) def proof_inv_found(peer): with p2p_lock: return peer.last_message.get( "inv") and peer.last_message["inv"].inv[-1].hash == proof.proofid self.wait_until(lambda: all(proof_inv_found(i) for i in node.p2ps)) self.log.info("Test that we don't send the same inv several times") extra_peer = ProofInvStoreP2PInterface() node.add_p2p_connection(extra_peer) # Send the same proof one more time node.sendavalancheproof(proof.serialize().hex()) # Our new extra peer should receive it but not the others self.wait_until(lambda: proof_inv_found(extra_peer)) assert all(p.proof_invs_counter == 1 for p in node.p2ps) # Send the proof again and force the send loop to be processed for peer in node.p2ps: node.sendavalancheproof(proof.serialize().hex()) peer.sync_with_ping() assert all(p.proof_invs_counter == 1 for p in node.p2ps) def test_receive_proof(self): self.log.info("Test a peer is created on proof reception") node = self.nodes[0] _, proof = gen_proof(node) peer = node.add_p2p_connection(P2PInterface()) msg = msg_avaproof() msg.proof = proof peer.send_message(msg) self.wait_until(lambda: proof.proofid in get_proof_ids(node)) self.log.info("Test receiving a proof with missing utxo is orphaned") privkey = ECKey() privkey.generate() orphan_hex = node.buildavalancheproof( 42, 2000000000, bytes_to_wif(privkey.get_bytes()), [{ 'txid': '0' * 64, 'vout': 0, 'amount': 10e6, 'height': 42, 'iscoinbase': False, 'privatekey': bytes_to_wif(privkey.get_bytes()), }] ) orphan = FromHex(AvalancheProof(), orphan_hex) orphan_proofid = "{:064x}".format(orphan.proofid) msg = msg_avaproof() msg.proof = orphan peer.send_message(msg) wait_for_proof(node, orphan_proofid, expect_orphan=True) def test_ban_invalid_proof(self): node = self.nodes[0] _, bad_proof = gen_proof(node) bad_proof.stakes = [] peer = node.add_p2p_connection(P2PInterface()) msg = msg_avaproof() msg.proof = bad_proof with node.assert_debug_log([ 'Misbehaving', 'invalid-avaproof', ]): peer.send_message(msg) peer.wait_for_disconnect() def test_proof_relay(self): # This test makes no sense with a single node ! assert_greater_than(self.num_nodes, 1) def restart_nodes_with_proof(nodes=self.nodes): proofids = set() for i, node in enumerate(nodes): privkey, proof = gen_proof(node) proofids.add(proof.proofid) self.restart_node(node.index, self.extra_args[node.index] + [ "-avaproof={}".format(proof.serialize().hex()), "-avamasterkey={}".format(bytes_to_wif(privkey.get_bytes())) ]) # Connect a block to make the proof be added to our pool node.generate(1) self.wait_until(lambda: proof.proofid in get_proof_ids(node)) [connect_nodes(node, n) for n in nodes[:i]] return proofids proofids = restart_nodes_with_proof(self.nodes) self.log.info("Nodes should eventually get the proof from their peer") self.sync_proofs() for node in self.nodes: assert_equal(set(get_proof_ids(node)), proofids) def test_manually_sent_proof(self): node0 = self.nodes[0] _, proof = gen_proof(node0) self.log.info( "Send a proof via RPC and check all the nodes download it") node0.sendavalancheproof(proof.serialize().hex()) self.sync_proofs() def test_unbroadcast(self): self.log.info("Test broadcasting proofs") node = self.nodes[0] # Disconnect the other nodes, or they will request the proof and # invalidate the test [node.stop_node() for node in self.nodes[1:]] def add_peers(count): peers = [] for i in range(count): peer = node.add_p2p_connection(ProofInvStoreP2PInterface()) peer.wait_for_verack() peers.append(peer) return peers _, proof = gen_proof(node) proofid_hex = "{:064x}".format(proof.proofid) # Broadcast the proof peers = add_peers(3) assert node.sendavalancheproof(proof.serialize().hex()) wait_for_proof(node, proofid_hex) def proof_inv_received(peers): with p2p_lock: return all(p.last_message.get( "inv") and p.last_message["inv"].inv[-1].hash == proof.proofid for p in peers) self.wait_until(lambda: proof_inv_received(peers)) # If no peer request the proof for download, the node should reattempt # broadcasting to all new peers after 10 to 15 minutes. peers = add_peers(3) node.mockscheduler(MAX_INITIAL_BROADCAST_DELAY + 1) peers[-1].sync_with_ping() self.wait_until(lambda: proof_inv_received(peers)) # If at least one peer requests the proof, there is no more attempt to # broadcast it node.setmocktime(int(time.time()) + UNCONDITIONAL_RELAY_DELAY) msg = msg_getdata([CInv(t=MSG_AVA_PROOF, h=proof.proofid)]) peers[-1].send_message(msg) # Give enough time for the node to broadcast the proof again peers = add_peers(3) node.mockscheduler(MAX_INITIAL_BROADCAST_DELAY + 1) peers[-1].sync_with_ping() assert not proof_inv_received(peers) self.log.info( "Proofs that become invalid should no longer be broadcasted") # Restart and add connect a new set of peers self.restart_node(0) # Broadcast the proof peers = add_peers(3) assert node.sendavalancheproof(proof.serialize().hex()) self.wait_until(lambda: proof_inv_received(peers)) # Sanity check our node knows the proof, and it is valid wait_for_proof(node, proofid_hex, expect_orphan=False) # Mature the utxo then spend it node.generate(100) utxo = proof.stakes[0].stake.utxo raw_tx = node.createrawtransaction( inputs=[{ # coinbase "txid": "{:064x}".format(utxo.hash), "vout": utxo.n }], outputs={ADDRESS_ECREG_UNSPENDABLE: 25_000_000 - 250.00}, ) signed_tx = node.signrawtransactionwithkey( hexstring=raw_tx, privkeys=[node.get_deterministic_priv_key().key], ) node.sendrawtransaction(signed_tx['hex']) # Mine the tx in a block node.generate(1) # Wait for the proof to be orphaned self.wait_until(lambda: node.getrawavalancheproof( proofid_hex)["orphan"] is True) # It should no longer be broadcasted peers = add_peers(3) node.mockscheduler(MAX_INITIAL_BROADCAST_DELAY + 1) peers[-1].sync_with_ping() assert not proof_inv_received(peers) def run_test(self): self.test_send_proof_inv() self.test_receive_proof() self.test_ban_invalid_proof() self.test_proof_relay() self.test_manually_sent_proof() # Run this test last because it needs to disconnect the nodes self.test_unbroadcast() if __name__ == '__main__': ProofInventoryTest().main() diff --git a/test/functional/abc_rpc_addavalanchenode.py b/test/functional/abc_rpc_addavalanchenode.py index 6e1956c31..abe352dda 100644 --- a/test/functional/abc_rpc_addavalanchenode.py +++ b/test/functional/abc_rpc_addavalanchenode.py @@ -1,190 +1,188 @@ #!/usr/bin/env python3 # Copyright (c) 2021 The Bitcoin developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the addavalanchenode RPC""" from test_framework.avatools import create_coinbase_stakes from test_framework.key import ECKey from test_framework.messages import ( AvalancheDelegation, AvalancheDelegationLevel, AvalancheProof, FromHex, hash256, ser_string, ) from test_framework.p2p import P2PInterface from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import ( - assert_raises_rpc_error, -) +from test_framework.util import assert_raises_rpc_error from test_framework.wallet_util import bytes_to_wif def add_interface_node(test_node) -> int: """Create a peer, connect it to test_node, return the nodeid of the peer as registered by test_node. """ n = P2PInterface() test_node.add_p2p_connection(n) n.wait_for_verack() return test_node.getpeerinfo()[-1]['id'] class AddAvalancheNodeTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 self.extra_args = [['-enableavalanche=1', '-avacooldown=0']] def run_test(self): node = self.nodes[0] addrkey0 = node.get_deterministic_priv_key() blockhashes = node.generatetoaddress(2, addrkey0.address) stakes = create_coinbase_stakes(node, [blockhashes[0]], addrkey0.key) privkey = ECKey() privkey.generate() wif_privkey = bytes_to_wif(privkey.get_bytes()) proof_master = privkey.get_pubkey().get_bytes().hex() proof_sequence = 42 proof_expiration = 2000000000 proof = node.buildavalancheproof( proof_sequence, proof_expiration, wif_privkey, stakes) nodeid = add_interface_node(node) def check_addavalanchenode_error( error_code, error_message, nodeid=nodeid, proof=proof, pubkey=proof_master, delegation=None): assert_raises_rpc_error( error_code, error_message, node.addavalanchenode, nodeid, pubkey, proof, delegation, ) self.log.info("Invalid proof") check_addavalanchenode_error(-22, "Proof must be an hexadecimal string", proof="not a proof") check_addavalanchenode_error(-22, "Proof has invalid format", proof="f000") no_stake = node.buildavalancheproof( proof_sequence, proof_expiration, wif_privkey, []) check_addavalanchenode_error(-8, "The proof is invalid: no-stake", proof=no_stake) self.log.info("Node doesn't exist") check_addavalanchenode_error(-8, f"The node does not exist: {nodeid + 1}", nodeid=nodeid + 1) self.log.info("Invalid delegation") dg_privkey = ECKey() dg_privkey.generate() dg_pubkey = dg_privkey.get_pubkey().get_bytes() check_addavalanchenode_error(-22, "Delegation must be an hexadecimal string", pubkey=dg_pubkey.hex(), delegation="not a delegation") check_addavalanchenode_error(-22, "Delegation has invalid format", pubkey=dg_pubkey.hex(), delegation="f000") self.log.info("Delegation mismatch with the proof") delegation_wrong_proofid = AvalancheDelegation() check_addavalanchenode_error(-8, "The delegation does not match the proof", pubkey=dg_pubkey.hex(), delegation=delegation_wrong_proofid.serialize().hex()) proofobj = FromHex(AvalancheProof(), proof) delegation = AvalancheDelegation( limited_proofid=proofobj.limited_proofid, proof_master=proofobj.master, ) self.log.info("Delegation with bad signature") bad_level = AvalancheDelegationLevel( pubkey=dg_pubkey, ) delegation.levels.append(bad_level) check_addavalanchenode_error(-8, "The delegation is invalid", pubkey=dg_pubkey.hex(), delegation=delegation.serialize().hex()) delegation.levels = [] level = AvalancheDelegationLevel( pubkey=dg_pubkey, sig=privkey.sign_schnorr( hash256( delegation.getid() + ser_string(dg_pubkey) ) ) ) delegation.levels.append(level) self.log.info("Key mismatch with the proof") check_addavalanchenode_error( -5, "The public key does not match the proof", pubkey=dg_pubkey.hex(), ) self.log.info("Key mismatch with the delegation") random_privkey = ECKey() random_privkey.generate() random_pubkey = random_privkey.get_pubkey() check_addavalanchenode_error( -5, "The public key does not match the delegation", pubkey=random_pubkey.get_bytes().hex(), delegation=delegation.serialize().hex(), ) self.log.info("Happy path") assert node.addavalanchenode(nodeid, proof_master, proof) # Adding several times is OK assert node.addavalanchenode(nodeid, proof_master, proof) # Use an hardcoded proof. This will help detecting proof format changes. # Generated using: # stakes = create_coinbase_stakes(node, [blockhashes[1]], addrkey0.key) # hardcoded_proof = node.buildavalancheproof( # proof_sequence, proof_expiration, random_pubkey, stakes) hardcoded_pubkey = "037d20fcfe118296bb53f0a8f87c864e7b9831c4fcd7c6a0bb9a58e0e0f53d5cbc" hardcoded_proof = ( "2a00000000000000009435770000000021037d20fcfe118296bb53f0a8f87c864e" "7b9831c4fcd7c6a0bb9a58e0e0f53d5cbc01683ef49024cf25bb55775b327f5e68" "c79da3a7824dc03df5623c96f4a60158f90000000000f902950000000095010000" "210227d85ba011276cf25b51df6a188b75e604b38770a462b2d0e9fb2fc839ef5d" "3f612834ef0e2545d6359e9f34967c2bb69cb88fe246fed716d998f3f62eba1ef6" "6a547606a7ac14c1b5697f4acc20853b3f99954f4f7b6e9bf8a085616d3adfc7" ) assert node.addavalanchenode(nodeid, hardcoded_pubkey, hardcoded_proof) self.log.info("Add a node with a valid delegation") assert node.addavalanchenode( nodeid, dg_pubkey.hex(), proof, delegation.serialize().hex(), ) self.log.info("Several nodes can share a proof") nodeid2 = add_interface_node(node) assert node.addavalanchenode(nodeid2, proof_master, proof) if __name__ == '__main__': AddAvalancheNodeTest().main() diff --git a/test/functional/abc_rpc_avalancheproof.py b/test/functional/abc_rpc_avalancheproof.py index ddfd3a24b..e3bcadb67 100644 --- a/test/functional/abc_rpc_avalancheproof.py +++ b/test/functional/abc_rpc_avalancheproof.py @@ -1,481 +1,481 @@ #!/usr/bin/env python3 # Copyright (c) 2021 The Bitcoin developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test building avalanche proofs and using them to add avalanche peers.""" import base64 from decimal import Decimal from test_framework.address import ADDRESS_ECREG_UNSPENDABLE from test_framework.avatools import ( create_coinbase_stakes, create_stakes, get_proof_ids, wait_for_proof, ) from test_framework.key import ECKey from test_framework.messages import ( AvalancheDelegation, AvalancheDelegationLevel, AvalancheProof, FromHex, ) from test_framework.p2p import P2PInterface, p2p_lock from test_framework.test_framework import BitcoinTestFramework from test_framework.test_node import ErrorMatch from test_framework.util import ( append_config, assert_equal, - connect_nodes, assert_raises_rpc_error, + connect_nodes, ) from test_framework.wallet_util import bytes_to_wif AVALANCHE_MAX_PROOF_STAKES = 1000 PROOF_DUST_THRESHOLD = 1000000.0 """Minimum amount per UTXO in a proof (in coins, not in satoshis)""" def add_interface_node(test_node) -> str: """Create a mininode, connect it to test_node, return the nodeid of the mininode as registered by test_node. """ n = P2PInterface() test_node.add_p2p_connection(n) n.wait_for_verack() return test_node.getpeerinfo()[-1]['id'] class AvalancheProofTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 2 self.extra_args = [['-enableavalanche=1', '-avacooldown=0'], ['-enableavalanche=1', '-avacooldown=0']] self.supports_cli = False self.rpc_timeout = 120 def run_test(self): # Turn off node 1 while node 0 mines blocks to generate stakes, # so that we can later try starting node 1 with an orphan proof. self.stop_node(1) node = self.nodes[0] addrkey0 = node.get_deterministic_priv_key() blockhashes = node.generatetoaddress(100, addrkey0.address) self.log.info( "Make build a valid proof and restart the node to use it") privkey = ECKey() privkey.set(bytes.fromhex( "12b004fff7f4b69ef8650e767f18f11ede158148b425660723b9f9a66e61f747"), True) wif_privkey = bytes_to_wif(privkey.get_bytes()) def get_hex_pubkey(privkey): return privkey.get_pubkey().get_bytes().hex() proof_master = get_hex_pubkey(privkey) proof_sequence = 11 proof_expiration = 12 stakes = create_coinbase_stakes(node, [blockhashes[0]], addrkey0.key) proof = node.buildavalancheproof( proof_sequence, proof_expiration, wif_privkey, stakes) self.log.info("Test decodeavalancheproof RPC") proofobj = FromHex(AvalancheProof(), proof) decodedproof = node.decodeavalancheproof(proof) limited_id_hex = f"{proofobj.limited_proofid:0{64}x}" assert_equal(decodedproof["sequence"], proof_sequence) assert_equal(decodedproof["expiration"], proof_expiration) assert_equal(decodedproof["master"], proof_master) assert_equal(decodedproof["proofid"], f"{proofobj.proofid:0{64}x}") assert_equal(decodedproof["limitedid"], limited_id_hex) assert_equal(decodedproof["stakes"][0]["txid"], stakes[0]["txid"]) assert_equal(decodedproof["stakes"][0]["vout"], stakes[0]["vout"]) assert_equal(decodedproof["stakes"][0]["height"], stakes[0]["height"]) assert_equal( decodedproof["stakes"][0]["iscoinbase"], stakes[0]["iscoinbase"]) assert_equal( decodedproof["stakes"][0]["signature"], base64.b64encode(proofobj.stakes[0].sig).decode("ascii")) # Invalid hex (odd number of hex digits) assert_raises_rpc_error(-22, "Proof must be an hexadecimal string", node.decodeavalancheproof, proof[:-1]) # Valid hex but invalid proof assert_raises_rpc_error(-22, "Proof has invalid format", node.decodeavalancheproof, proof[:-2]) # Restart the node with this proof self.restart_node(0, self.extra_args[0] + [ "-avaproof={}".format(proof), "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", ]) self.log.info("The proof is registered at first chaintip update") assert_equal(len(node.getavalanchepeerinfo()), 0) node.generate(1) self.wait_until(lambda: len(node.getavalanchepeerinfo()) == 1, timeout=5) # This case will occur for users building proofs with a third party # tool and then starting a new node that is not yet aware of the # transactions used for stakes. self.log.info("Start a node with an orphan proof") self.start_node(1, self.extra_args[0] + [ "-avaproof={}".format(proof), "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", ]) # Mine a block to trigger an attempt at registering the proof self.nodes[1].generate(1) wait_for_proof(self.nodes[1], f"{proofobj.proofid:0{64}x}", expect_orphan=True) self.log.info("Connect to an up-to-date node to unorphan the proof") connect_nodes(self.nodes[1], node) self.sync_all() wait_for_proof(self.nodes[1], f"{proofobj.proofid:0{64}x}", expect_orphan=False) self.log.info("Generate delegations for the proof") # Stack up a few delegation levels def gen_privkey(): pk = ECKey() pk.generate() return pk delegator_privkey = privkey delegation = None for _ in range(10): delegated_privkey = gen_privkey() delegation = node.delegateavalancheproof( limited_id_hex, bytes_to_wif(delegator_privkey.get_bytes()), get_hex_pubkey(delegated_privkey), delegation, ) delegator_privkey = delegated_privkey random_privkey = gen_privkey() random_pubkey = get_hex_pubkey(random_privkey) # Invalid proof no_stake = node.buildavalancheproof(proof_sequence, proof_expiration, wif_privkey, []) # Invalid privkey assert_raises_rpc_error(-5, "The private key is invalid", node.delegateavalancheproof, limited_id_hex, bytes_to_wif(bytes(32)), random_pubkey, ) # Invalid delegation bad_dg = AvalancheDelegation() assert_raises_rpc_error(-8, "The delegation does not match the proof", node.delegateavalancheproof, limited_id_hex, bytes_to_wif(privkey.get_bytes()), random_pubkey, bad_dg.serialize().hex(), ) # Still invalid, but with a matching proofid bad_dg.limited_proofid = proofobj.limited_proofid bad_dg.proof_master = proofobj.master bad_dg.levels = [AvalancheDelegationLevel()] assert_raises_rpc_error(-8, "The delegation is invalid", node.delegateavalancheproof, limited_id_hex, bytes_to_wif(privkey.get_bytes()), random_pubkey, bad_dg.serialize().hex(), ) # Wrong privkey, match the proof but does not match the delegation assert_raises_rpc_error(-5, "The private key does not match the delegation", node.delegateavalancheproof, limited_id_hex, bytes_to_wif(privkey.get_bytes()), random_pubkey, delegation, ) # Delegation not hex assert_raises_rpc_error(-22, "Delegation must be an hexadecimal string.", node.delegateavalancheproof, limited_id_hex, bytes_to_wif(privkey.get_bytes()), random_pubkey, "f00", ) # Delegation is hex but ill-formed assert_raises_rpc_error(-22, "Delegation has invalid format", node.delegateavalancheproof, limited_id_hex, bytes_to_wif(privkey.get_bytes()), random_pubkey, "dead", ) # Test invalid proofs dust = node.buildavalancheproof( proof_sequence, proof_expiration, wif_privkey, create_coinbase_stakes(node, [blockhashes[0]], addrkey0.key, amount="0")) dust_amount = Decimal(f"{PROOF_DUST_THRESHOLD * 0.9999:.4f}") dust2 = node.buildavalancheproof( proof_sequence, proof_expiration, wif_privkey, create_coinbase_stakes(node, [blockhashes[0]], addrkey0.key, amount=str(dust_amount))) missing_stake = node.buildavalancheproof( proof_sequence, proof_expiration, wif_privkey, [{ 'txid': '0' * 64, 'vout': 0, 'amount': 10000000, 'height': 42, 'iscoinbase': False, 'privatekey': addrkey0.key, }] ) duplicate_stake = ("0b000000000000000c0000000000000021030b4c866585dd868" "a9d62348a9cd008d6a312937048fff31670e7e920cfc7a74402" "05c5f72f5d6da3085583e75ee79340eb4eff208c89988e7ed0e" "fb30b87298fa30000000000f2052a0100000003000000210227" "d85ba011276cf25b51df6a188b75e604b38770a462b2d0e9fb2" "fc839ef5d3f86076def2e8bc3c40671c1a0eb505da5857a950a" "0cf4625a80018cdd75ac62e61273ff8142f747de67e73f6368c" "8648942b0ef6c065d72a81ad7438a23c11cca05c5f72f5d6da3" "085583e75ee79340eb4eff208c89988e7ed0efb30b87298fa30" "000000000f2052a0100000003000000210227d85ba011276cf2" "5b51df6a188b75e604b38770a462b2d0e9fb2fc839ef5d3f860" "76def2e8bc3c40671c1a0eb505da5857a950a0cf4625a80018c" "dd75ac62e61273ff8142f747de67e73f6368c8648942b0ef6c0" "65d72a81ad7438a23c11cca") bad_sig = ("0b000000000000000c0000000000000021030b4c866585dd868a9d62348" "a9cd008d6a312937048fff31670e7e920cfc7a7440105c5f72f5d6da3085" "583e75ee79340eb4eff208c89988e7ed0efb30b87298fa30000000000f20" "52a0100000003000000210227d85ba011276cf25b51df6a188b75e604b3" "8770a462b2d0e9fb2fc839ef5d3faf07f001dd38e9b4a43d07d5d449cc0" "f7d2888d96b82962b3ce516d1083c0e031773487fc3c4f2e38acd1db974" "1321b91a79b82d1c2cfd47793261e4ba003cf5") wrong_order = ("c964aa6fde575e4ce8404581c7be874e21023beefdde700a6bc0203" "6335b4df141c8bc67bb05a971f5ac2745fd683797dde30305d427b7" "06705a5d4b6a368a231d6db62abacf8c29bc32b61e7f65a0a6976aa" "8b86b687bc0260e821e4f0200b9d3bf6d2102449fb5237efe8f647d" "32e8b64f06c22d1d40368eaca2a71ffc6a13ecc8bce68052365271b" "6c71189f5cd7e3b694b77b579080f0b35bae567b96590ab6aa3019b" "018ff9f061f52f1426bdb195d4b6d4dff5114cee90e33dabf0c588e" "badf7774418f54247f6390791706af36fac782302479898b5273f9e" "51a92cb1fb5af43deeb6c8c269403d30ffcb380300134398c42103e" "49f9df52de2dea81cf7838b82521b69f2ea360f1c4eed9e6c89b7d0" "f9e645efa08e97ea0c60e1f0a064fbf08989c084707082727e85dcb" "9f79bb503f76ee6c8dad42a07ef15c89b3750a5631d604b21fafff0" "f4de354ade95c2f28160ae549af0d4ce48c4ca9d0714b1fa5192027" "0f8575e0af610f07b4e602a018ecdbb649b64fff614c0026e9fc8e0" "030092533d422103aac52f4cfca700e7e9824298e0184755112e32f" "359c832f5f6ad2ef62a2c024af812d6d7f2ecc6223a774e19bce1fb" "20d94d6b01ea693638f55c74fdaa5358fa9239d03e4caf3d817e8f7" "48ccad55a27b9d365db06ad5a0b779ac385f3dc8710") self.log.info( "Check the verifyavalancheproof and sendavalancheproof RPCs") if self.is_wallet_compiled(): self.log.info( "Check a proof with the maximum number of UTXO is valid") new_blocks = node.generate(AVALANCHE_MAX_PROOF_STAKES // 10 + 1) # confirm the coinbase UTXOs node.generate(101) too_many_stakes = create_stakes( node, new_blocks, AVALANCHE_MAX_PROOF_STAKES + 1) maximum_stakes = too_many_stakes[:-1] good_proof = node.buildavalancheproof( proof_sequence, proof_expiration, wif_privkey, maximum_stakes) too_many_utxos = node.buildavalancheproof( proof_sequence, proof_expiration, wif_privkey, too_many_stakes) assert node.verifyavalancheproof(good_proof) for rpc in [node.verifyavalancheproof, node.sendavalancheproof]: assert_raises_rpc_error(-22, "Proof must be an hexadecimal string", rpc, "f00") assert_raises_rpc_error(-22, "Proof has invalid format", rpc, "f00d") def check_rpc_failure(proof, message): assert_raises_rpc_error(-8, "The proof is invalid: " + message, rpc, proof) check_rpc_failure(no_stake, "no-stake") check_rpc_failure(dust, "amount-below-dust-threshold") check_rpc_failure(duplicate_stake, "duplicated-stake") check_rpc_failure(missing_stake, "utxo-missing-or-spent") check_rpc_failure(bad_sig, "invalid-stake-signature") check_rpc_failure(wrong_order, "wrong-stake-ordering") if self.is_wallet_compiled(): check_rpc_failure(too_many_utxos, "too-many-utxos") conflicting_utxo = node.buildavalancheproof( proof_sequence + 1, proof_expiration, wif_privkey, stakes) assert_raises_rpc_error(-8, "The proof has conflicting utxo with an existing proof", node.sendavalancheproof, conflicting_utxo) # Good proof assert node.verifyavalancheproof(proof) peer = node.add_p2p_connection(P2PInterface()) proofid = FromHex(AvalancheProof(), proof).proofid node.sendavalancheproof(proof) assert proofid in get_proof_ids(node) def inv_found(): with p2p_lock: return peer.last_message.get( "inv") and peer.last_message["inv"].inv[-1].hash == proofid self.wait_until(inv_found) self.log.info("Check the getrawproof RPC") raw_proof = node.getrawavalancheproof("{:064x}".format(proofid)) assert_equal(raw_proof['proof'], proof) assert_equal(raw_proof['orphan'], False) assert_raises_rpc_error(-8, "Proof not found", node.getrawavalancheproof, '0' * 64) # Orphan the proof by sending the stake raw_tx = node.createrawtransaction( [{"txid": stakes[-1]["txid"], "vout": 0}], {ADDRESS_ECREG_UNSPENDABLE: stakes[-1] ["amount"] - Decimal('10000')} ) signed_tx = node.signrawtransactionwithkey(raw_tx, [addrkey0.key]) node.sendrawtransaction(signed_tx["hex"]) node.generate(1) self.wait_until(lambda: proofid not in get_proof_ids(node)) raw_proof = node.getrawavalancheproof("{:064x}".format(proofid)) assert_equal(raw_proof['proof'], proof) assert_equal(raw_proof['orphan'], True) self.log.info("Bad proof should be rejected at startup") self.stop_node(0) node.assert_start_raises_init_error( self.extra_args[0] + [ "-avasessionkey=0", ], expected_msg="Error: The avalanche session key is invalid.", ) node.assert_start_raises_init_error( self.extra_args[0] + [ "-avaproof={}".format(proof), ], expected_msg="Error: The avalanche master key is missing for the avalanche proof.", ) node.assert_start_raises_init_error( self.extra_args[0] + [ "-avaproof={}".format(proof), "-avamasterkey=0", ], expected_msg="Error: The avalanche master key is invalid.", ) def check_proof_init_error(proof, message): node.assert_start_raises_init_error( self.extra_args[0] + [ "-avaproof={}".format(proof), "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", ], expected_msg="Error: " + message, ) check_proof_init_error(no_stake, "The avalanche proof has no stake.") check_proof_init_error(dust, "The avalanche proof stake is too low.") check_proof_init_error(dust2, "The avalanche proof stake is too low.") check_proof_init_error(duplicate_stake, "The avalanche proof has duplicated stake.") check_proof_init_error(bad_sig, "The avalanche proof has invalid stake signatures.") if self.is_wallet_compiled(): # The too many utxos case creates a proof which is that large that it # cannot fit on the command line append_config(node.datadir, ["avaproof={}".format(too_many_utxos)]) node.assert_start_raises_init_error( self.extra_args[0] + [ "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", ], expected_msg="Error: The avalanche proof has too many utxos.", match=ErrorMatch.PARTIAL_REGEX, ) # Master private key mismatch random_privkey = ECKey() random_privkey.generate() node.assert_start_raises_init_error( self.extra_args[0] + [ "-avaproof={}".format(proof), "-avamasterkey={}".format( bytes_to_wif(random_privkey.get_bytes())), ], expected_msg="Error: The master key does not match the proof public key.", ) self.log.info("Bad delegation should be rejected at startup") def check_delegation_init_error(delegation, message): node.assert_start_raises_init_error( self.extra_args[0] + [ "-avadelegation={}".format(delegation), "-avaproof={}".format(proof), "-avamasterkey={}".format( bytes_to_wif(delegated_privkey.get_bytes())), ], expected_msg="Error: " + message, ) check_delegation_init_error( AvalancheDelegation().serialize().hex(), "The delegation does not match the proof.") bad_level_sig = FromHex(AvalancheDelegation(), delegation) # Tweak some key to cause the signature to mismatch bad_level_sig.levels[-2].pubkey = bytes.fromhex(proof_master) check_delegation_init_error(bad_level_sig.serialize().hex(), "The avalanche delegation has invalid signatures.") node.assert_start_raises_init_error( self.extra_args[0] + [ "-avadelegation={}".format(delegation), "-avaproof={}".format(proof), "-avamasterkey={}".format( bytes_to_wif(random_privkey.get_bytes())), ], expected_msg="Error: The master key does not match the delegation public key.", ) if __name__ == '__main__': AvalancheProofTest().main() diff --git a/test/functional/abc_rpc_buildavalancheproof.py b/test/functional/abc_rpc_buildavalancheproof.py index 9104953ad..97f73935a 100644 --- a/test/functional/abc_rpc_buildavalancheproof.py +++ b/test/functional/abc_rpc_buildavalancheproof.py @@ -1,135 +1,133 @@ #!/usr/bin/env python3 # Copyright (c) 2021 The Bitcoin developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the buildavalancheproof RPC""" from test_framework.address import ADDRESS_ECREG_UNSPENDABLE from test_framework.avatools import create_coinbase_stakes from test_framework.key import ECKey from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import ( - assert_raises_rpc_error, -) +from test_framework.util import assert_raises_rpc_error from test_framework.wallet_util import bytes_to_wif class BuildAvalancheProofTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 self.extra_args = [['-enableavalanche=1', '-avacooldown=0']] def run_test(self): node = self.nodes[0] addrkey0 = node.get_deterministic_priv_key() blockhashes = node.generatetoaddress(2, addrkey0.address) stakes = create_coinbase_stakes(node, [blockhashes[0]], addrkey0.key) privkey = ECKey() privkey.generate() wif_privkey = bytes_to_wif(privkey.get_bytes()) def check_buildavalancheproof_error( error_code, error_message, stakes, master_key=wif_privkey): assert_raises_rpc_error( error_code, error_message, node.buildavalancheproof, # Sequence 0, # Expiration 0, master_key, stakes, ) good_stake = stakes[0] self.log.info("Error cases") check_buildavalancheproof_error(-8, "Invalid master key", [good_stake], master_key=bytes_to_wif(b'f00') ) negative_vout = good_stake.copy() negative_vout['vout'] = -1 check_buildavalancheproof_error(-22, "vout must be positive", [negative_vout], ) zero_height = good_stake.copy() zero_height['height'] = 0 check_buildavalancheproof_error(-22, "height must be positive", [zero_height], ) negative_height = good_stake.copy() negative_height['height'] = -1 check_buildavalancheproof_error(-22, "height must be positive", [negative_height], ) missing_amount = good_stake.copy() del missing_amount['amount'] check_buildavalancheproof_error(-8, "Missing amount", [missing_amount], ) invalid_privkey = good_stake.copy() invalid_privkey['privatekey'] = 'foobar' check_buildavalancheproof_error(-8, "Invalid private key", [invalid_privkey], ) duplicate_stake = [good_stake] * 2 check_buildavalancheproof_error(-8, "Duplicated stake", duplicate_stake, ) self.log.info("Happy path") assert node.buildavalancheproof(0, 0, wif_privkey, [good_stake]) self.log.info("Check the payout address") self.restart_node( 0, extra_args=self.extra_args[0] + ['-legacyavaproof=0']) assert_raises_rpc_error(-8, "A payout address is required if `-legacyavaproof` is false", node.buildavalancheproof, 0, 0, wif_privkey, [good_stake], ) assert_raises_rpc_error(-8, "Invalid payout address", node.buildavalancheproof, 0, 0, wif_privkey, [good_stake], "ecregtest:qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqcrl5mqkq", ) # Happy path node.buildavalancheproof( 0, 0, wif_privkey, [good_stake], ADDRESS_ECREG_UNSPENDABLE) if __name__ == '__main__': BuildAvalancheProofTest().main() diff --git a/test/functional/abc_rpc_getavalanchepeerinfo.py b/test/functional/abc_rpc_getavalanchepeerinfo.py index 2fd386ac7..e97a89813 100755 --- a/test/functional/abc_rpc_getavalanchepeerinfo.py +++ b/test/functional/abc_rpc_getavalanchepeerinfo.py @@ -1,72 +1,72 @@ #!/usr/bin/env python3 # Copyright (c) 2020-2021 The Bitcoin developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the getavalanchepeerinfo RPC.""" from test_framework.avatools import ( - get_ava_p2p_interface, create_coinbase_stakes, + get_ava_p2p_interface, ) from test_framework.key import ECKey from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal from test_framework.wallet_util import bytes_to_wif class GetAvalanchePeerInfoTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 self.extra_args = [['-enableavalanche=1', '-avacooldown=0']] def run_test(self): node = self.nodes[0] peercount = 5 nodecount = 10 self.log.info( f"Generating {peercount} peers with {nodecount} nodes each") addrkey0 = node.get_deterministic_priv_key() blockhashes = node.generatetoaddress(peercount, addrkey0.address) # Use the first coinbase to create a stake stakes = create_coinbase_stakes(node, blockhashes, addrkey0.key) def getProof(stake): privkey = ECKey() privkey.generate() pubkey = privkey.get_pubkey() proof_sequence = 11 proof_expiration = 12 proof = node.buildavalancheproof( proof_sequence, proof_expiration, bytes_to_wif( privkey.get_bytes()), [stake]) return (pubkey.get_bytes().hex(), proof) # Create peercount * nodecount node array nodes = [[get_ava_p2p_interface(node) for n in range( nodecount)] for p in range(peercount)] # Add peercount peers and bind all the nodes to each proofs = [] for i in range(peercount): pubkey_hex, proof = getProof(stakes[i]) proofs.append(proof) [node.addavalanchenode(n.nodeid, pubkey_hex, proof) for n in nodes[i]] self.log.info("Testing getavalanchepeerinfo...") avapeerinfo = node.getavalanchepeerinfo() assert_equal(len(avapeerinfo), peercount) for i, peer in enumerate(avapeerinfo): assert_equal(peer["peerid"], i) assert_equal(peer["proof"], proofs[i]) assert_equal(peer["nodecount"], nodecount) assert_equal(set(peer["nodes"]), set([n.nodeid for n in nodes[i]])) if __name__ == '__main__': GetAvalanchePeerInfoTest().main() diff --git a/test/functional/abc_wallet_dumpcoins.py b/test/functional/abc_wallet_dumpcoins.py index 7a006d02c..375be4447 100755 --- a/test/functional/abc_wallet_dumpcoins.py +++ b/test/functional/abc_wallet_dumpcoins.py @@ -1,72 +1,72 @@ #!/usr/bin/env python3 # Copyright (c) 2020 The Bitcoin developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """ Test the dumpcoins RPCs call. """ +from decimal import Decimal + from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal -from decimal import Decimal - class DumpCoinsTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 self.setup_clean_chain = True def skip_test_if_missing_module(self): self.skip_if_no_wallet() def run_test(self): node = self.nodes[0] assert_equal(node.dumpcoins(), {}) address0 = node.getnewaddress() address1 = node.getnewaddress() coinbases = [] def generate_and_get_txid(address, expected_coins): blockhash = node.generatetoaddress(1, address)[0] assert_equal(node.dumpcoins(), expected_coins) # Get the coinbase txid coinbases.append(node.getblock(blockhash)["tx"][0]) generate_and_get_txid(address0, {}) for _ in range(99): generate_and_get_txid(address1, {}) # Coinbases reach maturity and start to show up. generate_and_get_txid(address1, { address0: [{ "txid": coinbases[0], "vout": 0, "depth": 101, "value": Decimal('50000000.00'), }], }) # And now on address1 generate_and_get_txid(address1, { address0: [{ "txid": coinbases[0], "vout": 0, "depth": 102, "value": Decimal('50000000.00'), }], address1: [{ "txid": coinbases[1], "vout": 0, "depth": 101, "value": Decimal('50000000.00'), }], }) if __name__ == '__main__': DumpCoinsTest().main() diff --git a/test/functional/abc_wallet_standardness.py b/test/functional/abc_wallet_standardness.py index 4919c4bbf..4b1380aac 100755 --- a/test/functional/abc_wallet_standardness.py +++ b/test/functional/abc_wallet_standardness.py @@ -1,194 +1,187 @@ #!/usr/bin/env python3 # Copyright (c) 2019 The Bitcoin developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the response of wallet to a variety of weird / nonstandard coins that it might try to spend.""" from decimal import Decimal -from test_framework.messages import ( - CTransaction, - CTxOut, - FromHex, - ToHex, -) + +from test_framework.messages import CTransaction, CTxOut, FromHex, ToHex from test_framework.script import ( - CScript, OP_1, OP_5, - OP_CHECKSIG, OP_CHECKMULTISIG, + OP_CHECKSIG, OP_DUP, OP_EQUALVERIFY, OP_HASH160, OP_PUSHDATA1, + CScript, hash160, ) from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import ( - assert_raises_rpc_error, - assert_equal, -) +from test_framework.util import assert_equal, assert_raises_rpc_error SATOSHI = Decimal('0.01') class WalletStandardnessTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 2 self.extra_args = [['-acceptnonstdtxn=0'], ['-acceptnonstdtxn=1']] def skip_test_if_missing_module(self): self.skip_if_no_wallet() def run_test(self): std_node, nonstd_node = self.nodes address_nonstd = nonstd_node.getnewaddress() # make and mature some coins for the nonstandard node nonstd_node.generate(120) self.sync_blocks() def fund_and_test_wallet(scriptPubKey, is_standard, expected_in_std_wallet, amount=10000, spendfee=500, nonstd_error="scriptpubkey", sign_error=None): """ Get the nonstandard node to fund a transaction, test its standardness by trying to broadcast on the standard node, then mine it and see if it ended up in the standard node's wallet. Finally, it attempts to spend the coin. """ self.log.info("Trying script {}".format(scriptPubKey.hex(),)) # get nonstandard node to fund the script tx = CTransaction() tx.vout.append(CTxOut(max(amount, 10000), scriptPubKey)) rawtx = nonstd_node.fundrawtransaction( ToHex(tx), {'lockUnspents': True, 'changePosition': 1})['hex'] # fundrawtransaction doesn't like to fund dust outputs, so we # have to manually override the amount. FromHex(tx, rawtx) tx.vout[0].nValue = min(amount, 10000) rawtx = nonstd_node.signrawtransactionwithwallet(ToHex(tx))['hex'] # ensure signing process did not disturb scriptPubKey signedtx = FromHex(CTransaction(), rawtx) assert_equal(scriptPubKey, signedtx.vout[0].scriptPubKey) txid = signedtx.rehash() balance_initial = std_node.getbalance() # try broadcasting it on the standard node if is_standard: std_node.sendrawtransaction(rawtx) assert txid in std_node.getrawmempool() else: assert_raises_rpc_error(-26, nonstd_error, std_node.sendrawtransaction, rawtx) assert txid not in std_node.getrawmempool() # make sure it's in nonstandard node's mempool, then mine it nonstd_node.sendrawtransaction(rawtx) assert txid in nonstd_node.getrawmempool() [blockhash] = nonstd_node.generate(1) # make sure it was mined assert txid in nonstd_node.getblock(blockhash)["tx"] self.sync_blocks() wallet_outpoints = {(entry['txid'], entry['vout']) for entry in std_node.listunspent()} # calculate wallet balance change just as a double check balance_change = std_node.getbalance() - balance_initial if expected_in_std_wallet: assert (txid, 0) in wallet_outpoints assert balance_change == amount * SATOSHI else: assert (txid, 0) not in wallet_outpoints assert balance_change == 0 # try spending the funds using the wallet. outamount = (amount - spendfee) * SATOSHI if outamount < 546 * SATOSHI: # If the final amount would be too small, then just donate # to miner fees. outputs = [{"data": b"to miner, with love".hex()}] else: outputs = [{address_nonstd: outamount}] spendtx = std_node.createrawtransaction( [{'txid': txid, 'vout': 0}], outputs) signresult = std_node.signrawtransactionwithwallet(spendtx) if sign_error is None: assert_equal(signresult['complete'], True) txid = std_node.sendrawtransaction(signresult['hex']) [blockhash] = std_node.generate(1) # make sure it was mined assert txid in std_node.getblock(blockhash)["tx"] self.sync_blocks() else: assert_equal(signresult['complete'], False) assert_equal(signresult['errors'][0]['error'], sign_error) # we start with an empty wallet assert_equal(std_node.getbalance(), 0) address = std_node.getnewaddress() pubkey = bytes.fromhex(std_node.getaddressinfo(address)['pubkey']) pubkeyhash = hash160(pubkey) # P2PK fund_and_test_wallet(CScript([pubkey, OP_CHECKSIG]), True, True) fund_and_test_wallet( CScript([OP_PUSHDATA1, pubkey, OP_CHECKSIG]), False, False, sign_error='Data push larger than necessary') # P2PKH fund_and_test_wallet(CScript( [OP_DUP, OP_HASH160, pubkeyhash, OP_EQUALVERIFY, OP_CHECKSIG]), True, True) # The signing error changes here since the script check (with empty # scriptSig) hits OP_DUP before it hits the nonminimal push; in all # other cases we hit the nonminimal push first. fund_and_test_wallet(CScript( [OP_DUP, OP_HASH160, OP_PUSHDATA1, pubkeyhash, OP_EQUALVERIFY, OP_CHECKSIG]), False, False, sign_error='Unable to sign input, invalid stack size (possibly missing key)') # Bare multisig fund_and_test_wallet( CScript([OP_1, pubkey, OP_1, OP_CHECKMULTISIG]), True, False) fund_and_test_wallet( CScript([OP_1, OP_PUSHDATA1, pubkey, OP_1, OP_CHECKMULTISIG]), False, False, sign_error='Data push larger than necessary') fund_and_test_wallet( CScript([OP_1, pubkey, b'\x01', OP_CHECKMULTISIG]), False, False, sign_error='Data push larger than necessary') fund_and_test_wallet( CScript([b'\x01', pubkey, OP_1, OP_CHECKMULTISIG]), False, False, sign_error='Data push larger than necessary') # Note: 1-of-5 is nonstandard to fund yet is standard to spend. However, # trying to spend it with our wallet in particular will generate # too-dense sigchecks since our wallet currently only signs with ECDSA # (Schnorr would not have this issue). fund_and_test_wallet( CScript([OP_1, pubkey, pubkey, pubkey, pubkey, pubkey, OP_5, OP_CHECKMULTISIG]), False, False, sign_error='Input SigChecks limit exceeded') fund_and_test_wallet( CScript([OP_1, pubkey, pubkey, pubkey, OP_PUSHDATA1, pubkey, pubkey, OP_5, OP_CHECKMULTISIG]), False, False, sign_error='Data push larger than necessary') # Dust also is nonstandard to fund but standard to spend. fund_and_test_wallet( CScript([pubkey, OP_CHECKSIG]), False, True, amount=200, nonstd_error="dust") # and we end with an empty wallet assert_equal(std_node.getbalance(), 0) if __name__ == '__main__': WalletStandardnessTest().main()