dash/test/functional/feature_llmq_evo.py

337 lines
15 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
# Copyright (c) 2015-2024 The Dash Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
'''
feature_llmq_evo.py
Checks EvoNodes
'''
from _decimal import Decimal
from io import BytesIO
Merge #19760: test: Remove confusing mininode terminology d5800da5199527a366024bc80cad7fcca17d5c4a [test] Remove final references to mininode (John Newbery) 5e8df3312e47a73e747ee892face55ed9ababeea test: resort imports (John Newbery) 85165d4332b0f72d30e0c584b476249b542338e6 scripted-diff: Rename mininode to p2p (John Newbery) 9e2897d020b114a10c860f90c5405be029afddba scripted-diff: Rename mininode_lock to p2p_lock (John Newbery) Pull request description: New contributors are often confused by the terminology in the test framework, and what the difference between a _node_ and a _peer_ is. To summarize: - a 'node' is a bitcoind instance. This is the thing whose behavior is being tested. Each bitcoind node is managed by a python `TestNode` object which is used to start/stop the node, manage the node's data directory, read state about the node (eg process status, log file), and interact with the node over different interfaces. - one of the interfaces that we can use to interact with the node is the p2p interface. Each connection to a node using this interface is managed by a python `P2PInterface` or derived object (which is owned by the `TestNode` object). We can open zero, one or many p2p connections to each bitcoind node. The node sees these connections as 'peers'. For historic reasons, the word 'mininode' has been used to refer to those p2p interface objects that we use to connect to the bitcoind node (the code was originally taken from the 'mini-node' branch of https://github.com/jgarzik/pynode/tree/mini-node). However that name has proved to be confusing for new contributors, so rename the remaining references. ACKs for top commit: amitiuttarwar: ACK d5800da519 MarcoFalke: ACK d5800da5199527a366024bc80cad7fcca17d5c4a 🚞 Tree-SHA512: 2c46c2ac3c4278b6e3c647cfd8108428a41e80788fc4f0e386e5b0c47675bc687d94779496c09a3e5ea1319617295be10c422adeeff2d2bd68378e00e0eeb5de
2024-01-15 20:35:29 +01:00
from test_framework.p2p import P2PInterface
Merge bitcoin/bitcoin#22257: test: refactor: various (de)serialization helpers cleanups/improvements bdb8b9a347e68f80a2e8d44ce5590a2e8214b6bb test: doc: improve doc for `from_hex` helper (mention `to_hex` alternative) (Sebastian Falbesoner) 191405420815d49ab50184513717a303fc2744d6 scripted-diff: test: rename `FromHex` to `from_hex` (Sebastian Falbesoner) a79396fe5f8f81c78cf84117a87074c6ff6c9d95 test: remove `ToHex` helper, use .serialize().hex() instead (Sebastian Falbesoner) 2ce7b47958c4a10ba20dc86c011d71cda4b070a5 test: introduce `tx_from_hex` helper for tx deserialization (Sebastian Falbesoner) Pull request description: There are still many functional tests that perform conversions from a hex-string to a message object (deserialization) manually. This PR identifies all those instances and replaces them with a newly introduced helper `tx_from_hex`. Instances were found via * `git grep "deserialize.*BytesIO"` and some of them manually, when it were not one-liners. Further, the helper `ToHex` was removed and simply replaced by `.serialize().hex()`, since now both variants are in use (sometimes even within the same test) and using the helper doesn't really have an advantage in readability. (see discussion https://github.com/bitcoin/bitcoin/pull/22257#discussion_r652404782) ACKs for top commit: MarcoFalke: review re-ACK bdb8b9a347e68f80a2e8d44ce5590a2e8214b6bb 😁 Tree-SHA512: e25d7dc85918de1d6755a5cea65471b07a743204c20ad1c2f71ff07ef48cc1b9ad3fe5f515c1efaba2b2e3d89384e7980380c5d81895f9826e2046808cd3266e
2021-06-24 12:47:04 +02:00
from test_framework.messages import CBlock, CBlockHeader, CCbTx, CMerkleBlock, from_hex, hash256, msg_getmnlistd, \
QuorumId, ser_uint256
from test_framework.test_framework import DashTestFramework
from test_framework.util import (
assert_equal, assert_greater_than_or_equal, p2p_port
)
def extract_quorum_members(quorum_info):
return [d['proTxHash'] for d in quorum_info["members"]]
class TestP2PConn(P2PInterface):
def __init__(self):
super().__init__()
self.last_mnlistdiff = None
def on_mnlistdiff(self, message):
self.last_mnlistdiff = message
def wait_for_mnlistdiff(self, timeout=30):
def received_mnlistdiff():
return self.last_mnlistdiff is not None
return self.wait_until(received_mnlistdiff, timeout=timeout)
def getmnlistdiff(self, baseBlockHash, blockHash):
msg = msg_getmnlistd(baseBlockHash, blockHash)
self.last_mnlistdiff = None
self.send_message(msg)
self.wait_for_mnlistdiff()
return self.last_mnlistdiff
class LLMQEvoNodesTest(DashTestFramework):
def set_test_params(self):
self.set_dash_test_params(5, 4, fast_dip3_enforcement=True, evo_count=5)
self.set_dash_llmq_test_params(4, 4)
def run_test(self):
# Connect all nodes to node1 so that we always have the whole network connected
# Otherwise only masternode connections will be established between nodes, which won't propagate TXs/blocks
# Usually node0 is the one that does this, but in this test we isolate it multiple times
self.test_node = self.nodes[0].add_p2p_connection(TestP2PConn())
null_hash = format(0, "064x")
for i in range(len(self.nodes)):
self.connect_nodes(i, 0)
self.nodes[0].sporkupdate("SPORK_17_QUORUM_DKG_ENABLED", 0)
self.nodes[0].sporkupdate("SPORK_2_INSTANTSEND_ENABLED", 1)
self.wait_for_sporks_same()
expectedUpdated = [mn.proTxHash for mn in self.mninfo]
b_0 = self.nodes[0].getbestblockhash()
self.test_getmnlistdiff(null_hash, b_0, {}, [], expectedUpdated)
self.log.info("Test that EvoNodes registration is rejected before v19")
self.test_evo_is_rejected_before_v19()
self.test_masternode_count(expected_mns_count=4, expected_evo_count=0)
self.activate_v19(expected_activation_height=900)
self.log.info("Activated v19 at height:" + str(self.nodes[0].getblockcount()))
self.nodes[0].sporkupdate("SPORK_2_INSTANTSEND_ENABLED", 0)
self.wait_for_sporks_same()
self.move_to_next_cycle()
self.log.info("Cycle H height:" + str(self.nodes[0].getblockcount()))
self.move_to_next_cycle()
self.log.info("Cycle H+C height:" + str(self.nodes[0].getblockcount()))
self.move_to_next_cycle()
self.log.info("Cycle H+2C height:" + str(self.nodes[0].getblockcount()))
self.mine_cycle_quorum(llmq_type_name='llmq_test_dip0024', llmq_type=103)
evo_protxhash_list = list()
for i in range(self.evo_count):
evo_info = self.dynamically_add_masternode(evo=True)
evo_protxhash_list.append(evo_info.proTxHash)
self.nodes[0].generate(8)
self.sync_blocks(self.nodes)
expectedUpdated.append(evo_info.proTxHash)
b_i = self.nodes[0].getbestblockhash()
self.test_getmnlistdiff(null_hash, b_i, {}, [], expectedUpdated)
self.test_masternode_count(expected_mns_count=4, expected_evo_count=i+1)
self.dynamically_evo_update_service(evo_info)
self.log.info("Test llmq_platform are formed only with EvoNodes")
Merge #19674: refactor: test: use throwaway _ variable for unused loop counters dac7a111bdd3b0233d94cf68dae7a8bfc6ac9c64 refactor: test: use _ variable for unused loop counters (Sebastian Falbesoner) Pull request description: This tiny PR substitutes Python loops in the form of `for x in range(N): ...` by `for _ in range(N): ...` where applicable. The idea is indicating to the reader that a block (or statement, in list comprehensions) is just repeated N times, and that the loop counter is not used in the body, hence using the throwaway variable. This is already done quite often in the current tests (see e.g. `$ git grep "for _ in range("`). Another alternative would be using `itertools.repeat` (according to Python core developer Raymond Hettinger it's [even faster](https://twitter.com/raymondh/status/1144527183341375488)), but that doesn't seem to be widespread in use and I'm not sure about a readability increase. The only drawback I see is that whenever one wants to debug loop iterations, one would need to introduce a loop variable again. Reviewing this is basically a no-brainer, since tests would fail immediately if a a substitution has taken place on a loop where the variable is used. Instances to replace were found by `$ git grep "for.*in range("` and manually checked. ACKs for top commit: darosior: ACK dac7a111bdd3b0233d94cf68dae7a8bfc6ac9c64 instagibbs: manual inspection ACK https://github.com/bitcoin/bitcoin/pull/19674/commits/dac7a111bdd3b0233d94cf68dae7a8bfc6ac9c64 practicalswift: ACK dac7a111bdd3b0233d94cf68dae7a8bfc6ac9c64 -- the updated code is easier to reason about since the throwaway nature of a variable is expressed explicitly (using the Pythonic `_` idiom) instead of implicitly. Explicit is better than implicit was we all know by now :) Tree-SHA512: 5f43ded9ce14e5e00b3876ec445b90acda1842f813149ae7bafa93f3ac3d510bb778e2c701187fd2c73585e6b87797bb2d2987139bd1a9ba7d58775a59392406
2020-08-11 02:50:34 +02:00
for _ in range(3):
quorum_i_hash = self.mine_quorum(llmq_type_name='llmq_test_platform', llmq_type=106, expected_connections=2, expected_members=3, expected_contributions=3, expected_complaints=0, expected_justifications=0, expected_commitments=3 )
self.test_quorum_members_are_evo_nodes(quorum_i_hash, llmq_type=106)
self.log.info("Test that EvoNodes are present in MN list")
self.test_evo_protx_are_in_mnlist(evo_protxhash_list)
self.log.info("Test that EvoNodes are paid 4x blocks in a row")
self.test_evo_payments(window_analysis=48)
self.test_masternode_winners()
self.activate_mn_rr()
self.log.info("Activated MN RewardReallocation at height:" + str(self.nodes[0].getblockcount()))
# Generate a few blocks to make EvoNode/MN analysis on a pure MN RewardReallocation window
self.bump_mocktime(1)
self.nodes[0].generate(4)
self.sync_blocks()
self.log.info("Test that EvoNodes are paid 1 block in a row after MN RewardReallocation activation")
self.test_evo_payments(window_analysis=48, mnrr_active=True)
self.test_masternode_winners(mn_rr_active=True)
self.log.info(self.nodes[0].masternodelist())
return
def test_evo_payments(self, window_analysis, mnrr_active=False):
current_evo = None
consecutive_payments = 0
n_payments = 0 if mnrr_active else 4
for i in range(0, window_analysis):
payee = self.get_mn_payee_for_block(self.nodes[0].getbestblockhash())
if payee is not None and payee.evo:
if current_evo is not None and payee.proTxHash == current_evo.proTxHash:
# same EvoNode
assert consecutive_payments > 0
if not mnrr_active:
consecutive_payments += 1
consecutive_payments_rpc = self.nodes[0].protx('info', current_evo.proTxHash)['state']['consecutivePayments']
assert_equal(consecutive_payments, consecutive_payments_rpc)
else:
# new EvoNode
if current_evo is not None:
# make sure the old one was paid N times in a row
assert_equal(consecutive_payments, n_payments)
consecutive_payments_rpc = self.nodes[0].protx('info', current_evo.proTxHash)['state']['consecutivePayments']
# old EvoNode should have its nConsecutivePayments reset to 0
assert_equal(consecutive_payments_rpc, 0)
consecutive_payments_rpc = self.nodes[0].protx('info', payee.proTxHash)['state']['consecutivePayments']
# if EvoNode is the one we start "for" loop with,
# we have no idea how many times it was paid before - rely on rpc results here
new_payment_value = 0 if mnrr_active else 1
consecutive_payments = consecutive_payments_rpc if i == 0 and current_evo is None else new_payment_value
current_evo = payee
assert_equal(consecutive_payments, consecutive_payments_rpc)
else:
# not an EvoNode
if current_evo is not None:
# make sure the old one was paid N times in a row
assert_equal(consecutive_payments, n_payments)
consecutive_payments_rpc = self.nodes[0].protx('info', current_evo.proTxHash)['state']['consecutivePayments']
# old EvoNode should have its nConsecutivePayments reset to 0
assert_equal(consecutive_payments_rpc, 0)
current_evo = None
consecutive_payments = 0
self.nodes[0].generate(1)
if i % 8 == 0:
self.sync_blocks()
def get_mn_payee_for_block(self, block_hash):
mn_payee_info = self.nodes[0].masternode("payments", block_hash)[0]
mn_payee_protx = mn_payee_info['masternodes'][0]['proTxHash']
mninfos_online = self.mninfo.copy()
for mn_info in mninfos_online:
if mn_info.proTxHash == mn_payee_protx:
return mn_info
return None
def test_quorum_members_are_evo_nodes(self, quorum_hash, llmq_type):
quorum_info = self.nodes[0].quorum("info", llmq_type, quorum_hash)
quorum_members = extract_quorum_members(quorum_info)
mninfos_online = self.mninfo.copy()
for qm in quorum_members:
found = False
for mn in mninfos_online:
if mn.proTxHash == qm:
assert_equal(mn.evo, True)
found = True
break
assert_equal(found, True)
def test_evo_protx_are_in_mnlist(self, evo_protx_list):
mn_list = self.nodes[0].masternodelist()
for evo_protx in evo_protx_list:
found = False
for mn in mn_list:
if mn_list.get(mn)['proTxHash'] == evo_protx:
found = True
assert_equal(mn_list.get(mn)['type'], "Evo")
assert_equal(found, True)
def test_evo_is_rejected_before_v19(self):
bls = self.nodes[0].bls('generate')
collateral_address = self.nodes[0].getnewaddress()
funds_address = self.nodes[0].getnewaddress()
owner_address = self.nodes[0].getnewaddress()
voting_address = self.nodes[0].getnewaddress()
reward_address = self.nodes[0].getnewaddress()
collateral_amount = 4000
outputs = {collateral_address: collateral_amount, funds_address: 1}
collateral_txid = self.nodes[0].sendmany("", outputs)
self.nodes[0].generate(8)
self.sync_all(self.nodes)
rawtx = self.nodes[0].getrawtransaction(collateral_txid, 1)
collateral_vout = 0
for txout in rawtx['vout']:
if txout['value'] == Decimal(collateral_amount):
collateral_vout = txout['n']
break
assert collateral_vout is not None
ipAndPort = '127.0.0.1:%d' % p2p_port(len(self.nodes))
operatorReward = len(self.nodes)
try:
self.nodes[0].protx('register_evo', collateral_txid, collateral_vout, ipAndPort, owner_address, bls['public'], voting_address, operatorReward, reward_address, funds_address, True)
# this should never succeed
assert False
except:
self.log.info("protx_evo rejected")
def test_masternode_count(self, expected_mns_count, expected_evo_count):
mn_count = self.nodes[0].masternode('count')
assert_equal(mn_count['total'], expected_mns_count + expected_evo_count)
detailed_count = mn_count['detailed']
assert_equal(detailed_count['regular']['total'], expected_mns_count)
assert_equal(detailed_count['evo']['total'], expected_evo_count)
def test_masternode_winners(self, mn_rr_active=False):
# ignore recent winners, test future ones only
# we get up to 21 entries here: tip + up to 20 future payees
winners = self.nodes[0].masternode('winners', '0')
weighted_count = self.mn_count + self.evo_count * (1 if mn_rr_active else 4)
assert_equal(len(winners.keys()) - 1, 20 if weighted_count > 20 else weighted_count)
consecutive_payments = 0
full_consecutive_payments_found = 0
payment_cycles = 0
first_payee = None
prev_winner = None
for height in winners.keys():
winner = winners[height]
if mn_rr_active:
assert_equal(prev_winner == winner, False)
else:
if prev_winner == winner:
consecutive_payments += 1
else:
if consecutive_payments == 3:
full_consecutive_payments_found += 1
consecutive_payments = 0
assert_greater_than_or_equal(3, consecutive_payments)
if consecutive_payments == 0 and winner == first_payee:
payment_cycles += 1
if first_payee is None:
first_payee = winner
prev_winner = winner
if mn_rr_active:
assert_equal(full_consecutive_payments_found, 0)
else:
assert_greater_than_or_equal(full_consecutive_payments_found, (len(winners.keys()) - 1 - self.mn_count) // 4 - 1)
assert_equal(payment_cycles, (len(winners.keys()) - 1) // weighted_count)
def test_getmnlistdiff(self, baseBlockHash, blockHash, baseMNList, expectedDeleted, expectedUpdated):
d = self.test_getmnlistdiff_base(baseBlockHash, blockHash)
# Assert that the deletedMNs and mnList fields are what we expected
assert_equal(set(d.deletedMNs), set([int(e, 16) for e in expectedDeleted]))
assert_equal(set([e.proRegTxHash for e in d.mnList]), set(int(e, 16) for e in expectedUpdated))
# Build a new list based on the old list and the info from the diff
newMNList = baseMNList.copy()
for e in d.deletedMNs:
newMNList.pop(format(e, '064x'))
for e in d.mnList:
newMNList[format(e.proRegTxHash, '064x')] = e
cbtx = CCbTx()
cbtx.deserialize(BytesIO(d.cbTx.vExtraPayload))
# Verify that the merkle root matches what we locally calculate
hashes = []
for mn in sorted(newMNList.values(), key=lambda mn: ser_uint256(mn.proRegTxHash)):
feat: store protx version in CSimplifiedMNListEntry and use it to ser/deser pubKeyOperator (#5397) ## Issue being fixed or feature implemented Mobile wallets would have to convert 4k+ pubkeys at the V19 fork point and it's a pretty hard job for them that can easily take 10-15 seconds if not more. Also after the HF, if a masternode list is requested from before the HF, the operator keys come in basic scheme, but the merkelroot was calculated with legacy. From mobile team work it wasn't possible to convert all operator keys to legacy and then calculate the correct merkleroot. ~This PR builds on top of ~#5392~ #5403 (changes that belong to this PR: 26f7e966500bdea4c604f1d16716b40b366fc707 and 4b42dc8fcee3354afd82ce7e3a72ebe1659f5f22) and aims to solve both of these issues.~ cc @hashengineering @QuantumExplorer ## What was done? Introduce `nVersion` on p2p level for every CSimplifiedMNListEntry. Set `nVersion` to the same value we have it in CDeterministicMNState i.e. pubkey serialization would not be via basic scheme only after the V19 fork, it would match the way it’s serialized on-chain/in CDeterministicMNState for that specific MN. ## How Has This Been Tested? run tests ## Breaking Changes NOTE: `testnet` is going to re-fork at v19 forkpoint because `merkleRootMNList` is not going to match ## Checklist: - [x] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have added or updated relevant unit/integration/functional/e2e tests - [ ] I have made corresponding changes to the documentation - [ ] I have assigned this pull request to a milestone _(for repository code-owners and collaborators only)_
2023-06-11 19:29:00 +02:00
hashes.append(hash256(mn.serialize(with_version = False)))
merkleRoot = CBlock.get_merkle_root(hashes)
assert_equal(merkleRoot, cbtx.merkleRootMNList)
return newMNList
def test_getmnlistdiff_base(self, baseBlockHash, blockHash):
hexstr = self.nodes[0].getblockheader(blockHash, False)
Merge bitcoin/bitcoin#22257: test: refactor: various (de)serialization helpers cleanups/improvements bdb8b9a347e68f80a2e8d44ce5590a2e8214b6bb test: doc: improve doc for `from_hex` helper (mention `to_hex` alternative) (Sebastian Falbesoner) 191405420815d49ab50184513717a303fc2744d6 scripted-diff: test: rename `FromHex` to `from_hex` (Sebastian Falbesoner) a79396fe5f8f81c78cf84117a87074c6ff6c9d95 test: remove `ToHex` helper, use .serialize().hex() instead (Sebastian Falbesoner) 2ce7b47958c4a10ba20dc86c011d71cda4b070a5 test: introduce `tx_from_hex` helper for tx deserialization (Sebastian Falbesoner) Pull request description: There are still many functional tests that perform conversions from a hex-string to a message object (deserialization) manually. This PR identifies all those instances and replaces them with a newly introduced helper `tx_from_hex`. Instances were found via * `git grep "deserialize.*BytesIO"` and some of them manually, when it were not one-liners. Further, the helper `ToHex` was removed and simply replaced by `.serialize().hex()`, since now both variants are in use (sometimes even within the same test) and using the helper doesn't really have an advantage in readability. (see discussion https://github.com/bitcoin/bitcoin/pull/22257#discussion_r652404782) ACKs for top commit: MarcoFalke: review re-ACK bdb8b9a347e68f80a2e8d44ce5590a2e8214b6bb 😁 Tree-SHA512: e25d7dc85918de1d6755a5cea65471b07a743204c20ad1c2f71ff07ef48cc1b9ad3fe5f515c1efaba2b2e3d89384e7980380c5d81895f9826e2046808cd3266e
2021-06-24 12:47:04 +02:00
header = from_hex(CBlockHeader(), hexstr)
d = self.test_node.getmnlistdiff(int(baseBlockHash, 16), int(blockHash, 16))
assert_equal(d.baseBlockHash, int(baseBlockHash, 16))
assert_equal(d.blockHash, int(blockHash, 16))
# Check that the merkle proof is valid
proof = CMerkleBlock(header, d.merkleProof)
proof = proof.serialize().hex()
assert_equal(self.nodes[0].verifytxoutproof(proof), [d.cbTx.hash])
# Check if P2P messages match with RPCs
d2 = self.nodes[0].protx("diff", baseBlockHash, blockHash)
assert_equal(d2["baseBlockHash"], baseBlockHash)
assert_equal(d2["blockHash"], blockHash)
assert_equal(d2["cbTxMerkleTree"], d.merkleProof.serialize().hex())
assert_equal(d2["cbTx"], d.cbTx.serialize().hex())
assert_equal(set([int(e, 16) for e in d2["deletedMNs"]]), set(d.deletedMNs))
assert_equal(set([int(e["proRegTxHash"], 16) for e in d2["mnList"]]), set([e.proRegTxHash for e in d.mnList]))
assert_equal(set([QuorumId(e["llmqType"], int(e["quorumHash"], 16)) for e in d2["deletedQuorums"]]), set(d.deletedQuorums))
assert_equal(set([QuorumId(e["llmqType"], int(e["quorumHash"], 16)) for e in d2["newQuorums"]]), set([QuorumId(e.llmqType, e.quorumHash) for e in d.newQuorums]))
return d
if __name__ == '__main__':
LLMQEvoNodesTest().main()