Changeset View
Changeset View
Standalone View
Standalone View
test/functional/abc_rpc_avalancheproof.py
Show First 20 Lines • Show All 44 Lines • ▼ Show 20 Lines | |||||
def add_interface_node(test_node) -> str: | def add_interface_node(test_node) -> str: | ||||
"""Create a mininode, connect it to test_node, return the nodeid | """Create a mininode, connect it to test_node, return the nodeid | ||||
of the mininode as registered by test_node. | of the mininode as registered by test_node. | ||||
""" | """ | ||||
n = P2PInterface() | n = P2PInterface() | ||||
test_node.add_p2p_connection(n) | test_node.add_p2p_connection(n) | ||||
n.wait_for_verack() | n.wait_for_verack() | ||||
return test_node.getpeerinfo()[-1]['id'] | return test_node.getpeerinfo()[-1]["id"] | ||||
class AvalancheProofTest(BitcoinTestFramework): | class AvalancheProofTest(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 = [ | ||||
f'-avaproofstakeutxodustthreshold={PROOF_DUST_THRESHOLD}', | [ | ||||
'-avaproofstakeutxoconfirmations=1', | f"-avaproofstakeutxodustthreshold={PROOF_DUST_THRESHOLD}", | ||||
'-avalancheconflictingproofcooldown=0', | "-avaproofstakeutxoconfirmations=1", | ||||
'-avacooldown=0', | "-avalancheconflictingproofcooldown=0", | ||||
'-whitelist=noban@127.0.0.1', | "-avacooldown=0", | ||||
]] * self.num_nodes | "-whitelist=noban@127.0.0.1", | ||||
] | |||||
] * self.num_nodes | |||||
self.supports_cli = False | self.supports_cli = False | ||||
self.rpc_timeout = 120 | self.rpc_timeout = 120 | ||||
def run_test(self): | def run_test(self): | ||||
# Turn off node 1 while node 0 mines blocks to generate stakes, | # Turn off node 1 while node 0 mines blocks to generate stakes, | ||||
# so that we can later try starting node 1 with an immature proof. | # so that we can later try starting node 1 with an immature proof. | ||||
self.stop_node(1) | self.stop_node(1) | ||||
node = self.nodes[0] | node = self.nodes[0] | ||||
# FIXME Remove after the hardcoded addresses have been converted in the | # FIXME Remove after the hardcoded addresses have been converted in the | ||||
# LUT from test_node.py | # LUT from test_node.py | ||||
def legacy_to_ecash_p2pkh(legacy): | def legacy_to_ecash_p2pkh(legacy): | ||||
payload, _ = base58_to_byte(legacy) | payload, _ = base58_to_byte(legacy) | ||||
return encode_full('ecregtest', PUBKEY_TYPE, payload) | return encode_full("ecregtest", PUBKEY_TYPE, payload) | ||||
addrkey0 = node.get_deterministic_priv_key() | addrkey0 = node.get_deterministic_priv_key() | ||||
node_ecash_addr = legacy_to_ecash_p2pkh(addrkey0.address) | node_ecash_addr = legacy_to_ecash_p2pkh(addrkey0.address) | ||||
blockhashes = self.generatetoaddress( | blockhashes = self.generatetoaddress( | ||||
node, 100, node_ecash_addr, sync_fun=self.no_op) | node, 100, node_ecash_addr, sync_fun=self.no_op | ||||
) | |||||
self.log.info( | self.log.info("Make build a valid proof and restart the node to use it") | ||||
"Make build a valid proof and restart the node to use it") | |||||
privkey = ECKey() | privkey = ECKey() | ||||
privkey.set(bytes.fromhex( | privkey.set( | ||||
"12b004fff7f4b69ef8650e767f18f11ede158148b425660723b9f9a66e61f747"), True) | bytes.fromhex( | ||||
"12b004fff7f4b69ef8650e767f18f11ede158148b425660723b9f9a66e61f747" | |||||
), | |||||
True, | |||||
) | |||||
wif_privkey = bytes_to_wif(privkey.get_bytes()) | wif_privkey = bytes_to_wif(privkey.get_bytes()) | ||||
def get_hex_pubkey(privkey): | def get_hex_pubkey(privkey): | ||||
return privkey.get_pubkey().get_bytes().hex() | return privkey.get_pubkey().get_bytes().hex() | ||||
proof_master = get_hex_pubkey(privkey) | proof_master = get_hex_pubkey(privkey) | ||||
proof_sequence = 11 | proof_sequence = 11 | ||||
proof_expiration = 0 | proof_expiration = 0 | ||||
stakes = create_coinbase_stakes(node, [blockhashes[0]], addrkey0.key) | stakes = create_coinbase_stakes(node, [blockhashes[0]], addrkey0.key) | ||||
proof = node.buildavalancheproof( | proof = node.buildavalancheproof( | ||||
proof_sequence, proof_expiration, wif_privkey, stakes, ADDRESS_ECREG_UNSPENDABLE) | proof_sequence, | ||||
proof_expiration, | |||||
wif_privkey, | |||||
stakes, | |||||
ADDRESS_ECREG_UNSPENDABLE, | |||||
) | |||||
self.log.info("Test decodeavalancheproof RPC") | self.log.info("Test decodeavalancheproof RPC") | ||||
# Invalid hex (odd number of hex digits) | # Invalid hex (odd number of hex digits) | ||||
assert_raises_rpc_error(-22, "Proof must be an hexadecimal string", | assert_raises_rpc_error( | ||||
node.decodeavalancheproof, proof[:-1]) | -22, | ||||
"Proof must be an hexadecimal string", | |||||
node.decodeavalancheproof, | |||||
proof[:-1], | |||||
) | |||||
# Valid hex but invalid proof | # Valid hex but invalid proof | ||||
assert_raises_rpc_error(-22, "Proof has invalid format", | assert_raises_rpc_error( | ||||
node.decodeavalancheproof, proof[:-2]) | -22, "Proof has invalid format", node.decodeavalancheproof, proof[:-2] | ||||
) | |||||
decoded_proof = node.decodeavalancheproof(proof) | decoded_proof = node.decodeavalancheproof(proof) | ||||
assert_equal( | assert_equal(decoded_proof["sequence"], proof_sequence) | ||||
decoded_proof["sequence"], | assert_equal(decoded_proof["expiration"], proof_expiration) | ||||
proof_sequence) | |||||
assert_equal( | |||||
decoded_proof["expiration"], | |||||
proof_expiration) | |||||
assert_equal(decoded_proof["master"], proof_master) | assert_equal(decoded_proof["master"], proof_master) | ||||
assert_equal(decoded_proof["payoutscript"], { | assert_equal( | ||||
"asm": "OP_DUP OP_HASH160 0000000000000000000000000000000000000000 OP_EQUALVERIFY OP_CHECKSIG", | decoded_proof["payoutscript"], | ||||
{ | |||||
"asm": ( | |||||
"OP_DUP OP_HASH160 0000000000000000000000000000000000000000" | |||||
" OP_EQUALVERIFY OP_CHECKSIG" | |||||
), | |||||
"hex": "76a914000000000000000000000000000000000000000088ac", | "hex": "76a914000000000000000000000000000000000000000088ac", | ||||
"reqSigs": 1, | "reqSigs": 1, | ||||
"type": "pubkeyhash", | "type": "pubkeyhash", | ||||
"addresses": [ADDRESS_ECREG_UNSPENDABLE], | "addresses": [ADDRESS_ECREG_UNSPENDABLE], | ||||
}) | }, | ||||
) | |||||
proofobj = FromHex(AvalancheProof(), proof) | proofobj = FromHex(AvalancheProof(), proof) | ||||
limited_id_hex = uint256_hex(proofobj.limited_proofid) | limited_id_hex = uint256_hex(proofobj.limited_proofid) | ||||
proofid_hex = uint256_hex(proofobj.proofid) | proofid_hex = uint256_hex(proofobj.proofid) | ||||
assert_equal( | assert_equal( | ||||
decoded_proof["signature"], | decoded_proof["signature"], | ||||
base64.b64encode(proofobj.signature).decode("ascii")) | base64.b64encode(proofobj.signature).decode("ascii"), | ||||
assert_equal( | ) | ||||
decoded_proof["proofid"], | assert_equal(decoded_proof["proofid"], uint256_hex(proofobj.proofid)) | ||||
uint256_hex(proofobj.proofid)) | assert_equal(decoded_proof["limitedid"], uint256_hex(proofobj.limited_proofid)) | ||||
assert_equal( | assert_equal(decoded_proof["staked_amount"], Decimal("50000000.00")) | ||||
decoded_proof["limitedid"], | assert_equal(decoded_proof["score"], 5000) | ||||
uint256_hex(proofobj.limited_proofid)) | assert_equal(decoded_proof["stakes"][0]["txid"], stakes[0]["txid"]) | ||||
assert_equal( | assert_equal(decoded_proof["stakes"][0]["vout"], stakes[0]["vout"]) | ||||
decoded_proof["staked_amount"], | assert_equal(decoded_proof["stakes"][0]["height"], stakes[0]["height"]) | ||||
Decimal('50000000.00')) | assert_equal(decoded_proof["stakes"][0]["iscoinbase"], stakes[0]["iscoinbase"]) | ||||
assert_equal( | assert_equal(decoded_proof["stakes"][0]["address"], node_ecash_addr) | ||||
decoded_proof["score"], | |||||
5000) | |||||
assert_equal( | |||||
decoded_proof["stakes"][0]["txid"], | |||||
stakes[0]["txid"]) | |||||
assert_equal( | |||||
decoded_proof["stakes"][0]["vout"], | |||||
stakes[0]["vout"]) | |||||
assert_equal( | |||||
decoded_proof["stakes"][0]["height"], | |||||
stakes[0]["height"]) | |||||
assert_equal( | |||||
decoded_proof["stakes"][0]["iscoinbase"], | |||||
stakes[0]["iscoinbase"]) | |||||
assert_equal( | |||||
decoded_proof["stakes"][0]["address"], | |||||
node_ecash_addr) | |||||
assert_equal( | assert_equal( | ||||
decoded_proof["stakes"][0]["signature"], | decoded_proof["stakes"][0]["signature"], | ||||
base64.b64encode(proofobj.stakes[0].sig).decode("ascii")) | base64.b64encode(proofobj.stakes[0].sig).decode("ascii"), | ||||
) | |||||
# Restart the node with this proof | # Restart the node with this proof | ||||
self.restart_node(0, self.extra_args[0] + [ | self.restart_node( | ||||
0, | |||||
self.extra_args[0] | |||||
+ [ | |||||
f"-avaproof={proof}", | f"-avaproof={proof}", | ||||
"-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", | "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", | ||||
]) | ], | ||||
) | |||||
self.log.info( | self.log.info( | ||||
"The proof is registered at first chaintip update if we have inbounds") | "The proof is registered at first chaintip update if we have inbounds" | ||||
) | |||||
assert_equal(len(node.getavalanchepeerinfo()), 0) | assert_equal(len(node.getavalanchepeerinfo()), 0) | ||||
self.generate(node, 1, sync_fun=self.no_op) | self.generate(node, 1, sync_fun=self.no_op) | ||||
node.syncwithvalidationinterfacequeue() | node.syncwithvalidationinterfacequeue() | ||||
assert_equal(len(node.getavalanchepeerinfo()), 0) | assert_equal(len(node.getavalanchepeerinfo()), 0) | ||||
# Add an inbound and check it now registers the proof | # Add an inbound and check it now registers the proof | ||||
node.add_p2p_connection(P2PInterface()) | node.add_p2p_connection(P2PInterface()) | ||||
self.generate(node, 1, sync_fun=self.no_op) | self.generate(node, 1, sync_fun=self.no_op) | ||||
self.wait_until(lambda: len(node.getavalanchepeerinfo()) == 1) | self.wait_until(lambda: len(node.getavalanchepeerinfo()) == 1) | ||||
# This case will occur for users building proofs with a third party | # 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 | # tool and then starting a new node that is not yet aware of the | ||||
# transactions used for stakes. | # transactions used for stakes. | ||||
self.log.info("Start a node with an immature proof") | self.log.info("Start a node with an immature proof") | ||||
stake_age = node.getblockcount() + 2 | stake_age = node.getblockcount() + 2 | ||||
self.restart_node(1, self.extra_args[0] + [ | self.restart_node( | ||||
1, | |||||
self.extra_args[0] | |||||
+ [ | |||||
f"-avaproofstakeutxoconfirmations={stake_age}", | f"-avaproofstakeutxoconfirmations={stake_age}", | ||||
f"-avaproof={proof}", | f"-avaproof={proof}", | ||||
"-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", | "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", | ||||
]) | ], | ||||
) | |||||
self.connect_nodes(1, node.index) | self.connect_nodes(1, node.index) | ||||
self.sync_blocks() | self.sync_blocks() | ||||
# Add an inbound so the node proof can be registered and advertised | # Add an inbound so the node proof can be registered and advertised | ||||
self.nodes[1].add_p2p_connection(P2PInterface()) | self.nodes[1].add_p2p_connection(P2PInterface()) | ||||
# Mine a block to trigger an attempt at registering the proof | # Mine a block to trigger an attempt at registering the proof | ||||
self.generate(self.nodes[1], 1, sync_fun=self.no_op) | self.generate(self.nodes[1], 1, sync_fun=self.no_op) | ||||
wait_for_proof(self.nodes[1], proofid_hex, expect_status="immature") | wait_for_proof(self.nodes[1], proofid_hex, expect_status="immature") | ||||
# Mine another block to make the proof mature | # Mine another block to make the proof mature | ||||
self.generate(self.nodes[1], 1, sync_fun=self.no_op) | self.generate(self.nodes[1], 1, sync_fun=self.no_op) | ||||
self.wait_until(lambda: self.nodes[1].getrawavalancheproof( | self.wait_until( | ||||
proofid_hex)["boundToPeer"] is True) | lambda: self.nodes[1].getrawavalancheproof(proofid_hex)["boundToPeer"] | ||||
is True | |||||
) | |||||
self.log.info( | self.log.info("Generate delegations for the proof, verify and decode them") | ||||
"Generate delegations for the proof, verify and decode them") | |||||
# Stack up a few delegation levels | # Stack up a few delegation levels | ||||
def gen_privkey(): | def gen_privkey(): | ||||
pk = ECKey() | pk = ECKey() | ||||
pk.generate() | pk.generate() | ||||
return pk | return pk | ||||
delegator_privkey = privkey | delegator_privkey = privkey | ||||
delegation = None | delegation = None | ||||
for i in range(MAX_DELEGATION_LEVELS): | for i in range(MAX_DELEGATION_LEVELS): | ||||
delegated_privkey = gen_privkey() | delegated_privkey = gen_privkey() | ||||
delegated_pubkey = get_hex_pubkey(delegated_privkey) | delegated_pubkey = get_hex_pubkey(delegated_privkey) | ||||
delegation = node.delegateavalancheproof( | delegation = node.delegateavalancheproof( | ||||
limited_id_hex, | limited_id_hex, | ||||
bytes_to_wif(delegator_privkey.get_bytes()), | bytes_to_wif(delegator_privkey.get_bytes()), | ||||
delegated_pubkey, | delegated_pubkey, | ||||
delegation, | delegation, | ||||
) | ) | ||||
assert node.verifyavalanchedelegation(delegation) | assert node.verifyavalanchedelegation(delegation) | ||||
dg_info = node.decodeavalanchedelegation(delegation) | dg_info = node.decodeavalanchedelegation(delegation) | ||||
assert_equal(dg_info['pubkey'], delegated_pubkey) | assert_equal(dg_info["pubkey"], delegated_pubkey) | ||||
assert_equal(dg_info['proofmaster'], proof_master) | assert_equal(dg_info["proofmaster"], proof_master) | ||||
assert 'delegationid' in dg_info.keys() | assert "delegationid" in dg_info.keys() | ||||
assert_equal(dg_info['limitedid'], limited_id_hex) | assert_equal(dg_info["limitedid"], limited_id_hex) | ||||
assert_equal(dg_info['proofid'], proofid_hex) | assert_equal(dg_info["proofid"], proofid_hex) | ||||
assert_equal(dg_info['depth'], i + 1) | assert_equal(dg_info["depth"], i + 1) | ||||
assert_equal(len(dg_info['levels']), dg_info['depth']) | assert_equal(len(dg_info["levels"]), dg_info["depth"]) | ||||
assert_equal(dg_info['levels'][-1]['pubkey'], delegated_pubkey) | assert_equal(dg_info["levels"][-1]["pubkey"], delegated_pubkey) | ||||
assert 'signature' in dg_info['levels'][-1] | assert "signature" in dg_info["levels"][-1] | ||||
delegator_privkey = delegated_privkey | delegator_privkey = delegated_privkey | ||||
self.log.info("Check the delegation levels are limited") | self.log.info("Check the delegation levels are limited") | ||||
too_many_levels_privkey = gen_privkey() | too_many_levels_privkey = gen_privkey() | ||||
too_many_levels_delegation = node.delegateavalancheproof( | too_many_levels_delegation = node.delegateavalancheproof( | ||||
limited_id_hex, | limited_id_hex, | ||||
bytes_to_wif(delegator_privkey.get_bytes()), | bytes_to_wif(delegator_privkey.get_bytes()), | ||||
get_hex_pubkey(too_many_levels_privkey), | get_hex_pubkey(too_many_levels_privkey), | ||||
delegation, | delegation, | ||||
) | ) | ||||
assert_raises_rpc_error(-8, | assert_raises_rpc_error( | ||||
-8, | |||||
"too-many-levels", | "too-many-levels", | ||||
node.verifyavalanchedelegation, | node.verifyavalanchedelegation, | ||||
too_many_levels_delegation) | too_many_levels_delegation, | ||||
) | |||||
random_privkey = gen_privkey() | random_privkey = gen_privkey() | ||||
random_pubkey = get_hex_pubkey(random_privkey) | random_pubkey = get_hex_pubkey(random_privkey) | ||||
# Invalid proof | # Invalid proof | ||||
no_stake = node.buildavalancheproof(proof_sequence, proof_expiration, | no_stake = node.buildavalancheproof( | ||||
wif_privkey, []) | proof_sequence, proof_expiration, wif_privkey, [] | ||||
) | |||||
# Invalid privkey | # Invalid privkey | ||||
assert_raises_rpc_error(-5, "The private key is invalid", | assert_raises_rpc_error( | ||||
-5, | |||||
"The private key is invalid", | |||||
node.delegateavalancheproof, | node.delegateavalancheproof, | ||||
limited_id_hex, | limited_id_hex, | ||||
bytes_to_wif(bytes(32)), | bytes_to_wif(bytes(32)), | ||||
random_pubkey, | random_pubkey, | ||||
) | ) | ||||
# Invalid delegation | # Invalid delegation | ||||
bad_dg = AvalancheDelegation() | bad_dg = AvalancheDelegation() | ||||
assert_raises_rpc_error(-8, "The delegation does not match the proof", | assert_raises_rpc_error( | ||||
-8, | |||||
"The delegation does not match the proof", | |||||
node.delegateavalancheproof, | node.delegateavalancheproof, | ||||
limited_id_hex, | limited_id_hex, | ||||
bytes_to_wif(privkey.get_bytes()), | bytes_to_wif(privkey.get_bytes()), | ||||
random_pubkey, | random_pubkey, | ||||
bad_dg.serialize().hex(), | bad_dg.serialize().hex(), | ||||
) | ) | ||||
# Still invalid, but with a matching proofid | # Still invalid, but with a matching proofid | ||||
bad_dg.limited_proofid = proofobj.limited_proofid | bad_dg.limited_proofid = proofobj.limited_proofid | ||||
bad_dg.proof_master = proofobj.master | bad_dg.proof_master = proofobj.master | ||||
bad_dg.levels = [AvalancheDelegationLevel()] | bad_dg.levels = [AvalancheDelegationLevel()] | ||||
assert_raises_rpc_error(-8, "The delegation is invalid", | assert_raises_rpc_error( | ||||
-8, | |||||
"The delegation is invalid", | |||||
node.delegateavalancheproof, | node.delegateavalancheproof, | ||||
limited_id_hex, | limited_id_hex, | ||||
bytes_to_wif(privkey.get_bytes()), | bytes_to_wif(privkey.get_bytes()), | ||||
random_pubkey, | random_pubkey, | ||||
bad_dg.serialize().hex(), | bad_dg.serialize().hex(), | ||||
) | ) | ||||
# Wrong privkey, match the proof but does not match the delegation | # Wrong privkey, match the proof but does not match the delegation | ||||
assert_raises_rpc_error(-5, "The private key does not match the delegation", | assert_raises_rpc_error( | ||||
-5, | |||||
"The private key does not match the delegation", | |||||
node.delegateavalancheproof, | node.delegateavalancheproof, | ||||
limited_id_hex, | limited_id_hex, | ||||
bytes_to_wif(privkey.get_bytes()), | bytes_to_wif(privkey.get_bytes()), | ||||
random_pubkey, | random_pubkey, | ||||
delegation, | delegation, | ||||
) | ) | ||||
# Delegation not hex | # Delegation not hex | ||||
assert_raises_rpc_error(-22, "Delegation must be an hexadecimal string.", | assert_raises_rpc_error( | ||||
-22, | |||||
"Delegation must be an hexadecimal string.", | |||||
node.delegateavalancheproof, | node.delegateavalancheproof, | ||||
limited_id_hex, | limited_id_hex, | ||||
bytes_to_wif(privkey.get_bytes()), | bytes_to_wif(privkey.get_bytes()), | ||||
random_pubkey, | random_pubkey, | ||||
"f00", | "f00", | ||||
) | ) | ||||
# Delegation is hex but ill-formed | # Delegation is hex but ill-formed | ||||
assert_raises_rpc_error(-22, "Delegation has invalid format", | assert_raises_rpc_error( | ||||
-22, | |||||
"Delegation has invalid format", | |||||
node.delegateavalancheproof, | node.delegateavalancheproof, | ||||
limited_id_hex, | limited_id_hex, | ||||
bytes_to_wif(privkey.get_bytes()), | bytes_to_wif(privkey.get_bytes()), | ||||
random_pubkey, | random_pubkey, | ||||
"dead", | "dead", | ||||
) | ) | ||||
# Test invalid proofs | # Test invalid proofs | ||||
dust = node.buildavalancheproof( | dust = node.buildavalancheproof( | ||||
proof_sequence, proof_expiration, wif_privkey, | proof_sequence, | ||||
create_coinbase_stakes(node, [blockhashes[0]], addrkey0.key, amount="0")) | proof_expiration, | ||||
wif_privkey, | |||||
create_coinbase_stakes(node, [blockhashes[0]], addrkey0.key, amount="0"), | |||||
) | |||||
dust2 = node.buildavalancheproof( | dust2 = node.buildavalancheproof( | ||||
proof_sequence, proof_expiration, wif_privkey, | proof_sequence, | ||||
create_coinbase_stakes(node, [blockhashes[0]], addrkey0.key, | proof_expiration, | ||||
amount=f"{PROOF_DUST_THRESHOLD * 0.9999:.2f}")) | wif_privkey, | ||||
create_coinbase_stakes( | |||||
node, | |||||
[blockhashes[0]], | |||||
addrkey0.key, | |||||
amount=f"{PROOF_DUST_THRESHOLD * 0.9999:.2f}", | |||||
), | |||||
) | |||||
missing_stake = node.buildavalancheproof( | missing_stake = node.buildavalancheproof( | ||||
proof_sequence, proof_expiration, wif_privkey, [{ | proof_sequence, | ||||
'txid': '0' * 64, | proof_expiration, | ||||
'vout': 0, | wif_privkey, | ||||
'amount': 10000000, | [ | ||||
'height': 42, | { | ||||
'iscoinbase': False, | "txid": "0" * 64, | ||||
'privatekey': addrkey0.key, | "vout": 0, | ||||
}] | "amount": 10000000, | ||||
"height": 42, | |||||
"iscoinbase": False, | |||||
"privatekey": addrkey0.key, | |||||
} | |||||
], | |||||
) | ) | ||||
# The hardcoded proofs are extracted from proof_tests.cpp | # The hardcoded proofs are extracted from proof_tests.cpp | ||||
duplicate_stake = ( | duplicate_stake = ( | ||||
"c964aa6fde575e4ce8404581c7be874e21023beefdde700a6bc02036335b4df141" | "c964aa6fde575e4ce8404581c7be874e21023beefdde700a6bc02036335b4df141" | ||||
"c8bc67bb05a971f5ac2745fd683797dde302d1e26c2287948bc6ab2b55945c591b" | "c8bc67bb05a971f5ac2745fd683797dde302d1e26c2287948bc6ab2b55945c591b" | ||||
"8ba3ffa237f5d9164d30a4f10145a61f788e639b1480731e2aead30500bf846287" | "8ba3ffa237f5d9164d30a4f10145a61f788e639b1480731e2aead30500bf846287" | ||||
"2102449fb5237efe8f647d32e8b64f06c22d1d40368eaca2a71ffc6a13ecc8bce6" | "2102449fb5237efe8f647d32e8b64f06c22d1d40368eaca2a71ffc6a13ecc8bce6" | ||||
Show All 35 Lines | def run_test(self): | ||||
"f0dca8316fb119ab8a605ff7f5b866df91c1b3df17810b943253b38ac6fa1d8bfc" | "f0dca8316fb119ab8a605ff7f5b866df91c1b3df17810b943253b38ac6fa1d8bfc" | ||||
"cc1cc1151ff91e7d7d531c88911976a91400000000000000000000000000000000" | "cc1cc1151ff91e7d7d531c88911976a91400000000000000000000000000000000" | ||||
"0000000088acc07fe1333c229902879510ef5c5081d793451aa88253a77b69688d" | "0000000088acc07fe1333c229902879510ef5c5081d793451aa88253a77b69688d" | ||||
"01206c035b1c6a0db54d60bd840862030e9a2e35b2f82d92c0e0298e2cde435974" | "01206c035b1c6a0db54d60bd840862030e9a2e35b2f82d92c0e0298e2cde435974" | ||||
"998d95ed" | "998d95ed" | ||||
) | ) | ||||
expired = node.buildavalancheproof( | expired = node.buildavalancheproof( | ||||
proof_sequence, 1, wif_privkey, | proof_sequence, | ||||
create_coinbase_stakes(node, [blockhashes[0]], addrkey0.key)) | 1, | ||||
wif_privkey, | |||||
create_coinbase_stakes(node, [blockhashes[0]], addrkey0.key), | |||||
) | |||||
self.log.info( | self.log.info("Check the verifyavalancheproof and sendavalancheproof RPCs") | ||||
"Check the verifyavalancheproof and sendavalancheproof RPCs") | |||||
if self.is_wallet_compiled(): | if self.is_wallet_compiled(): | ||||
self.log.info( | self.log.info("Check a proof with the maximum number of UTXO is valid") | ||||
"Check a proof with the maximum number of UTXO is valid") | |||||
new_blocks = self.generate( | new_blocks = self.generate( | ||||
node, AVALANCHE_MAX_PROOF_STAKES // 10 + 1, sync_fun=self.no_op) | node, AVALANCHE_MAX_PROOF_STAKES // 10 + 1, sync_fun=self.no_op | ||||
) | |||||
# confirm the coinbase UTXOs | # confirm the coinbase UTXOs | ||||
self.generate(node, 101, sync_fun=self.no_op) | self.generate(node, 101, sync_fun=self.no_op) | ||||
too_many_stakes = create_stakes( | too_many_stakes = create_stakes( | ||||
self, node, new_blocks, AVALANCHE_MAX_PROOF_STAKES + 1) | self, node, new_blocks, AVALANCHE_MAX_PROOF_STAKES + 1 | ||||
) | |||||
# Make the newly split UTXOs mature | # Make the newly split UTXOs mature | ||||
self.generate(node, stake_age, sync_fun=self.no_op) | self.generate(node, stake_age, sync_fun=self.no_op) | ||||
maximum_stakes = too_many_stakes[:-1] | maximum_stakes = too_many_stakes[:-1] | ||||
good_proof = node.buildavalancheproof( | good_proof = node.buildavalancheproof( | ||||
proof_sequence, proof_expiration, | proof_sequence, proof_expiration, wif_privkey, maximum_stakes | ||||
wif_privkey, maximum_stakes) | ) | ||||
too_many_utxos = node.buildavalancheproof( | too_many_utxos = node.buildavalancheproof( | ||||
proof_sequence, proof_expiration, | proof_sequence, proof_expiration, wif_privkey, too_many_stakes | ||||
wif_privkey, too_many_stakes) | ) | ||||
assert node.verifyavalancheproof(good_proof) | assert node.verifyavalancheproof(good_proof) | ||||
for rpc in [node.verifyavalancheproof, node.sendavalancheproof]: | for rpc in [node.verifyavalancheproof, node.sendavalancheproof]: | ||||
assert_raises_rpc_error(-22, "Proof must be an hexadecimal string", | assert_raises_rpc_error( | ||||
rpc, "f00") | -22, "Proof must be an hexadecimal string", rpc, "f00" | ||||
assert_raises_rpc_error(-22, "Proof has invalid format", | ) | ||||
rpc, "f00d") | assert_raises_rpc_error(-22, "Proof has invalid format", rpc, "f00d") | ||||
def check_rpc_failure(proof, message): | def check_rpc_failure(proof, message): | ||||
assert_raises_rpc_error(-8, f"The proof is invalid: {message}", | assert_raises_rpc_error( | ||||
rpc, proof) | -8, f"The proof is invalid: {message}", rpc, proof | ||||
) | |||||
check_rpc_failure(no_stake, "no-stake") | check_rpc_failure(no_stake, "no-stake") | ||||
check_rpc_failure(dust, "amount-below-dust-threshold") | check_rpc_failure(dust, "amount-below-dust-threshold") | ||||
check_rpc_failure(duplicate_stake, "duplicated-stake") | check_rpc_failure(duplicate_stake, "duplicated-stake") | ||||
check_rpc_failure(missing_stake, "utxo-missing-or-spent") | check_rpc_failure(missing_stake, "utxo-missing-or-spent") | ||||
check_rpc_failure(bad_sig, "invalid-stake-signature") | check_rpc_failure(bad_sig, "invalid-stake-signature") | ||||
check_rpc_failure(wrong_order, "wrong-stake-ordering") | check_rpc_failure(wrong_order, "wrong-stake-ordering") | ||||
check_rpc_failure(expired, "expired-proof") | check_rpc_failure(expired, "expired-proof") | ||||
if self.is_wallet_compiled(): | if self.is_wallet_compiled(): | ||||
check_rpc_failure(too_many_utxos, "too-many-utxos") | check_rpc_failure(too_many_utxos, "too-many-utxos") | ||||
conflicting_utxo = node.buildavalancheproof( | conflicting_utxo = node.buildavalancheproof( | ||||
proof_sequence - 1, proof_expiration, wif_privkey, stakes) | proof_sequence - 1, proof_expiration, wif_privkey, stakes | ||||
assert_raises_rpc_error(-8, "conflicting-utxos", | ) | ||||
node.sendavalancheproof, conflicting_utxo) | assert_raises_rpc_error( | ||||
-8, "conflicting-utxos", node.sendavalancheproof, conflicting_utxo | |||||
) | |||||
# Clear the proof pool | # Clear the proof pool | ||||
stake_age = node.getblockcount() | stake_age = node.getblockcount() | ||||
self.restart_node(0, self.extra_args[0] + [ | self.restart_node( | ||||
0, | |||||
self.extra_args[0] | |||||
+ [ | |||||
f"-avaproofstakeutxoconfirmations={stake_age}", | f"-avaproofstakeutxoconfirmations={stake_age}", | ||||
'-avalancheconflictingproofcooldown=0' | "-avalancheconflictingproofcooldown=0", | ||||
]) | ], | ||||
) | |||||
# Good proof | # Good proof | ||||
assert node.verifyavalancheproof(proof) | assert node.verifyavalancheproof(proof) | ||||
peer = node.add_p2p_connection(P2PInterface()) | peer = node.add_p2p_connection(P2PInterface()) | ||||
proofid = FromHex(AvalancheProof(), proof).proofid | proofid = FromHex(AvalancheProof(), proof).proofid | ||||
node.sendavalancheproof(proof) | node.sendavalancheproof(proof) | ||||
assert proofid in get_proof_ids(node) | assert proofid in get_proof_ids(node) | ||||
def inv_found(): | def inv_found(): | ||||
with p2p_lock: | with p2p_lock: | ||||
return peer.last_message.get( | return ( | ||||
"inv") and peer.last_message["inv"].inv[-1].hash == proofid | peer.last_message.get("inv") | ||||
and peer.last_message["inv"].inv[-1].hash == proofid | |||||
) | |||||
self.wait_until(inv_found) | self.wait_until(inv_found) | ||||
self.log.info("Check the getrawproof RPC") | self.log.info("Check the getrawproof RPC") | ||||
raw_proof = node.getrawavalancheproof(uint256_hex(proofid)) | raw_proof = node.getrawavalancheproof(uint256_hex(proofid)) | ||||
assert_equal(raw_proof['proof'], proof) | assert_equal(raw_proof["proof"], proof) | ||||
assert_equal(raw_proof['immature'], False) | assert_equal(raw_proof["immature"], False) | ||||
assert_equal(raw_proof['boundToPeer'], True) | assert_equal(raw_proof["boundToPeer"], True) | ||||
assert_equal(raw_proof['conflicting'], False) | assert_equal(raw_proof["conflicting"], False) | ||||
assert_equal(raw_proof['finalized'], False) | assert_equal(raw_proof["finalized"], False) | ||||
assert_raises_rpc_error(-8, "Proof not found", | assert_raises_rpc_error( | ||||
node.getrawavalancheproof, '0' * 64) | -8, "Proof not found", node.getrawavalancheproof, "0" * 64 | ||||
) | |||||
conflicting_proof = node.buildavalancheproof( | conflicting_proof = node.buildavalancheproof( | ||||
proof_sequence - 1, proof_expiration, wif_privkey, stakes) | proof_sequence - 1, proof_expiration, wif_privkey, stakes | ||||
) | |||||
conflicting_proofobj = avalanche_proof_from_hex(conflicting_proof) | conflicting_proofobj = avalanche_proof_from_hex(conflicting_proof) | ||||
conflicting_proofid_hex = uint256_hex(conflicting_proofobj.proofid) | conflicting_proofid_hex = uint256_hex(conflicting_proofobj.proofid) | ||||
msg = msg_avaproof() | msg = msg_avaproof() | ||||
msg.proof = conflicting_proofobj | msg.proof = conflicting_proofobj | ||||
peer.send_message(msg) | peer.send_message(msg) | ||||
wait_for_proof( | wait_for_proof(node, conflicting_proofid_hex, expect_status="conflicting") | ||||
node, | |||||
conflicting_proofid_hex, | |||||
expect_status="conflicting") | |||||
raw_proof = node.getrawavalancheproof(conflicting_proofid_hex) | raw_proof = node.getrawavalancheproof(conflicting_proofid_hex) | ||||
assert_equal(raw_proof['proof'], conflicting_proof) | assert_equal(raw_proof["proof"], conflicting_proof) | ||||
assert_equal(raw_proof['immature'], False) | assert_equal(raw_proof["immature"], False) | ||||
assert_equal(raw_proof['boundToPeer'], False) | assert_equal(raw_proof["boundToPeer"], False) | ||||
assert_equal(raw_proof['conflicting'], True) | assert_equal(raw_proof["conflicting"], True) | ||||
assert_equal(raw_proof['finalized'], False) | assert_equal(raw_proof["finalized"], False) | ||||
# Make the proof immature by switching to a shorter chain | # Make the proof immature by switching to a shorter chain | ||||
node.invalidateblock(node.getbestblockhash()) | node.invalidateblock(node.getbestblockhash()) | ||||
# Although the chaintip has changed, updatedBlockTip does not get | # Although the chaintip has changed, updatedBlockTip does not get | ||||
# called unless new chainwork needs evaluating, so invalidate another | # called unless new chainwork needs evaluating, so invalidate another | ||||
# block and then mine a new one. | # block and then mine a new one. | ||||
node.invalidateblock(node.getbestblockhash()) | node.invalidateblock(node.getbestblockhash()) | ||||
node.setmocktime( | node.setmocktime(node.getblock(node.getbestblockhash())["mediantime"] + 100) | ||||
node.getblock( | |||||
node.getbestblockhash())['mediantime'] + | |||||
100) | |||||
self.generate(node, 1, sync_fun=self.no_op) | self.generate(node, 1, sync_fun=self.no_op) | ||||
# Wait until UpdatedBlockTip has been called so we know the proof | # Wait until UpdatedBlockTip has been called so we know the proof | ||||
# validity has updated | # validity has updated | ||||
node.syncwithvalidationinterfacequeue() | node.syncwithvalidationinterfacequeue() | ||||
raw_proof = node.getrawavalancheproof(uint256_hex(proofid)) | raw_proof = node.getrawavalancheproof(uint256_hex(proofid)) | ||||
assert_equal(raw_proof['proof'], proof) | assert_equal(raw_proof["proof"], proof) | ||||
assert_equal(raw_proof['immature'], True) | assert_equal(raw_proof["immature"], True) | ||||
assert_equal(raw_proof['boundToPeer'], False) | assert_equal(raw_proof["boundToPeer"], False) | ||||
assert_equal(raw_proof['conflicting'], False) | assert_equal(raw_proof["conflicting"], False) | ||||
assert_equal(raw_proof['finalized'], False) | assert_equal(raw_proof["finalized"], False) | ||||
self.log.info("Bad proof should be rejected at startup") | self.log.info("Bad proof should be rejected at startup") | ||||
self.stop_node(0) | self.stop_node(0) | ||||
node.assert_start_raises_init_error( | node.assert_start_raises_init_error( | ||||
self.extra_args[0] + [ | self.extra_args[0] | ||||
+ [ | |||||
"-avasessionkey=0", | "-avasessionkey=0", | ||||
], | ], | ||||
expected_msg="Error: The avalanche session key is invalid.", | expected_msg="Error: The avalanche session key is invalid.", | ||||
) | ) | ||||
node.assert_start_raises_init_error( | node.assert_start_raises_init_error( | ||||
self.extra_args[0] + [ | self.extra_args[0] | ||||
+ [ | |||||
f"-avaproof={proof}", | f"-avaproof={proof}", | ||||
], | ], | ||||
expected_msg="Error: The avalanche master key is missing for the avalanche proof.", | expected_msg=( | ||||
"Error: The avalanche master key is missing for the avalanche proof." | |||||
), | |||||
) | ) | ||||
node.assert_start_raises_init_error( | node.assert_start_raises_init_error( | ||||
self.extra_args[0] + [ | self.extra_args[0] | ||||
+ [ | |||||
f"-avaproof={proof}", | f"-avaproof={proof}", | ||||
"-avamasterkey=0", | "-avamasterkey=0", | ||||
], | ], | ||||
expected_msg="Error: The avalanche master key is invalid.", | expected_msg="Error: The avalanche master key is invalid.", | ||||
) | ) | ||||
def check_proof_init_error(proof, message): | def check_proof_init_error(proof, message): | ||||
node.assert_start_raises_init_error( | node.assert_start_raises_init_error( | ||||
self.extra_args[0] + [ | self.extra_args[0] | ||||
+ [ | |||||
f"-avaproof={proof}", | f"-avaproof={proof}", | ||||
"-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", | "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", | ||||
], | ], | ||||
expected_msg=f"Error: {message}", | expected_msg=f"Error: {message}", | ||||
) | ) | ||||
check_proof_init_error(no_stake, | check_proof_init_error(no_stake, "The avalanche proof has no stake.") | ||||
"The avalanche proof has no stake.") | check_proof_init_error(dust, "The avalanche proof stake is too low.") | ||||
check_proof_init_error(dust, | check_proof_init_error(dust2, "The avalanche proof stake is too low.") | ||||
"The avalanche proof stake is too low.") | check_proof_init_error( | ||||
check_proof_init_error(dust2, | duplicate_stake, "The avalanche proof has duplicated stake." | ||||
"The avalanche proof stake is too low.") | ) | ||||
check_proof_init_error(duplicate_stake, | check_proof_init_error( | ||||
"The avalanche proof has duplicated stake.") | bad_sig, "The avalanche proof has invalid stake signatures." | ||||
check_proof_init_error(bad_sig, | ) | ||||
"The avalanche proof has invalid stake signatures.") | |||||
if self.is_wallet_compiled(): | if self.is_wallet_compiled(): | ||||
# The too many utxos case creates a proof which is that large that it | # The too many utxos case creates a proof which is that large that it | ||||
# cannot fit on the command line | # cannot fit on the command line | ||||
append_config(node.datadir, [f"avaproof={too_many_utxos}"]) | append_config(node.datadir, [f"avaproof={too_many_utxos}"]) | ||||
node.assert_start_raises_init_error( | node.assert_start_raises_init_error( | ||||
self.extra_args[0] + [ | self.extra_args[0] | ||||
+ [ | |||||
"-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", | "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", | ||||
], | ], | ||||
expected_msg="Error: The avalanche proof has too many utxos.", | expected_msg="Error: The avalanche proof has too many utxos.", | ||||
match=ErrorMatch.PARTIAL_REGEX, | match=ErrorMatch.PARTIAL_REGEX, | ||||
) | ) | ||||
# Master private key mismatch | # Master private key mismatch | ||||
random_privkey = ECKey() | random_privkey = ECKey() | ||||
random_privkey.generate() | random_privkey.generate() | ||||
node.assert_start_raises_init_error( | node.assert_start_raises_init_error( | ||||
self.extra_args[0] + [ | self.extra_args[0] | ||||
+ [ | |||||
f"-avaproof={proof}", | f"-avaproof={proof}", | ||||
f"-avamasterkey={bytes_to_wif(random_privkey.get_bytes())}", | f"-avamasterkey={bytes_to_wif(random_privkey.get_bytes())}", | ||||
], | ], | ||||
expected_msg="Error: The master key does not match the proof public key.", | expected_msg="Error: The master key does not match the proof public key.", | ||||
) | ) | ||||
self.log.info("Bad delegation should be rejected at startup") | self.log.info("Bad delegation should be rejected at startup") | ||||
def check_delegation_init_error(delegation, message): | def check_delegation_init_error(delegation, message): | ||||
node.assert_start_raises_init_error( | node.assert_start_raises_init_error( | ||||
self.extra_args[0] + [ | self.extra_args[0] | ||||
+ [ | |||||
f"-avadelegation={delegation}", | f"-avadelegation={delegation}", | ||||
f"-avaproof={proof}", | f"-avaproof={proof}", | ||||
f"-avamasterkey={bytes_to_wif(delegated_privkey.get_bytes())}", | f"-avamasterkey={bytes_to_wif(delegated_privkey.get_bytes())}", | ||||
# Prevent the node from adding a delegation level | # Prevent the node from adding a delegation level | ||||
f"-avasessionkey={bytes_to_wif(delegated_privkey.get_bytes())}", | f"-avasessionkey={bytes_to_wif(delegated_privkey.get_bytes())}", | ||||
], | ], | ||||
expected_msg=f"Error: {message}", | expected_msg=f"Error: {message}", | ||||
) | ) | ||||
check_delegation_init_error( | check_delegation_init_error( | ||||
AvalancheDelegation().serialize().hex(), | AvalancheDelegation().serialize().hex(), | ||||
"The delegation does not match the proof.") | "The delegation does not match the proof.", | ||||
) | |||||
bad_level_sig = FromHex(AvalancheDelegation(), delegation) | bad_level_sig = FromHex(AvalancheDelegation(), delegation) | ||||
# Tweak some key to cause the signature to mismatch | # Tweak some key to cause the signature to mismatch | ||||
bad_level_sig.levels[-2].pubkey = bytes.fromhex(proof_master) | bad_level_sig.levels[-2].pubkey = bytes.fromhex(proof_master) | ||||
check_delegation_init_error(bad_level_sig.serialize().hex(), | check_delegation_init_error( | ||||
"The avalanche delegation has invalid signatures.") | bad_level_sig.serialize().hex(), | ||||
"The avalanche delegation has invalid signatures.", | |||||
) | |||||
node.assert_start_raises_init_error( | node.assert_start_raises_init_error( | ||||
self.extra_args[0] + [ | self.extra_args[0] | ||||
+ [ | |||||
f"-avadelegation={delegation}", | f"-avadelegation={delegation}", | ||||
f"-avaproof={proof}", | f"-avaproof={proof}", | ||||
f"-avamasterkey={bytes_to_wif(random_privkey.get_bytes())}", | f"-avamasterkey={bytes_to_wif(random_privkey.get_bytes())}", | ||||
], | ], | ||||
expected_msg="Error: The master key does not match the delegation public key.", | expected_msg=( | ||||
"Error: The master key does not match the delegation public key." | |||||
), | |||||
) | ) | ||||
# The node stacks another delegation level at startup | # The node stacks another delegation level at startup | ||||
node.assert_start_raises_init_error( | node.assert_start_raises_init_error( | ||||
self.extra_args[0] + [ | self.extra_args[0] | ||||
+ [ | |||||
f"-avadelegation={delegation}", | f"-avadelegation={delegation}", | ||||
f"-avaproof={proof}", | f"-avaproof={proof}", | ||||
f"-avamasterkey={bytes_to_wif(delegated_privkey.get_bytes())}", | f"-avamasterkey={bytes_to_wif(delegated_privkey.get_bytes())}", | ||||
], | ], | ||||
expected_msg="Error: The avalanche delegation has too many delegation levels.", | expected_msg=( | ||||
"Error: The avalanche delegation has too many delegation levels." | |||||
), | |||||
) | ) | ||||
node.assert_start_raises_init_error( | node.assert_start_raises_init_error( | ||||
self.extra_args[0] + [ | self.extra_args[0] | ||||
+ [ | |||||
f"-avadelegation={too_many_levels_delegation}", | f"-avadelegation={too_many_levels_delegation}", | ||||
f"-avaproof={proof}", | f"-avaproof={proof}", | ||||
f"-avamasterkey={bytes_to_wif(too_many_levels_privkey.get_bytes())}", | f"-avamasterkey={bytes_to_wif(too_many_levels_privkey.get_bytes())}", | ||||
f"-avasessionkey={bytes_to_wif(too_many_levels_privkey.get_bytes())}", | f"-avasessionkey={bytes_to_wif(too_many_levels_privkey.get_bytes())}", | ||||
], | ], | ||||
expected_msg="Error: The avalanche delegation has too many delegation levels.", | expected_msg=( | ||||
"Error: The avalanche delegation has too many delegation levels." | |||||
), | |||||
) | ) | ||||
if __name__ == '__main__': | if __name__ == "__main__": | ||||
AvalancheProofTest().main() | AvalancheProofTest().main() |