From 1287cce0f7dd885f5af0b4eec9ab4e4844249ad2 Mon Sep 17 00:00:00 2001 From: Nicholas Dudfield Date: Sun, 12 Jul 2026 09:27:16 +0700 Subject: [PATCH] fix(consensus): recycle member evidence cache --- src/test/app/Manifest_test.cpp | 84 ++++++++++++++++++++++++++ src/xrpld/app/misc/Manifest.h | 7 ++- src/xrpld/app/misc/detail/Manifest.cpp | 18 ++++-- 3 files changed, 102 insertions(+), 7 deletions(-) diff --git a/src/test/app/Manifest_test.cpp b/src/test/app/Manifest_test.cpp index a9225f106..cde400d82 100644 --- a/src/test/app/Manifest_test.cpp +++ b/src/test/app/Manifest_test.cpp @@ -829,6 +829,90 @@ public: })); } } + { + testcase( + "UNLReport member manifest evidence evicts oldest retained " + "master"); + + ManifestCache cache; + std::optional oldest; + std::optional secondOldest; + std::optional secondMasterSecret; + std::optional secondSigningSecret; + for (std::size_t i = 0; i < maxUNLReportMemberEvidenceMasters; ++i) + { + auto const sk = randomSecretKey(); + auto const kp = randomKeyPair(KeyType::secp256k1); + auto manifest = makeManifest( + sk, KeyType::ed25519, kp.second, KeyType::secp256k1, 1); + if (i == 0) + oldest = clone(manifest); + else if (i == 1) + { + secondOldest = clone(manifest); + secondMasterSecret = sk; + secondSigningSecret = kp.second; + } + BEAST_EXPECT(cache.observeUNLReportMemberManifestEvidence( + makeSlice(manifest.serialized))); + } + + BEAST_EXPECT(oldest && secondOldest); + BEAST_EXPECT( + oldest && + !cache.observeUNLReportMemberManifestEvidence( + makeSlice(oldest->serialized))); + + auto const replacementSecret = randomSecretKey(); + auto const replacementKey = randomKeyPair(KeyType::secp256k1); + auto const replacement = makeManifest( + replacementSecret, + KeyType::ed25519, + replacementKey.second, + KeyType::secp256k1, + 1); + BEAST_EXPECT(cache.observeUNLReportMemberManifestEvidence( + makeSlice(replacement.serialized))); + + auto const snapshot = + cache.getUNLReportMemberManifestEvidenceSnapshot(); + BEAST_EXPECT(snapshot.size() == maxUNLReportMemberEvidenceMasters); + BEAST_EXPECT(oldest && !containsEvidence(snapshot, *oldest)); + BEAST_EXPECT( + secondOldest && containsEvidence(snapshot, *secondOldest)); + BEAST_EXPECT(containsEvidence(snapshot, replacement)); + + BEAST_EXPECT(secondMasterSecret && secondSigningSecret); + auto const refreshed = makeManifest( + *secondMasterSecret, + KeyType::ed25519, + *secondSigningSecret, + KeyType::secp256k1, + 2); + BEAST_EXPECT(cache.observeUNLReportMemberManifestEvidence( + makeSlice(refreshed.serialized))); + + auto const nextSecret = randomSecretKey(); + auto const nextKey = randomKeyPair(KeyType::secp256k1); + auto const next = makeManifest( + nextSecret, + KeyType::ed25519, + nextKey.second, + KeyType::secp256k1, + 1); + BEAST_EXPECT(cache.observeUNLReportMemberManifestEvidence( + makeSlice(next.serialized))); + + auto const refreshedSnapshot = + cache.getUNLReportMemberManifestEvidenceSnapshot(); + BEAST_EXPECT( + refreshedSnapshot.size() == maxUNLReportMemberEvidenceMasters); + BEAST_EXPECT( + secondOldest && + !containsEvidence(refreshedSnapshot, *secondOldest)); + BEAST_EXPECT(containsEvidence(refreshedSnapshot, refreshed)); + BEAST_EXPECT(containsEvidence(refreshedSnapshot, next)); + } } void diff --git a/src/xrpld/app/misc/Manifest.h b/src/xrpld/app/misc/Manifest.h index 42f54cb0b..5bab574c6 100644 --- a/src/xrpld/app/misc/Manifest.h +++ b/src/xrpld/app/misc/Manifest.h @@ -34,6 +34,8 @@ namespace ripple { inline constexpr std::size_t maxUNLReportMemberManifestSize = 2048; +inline constexpr std::size_t maxUNLReportMemberEvidenceMasters = 256; +inline constexpr std::size_t maxUNLReportMemberEvidencePerMaster = 2; /* Validator key manifests @@ -266,9 +268,6 @@ private: beast::Journal j_; std::shared_mutex mutable mutex_; - static constexpr std::size_t maxUNLReportMemberEvidenceMasters_ = 256; - static constexpr std::size_t maxUNLReportMemberEvidencePerMaster_ = 2; - /** Active manifests stored by master public key. */ hash_map map_; @@ -285,11 +284,13 @@ private: struct UNLReportMemberManifestEvidence { std::uint32_t sequence = 0; + std::uint64_t retentionOrder = 0; std::vector manifests; }; hash_map unlReportMemberManifestEvidence_; + std::uint64_t unlReportMemberEvidenceRetentionOrder_ = 0; std::atomic seq_{0}; diff --git a/src/xrpld/app/misc/detail/Manifest.cpp b/src/xrpld/app/misc/detail/Manifest.cpp index 347c91260..57d26e594 100644 --- a/src/xrpld/app/misc/detail/Manifest.cpp +++ b/src/xrpld/app/misc/detail/Manifest.cpp @@ -597,8 +597,17 @@ ManifestCache::observeUNLReportMemberManifestEvidence(Slice serialized) if (iter == unlReportMemberManifestEvidence_.end()) { if (unlReportMemberManifestEvidence_.size() >= - maxUNLReportMemberEvidenceMasters_) - return false; + maxUNLReportMemberEvidenceMasters) + { + auto const oldest = std::min_element( + unlReportMemberManifestEvidence_.begin(), + unlReportMemberManifestEvidence_.end(), + [](auto const& lhs, auto const& rhs) { + return lhs.second.retentionOrder < + rhs.second.retentionOrder; + }); + unlReportMemberManifestEvidence_.erase(oldest); + } iter = unlReportMemberManifestEvidence_ .emplace(masterKey, UNLReportMemberManifestEvidence{}) @@ -622,10 +631,11 @@ ManifestCache::observeUNLReportMemberManifestEvidence(Slice serialized) return false; } - if (evidence.manifests.size() >= maxUNLReportMemberEvidencePerMaster_) + if (evidence.manifests.size() >= maxUNLReportMemberEvidencePerMaster) return false; evidence.manifests.emplace_back(std::move(m)); + evidence.retentionOrder = ++unlReportMemberEvidenceRetentionOrder_; return true; } @@ -638,7 +648,7 @@ ManifestCache::getUNLReportMemberManifestEvidenceSnapshot() const snapshot.reserve( map_.size() + unlReportMemberManifestEvidence_.size() * - maxUNLReportMemberEvidencePerMaster_); + maxUNLReportMemberEvidencePerMaster); for (auto const& [_, manifest] : map_) {