|
61 | 61 | set_node_times, |
62 | 62 | SPORK_ACTIVATION_TIME, |
63 | 63 | SPORK_DEACTIVATION_TIME, |
64 | | - satoshi_round |
| 64 | + satoshi_round, |
| 65 | + wait_until, |
65 | 66 | ) |
66 | 67 |
|
67 | 68 | class TestStatus(Enum): |
@@ -1336,6 +1337,170 @@ def check_proreg_payload(self, dmn, json_tx): |
1336 | 1337 | assert_equal(pl["operatorPubKey"], dmn.operator_pk) |
1337 | 1338 | assert_equal(pl["payoutAddress"], dmn.payee) |
1338 | 1339 |
|
| 1340 | +# LLMQs |
| 1341 | + |
| 1342 | + def get_quorum_connections(self, node, members): |
| 1343 | + conn = [] |
| 1344 | + for peer in [p for p in node.getpeerinfo() if p["masternode"]]: |
| 1345 | + x = [m for m in members if m.proTx == peer["verif_mn_proreg_tx_hash"]] |
| 1346 | + if len(x) > 0: |
| 1347 | + conn.append(x[0].idx) |
| 1348 | + return conn |
| 1349 | + |
| 1350 | + def wait_for_mn_connections(self, members): |
| 1351 | + def count_mn_conn(n): |
| 1352 | + return len(self.get_quorum_connections(n, members)) |
| 1353 | + ql = len(members) |
| 1354 | + wait_until(lambda: [count_mn_conn(self.nodes[mn.idx]) for mn in members] == [ql - 1] * ql) |
| 1355 | + for mn in members: |
| 1356 | + conn = self.get_quorum_connections(self.nodes[mn.idx], members) |
| 1357 | + self.log.info("Authenticated connections to node %d: %s" % (mn.idx, conn)) |
| 1358 | + |
| 1359 | + def wait_for_dkg_phase(self, phase, quorum_hash, quorum_height, members_online, missing_count): |
| 1360 | + assert_greater_than(missing_count, -1) |
| 1361 | + online_count = len(members_online) |
| 1362 | + |
| 1363 | + def check_phase(): |
| 1364 | + status = [self.nodes[mn.idx].quorumdkgstatus()["session"] |
| 1365 | + for mn in members_online] |
| 1366 | + for s in status: |
| 1367 | + if "llmq_test" not in s: |
| 1368 | + return False |
| 1369 | + s = s["llmq_test"] |
| 1370 | + if (s["quorumHash"] != quorum_hash |
| 1371 | + or s["quorumHeight"] != quorum_height |
| 1372 | + or s["phase"] != phase |
| 1373 | + or s["sentContributions"] != (phase != 1) |
| 1374 | + or s["sentComplaint"] != (phase > 2 and missing_count > 0) |
| 1375 | + or s["sentJustification"] |
| 1376 | + or s["sentPrematureCommitment"] != (phase > 4) |
| 1377 | + or s["receivedContributions"] != (0 if phase == 1 else online_count) |
| 1378 | + or s["receivedComplaints"] != (0 if phase <= 2 else (online_count * missing_count)) |
| 1379 | + or s["receivedJustifications"] != 0 |
| 1380 | + or s["receivedPrematureCommitments"] != (0 if phase <= 4 else online_count)): |
| 1381 | + return False |
| 1382 | + return True |
| 1383 | + |
| 1384 | + timeout = time.time() + 60 |
| 1385 | + while time.time() < timeout: |
| 1386 | + if check_phase(): |
| 1387 | + return # all good |
| 1388 | + time.sleep(6) |
| 1389 | + |
| 1390 | + # Timeout: print the cause |
| 1391 | + self.log.error("Cannot reach phase %d" % phase) |
| 1392 | + for mo in members_online: |
| 1393 | + self.log.error("Checking node %d..." % mo.idx) |
| 1394 | + conn = self.get_quorum_connections(self.nodes[mo.idx], members_online) |
| 1395 | + self.log.error("Connected to: %s" % str(conn)) |
| 1396 | + fs = self.nodes[mo.idx].quorumdkgstatus()["session"] |
| 1397 | + assert "llmq_test" in fs |
| 1398 | + fs = fs["llmq_test"] |
| 1399 | + assert_equal(fs["quorumHash"], quorum_hash) |
| 1400 | + assert_equal(fs["quorumHeight"], quorum_height) |
| 1401 | + assert_equal(fs["phase"], phase) |
| 1402 | + assert_equal(fs["sentContributions"], (phase != 1)) |
| 1403 | + assert_equal(fs["sentComplaint"], (phase > 2 and missing_count > 0)) |
| 1404 | + assert_equal(fs["sentJustification"], False) |
| 1405 | + assert_equal(fs["sentPrematureCommitment"], (phase > 4)) |
| 1406 | + assert_equal(fs["receivedContributions"], (0 if phase == 1 else online_count)) |
| 1407 | + assert_equal(fs["receivedComplaints"], (0 if phase <= 2 else (online_count * missing_count))) |
| 1408 | + assert_equal(fs["receivedJustifications"], 0) |
| 1409 | + assert_equal(fs["receivedPrematureCommitments"], (0 if phase <= 4 else online_count)) |
| 1410 | + |
| 1411 | + def get_quorum_members(self, quorum_hash): |
| 1412 | + members = [] |
| 1413 | + # preserve getquorummembers order |
| 1414 | + for protx in self.nodes[self.minerPos].getquorummembers(100, quorum_hash): |
| 1415 | + members.append(next(mn for mn in self.mns if mn.proTx == protx)) |
| 1416 | + return members |
| 1417 | + |
| 1418 | + def check_final_commitment(self, qfc, valid, signers): |
| 1419 | + signersCount = 0 |
| 1420 | + signersBitStr = 0 |
| 1421 | + for i, s in enumerate(signers): |
| 1422 | + signersCount += s |
| 1423 | + signersBitStr += (s << i) |
| 1424 | + signersBitStr = "0%d" % signersBitStr |
| 1425 | + validCount = 0 |
| 1426 | + validBitStr = 0 |
| 1427 | + for i, s in enumerate(valid): |
| 1428 | + validCount += s |
| 1429 | + validBitStr += (s << i) |
| 1430 | + validBitStr = "0%d" % validBitStr |
| 1431 | + assert_equal(qfc['version'], 1) |
| 1432 | + assert_equal(qfc['llmqType'], 100) |
| 1433 | + assert_equal(qfc['signersCount'], signersCount) |
| 1434 | + assert_equal(qfc['signers'], signersBitStr) |
| 1435 | + assert_equal(qfc['validMembersCount'], validCount) |
| 1436 | + assert_equal(qfc['validMembers'], validBitStr) |
| 1437 | + |
| 1438 | + """ |
| 1439 | + Returns pair (qfc, bad_member) |
| 1440 | + qfc : quorum final commitment json object |
| 1441 | + bad_member : Masternode object for non participating node |
| 1442 | + (or None if all members participated) |
| 1443 | + """ |
| 1444 | + def mine_quorum(self, invalidate_func=None, invalidated_idx=None, skip_bad_member_sync=False): |
| 1445 | + nodes_to_sync = self.nodes.copy() |
| 1446 | + if invalidated_idx: |
| 1447 | + assert invalidate_func is None |
| 1448 | + if skip_bad_member_sync: |
| 1449 | + nodes_to_sync.pop(invalidated_idx) |
| 1450 | + miner = self.nodes[self.minerPos] |
| 1451 | + session_blocks = miner.getblockcount() % 20 |
| 1452 | + if session_blocks != 0: |
| 1453 | + miner.generate(20 - session_blocks) |
| 1454 | + self.sync_blocks(nodes_to_sync) |
| 1455 | + quorum_height = miner.getblockcount() |
| 1456 | + self.log.info("New DKG starts at block %d..." % quorum_height) |
| 1457 | + quorum_hash = miner.getblockhash(quorum_height) |
| 1458 | + members = self.get_quorum_members(quorum_hash) |
| 1459 | + self.log.info("members: %s" % str([m.idx for m in members])) |
| 1460 | + bad_member = None |
| 1461 | + mvalid = msigners = [1, 1, 1] |
| 1462 | + if invalidate_func is not None: |
| 1463 | + assert invalidated_idx is None |
| 1464 | + bad_member = members.pop() |
| 1465 | + invalidate_func(self.nodes[bad_member.idx]) |
| 1466 | + mvalid[2] = msigners[2] = 0 |
| 1467 | + if skip_bad_member_sync: |
| 1468 | + nodes_to_sync.pop(bad_member.idx) |
| 1469 | + elif invalidated_idx in [m.idx for m in members]: |
| 1470 | + bad_member = next(m for m in members if m.idx == invalidated_idx) |
| 1471 | + j = members.index(bad_member) |
| 1472 | + mvalid[j] = msigners[j] = 0 |
| 1473 | + members.remove(bad_member) |
| 1474 | + if bad_member is not None: |
| 1475 | + self.log.info("Removed node %d" % bad_member.idx) |
| 1476 | + assert_equal(len(members), 3 if bad_member is None else 2) |
| 1477 | + self.wait_for_mn_connections(members) |
| 1478 | + phase_string = [ |
| 1479 | + "1 (Initialization)", |
| 1480 | + "2 (Contribution)", |
| 1481 | + "3 (Complaining)", |
| 1482 | + "4 (Justification)", |
| 1483 | + "5 (Commitment/Finalization)", |
| 1484 | + "6 (Mining)" |
| 1485 | + ] |
| 1486 | + for phase in range(1, 7): |
| 1487 | + self.log.info("Phase %s - block %d" % (phase_string[phase-1], miner.getblockcount())) |
| 1488 | + self.wait_for_dkg_phase(phase, |
| 1489 | + quorum_hash, |
| 1490 | + quorum_height, |
| 1491 | + members, |
| 1492 | + missing_count=(0 if bad_member is None else 1)) |
| 1493 | + miner.generate(2 if phase != 6 else 1) |
| 1494 | + self.sync_all(nodes_to_sync) |
| 1495 | + time.sleep(1 if phase != 5 else 2) # sleep a bit more during finalization |
| 1496 | + assert_equal(quorum_height + 11, miner.getblockcount()) |
| 1497 | + qfc = miner.getminedcommitment(100, quorum_hash) |
| 1498 | + self.check_final_commitment(qfc, valid=mvalid, signers=msigners) |
| 1499 | + comm_height = miner.getblock(qfc['block_hash'], True)['height'] |
| 1500 | + assert_equal(comm_height, quorum_height + 11) |
| 1501 | + self.log.info("Final commitment correctly mined on chain") |
| 1502 | + return qfc, bad_member |
| 1503 | + |
1339 | 1504 |
|
1340 | 1505 | # ------------------------------------------------------ |
1341 | 1506 |
|
|
0 commit comments