forked from dashpay/dash
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathfeature_llmq_evo.py
executable file
·283 lines (235 loc) · 13 KB
/
feature_llmq_evo.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
#!/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 io import BytesIO
from test_framework.p2p import P2PInterface
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,
)
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, [["-testactivationheight=mn_rr@400"]] * 5, 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")
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.test_masternode_count(expected_mns_count=4, expected_evo_count=0)
self.nodes[0].sporkupdate("SPORK_2_INSTANTSEND_ENABLED", 0)
self.wait_for_sporks_same()
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.generate(self.nodes[0], 8, sync_fun=lambda: self.sync_blocks())
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")
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, mnrr_active=False)
self.test_masternode_winners()
self.activate_mn_rr()
self.log.info("Activated MN RewardReallocation, current 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.generate(self.nodes[0], 4, sync_fun=lambda: 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):
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.generate(self.nodes[0], 1, sync_fun=self.no_op)
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_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)):
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)
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()