mirror of
https://github.com/Xahau/xahaud.git
synced 2026-04-29 15:37:46 +00:00
fix(consensus): harden sidecar quorum inputs
This commit is contained in:
@@ -168,9 +168,9 @@ message TMProposeSet
|
||||
optional uint32 hops = 12 [deprecated=true];
|
||||
|
||||
// Export signatures for pending exports seen in the proposal set.
|
||||
// Each entry is: txnHash (32 bytes) + validator pubkey (33 bytes).
|
||||
// Validators attach these so export quorum can be reached within
|
||||
// the same consensus round.
|
||||
// Each entry is: txnHash (32 bytes) + validator pubkey (33 bytes)
|
||||
// + multisign signature (variable length). Validators attach these
|
||||
// so export quorum can be reached within the same consensus round.
|
||||
repeated bytes exportSignatures = 13;
|
||||
}
|
||||
|
||||
@@ -224,9 +224,9 @@ message TMValidation
|
||||
// Number of hops traveled
|
||||
optional uint32 hops = 3 [deprecated = true];
|
||||
|
||||
// Export signatures for pending exports validated in this ledger.
|
||||
// Each entry is: txnHash (32 bytes) + serialized sfSigner STObject.
|
||||
// Used for ephemeral export signature collection via validation gossip.
|
||||
// Legacy export signature gossip field retained for wire compatibility.
|
||||
// Current proposal-based export signatures use
|
||||
// TMProposeSet.exportSignatures.
|
||||
repeated bytes exportSignatures = 4;
|
||||
}
|
||||
|
||||
@@ -395,4 +395,3 @@ message TMHaveTransactions
|
||||
{
|
||||
repeated bytes hashes = 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace ripple {
|
||||
//
|
||||
// These limits bound the DoS surface of the export signature system:
|
||||
// - Each pending export requires every validator to sign it every round
|
||||
// (sign-once, broadcast-many via TMValidation)
|
||||
// (sign-once, attach once via TMProposeSet)
|
||||
// - Inbound signature processing involves crypto verification per sig
|
||||
// - The directory cap (maxPendingExports) is the root constraint;
|
||||
// signing throughput and inbound processing are transitively bounded by it
|
||||
@@ -21,8 +21,8 @@ struct ExportLimits
|
||||
|
||||
// Maximum pending exports in the exported directory at any time.
|
||||
// This transitively caps:
|
||||
// - signatures per TMValidation message (1 per pending export)
|
||||
// - inbound signature processing in PeerImp (clamped to this)
|
||||
// - signatures per TMProposeSet message (1 per pending export)
|
||||
// - inbound proposal signature processing (clamped to this)
|
||||
// - validator signing work per round
|
||||
static constexpr std::uint8_t maxPendingExports = 8;
|
||||
};
|
||||
|
||||
@@ -100,8 +100,7 @@ struct TestVLPublisher
|
||||
buildVLData(
|
||||
std::vector<TestValidator> const& validators,
|
||||
std::uint32_t sequence = 1,
|
||||
std::uint32_t expiration =
|
||||
767784645) const // ~2024, matches Import_test
|
||||
std::uint32_t expiration = 767784645) const
|
||||
{
|
||||
// Build the JSON blob
|
||||
std::string data = "{\"sequence\":" + std::to_string(sequence) +
|
||||
|
||||
@@ -136,10 +136,19 @@ bool
|
||||
ConsensusExtensions::hasQuorumOfCommits() const
|
||||
{
|
||||
auto threshold = quorumThreshold();
|
||||
bool result = pendingCommits_.size() >= threshold;
|
||||
JLOG(j_.trace()) << "RNG: hasQuorumOfCommits? " << pendingCommits_.size()
|
||||
<< "/" << threshold << " -> " << (result ? "YES" : "no")
|
||||
<< " (activeUNL=" << unlReportNodeIds_.size()
|
||||
auto const proofedCommitCount = std::count_if(
|
||||
pendingCommits_.begin(),
|
||||
pendingCommits_.end(),
|
||||
[this](auto const& entry) {
|
||||
auto const& nid = entry.first;
|
||||
return isUNLReportMember(nid) && nodeIdToKey_.count(nid) > 0 &&
|
||||
commitProofs_.count(nid) > 0;
|
||||
});
|
||||
bool result = static_cast<std::size_t>(proofedCommitCount) >= threshold;
|
||||
JLOG(j_.trace()) << "RNG: hasQuorumOfCommits? " << proofedCommitCount << "/"
|
||||
<< threshold << " -> " << (result ? "YES" : "no")
|
||||
<< " (pending=" << pendingCommits_.size()
|
||||
<< ", activeUNL=" << unlReportNodeIds_.size()
|
||||
<< ", likelyParticipants=" << likelyParticipants_.size()
|
||||
<< ")";
|
||||
return result;
|
||||
@@ -341,6 +350,9 @@ ConsensusExtensions::buildCommitSet(LedgerIndex seq)
|
||||
auto kit = nodeIdToKey_.find(nid);
|
||||
if (kit == nodeIdToKey_.end())
|
||||
continue;
|
||||
auto proofIt = commitProofs_.find(nid);
|
||||
if (proofIt == commitProofs_.end())
|
||||
continue;
|
||||
|
||||
// Encode the NodeID into sfAccount so onAcquiredSidecarSet can
|
||||
// recover it without recomputing (master vs signing key issue).
|
||||
@@ -353,9 +365,7 @@ ConsensusExtensions::buildCommitSet(LedgerIndex seq)
|
||||
sidecar.setAccountID(sfAccount, acctId);
|
||||
sidecar.setFieldH256(sfDigest, commit);
|
||||
sidecar.setFieldVL(sfSigningPubKey, kit->second.slice());
|
||||
auto proofIt = commitProofs_.find(nid);
|
||||
if (proofIt != commitProofs_.end())
|
||||
sidecar.setFieldVL(sfBlob, serializeProof(proofIt->second));
|
||||
sidecar.setFieldVL(sfBlob, serializeProof(proofIt->second));
|
||||
|
||||
auto const itemKey = sidecar.getHash(HashPrefix::sidecar);
|
||||
Serializer s(2048);
|
||||
@@ -550,17 +560,19 @@ ConsensusExtensions::clearRngState()
|
||||
//@@end clear-rng-state
|
||||
|
||||
void
|
||||
ConsensusExtensions::cacheUNLReport()
|
||||
ConsensusExtensions::cacheUNLReport(
|
||||
std::shared_ptr<Ledger const> const& prevLedger)
|
||||
{
|
||||
unlReportNodeIds_.clear();
|
||||
bool const includeSelf = mode_ == ConsensusMode::proposing &&
|
||||
app_.getValidatorKeys().keys &&
|
||||
app_.getValidatorKeys().nodeID != beast::zero;
|
||||
|
||||
// Try UNL Report from the validated ledger
|
||||
if (auto const prevLedger = app_.getLedgerMaster().getValidatedLedger())
|
||||
// Try UNL Report from the consensus parent ledger. Falling back to
|
||||
// LedgerMaster preserves older tests that call cacheUNLReport directly,
|
||||
// but live rounds should pass the exact parent ledger for the round.
|
||||
auto const sourceLedger =
|
||||
prevLedger ? prevLedger : app_.getLedgerMaster().getValidatedLedger();
|
||||
if (sourceLedger)
|
||||
{
|
||||
if (auto const sle = prevLedger->read(keylet::UNLReport()))
|
||||
if (auto const sle = sourceLedger->read(keylet::UNLReport()))
|
||||
{
|
||||
if (sle->isFieldPresent(sfActiveValidators))
|
||||
{
|
||||
@@ -584,14 +596,11 @@ ConsensusExtensions::cacheUNLReport()
|
||||
{
|
||||
unlReportNodeIds_.insert(calcNodeID(masterKey));
|
||||
}
|
||||
}
|
||||
|
||||
// Only include ourselves when actively proposing. Observers/non-validators
|
||||
// do not emit commitments and must not be expected in commit quorum.
|
||||
if (includeSelf)
|
||||
unlReportNodeIds_.insert(app_.getValidatorKeys().nodeID);
|
||||
else
|
||||
unlReportNodeIds_.erase(app_.getValidatorKeys().nodeID);
|
||||
auto const& valKeys = app_.getValidatorKeys();
|
||||
if (valKeys.keys && valKeys.nodeID != beast::zero)
|
||||
unlReportNodeIds_.insert(valKeys.nodeID);
|
||||
}
|
||||
|
||||
JLOG(j_.trace()) << "RNG: cacheUNLReport size=" << unlReportNodeIds_.size();
|
||||
}
|
||||
@@ -1048,6 +1057,7 @@ ConsensusExtensions::fetchSidecarsIfNeeded(ExtendedPosition const& peerPos)
|
||||
{
|
||||
fetchRngSetIfNeeded(peerPos.commitSetHash, SidecarKind::commit);
|
||||
fetchRngSetIfNeeded(peerPos.entropySetHash, SidecarKind::reveal);
|
||||
fetchRngSetIfNeeded(peerPos.exportSigSetHash, SidecarKind::exportSig);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -1093,8 +1103,8 @@ ConsensusExtensions::onPreBuild(CanonicalTXSet& retriableTxs, LedgerIndex seq)
|
||||
// same entropy — preventing reveal-subset divergence at
|
||||
// timeout boundaries.
|
||||
//
|
||||
// Each leaf is an STTx with sfSigningPubKey (validator key)
|
||||
// and sfDigest (the reveal).
|
||||
// Each leaf is an STObject(sfGeneric) sidecar with sfSigningPubKey
|
||||
// (validator key) and sfDigest (the reveal).
|
||||
std::vector<std::pair<PublicKey, uint256>> sorted;
|
||||
entropySetMap_->visitLeaves(
|
||||
[&](boost::intrusive_ptr<SHAMapItem const> const& item) {
|
||||
@@ -1453,7 +1463,7 @@ ConsensusExtensions::onRoundStart(
|
||||
hash_set<NodeID> lastProposers)
|
||||
{
|
||||
clearRngState();
|
||||
cacheUNLReport();
|
||||
cacheUNLReport(prevLedger.ledger_);
|
||||
setExpectedProposers(std::move(lastProposers));
|
||||
resetSubState();
|
||||
}
|
||||
@@ -1705,7 +1715,7 @@ ConsensusExtensions::decoratePosition(
|
||||
}
|
||||
|
||||
setMode(ConsensusMode::proposing);
|
||||
cacheUNLReport();
|
||||
cacheUNLReport(prevLedger);
|
||||
generateEntropySecret();
|
||||
|
||||
auto const& valKeys = app_.getValidatorKeys();
|
||||
|
||||
@@ -27,9 +27,6 @@ using TickContext = ConsensusTick<ExtendedPosition, RCLCxPeerPos, RCLTxSet>;
|
||||
/// Owns all RNG/Export state that was previously scattered across
|
||||
/// RCLCxAdaptor and Consensus.h. Lifecycle hooks are grouped by
|
||||
/// caller/threading context.
|
||||
///
|
||||
/// See .ai-docs/refactoring-xahaud-consensus-extensions-v8.md.j2
|
||||
/// for design rationale.
|
||||
class ConsensusExtensions
|
||||
{
|
||||
Application& app_;
|
||||
@@ -190,7 +187,7 @@ public:
|
||||
fetchSidecarsIfNeeded(ExtendedPosition const& peerPos);
|
||||
|
||||
void
|
||||
cacheUNLReport();
|
||||
cacheUNLReport(std::shared_ptr<Ledger const> const& prevLedger = {});
|
||||
|
||||
bool
|
||||
isUNLReportMember(NodeID const& nodeId) const;
|
||||
|
||||
Reference in New Issue
Block a user