diff --git a/include/xrpl/proto/ripple.proto b/include/xrpl/proto/ripple.proto index ef651db19..68234db08 100644 --- a/include/xrpl/proto/ripple.proto +++ b/include/xrpl/proto/ripple.proto @@ -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; } - diff --git a/include/xrpl/protocol/ExportLimits.h b/include/xrpl/protocol/ExportLimits.h index 853575906..88e164864 100644 --- a/include/xrpl/protocol/ExportLimits.h +++ b/include/xrpl/protocol/ExportLimits.h @@ -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; }; diff --git a/src/test/jtx/xpop.h b/src/test/jtx/xpop.h index 37d74e35c..e6e1cde4e 100644 --- a/src/test/jtx/xpop.h +++ b/src/test/jtx/xpop.h @@ -100,8 +100,7 @@ struct TestVLPublisher buildVLData( std::vector 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) + diff --git a/src/xrpld/app/consensus/ConsensusExtensions.cpp b/src/xrpld/app/consensus/ConsensusExtensions.cpp index 321cbd20e..e684d1960 100644 --- a/src/xrpld/app/consensus/ConsensusExtensions.cpp +++ b/src/xrpld/app/consensus/ConsensusExtensions.cpp @@ -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(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 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> sorted; entropySetMap_->visitLeaves( [&](boost::intrusive_ptr const& item) { @@ -1453,7 +1463,7 @@ ConsensusExtensions::onRoundStart( hash_set 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(); diff --git a/src/xrpld/app/consensus/ConsensusExtensions.h b/src/xrpld/app/consensus/ConsensusExtensions.h index 786d1b336..9d0cbd3a7 100644 --- a/src/xrpld/app/consensus/ConsensusExtensions.h +++ b/src/xrpld/app/consensus/ConsensusExtensions.h @@ -27,9 +27,6 @@ using TickContext = ConsensusTick; /// 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 const& prevLedger = {}); bool isUNLReportMember(NodeID const& nodeId) const;