diff --git a/test/functional/abc_p2p_avalanche.py b/test/functional/abc_p2p_avalanche.py index e92bee827..7aae832eb 100755 --- a/test/functional/abc_p2p_avalanche.py +++ b/test/functional/abc_p2p_avalanche.py @@ -1,366 +1,366 @@ #!/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_stakes +from test_framework.avatools import create_coinbase_stakes from test_framework.key import ( ECKey, ECPubKey, ) from test_framework.mininode import P2PInterface, mininode_lock from test_framework.messages import ( AvalancheResponse, AvalancheVote, CInv, msg_avapoll, msg_tcpavaresponse, NODE_AVALANCHE, NODE_NETWORK, TCPAvalancheResponse, ) from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, wait_until, ) 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 TestNode(P2PInterface): def __init__(self): self.round = 0 self.avahello = None self.avaresponses = [] self.avapolls = [] super().__init__() def peer_connect(self, *args, **kwargs): create_conn = super().peer_connect(*args, **kwargs) # Save the nonce and extra entropy so they can be reused later. self.local_nonce = self.on_connection_send_msg.nNonce self.local_extra_entropy = self.on_connection_send_msg.nExtraEntropy return create_conn def on_version(self, message): super().on_version(message) # Save the nonce and extra entropy so they can be reused later. self.remote_nonce = message.nNonce self.remote_extra_entropy = message.nExtraEntropy def on_avaresponse(self, message): self.avaresponses.append(message.response) def on_avapoll(self, message): self.avapolls.append(message.poll) def on_avahello(self, message): assert(self.avahello is None) self.avahello = message def send_avaresponse(self, round, votes, privkey): response = AvalancheResponse(round, 0, votes) sig = privkey.sign_schnorr(response.get_hash()) msg = msg_tcpavaresponse() msg.response = TCPAvalancheResponse(response, sig) self.send_message(msg) def wait_for_avaresponse(self, timeout=5): wait_until( lambda: len(self.avaresponses) > 0, timeout=timeout, lock=mininode_lock) with mininode_lock: return self.avaresponses.pop(0) def send_poll(self, hashes): msg = msg_avapoll() msg.poll.round = self.round self.round += 1 for h in hashes: msg.poll.invs.append(CInv(2, h)) self.send_message(msg) def get_avapoll_if_available(self): with mininode_lock: return self.avapolls.pop(0) if len(self.avapolls) > 0 else None def wait_for_avahello(self, timeout=5): wait_until( lambda: self.avahello is not None, timeout=timeout, lock=mininode_lock) with mininode_lock: return self.avahello 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_node(): n = TestNode() node.add_p2p_connection( n, services=NODE_NETWORK | NODE_AVALANCHE) n.wait_for_verack() # Get our own node id so we can use it later. n.nodeid = node.getpeerinfo()[-1]['id'] return n def get_quorum(): return [get_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 = 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 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, 'bchreg:pqv2r67sgz3qumufap3h2uuj0zfmnzuv8v7ej0fffv') 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) pubkey = privkey.get_pubkey() proof_sequence = 11 proof_expiration = 12 proof = node.buildavalancheproof( proof_sequence, proof_expiration, pubkey.get_bytes().hex(), stakes) # Activate the quorum. for n in quorum: success = node.addavalanchenode( n.nodeid, pubkey.get_bytes().hex(), proof) assert success is True self.log.info("Testing getavalanchepeerinfo...") avapeerinfo = node.getavalanchepeerinfo() # There is a single peer because all nodes share the same proof. assert_equal(len(avapeerinfo), 1) assert_equal(avapeerinfo[0]["peerid"], 0) assert_equal(avapeerinfo[0]["nodecount"], len(quorum)) # The first avalanche node index is 1, because 0 is self.nodes[1]. assert_equal(sorted(avapeerinfo[0]["nodes"]), list(range(1, QUORUM_NODE_COUNT + 1))) assert_equal(avapeerinfo[0]["sequence"], proof_sequence) assert_equal(avapeerinfo[0]["expiration"], proof_expiration) assert_equal(avapeerinfo[0]["master"], pubkey.get_bytes().hex()) assert_equal(avapeerinfo[0]["proof"], proof) assert_equal(len(avapeerinfo[0]["stakes"]), 1) assert_equal(avapeerinfo[0]["stakes"][0]["txid"], stakes[0]['txid']) 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) 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() 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. 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. wait_until(has_parked_new_tip, timeout=15) assert_equal(node.getbestblockhash(), fork_tip) 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) # 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) self.log.info("Test the avahello signature") quorum = get_quorum() poll_node = quorum[0] avahello = poll_node.wait_for_avahello().hello avakey.set(bytes.fromhex(node.getavalanchekey())) assert avakey.verify_schnorr( avahello.sig, avahello.get_sighash(poll_node)) if __name__ == '__main__': AvalancheTest().main() diff --git a/test/functional/abc_rpc_avalancheproof.py b/test/functional/abc_rpc_avalancheproof.py index a34e08e7c..5ecb949fc 100644 --- a/test/functional/abc_rpc_avalancheproof.py +++ b/test/functional/abc_rpc_avalancheproof.py @@ -1,242 +1,250 @@ #!/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.""" -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 from test_framework.test_framework import BitcoinTestFramework from test_framework.test_node import ErrorMatch from test_framework.util import ( append_config, wait_until, assert_raises_rpc_error, ) AVALANCHE_MAX_PROOF_STAKES = 1000 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 = 1 self.extra_args = [['-enableavalanche=1', '-avacooldown=0'], ] self.supports_cli = False self.rpc_timeout = 120 def run_test(self): 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) def get_hex_pubkey(privkey): return privkey.get_pubkey().get_bytes().hex() proof_master = get_hex_pubkey(privkey) proof_sequence = 11 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 self.restart_node(0, self.extra_args[0] + [ "-avaproof={}".format(proof), "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", "-minimumchainwork=0x{:x}".format(minchainwork), ]) self.log.info( "The proof verification should be delayed until IBD is complete") assert node.getblockchaininfo()["initialblockdownload"] is True # Our proof cannot be verified during IBD, so we should have no peer assert not node.getavalanchepeerinfo() # Mining a few more blocks should cause us to leave IBD node.generate(2) # Our proof is now verified and our node is added as a peer 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") # 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( proof, 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, 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, ) # Invalid privkey assert_raises_rpc_error(-5, "The private key is invalid", node.delegateavalancheproof, proof, bytes_to_wif(bytes(32)), random_pubkey, ) # Invalid delegation bad_dg = AvalancheDelegation() assert_raises_rpc_error(-8, "The supplied delegation is not valid", node.delegateavalancheproof, proof, bytes_to_wif(privkey.get_bytes()), random_pubkey, bad_dg.serialize().hex(), ) # Wrong privkey, does not match the proof assert_raises_rpc_error(-8, "The private key does not match the proof or the delegation", node.delegateavalancheproof, proof, bytes_to_wif(random_privkey.get_bytes()), random_pubkey, ) # Wrong privkey, match the proof but does not match the delegation assert_raises_rpc_error(-8, "The private key does not match the proof or the delegation", node.delegateavalancheproof, proof, bytes_to_wif(privkey.get_bytes()), random_pubkey, delegation, ) # 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" "583e75ee79340eb4eff208c89988e7ed0efb30b87298fa30000000000f20" "52a0100000003000000210227d85ba011276cf25b51df6a188b75e604b3" "8770a462b2d0e9fb2fc839ef5d3faf07f001dd38e9b4a43d07d5d449cc0" "f7d2888d96b82962b3ce516d1083c0e031773487fc3c4f2e38acd1db974" "1321b91a79b82d1c2cfd47793261e4ba003cf5") 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(duplicate_stake, "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__': AvalancheProofTest().main() diff --git a/test/functional/test_framework/avatools.py b/test/functional/test_framework/avatools.py index 01e94b66b..07cb270f8 100644 --- a/test/functional/test_framework/avatools.py +++ b/test/functional/test_framework/avatools.py @@ -1,43 +1,116 @@ #!/usr/bin/env python3 # Copyright (c) 2021 The Bitcoin ABC developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Utilities for avalanche tests.""" 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 as a stake. :param priv_key: Private key controlling the coinbase UTXO :param amount: If specified, this overwrites the amount information in the coinbase dicts. """ blocks = [node.getblock(h, 2) for h in blockhashes] coinbases = [ { 'height': b['height'], 'txid': b['tx'][0]['txid'], 'n': 0, 'value': b['tx'][0]['vout'][0]['value'], } for b in blocks ] return [{ 'txid': coinbase['txid'], 'vout': coinbase['n'], 'amount': amount or coinbase['value'], 'height': coinbase['height'], '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