diff --git a/test/functional/abc_p2p_avalanche.py b/test/functional/abc_p2p_avalanche.py --- a/test/functional/abc_p2p_avalanche.py +++ b/test/functional/abc_p2p_avalanche.py @@ -5,7 +5,7 @@ """Test the resolution of forks via avalanche.""" import random -from test_framework.avatools import get_stakes +from test_framework.avatools import create_coinbase_stakes from test_framework.key import ( ECKey, ECPubKey, @@ -147,7 +147,7 @@ addrkey0 = node.get_deterministic_priv_key() blockhashes = node.generatetoaddress(100, addrkey0.address) # Use the first coinbase to create a stake - stakes = get_stakes(node, [blockhashes[0]], addrkey0.key) + stakes = create_coinbase_stakes(node, [blockhashes[0]], addrkey0.key) fork_node = self.nodes[1] # Make sure the fork node has synced the blocks diff --git a/test/functional/abc_rpc_avalancheproof.py b/test/functional/abc_rpc_avalancheproof.py --- a/test/functional/abc_rpc_avalancheproof.py +++ b/test/functional/abc_rpc_avalancheproof.py @@ -4,7 +4,10 @@ # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test building avalanche proofs and using them to add avalanche peers.""" -from test_framework.avatools import get_stakes +from test_framework.avatools import ( + create_coinbase_stakes, + create_stakes, +) from test_framework.key import ECKey, bytes_to_wif from test_framework.messages import AvalancheDelegation from test_framework.mininode import P2PInterface @@ -57,7 +60,7 @@ proof_expiration = 12 proof = node.buildavalancheproof( proof_sequence, proof_expiration, proof_master, - get_stakes(node, [blockhashes[0]], addrkey0.key)) + create_coinbase_stakes(node, [blockhashes[0]], addrkey0.key)) # Restart the node, making sure it is initially in IBD mode minchainwork = int(node.getblockchaininfo()["chainwork"], 16) + 1 @@ -78,25 +81,30 @@ assert node.getblockchaininfo()["initialblockdownload"] is False wait_until(lambda: len(node.getavalanchepeerinfo()) == 1, timeout=5) - self.log.info( - "A proof using the maximum number of stakes is accepted...") - blockhashes = node.generatetoaddress(AVALANCHE_MAX_PROOF_STAKES + 1, - addrkey0.address) - - too_many_stakes = get_stakes(node, blockhashes, addrkey0.key) - maximum_stakes = get_stakes(node, blockhashes[:-1], addrkey0.key) - good_proof = node.buildavalancheproof( - proof_sequence, proof_expiration, - proof_master, maximum_stakes) - peerid1 = add_interface_node(node) - assert node.addavalanchenode(peerid1, proof_master, good_proof) - - self.log.info("A proof using too many stakes should be rejected...") - too_many_utxos = node.buildavalancheproof( - proof_sequence, proof_expiration, - proof_master, too_many_stakes) - peerid2 = add_interface_node(node) - assert not node.addavalanchenode(peerid2, proof_master, too_many_utxos) + if self.is_wallet_compiled(): + self.log.info( + "A proof using the maximum number of stakes is accepted...") + + 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, + proof_master, maximum_stakes) + peerid1 = add_interface_node(node) + assert node.addavalanchenode(peerid1, proof_master, good_proof) + + self.log.info( + "A proof using too many stakes should be rejected...") + too_many_utxos = node.buildavalancheproof( + proof_sequence, proof_expiration, + proof_master, too_many_stakes) + peerid2 = add_interface_node(node) + assert not node.addavalanchenode( + peerid2, proof_master, too_many_utxos) self.log.info("Generate delegations for the proof") @@ -122,9 +130,11 @@ random_pubkey = get_hex_pubkey(random_privkey) # Invalid proof + no_stake = node.buildavalancheproof( + proof_sequence, proof_expiration, proof_master, []) assert_raises_rpc_error(-8, "The proof is invalid", node.delegateavalancheproof, - too_many_utxos, + no_stake, bytes_to_wif(privkey.get_bytes()), random_pubkey, ) @@ -166,16 +176,13 @@ # Test invalid proofs self.log.info("Bad proof should be rejected at startup") - no_stake = node.buildavalancheproof( - proof_sequence, proof_expiration, proof_master, []) - dust = node.buildavalancheproof( proof_sequence, proof_expiration, proof_master, - get_stakes(node, [blockhashes[0]], addrkey0.key, amount="0")) + create_coinbase_stakes(node, [blockhashes[0]], addrkey0.key, amount="0")) duplicate_stake = node.buildavalancheproof( proof_sequence, proof_expiration, proof_master, - get_stakes(node, [blockhashes[0]] * 2, addrkey0.key)) + create_coinbase_stakes(node, [blockhashes[0]] * 2, addrkey0.key)) bad_sig = ("0b000000000000000c0000000000000021030b4c866585dd868a9d62348" "a9cd008d6a312937048fff31670e7e920cfc7a7440105c5f72f5d6da3085" @@ -226,16 +233,17 @@ "the avalanche proof has duplicated stake") check_proof_init_error(bad_sig, "the avalanche proof has invalid stake signatures") - # 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, - ) + 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, + ) if __name__ == '__main__': diff --git a/test/functional/test_framework/avatools.py b/test/functional/test_framework/avatools.py --- a/test/functional/test_framework/avatools.py +++ b/test/functional/test_framework/avatools.py @@ -6,15 +6,23 @@ from typing import Any, Optional, List, Dict +from .messages import ( + CTransaction, + FromHex, + ToHex +) from .test_node import TestNode +from .util import satoshi_round -def get_stakes(node: TestNode, - blockhashes: List[str], - priv_key: str, - amount: Optional[str] = None) -> List[Dict[str, Any]]: +def create_coinbase_stakes( + node: TestNode, + blockhashes: List[str], + priv_key: str, + amount: Optional[str] = None) -> List[Dict[str, Any]]: """Returns a list of dictionaries representing stakes, in a format - compatible with the buildavalancheproof RPC. + compatible with the buildavalancheproof RPC, using only coinbase + transactions. :param node: Test node used to get the block and coinbase data. :param blockhashes: List of block hashes, whose coinbase tx will be used @@ -41,3 +49,68 @@ 'iscoinbase': True, 'privatekey': priv_key, } for coinbase in coinbases] + + +def get_utxos_in_blocks(node: TestNode, blockhashes: List[str]) -> List[Dict]: + """Return all UTXOs in the specified list of blocks. + """ + utxos = filter( + lambda u: node.gettransaction(u["txid"])["blockhash"] in blockhashes, + node.listunspent()) + return list(utxos) + + +def create_stakes( + node: TestNode, blockhashes: List[str], count: int +) -> List[Dict[str, Any]]: + """ + Create a list of stakes by splitting existing UTXOs from a specified list + of blocks into 10 new coins. + + This function can generate more valid stakes than `get_coinbase_stakes` + does, because on the regtest chain halving happens every 150 blocks so + the coinbase amount is below the dust threshold after only 900 blocks. + + :param node: Test node used to generate blocks and send transactions + :param blockhashes: List of block hashes whose UTXOs will be split. + :param count: Number of stakes to return. + """ + assert 10 * len(blockhashes) >= count + utxos = get_utxos_in_blocks(node, blockhashes) + + addresses = [node.getnewaddress() for _ in range(10)] + private_keys = {addr: node.dumpprivkey(addr) for addr in addresses} + + for u in utxos: + inputs = [{"txid": u["txid"], "vout": u["vout"]}] + outputs = { + addr: satoshi_round(u['amount'] / 10) for addr in addresses} + raw_tx = node.createrawtransaction(inputs, outputs) + ctx = FromHex(CTransaction(), raw_tx) + ctx.vout[0].nValue -= node.calculate_fee(ctx) + signed_tx = node.signrawtransactionwithwallet(ToHex(ctx))["hex"] + node.sendrawtransaction(signed_tx) + + # confirm the transactions + new_blocks = [] + while node.getmempoolinfo()['size'] > 0: + new_blocks += node.generate(1) + + utxos = get_utxos_in_blocks(node, new_blocks) + stakes = [] + # cache block heights + heights = {} + for utxo in utxos[:count]: + blockhash = node.gettransaction(utxo["txid"])["blockhash"] + if blockhash not in heights: + heights[blockhash] = node.getblock(blockhash, 1)["height"] + stakes.append({ + 'txid': utxo['txid'], + 'vout': utxo['vout'], + 'amount': utxo['amount'], + 'iscoinbase': utxo['label'] == "coinbase", + 'height': heights[blockhash], + 'privatekey': private_keys[utxo["address"]], + }) + + return stakes