fix!: incorrect CalcCbTxMerkleRootQuorums with rotation (#4833)

* Fix for CalcCbTxMerkleRootQuorums with rotation

* Added check for merkleRootQuorums in feature_llmq_rotation

* Correct logging

* Update test/functional/feature_llmq_rotation.py

Co-authored-by: UdjinM6 <UdjinM6@users.noreply.github.com>

* Update test/functional/feature_llmq_rotation.py

Co-authored-by: UdjinM6 <UdjinM6@users.noreply.github.com>

* lint: fix python linter

Co-authored-by: UdjinM6 <UdjinM6@users.noreply.github.com>
Co-authored-by: pasta <pasta@dashboost.org>
This commit is contained in:
Odysseas Gabrielides 2022-05-18 20:45:15 +03:00 committed by GitHub
parent bb4be52b48
commit 73cf7ff978
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 153 additions and 20 deletions

View File

@ -167,35 +167,33 @@ bool CalcCbTxMerkleRootQuorums(const CBlock& block, const CBlockIndex* pindexPre
int64_t nTime1 = GetTimeMicros();
static std::map<Consensus::LLMQType, std::vector<const CBlockIndex*>> quorumsCached;
static std::map<Consensus::LLMQType, std::vector<uint256>> qcHashesCached;
// The returned quorums are in reversed order, so the most recent one is at index 0
auto quorums = llmq::quorumBlockProcessor->GetMinedAndActiveCommitmentsUntilBlock(pindexPrev);
std::map<Consensus::LLMQType, std::vector<uint256>> qcHashes;
std::map<Consensus::LLMQType, std::map<int16_t, uint256>> qcIndexedHashes;
size_t hashCount = 0;
int64_t nTime2 = GetTimeMicros(); nTimeMinedAndActive += nTime2 - nTime1;
LogPrint(BCLog::BENCHMARK, " - GetMinedAndActiveCommitmentsUntilBlock: %.2fms [%.2fs]\n", 0.001 * (nTime2 - nTime1), nTimeMinedAndActive * 0.000001);
if (quorums == quorumsCached) {
qcHashes = qcHashesCached;
} else {
for (const auto& p : quorums) {
auto& v = qcHashes[p.first];
v.reserve(p.second.size());
for (const auto& p2 : p.second) {
uint256 minedBlockHash;
llmq::CFinalCommitmentPtr qc = llmq::quorumBlockProcessor->GetMinedCommitment(p.first, p2->GetBlockHash(), minedBlockHash);
if (qc == nullptr) return state.DoS(100, false, REJECT_INVALID, "commitment-not-found");
v.emplace_back(::SerializeHash(*qc));
hashCount++;
for (const auto& p : quorums) {
auto& v = qcHashes[p.first];
v.reserve(p.second.size());
for (const auto& p2 : p.second) {
uint256 minedBlockHash;
llmq::CFinalCommitmentPtr qc = llmq::quorumBlockProcessor->GetMinedCommitment(p.first, p2->GetBlockHash(), minedBlockHash);
if (qc == nullptr) return state.DoS(100, false, REJECT_INVALID, "commitment-not-found");
if (llmq::CLLMQUtils::IsQuorumRotationEnabled(qc->llmqType, pindexPrev)) {
auto& qi = qcIndexedHashes[p.first];
qi.insert(std::make_pair(qc->quorumIndex, ::SerializeHash(*qc)));
continue;
}
v.emplace_back(::SerializeHash(*qc));
hashCount++;
}
quorumsCached = quorums;
qcHashesCached = qcHashes;
}
int64_t nTime3 = GetTimeMicros(); nTimeMined += nTime3 - nTime2;
LogPrint(BCLog::BENCHMARK, " - GetMinedCommitment: %.2fms [%.2fs]\n", 0.001 * (nTime3 - nTime2), nTimeMined * 0.000001);
@ -215,6 +213,11 @@ bool CalcCbTxMerkleRootQuorums(const CBlock& block, const CBlockIndex* pindexPre
auto qcHash = ::SerializeHash(qc.commitment);
const auto& llmq_params = llmq::GetLLMQParams(qc.commitment.llmqType);
auto& v = qcHashes[llmq_params.type];
if (llmq::CLLMQUtils::IsQuorumRotationEnabled(qc.commitment.llmqType, pindexPrev)) {
auto& qi = qcIndexedHashes[qc.commitment.llmqType];
qi[qc.commitment.quorumIndex] = qcHash;
continue;
}
if (v.size() == size_t(llmq_params.signingActiveQuorumCount)) {
// we pop the last entry, which is actually the oldest quorum as GetMinedAndActiveCommitmentsUntilBlock
// returned quorums in reversed order. This pop and later push can only work ONCE, but we rely on the
@ -229,6 +232,16 @@ bool CalcCbTxMerkleRootQuorums(const CBlock& block, const CBlockIndex* pindexPre
}
}
if (!qcIndexedHashes.empty()) {
for (const auto& q : qcIndexedHashes) {
auto& v = qcHashes[q.first];
for (const auto& qq : q.second) {
v.emplace_back(qq.second);
hashCount++;
}
}
}
std::vector<uint256> qcHashesVec;
qcHashesVec.reserve(hashCount);

View File

@ -9,7 +9,11 @@ feature_llmq_rotation.py
Checks LLMQs Quorum Rotation
'''
from io import BytesIO
from test_framework.test_framework import DashTestFramework
from test_framework.messages import CBlock, CBlockHeader, CCbTx, CMerkleBlock, FromHex, hash256, msg_getmnlistd, QuorumId
from test_framework.mininode import P2PInterface
from test_framework.util import (
assert_equal,
assert_greater_than_or_equal,
@ -27,6 +31,25 @@ def intersection(lst1, lst2):
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 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 LLMQQuorumRotationTest(DashTestFramework):
def set_test_params(self):
@ -34,10 +57,11 @@ class LLMQQuorumRotationTest(DashTestFramework):
self.set_dash_llmq_test_params(4, 4)
def run_test(self):
llmq_type=103
llmq_type_name="llmq_test_dip0024"
self.test_node = self.nodes[0].add_p2p_connection(TestP2PConn())
# 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
@ -63,6 +87,8 @@ class LLMQQuorumRotationTest(DashTestFramework):
self.move_to_next_cycle()
self.log.info("Cycle H+2C height:" + str(self.nodes[0].getblockcount()))
b_0 = self.nodes[0].getbestblockhash()
(quorum_info_0_0, quorum_info_0_1) = self.mine_cycle_quorum(llmq_type_name=llmq_type_name, llmq_type=llmq_type)
quorum_members_0_0 = extract_quorum_members(quorum_info_0_0)
quorum_members_0_1 = extract_quorum_members(quorum_info_0_1)
@ -70,6 +96,17 @@ class LLMQQuorumRotationTest(DashTestFramework):
self.log.info("Quorum #0_0 members: " + str(quorum_members_0_0))
self.log.info("Quorum #0_1 members: " + str(quorum_members_0_1))
q_100_0 = QuorumId(100, int(quorum_info_0_0["quorumHash"], 16))
q_102_0 = QuorumId(102, int(quorum_info_0_0["quorumHash"], 16))
q_104_0 = QuorumId(104, int(quorum_info_0_0["quorumHash"], 16))
q_103_0_0 = QuorumId(103, int(quorum_info_0_0["quorumHash"], 16))
q_103_0_1 = QuorumId(103, int(quorum_info_0_1["quorumHash"], 16))
b_1 = self.nodes[0].getbestblockhash()
expectedDeleted = []
expectedNew = [q_100_0, q_102_0, q_104_0, q_103_0_0, q_103_0_1]
quorumList = self.test_getmnlistdiff_quorums(b_0, b_1, {}, expectedDeleted, expectedNew)
(quorum_info_1_0, quorum_info_1_1) = self.mine_cycle_quorum(llmq_type_name=llmq_type_name, llmq_type=llmq_type)
quorum_members_1_0 = extract_quorum_members(quorum_info_1_0)
quorum_members_1_1 = extract_quorum_members(quorum_info_1_1)
@ -77,6 +114,16 @@ class LLMQQuorumRotationTest(DashTestFramework):
self.log.info("Quorum #1_0 members: " + str(quorum_members_1_0))
self.log.info("Quorum #1_1 members: " + str(quorum_members_1_1))
q_100_1 = QuorumId(100, int(quorum_info_1_0["quorumHash"], 16))
q_102_1 = QuorumId(102, int(quorum_info_1_0["quorumHash"], 16))
q_103_1_0 = QuorumId(103, int(quorum_info_1_0["quorumHash"], 16))
q_103_1_1 = QuorumId(103, int(quorum_info_1_1["quorumHash"], 16))
b_2 = self.nodes[0].getbestblockhash()
expectedDeleted = [q_103_0_0, q_103_0_1]
expectedNew = [q_100_1, q_102_1, q_103_1_0, q_103_1_1]
quorumList = self.test_getmnlistdiff_quorums(b_1, b_2, quorumList, expectedDeleted, expectedNew)
(quorum_info_2_0, quorum_info_2_1) = self.mine_cycle_quorum(llmq_type_name=llmq_type_name, llmq_type=llmq_type)
quorum_members_2_0 = extract_quorum_members(quorum_info_2_0)
quorum_members_2_1 = extract_quorum_members(quorum_info_2_1)
@ -84,6 +131,16 @@ class LLMQQuorumRotationTest(DashTestFramework):
self.log.info("Quorum #2_0 members: " + str(quorum_members_2_0))
self.log.info("Quorum #2_1 members: " + str(quorum_members_2_1))
q_100_2 = QuorumId(100, int(quorum_info_2_0["quorumHash"], 16))
q_102_2 = QuorumId(102, int(quorum_info_2_0["quorumHash"], 16))
q_103_2_0 = QuorumId(103, int(quorum_info_2_0["quorumHash"], 16))
q_103_2_1 = QuorumId(103, int(quorum_info_2_1["quorumHash"], 16))
b_3 = self.nodes[0].getbestblockhash()
expectedDeleted = [q_100_0, q_102_0, q_103_1_0, q_103_1_1]
expectedNew = [q_100_2, q_102_2, q_103_2_0, q_103_2_1]
quorumList = self.test_getmnlistdiff_quorums(b_2, b_3, quorumList, expectedDeleted, expectedNew)
mninfos_online = self.mninfo.copy()
nodes = [self.nodes[0]] + [mn.node for mn in mninfos_online]
sync_blocks(nodes)
@ -101,7 +158,7 @@ class LLMQQuorumRotationTest(DashTestFramework):
assert_greater_than_or_equal(len(intersection(quorum_members_1_0, quorum_members_2_0)), 3)
assert_greater_than_or_equal(len(intersection(quorum_members_1_1, quorum_members_2_1)), 3)
self.log.info("mine a quorum to invalidate")
self.log.info("Mine a quorum to invalidate")
(quorum_info_3_0, quorum_info_3_1) = self.mine_cycle_quorum(llmq_type_name=llmq_type_name, llmq_type=llmq_type)
new_quorum_list = self.nodes[0].quorum("list", llmq_type)
@ -127,6 +184,59 @@ class LLMQQuorumRotationTest(DashTestFramework):
wait_until(lambda: self.nodes[0].getbestblockhash() == new_quorum_blockhash, sleep=1)
assert_equal(self.nodes[0].quorum("list", llmq_type), new_quorum_list)
def test_getmnlistdiff_quorums(self, baseBlockHash, blockHash, baseQuorumList, expectedDeleted, expectedNew):
d = self.test_getmnlistdiff_base(baseBlockHash, blockHash)
assert_equal(set(d.deletedQuorums), set(expectedDeleted))
assert_equal(set([QuorumId(e.llmqType, e.quorumHash) for e in d.newQuorums]), set(expectedNew))
newQuorumList = baseQuorumList.copy()
for e in d.deletedQuorums:
newQuorumList.pop(e)
for e in d.newQuorums:
newQuorumList[QuorumId(e.llmqType, e.quorumHash)] = e
cbtx = CCbTx()
cbtx.deserialize(BytesIO(d.cbTx.vExtraPayload))
if cbtx.version >= 2:
hashes = []
for qc in newQuorumList.values():
hashes.append(hash256(qc.serialize()))
hashes.sort()
merkleRoot = CBlock.get_merkle_root(hashes)
assert_equal(merkleRoot, cbtx.merkleRootQuorums)
return newQuorumList
def test_getmnlistdiff_base(self, baseBlockHash, blockHash):
hexstr = self.nodes[0].getblockheader(blockHash, False)
header = FromHex(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__':
LLMQQuorumRotationTest().main()

View File

@ -1103,7 +1103,7 @@ class CSimplifiedMNListEntry:
class CFinalCommitment:
__slots__ = ("nVersion", "llmqType", "quorumHash", "signers", "validMembers", "quorumPublicKey",
__slots__ = ("nVersion", "llmqType", "quorumHash", "quorumIndex", "signers", "validMembers", "quorumPublicKey",
"quorumVvecHash", "quorumSig", "membersSig")
def __init__(self):
@ -1113,6 +1113,7 @@ class CFinalCommitment:
self.nVersion = 0
self.llmqType = 0
self.quorumHash = 0
self.quorumIndex = 0
self.signers = []
self.validMembers = []
self.quorumPublicKey = b'\\x0' * 48
@ -1124,6 +1125,8 @@ class CFinalCommitment:
self.nVersion = struct.unpack("<H", f.read(2))[0]
self.llmqType = struct.unpack("<B", f.read(1))[0]
self.quorumHash = deser_uint256(f)
if self.nVersion == 2:
self.quorumIndex = struct.unpack("<H", f.read(2))[0]
self.signers = deser_dyn_bitset(f, False)
self.validMembers = deser_dyn_bitset(f, False)
self.quorumPublicKey = f.read(48)
@ -1136,6 +1139,8 @@ class CFinalCommitment:
r += struct.pack("<H", self.nVersion)
r += struct.pack("<B", self.llmqType)
r += ser_uint256(self.quorumHash)
if self.nVersion == 2:
r += struct.pack("<H", self.quorumIndex)
r += ser_dyn_bitset(self.signers, False)
r += ser_dyn_bitset(self.validMembers, False)
r += self.quorumPublicKey
@ -1144,6 +1149,11 @@ class CFinalCommitment:
r += self.membersSig
return r
def __repr__(self):
return "CFinalCommitment(nVersion={} llmqType={} quorumHash={:x} quorumIndex={} signers={}" \
" validMembers={} quorumPublicKey={} quorumVvecHash={:x}) quorumSig={} membersSig={})" \
.format(self.nVersion, self.llmqType, self.quorumHash, self.quorumIndex, repr(self.signers),
repr(self.validMembers), self.quorumPublicKey.hex(), self.quorumVvecHash, self.quorumSig.hex(), self.membersSig.hex())
class CGovernanceObject:
__slots__ = ("nHashParent", "nRevision", "nTime", "nCollateralHash", "vchData", "nObjectType",