mirror of
https://github.com/XRPLF/rippled.git
synced 2026-09-27 23:38:08 +00:00
fix: Use a hardened hash on the STPathElement
This commit is contained in:
@@ -141,6 +141,7 @@ words:
|
||||
- hwrap
|
||||
- ifndef
|
||||
- inequation
|
||||
- Injectivity
|
||||
- insuf
|
||||
- insuff
|
||||
- invasively
|
||||
|
||||
@@ -5,9 +5,11 @@
|
||||
#include <xrpl/protocol/Concepts.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <ostream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <variant>
|
||||
|
||||
namespace xrpl {
|
||||
@@ -121,9 +123,32 @@ operator==(PathAsset const& lhs, PathAsset const& rhs)
|
||||
|
||||
template <typename Hasher>
|
||||
void
|
||||
hash_append(Hasher& h, PathAsset const& pathAsset)
|
||||
hash_append(Hasher& h, PathAsset const& pathAsset) noexcept
|
||||
{
|
||||
std::visit([&]<ValidPathAsset T>(T const& e) { hash_append(h, e); }, pathAsset.value());
|
||||
using beast::hash_append;
|
||||
using Variant = std::remove_cvref_t<decltype(pathAsset.value())>;
|
||||
|
||||
static_assert(
|
||||
std::variant_size_v<Variant> < 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<std::uint8_t>(0xFFu));
|
||||
return;
|
||||
}
|
||||
|
||||
hash_append(h, static_cast<std::uint8_t>(pathAsset.value().index()));
|
||||
std::visit(
|
||||
[&]<ValidPathAsset T>(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
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@@ -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 <class Hasher>
|
||||
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<STPath>
|
||||
{
|
||||
std::vector<STPathElement> path_;
|
||||
@@ -176,9 +186,10 @@ template <class Hasher>
|
||||
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<STPathSet>
|
||||
{
|
||||
std::vector<STPath> value_;
|
||||
xrpl::hardened_hash_set<STPath> 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<hardened_hash_set<STPath>> 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 <typename... Args>
|
||||
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 <typename Append>
|
||||
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 <typename Append>
|
||||
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 <typename... Args>
|
||||
inline void
|
||||
STPathSet::emplaceBack(Args&&... args)
|
||||
{
|
||||
value_.emplace_back(std::forward<Args>(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 <typename... Args>
|
||||
inline bool
|
||||
STPathSet::emplaceBack(Args&&... args)
|
||||
{
|
||||
return appendUnique([&](auto& value) { value.emplace_back(std::forward<Args>(args)...); });
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include <xrpl/protocol/STPathSet.h>
|
||||
|
||||
#include <xrpl/basics/CountedObject.h>
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/UnorderedContainers.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/contract.h>
|
||||
#include <xrpl/beast/hash/uhash.h>
|
||||
@@ -11,10 +13,12 @@
|
||||
#include <xrpl/protocol/STBase.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
#include <xrpl/protocol/detail/STVar.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@@ -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<hardened_hash_set<STPath>>()}
|
||||
{
|
||||
return STPathElement::getHash(*this);
|
||||
}
|
||||
|
||||
STPathSet::STPathSet(STPathSet const& other)
|
||||
: STBase{other}
|
||||
, CountedObject<STPathSet>{other}
|
||||
, value_{other.value_}
|
||||
, seen_{
|
||||
other.seen_ != nullptr ? std::make_unique<hardened_hash_set<STPath>>(*other.seen_)
|
||||
: nullptr}
|
||||
{
|
||||
}
|
||||
|
||||
STPathSet&
|
||||
STPathSet::operator=(STPathSet const& other)
|
||||
{
|
||||
if (this == &other)
|
||||
{
|
||||
return *this;
|
||||
}
|
||||
auto newSeen = other.seen_ != nullptr
|
||||
? std::make_unique<hardened_hash_set<STPath>>(*other.seen_)
|
||||
: nullptr;
|
||||
STBase::operator=(other);
|
||||
CountedObject<STPathSet>::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<std::runtime_error>("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
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include <xrpld/rpc/detail/Tuning.h>
|
||||
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/hardened_hash.h>
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/core/Job.h>
|
||||
#include <xrpl/core/JobQueue.h>
|
||||
@@ -46,16 +47,20 @@
|
||||
#include <xrpl/resource/Consumer.h>
|
||||
#include <xrpl/resource/Fees.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
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<STPathElement>{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<STPathElement> 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<int>{0, 0, 1, 1};
|
||||
auto hashes = std::set<std::size_t>{};
|
||||
auto orderings = 0uz;
|
||||
do
|
||||
{
|
||||
auto elements = std::vector<STPathElement>{};
|
||||
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<STPath>{};
|
||||
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<std::pair<STPathElement, STPathElement>>{
|
||||
// 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<STPath>{};
|
||||
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();
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -207,7 +207,7 @@ private:
|
||||
std::shared_ptr<AssetCache> rLCache_;
|
||||
|
||||
STPathElement source_;
|
||||
STPathSet completePaths_;
|
||||
STPathSet completePaths_{STPathSet::DeduplicationTag{}};
|
||||
std::vector<PathRank> pathRanks_;
|
||||
std::map<PathType, STPathSet> paths_;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user