From 53628b70c0c0fa2e5bea9cdc791235b5bef0321e Mon Sep 17 00:00:00 2001 From: Timothy Banks Date: Thu, 3 Sep 2026 17:06:27 -0400 Subject: [PATCH] fix: Use a hardened hash on the STPathElement --- .cspell.config.yaml | 1 + include/xrpl/protocol/PathAsset.h | 29 +++- include/xrpl/protocol/STPathSet.h | 161 +++++++++++++++++--- include/xrpl/protocol/detail/STVar.h | 3 +- src/libxrpl/protocol/STPathSet.cpp | 67 ++++++-- src/test/app/Path_test.cpp | 219 ++++++++++++++++++++++++++- src/xrpld/rpc/detail/Pathfinder.cpp | 31 ++-- src/xrpld/rpc/detail/Pathfinder.h | 2 +- 8 files changed, 452 insertions(+), 61 deletions(-) diff --git a/.cspell.config.yaml b/.cspell.config.yaml index c1af739255..a706c344de 100644 --- a/.cspell.config.yaml +++ b/.cspell.config.yaml @@ -141,6 +141,7 @@ words: - hwrap - ifndef - inequation + - Injectivity - insuf - insuff - invasively diff --git a/include/xrpl/protocol/PathAsset.h b/include/xrpl/protocol/PathAsset.h index ebf6fb68a4..02de9aa7df 100644 --- a/include/xrpl/protocol/PathAsset.h +++ b/include/xrpl/protocol/PathAsset.h @@ -5,9 +5,11 @@ #include #include +#include #include #include #include +#include #include namespace xrpl { @@ -121,9 +123,32 @@ operator==(PathAsset const& lhs, PathAsset const& rhs) template void -hash_append(Hasher& h, PathAsset const& pathAsset) +hash_append(Hasher& h, PathAsset const& pathAsset) noexcept { - std::visit([&](T const& e) { hash_append(h, e); }, pathAsset.value()); + using beast::hash_append; + using Variant = std::remove_cvref_t; + + static_assert( + std::variant_size_v < 0xFFu, + "PathAsset's discriminant must fit in a byte, leaving 0xFF reserved."); + + // std::visit is not noexcept: it throws bad_variant_access when the variant + // is valueless_by_exception. + if (pathAsset.value().valueless_by_exception()) [[unlikely]] + { + hash_append(h, static_cast(0xFFu)); + return; + } + + hash_append(h, static_cast(pathAsset.value().index())); + std::visit( + [&](T const& e) noexcept { + static_assert( + noexcept(hash_append(h, e)), + "Every PathAsset alternative must be nothrow-hashable."); + hash_append(h, e); + }, + pathAsset.value()); } inline bool diff --git a/include/xrpl/protocol/STPathSet.h b/include/xrpl/protocol/STPathSet.h index 5768721111..b91d899071 100644 --- a/include/xrpl/protocol/STPathSet.h +++ b/include/xrpl/protocol/STPathSet.h @@ -12,6 +12,8 @@ #include #include +#include +#include #include #include #include @@ -65,7 +67,7 @@ public: PathAsset const& asset, AccountID const& issuer); - [[nodiscard]] auto + [[nodiscard]] std::uint32_t getNodeType() const; [[nodiscard]] bool @@ -109,9 +111,6 @@ public: [[nodiscard]] bool isType(Type const& pe) const; - [[nodiscard]] size_t - getHash() const; - bool operator==(STPathElement const& t) const; @@ -120,6 +119,17 @@ private: getHash(STPathElement const& element); }; +template +void +hash_append(Hasher& h, STPathElement const& e) noexcept +{ + using beast::hash_append; + hash_append(h, (e.getNodeType() & STPathElement::TypeAccount) != 0u); + hash_append(h, e.getAccountID()); + hash_append(h, e.getPathAsset()); + hash_append(h, e.getIssuerID()); +} + class STPath final : public CountedObject { std::vector path_; @@ -176,9 +186,10 @@ template void hash_append(Hasher& h, STPath const& p) noexcept { + using beast::hash_append; for (auto const& e : p) { - beast::hash_append(h, e.getHash()); + hash_append(h, e); } } @@ -188,13 +199,39 @@ hash_append(Hasher& h, STPath const& p) noexcept class STPathSet final : public STBase, public CountedObject { std::vector value_; - xrpl::hardened_hash_set seenHashes_; + + /** + * Deduplication index over `value_`, for pathfinding. + * The use of a std::unique_ptr is intentional as it + * only requires 8 additional bytes of storage for the pointer + * as opposed to 64 bytes with an optional. This keeps the size + * of the STPathSet to within the `STVar::kMaxSize` limit of 72 bytes. + */ + std::unique_ptr> seen_; public: + struct DeduplicationTag + { + }; + STPathSet() = default; + /** + * Deduplication tagged constructor. + * Use when you want to ensure that the STPathSet does not contain duplicate paths. + */ + explicit STPathSet(DeduplicationTag); STPathSet(SField const& n); STPathSet(SerialIter& sit, SField const& name); + STPathSet(STPathSet const& other); + STPathSet(STPathSet&&) = default; + + STPathSet& + operator=(STPathSet const& other); + STPathSet& + operator=(STPathSet&&) = default; + + ~STPathSet() override = default; void add(Serializer& s) const override; @@ -204,6 +241,16 @@ public: [[nodiscard]] SerializedTypeID getSType() const override; + /** + * @brief assembleAdd adds a path to the set by combining a base path and a tail element. + * + * @param base The base path. + * @param tail The tail element. + * @return true if the path was added, false if it was a duplicate and not added. + * @remarks Requires the STPathSet to be constructed with the DeduplicationTag. The return value + * indicates whether the combined path was inserted (true) or rejected as a duplicate (false). + * It is fine for callers to ignore the return value. + */ bool assembleAdd(STPath const& base, STPathElement const& tail); @@ -229,22 +276,61 @@ public: [[nodiscard]] bool empty() const; - void + /** + * @brief pushBack adds a path to the set. + * + * @param e The path to add. + * @return true if the path was added, false if it was a duplicate and not added. + * @remarks If the STPathSet was constructed with the DeduplicationTag, then this method will + * check for duplicates and only add the path if it is not already present in the + * set. If the STPathSet was constructed without the DeduplicationTag, + * then this method will always add the path to the set, regardless of duplicates. + * It is fine for callers to ignore the return value. + */ + bool pushBack(STPath const& e); + /** + * @brief emplaceBack adds a path to the set. + * + * @param args The arguments to construct the path with. + * @return true if the path was added, false if it was a duplicate and not added. + * @remarks If the STPathSet was constructed with the DeduplicationTag, then this method will + * check for duplicates and only add the path if it is not already present in the + * set. If the STPathSet was constructed without the DeduplicationTag, + * then this method will always add the path to the set, regardless of duplicates. + * It is fine for callers to ignore the return value. + * @note The path is constructed before the duplicate check, so on a false + * return the constructed path is discarded and any argument + * forwarded as an rvalue is left in a moved-from state. Use + * pushBack when the caller needs to keep its path on rejection. + */ template - void + bool emplaceBack(Args&&... args); - [[nodiscard]] bool - contains(STPath const& path) const; - private: STBase* copy(std::size_t n, void* buf) const override; STBase* move(std::size_t n, void* buf) override; + /** + * @brief Append a path via `append`, then register it in the deduplication index. + * + * @param append Invoked with `value_`; must append exactly one path to it. + * @return true if the path was kept, false if it was a duplicate and was rolled back. + * @remarks Appends to the vector before touching the index, so that a failed allocation + * there leaves both containers untouched rather than leaving the index holding + * a path the vector does not. If the index insert reports a duplicate, or + * throws, the append is rolled back so the two containers stay consistent; in + * the throwing case the exception propagates. With no index (constructed + * without the DeduplicationTag) the append is unconditional. + */ + template + bool + appendUnique(Append&& append); + friend class detail::STVar; }; @@ -336,7 +422,7 @@ inline STPathElement::STPathElement( hashValue_ = getHash(*this); } -inline auto +inline std::uint32_t STPathElement::getNodeType() const { return type_; @@ -545,25 +631,50 @@ STPathSet::empty() const return value_.empty(); } -inline void -STPathSet::pushBack(STPath const& e) +template +inline bool +STPathSet::appendUnique(Append&& append) { - value_.push_back(e); - seenHashes_.emplace(value_.back()); -} + // Append to the vector first, so that a failed allocation there leaves both + // containers untouched rather than leaving the index holding a path the + // vector does not. + append(value_); -template -inline void -STPathSet::emplaceBack(Args&&... args) -{ - value_.emplace_back(std::forward(args)...); - seenHashes_.emplace(value_.back()); + if (seen_ == nullptr) + { + return true; + } + + try + { + if (!seen_->insert(value_.back()).second) + { + // Already present: roll back the append. + value_.pop_back(); + return false; + } + } + catch (...) + { + // The index insert failed, so roll back the append to keep the vector + // and the index consistent. + value_.pop_back(); + throw; + } + return true; } inline bool -STPathSet::contains(STPath const& path) const +STPathSet::pushBack(STPath const& e) { - return seenHashes_.contains(path); + return appendUnique([&](auto& value) { value.push_back(e); }); +} + +template +inline bool +STPathSet::emplaceBack(Args&&... args) +{ + return appendUnique([&](auto& value) { value.emplace_back(std::forward(args)...); }); } } // namespace xrpl diff --git a/include/xrpl/protocol/detail/STVar.h b/include/xrpl/protocol/detail/STVar.h index 56f868b665..72a310546e 100644 --- a/include/xrpl/protocol/detail/STVar.h +++ b/include/xrpl/protocol/detail/STVar.h @@ -34,10 +34,11 @@ concept ValidConstructSTArgs = // and includes a small-object allocation optimization. class STVar { -private: +public: // The largest "small object" we can accommodate static constexpr std::size_t kMaxSize = 72; +private: alignas(std::max_align_t) std::byte d_[kMaxSize] = {}; STBase* p_ = nullptr; diff --git a/src/libxrpl/protocol/STPathSet.cpp b/src/libxrpl/protocol/STPathSet.cpp index 658aaa65dd..2c074c3f2f 100644 --- a/src/libxrpl/protocol/STPathSet.cpp +++ b/src/libxrpl/protocol/STPathSet.cpp @@ -1,6 +1,8 @@ #include +#include #include +#include #include #include #include @@ -11,10 +13,12 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -31,6 +35,11 @@ STPathElement::getHash(STPathElement const& element) // NIKB NOTE: This doesn't have to be a secure hash as speed is more // important. We don't even really need to fully hash the whole // base_uint here, as a few bytes would do for our use. + // + // The note above is only true because the result of this function reaches + // nothing but STPathElement::operator==, where it is a fast-reject + // prefilter ahead of the field comparisons that decide the answer. Do not + // use it to key a container. for (auto const x : element.getAccountID()) hashAccount += (hashAccount * 257) ^ x; @@ -51,10 +60,49 @@ STPathElement::getHash(STPathElement const& element) return (hashAccount ^ hashCurrency ^ hashIssuer); } -[[nodiscard]] size_t -STPathElement::getHash() const +// For guidance on deciding which option to pursue: +// 1. Try to decrease the size of the STPathSet first. For instance, if a std::optional was +// injected into the type, could you get the same functionality using a std::unique_ptr instead? +// 2. If the size of the STPathSet is already as small as it can be, then consider what the cost +// of increasing STVar::kMaxSize would be on all the other STVar types. Each of those types +// will carry the additional cost of accommodating the larger STPathSet in their SBO. +// 3. If the cost of increasing STVar::kMaxSize is too high, then heap allocate the STPathSet and +// remove this static_assert. +static_assert( + sizeof(STPathSet) <= detail::STVar::kMaxSize, + "STPathSet is too large to fit in STVar's small object optimization. Please verify if it " + "should, if the kMaxSize should be increased, or if STPathSet should be stored on the heap " + "instead of in STVar."); + +STPathSet::STPathSet(DeduplicationTag) : seen_{std::make_unique>()} { - return STPathElement::getHash(*this); +} + +STPathSet::STPathSet(STPathSet const& other) + : STBase{other} + , CountedObject{other} + , value_{other.value_} + , seen_{ + other.seen_ != nullptr ? std::make_unique>(*other.seen_) + : nullptr} +{ +} + +STPathSet& +STPathSet::operator=(STPathSet const& other) +{ + if (this == &other) + { + return *this; + } + auto newSeen = other.seen_ != nullptr + ? std::make_unique>(*other.seen_) + : nullptr; + STBase::operator=(other); + CountedObject::operator=(other); + value_ = other.value_; + seen_ = std::move(newSeen); + return *this; } STPathSet::STPathSet(SerialIter& sit, SField const& name) : STBase(name) @@ -72,7 +120,8 @@ STPathSet::STPathSet(SerialIter& sit, SField const& name) : STBase(name) Throw("empty path"); } - pushBack(path); + // Move rather than converting the vector to an STPath by copy. + value_.emplace_back(std::move(path)); path.clear(); if (iType == STPathElement::TypeNone) @@ -132,16 +181,10 @@ STPathSet::move(std::size_t n, void* buf) bool STPathSet::assembleAdd(STPath const& base, STPathElement const& tail) { // assemble base+tail and add it to the set if it's not a duplicate + XRPL_ASSERT(seen_ != nullptr, "xrpl::STPathSet::assembleAdd : DeduplicationTag"); STPath combined = base; combined.pushBack(tail); - - if (!seenHashes_.insert(combined).second) - { - return false; - } - - value_.push_back(std::move(combined)); - return true; + return appendUnique([&](auto& value) { value.push_back(std::move(combined)); }); } bool diff --git a/src/test/app/Path_test.cpp b/src/test/app/Path_test.cpp index cd61668b03..5ecad1a420 100644 --- a/src/test/app/Path_test.cpp +++ b/src/test/app/Path_test.cpp @@ -25,6 +25,7 @@ #include #include +#include #include #include #include @@ -46,16 +47,20 @@ #include #include +#include #include #include +#include #include #include #include #include +#include #include #include #include #include +#include namespace xrpl::test { @@ -1943,7 +1948,7 @@ public: static constexpr AccountID kAccountID7{kAccount7}; static constexpr AccountID kAccountID8{kAccount8}; - auto ps = STPathSet{}; + auto ps = STPathSet{STPathSet::DeduplicationTag{}}; auto createPathElements = [](auto const& account1, auto const& account2) { auto base = STPath{}; @@ -2017,6 +2022,215 @@ public: BEAST_EXPECT(ps.size() == 6); } + void + testPushBackDeduplication() + { + testcase("STPathSet::pushBack/emplaceBack deduplication"); + + // pushBack and emplaceBack reject duplicates on a set built with the + // DeduplicationTag, and append unconditionally without it. Both + // report which happened. The unconditional case is the one the wire + // and JSON paths rely on: collapsing duplicates there would change the + // signed content of a transaction. + + static constexpr AccountID kAccountID1{"A3F19C7B2E5D08146FB93A7C0E2D5184BC6F3A09"}; + static constexpr AccountID kAccountID2{"1D7E4B90C2A6F3851E0B9D47A2C5F8136E0A4B7D"}; + static constexpr AccountID kAccountID3{"F08C36A1D95E27B40CA1F63E8D204B7950E1C3A6"}; + + auto makePath = [](AccountID const& account) { + auto p = STPath{}; + p.pushBack(STPathElement{STPathElement::TypeAccount, account, xrpCurrency(), account}); + return p; + }; + + auto const first = makePath(kAccountID1); + auto const second = makePath(kAccountID2); + auto const third = makePath(kAccountID3); + + // Deduplicating set: the second insert of a path is rejected, and the + // rejection is reported rather than silently swallowed. + { + auto ps = STPathSet{STPathSet::DeduplicationTag{}}; + + BEAST_EXPECT(ps.pushBack(first)); + BEAST_EXPECT(ps.size() == 1); + + BEAST_EXPECT(!ps.pushBack(first)); + BEAST_EXPECT(ps.size() == 1); + + // emplaceBack sees paths registered by pushBack... + BEAST_EXPECT(!ps.emplaceBack(first)); + BEAST_EXPECT(ps.size() == 1); + + BEAST_EXPECT(ps.emplaceBack(second)); + BEAST_EXPECT(ps.size() == 2); + + // ...and pushBack sees paths registered by emplaceBack. + BEAST_EXPECT(!ps.pushBack(second)); + BEAST_EXPECT(ps.size() == 2); + + // emplaceBack's forwarding form registers the same way. + BEAST_EXPECT(ps.emplaceBack(std::vector{third.front()})); + BEAST_EXPECT(ps.size() == 3); + BEAST_EXPECT(!ps.pushBack(third)); + BEAST_EXPECT(ps.size() == 3); + + // A rejected duplicate must not disturb what is already stored. + BEAST_EXPECT(ps[0] == first); + BEAST_EXPECT(ps[1] == second); + BEAST_EXPECT(ps[2] == third); + } + + // Without the tag there is no index, so duplicates are appended and + // both methods report success every time. + { + auto plain = STPathSet{}; + BEAST_EXPECT(plain.pushBack(first)); + BEAST_EXPECT(plain.pushBack(first)); + BEAST_EXPECT(plain.emplaceBack(first)); + BEAST_EXPECT(plain.size() == 3); + + auto named = STPathSet{sfPaths}; + BEAST_EXPECT(named.pushBack(first)); + BEAST_EXPECT(named.pushBack(first)); + BEAST_EXPECT(named.size() == 2); + } + } + + void + testPathHashInjectivity() + { + testcase("STPathElement hash injectivity"); + + auto const zeroCurrency = + STPathElement{AccountID{}, PathAsset{Currency{}}, AccountID{}, true}; + auto const zeroMPT = STPathElement{AccountID{}, PathAsset{MPTID{}}, AccountID{}, true}; + + BEAST_EXPECT(!(zeroCurrency == zeroMPT)); + + auto path = [](std::vector const& elements) { + auto p = STPath{}; + for (auto const& element : elements) + p.pushBack(element); + return p; + }; + + auto const currencyFirst = path({zeroCurrency, zeroMPT}); + auto const mptFirst = path({zeroMPT, zeroCurrency}); + + BEAST_EXPECT(!(currencyFirst == mptFirst)); + + auto const hasher = HardenedHash<>{}; + BEAST_EXPECT(hasher(currencyFirst) != hasher(mptFirst)); + + auto mask = std::vector{0, 0, 1, 1}; + auto hashes = std::set{}; + auto orderings = 0uz; + do + { + auto elements = std::vector{}; + for (auto const isMPT : mask) + { + elements.push_back(isMPT != 0 ? zeroMPT : zeroCurrency); + } + hashes.insert(hasher(path(elements))); + ++orderings; + } while (std::ranges::next_permutation(mask).found); + + BEAST_EXPECT(orderings == 6); + BEAST_EXPECT(hashes.size() == orderings); + + auto seen = hardened_hash_set{}; + for (auto const& p : {currencyFirst, mptFirst}) + { + seen.emplace(p); + } + BEAST_EXPECT(seen.size() == 2); + + // The other half of the invariant: equal elements must hash equally. + // STPathElement::operator== masks type_ down to the TypeAccount bit, so + // elements whose remaining type bits differ still compare equal -- + // hashing the full type_ would give them distinct hashes and silently + // defeat deduplication. + static constexpr AccountID kAccount{"A3F19C7B2E5D08146FB93A7C0E2D5184BC6F3A09"}; + static constexpr AccountID kIssuer{"1D7E4B90C2A6F3851E0B9D47A2C5F8136E0A4B7D"}; + + auto const equivalent = std::vector>{ + // forceAsset toggles TypeCurrency on an XRP asset. + {STPathElement{kAccount, PathAsset{xrpCurrency()}, kIssuer, true}, + STPathElement{kAccount, PathAsset{xrpCurrency()}, kIssuer, false}}, + // An explicit type mask vs. one derived from the populated fields. + {STPathElement{STPathElement::TypeAccount, kAccount, xrpCurrency(), kIssuer}, + STPathElement{kAccount, PathAsset{xrpCurrency()}, kIssuer, false}}, + }; + + for (auto const& [lhs, rhs] : equivalent) + { + BEAST_EXPECT(lhs.getNodeType() != rhs.getNodeType()); + BEAST_EXPECT(lhs == rhs); + + auto const lhsPath = path({lhs}); + auto const rhsPath = path({rhs}); + BEAST_EXPECT(hasher(lhsPath) == hasher(rhsPath)); + + auto equal = hardened_hash_set{}; + equal.emplace(lhsPath); + equal.emplace(rhsPath); + BEAST_EXPECT(equal.size() == 1); + } + } + + void + testDeserializationPreservesDuplicates() + { + testcase("STPathSet deserialization preserves duplicate paths"); + + // The `Paths` field of a signed transaction must round-trip byte for + // byte. The deduplication index exists solely for pathfinding, so the + // deserializing constructor must never engage it: collapsing duplicates + // on parse would silently change the signed content of a transaction. + + static constexpr AccountID kAccountID1{"A3F19C7B2E5D08146FB93A7C0E2D5184BC6F3A09"}; + static constexpr AccountID kAccountID2{"1D7E4B90C2A6F3851E0B9D47A2C5F8136E0A4B7D"}; + + auto const element = + STPathElement{kAccountID1, PathAsset{xrpCurrency()}, kAccountID2, true}; + + auto path = STPath{}; + path.pushBack(element); + + static constexpr auto kDuplicates = 64uz; + + auto original = STPathSet{sfPaths}; + for (auto i = 0uz; i < kDuplicates; ++i) + { + original.pushBack(path); + } + + // No index was requested, so nothing is deduplicated on the way in. + BEAST_EXPECT(original.size() == kDuplicates); + + auto s = Serializer{}; + original.add(s); + + auto sit = SerialIter{s.slice()}; + auto const parsed = STPathSet{sit, sfPaths}; + + // The duplicates survive the round trip... + BEAST_EXPECT(parsed.size() == kDuplicates); + BEAST_EXPECT(parsed.isEquivalent(original)); + + // ...and re-serializing reproduces the original bytes exactly. + auto serialized = Serializer{}; + parsed.add(serialized); + BEAST_EXPECT(serialized.getData() == s.getData()); + + // A parsed set holds no index, so appending to it stays append-only. + auto appended = parsed; + appended.pushBack(path); + BEAST_EXPECT(appended.size() == kDuplicates + 1); + } + void run() override { @@ -2031,6 +2245,9 @@ public: issuesPathNegativeRippleClientIssue23Larger(); qualityPathsQualitySetAndTest(); testAssembleAddDeduplication(); + testPushBackDeduplication(); + testPathHashInjectivity(); + testDeserializationPreservesDuplicates(); trustAutoClearTrustNormalClear(); trustAutoClearTrustAutoClear(); norippleCombinations(); diff --git a/src/xrpld/rpc/detail/Pathfinder.cpp b/src/xrpld/rpc/detail/Pathfinder.cpp index 1f530a1165..c3e74fa4eb 100644 --- a/src/xrpld/rpc/detail/Pathfinder.cpp +++ b/src/xrpld/rpc/detail/Pathfinder.cpp @@ -862,10 +862,11 @@ Pathfinder::addPathsForType( return it->second; // Otherwise, if the type has no nodes, return the empty path. - if (pathType.empty()) - return paths_[pathType]; - if (continueCallback && !continueCallback()) - return paths_[{}]; + if (pathType.empty() || (continueCallback && !continueCallback())) + { + static auto const kEmptyPath = PathType{}; + return paths_.try_emplace(kEmptyPath, STPathSet::DeduplicationTag{}).first->second; + } // Otherwise, get the paths for the parent PathType by calling // addPathsForType recursively. @@ -873,7 +874,7 @@ Pathfinder::addPathsForType( parentPathType.pop_back(); STPathSet const& parentPaths = addPathsForType(parentPathType, continueCallback); - STPathSet& pathsOut = paths_[pathType]; + STPathSet& pathsOut = paths_.try_emplace(pathType, STPathSet::DeduplicationTag{}).first->second; JLOG(j_.debug()) << "getPaths< adding onto '" << pathTypeToString(parentPathType) << "' to get '" << pathTypeToString(pathType) << "'"; @@ -959,15 +960,6 @@ Pathfinder::isNoRippleOut(STPath const& currentPath) return endElement.hasCurrency() && isNoRipple(fromAccount, toAccount, endElement.getCurrency()); } -void -addUniquePath(STPathSet& pathSet, STPath const& path) -{ - if (!pathSet.contains(path)) - { - pathSet.pushBack(path); - } -} - void Pathfinder::addLink( STPath const& currentPath, // The path to build from @@ -999,7 +991,7 @@ Pathfinder::addLink( { // non-default path to XRP destination JLOG(j_.trace()) << "complete path found ax: " << currentPath.getJson(JsonOptions::Values::None); - addUniquePath(completePaths_, currentPath); + completePaths_.pushBack(currentPath); } } else @@ -1107,7 +1099,7 @@ Pathfinder::addLink( JLOG(j_.trace()) << "complete path found ae: " << currentPath.getJson(JsonOptions::Values::None); - addUniquePath(completePaths_, currentPath); + completePaths_.pushBack(currentPath); } } else if (!bDestOnly) @@ -1237,11 +1229,12 @@ Pathfinder::addLink( // complete JLOG(j_.trace()) << "complete path found bx: " << currentPath.getJson(JsonOptions::Values::None); - addUniquePath(completePaths_, newPath); + completePaths_.pushBack(newPath); } else { - incompletePaths.pushBack(newPath); + [[maybe_unused]] auto result = incompletePaths.pushBack(newPath); + XRPL_ASSERT(result, "xrpl::Pathfinder::addLink : unique path"); } } else if (!currentPath.hasSeen( @@ -1283,7 +1276,7 @@ Pathfinder::addLink( // complete JLOG(j_.trace()) << "complete path found ba: " << currentPath.getJson(JsonOptions::Values::None); - addUniquePath(completePaths_, newPath); + completePaths_.pushBack(newPath); } else { diff --git a/src/xrpld/rpc/detail/Pathfinder.h b/src/xrpld/rpc/detail/Pathfinder.h index aeacd218d2..0b4da6abde 100644 --- a/src/xrpld/rpc/detail/Pathfinder.h +++ b/src/xrpld/rpc/detail/Pathfinder.h @@ -207,7 +207,7 @@ private: std::shared_ptr rLCache_; STPathElement source_; - STPathSet completePaths_; + STPathSet completePaths_{STPathSet::DeduplicationTag{}}; std::vector pathRanks_; std::map paths_;