mirror of
https://github.com/dashpay/dash.git
synced 2024-12-25 12:02:48 +01:00
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:
parent
bb4be52b48
commit
73cf7ff978
@ -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);
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user