mirror of
https://github.com/Xahau/xahaud.git
synced 2026-06-25 11:36:36 +00:00
fix(rng): scope sidecar alignment count to the active validator view (F1)
The entropy/export bless-vs-fallback gate counted alignment over ALL tx-converged trusted proposers (currPeerPositions_), while the thresholds and the entropy leaf set are computed over the active validator view. A node that locally trusts proposers outside the on-ledger UNLReport active set could pad alignedParticipants(), inflating the counting universe N above originalViewSize and eroding the Tier-2 intersection margin (2t - N) below the Byzantine floor f -- so two equivocation cohorts padded by non-active aligners could each clear the gate (review finding F1). Backstopped by the 80% validation quorum, but the proof's universe and the code's universe must match. inspectTxConvergedSidecarPeers now takes an active-view membership predicate and counts only member peers toward alignment; the local +1 is gated on localIsActiveValidator(). Both the RNG entropy gate and the export-sig gate pass ext.isUNLReportMember / ext.localIsActiveValidator, mirroring buildEntropySet / hasQuorumOfCommits' containsNode filter. New method on ConsensusExtensions + CSF Peer + the FakeExtensions stub. Regression test (Sidecar peer alignment helper): a trusted-but-non-active aligned proposer pushes unfiltered aligned to 2 but filtered aligned stays 1, and a non-active local node's +1 is suppressed -- padding cannot satisfy the gate. ConsensusExtensions 900, ConsensusRng 386, Consensus 1399, all green. Note: the explicit-final proposal path (ConsensusExtensionsTick.h ~:934) counts over prevProposers and is NOT yet filtered (separate, experimental path).
This commit is contained in:
@@ -397,6 +397,22 @@ struct FakeExtensions
|
||||
return exportQuorum;
|
||||
}
|
||||
|
||||
// Membership is a no-op in the FakeExtensions tick tests (every peer
|
||||
// counts, local counts) so the gate behavior is unchanged; F1 active-view
|
||||
// filtering is exercised directly against inspectTxConvergedSidecarPeers.
|
||||
template <class Id>
|
||||
bool
|
||||
isUNLReportMember(Id const&) const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
localIsActiveValidator() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
pendingCommitCount() const
|
||||
{
|
||||
@@ -777,11 +793,18 @@ class ConsensusExtensions_test : public beast::unit_test::suite
|
||||
harness.addPeer(3, std::nullopt);
|
||||
harness.addPeer(4, localHash, makeHash("other-tx-set"));
|
||||
|
||||
auto const exportHashOf = [](auto const& position) {
|
||||
return position.exportSigSetHash;
|
||||
};
|
||||
auto const allMembers = [](auto const&) { return true; };
|
||||
|
||||
std::vector<uint256> fetched;
|
||||
auto const state = detail::inspectTxConvergedSidecarPeers(
|
||||
harness.peers,
|
||||
harness.position,
|
||||
[](auto const& position) { return position.exportSigSetHash; },
|
||||
true,
|
||||
exportHashOf,
|
||||
allMembers,
|
||||
[&](auto const& hash) {
|
||||
if (hash)
|
||||
fetched.push_back(*hash);
|
||||
@@ -804,12 +827,56 @@ class ConsensusExtensions_test : public beast::unit_test::suite
|
||||
auto const unpublishedState = detail::inspectTxConvergedSidecarPeers(
|
||||
harness.peers,
|
||||
harness.position,
|
||||
[](auto const& position) { return position.exportSigSetHash; },
|
||||
true,
|
||||
exportHashOf,
|
||||
allMembers,
|
||||
[](auto const&) {});
|
||||
BEAST_EXPECT(!unpublishedState.localPublished);
|
||||
BEAST_EXPECT(unpublishedState.alignedParticipants() == 0);
|
||||
BEAST_EXPECT(!unpublishedState.quorumAligned(1));
|
||||
BEAST_EXPECT(unpublishedState.fullObservation());
|
||||
|
||||
// F1: the alignment-counting universe must be the active validator
|
||||
// view, not the full trusted-proposer set. A trusted-but-non-active
|
||||
// proposer (node 5) that tx-converges and aligns on the SAME hash must
|
||||
// NOT inflate alignedParticipants() — otherwise two equivocation
|
||||
// cohorts padded by non-active peers could each clear the gate.
|
||||
harness.position.exportSigSetHash = localHash;
|
||||
harness.addPeer(5, localHash); // trusted, but outside the active view
|
||||
auto const activeOnly = [](auto const& id) {
|
||||
return id != makeNode(5); // nodes 1..4 active; 5 is not
|
||||
};
|
||||
|
||||
auto const padded = detail::inspectTxConvergedSidecarPeers(
|
||||
harness.peers,
|
||||
harness.position,
|
||||
true,
|
||||
exportHashOf,
|
||||
allMembers,
|
||||
[](auto const&) {});
|
||||
BEAST_EXPECT(padded.aligned == 2); // node 1 + node 5, unfiltered
|
||||
|
||||
auto const filtered = detail::inspectTxConvergedSidecarPeers(
|
||||
harness.peers,
|
||||
harness.position,
|
||||
true,
|
||||
exportHashOf,
|
||||
activeOnly,
|
||||
[](auto const&) {});
|
||||
BEAST_EXPECT(filtered.aligned == 1); // node 5 excluded
|
||||
BEAST_EXPECT(filtered.alignedParticipants() == 2); // node 1 + local
|
||||
BEAST_EXPECT(!filtered.quorumAligned(3)); // padding can't reach quorum
|
||||
|
||||
// The local +1 is likewise gated on local active-view membership.
|
||||
auto const nonActiveLocal = detail::inspectTxConvergedSidecarPeers(
|
||||
harness.peers,
|
||||
harness.position,
|
||||
false,
|
||||
exportHashOf,
|
||||
activeOnly,
|
||||
[](auto const&) {});
|
||||
BEAST_EXPECT(!nonActiveLocal.localPublished);
|
||||
BEAST_EXPECT(nonActiveLocal.alignedParticipants() == 1); // node 1 only
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -721,6 +721,12 @@ struct Peer
|
||||
return unlNodes_.count(nodeId) > 0;
|
||||
}
|
||||
|
||||
bool
|
||||
localIsActiveValidator() const
|
||||
{
|
||||
return isUNLReportMember(peer.id);
|
||||
}
|
||||
|
||||
void
|
||||
finalizeRoundEntropy(
|
||||
std::uint32_t seq,
|
||||
|
||||
@@ -987,6 +987,18 @@ ConsensusExtensions::isUNLReportMember(NodeID const& nodeId) const
|
||||
return activeValidatorView()->containsNode(nodeId);
|
||||
}
|
||||
|
||||
bool
|
||||
ConsensusExtensions::localIsActiveValidator() const
|
||||
{
|
||||
// Our own sidecar position only counts toward alignment when this validator
|
||||
// is itself in the active view — the same universe as the peer-membership
|
||||
// filter and the entropy/export thresholds.
|
||||
auto const& valKeys = app_.getValidatorKeys();
|
||||
if (!valKeys.keys || valKeys.nodeID == beast::zero)
|
||||
return false;
|
||||
return activeValidatorView()->containsNode(valKeys.nodeID);
|
||||
}
|
||||
|
||||
ConsensusExtensions::ActiveValidatorViewPtr
|
||||
ConsensusExtensions::activeValidatorView() const
|
||||
{
|
||||
|
||||
@@ -318,6 +318,12 @@ public:
|
||||
bool
|
||||
isUNLReportMember(NodeID const& nodeId) const;
|
||||
|
||||
// True only when THIS node's own validator key is in the active view. Used
|
||||
// to gate our own +1 in the sidecar alignment count to the same universe as
|
||||
// the peer-membership filter and the entropy/export thresholds.
|
||||
bool
|
||||
localIsActiveValidator() const;
|
||||
|
||||
void
|
||||
generateEntropySecret();
|
||||
|
||||
|
||||
@@ -37,12 +37,19 @@ struct SidecarPeerAlignment
|
||||
}
|
||||
};
|
||||
|
||||
template <class PeerPositions, class Position, class GetHash, class OnMismatch>
|
||||
template <
|
||||
class PeerPositions,
|
||||
class Position,
|
||||
class GetHash,
|
||||
class IsMember,
|
||||
class OnMismatch>
|
||||
SidecarPeerAlignment
|
||||
inspectTxConvergedSidecarPeers(
|
||||
PeerPositions const& peerPositions,
|
||||
Position const& pos,
|
||||
bool localIsMember,
|
||||
GetHash getHash,
|
||||
IsMember isMember,
|
||||
OnMismatch onMismatch)
|
||||
{
|
||||
SidecarPeerAlignment state;
|
||||
@@ -50,9 +57,21 @@ inspectTxConvergedSidecarPeers(
|
||||
if (!localHash)
|
||||
return state;
|
||||
|
||||
state.localPublished = true;
|
||||
for (auto const& [_, peerPos] : peerPositions)
|
||||
// The alignment-counting universe must be the active validator view, the
|
||||
// same denominator the entropy/export thresholds use (quorumThreshold /
|
||||
// tier2Threshold are computed over that view). A trusted-but-non-active
|
||||
// proposer can tx-converge and advertise a sidecar hash, but it must NOT
|
||||
// pad alignedParticipants(): counting outside the active view inflates the
|
||||
// universe N above originalViewSize and erodes the Tier-2 intersection
|
||||
// margin (2t - N) below the Byzantine floor f, breaking equivocation
|
||||
// uniqueness. Mirror buildEntropySet/hasQuorumOfCommits' containsNode
|
||||
// filter, and only count our own +1 when this node is itself active.
|
||||
state.localPublished = localIsMember;
|
||||
for (auto const& [nodeId, peerPos] : peerPositions)
|
||||
{
|
||||
if (!isMember(nodeId))
|
||||
continue; // outside the active view -> not in the counting
|
||||
// universe
|
||||
auto const& pp = peerPos.proposal().position();
|
||||
if (!(pp == pos))
|
||||
continue; // not tx-converged
|
||||
@@ -705,9 +724,13 @@ extensionsTick(Ext& ext, Ctx const& ctx)
|
||||
return detail::inspectTxConvergedSidecarPeers(
|
||||
ctx.peerPositions,
|
||||
pos,
|
||||
ext.localIsActiveValidator(),
|
||||
[](auto const& position) {
|
||||
return position.entropySetHash;
|
||||
},
|
||||
[&ext](auto const& nodeId) {
|
||||
return ext.isUNLReportMember(nodeId);
|
||||
},
|
||||
[&](auto const& hash) {
|
||||
if (fetchMismatches)
|
||||
ext.fetchRngSetIfNeeded(
|
||||
@@ -1210,9 +1233,13 @@ extensionsTick(Ext& ext, Ctx const& ctx)
|
||||
return detail::inspectTxConvergedSidecarPeers(
|
||||
ctx.peerPositions,
|
||||
pos,
|
||||
ext.localIsActiveValidator(),
|
||||
[](auto const& position) {
|
||||
return position.exportSigSetHash;
|
||||
},
|
||||
[&ext](auto const& nodeId) {
|
||||
return ext.isUNLReportMember(nodeId);
|
||||
},
|
||||
[&](auto const& hash) {
|
||||
if (fetchMismatches)
|
||||
ext.fetchRngSetIfNeeded(
|
||||
|
||||
Reference in New Issue
Block a user