mirror of
https://github.com/Xahau/xahaud.git
synced 2026-06-02 16:26:37 +00:00
feat(consensus): add active participant diagnostics
Carry a signed observedParticipantsHash in ExtendedPosition and log a local canonical bitmap over the active validator view for degraded-round debugging. The field is diagnostic only and does not affect proposal equality or quorum thresholds.
This commit is contained in:
@@ -66,6 +66,7 @@ class ExtendedPosition_test : public beast::unit_test::suite
|
||||
BEAST_EXPECT(!deserialized->entropySetHash);
|
||||
BEAST_EXPECT(!deserialized->exportSigSetHash);
|
||||
BEAST_EXPECT(!deserialized->exportSignaturesHash);
|
||||
BEAST_EXPECT(!deserialized->observedParticipantsHash);
|
||||
}
|
||||
|
||||
// Position with commitment
|
||||
@@ -94,6 +95,34 @@ class ExtendedPosition_test : public beast::unit_test::suite
|
||||
BEAST_EXPECT(!deserialized->myReveal);
|
||||
}
|
||||
|
||||
// Position with diagnostic participant hash only
|
||||
{
|
||||
auto const txSet = makeHash("txset-participants");
|
||||
auto const participants = makeHash("participants");
|
||||
|
||||
ExtendedPosition pos{txSet};
|
||||
pos.observedParticipantsHash = participants;
|
||||
|
||||
Serializer s;
|
||||
pos.add(s);
|
||||
|
||||
// 32 (txSet) + 1 (flags) + 32 (participant hash) = 65
|
||||
BEAST_EXPECT(s.getDataLength() == 65);
|
||||
|
||||
SerialIter sit(s.slice());
|
||||
auto deserialized =
|
||||
ExtendedPosition::fromSerialIter(sit, s.getDataLength());
|
||||
|
||||
BEAST_EXPECT(deserialized.has_value());
|
||||
if (!deserialized)
|
||||
return;
|
||||
BEAST_EXPECT(deserialized->txSetHash == txSet);
|
||||
BEAST_EXPECT(
|
||||
deserialized->observedParticipantsHash == participants);
|
||||
BEAST_EXPECT(!deserialized->myCommitment);
|
||||
BEAST_EXPECT(!deserialized->myReveal);
|
||||
}
|
||||
|
||||
// Position with all fields
|
||||
{
|
||||
auto const txSet = makeHash("txset-c");
|
||||
@@ -101,6 +130,7 @@ class ExtendedPosition_test : public beast::unit_test::suite
|
||||
auto const entropySet = makeHash("entropyset-c");
|
||||
auto const exportSigSet = makeHash("exportsigset-c");
|
||||
auto const exportSigs = makeHash("exportsigs-c");
|
||||
auto const participants = makeHash("participants-c");
|
||||
auto const commit = makeHash("commit-c");
|
||||
auto const reveal = makeHash("reveal-c");
|
||||
|
||||
@@ -109,14 +139,15 @@ class ExtendedPosition_test : public beast::unit_test::suite
|
||||
pos.entropySetHash = entropySet;
|
||||
pos.exportSigSetHash = exportSigSet;
|
||||
pos.exportSignaturesHash = exportSigs;
|
||||
pos.observedParticipantsHash = participants;
|
||||
pos.myCommitment = commit;
|
||||
pos.myReveal = reveal;
|
||||
|
||||
Serializer s;
|
||||
pos.add(s);
|
||||
|
||||
// 32 + 1 + 6*32 = 225
|
||||
BEAST_EXPECT(s.getDataLength() == 225);
|
||||
// 32 + 1 + 7*32 = 257
|
||||
BEAST_EXPECT(s.getDataLength() == 257);
|
||||
|
||||
SerialIter sit(s.slice());
|
||||
auto deserialized =
|
||||
@@ -130,6 +161,8 @@ class ExtendedPosition_test : public beast::unit_test::suite
|
||||
BEAST_EXPECT(deserialized->entropySetHash == entropySet);
|
||||
BEAST_EXPECT(deserialized->exportSigSetHash == exportSigSet);
|
||||
BEAST_EXPECT(deserialized->exportSignaturesHash == exportSigs);
|
||||
BEAST_EXPECT(
|
||||
deserialized->observedParticipantsHash == participants);
|
||||
BEAST_EXPECT(deserialized->myCommitment == commit);
|
||||
BEAST_EXPECT(deserialized->myReveal == reveal);
|
||||
}
|
||||
@@ -359,7 +392,7 @@ class ExtendedPosition_test : public beast::unit_test::suite
|
||||
auto const txSet = makeHash("txset-unkflags");
|
||||
Serializer s;
|
||||
s.addBitString(txSet);
|
||||
s.add8(0x41); // bit 6 is unknown, bit 0 = commitSetHash
|
||||
s.add8(0x81); // bit 7 is unknown, bit 0 = commitSetHash
|
||||
s.addBitString(makeHash("commitset-unkflags"));
|
||||
SerialIter sit(s.slice());
|
||||
auto result =
|
||||
@@ -433,6 +466,10 @@ class ExtendedPosition_test : public beast::unit_test::suite
|
||||
b.exportSignaturesHash = makeHash("export-sigs-eq");
|
||||
BEAST_EXPECT(a == b);
|
||||
|
||||
// Same txSetHash, different participant diagnostics -> still equal
|
||||
b.observedParticipantsHash = makeHash("participants-eq");
|
||||
BEAST_EXPECT(a == b);
|
||||
|
||||
// Different txSetHash -> not equal
|
||||
ExtendedPosition c{txSet2};
|
||||
BEAST_EXPECT(a != c);
|
||||
|
||||
@@ -59,6 +59,23 @@ ConsensusExtensions::ConsensusExtensions(Application& app, beast::Journal j)
|
||||
|
||||
namespace {
|
||||
|
||||
std::string
|
||||
buildObservedParticipantBitmap(
|
||||
std::vector<NodeID> const& activeSorted,
|
||||
hash_set<NodeID> const& observed)
|
||||
{
|
||||
std::string bitmapBin;
|
||||
bitmapBin.reserve(activeSorted.size());
|
||||
|
||||
for (std::size_t i = 0; i < activeSorted.size(); ++i)
|
||||
{
|
||||
bool const seen = observed.count(activeSorted[i]) != 0;
|
||||
bitmapBin.push_back(seen ? '1' : '0');
|
||||
}
|
||||
|
||||
return bitmapBin;
|
||||
}
|
||||
|
||||
ConsensusExtensions::ActiveValidatorView
|
||||
buildActiveValidatorView(
|
||||
Application& app,
|
||||
@@ -817,6 +834,9 @@ ConsensusExtensions::clearRngState()
|
||||
consensusExportTxns_.clear();
|
||||
consensusTxSetHash_.reset();
|
||||
pendingRngFetches_.clear();
|
||||
observedParticipantsHash_.reset();
|
||||
observedParticipantsCount_ = 0;
|
||||
observedParticipantsBitmapBin_.clear();
|
||||
exportSigGateStarted_ = false;
|
||||
exportSigGateStart_ = {};
|
||||
exportSigConvergenceFailed_ = false;
|
||||
@@ -1335,6 +1355,104 @@ ConsensusExtensions::fetchSidecarsIfNeeded(ExtendedPosition const& peerPos)
|
||||
fetchRngSetIfNeeded(peerPos.exportSigSetHash, SidecarKind::exportSig);
|
||||
}
|
||||
|
||||
void
|
||||
ConsensusExtensions::recordParticipantDiagnostics(
|
||||
ConsensusMode mode,
|
||||
std::vector<NodeID> peerNodeIds)
|
||||
{
|
||||
auto const view = activeValidatorView();
|
||||
hash_set<NodeID> observed;
|
||||
observed.reserve(peerNodeIds.size() + 1);
|
||||
|
||||
for (auto const& nodeId : peerNodeIds)
|
||||
{
|
||||
if (view->containsNode(nodeId))
|
||||
observed.insert(nodeId);
|
||||
}
|
||||
|
||||
auto const& valKeys = app_.getValidatorKeys();
|
||||
if (mode == ConsensusMode::proposing && valKeys.nodeID != beast::zero &&
|
||||
view->containsNode(valKeys.nodeID))
|
||||
{
|
||||
observed.insert(valKeys.nodeID);
|
||||
}
|
||||
|
||||
std::vector<NodeID> activeSorted(
|
||||
view->nodeIds.begin(), view->nodeIds.end());
|
||||
std::sort(activeSorted.begin(), activeSorted.end());
|
||||
std::vector<NodeID> observedSorted(observed.begin(), observed.end());
|
||||
std::sort(observedSorted.begin(), observedSorted.end());
|
||||
auto bitmapBin = buildObservedParticipantBitmap(activeSorted, observed);
|
||||
|
||||
Serializer s(512);
|
||||
if (view->sourceLedgerHash)
|
||||
s.addBitString(*view->sourceLedgerHash);
|
||||
else
|
||||
{
|
||||
uint256 noSource;
|
||||
noSource.zero();
|
||||
s.addBitString(noSource);
|
||||
}
|
||||
s.add32(static_cast<std::uint32_t>(view->size()));
|
||||
for (auto const& nodeId : activeSorted)
|
||||
s.addBitString(nodeId);
|
||||
s.add32(static_cast<std::uint32_t>(observedSorted.size()));
|
||||
for (auto const& nodeId : observedSorted)
|
||||
s.addBitString(nodeId);
|
||||
|
||||
auto const hash = s.getSHA512Half();
|
||||
bool const changed = !observedParticipantsHash_ ||
|
||||
*observedParticipantsHash_ != hash ||
|
||||
observedParticipantsCount_ != observedSorted.size() ||
|
||||
observedParticipantsBitmapBin_ != bitmapBin;
|
||||
|
||||
observedParticipantsHash_ = hash;
|
||||
observedParticipantsCount_ = observedSorted.size();
|
||||
observedParticipantsBitmapBin_ = std::move(bitmapBin);
|
||||
|
||||
if (changed)
|
||||
{
|
||||
JLOG(j_.debug()) << "STALLDIAG: observed-active-participants"
|
||||
<< " count=" << observedParticipantsCount_
|
||||
<< " activeView=" << view->size()
|
||||
<< " quorum=" << quorumThreshold() << " hash=" << hash
|
||||
<< " source="
|
||||
<< (view->fromUNLReport ? "UNLReport"
|
||||
: "trusted-fallback")
|
||||
<< " mode=" << to_string(mode)
|
||||
<< " peerPositions=" << peerNodeIds.size()
|
||||
<< " bitmap=" << observedParticipantsBitmapBin_;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
ConsensusExtensions::attachParticipantDiagnostics(ExtendedPosition& pos) const
|
||||
{
|
||||
if (!rngEnabledThisRound_ && !exportEnabledThisRound_)
|
||||
return;
|
||||
|
||||
if (observedParticipantsHash_)
|
||||
pos.observedParticipantsHash = observedParticipantsHash_;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
ConsensusExtensions::observedParticipantCount() const
|
||||
{
|
||||
return observedParticipantsCount_;
|
||||
}
|
||||
|
||||
std::optional<uint256>
|
||||
ConsensusExtensions::observedParticipantsHash() const
|
||||
{
|
||||
return observedParticipantsHash_;
|
||||
}
|
||||
|
||||
std::string const&
|
||||
ConsensusExtensions::observedParticipantsBitmapBin() const
|
||||
{
|
||||
return observedParticipantsBitmapBin_;
|
||||
}
|
||||
|
||||
void
|
||||
ConsensusExtensions::cacheConsensusTxSet(RCLTxSet const& txns)
|
||||
{
|
||||
@@ -1870,6 +1988,13 @@ ConsensusExtensions::appendJson(Json::Value& ret) const
|
||||
rng["any_reveals"] = hasAnyReveals();
|
||||
rng["reveals"] = static_cast<Int>(pendingRevealCount());
|
||||
rng["likely_participants"] = static_cast<Int>(expectedProposerCount());
|
||||
rng["observed_active_participants"] =
|
||||
static_cast<Int>(observedParticipantCount());
|
||||
if (observedParticipantsHash_)
|
||||
{
|
||||
rng["observed_participants"] = to_string(*observedParticipantsHash_);
|
||||
rng["observed_participants_bitmap"] = observedParticipantsBitmapBin_;
|
||||
}
|
||||
|
||||
ret["rng"] = std::move(rng);
|
||||
}
|
||||
@@ -1890,6 +2015,10 @@ ConsensusExtensions::logPosition(
|
||||
<< " entropySetHash="
|
||||
<< (pos.entropySetHash ? to_string(*pos.entropySetHash)
|
||||
: std::string{"none"})
|
||||
<< " observedParticipantsHash="
|
||||
<< (pos.observedParticipantsHash
|
||||
? to_string(*pos.observedParticipantsHash)
|
||||
: std::string{"none"})
|
||||
<< " myCommitment=" << (pos.myCommitment ? "yes" : "no")
|
||||
<< " myReveal=" << (pos.myReveal ? "yes" : "no");
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
@@ -120,6 +121,9 @@ private:
|
||||
|
||||
// Recent proposers intersected with the active UNL (liveness hint)
|
||||
hash_set<NodeID> likelyParticipants_;
|
||||
std::optional<uint256> observedParticipantsHash_;
|
||||
std::size_t observedParticipantsCount_ = 0;
|
||||
std::string observedParticipantsBitmapBin_;
|
||||
|
||||
// Current consensus mode (set by adaptor at round start)
|
||||
ConsensusMode mode_{ConsensusMode::observing};
|
||||
@@ -274,6 +278,36 @@ public:
|
||||
void
|
||||
fetchSidecarsIfNeeded(ExtendedPosition const& peerPos);
|
||||
|
||||
template <class PeerPositions>
|
||||
void
|
||||
recordParticipantDiagnostics(
|
||||
ConsensusMode mode,
|
||||
PeerPositions const& peerPositions)
|
||||
{
|
||||
std::vector<NodeID> peerNodeIds;
|
||||
peerNodeIds.reserve(peerPositions.size());
|
||||
for (auto const& entry : peerPositions)
|
||||
peerNodeIds.push_back(entry.first);
|
||||
recordParticipantDiagnostics(mode, std::move(peerNodeIds));
|
||||
}
|
||||
|
||||
void
|
||||
recordParticipantDiagnostics(
|
||||
ConsensusMode mode,
|
||||
std::vector<NodeID> peerNodeIds);
|
||||
|
||||
void
|
||||
attachParticipantDiagnostics(ExtendedPosition& pos) const;
|
||||
|
||||
std::size_t
|
||||
observedParticipantCount() const;
|
||||
|
||||
std::optional<uint256>
|
||||
observedParticipantsHash() const;
|
||||
|
||||
std::string const&
|
||||
observedParticipantsBitmapBin() const;
|
||||
|
||||
void
|
||||
cacheConsensusTxSet(RCLTxSet const& txns);
|
||||
|
||||
|
||||
@@ -98,6 +98,27 @@ Be careful with `prevProposers`: in the generic consensus code it is peer-only.
|
||||
When checking whether the previous round had enough active participants, count
|
||||
our own proposer slot if this node is proposing.
|
||||
|
||||
## Participant Diagnostics
|
||||
|
||||
Proposals may carry a signed `observedParticipantsHash` for debugging active-UNL
|
||||
visibility during RNG/Export rounds. The hash represents the active validators
|
||||
this node has observed participating in the current establish round, including
|
||||
self when proposing. It also commits to the active validator view used by the
|
||||
node, so mismatched fallback/configured views do not collapse to the same
|
||||
diagnostic hash merely because they have the same size.
|
||||
|
||||
Local diagnostics also log the same observed set as a canonical binary bitmap
|
||||
over the sorted active validator view. The bitmap is not sent on the wire; the
|
||||
signed proposal field remains the hash.
|
||||
|
||||
This field is diagnostic only:
|
||||
|
||||
- It is covered by the proposal signature and duplicate-suppression identity.
|
||||
- It does not participate in `ExtendedPosition::operator==`.
|
||||
- It does not lower the active validator quorum denominator.
|
||||
- It is intended to explain timing/degraded-network cases where commits,
|
||||
reveals, or sidecar hashes arrive late or asymmetrically.
|
||||
|
||||
## RNG Commit/Reveal Principles
|
||||
|
||||
RNG proceeds through establish sub-states:
|
||||
|
||||
@@ -262,8 +262,10 @@ RCLConsensus::Adaptor::propose(RCLCxPeerPos::Proposal const& proposal)
|
||||
wirePosition.exportSignaturesHash =
|
||||
proposalExportSignaturesHash(prop.exportsignatures());
|
||||
|
||||
// Serialize full ExtendedPosition (includes RNG leaves and export
|
||||
// signature digest)
|
||||
ce().attachParticipantDiagnostics(wirePosition);
|
||||
|
||||
// Serialize full ExtendedPosition (includes RNG leaves, export signature
|
||||
// digest, and signed diagnostics)
|
||||
Serializer positionData;
|
||||
wirePosition.add(positionData);
|
||||
auto const posSlice = positionData.slice();
|
||||
|
||||
@@ -59,6 +59,8 @@ struct ExtendedPosition
|
||||
std::optional<uint256> entropySetHash;
|
||||
std::optional<uint256> exportSigSetHash;
|
||||
std::optional<uint256> exportSignaturesHash;
|
||||
// Signed diagnostic only: not a quorum input and not part of operator==.
|
||||
std::optional<uint256> observedParticipantsHash;
|
||||
|
||||
// === Per-Validator Leaves (unique per proposer) ===
|
||||
std::optional<uint256> myCommitment;
|
||||
@@ -166,7 +168,8 @@ struct ExtendedPosition
|
||||
// Wire compatibility: if no extensions, emit exactly 32 bytes
|
||||
// so legacy nodes that expect a plain uint256 work unchanged.
|
||||
if (!commitSetHash && !entropySetHash && !exportSigSetHash &&
|
||||
!exportSignaturesHash && !myCommitment && !myReveal)
|
||||
!exportSignaturesHash && !observedParticipantsHash &&
|
||||
!myCommitment && !myReveal)
|
||||
return;
|
||||
|
||||
std::uint8_t flags = 0;
|
||||
@@ -182,6 +185,8 @@ struct ExtendedPosition
|
||||
flags |= 0x10;
|
||||
if (exportSignaturesHash)
|
||||
flags |= 0x20;
|
||||
if (observedParticipantsHash)
|
||||
flags |= 0x40;
|
||||
s.add8(flags);
|
||||
|
||||
if (commitSetHash)
|
||||
@@ -196,6 +201,8 @@ struct ExtendedPosition
|
||||
s.addBitString(*exportSigSetHash);
|
||||
if (exportSignaturesHash)
|
||||
s.addBitString(*exportSignaturesHash);
|
||||
if (observedParticipantsHash)
|
||||
s.addBitString(*observedParticipantsHash);
|
||||
}
|
||||
//@@end rng-extended-position-serialize
|
||||
|
||||
@@ -212,6 +219,8 @@ struct ExtendedPosition
|
||||
ret["export_sig_set"] = to_string(*exportSigSetHash);
|
||||
if (exportSignaturesHash)
|
||||
ret["export_signatures"] = to_string(*exportSignaturesHash);
|
||||
if (observedParticipantsHash)
|
||||
ret["observed_participants"] = to_string(*observedParticipantsHash);
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -241,13 +250,13 @@ struct ExtendedPosition
|
||||
std::uint8_t flags = sit.get8();
|
||||
|
||||
// Reject unknown flag bits (reduces wire malleability)
|
||||
if (flags & 0xC0)
|
||||
if (flags & 0x80)
|
||||
return std::nullopt;
|
||||
|
||||
// Validate exact byte count for the flagged fields.
|
||||
// Each flag bit indicates a 32-byte uint256.
|
||||
int fieldCount = 0;
|
||||
for (int i = 0; i < 6; ++i)
|
||||
for (int i = 0; i < 7; ++i)
|
||||
if (flags & (1 << i))
|
||||
++fieldCount;
|
||||
|
||||
@@ -266,6 +275,8 @@ struct ExtendedPosition
|
||||
pos.exportSigSetHash = sit.get256();
|
||||
if (flags & 0x20)
|
||||
pos.exportSignaturesHash = sit.get256();
|
||||
if (flags & 0x40)
|
||||
pos.observedParticipantsHash = sit.get256();
|
||||
|
||||
return pos;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ extensionsTick(Ext& ext, Ctx const& ctx)
|
||||
// crash between commit and reveal.
|
||||
|
||||
bool const isRngEnabled = ext.rngEnabled();
|
||||
bool const isExportEnabled = ext.exportEnabled();
|
||||
|
||||
JLOG(ext.j_.trace()) << "RNGGATE: phaseEstablish prevSeq="
|
||||
<< (static_cast<std::uint32_t>(ctx.buildSeq) - 1)
|
||||
@@ -52,6 +53,20 @@ extensionsTick(Ext& ext, Ctx const& ctx)
|
||||
<< " mode=" << to_string(ctx.mode)
|
||||
<< " roundMs=" << ctx.roundTime.count();
|
||||
|
||||
if (isRngEnabled || isExportEnabled)
|
||||
{
|
||||
if constexpr (requires {
|
||||
ext.recordParticipantDiagnostics(
|
||||
ctx.mode, ctx.peerPositions);
|
||||
})
|
||||
{
|
||||
// Diagnostic only: this records the active-UNL participants visible
|
||||
// to this node so proposals can carry a signed hash for debugging
|
||||
// timing/degraded-network cases. It is not a quorum denominator.
|
||||
ext.recordParticipantDiagnostics(ctx.mode, ctx.peerPositions);
|
||||
}
|
||||
}
|
||||
|
||||
if (isRngEnabled)
|
||||
{
|
||||
auto const buildSeq = ctx.buildSeq;
|
||||
@@ -112,6 +127,23 @@ extensionsTick(Ext& ext, Ctx const& ctx)
|
||||
<< " minReveals=" << (minReveals ? "yes" : "no")
|
||||
<< " anyReveals=" << (anyReveals ? "yes" : "no")
|
||||
<< " likelyParticipants=" << std::to_string(likelyParticipants);
|
||||
|
||||
if constexpr (requires {
|
||||
ext.observedParticipantCount();
|
||||
ext.observedParticipantsHash();
|
||||
ext.observedParticipantsBitmapBin();
|
||||
})
|
||||
{
|
||||
auto const observedHash = ext.observedParticipantsHash();
|
||||
JLOG(ext.j_.debug())
|
||||
<< "STALLDIAG: participant-diagnostics"
|
||||
<< " observedActiveParticipants="
|
||||
<< ext.observedParticipantCount()
|
||||
<< " observedParticipantsHash="
|
||||
<< (observedHash ? to_string(*observedHash)
|
||||
: std::string{"none"})
|
||||
<< " bitmap=" << ext.observedParticipantsBitmapBin();
|
||||
}
|
||||
};
|
||||
auto publishEntropySet = [&]() {
|
||||
auto entropySetHash = ext.buildEntropySet(buildSeq);
|
||||
|
||||
Reference in New Issue
Block a user