Changeset View
Changeset View
Standalone View
Standalone View
test/functional/test_framework/avatools.py
Show First 20 Lines • Show All 43 Lines • ▼ Show 20 Lines | |||||
from .wallet_util import bytes_to_wif | from .wallet_util import bytes_to_wif | ||||
def avalanche_proof_from_hex(proof_hex: str) -> AvalancheProof: | def avalanche_proof_from_hex(proof_hex: str) -> AvalancheProof: | ||||
return FromHex(AvalancheProof(), proof_hex) | return FromHex(AvalancheProof(), proof_hex) | ||||
def create_coinbase_stakes( | def create_coinbase_stakes( | ||||
node: TestNode, | node: TestNode, blockhashes: List[str], priv_key: str, amount: Optional[str] = None | ||||
blockhashes: List[str], | ) -> List[Dict[str, Any]]: | ||||
priv_key: str, | |||||
amount: Optional[str] = None) -> List[Dict[str, Any]]: | |||||
"""Returns a list of dictionaries representing stakes, in a format | """Returns a list of dictionaries representing stakes, in a format | ||||
compatible with the buildavalancheproof RPC, using only coinbase | compatible with the buildavalancheproof RPC, using only coinbase | ||||
transactions. | transactions. | ||||
:param node: Test node used to get the block and coinbase data. | :param node: Test node used to get the block and coinbase data. | ||||
:param blockhashes: List of block hashes, whose coinbase tx will be used | :param blockhashes: List of block hashes, whose coinbase tx will be used | ||||
as a stake. | as a stake. | ||||
:param priv_key: Private key controlling the coinbase UTXO | :param priv_key: Private key controlling the coinbase UTXO | ||||
:param amount: If specified, this overwrites the amount information | :param amount: If specified, this overwrites the amount information | ||||
in the coinbase dicts. | in the coinbase dicts. | ||||
""" | """ | ||||
blocks = [node.getblock(h, 2) for h in blockhashes] | blocks = [node.getblock(h, 2) for h in blockhashes] | ||||
coinbases = [ | coinbases = [ | ||||
{ | { | ||||
'height': b['height'], | "height": b["height"], | ||||
'txid': b['tx'][0]['txid'], | "txid": b["tx"][0]["txid"], | ||||
'n': 0, | "n": 0, | ||||
'value': b['tx'][0]['vout'][0]['value'], | "value": b["tx"][0]["vout"][0]["value"], | ||||
} for b in blocks | } | ||||
for b in blocks | |||||
] | ] | ||||
return [{ | return [ | ||||
'txid': coinbase['txid'], | { | ||||
'vout': coinbase['n'], | "txid": coinbase["txid"], | ||||
'amount': amount or coinbase['value'], | "vout": coinbase["n"], | ||||
'height': coinbase['height'], | "amount": amount or coinbase["value"], | ||||
'iscoinbase': True, | "height": coinbase["height"], | ||||
'privatekey': priv_key, | "iscoinbase": True, | ||||
} for coinbase in coinbases] | "privatekey": priv_key, | ||||
} | |||||
for coinbase in coinbases | |||||
] | |||||
def get_utxos_in_blocks(node: TestNode, blockhashes: List[str]) -> List[Dict]: | def get_utxos_in_blocks(node: TestNode, blockhashes: List[str]) -> List[Dict]: | ||||
"""Return all UTXOs in the specified list of blocks. | """Return all UTXOs in the specified list of blocks.""" | ||||
""" | |||||
utxos = filter( | utxos = filter( | ||||
lambda u: node.gettransaction(u["txid"])["blockhash"] in blockhashes, | lambda u: node.gettransaction(u["txid"])["blockhash"] in blockhashes, | ||||
node.listunspent()) | node.listunspent(), | ||||
) | |||||
return list(utxos) | return list(utxos) | ||||
def create_stakes( | def create_stakes( | ||||
test_framework: 'BitcoinTestFramework', | test_framework: "BitcoinTestFramework", | ||||
node: TestNode, | node: TestNode, | ||||
blockhashes: List[str], | blockhashes: List[str], | ||||
count: int, | count: int, | ||||
sync_fun=None,) -> List[Dict[str, Any]]: | sync_fun=None, | ||||
) -> List[Dict[str, Any]]: | |||||
""" | """ | ||||
Create a list of stakes by splitting existing UTXOs from a specified list | Create a list of stakes by splitting existing UTXOs from a specified list | ||||
of blocks into 10 new coins. | of blocks into 10 new coins. | ||||
This function can generate more valid stakes than `get_coinbase_stakes` | This function can generate more valid stakes than `get_coinbase_stakes` | ||||
does, because on the regtest chain halving happens every 150 blocks so | does, because on the regtest chain halving happens every 150 blocks so | ||||
the coinbase amount is below the dust threshold after only 900 blocks. | the coinbase amount is below the dust threshold after only 900 blocks. | ||||
:param node: Test node used to generate blocks and send transactions | :param node: Test node used to generate blocks and send transactions | ||||
:param blockhashes: List of block hashes whose UTXOs will be split. | :param blockhashes: List of block hashes whose UTXOs will be split. | ||||
:param count: Number of stakes to return. | :param count: Number of stakes to return. | ||||
""" | """ | ||||
assert 10 * len(blockhashes) >= count | assert 10 * len(blockhashes) >= count | ||||
utxos = get_utxos_in_blocks(node, blockhashes) | utxos = get_utxos_in_blocks(node, blockhashes) | ||||
addresses = [node.getnewaddress() for _ in range(10)] | addresses = [node.getnewaddress() for _ in range(10)] | ||||
private_keys = {addr: node.dumpprivkey(addr) for addr in addresses} | private_keys = {addr: node.dumpprivkey(addr) for addr in addresses} | ||||
for u in utxos: | for u in utxos: | ||||
inputs = [{"txid": u["txid"], "vout": u["vout"]}] | inputs = [{"txid": u["txid"], "vout": u["vout"]}] | ||||
outputs = { | outputs = {addr: satoshi_round(u["amount"] / 10) for addr in addresses} | ||||
addr: satoshi_round(u['amount'] / 10) for addr in addresses} | |||||
raw_tx = node.createrawtransaction(inputs, outputs) | raw_tx = node.createrawtransaction(inputs, outputs) | ||||
ctx = FromHex(CTransaction(), raw_tx) | ctx = FromHex(CTransaction(), raw_tx) | ||||
ctx.vout[0].nValue -= node.calculate_fee(ctx) | ctx.vout[0].nValue -= node.calculate_fee(ctx) | ||||
signed_tx = node.signrawtransactionwithwallet(ToHex(ctx))["hex"] | signed_tx = node.signrawtransactionwithwallet(ToHex(ctx))["hex"] | ||||
node.sendrawtransaction(signed_tx) | node.sendrawtransaction(signed_tx) | ||||
# confirm the transactions | # confirm the transactions | ||||
new_blocks = [] | new_blocks = [] | ||||
while node.getmempoolinfo()['size'] > 0: | while node.getmempoolinfo()["size"] > 0: | ||||
new_blocks += test_framework.generate( | new_blocks += test_framework.generate( | ||||
node, 1, sync_fun=test_framework.no_op if sync_fun is None else sync_fun) | node, 1, sync_fun=test_framework.no_op if sync_fun is None else sync_fun | ||||
) | |||||
utxos = get_utxos_in_blocks(node, new_blocks) | utxos = get_utxos_in_blocks(node, new_blocks) | ||||
stakes = [] | stakes = [] | ||||
# cache block heights | # cache block heights | ||||
heights = {} | heights = {} | ||||
for utxo in utxos[:count]: | for utxo in utxos[:count]: | ||||
blockhash = node.gettransaction(utxo["txid"])["blockhash"] | blockhash = node.gettransaction(utxo["txid"])["blockhash"] | ||||
if blockhash not in heights: | if blockhash not in heights: | ||||
heights[blockhash] = node.getblock(blockhash, 1)["height"] | heights[blockhash] = node.getblock(blockhash, 1)["height"] | ||||
stakes.append({ | stakes.append( | ||||
'txid': utxo['txid'], | { | ||||
'vout': utxo['vout'], | "txid": utxo["txid"], | ||||
'amount': utxo['amount'], | "vout": utxo["vout"], | ||||
'iscoinbase': utxo['label'] == "coinbase", | "amount": utxo["amount"], | ||||
'height': heights[blockhash], | "iscoinbase": utxo["label"] == "coinbase", | ||||
'privatekey': private_keys[utxo["address"]], | "height": heights[blockhash], | ||||
}) | "privatekey": private_keys[utxo["address"]], | ||||
} | |||||
) | |||||
return stakes | return stakes | ||||
def get_proof_ids(node): | def get_proof_ids(node): | ||||
return [int(peer['proofid'], 16) for peer in node.getavalanchepeerinfo()] | return [int(peer["proofid"], 16) for peer in node.getavalanchepeerinfo()] | ||||
def wait_for_proof(node, proofid_hex, expect_status="boundToPeer", timeout=60): | def wait_for_proof(node, proofid_hex, expect_status="boundToPeer", timeout=60): | ||||
""" | """ | ||||
Wait for the proof to be known by the node. The expect_status is checked | Wait for the proof to be known by the node. The expect_status is checked | ||||
once after the proof is found and can be one of the following: "immature", | once after the proof is found and can be one of the following: "immature", | ||||
"boundToPeer", "conflicting" or "finalized". | "boundToPeer", "conflicting" or "finalized". | ||||
""" | """ | ||||
ret = {} | ret = {} | ||||
def proof_found(): | def proof_found(): | ||||
nonlocal ret | nonlocal ret | ||||
try: | try: | ||||
ret = node.getrawavalancheproof(proofid_hex) | ret = node.getrawavalancheproof(proofid_hex) | ||||
return True | return True | ||||
except JSONRPCException: | except JSONRPCException: | ||||
return False | return False | ||||
wait_until_helper(proof_found, timeout=timeout) | wait_until_helper(proof_found, timeout=timeout) | ||||
assert ret.get(expect_status, False) is True | assert ret.get(expect_status, False) is True | ||||
class NoHandshakeAvaP2PInterface(P2PInterface): | class NoHandshakeAvaP2PInterface(P2PInterface): | ||||
"""P2PInterface with avalanche capabilities""" | """P2PInterface with avalanche capabilities""" | ||||
def __init__(self): | def __init__(self): | ||||
▲ Show 20 Lines • Show All 42 Lines • ▼ Show 20 Lines | class NoHandshakeAvaP2PInterface(P2PInterface): | ||||
def send_avaresponse(self, avaround, votes, privkey): | def send_avaresponse(self, avaround, votes, privkey): | ||||
response = AvalancheResponse(avaround, 0, votes) | response = AvalancheResponse(avaround, 0, votes) | ||||
sig = privkey.sign_schnorr(response.get_hash()) | sig = privkey.sign_schnorr(response.get_hash()) | ||||
msg = msg_tcpavaresponse() | msg = msg_tcpavaresponse() | ||||
msg.response = TCPAvalancheResponse(response, sig) | msg.response = TCPAvalancheResponse(response, sig) | ||||
self.send_message(msg) | self.send_message(msg) | ||||
def wait_for_avaresponse(self, timeout=5): | def wait_for_avaresponse(self, timeout=5): | ||||
self.wait_until( | self.wait_until(lambda: len(self.avaresponses) > 0, timeout=timeout) | ||||
lambda: len(self.avaresponses) > 0, | |||||
timeout=timeout) | |||||
with p2p_lock: | with p2p_lock: | ||||
return self.avaresponses.pop(0) | return self.avaresponses.pop(0) | ||||
def send_poll(self, hashes, inv_type=MSG_BLOCK): | def send_poll(self, hashes, inv_type=MSG_BLOCK): | ||||
msg = msg_avapoll() | msg = msg_avapoll() | ||||
msg.poll.round = self.round | msg.poll.round = self.round | ||||
self.round += 1 | self.round += 1 | ||||
for h in hashes: | for h in hashes: | ||||
msg.poll.invs.append(CInv(inv_type, h)) | msg.poll.invs.append(CInv(inv_type, h)) | ||||
self.send_message(msg) | self.send_message(msg) | ||||
def send_proof(self, proof): | def send_proof(self, proof): | ||||
msg = msg_avaproof() | msg = msg_avaproof() | ||||
msg.proof = proof | msg.proof = proof | ||||
self.send_message(msg) | self.send_message(msg) | ||||
def get_avapoll_if_available(self): | def get_avapoll_if_available(self): | ||||
with p2p_lock: | with p2p_lock: | ||||
return self.avapolls.pop(0) if len(self.avapolls) > 0 else None | return self.avapolls.pop(0) if len(self.avapolls) > 0 else None | ||||
def wait_for_avahello(self, timeout=5): | def wait_for_avahello(self, timeout=5): | ||||
self.wait_until( | self.wait_until(lambda: self.avahello is not None, timeout=timeout) | ||||
lambda: self.avahello is not None, | |||||
timeout=timeout) | |||||
with p2p_lock: | with p2p_lock: | ||||
return self.avahello | return self.avahello | ||||
def build_avahello(self, delegation: AvalancheDelegation, | def build_avahello( | ||||
delegated_privkey: ECKey) -> msg_avahello: | self, delegation: AvalancheDelegation, delegated_privkey: ECKey | ||||
) -> msg_avahello: | |||||
local_sighash = hash256( | local_sighash = hash256( | ||||
delegation.getid() + | delegation.getid() | ||||
struct.pack("<QQQQ", self.local_nonce, self.remote_nonce, | + struct.pack( | ||||
self.local_extra_entropy, self.remote_extra_entropy)) | "<QQQQ", | ||||
self.local_nonce, | |||||
self.remote_nonce, | |||||
self.local_extra_entropy, | |||||
self.remote_extra_entropy, | |||||
) | |||||
) | |||||
msg = msg_avahello() | msg = msg_avahello() | ||||
msg.hello.delegation = delegation | msg.hello.delegation = delegation | ||||
msg.hello.sig = delegated_privkey.sign_schnorr(local_sighash) | msg.hello.sig = delegated_privkey.sign_schnorr(local_sighash) | ||||
return msg | return msg | ||||
def send_avahello(self, delegation_hex: str, delegated_privkey: ECKey): | def send_avahello(self, delegation_hex: str, delegated_privkey: ECKey): | ||||
delegation = FromHex(AvalancheDelegation(), delegation_hex) | delegation = FromHex(AvalancheDelegation(), delegation_hex) | ||||
msg = self.build_avahello(delegation, delegated_privkey) | msg = self.build_avahello(delegation, delegated_privkey) | ||||
self.send_message(msg) | self.send_message(msg) | ||||
return msg.hello.delegation.proofid | return msg.hello.delegation.proofid | ||||
def send_avaproof(self, proof: AvalancheProof): | def send_avaproof(self, proof: AvalancheProof): | ||||
msg = msg_avaproof() | msg = msg_avaproof() | ||||
msg.proof = proof | msg.proof = proof | ||||
self.send_message(msg) | self.send_message(msg) | ||||
class AvaP2PInterface(NoHandshakeAvaP2PInterface): | class AvaP2PInterface(NoHandshakeAvaP2PInterface): | ||||
def __init__(self, test_framework=None, node=None): | def __init__(self, test_framework=None, node=None): | ||||
if (test_framework is not None and node is None) or ( | if (test_framework is not None and node is None) or ( | ||||
node is not None and test_framework is None): | node is not None and test_framework is None | ||||
): | |||||
raise AssertionError( | raise AssertionError( | ||||
"test_framework and node should both be either set or None") | "test_framework and node should both be either set or None" | ||||
) | |||||
super().__init__() | super().__init__() | ||||
self.master_privkey = None | self.master_privkey = None | ||||
self.proof = None | self.proof = None | ||||
self.delegated_privkey = ECKey() | self.delegated_privkey = ECKey() | ||||
self.delegated_privkey.generate() | self.delegated_privkey.generate() | ||||
Show All 10 Lines | def __init__(self, test_framework=None, node=None): | ||||
self.delegation = FromHex(AvalancheDelegation(), delegation_hex) | self.delegation = FromHex(AvalancheDelegation(), delegation_hex) | ||||
def on_version(self, message): | def on_version(self, message): | ||||
super().on_version(message) | super().on_version(message) | ||||
avahello = msg_avahello() | avahello = msg_avahello() | ||||
if self.delegation is not None: | if self.delegation is not None: | ||||
avahello = self.build_avahello( | avahello = self.build_avahello(self.delegation, self.delegated_privkey) | ||||
self.delegation, self.delegated_privkey) | |||||
elif self.proof is not None: | elif self.proof is not None: | ||||
avahello = self.build_avahello( | avahello = self.build_avahello( | ||||
AvalancheDelegation( | AvalancheDelegation( | ||||
self.proof.limited_proofid, | self.proof.limited_proofid, | ||||
self.master_privkey.get_pubkey().get_bytes()), | self.master_privkey.get_pubkey().get_bytes(), | ||||
self.master_privkey) | ), | ||||
self.master_privkey, | |||||
) | |||||
self.send_message(avahello) | self.send_message(avahello) | ||||
def on_getdata(self, message): | def on_getdata(self, message): | ||||
super().on_getdata(message) | super().on_getdata(message) | ||||
not_found = [] | not_found = [] | ||||
for inv in message.inv: | for inv in message.inv: | ||||
if inv.type == MSG_AVA_PROOF and self.proof is not None and inv.hash == self.proof.proofid: | if ( | ||||
inv.type == MSG_AVA_PROOF | |||||
and self.proof is not None | |||||
and inv.hash == self.proof.proofid | |||||
): | |||||
self.send_avaproof(self.proof) | self.send_avaproof(self.proof) | ||||
else: | else: | ||||
not_found.append(inv) | not_found.append(inv) | ||||
if len(not_found) > 0: | if len(not_found) > 0: | ||||
self.send_message(msg_notfound(not_found)) | self.send_message(msg_notfound(not_found)) | ||||
def get_ava_p2p_interface_no_handshake( | def get_ava_p2p_interface_no_handshake( | ||||
node: TestNode, | node: TestNode, services=NODE_NETWORK | NODE_AVALANCHE | ||||
services=NODE_NETWORK | NODE_AVALANCHE) -> NoHandshakeAvaP2PInterface: | ) -> NoHandshakeAvaP2PInterface: | ||||
"""Build and return a NoHandshakeAvaP2PInterface connected to the specified | """Build and return a NoHandshakeAvaP2PInterface connected to the specified | ||||
TestNode. | TestNode. | ||||
""" | """ | ||||
n = NoHandshakeAvaP2PInterface() | n = NoHandshakeAvaP2PInterface() | ||||
node.add_p2p_connection( | node.add_p2p_connection(n, services=services) | ||||
n, services=services) | |||||
n.wait_for_verack() | n.wait_for_verack() | ||||
n.nodeid = node.getpeerinfo()[-1]['id'] | n.nodeid = node.getpeerinfo()[-1]["id"] | ||||
return n | return n | ||||
def get_ava_p2p_interface( | def get_ava_p2p_interface( | ||||
test_framework: 'BitcoinTestFramework', | test_framework: "BitcoinTestFramework", | ||||
node: TestNode, | node: TestNode, | ||||
services=NODE_NETWORK | NODE_AVALANCHE, | services=NODE_NETWORK | NODE_AVALANCHE, | ||||
stake_utxo_confirmations=1, | stake_utxo_confirmations=1, | ||||
sync_fun=None,) -> AvaP2PInterface: | sync_fun=None, | ||||
"""Build and return an AvaP2PInterface connected to the specified TestNode. | ) -> AvaP2PInterface: | ||||
""" | """Build and return an AvaP2PInterface connected to the specified TestNode.""" | ||||
n = AvaP2PInterface(test_framework, node) | n = AvaP2PInterface(test_framework, node) | ||||
# Make sure the proof utxos are mature | # Make sure the proof utxos are mature | ||||
if stake_utxo_confirmations > 1: | if stake_utxo_confirmations > 1: | ||||
test_framework.generate( | test_framework.generate( | ||||
node, | node, | ||||
stake_utxo_confirmations - 1, | stake_utxo_confirmations - 1, | ||||
sync_fun=test_framework.no_op if sync_fun is None else sync_fun) | sync_fun=test_framework.no_op if sync_fun is None else sync_fun, | ||||
) | |||||
assert node.verifyavalancheproof(n.proof.serialize().hex()) | assert node.verifyavalancheproof(n.proof.serialize().hex()) | ||||
proofid_hex = uint256_hex(n.proof.proofid) | proofid_hex = uint256_hex(n.proof.proofid) | ||||
node.add_p2p_connection(n, services=services) | node.add_p2p_connection(n, services=services) | ||||
n.nodeid = node.getpeerinfo()[-1]['id'] | n.nodeid = node.getpeerinfo()[-1]["id"] | ||||
def avapeer_connected(): | def avapeer_connected(): | ||||
node_list = [] | node_list = [] | ||||
try: | try: | ||||
node_list = node.getavalanchepeerinfo(proofid_hex)[0]['node_list'] | node_list = node.getavalanchepeerinfo(proofid_hex)[0]["node_list"] | ||||
except BaseException: | except BaseException: | ||||
pass | pass | ||||
return n.nodeid in node_list | return n.nodeid in node_list | ||||
wait_until_helper(avapeer_connected, timeout=5) | wait_until_helper(avapeer_connected, timeout=5) | ||||
return n | return n | ||||
def gen_proof(test_framework, node, coinbase_utxos=1, expiry=0, sync_fun=None): | def gen_proof(test_framework, node, coinbase_utxos=1, expiry=0, sync_fun=None): | ||||
blockhashes = test_framework.generate( | blockhashes = test_framework.generate( | ||||
node, | node, | ||||
coinbase_utxos, | coinbase_utxos, | ||||
sync_fun=test_framework.no_op if sync_fun is None else sync_fun) | sync_fun=test_framework.no_op if sync_fun is None else sync_fun, | ||||
) | |||||
privkey = ECKey() | privkey = ECKey() | ||||
privkey.generate() | privkey.generate() | ||||
stakes = create_coinbase_stakes( | stakes = create_coinbase_stakes( | ||||
node, blockhashes, node.get_deterministic_priv_key().key) | node, blockhashes, node.get_deterministic_priv_key().key | ||||
) | |||||
proof_hex = node.buildavalancheproof( | proof_hex = node.buildavalancheproof( | ||||
42, expiry, bytes_to_wif(privkey.get_bytes()), stakes) | 42, expiry, bytes_to_wif(privkey.get_bytes()), stakes | ||||
) | |||||
return privkey, avalanche_proof_from_hex(proof_hex) | return privkey, avalanche_proof_from_hex(proof_hex) | ||||
def build_msg_avaproofs(proofs: List[AvalancheProof], prefilled_proofs: Optional[List[AvalancheProof]] | def build_msg_avaproofs( | ||||
= None, key_pair: Optional[List[int]] = None) -> msg_avaproofs: | proofs: List[AvalancheProof], | ||||
prefilled_proofs: Optional[List[AvalancheProof]] = None, | |||||
key_pair: Optional[List[int]] = None, | |||||
) -> msg_avaproofs: | |||||
if key_pair is None: | if key_pair is None: | ||||
key_pair = [random.randint(0, 2**64 - 1)] * 2 | key_pair = [random.randint(0, 2**64 - 1)] * 2 | ||||
msg = msg_avaproofs() | msg = msg_avaproofs() | ||||
msg.key0 = key_pair[0] | msg.key0 = key_pair[0] | ||||
msg.key1 = key_pair[1] | msg.key1 = key_pair[1] | ||||
msg.prefilled_proofs = prefilled_proofs or [] | msg.prefilled_proofs = prefilled_proofs or [] | ||||
msg.shortids = [ | msg.shortids = [ | ||||
calculate_shortid( | calculate_shortid(msg.key0, msg.key1, proof.proofid) for proof in proofs | ||||
msg.key0, | ] | ||||
msg.key1, | |||||
proof.proofid) for proof in proofs] | |||||
return msg | return msg | ||||
def can_find_inv_in_poll(quorum, inv_hash, response=AvalancheVoteError.ACCEPTED): | def can_find_inv_in_poll(quorum, inv_hash, response=AvalancheVoteError.ACCEPTED): | ||||
found_hash = False | found_hash = False | ||||
for n in quorum: | for n in quorum: | ||||
poll = n.get_avapoll_if_available() | poll = n.get_avapoll_if_available() | ||||
Show All 21 Lines |