Changeset View
Changeset View
Standalone View
Standalone View
test/functional/abc_p2p_avalanche.py
#!/usr/bin/env python3 | #!/usr/bin/env python3 | ||||
# Copyright (c) 2018 The Bitcoin developers | # Copyright (c) 2020-2021 The Bitcoin developers | ||||
# Distributed under the MIT software license, see the accompanying | # Distributed under the MIT software license, see the accompanying | ||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php. | # file COPYING or http://www.opensource.org/licenses/mit-license.php. | ||||
"""Test the resolution of forks via avalanche.""" | """Test the resolution of forks via avalanche""" | ||||
import random | import random | ||||
from typing import List, Dict | |||||
from test_framework.key import ( | from test_framework.key import ( | ||||
ECKey, | ECKey, | ||||
ECPubKey, | ECPubKey, | ||||
) | ) | ||||
from test_framework.mininode import P2PInterface, mininode_lock | |||||
from test_framework.messages import ( | from test_framework.messages import ( | ||||
AvalancheResponse, | |||||
AvalancheVote, | AvalancheVote, | ||||
CInv, | |||||
MSG_AVALANCHE_PROOF, | |||||
msg_avapoll, | |||||
msg_avahello, | |||||
msg_getdata, | |||||
msg_tcpavaresponse, | |||||
NODE_AVALANCHE, | NODE_AVALANCHE, | ||||
NODE_NETWORK, | NODE_NETWORK, | ||||
TCPAvalancheResponse, | |||||
) | ) | ||||
from test_framework.mininode import P2PAvalancheTestNode | |||||
from test_framework.test_framework import BitcoinTestFramework | from test_framework.test_framework import BitcoinTestFramework | ||||
from test_framework.util import ( | from test_framework.util import ( | ||||
assert_equal, | assert_equal, | ||||
assert_raises_rpc_error, | |||||
wait_until, | wait_until, | ||||
) | ) | ||||
AVALANCHE_MAX_PROOF_STAKES = 1000 | AVALANCHE_MAX_PROOF_STAKES = 1000 | ||||
BLOCK_ACCEPTED = 0 | BLOCK_ACCEPTED = 0 | ||||
BLOCK_INVALID = 1 | BLOCK_INVALID = 1 | ||||
BLOCK_PARKED = 2 | BLOCK_PARKED = 2 | ||||
BLOCK_FORK = 3 | BLOCK_FORK = 3 | ||||
BLOCK_UNKNOWN = -1 | BLOCK_UNKNOWN = -1 | ||||
BLOCK_MISSING = -2 | BLOCK_MISSING = -2 | ||||
BLOCK_PENDING = -3 | BLOCK_PENDING = -3 | ||||
QUORUM_NODE_COUNT = 16 | QUORUM_NODE_COUNT = 16 | ||||
DUMMY_PROOFID = 1337 | |||||
class TestNode(P2PInterface): | |||||
def __init__(self): | |||||
self.round = 0 | |||||
self.avahello = None | |||||
self.avaresponses = [] | |||||
self.avapolls = [] | |||||
self.avaproof = None | |||||
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): | |||||
with mininode_lock: | |||||
self.avaresponses.append(message.response) | |||||
def on_avapoll(self, message): | |||||
with mininode_lock: | |||||
self.avapolls.append(message.poll) | |||||
def on_avahello(self, message): | |||||
with mininode_lock: | |||||
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 | |||||
def send_avahello(self): | |||||
msg = msg_avahello() | |||||
msg.hello.delegation.proofid = DUMMY_PROOFID | |||||
self.send_message(msg) | |||||
def send_getdata(self, inv: List[CInv]): | |||||
msg = msg_getdata() | |||||
msg.inv = inv | |||||
self.send_message(msg) | |||||
def on_avaproof(self, message): | |||||
with mininode_lock: | |||||
assert(self.avaproof is None) | |||||
self.avaproof = message | |||||
def wait_for_avaproof(self, timeout=10): | |||||
wait_until( | |||||
lambda: self.avaproof is not None, | |||||
timeout=timeout, | |||||
lock=mininode_lock) | |||||
with mininode_lock: | |||||
return self.avaproof | |||||
def get_stakes(coinbases: List[Dict], | |||||
priv_key: str) -> List[Dict]: | |||||
return [{ | |||||
'txid': coinbase['txid'], | |||||
'vout': coinbase['n'], | |||||
'amount': coinbase['value'], | |||||
'height': coinbase['height'], | |||||
'iscoinbase': True, | |||||
'privatekey': priv_key, | |||||
} for coinbase in coinbases] | |||||
class AvalancheTest(BitcoinTestFramework): | class AvalancheTest(BitcoinTestFramework): | ||||
def set_test_params(self): | def set_test_params(self): | ||||
self.setup_clean_chain = True | self.setup_clean_chain = True | ||||
self.num_nodes = 2 | self.num_nodes = 2 | ||||
self.extra_args = [ | self.extra_args = [ | ||||
['-enableavalanche=1', '-avacooldown=0'], | ['-enableavalanche=1', '-avacooldown=0'], | ||||
['-enableavalanche=1', '-avacooldown=0', '-noparkdeepreorg', '-maxreorgdepth=-1']] | ['-enableavalanche=1', '-avacooldown=0', '-noparkdeepreorg', '-maxreorgdepth=-1']] | ||||
self.supports_cli = False | self.supports_cli = False | ||||
def run_test(self): | def run_test(self): | ||||
node = self.nodes[0] | node = self.nodes[0] | ||||
self.log.info("Check the node is signalling the avalanche service.") | |||||
assert_equal( | |||||
int(node.getnetworkinfo()['localservices'], 16) & NODE_AVALANCHE, | |||||
NODE_AVALANCHE) | |||||
# Build a fake quorum of nodes. | # Build a fake quorum of nodes. | ||||
def get_node(): | def get_node(): | ||||
n = TestNode() | n = P2PAvalancheTestNode() | ||||
node.add_p2p_connection( | node.add_p2p_connection( | ||||
n, services=NODE_NETWORK | NODE_AVALANCHE) | n, services=NODE_NETWORK | NODE_AVALANCHE) | ||||
n.wait_for_verack() | n.wait_for_verack() | ||||
# Get our own node id so we can use it later. | # Get our own node id so we can use it later. | ||||
n.nodeid = node.getpeerinfo()[-1]['id'] | n.nodeid = node.getpeerinfo()[-1]['id'] | ||||
return n | return n | ||||
▲ Show 20 Lines • Show All 208 Lines • ▼ Show 20 Lines | def run_test(self): | ||||
def has_parked_new_tip(): | def has_parked_new_tip(): | ||||
can_find_block_in_poll(hash_to_find, BLOCK_PARKED) | can_find_block_in_poll(hash_to_find, BLOCK_PARKED) | ||||
return node.getbestblockhash() == fork_tip | return node.getbestblockhash() == fork_tip | ||||
# Because everybody answers no, the node will park that block. | # Because everybody answers no, the node will park that block. | ||||
wait_until(has_parked_new_tip, timeout=15) | wait_until(has_parked_new_tip, timeout=15) | ||||
assert_equal(node.getbestblockhash(), fork_tip) | assert_equal(node.getbestblockhash(), fork_tip) | ||||
# Restart the node | |||||
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 | |||||
assert_equal(len(node.getavalanchepeerinfo()), 1) | |||||
# Rebuild the quorum | |||||
self.log.info("Test the avahello signature") | |||||
quorum = get_quorum() | |||||
poll_node = quorum[0] | |||||
# Check the handshake sequence | |||||
self.log.info("Test avahello sequence node -> poll_node") | |||||
avahello = poll_node.wait_for_avahello().hello | |||||
avakey.set(bytes.fromhex(node.getavalanchekey())) | |||||
assert avakey.verify_schnorr( | |||||
avahello.sig, avahello.get_sighash(poll_node)) | |||||
poll_node.send_getdata([CInv(MSG_AVALANCHE_PROOF, | |||||
avahello.delegation.proofid)]) | |||||
avaproof = poll_node.wait_for_avaproof().proof | |||||
assert_equal(proof, avaproof.serialize().hex()) | |||||
assert_equal(avaproof.proofid, avahello.delegation.proofid) | |||||
self.log.info( | |||||
"Test avahello sequence poll_node -> node") | |||||
# This is testing only the first couple of steps (for now) | |||||
# TODO: make a proper delegation and a valid signature for avahello, | |||||
# reply with a valid avaproof, test that the node adds us. | |||||
poll_node.send_avahello() | |||||
poll_node.wait_for_getdata([DUMMY_PROOFID]) | |||||
# Check the maximum number of stakes policy | |||||
blocks = node.generatetoaddress(AVALANCHE_MAX_PROOF_STAKES + 1, | |||||
addrkey0.address) | |||||
too_many_coinbases = [get_coinbase(h) for h in blocks] | |||||
too_many_stakes = get_stakes(too_many_coinbases, addrkey0.key) | |||||
self.log.info( | |||||
"A proof using the maximum number of stakes is accepted...") | |||||
maximum_stakes = get_stakes(too_many_coinbases[:-1], | |||||
addrkey0.key) | |||||
good_proof = node.buildavalancheproof( | |||||
proof_sequence, proof_expiration, | |||||
proof_master, maximum_stakes) | |||||
node.addavalanchenode( | |||||
get_node().nodeid, proof_master, good_proof) | |||||
self.log.info("A proof using too many stakes should be rejected...") | |||||
bad_proof = node.buildavalancheproof( | |||||
proof_sequence, proof_expiration, | |||||
proof_master, too_many_stakes) | |||||
assert_raises_rpc_error(-32602, "Avalanche proof has too many UTXOs", | |||||
node.addavalanchenode, | |||||
get_node().nodeid, proof_master, | |||||
bad_proof) | |||||
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, | |||||
[{ | |||||
'txid': coinbases[0]['txid'], | |||||
'vout': coinbases[0]['n'], | |||||
'amount': '0', | |||||
'height': coinbases[0]['height'], | |||||
'iscoinbase': True, | |||||
'privatekey': addrkey0.key, | |||||
}]) | |||||
duplicate_stake = node.buildavalancheproof( | |||||
proof_sequence, proof_expiration, proof_master, | |||||
[{ | |||||
'txid': coinbases[0]['txid'], | |||||
'vout': coinbases[0]['n'], | |||||
'amount': coinbases[0]['value'], | |||||
'height': coinbases[0]['height'], | |||||
'iscoinbase': True, | |||||
'privatekey': addrkey0.key, | |||||
}] * 2) | |||||
bad_sig = ("0b000000000000000c0000000000000021030b4c866585dd868a9d62348" | |||||
"a9cd008d6a312937048fff31670e7e920cfc7a7440105c5f72f5d6da3085" | |||||
"583e75ee79340eb4eff208c89988e7ed0efb30b87298fa30000000000f20" | |||||
"52a0100000003000000210227d85ba011276cf25b51df6a188b75e604b3" | |||||
"8770a462b2d0e9fb2fc839ef5d3faf07f001dd38e9b4a43d07d5d449cc0" | |||||
"f7d2888d96b82962b3ce516d1083c0e031773487fc3c4f2e38acd1db974" | |||||
"1321b91a79b82d1c2cfd47793261e4ba003cf5") | |||||
self.stop_node(0) | |||||
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") | |||||
if __name__ == '__main__': | if __name__ == '__main__': | ||||
AvalancheTest().main() | AvalancheTest().main() |