Compare commits

..

16 Commits

Author SHA1 Message Date
Ed Hennis
2881c08d2e Merge branch 'develop' into ximinez/fix-getledger 2026-04-25 14:45:50 -04:00
Ed Hennis
ddbe7e87fb Merge branch 'develop' into ximinez/fix-getledger 2026-04-23 15:56:06 -04:00
Ed Hennis
f1ea5233a8 Merge branch 'develop' into ximinez/fix-getledger 2026-04-22 23:40:16 -04:00
Ed Hennis
85342554b7 Merge branch 'develop' into ximinez/fix-getledger 2026-04-22 14:49:08 -04:00
Ed Hennis
cb9b5f0399 Merge branch 'develop' into ximinez/fix-getledger 2026-04-22 13:10:39 -04:00
Ed Hennis
2f79477190 Merge branch 'develop' into ximinez/fix-getledger 2026-04-21 18:48:35 -04:00
Ed Hennis
55e5374f56 Merge branch 'develop' into ximinez/fix-getledger 2026-04-21 15:20:21 -04:00
Ed Hennis
846f29e8ad Merge branch 'develop' into ximinez/fix-getledger 2026-04-20 17:49:43 -04:00
Ed Hennis
d236569282 Merge branch 'develop' into ximinez/fix-getledger 2026-04-20 15:45:00 -04:00
Ed Hennis
58170b5ece Merge branch 'develop' into ximinez/fix-getledger 2026-04-20 11:38:57 -04:00
Ed Hennis
8bb87e9ca9 Merge remote-tracking branch 'XRPLF/develop' into ximinez/fix-getledger
* XRPLF/develop:
  chore: Enable clang-tidy include cleaner (6947)
  fix: Change AMMClawback return code to tecNO_PERMISSION (6946)
  ci: [DEPENDABOT] bump actions/upload-pages-artifact from 4.0.0 to 5.0.0 (6927)
  ci: [DEPENDABOT] bump actions/upload-artifact from 7.0.0 to 7.0.1 (6928)
  chore: Enable clang-tidy readability checks (6930)
2026-04-17 18:08:54 -04:00
Ed Hennis
9545e1f7ce Merge branch 'develop' into ximinez/fix-getledger 2026-04-16 13:44:33 -04:00
Ed Hennis
22b3cbbada Merge branch 'develop' into ximinez/fix-getledger 2026-04-15 19:06:26 -04:00
Ed Hennis
cbc2288bdc Merge branch 'develop' into ximinez/fix-getledger 2026-04-15 14:28:51 -04:00
Ed Hennis
5fa9fc55bb Merge branch 'develop' into ximinez/fix-getledger 2026-04-13 20:28:41 -04:00
Ed Hennis
4ab2950410 Reduce duplicate peer traffic for ledger data (#5126)
Improve job queue collision checks and logging
- Improve logging related to ledger acquisition and operating mode
  changes
- Class "CanProcess" to keep track of processing of distinct items

- Drop duplicate outgoing TMGetLedger messages per peer
  - Allow a retry after 30s in case of peer or network congestion.
  - Addresses RIPD-1870
  - (Changes levelization. That is not desirable, and will need to be fixed.)
- Drop duplicate incoming TMGetLedger messages per peer
  - Allow a retry after 15s in case of peer or network congestion.
  - The requestCookie is ignored when computing the hash, thus increasing
    the chances of detecting duplicate messages.
  - With duplicate messages, keep track of the different requestCookies
    (or lack of cookie). When work is finally done for a given request,
    send the response to all the peers that are waiting on the request,
    sending one message per peer, including all the cookies and
    a "directResponse" flag indicating the data is intended for the
    sender, too.
  - Addresses RIPD-1871
- Drop duplicate incoming TMLedgerData messages
  - Addresses RIPD-1869
2026-04-13 18:40:46 -04:00
42 changed files with 913 additions and 2576 deletions

View File

@@ -188,16 +188,10 @@ test.toplevel > xrpl.json
test.unit_test > xrpl.basics
test.unit_test > xrpl.protocol
tests.libxrpl > xrpl.basics
tests.libxrpl > xrpl.core
tests.libxrpl > xrpl.json
tests.libxrpl > xrpl.ledger
tests.libxrpl > xrpl.net
tests.libxrpl > xrpl.nodestore
tests.libxrpl > xrpl.protocol
tests.libxrpl > xrpl.protocol_autogen
tests.libxrpl > xrpl.server
tests.libxrpl > xrpl.shamap
tests.libxrpl > xrpl.tx
xrpl.conditions > xrpl.basics
xrpl.conditions > xrpl.protocol
xrpl.core > xrpl.basics

View File

@@ -51,21 +51,20 @@ def generate_strategy_matrix(all: bool, config: Config) -> list:
# Only generate a subset of configurations in PRs.
if not all:
# Debian:
# - Bookworm using GCC 13: Debug on linux/amd64, set the reference
# fee to 500 and enable code coverage (which will be done below).
# - Bookworm using GCC 15: Debug on linux/amd64, enable Address and
# UB sanitizers (which will be done below).
# - Bookworm using GCC 13: Release on linux/amd64, set the reference
# fee to 500.
# - Bookworm using GCC 15: Debug on linux/amd64, enable code
# coverage (which will be done below).
# - Bookworm using Clang 16: Debug on linux/amd64, enable voidstar.
# - Bookworm using Clang 17: Release on linux/amd64, set the
# reference fee to 1000.
# - Bookworm using Clang 20: Debug on linux/amd64, enable Address
# and UB sanitizers (which will be done below).
# - Bookworm using Clang 20: Debug on linux/amd64.
if os["distro_name"] == "debian":
skip = True
if os["distro_version"] == "bookworm":
if (
f"{os['compiler_name']}-{os['compiler_version']}" == "gcc-13"
and build_type == "Debug"
and build_type == "Release"
and architecture["platform"] == "linux/amd64"
):
cmake_args = f"-DUNIT_TEST_REFERENCE_FEE=500 {cmake_args}"
@@ -194,11 +193,11 @@ def generate_strategy_matrix(all: bool, config: Config) -> list:
):
continue
# Enable code coverage for Debian Bookworm using GCC 13 in Debug on
# linux/amd64.
# Enable code coverage for Debian Bookworm using GCC 15 in Debug on
# linux/amd64
if (
f"{os['distro_name']}-{os['distro_version']}" == "debian-bookworm"
and f"{os['compiler_name']}-{os['compiler_version']}" == "gcc-13"
and f"{os['compiler_name']}-{os['compiler_version']}" == "gcc-15"
and build_type == "Debug"
and architecture["platform"] == "linux/amd64"
):
@@ -235,39 +234,23 @@ def generate_strategy_matrix(all: bool, config: Config) -> list:
# Add the configuration to the list, with the most unique fields first,
# so that they are easier to identify in the GitHub Actions UI, as long
# names get truncated.
# Add Address and UB sanitizers as separate configurations for specific
# bookworm distros. Thread sanitizer is currently disabled (see below).
# Add Address and Thread (both coupled with UB) sanitizers for specific bookworm distros.
# GCC-Asan xrpld-embedded tests are failing because of https://github.com/google/sanitizers/issues/856
if os[
"distro_version"
] == "bookworm" and f"{os['compiler_name']}-{os['compiler_version']}" in [
"gcc-15",
"clang-20",
]:
# Add ASAN configuration.
if (
os["distro_version"] == "bookworm"
and f"{os['compiler_name']}-{os['compiler_version']}" == "clang-20"
):
# Add ASAN + UBSAN configuration.
configurations.append(
{
"config_name": config_name + "-asan",
"config_name": config_name + "-asan-ubsan",
"cmake_args": cmake_args,
"cmake_target": cmake_target,
"build_only": build_only,
"build_type": build_type,
"os": os,
"architecture": architecture,
"sanitizers": "address",
}
)
# Add UBSAN configuration.
configurations.append(
{
"config_name": config_name + "-ubsan",
"cmake_args": cmake_args,
"cmake_target": cmake_target,
"build_only": build_only,
"build_type": build_type,
"os": os,
"architecture": architecture,
"sanitizers": "undefinedbehavior",
"sanitizers": "address,undefinedbehavior",
}
)
# TSAN is deactivated due to seg faults with latest compilers.

View File

@@ -78,11 +78,6 @@ public:
cleanup_();
}
CanProcess(CanProcess const&) = delete;
CanProcess&
operator=(CanProcess const&) = delete;
explicit
operator bool() const
{

View File

@@ -67,10 +67,8 @@ private:
}
else
{
for (; elapsed > 0; --elapsed)
{
while ((elapsed--) != 0u)
m_value -= (m_value + Window - 1) / Window;
}
}
}

View File

@@ -43,10 +43,8 @@ public:
: work_(boost::asio::make_work_guard(ios_))
{
threads_.reserve(concurrency);
for (std::size_t i = 0; i < concurrency; ++i)
{
while ((concurrency--) != 0u)
threads_.emplace_back([&] { ios_.run(); });
}
}
~enable_yield_to()

View File

@@ -137,6 +137,13 @@ private:
return std::move(peers_);
}
/** Return set of peers waiting for reply. Leaves list unchanged. */
std::set<PeerShortID> const&
peekPeerSet()
{
return peers_;
}
/** Return seated relay time point if the message has been relayed */
[[nodiscard]] std::optional<Stopwatch::time_point>
relayed() const
@@ -168,6 +175,20 @@ private:
return true;
}
bool
shouldProcessForPeer(
PeerShortID peer,
Stopwatch::time_point now,
std::chrono::seconds interval)
{
if (peerProcessed_.contains(peer) && ((peerProcessed_[peer] + interval) > now))
return false;
// Peer may already be in the list, but adding it again doesn't hurt
addPeer(peer);
peerProcessed_[peer] = now;
return true;
}
private:
HashRouterFlags flags_ = HashRouterFlags::UNDEFINED;
std::set<PeerShortID> peers_;
@@ -175,6 +196,7 @@ private:
// than one flag needs to expire independently.
std::optional<Stopwatch::time_point> relayed_;
std::optional<Stopwatch::time_point> processed_;
std::map<PeerShortID, Stopwatch::time_point> peerProcessed_;
};
public:
@@ -214,6 +236,15 @@ public:
HashRouterFlags& flags,
std::chrono::seconds tx_interval);
/** Determines whether the hashed item should be processed for the given
peer. Could be an incoming or outgoing message.
Items filtered with this function should only be processed for the given
peer once. Unlike shouldProcess, it can be processed for other peers.
*/
bool
shouldProcessForPeer(uint256 const& key, PeerShortID peer, std::chrono::seconds interval);
/** Set the flags on a hash.
@return `true` if the flags were changed. `false` if unchanged.
@@ -239,6 +270,11 @@ public:
std::optional<std::set<PeerShortID>>
shouldRelay(uint256 const& key);
/** Returns a copy of the set of peers in the Entry for the key
*/
std::set<PeerShortID>
getPeers(uint256 const& key);
private:
// pair.second indicates whether the entry was created
std::pair<Entry&, bool>

View File

@@ -54,9 +54,8 @@ read_varint(void const* buf, std::size_t buflen, std::size_t& t)
return 1;
}
auto const used = n;
while (n > 0)
while (n--)
{
--n;
auto const d = p[n];
auto const t0 = t;
t *= 127;

View File

@@ -288,8 +288,18 @@ message TMLedgerData {
required uint32 ledgerSeq = 2;
required TMLedgerInfoType type = 3;
repeated TMLedgerNode nodes = 4;
// If the peer supports "responseCookies", this field will
// never be populated.
optional uint32 requestCookie = 5;
optional TMReplyError error = 6;
// The old field is called "requestCookie", but this is
// a response, so this name makes more sense
repeated uint32 responseCookies = 7;
// If a TMGetLedger request was received without a "requestCookie",
// and the peer supports it, this flag will be set to true to
// indicate that the receiver should process the result in addition
// to forwarding it to its "responseCookies" peers.
optional bool directResponse = 8;
}
message TMPing {

View File

@@ -15,7 +15,6 @@
// Add new amendments to the top of this list.
// Keep it sorted in reverse chronological order.
XRPL_FIX (Cleanup3_2_0, Supported::no, VoteBehavior::DefaultNo)
XRPL_FEATURE(MPTokensV2, Supported::no, VoteBehavior::DefaultNo)
XRPL_FIX (Security3_1_3, Supported::no, VoteBehavior::DefaultNo)
XRPL_FIX (PermissionedDomainInvariant, Supported::yes, VoteBehavior::DefaultNo)

View File

@@ -130,19 +130,6 @@ public:
return sle_->at(sfFlags);
}
/**
* @brief Check if a specific flag is set.
*
* @param f The flag bitmask to check
* @return true if all bits in f are set in the flags field
*/
[[nodiscard]]
bool
isFlag(std::uint32_t f) const
{
return sle_->isFlag(f);
}
/**
* @brief Get the underlying SLE object.
*

View File

@@ -11,6 +11,7 @@ float-cast-overflow:external
float-divide-by-zero:external
function:external
implicit-integer-sign-change:external
implicit-signed-integer-truncation::external
implicit-signed-integer-truncation:external
implicit-unsigned-integer-truncation:external
integer-divide-by-zero:external
@@ -70,15 +71,145 @@ vla-bound:boost
vptr_check:boost
vptr:boost
# Google protobuf - intentional overflows in hash functions
# Google protobuf
undefined:protobuf
# Suppress UBSan errors in xrpld code by source file path
undefined:src/libxrpl/basics/base64.cpp
undefined:src/libxrpl/basics/Number.cpp
undefined:src/libxrpl/beast/utility/beast_Journal.cpp
undefined:src/libxrpl/crypto/RFC1751.cpp
undefined:src/libxrpl/ledger/ApplyView.cpp
undefined:src/libxrpl/ledger/View.cpp
undefined:src/libxrpl/protocol/Permissions.cpp
undefined:src/libxrpl/protocol/STAmount.cpp
undefined:src/libxrpl/protocol/STPathSet.cpp
undefined:src/libxrpl/protocol/tokens.cpp
undefined:src/libxrpl/shamap/SHAMap.cpp
undefined:src/test/app/Batch_test.cpp
undefined:src/test/app/Invariants_test.cpp
undefined:src/test/app/NFToken_test.cpp
undefined:src/test/app/Offer_test.cpp
undefined:src/test/app/Path_test.cpp
undefined:src/test/basics/XRPAmount_test.cpp
undefined:src/test/beast/LexicalCast_test.cpp
undefined:src/test/jtx/impl/acctdelete.cpp
undefined:src/test/ledger/SkipList_test.cpp
undefined:src/test/rpc/Subscribe_test.cpp
undefined:src/tests/libxrpl/basics/RangeSet.cpp
undefined:src/xrpld/app/main/BasicApp.cpp
undefined:src/xrpld/app/main/BasicApp.cpp
undefined:src/xrpld/app/misc/detail/AmendmentTable.cpp
undefined:src/xrpld/app/misc/NetworkOPs.cpp
undefined:src/libxrpl/json/json_value.cpp
undefined:src/xrpld/app/paths/detail/StrandFlow.h
undefined:src/xrpld/app/tx/detail/NFTokenMint.cpp
undefined:src/xrpld/app/tx/detail/OracleSet.cpp
undefined:src/xrpld/core/detail/JobQueue.cpp
undefined:src/xrpld/core/detail/Workers.cpp
undefined:src/xrpld/rpc/detail/Role.cpp
undefined:src/xrpld/rpc/handlers/GetAggregatePrice.cpp
undefined:xrpl/basics/base_uint.h
undefined:xrpl/basics/DecayingSample.h
undefined:xrpl/beast/test/yield_to.h
undefined:xrpl/beast/xor_shift_engine.h
undefined:xrpl/nodestore/detail/varint.h
undefined:xrpl/peerfinder/detail/Counts.h
undefined:xrpl/protocol/nft.h
# basic_string.h:483:51: runtime error: unsigned integer overflow
unsigned-integer-overflow:basic_string.h
unsigned-integer-overflow:bits/chrono.h
unsigned-integer-overflow:bits/random.h
unsigned-integer-overflow:bits/random.tcc
unsigned-integer-overflow:bits/stl_algobase.h
unsigned-integer-overflow:bits/uniform_int_dist.h
unsigned-integer-overflow:string_view
# runtime error: unsigned integer overflow: 0 - 1 cannot be represented in type 'std::size_t' (aka 'unsigned long')
unsigned-integer-overflow:src/libxrpl/basics/base64.cpp
unsigned-integer-overflow:src/libxrpl/basics/Number.cpp
unsigned-integer-overflow:src/libxrpl/crypto/RFC1751.cpp
unsigned-integer-overflow:rc/libxrpl/json/json_value.cpp
unsigned-integer-overflow:src/libxrpl/ledger/ApplyView.cpp
unsigned-integer-overflow:src/libxrpl/ledger/View.cpp
unsigned-integer-overflow:src/libxrpl/protocol/Permissions.cpp
unsigned-integer-overflow:src/libxrpl/protocol/STAmount.cpp
unsigned-integer-overflow:src/libxrpl/protocol/STPathSet.cpp
unsigned-integer-overflow:src/libxrpl/protocol/tokens.cpp
unsigned-integer-overflow:src/libxrpl/shamap/SHAMap.cpp
unsigned-integer-overflow:src/test/app/Batch_test.cpp
unsigned-integer-overflow:src/test/app/Invariants_test.cpp
unsigned-integer-overflow:src/test/app/NFToken_test.cpp
unsigned-integer-overflow:src/test/app/Offer_test.cpp
unsigned-integer-overflow:src/test/app/Path_test.cpp
unsigned-integer-overflow:src/test/basics/XRPAmount_test.cpp
unsigned-integer-overflow:src/test/beast/LexicalCast_test.cpp
unsigned-integer-overflow:src/test/jtx/impl/acctdelete.cpp
unsigned-integer-overflow:src/test/ledger/SkipList_test.cpp
unsigned-integer-overflow:src/test/rpc/Subscribe_test.cpp
unsigned-integer-overflow:src/tests/libxrpl/basics/RangeSet.cpp
unsigned-integer-overflow:src/xrpld/app/main/BasicApp.cpp
unsigned-integer-overflow:src/xrpld/app/misc/detail/AmendmentTable.cpp
unsigned-integer-overflow:src/xrpld/app/misc/NetworkOPs.cpp
unsigned-integer-overflow:src/xrpld/app/paths/detail/StrandFlow.h
unsigned-integer-overflow:src/xrpld/app/tx/detail/NFTokenMint.cpp
unsigned-integer-overflow:src/xrpld/app/tx/detail/OracleSet.cpp
unsigned-integer-overflow:src/xrpld/rpc/detail/Role.cpp
unsigned-integer-overflow:src/xrpld/rpc/handlers/GetAggregatePrice.cpp
unsigned-integer-overflow:xrpl/basics/base_uint.h
unsigned-integer-overflow:xrpl/basics/DecayingSample.h
unsigned-integer-overflow:xrpl/beast/test/yield_to.h
unsigned-integer-overflow:xrpl/beast/xor_shift_engine.h
unsigned-integer-overflow:xrpl/nodestore/detail/varint.h
unsigned-integer-overflow:xrpl/peerfinder/detail/Counts.h
unsigned-integer-overflow:xrpl/protocol/nft.h
# Xrpld intentional overflows and operations
# STAmount uses intentional negation of INT64_MIN and overflow in arithmetic
signed-integer-overflow:src/libxrpl/protocol/STAmount.cpp
unsigned-integer-overflow:src/libxrpl/protocol/STAmount.cpp
# XRPAmount test intentional overflows
signed-integer-overflow:src/test/basics/XRPAmount_test.cpp
# Peerfinder intentional overflow in counter arithmetic
unsigned-integer-overflow:src/xrpld/peerfinder/detail/Counts.h
# Signed integer overflow suppressions
signed-integer-overflow:src/test/beast/LexicalCast_test.cpp
# External library suppressions
unsigned-integer-overflow:nudb/detail/xxhash.hpp
# Loan_test.cpp intentional underflow in test arithmetic
unsigned-integer-overflow:src/test/app/Loan_test.cpp
undefined:src/test/app/Loan_test.cpp
# Source tree restructured paths (libxrpl/tx/transactors/)
# These duplicate the xrpld/app/tx/detail entries above for the new layout
unsigned-integer-overflow:src/libxrpl/tx/transactors/oracle/OracleSet.cpp
undefined:src/libxrpl/tx/transactors/oracle/OracleSet.cpp
unsigned-integer-overflow:src/libxrpl/tx/transactors/nft/NFTokenMint.cpp
undefined:src/libxrpl/tx/transactors/nft/NFTokenMint.cpp
# Protobuf intentional overflows in hash functions
# Protobuf uses intentional unsigned overflow for hash computation (stringpiece.h:393)
unsigned-integer-overflow:google/protobuf/stubs/stringpiece.h
# gRPC intentional overflows in timer calculations
# gRPC intentional overflows
# gRPC uses intentional overflow in timer calculations
unsigned-integer-overflow:grpc
unsigned-integer-overflow:timer_manager.cc
# RocksDB intentional unsigned integer overflows in hash functions and CRC calculations
# Standard library intentional overflows
# These are intentional overflows in random number generation and character conversion
unsigned-integer-overflow:__random/seed_seq.h
unsigned-integer-overflow:__charconv/traits.h
# Suppress errors in RocksDB
# RocksDB uses intentional unsigned integer overflows in hash functions and CRC calculations
unsigned-integer-overflow:rocks*/*/util/xxhash.h
unsigned-integer-overflow:rocks*/*/util/xxph3.h
unsigned-integer-overflow:rocks*/*/util/hash.cc
@@ -90,14 +221,13 @@ unsigned-integer-overflow:rocks*/*/table/format.cc
unsigned-integer-overflow:rocks*/*/table/block_based/block_based_table_builder.cc
unsigned-integer-overflow:rocks*/*/table/block_based/reader_common.cc
unsigned-integer-overflow:rocks*/*/db/version_set.cc
# RocksDB misaligned loads (intentional for performance on ARM64)
alignment:rocks*/*/util/crc32c_arm64.cc
undefined:rocks*/*/util/crc32c_arm64.cc
undefined:rocks*/*/util/xxhash.h
# nudb intentional overflows in hash functions
unsigned-integer-overflow:nudb/detail/xxhash.hpp
alignment:nudb/detail/xxhash.hpp
undefined:nudb
# Snappy compression library intentional overflows
unsigned-integer-overflow:snappy.cc
@@ -109,40 +239,10 @@ unsigned-integer-overflow:absl/base/internal/low_level_alloc.cc
unsigned-integer-overflow:absl/hash/internal/hash.h
unsigned-integer-overflow:absl/container/internal/raw_hash_set.h
# Standard library intentional overflows
unsigned-integer-overflow:basic_string.h
unsigned-integer-overflow:bits/chrono.h
unsigned-integer-overflow:bits/random.h
unsigned-integer-overflow:bits/random.tcc
unsigned-integer-overflow:bits/stl_algobase.h
unsigned-integer-overflow:bits/uniform_int_dist.h
unsigned-integer-overflow:string_view
unsigned-integer-overflow:__random/seed_seq.h
unsigned-integer-overflow:__charconv/traits.h
# Standard library intentional overflows in chrono duration arithmetic
unsigned-integer-overflow:__chrono/duration.h
# =============================================================================
# Rippled code suppressions
# =============================================================================
# Signed integer negation (-value) in amount types.
# INT64_MIN cannot occur in practice due to domain invariants (mantissa ranges
# are well within int64_t bounds), but UBSan flags the pattern as potential
# signed overflow. Narrowed to operator- to avoid suppressing unrelated
# overflows anywhere in a stack trace containing these type names.
signed-integer-overflow:operator-*IOUAmount*
signed-integer-overflow:operator-*XRPAmount*
signed-integer-overflow:operator-*MPTAmount*
signed-integer-overflow:operator-*STAmount*
# STAmount::operator+ signed addition — operands are bounded by total supply
# (~10^17 for XRP, ~10^18 for MPT) so overflow cannot occur in practice.
signed-integer-overflow:operator+*STAmount*
# STAmount::getRate uses unsigned shift and addition
unsigned-integer-overflow:*STAmount*getRate*
# STAmount::serialize uses unsigned bitwise operations
unsigned-integer-overflow:*STAmount*serialize*
# nft::cipheredTaxon uses intentional uint32 wraparound (LCG permutation)
unsigned-integer-overflow:cipheredTaxon
# Suppress undefined errors in RocksDB and nudb
undefined:rocks.*/*/util/crc32c_arm64.cc
undefined:rocks.*/*/util/xxhash.h
undefined:nudb

View File

@@ -107,7 +107,7 @@ encode(void* dest, void const* src, std::size_t len)
char const* in = static_cast<char const*>(src);
auto const tab = base64::get_alphabet();
for (auto n = len / 3; n > 0; --n)
for (auto n = len / 3; n != 0u; --n)
{
*out++ = tab[(in[0] & 0xfc) >> 2];
*out++ = tab[((in[0] & 0x03) << 4) + ((in[1] & 0xf0) >> 4)];

View File

@@ -82,6 +82,19 @@ HashRouter::shouldProcess(
return s.shouldProcess(suppressionMap_.clock().now(), tx_interval);
}
bool
HashRouter::shouldProcessForPeer(
uint256 const& key,
PeerShortID peer,
std::chrono::seconds interval)
{
std::lock_guard lock(mutex_);
auto& entry = emplace(key).first;
return entry.shouldProcessForPeer(peer, suppressionMap_.clock().now(), interval);
}
HashRouterFlags
HashRouter::getFlags(uint256 const& key)
{
@@ -119,4 +132,13 @@ HashRouter::shouldRelay(uint256 const& key) -> std::optional<std::set<PeerShortI
return s.releasePeerSet();
}
auto
HashRouter::getPeers(uint256 const& key) -> std::set<PeerShortID>
{
std::lock_guard lock(mutex_);
auto& s = emplace(key).first;
return s.peekPeerSet();
}
} // namespace xrpl

View File

@@ -394,6 +394,33 @@ class HashRouter_test : public beast::unit_test::suite
BEAST_EXPECT(!any(HF::UNDEFINED));
}
void
testProcessPeer()
{
using namespace std::chrono_literals;
TestStopwatch stopwatch;
HashRouter router(getSetup(5s, 5s), stopwatch);
uint256 const key(1);
HashRouter::PeerShortID peer1 = 1;
HashRouter::PeerShortID peer2 = 2;
auto const timeout = 2s;
BEAST_EXPECT(router.shouldProcessForPeer(key, peer1, timeout));
BEAST_EXPECT(!router.shouldProcessForPeer(key, peer1, timeout));
++stopwatch;
BEAST_EXPECT(!router.shouldProcessForPeer(key, peer1, timeout));
BEAST_EXPECT(router.shouldProcessForPeer(key, peer2, timeout));
BEAST_EXPECT(!router.shouldProcessForPeer(key, peer2, timeout));
++stopwatch;
BEAST_EXPECT(router.shouldProcessForPeer(key, peer1, timeout));
BEAST_EXPECT(!router.shouldProcessForPeer(key, peer2, timeout));
++stopwatch;
BEAST_EXPECT(router.shouldProcessForPeer(key, peer2, timeout));
++stopwatch;
BEAST_EXPECT(router.shouldProcessForPeer(key, peer1, timeout));
BEAST_EXPECT(!router.shouldProcessForPeer(key, peer2, timeout));
}
public:
void
run() override
@@ -406,6 +433,7 @@ public:
testProcess();
testSetup();
testFlagsOps();
testProcessPeer();
}
};

View File

@@ -130,12 +130,7 @@ public:
}
void
acquireAsync(
JobType type,
std::string const& name,
uint256 const& hash,
std::uint32_t seq,
InboundLedger::Reason reason) override
acquireAsync(uint256 const& hash, std::uint32_t seq, InboundLedger::Reason reason) override
{
}
@@ -344,6 +339,11 @@ public:
{
return false;
}
std::set<std::optional<uint64_t>>
releaseRequestCookies(uint256 const& requestHash) override
{
return {};
}
[[nodiscard]] std::string const&
fingerprint() const override

View File

@@ -1,165 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2012-2016 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <xrpl/basics/CanProcess.h>
#include <xrpl/beast/unit_test.h>
#include <memory>
namespace ripple {
namespace test {
struct CanProcess_test : beast::unit_test::suite
{
template <class Mutex, class Collection, class Item>
void
test(
std::string const& name,
Mutex& mtx,
Collection& collection,
std::vector<Item> const& items)
{
testcase(name);
if (!BEAST_EXPECT(!items.empty()))
return;
if (!BEAST_EXPECT(collection.empty()))
return;
// CanProcess objects can't be copied or moved. To make that easier,
// store shared_ptrs
std::vector<std::shared_ptr<CanProcess>> trackers;
// Fill up the vector with two CanProcess for each Item. The first
// inserts the item into the collection and is "good". The second does
// not and is "bad".
for (int i = 0; i < items.size(); ++i)
{
{
auto const& good =
trackers.emplace_back(std::make_shared<CanProcess>(mtx, collection, items[i]));
BEAST_EXPECT(*good);
}
BEAST_EXPECT(trackers.size() == (2 * i) + 1);
BEAST_EXPECT(collection.size() == i + 1);
{
auto const& bad =
trackers.emplace_back(std::make_shared<CanProcess>(mtx, collection, items[i]));
BEAST_EXPECT(!*bad);
}
BEAST_EXPECT(trackers.size() == 2 * (i + 1));
BEAST_EXPECT(collection.size() == i + 1);
}
BEAST_EXPECT(collection.size() == items.size());
// Now remove the items from the vector<CanProcess> two at a time, and
// try to get another CanProcess for that item.
for (int i = 0; i < items.size(); ++i)
{
// Remove the "bad" one in the second position
// This will have no effect on the collection
{
auto const iter = trackers.begin() + 1;
BEAST_EXPECT(!**iter);
trackers.erase(iter);
}
BEAST_EXPECT(trackers.size() == (2 * items.size()) - 1);
BEAST_EXPECT(collection.size() == items.size());
{
// Append a new "bad" one
auto const& bad =
trackers.emplace_back(std::make_shared<CanProcess>(mtx, collection, items[i]));
BEAST_EXPECT(!*bad);
}
BEAST_EXPECT(trackers.size() == 2 * items.size());
BEAST_EXPECT(collection.size() == items.size());
// Remove the "good" one from the front
{
auto const iter = trackers.begin();
BEAST_EXPECT(**iter);
trackers.erase(iter);
}
BEAST_EXPECT(trackers.size() == (2 * items.size()) - 1);
BEAST_EXPECT(collection.size() == items.size() - 1);
{
// Append a new "good" one
auto const& good =
trackers.emplace_back(std::make_shared<CanProcess>(mtx, collection, items[i]));
BEAST_EXPECT(*good);
}
BEAST_EXPECT(trackers.size() == 2 * items.size());
BEAST_EXPECT(collection.size() == items.size());
}
// Now remove them all two at a time
for (int i = items.size() - 1; i >= 0; --i)
{
// Remove the "bad" one from the front
{
auto const iter = trackers.begin();
BEAST_EXPECT(!**iter);
trackers.erase(iter);
}
BEAST_EXPECT(trackers.size() == (2 * i) + 1);
BEAST_EXPECT(collection.size() == i + 1);
// Remove the "good" one now in front
{
auto const iter = trackers.begin();
BEAST_EXPECT(**iter);
trackers.erase(iter);
}
BEAST_EXPECT(trackers.size() == 2 * i);
BEAST_EXPECT(collection.size() == i);
}
BEAST_EXPECT(trackers.empty());
BEAST_EXPECT(collection.empty());
}
void
run() override
{
{
std::mutex m;
std::set<int> collection;
std::vector<int> const items{1, 2, 3, 4, 5};
test("set of int", m, collection, items);
}
{
std::mutex m;
std::set<std::string> collection;
std::vector<std::string> const items{"one", "two", "three", "four", "five"};
test("set of string", m, collection, items);
}
{
std::mutex m;
std::unordered_set<char> collection;
std::vector<char> const items{'1', '2', '3', '4', '5'};
test("unorderd_set of char", m, collection, items);
}
{
std::mutex m;
std::unordered_set<std::uint64_t> collection;
std::vector<std::uint64_t> const items{100u, 1000u, 150u, 4u, 0u};
test("unordered_set of uint64_t", m, collection, items);
}
}
};
BEAST_DEFINE_TESTSUITE(CanProcess, ripple_basics, ripple);
} // namespace test
} // namespace ripple

View File

@@ -24,7 +24,7 @@ public:
testInteger(IntType in)
{
std::string s;
IntType out = static_cast<IntType>(~in); // Ensure out != in
IntType out(in + 1);
expect(lexicalCastChecked(s, in));
expect(lexicalCastChecked(out, s));

View File

@@ -63,8 +63,8 @@ public:
negotiateProtocolVersion("RTXP/1.2, XRPL/2.0, XRPL/2.1") == make_protocol(2, 1));
BEAST_EXPECT(negotiateProtocolVersion("XRPL/2.2") == make_protocol(2, 2));
BEAST_EXPECT(
negotiateProtocolVersion("RTXP/1.2, XRPL/2.2, XRPL/2.3, XRPL/999.999") ==
make_protocol(2, 2));
negotiateProtocolVersion("RTXP/1.2, XRPL/2.2, XRPL/2.3, XRPL/2.4, XRPL/999.999") ==
make_protocol(2, 3));
BEAST_EXPECT(negotiateProtocolVersion("XRPL/999.999, WebSocket/1.0") == std::nullopt);
BEAST_EXPECT(negotiateProtocolVersion("") == std::nullopt);
}

View File

@@ -190,6 +190,11 @@ public:
removeTxQueue(uint256 const&) override
{
}
std::set<std::optional<uint64_t>>
releaseRequestCookies(uint256 const& requestHash) override
{
return {};
}
};
/** Manually advanced clock. */

View File

@@ -8,12 +8,9 @@ add_custom_target(xrpl.tests)
# Test helpers
add_library(xrpl.helpers.test STATIC)
target_sources(
xrpl.helpers.test
PRIVATE helpers/Account.cpp helpers/TestSink.cpp helpers/TxTest.cpp
)
target_sources(xrpl.helpers.test PRIVATE helpers/TestSink.cpp)
target_include_directories(xrpl.helpers.test PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(xrpl.helpers.test PUBLIC xrpl.libxrpl gtest::gtest)
target_link_libraries(xrpl.helpers.test PRIVATE xrpl.libxrpl)
# Common library dependencies for the rest of the tests.
add_library(xrpl.imports.test INTERFACE)
@@ -35,10 +32,6 @@ xrpl_add_test(json)
target_link_libraries(xrpl.test.json PRIVATE xrpl.imports.test)
add_dependencies(xrpl.tests xrpl.test.json)
xrpl_add_test(tx)
target_link_libraries(xrpl.test.tx PRIVATE xrpl.imports.test)
add_dependencies(xrpl.tests xrpl.test.tx)
xrpl_add_test(protocol_autogen)
target_link_libraries(xrpl.test.protocol_autogen PRIVATE xrpl.imports.test)
add_dependencies(xrpl.tests xrpl.test.protocol_autogen)

View File

@@ -1,19 +0,0 @@
#include <helpers/Account.h>
#include <xrpl/protocol/KeyType.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Seed.h>
namespace xrpl::test {
Account const Account::master{"masterpassphrase"};
Account::Account(std::string_view name, KeyType type)
: name_(name)
, keyPair_(generateKeyPair(type, generateSeed(name_)))
, id_(calcAccountID(keyPair_.first))
{
}
} // namespace xrpl::test

View File

@@ -1,81 +0,0 @@
#pragma once
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/KeyType.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/SecretKey.h>
#include <string>
#include <string_view>
#include <utility>
namespace xrpl::test {
/**
* @brief A test account with cryptographic keys.
*
* Generates keys deterministically from a name, making tests reproducible.
* The same name always produces the same AccountID and keys.
*/
class Account
{
public:
/**
* @brief The master account that holds all XRP in genesis.
*
* This account is created in the genesis ledger with all 100 billion XRP.
* It uses the well-known seed "masterpassphrase".
*/
static Account const master;
/**
* @brief Create an account from a name.
*
* Keys are derived deterministically from the name.
*
* @param name Human-readable name for the account.
* @param type Key type to use (defaults to secp256k1).
*/
explicit Account(std::string_view name, KeyType type = KeyType::secp256k1);
/** @brief Return the human-readable name. */
std::string const&
name() const noexcept
{
return name_;
}
/** @brief Return the AccountID. */
AccountID const&
id() const noexcept
{
return id_;
}
/** @brief Return the public key. */
PublicKey const&
pk() const noexcept
{
return keyPair_.first;
}
/** @brief Return the secret key. */
SecretKey const&
sk() const noexcept
{
return keyPair_.second;
}
/** @brief Implicit conversion to AccountID. */
operator AccountID const&() const noexcept
{
return id_;
}
private:
std::string name_;
std::pair<PublicKey, SecretKey> keyPair_;
AccountID id_;
};
} // namespace xrpl::test

View File

@@ -1,132 +0,0 @@
#pragma once
#include <xrpl/basics/Number.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/UintTypes.h>
#include <helpers/Account.h>
#include <concepts>
#include <string>
#include <string_view>
#include <type_traits>
namespace xrpl::test {
/**
* @brief Represents an IOU (issued currency) for testing.
*
* Provides a clear, explicit API for creating currencies issued by an account.
* This replaces the cryptic `Account::operator[]` from the jtx framework.
*
* @code
* Account gw("gateway");
* IOU USD("USD", gw);
*
* auto issue = USD.issue(); // Get the Issue
* auto asset = USD.asset(); // Get the Asset
* auto amt = USD.amount(100); // Get STAmount of 100 USD
* @endcode
*/
class IOU
{
public:
/**
* @brief Construct an IOU from a currency code and issuing account.
* @param currencyCode A 3-character ISO currency code (e.g., "USD").
* @param issuer The account that issues this currency.
*/
IOU(std::string_view currencyCode, Account const& issuer)
: currency_(to_currency(std::string(currencyCode))), issuer_(issuer.id())
{
XRPL_ASSERT(!isXRP(currency_), "IOU: currency code must not resolve to XRP");
}
/**
* @brief Construct an IOU from a Currency and issuing account.
* @param currency The Currency object.
* @param issuer The account that issues this currency.
*/
IOU(Currency currency, Account const& issuer)
: currency_(std::move(currency)), issuer_(issuer.id())
{
XRPL_ASSERT(!isXRP(currency_), "IOU: currency code must not resolve to XRP");
}
/**
* @brief Get the Issue (currency + issuer pair).
* @return An Issue object representing this IOU.
*/
[[nodiscard]] Issue
issue() const
{
return Issue{currency_, issuer_};
}
/**
* @brief Get the Asset.
* @return An Asset object representing this IOU.
*/
[[nodiscard]] Asset
asset() const
{
return Asset{issue()};
}
/**
* @brief Create an STAmount of this IOU.
*
* Works with any arithmetic type (int, double, etc.) by converting
* to string and parsing. This matches the jtx IOU behaviour.
*
* @tparam T An arithmetic type.
* @param value The amount as any arithmetic type.
* @return An STAmount representing value units of this IOU.
*/
template <typename T>
requires std::is_arithmetic_v<T>
[[nodiscard]] STAmount
amount(T value) const
{
return amountFromString(issue(), to_string(value));
}
/**
* @brief Create an STAmount of this IOU from a Number.
* @param value The amount as a Number.
* @return An STAmount representing value units of this IOU.
*/
[[nodiscard]] STAmount
amount(Number const& value) const
{
return STAmount{issue(), value};
}
/**
* @brief Get the currency.
* @return The currency.
*/
[[nodiscard]] Currency const&
currency() const
{
return currency_;
}
/**
* @brief Get the issuer account ID.
* @return The issuer's AccountID.
*/
[[nodiscard]] AccountID const&
issuer() const
{
return issuer_;
}
private:
Currency currency_;
AccountID issuer_;
};
} // namespace xrpl::test

View File

@@ -1,111 +0,0 @@
#pragma once
#include <xrpl/basics/chrono.h>
#include <xrpl/nodestore/DummyScheduler.h>
#include <xrpl/nodestore/Manager.h>
#include <xrpl/shamap/Family.h>
#include <memory>
namespace xrpl {
namespace test {
/** Test implementation of Family for unit tests.
Uses an in-memory NodeStore database and simple caches.
The missingNode methods throw since tests shouldn't encounter missing nodes.
*/
class TestFamily : public Family
{
private:
std::unique_ptr<NodeStore::Database> db_;
TestStopwatch clock_;
std::shared_ptr<FullBelowCache> fbCache_;
std::shared_ptr<TreeNodeCache> tnCache_;
NodeStore::DummyScheduler scheduler_;
beast::Journal j_;
public:
explicit TestFamily(beast::Journal j)
: fbCache_(std::make_shared<FullBelowCache>("TestFamily full below cache", clock_, j))
, tnCache_(
std::make_shared<TreeNodeCache>(
"TestFamily tree node cache",
65536,
std::chrono::minutes{1},
clock_,
j))
, j_(j)
{
Section config;
config.set("type", "memory");
config.set("path", "TestFamily");
db_ = NodeStore::Manager::instance().make_Database(megabytes(4), scheduler_, 1, config, j);
}
NodeStore::Database&
db() override
{
return *db_;
}
NodeStore::Database const&
db() const override
{
return *db_;
}
beast::Journal const&
journal() override
{
return j_;
}
std::shared_ptr<FullBelowCache>
getFullBelowCache() override
{
return fbCache_;
}
std::shared_ptr<TreeNodeCache>
getTreeNodeCache() override
{
return tnCache_;
}
void
sweep() override
{
fbCache_->sweep();
tnCache_->sweep();
}
void
missingNodeAcquireBySeq(std::uint32_t refNum, uint256 const& nodeHash) override
{
Throw<std::runtime_error>("TestFamily: missing node (by seq)");
}
void
missingNodeAcquireByHash(uint256 const& refHash, std::uint32_t refNum) override
{
Throw<std::runtime_error>("TestFamily: missing node (by hash)");
}
void
reset() override
{
fbCache_->reset();
tnCache_->reset();
}
/** Access the test clock for time manipulation in tests. */
TestStopwatch&
clock()
{
return clock_;
}
};
} // namespace test
} // namespace xrpl

View File

@@ -1,378 +0,0 @@
#pragma once
#include <xrpl/basics/Log.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/core/HashRouter.h>
#include <xrpl/core/NetworkIDService.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/PendingSaves.h>
#include <xrpl/server/LoadFeeTrack.h>
#include <boost/asio/io_context.hpp>
#include <helpers/TestFamily.h>
#include <helpers/TestSink.h>
#include <optional>
#include <stdexcept>
namespace xrpl {
namespace test {
/** Logs implementation that creates TestSink instances. */
class TestLogs : public Logs
{
public:
explicit TestLogs(beast::severities::Severity level = beast::severities::kWarning) : Logs(level)
{
}
std::unique_ptr<beast::Journal::Sink>
makeSink(std::string const&, beast::severities::Severity threshold) override
{
return std::make_unique<TestSink>(threshold);
}
};
/** Simple NetworkIDService implementation for tests. */
class TestNetworkIDService final : public NetworkIDService
{
public:
explicit TestNetworkIDService(std::uint32_t networkID = 0) : networkID_(networkID)
{
}
[[nodiscard]] std::uint32_t
getNetworkID() const noexcept override
{
return networkID_;
}
private:
std::uint32_t networkID_;
};
/** Test implementation of ServiceRegistry for unit tests.
This class provides real implementations for services that can be
instantiated from libxrpl (such as Logs, io_context, caches), and
throws std::logic_error for services that require the full Application.
Tests can subclass this to provide additional services they need.
*/
class TestServiceRegistry : public ServiceRegistry
{
TestLogs logs_{beast::severities::kWarning};
boost::asio::io_context io_context_;
TestFamily family_{logs_.journal("TestFamily")};
LoadFeeTrack feeTrack_{logs_.journal("LoadFeeTrack")};
TestNetworkIDService networkIDService_;
HashRouter hashRouter_{HashRouter::Setup{}, stopwatch()};
NodeCache tempNodeCache_{
"TempNodeCache",
16384,
std::chrono::minutes{1},
stopwatch(),
logs_.journal("TaggedCache")};
CachedSLEs cachedSLEs_{
"CachedSLEs",
16384,
std::chrono::minutes{1},
stopwatch(),
logs_.journal("TaggedCache")};
PendingSaves pendingSaves_;
std::optional<uint256> trapTxID_;
public:
TestServiceRegistry() = default;
~TestServiceRegistry() override = default;
// Core infrastructure services
CollectorManager&
getCollectorManager() override
{
throw std::logic_error("TestServiceRegistry::getCollectorManager() not implemented");
}
Family&
getNodeFamily() override
{
return family_;
}
TimeKeeper&
getTimeKeeper() override
{
throw std::logic_error("TestServiceRegistry::timeKeeper() not implemented");
}
JobQueue&
getJobQueue() override
{
throw std::logic_error("TestServiceRegistry::getJobQueue() not implemented");
}
NodeCache&
getTempNodeCache() override
{
return tempNodeCache_;
}
CachedSLEs&
getCachedSLEs() override
{
return cachedSLEs_;
}
NetworkIDService&
getNetworkIDService() override
{
return networkIDService_;
}
// Protocol and validation services
AmendmentTable&
getAmendmentTable() override
{
throw std::logic_error("TestServiceRegistry::getAmendmentTable() not implemented");
}
HashRouter&
getHashRouter() override
{
return hashRouter_;
}
LoadFeeTrack&
getFeeTrack() override
{
return feeTrack_;
}
LoadManager&
getLoadManager() override
{
throw std::logic_error("TestServiceRegistry::getLoadManager() not implemented");
}
RCLValidations&
getValidations() override
{
throw std::logic_error("TestServiceRegistry::getValidations() not implemented");
}
ValidatorList&
getValidators() override
{
throw std::logic_error("TestServiceRegistry::validators() not implemented");
}
ValidatorSite&
getValidatorSites() override
{
throw std::logic_error("TestServiceRegistry::validatorSites() not implemented");
}
ManifestCache&
getValidatorManifests() override
{
throw std::logic_error("TestServiceRegistry::validatorManifests() not implemented");
}
ManifestCache&
getPublisherManifests() override
{
throw std::logic_error("TestServiceRegistry::publisherManifests() not implemented");
}
// Network services
Overlay&
getOverlay() override
{
throw std::logic_error("TestServiceRegistry::overlay() not implemented");
}
Cluster&
getCluster() override
{
throw std::logic_error("TestServiceRegistry::cluster() not implemented");
}
PeerReservationTable&
getPeerReservations() override
{
throw std::logic_error("TestServiceRegistry::peerReservations() not implemented");
}
Resource::Manager&
getResourceManager() override
{
throw std::logic_error("TestServiceRegistry::getResourceManager() not implemented");
}
// Storage services
NodeStore::Database&
getNodeStore() override
{
throw std::logic_error("TestServiceRegistry::getNodeStore() not implemented");
}
SHAMapStore&
getSHAMapStore() override
{
throw std::logic_error("TestServiceRegistry::getSHAMapStore() not implemented");
}
RelationalDatabase&
getRelationalDatabase() override
{
throw std::logic_error("TestServiceRegistry::getRelationalDatabase() not implemented");
}
// Ledger services
InboundLedgers&
getInboundLedgers() override
{
throw std::logic_error("TestServiceRegistry::getInboundLedgers() not implemented");
}
InboundTransactions&
getInboundTransactions() override
{
throw std::logic_error("TestServiceRegistry::getInboundTransactions() not implemented");
}
TaggedCache<uint256, AcceptedLedger>&
getAcceptedLedgerCache() override
{
throw std::logic_error("TestServiceRegistry::getAcceptedLedgerCache() not implemented");
}
LedgerMaster&
getLedgerMaster() override
{
throw std::logic_error("TestServiceRegistry::getLedgerMaster() not implemented");
}
LedgerCleaner&
getLedgerCleaner() override
{
throw std::logic_error("TestServiceRegistry::getLedgerCleaner() not implemented");
}
LedgerReplayer&
getLedgerReplayer() override
{
throw std::logic_error("TestServiceRegistry::getLedgerReplayer() not implemented");
}
PendingSaves&
getPendingSaves() override
{
return pendingSaves_;
}
OpenLedger&
getOpenLedger() override
{
throw std::logic_error("TestServiceRegistry::openLedger() not implemented");
}
OpenLedger const&
getOpenLedger() const override
{
throw std::logic_error("TestServiceRegistry::openLedger() const not implemented");
}
// Transaction and operation services
NetworkOPs&
getOPs() override
{
throw std::logic_error("TestServiceRegistry::getOPs() not implemented");
}
OrderBookDB&
getOrderBookDB() override
{
throw std::logic_error("TestServiceRegistry::getOrderBookDB() not implemented");
}
TransactionMaster&
getMasterTransaction() override
{
throw std::logic_error("TestServiceRegistry::getMasterTransaction() not implemented");
}
TxQ&
getTxQ() override
{
throw std::logic_error("TestServiceRegistry::getTxQ() not implemented");
}
PathRequestManager&
getPathRequestManager() override
{
throw std::logic_error("TestServiceRegistry::getPathRequestManager() not implemented");
}
// Server services
ServerHandler&
getServerHandler() override
{
throw std::logic_error("TestServiceRegistry::getServerHandler() not implemented");
}
perf::PerfLog&
getPerfLog() override
{
throw std::logic_error("TestServiceRegistry::getPerfLog() not implemented");
}
// Configuration and state
bool
isStopping() const override
{
return false;
}
beast::Journal
getJournal(std::string const& name) override
{
return logs_.journal(name);
}
boost::asio::io_context&
getIOContext() override
{
return io_context_;
}
Logs&
getLogs() override
{
return logs_;
}
std::optional<uint256> const&
getTrapTxID() const override
{
return trapTxID_;
}
DatabaseCon&
getWalletDB() override
{
throw std::logic_error("TestServiceRegistry::getWalletDB() not implemented");
}
// Temporary: Get the underlying Application
Application&
getApp() override
{
throw std::logic_error(
"TestServiceRegistry::app() not implemented - no Application available in tests");
}
};
} // namespace test
} // namespace xrpl

View File

@@ -1,252 +0,0 @@
#include <helpers/TxTest.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/basics/contract.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/CanonicalTXSet.h>
#include <xrpl/ledger/Ledger.h>
#include <xrpl/ledger/OpenView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Fees.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol_autogen/ledger_entries/AccountRoot.h>
#include <xrpl/protocol_autogen/ledger_entries/RippleState.h>
#include <xrpl/protocol_autogen/transactions/AccountSet.h>
#include <xrpl/protocol_autogen/transactions/Payment.h>
#include <xrpl/tx/apply.h>
#include <helpers/Account.h>
#include <helpers/IOU.h>
#include <cstdint>
#include <memory>
#include <optional>
#include <stdexcept>
#include <utility>
#include <vector>
namespace xrpl::test {
//------------------------------------------------------------------------------
// Feature helpers
//------------------------------------------------------------------------------
FeatureBitset
allFeatures()
{
static FeatureBitset const features = [] {
auto const& sa = allAmendments();
std::vector<uint256> feats;
feats.reserve(sa.size());
for ([[maybe_unused]] auto const& [name, _] : sa)
{
if (auto const f = getRegisteredFeature(name); f.has_value())
feats.push_back(*f);
}
return FeatureBitset(feats);
}();
return features;
}
//------------------------------------------------------------------------------
// TxTest
//------------------------------------------------------------------------------
TxTest::TxTest(std::optional<FeatureBitset> features)
{
// Convert FeatureBitset to unordered_set for Rules constructor
auto const featureBits = features.value_or(allFeatures());
foreachFeature(featureBits, [&](uint256 const& f) { featureSet_.insert(f); });
// Create rules with the specified features
rules_.emplace(featureSet_);
// Default fees for testing
Fees const fees{XRPAmount{10}, XRPAmount{10000000}, XRPAmount{2000000}};
// Create a genesis ledger as the base
closedLedger_ = std::make_shared<Ledger>(
create_genesis,
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
*rules_,
fees,
std::vector<uint256>{featureSet_.begin(), featureSet_.end()},
registry_.getNodeFamily());
// Initialize time from the genesis ledger
now_ = closedLedger_->header().closeTime;
// Create an open view on top of the genesis ledger
openLedger_ =
std::make_shared<OpenView>(open_ledger, closedLedger_.get(), *rules_, closedLedger_);
}
bool
TxTest::isEnabled(uint256 const& feature) const
{
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
return rules_->enabled(feature);
}
Rules const&
TxTest::getRules() const
{
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
return *rules_;
}
[[nodiscard]] TxResult
TxTest::submit(std::shared_ptr<STTx const> stx)
{
auto result = apply(registry_, *openLedger_, *stx, tapNONE, registry_.getJournal("apply"));
// Track successfully applied transactions for canonical reordering on close
// We make a copy since the TransactionBase doesn't own the STTx
if (result.applied)
pendingTxs_.push_back(stx);
return TxResult{
.ter = result.ter,
.applied = result.applied,
.metadata = std::move(result).metadata,
.tx = std::move(stx)};
}
void
TxTest::createAccount(Account const& account, XRPAmount xrp, uint32_t accountFlags)
{
auto const paymentTer =
submit(transactions::PaymentBuilder{Account::master, account, xrp}, Account::master).ter;
if (paymentTer != tesSUCCESS)
{
throw std::runtime_error("TxTest::createAccount: failed to create account");
}
close();
if (accountFlags != 0)
{
auto const accountSetTer =
submit(transactions::AccountSetBuilder{account}.setSetFlag(accountFlags), account).ter;
if (accountSetTer != tesSUCCESS)
{
throw std::runtime_error("TxTest::createAccount: failed to set account flags");
}
close();
}
}
ledger_entries::AccountRoot
TxTest::getAccountRoot(AccountID const& id) const
{
auto const sle = getOpenLedger().read(keylet::account(id));
if (!sle)
Throw<std::runtime_error>("TxTest::getAccountRoot: account not found");
return ledger_entries::AccountRoot{std::const_pointer_cast<SLE const>(sle)};
}
OpenView&
TxTest::getOpenLedger()
{
return *openLedger_;
}
OpenView const&
TxTest::getOpenLedger() const
{
return *openLedger_;
}
ReadView const&
TxTest::getClosedLedger() const
{
return *closedLedger_;
}
void
TxTest::close()
{
// Build a new closed ledger from the previous closed ledger,
// similar to how buildLedgerImpl works:
// 1. Create a new Ledger from the previous closed ledger
// 2. Re-apply transactions in canonical order
// 3. Mark it as accepted/immutable
auto const& prevLedger = *closedLedger_;
auto const ledgerCloseTime = now_ + prevLedger.header().closeTimeResolution;
now_ = ledgerCloseTime;
auto newLedger = std::make_shared<Ledger>(prevLedger, ledgerCloseTime);
CanonicalTXSet txSet(prevLedger.header().hash);
for (auto const& tx : pendingTxs_)
txSet.insert(tx);
{
OpenView accum(&*newLedger);
for (auto const& [key, tx] : txSet)
{
auto result = apply(registry_, accum, *tx, tapNONE, registry_.getJournal("apply"));
if (!result.applied)
{
throw std::runtime_error("TxTest::close: failed to apply transaction");
}
}
accum.apply(*newLedger);
}
newLedger->setAccepted(ledgerCloseTime, newLedger->header().closeTimeResolution, true);
closedLedger_ = newLedger;
pendingTxs_.clear();
openLedger_ =
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
std::make_shared<OpenView>(open_ledger, closedLedger_.get(), *rules_, closedLedger_);
}
void
TxTest::advanceTime(NetClock::duration duration)
{
now_ += duration;
}
NetClock::time_point
TxTest::getCloseTime() const
{
return now_;
}
STAmount
TxTest::getBalance(AccountID const& account, IOU const& iou) const
{
auto const sle = openLedger_->read(keylet::line(account, iou.issue()));
if (!sle)
return STAmount{iou.issue(), 0};
auto const rippleState = ledger_entries::RippleState{sle};
auto balance = rippleState.getBalance();
if (iou.issue().account == account)
{
throw std::logic_error("TxTest::getBalance: account is issuer");
}
balance.get<Issue>().account = iou.issue().account;
if (account > iou.issue().account)
balance.negate();
return balance;
}
} // namespace xrpl::test

View File

@@ -1,364 +0,0 @@
#pragma once
#include <xrpl/basics/Number.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ApplyViewImpl.h>
#include <xrpl/ledger/Ledger.h>
#include <xrpl/ledger/OpenView.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Rules.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/protocol_autogen/TransactionBuilderBase.h>
#include <xrpl/protocol_autogen/ledger_entries/AccountRoot.h>
#include <xrpl/tx/applySteps.h>
#include <helpers/Account.h>
#include <helpers/IOU.h>
#include <helpers/TestServiceRegistry.h>
#include <cmath>
#include <concepts>
#include <memory>
#include <optional>
#include <stdexcept>
#include <type_traits>
#include <unordered_set>
#include <vector>
namespace xrpl::test {
//------------------------------------------------------------------------------
// Amount helpers
//------------------------------------------------------------------------------
/**
* @brief Convert XRP to drops (integral types).
* @param xrp The amount in XRP.
* @return The equivalent amount in drops as XRPAmount.
*/
template <std::integral T>
constexpr XRPAmount
XRP(T xrp)
{
return XRPAmount{static_cast<std::int64_t>(xrp) * DROPS_PER_XRP.drops()};
}
/**
* @brief Convert XRP to drops (floating point types).
* @param xrp The amount in XRP (may be fractional).
* @return The equivalent amount in drops as XRPAmount.
*/
template <std::floating_point T>
XRPAmount
XRP(T xrp)
{
return XRPAmount{static_cast<std::int64_t>(std::round(xrp * DROPS_PER_XRP.drops()))};
}
/**
* @brief Convert XRP to drops (Number type).
* @param xrp The amount in XRP as a Number.
* @return The equivalent amount in drops as XRPAmount.
*/
inline XRPAmount
XRP(Number const& xrp)
{
return XRPAmount{static_cast<std::int64_t>(xrp * DROPS_PER_XRP.drops())};
}
//------------------------------------------------------------------------------
// Flag helpers
//------------------------------------------------------------------------------
/**
* @brief Convert AccountSet flag (asf) to LedgerState flag (lsf).
* @param asf The AccountSet flag value.
* @return The corresponding LedgerState flag.
* @throws std::runtime_error if the flag is not supported.
*
* Supported flags:
* asfRequireDest, asfRequireAuth, asfDisallowXRP, asfDisableMaster,
* asfNoFreeze, asfGlobalFreeze, asfDefaultRipple, asfDepositAuth,
* asfAllowTrustLineClawback, asfDisallowIncomingCheck,
* asfDisallowIncomingNFTokenOffer, asfDisallowIncomingPayChan,
* asfDisallowIncomingTrustline, asfAllowTrustLineLocking
*/
constexpr std::uint32_t
asfToLsf(std::uint32_t asf)
{
switch (asf)
{
case asfRequireDest:
return lsfRequireDestTag;
case asfRequireAuth:
return lsfRequireAuth;
case asfDisallowXRP:
return lsfDisallowXRP;
case asfDisableMaster:
return lsfDisableMaster;
case asfNoFreeze:
return lsfNoFreeze;
case asfGlobalFreeze:
return lsfGlobalFreeze;
case asfDefaultRipple:
return lsfDefaultRipple;
case asfDepositAuth:
return lsfDepositAuth;
case asfAllowTrustLineClawback:
return lsfAllowTrustLineClawback;
case asfDisallowIncomingCheck:
return lsfDisallowIncomingCheck;
case asfDisallowIncomingNFTokenOffer:
return lsfDisallowIncomingNFTokenOffer;
case asfDisallowIncomingPayChan:
return lsfDisallowIncomingPayChan;
case asfDisallowIncomingTrustline:
return lsfDisallowIncomingTrustline;
case asfAllowTrustLineLocking:
return lsfAllowTrustLineLocking;
default:
throw std::runtime_error("Unknown asf flag");
}
}
//------------------------------------------------------------------------------
// Feature helpers
//------------------------------------------------------------------------------
/**
* @brief Returns all testable amendments.
* @note This is similar to jtx::testable_amendments() but for the TxTest framework.
*/
FeatureBitset
allFeatures();
//------------------------------------------------------------------------------
// TxResult
//------------------------------------------------------------------------------
/**
* @brief Result of a transaction submission in TxTest.
*
* Contains the TER code, whether the transaction was applied,
* optional metadata, and a reference to the submitted transaction.
* Use standard gtest macros (EXPECT_EQ, EXPECT_TRUE, etc.) to verify results.
*/
struct TxResult
{
TER ter; /**< The transaction engine result code. */
bool applied; /**< Whether the transaction was applied to the ledger. */
std::optional<TxMeta> metadata; /**< Transaction metadata, if available. */
std::shared_ptr<STTx const> tx; /**< Pointer to the submitted transaction. */
};
/**
* @brief A lightweight transaction testing harness.
*
* Unlike the JTx framework which requires a full Application and RPC layer,
* TxTest applies transactions directly to an OpenView using the transactor
* pipeline (preflight -> preclaim -> doApply).
*
* This makes it suitable for:
* - Unit testing individual transactors
* - Testing transaction validation logic
* - Fast, focused tests without full server infrastructure
*
* @code
* TxTest env;
* env.submit(paymentTx).expectSuccess();
* env.submit(badTx).expectTer(tecNO_ENTRY);
* @endcode
*/
class TxTest
{
public:
/**
* @brief Construct a TxTest environment.
*
* Creates a genesis ledger and an open view on top of it.
*
* @param features Optional set of features to enable. If not specified,
* uses all testable amendments.
*/
explicit TxTest(std::optional<FeatureBitset> features = std::nullopt);
/**
* @brief Check if a feature is enabled.
* @param feature The feature to check.
* @return True if the feature is enabled.
*/
[[nodiscard]] bool
isEnabled(uint256 const& feature) const;
/**
* @brief Get the current rules.
* @return The current consensus rules.
*/
[[nodiscard]] Rules const&
getRules() const;
/**
* @brief Submit a transaction from a builder.
*
* Convenience overload that accepts transaction builders.
* Automatically sets sequence and fee before submission.
*
* @tparam T A type derived from TransactionBuilderBase.
* @param builder The transaction builder.
* @param signer The account to sign with.
* @return TxResult containing the result code, applied status, and metadata.
*/
template <typename T>
requires std::
derived_from<std::decay_t<T>, transactions::TransactionBuilderBase<std::decay_t<T>>>
[[nodiscard]] TxResult
submit(T&& builder, Account const& signer)
{
auto const& obj = builder.getSTObject();
auto accountId = obj[sfAccount];
// Only set sequence if not using a ticket (ticket sets sequence to 0)
if (!obj.isFieldPresent(sfTicketSequence))
{
builder.setSequence(getAccountRoot(accountId).getSequence());
}
else
{
builder.setSequence(0);
}
builder.setFee(XRPAmount(10));
return submit(builder.build(signer.pk(), signer.sk()).getSTTx());
}
/**
* @brief Submit a transaction to the open ledger.
*
* Applies the transaction through the full transactor pipeline:
* preflight -> preclaim -> doApply -> invariant checks
*
* Invariant checks are automatically run after doApply. If any
* invariant fails, the result will be tecINVARIANT_FAILED.
*
* @param stx The transaction to submit.
* @return TxResult containing the result code, applied status, and metadata.
*/
[[nodiscard]] TxResult
submit(std::shared_ptr<STTx const> stx);
/**
* @brief Create a new account in the ledger.
*
* Sends a Payment from the master account to create and fund the account.
* Closes the ledger after creation. If accountFlags is non-zero, submits
* an AccountSet transaction and closes again.
*
* @param account The account to create.
* @param xrp The initial XRP balance.
* @param accountFlags Optional account flags to set. Defaults to 0
* (no flags).
*/
void
createAccount(Account const& account, XRPAmount xrp, uint32_t accountFlags = 0);
/**
* @brief Get the account root object from the current open ledger.
* @param id The account ID.
* @return The AccountRoot ledger entry.
* @throws std::runtime_error if the account does not exist.
* @todo Once we make keylet strongly typed, we can ditch this method.
*/
[[nodiscard]] ledger_entries::AccountRoot
getAccountRoot(AccountID const& id) const;
/**
* @brief Get the current open ledger view.
* @return A mutable reference to the open ledger.
*/
[[nodiscard]] OpenView&
getOpenLedger();
/**
* @brief Get the current open ledger view (const).
* @return A const reference to the open ledger.
*/
[[nodiscard]] OpenView const&
getOpenLedger() const;
/**
* @brief Get the closed (base) ledger view.
* @return A const reference to the closed ledger.
*/
[[nodiscard]] ReadView const&
getClosedLedger() const;
/**
* @brief Close the current ledger.
*
* Creates a new closed ledger from the current open ledger.
* All pending transactions are re-applied in canonical order.
*/
void
close();
/**
* @brief Advance time without closing the ledger.
*
* Useful for testing time-dependent features like escrow release
* times or offer expirations.
*
* @param duration The amount of time to advance.
*/
void
advanceTime(NetClock::duration duration);
/**
* @brief Get the current ledger close time.
* @return The current close time.
*/
[[nodiscard]] NetClock::time_point
getCloseTime() const;
/**
* @brief Get the balance of an IOU for an account.
*
* Returns the balance from the perspective of the specified account.
* If the trust line doesn't exist, returns zero.
*
* @param account The account to check.
* @param iou The IOU to check the balance for.
* @return The balance as an STAmount.
* @todo Once we make keylet strongly typed, we can ditch this method.
*/
[[nodiscard]] STAmount
getBalance(AccountID const& account, IOU const& iou) const;
/**
* @brief Get the service registry.
* @return A reference to the service registry.
*/
ServiceRegistry&
getServiceRegistry()
{
return registry_;
}
private:
TestServiceRegistry registry_;
std::unordered_set<uint256, beast::uhash<>> featureSet_;
std::optional<Rules> rules_;
std::shared_ptr<Ledger const> closedLedger_;
std::shared_ptr<OpenView> openLedger_;
/** Transactions submitted to the open ledger, for canonical reordering on close. */
std::vector<std::shared_ptr<STTx const>> pendingTxs_;
/** Current time (can be advanced arbitrarily for testing). */
NetClock::time_point now_;
};
} // namespace xrpl::test

View File

@@ -1,804 +0,0 @@
#include <xrpl/protocol_autogen/transactions/AccountSet.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/KeyType.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Quality.h>
#include <xrpl/protocol/Rate.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol_autogen/ledger_entries/AccountRoot.h>
#include <xrpl/protocol_autogen/transactions/Payment.h>
#include <xrpl/protocol_autogen/transactions/SetRegularKey.h>
#include <xrpl/protocol_autogen/transactions/SignerListSet.h>
#include <xrpl/protocol_autogen/transactions/TicketCreate.h>
#include <xrpl/protocol_autogen/transactions/TrustSet.h>
#include <gtest/gtest.h>
#include <helpers/Account.h>
#include <helpers/IOU.h>
#include <helpers/TxTest.h>
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <initializer_list>
#include <limits>
#include <memory>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
namespace xrpl::test {
TEST(AccountSet, NullAccountSet)
{
TxTest env;
Account const alice("alice");
env.createAccount(alice, XRP(10));
auto& view = env.getOpenLedger();
// ask for the ledger entry - account root, to check its flags
auto sle = view.read(keylet::account(alice));
EXPECT_NE(sle, nullptr);
ledger_entries::AccountRoot const accountRoot(sle);
EXPECT_EQ(accountRoot.getFlags(), 0);
}
TEST(AccountSet, MostFlags)
{
Account const alice("alice");
TxTest env;
env.createAccount(alice, XRP(10000));
// Give alice a regular key so she can legally set and clear
// her asfDisableMaster flag.
Account const aliceRegularKey{"aliceRegularKey", KeyType::secp256k1};
env.createAccount(aliceRegularKey, XRP(10000));
env.close();
EXPECT_EQ(
env.submit(transactions::SetRegularKeyBuilder{alice}.setRegularKey(aliceRegularKey), alice)
.ter,
tesSUCCESS);
env.close();
auto testFlags = [&alice, &aliceRegularKey, &env](
std::initializer_list<std::uint32_t> goodFlags) {
std::uint32_t const orig_flags = env.getAccountRoot(alice).getFlags();
for (std::uint32_t flag{1u}; flag < std::numeric_limits<std::uint32_t>::digits; ++flag)
{
if (flag == asfNoFreeze)
{
// The asfNoFreeze flag can't be cleared. It is tested
// elsewhere.
continue;
}
if (flag == asfAuthorizedNFTokenMinter)
{
// The asfAuthorizedNFTokenMinter flag requires the
// presence or absence of the sfNFTokenMinter field in
// the transaction. It is tested elsewhere.
continue;
}
if (flag == asfDisallowIncomingCheck || flag == asfDisallowIncomingPayChan ||
flag == asfDisallowIncomingNFTokenOffer || flag == asfDisallowIncomingTrustline)
{
// These flags are part of the DisallowIncoming amendment
// and are tested elsewhere
continue;
}
if (flag == asfAllowTrustLineClawback)
{
// The asfAllowTrustLineClawback flag can't be cleared. It
// is tested elsewhere.
continue;
}
if (flag == asfAllowTrustLineLocking)
{
// These flags are part of the AllowTokenLocking amendment
// and are tested elsewhere
continue;
}
if (std::ranges::find(goodFlags, flag) != goodFlags.end())
{
// Good flag
EXPECT_FALSE(env.getAccountRoot(alice).isFlag(asfToLsf(flag)));
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setSetFlag(flag), alice).ter,
tesSUCCESS);
env.close();
EXPECT_TRUE(env.getAccountRoot(alice).isFlag(asfToLsf(flag)));
EXPECT_EQ(
env.submit(
transactions::AccountSetBuilder{alice}.setClearFlag(flag),
aliceRegularKey)
.ter,
tesSUCCESS);
env.close();
EXPECT_FALSE(env.getAccountRoot(alice).isFlag(asfToLsf(flag)));
std::uint32_t const now_flags = env.getAccountRoot(alice).getFlags();
EXPECT_EQ(now_flags, orig_flags);
}
else
{
// Bad flag
EXPECT_EQ(env.getAccountRoot(alice).getFlags(), orig_flags);
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setSetFlag(flag), alice).ter,
tesSUCCESS);
env.close();
EXPECT_EQ(env.getAccountRoot(alice).getFlags(), orig_flags);
EXPECT_EQ(
env.submit(
transactions::AccountSetBuilder{alice}.setClearFlag(flag),
aliceRegularKey)
.ter,
tesSUCCESS);
env.close();
EXPECT_EQ(env.getAccountRoot(alice).getFlags(), orig_flags);
}
}
};
testFlags({
asfRequireDest,
asfRequireAuth,
asfDisallowXRP,
asfGlobalFreeze,
asfDisableMaster,
asfDefaultRipple,
asfDepositAuth,
});
}
TEST(AccountSet, SetAndResetAccountTxnID)
{
TxTest env;
Account const alice("alice");
env.createAccount(alice, XRP(10000));
std::uint32_t const orig_flags = env.getAccountRoot(alice).getFlags();
// asfAccountTxnID is special and not actually set as a flag,
// so we check the field presence instead
EXPECT_FALSE(env.getAccountRoot(alice).hasAccountTxnID());
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setSetFlag(asfAccountTxnID), alice).ter,
tesSUCCESS);
env.close();
EXPECT_TRUE(env.getAccountRoot(alice).hasAccountTxnID());
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setClearFlag(asfAccountTxnID), alice).ter,
tesSUCCESS);
env.close();
EXPECT_FALSE(env.getAccountRoot(alice).hasAccountTxnID());
std::uint32_t const now_flags = env.getAccountRoot(alice).getFlags();
EXPECT_EQ(now_flags, orig_flags);
}
TEST(AccountSet, SetNoFreeze)
{
TxTest env;
Account const alice("alice");
Account const eric("eric");
env.createAccount(alice, XRP(10000));
env.close();
// Set eric as alice's regular key (eric doesn't need to be funded)
EXPECT_EQ(
env.submit(transactions::SetRegularKeyBuilder{alice}.setRegularKey(eric), alice).ter,
tesSUCCESS);
env.close();
// Verify alice doesn't have NoFreeze flag
EXPECT_FALSE(env.getAccountRoot(alice).isFlag(lsfNoFreeze));
// Setting NoFreeze with regular key should fail - requires master key
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setSetFlag(asfNoFreeze), eric).ter,
tecNEED_MASTER_KEY);
env.close();
// Setting NoFreeze with master key should succeed
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setSetFlag(asfNoFreeze), alice).ter,
tesSUCCESS);
env.close();
// Verify alice now has NoFreeze flag
EXPECT_TRUE(env.getAccountRoot(alice).isFlag(lsfNoFreeze));
// Try to clear NoFreeze - transaction succeeds but flag remains set
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setClearFlag(asfNoFreeze), alice).ter,
tesSUCCESS);
env.close();
// Verify flag is still set (NoFreeze cannot be cleared once set)
EXPECT_TRUE(env.getAccountRoot(alice).isFlag(lsfNoFreeze));
}
TEST(AccountSet, Domain)
{
TxTest env;
Account const alice("alice");
env.createAccount(alice, XRP(10000));
env.close();
// The Domain field is represented as the hex string of the lowercase
// ASCII of the domain. For example, the domain example.com would be
// represented as "6578616d706c652e636f6d".
//
// To remove the Domain field from an account, send an AccountSet with
// the Domain set to an empty string.
std::string const domain = "example.com";
// Set domain
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setDomain(makeSlice(domain)), alice).ter,
tesSUCCESS);
env.close();
EXPECT_TRUE(env.getAccountRoot(alice).hasDomain());
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
EXPECT_EQ(*env.getAccountRoot(alice).getDomain(), makeSlice(domain));
// Clear domain by setting empty
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setDomain(Slice{}), alice).ter,
tesSUCCESS);
env.close();
EXPECT_FALSE(env.getAccountRoot(alice).hasDomain());
// The upper limit on the length is 256 bytes
// (defined as DOMAIN_BYTES_MAX in SetAccount)
// test the edge cases: 255, 256, 257.
std::size_t const maxLength = 256;
for (std::size_t len = maxLength - 1; len <= maxLength + 1; ++len)
{
std::string const domain2 = std::string(len - domain.length() - 1, 'a') + "." + domain;
EXPECT_EQ(domain2.length(), len);
if (len <= maxLength)
{
EXPECT_EQ(
env.submit(
transactions::AccountSetBuilder{alice}.setDomain(makeSlice(domain2)), alice)
.ter,
tesSUCCESS);
env.close();
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
EXPECT_EQ(*env.getAccountRoot(alice).getDomain(), makeSlice(domain2));
}
else
{
EXPECT_EQ(
env.submit(
transactions::AccountSetBuilder{alice}.setDomain(makeSlice(domain2)), alice)
.ter,
telBAD_DOMAIN);
env.close();
}
}
}
TEST(AccountSet, MessageKey)
{
TxTest env;
Account const alice("alice");
env.createAccount(alice, XRP(10000));
env.close();
// Generate a random ed25519 key pair for the message key
auto const rkp = randomKeyPair(KeyType::ed25519);
// Set the message key
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setMessageKey(rkp.first.slice()), alice)
.ter,
tesSUCCESS);
env.close();
EXPECT_TRUE(env.getAccountRoot(alice).hasMessageKey());
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
EXPECT_EQ(*env.getAccountRoot(alice).getMessageKey(), rkp.first.slice());
// Clear the message key by setting to empty
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setMessageKey(Slice{}), alice).ter,
tesSUCCESS);
env.close();
EXPECT_FALSE(env.getAccountRoot(alice).hasMessageKey());
// Try to set an invalid public key - should fail
using namespace std::string_literals;
EXPECT_EQ(
env.submit(
transactions::AccountSetBuilder{alice}.setMessageKey(
makeSlice("NOT_REALLY_A_PUBKEY"s)),
alice)
.ter,
telBAD_PUBLIC_KEY);
}
TEST(AccountSet, WalletID)
{
TxTest env;
Account const alice("alice");
env.createAccount(alice, XRP(10000));
env.close();
std::string_view const locator =
"9633EC8AF54F16B5286DB1D7B519EF49EEFC050C0C8AC4384F1D88ACD1BFDF05";
uint256 locatorHash{};
EXPECT_TRUE(locatorHash.parseHex(locator));
// Set the wallet locator
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setWalletLocator(locatorHash), alice).ter,
tesSUCCESS);
env.close();
EXPECT_TRUE(env.getAccountRoot(alice).hasWalletLocator());
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
EXPECT_EQ(*env.getAccountRoot(alice).getWalletLocator(), locatorHash);
// Clear the wallet locator by setting to zero
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setWalletLocator(beast::zero), alice).ter,
tesSUCCESS);
env.close();
EXPECT_FALSE(env.getAccountRoot(alice).hasWalletLocator());
}
TEST(AccountSet, EmailHash)
{
TxTest env;
Account const alice("alice");
env.createAccount(alice, XRP(10000));
env.close();
std::string_view const mh = "5F31A79367DC3137FADA860C05742EE6";
uint128 emailHash{};
EXPECT_TRUE(emailHash.parseHex(mh));
// Set the email hash
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setEmailHash(emailHash), alice).ter,
tesSUCCESS);
env.close();
EXPECT_TRUE(env.getAccountRoot(alice).hasEmailHash());
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
EXPECT_EQ(*env.getAccountRoot(alice).getEmailHash(), emailHash);
// Clear the email hash by setting to zero
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setEmailHash(beast::zero), alice).ter,
tesSUCCESS);
env.close();
EXPECT_FALSE(env.getAccountRoot(alice).hasEmailHash());
}
TEST(AccountSet, TransferRate)
{
struct TestCase
{
double set;
TER code;
double get;
};
// Test data: {rate to set, expected TER, expected stored rate}
std::vector<TestCase> const testData = {
{1.0, tesSUCCESS, 1.0},
{1.1, tesSUCCESS, 1.1},
{2.0, tesSUCCESS, 2.0},
{2.1, temBAD_TRANSFER_RATE, 2.0}, // > 2.0 is invalid
{0.0, tesSUCCESS, 1.0}, // 0 clears the rate (default = 1.0)
{2.0, tesSUCCESS, 2.0},
{0.9, temBAD_TRANSFER_RATE, 2.0}, // < 1.0 is invalid
};
TxTest env;
Account const alice("alice");
env.createAccount(alice, XRP(10000));
env.close();
for (auto const& r : testData)
{
auto const rateValue = static_cast<std::uint32_t>(QUALITY_ONE * r.set);
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setTransferRate(rateValue), alice)
.ter,
r.code);
env.close();
// If the field is not present, expect the default value (1.0)
if (!env.getAccountRoot(alice).hasTransferRate())
{
EXPECT_EQ(r.get, 1.0);
}
else
{
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
EXPECT_EQ(
*env.getAccountRoot(alice).getTransferRate(),
static_cast<std::uint32_t>(r.get * QUALITY_ONE));
}
}
}
TEST(AccountSet, BadInputs)
{
TxTest env;
Account const alice("alice");
env.createAccount(alice, XRP(10000));
env.close();
// Setting and clearing the same flag is invalid
EXPECT_EQ(
env.submit(
transactions::AccountSetBuilder{alice}
.setSetFlag(asfDisallowXRP)
.setClearFlag(asfDisallowXRP),
alice)
.ter,
temINVALID_FLAG);
EXPECT_EQ(
env.submit(
transactions::AccountSetBuilder{alice}
.setSetFlag(asfRequireAuth)
.setClearFlag(asfRequireAuth),
alice)
.ter,
temINVALID_FLAG);
EXPECT_EQ(
env.submit(
transactions::AccountSetBuilder{alice}
.setSetFlag(asfRequireDest)
.setClearFlag(asfRequireDest),
alice)
.ter,
temINVALID_FLAG);
// Setting asf flag while also using corresponding tf flag is invalid
EXPECT_EQ(
env.submit(
transactions::AccountSetBuilder{alice}
.setSetFlag(asfDisallowXRP)
.setFlags(tfAllowXRP),
alice)
.ter,
temINVALID_FLAG);
EXPECT_EQ(
env.submit(
transactions::AccountSetBuilder{alice}
.setSetFlag(asfRequireAuth)
.setFlags(tfOptionalAuth),
alice)
.ter,
temINVALID_FLAG);
EXPECT_EQ(
env.submit(
transactions::AccountSetBuilder{alice}
.setSetFlag(asfRequireDest)
.setFlags(tfOptionalDestTag),
alice)
.ter,
temINVALID_FLAG);
// Using invalid flags (mask) is invalid
EXPECT_EQ(
env.submit(
transactions::AccountSetBuilder{alice}
.setSetFlag(asfRequireDest)
.setFlags(tfAccountSetMask),
alice)
.ter,
temINVALID_FLAG);
// Disabling master key without an alternative key is invalid
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setSetFlag(asfDisableMaster), alice).ter,
tecNO_ALTERNATIVE_KEY);
}
TEST(AccountSet, RequireAuthWithDir)
{
TxTest env;
Account const alice("alice");
Account const bob("bob");
env.createAccount(alice, XRP(10000));
env.close();
// alice should have an empty directory
EXPECT_TRUE(dirIsEmpty(env.getClosedLedger(), keylet::ownerDir(alice.id())));
// Give alice a signer list, then there will be stuff in the directory
// Build the SignerEntries array
STArray signerEntries(1);
{
signerEntries.push_back(STObject::makeInnerObject(sfSignerEntry));
STObject& entry = signerEntries.back();
entry[sfAccount] = bob.id();
entry[sfSignerWeight] = std::uint16_t{1};
}
EXPECT_EQ(
env.submit(
transactions::SignerListSetBuilder{alice, 1}.setSignerEntries(signerEntries), alice)
.ter,
tesSUCCESS);
env.close();
EXPECT_FALSE(dirIsEmpty(env.getClosedLedger(), keylet::ownerDir(alice.id())));
// Setting RequireAuth should fail because alice has owner objects
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setSetFlag(asfRequireAuth), alice).ter,
tecOWNERS);
// Remove the signer list (quorum = 0, no entries)
EXPECT_EQ(env.submit(transactions::SignerListSetBuilder{alice, 0}, alice).ter, tesSUCCESS);
env.close();
EXPECT_TRUE(dirIsEmpty(env.getClosedLedger(), keylet::ownerDir(alice.id())));
// Now setting RequireAuth should succeed
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setSetFlag(asfRequireAuth), alice).ter,
tesSUCCESS);
}
TEST(AccountSet, Ticket)
{
TxTest env;
Account const alice("alice");
env.createAccount(alice, XRP(10000));
env.close();
// Get alice's current sequence - the ticket will be created at seq + 1
std::uint32_t const aliceSeqBefore = env.getAccountRoot(alice.id()).getSequence();
std::uint32_t const ticketSeq = aliceSeqBefore + 1;
// Create a ticket
EXPECT_EQ(env.submit(transactions::TicketCreateBuilder{alice, 1}, alice).ter, tesSUCCESS);
env.close();
// Verify alice has 1 owner object (the ticket)
EXPECT_EQ(env.getAccountRoot(alice.id()).getOwnerCount(), 1u);
// Verify ticket exists
EXPECT_TRUE(env.getClosedLedger().exists(keylet::ticket(alice.id(), ticketSeq)));
// Try using a ticket that alice doesn't have
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setTicketSequence(ticketSeq + 1), alice)
.ter,
terPRE_TICKET);
env.close();
// Verify ticket still exists
EXPECT_TRUE(env.getClosedLedger().exists(keylet::ticket(alice.id(), ticketSeq)));
// Get alice's sequence before using the ticket
std::uint32_t const aliceSeq = env.getAccountRoot(alice.id()).getSequence();
// Actually use alice's ticket (noop AccountSet)
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setTicketSequence(ticketSeq), alice).ter,
tesSUCCESS);
env.close();
// Verify ticket is consumed (no owner objects)
EXPECT_EQ(env.getAccountRoot(alice.id()).getOwnerCount(), 0u);
EXPECT_FALSE(env.getClosedLedger().exists(keylet::ticket(alice.id(), ticketSeq)));
// Verify alice's sequence did NOT advance (ticket use doesn't increment seq)
EXPECT_EQ(env.getAccountRoot(alice.id()).getSequence(), aliceSeq);
// Try re-using a ticket that alice already used
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setTicketSequence(ticketSeq), alice).ter,
tefNO_TICKET);
}
TEST(AccountSet, BadSigningKey)
{
TxTest env;
Account const alice("alice");
env.createAccount(alice, XRP(10000));
env.close();
// Build a valid transaction first, then corrupt the signing key
auto stx = transactions::AccountSetBuilder{alice}
.setSequence(env.getAccountRoot(alice.id()).getSequence())
.setFee(XRPAmount{10})
.build(alice.pk(), alice.sk())
.getSTTx();
// Create a copy with a bad signing key
STObject obj = *stx;
obj.setFieldVL(sfSigningPubKey, makeSlice(std::string("badkey")));
auto result = env.submit(std::make_shared<STTx>(std::move(obj)));
EXPECT_EQ(result.ter, temBAD_SIGNATURE);
EXPECT_FALSE(result.applied);
}
TEST(AccountSet, Gateway)
{
Account const alice("alice");
Account const bob("bob");
Account const gw("gateway");
IOU const USD("USD", gw);
// Test gateway with a variety of allowed transfer rates
for (double transferRate = 1.0; transferRate <= 2.0; transferRate += 0.03125)
{
TxTest env;
env.createAccount(gw, XRP(10000), asfDefaultRipple);
env.createAccount(alice, XRP(10000), asfDefaultRipple);
env.createAccount(bob, XRP(10000), asfDefaultRipple);
env.close();
// Set up trust lines: alice and bob trust gw for USD
EXPECT_EQ(
env.submit(transactions::TrustSetBuilder{alice}.setLimitAmount(USD.amount(10)), alice)
.ter,
tesSUCCESS);
EXPECT_EQ(
env.submit(transactions::TrustSetBuilder{bob}.setLimitAmount(USD.amount(10)), bob).ter,
tesSUCCESS);
env.close();
// Set transfer rate on the gateway
EXPECT_EQ(
env.submit(
transactions::AccountSetBuilder{gw}.setTransferRate(
static_cast<std::uint32_t>(transferRate * QUALITY_ONE)),
gw)
.ter,
tesSUCCESS);
env.close();
// Calculate the amount with transfer rate applied
auto const amount = USD.amount(1);
Rate const rate(static_cast<std::uint32_t>(transferRate * QUALITY_ONE));
auto const amountWithRate = multiply(amount, rate);
// Gateway pays alice 10 USD
EXPECT_EQ(
env.submit(transactions::PaymentBuilder{gw, alice, USD.amount(10)}, gw).ter,
tesSUCCESS);
env.close();
// Alice pays bob 1 USD (with sendmax to cover transfer fee)
EXPECT_EQ(
env.submit(
transactions::PaymentBuilder{alice, bob, USD.amount(1)}.setSendMax(
USD.amount(10)),
alice)
.ter,
tesSUCCESS);
env.close();
// Check balances
EXPECT_EQ(env.getBalance(alice.id(), USD), USD.amount(10) - amountWithRate);
EXPECT_EQ(env.getBalance(bob.id(), USD), USD.amount(1));
}
// Test out-of-bounds legacy transfer rates (4.0 and 4.294967295)
// These require direct ledger modification since the transactor blocks them
for (std::uint32_t const transferRate : {4000000000U, 4294967295U})
{
TxTest env;
env.createAccount(gw, XRP(10000), asfDefaultRipple);
env.createAccount(alice, XRP(10000), asfDefaultRipple);
env.createAccount(bob, XRP(10000), asfDefaultRipple);
env.close();
// Set up trust lines
EXPECT_EQ(
env.submit(transactions::TrustSetBuilder{alice}.setLimitAmount(USD.amount(10)), alice)
.ter,
tesSUCCESS);
EXPECT_EQ(
env.submit(transactions::TrustSetBuilder{bob}.setLimitAmount(USD.amount(10)), bob).ter,
tesSUCCESS);
env.close();
// Set an acceptable transfer rate first (we'll hack it later)
EXPECT_EQ(
env.submit(
transactions::AccountSetBuilder{gw}.setTransferRate(
static_cast<std::uint32_t>(2.0 * QUALITY_ONE)),
gw)
.ter,
tesSUCCESS);
env.close();
// Directly modify the ledger to set an out-of-bounds transfer rate
// This bypasses the transactor's validation
auto& view = env.getOpenLedger();
auto slePtr = view.read(keylet::account(gw.id()));
ASSERT_NE(slePtr, nullptr);
auto sleCopy = std::make_shared<SLE>(*slePtr);
(*sleCopy)[sfTransferRate] = transferRate;
view.rawReplace(sleCopy);
// Calculate the amount with the legacy transfer rate
auto const amount = USD.amount(1);
auto const amountWithRate = multiply(amount, Rate(transferRate));
// Gateway pays alice 10 USD
EXPECT_EQ(
env.submit(transactions::PaymentBuilder{gw, alice, USD.amount(10)}, gw).ter,
tesSUCCESS);
// Alice pays bob 1 USD
EXPECT_EQ(
env.submit(
transactions::PaymentBuilder{alice, bob, amount}.setSendMax(USD.amount(10)),
alice)
.ter,
tesSUCCESS);
// Check balances
EXPECT_EQ(env.getBalance(alice.id(), USD), USD.amount(10) - amountWithRate);
EXPECT_EQ(env.getBalance(bob.id(), USD), amount);
}
}
} // namespace xrpl::test

View File

@@ -1,8 +0,0 @@
#include <gtest/gtest.h>
int
main(int argc, char** argv)
{
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

View File

@@ -159,8 +159,10 @@ RCLConsensus::Adaptor::acquireLedger(LedgerHash const& hash)
// Tell the ledger acquire system that we need the consensus ledger
acquiringLedger_ = hash;
app_.getInboundLedgers().acquireAsync(
jtADVANCE, "GetConsL1", hash, 0, InboundLedger::Reason::CONSENSUS);
app_.getJobQueue().addJob(jtADVANCE, "GetConsL1", [id = hash, &app = app_, this]() {
JLOG(j_.debug()) << "JOB advanceLedger getConsensusLedger1 started";
app.getInboundLedgers().acquireAsync(id, 0, InboundLedger::Reason::CONSENSUS);
});
}
return std::nullopt;
}

View File

@@ -128,8 +128,12 @@ RCLValidationsAdaptor::acquire(LedgerHash const& hash)
{
JLOG(j_.warn()) << "Need validated ledger for preferred ledger analysis " << hash;
app_.getInboundLedgers().acquireAsync(
jtADVANCE, "GetConsL2", hash, 0, InboundLedger::Reason::CONSENSUS);
Application* pApp = &app_;
app_.getJobQueue().addJob(jtADVANCE, "GetConsL2", [pApp, hash, this]() {
JLOG(j_.debug()) << "JOB advanceLedger getConsensusLedger2 started";
pApp->getInboundLedgers().acquireAsync(hash, 0, InboundLedger::Reason::CONSENSUS);
});
return std::nullopt;
}

View File

@@ -172,4 +172,22 @@ private:
std::unique_ptr<PeerSet> mPeerSet;
};
inline std::string
to_string(InboundLedger::Reason reason)
{
using enum InboundLedger::Reason;
switch (reason)
{
case HISTORY:
return "HISTORY";
case GENERIC:
return "GENERIC";
case CONSENSUS:
return "CONSENSUS";
default:
UNREACHABLE("ripple::to_string(InboundLedger::Reason) : unknown value");
return "unknown";
}
}
} // namespace xrpl

View File

@@ -26,12 +26,7 @@ public:
// Queue. TODO review whether all callers of acquire() can use this
// instead. Inbound ledger acquisition is asynchronous anyway.
virtual void
acquireAsync(
JobType type,
std::string const& name,
uint256 const& hash,
std::uint32_t seq,
InboundLedger::Reason reason) = 0;
acquireAsync(uint256 const& hash, std::uint32_t seq, InboundLedger::Reason reason) = 0;
virtual std::shared_ptr<InboundLedger>
find(LedgerHash const& hash) = 0;

View File

@@ -79,12 +79,78 @@ public:
XRPL_ASSERT(
hash.isNonZero(), "xrpl::InboundLedgersImp::acquire::doAcquire : nonzero hash");
// probably not the right rule
if (app_.getOPs().isNeedNetworkLedger() && (reason != InboundLedger::Reason::GENERIC) &&
(reason != InboundLedger::Reason::CONSENSUS))
return {};
bool const needNetworkLedger = app_.getOPs().isNeedNetworkLedger();
bool const shouldAcquire = [&]() {
if (!needNetworkLedger)
return true;
if (reason == InboundLedger::Reason::GENERIC)
return true;
if (reason == InboundLedger::Reason::CONSENSUS)
return true;
return false;
}();
std::stringstream ss;
ss << "InboundLedger::acquire: "
<< "Request: " << to_string(hash) << ", " << seq
<< " NeedNetworkLedger: " << (needNetworkLedger ? "yes" : "no")
<< " Reason: " << to_string(reason)
<< " Should acquire: " << (shouldAcquire ? "true." : "false.");
/* Acquiring ledgers is somewhat expensive. It requires lots of
* computation and network communication. Avoid it when it's not
* appropriate. Every validation from a peer for a ledger that
* we do not have locally results in a call to this function: even
* if we are moments away from validating the same ledger.
*/
bool const shouldBroadcast = [&]() {
// If the node is not in "full" state, it needs to sync to
// the network, and doesn't have the necessary tx's and
// ledger entries to build the ledger.
bool const isFull = app_.getOPs().isFull();
// If everything else is ok, don't try to acquire the ledger
// if the requested seq is in the near future relative to
// the validated ledger. If the requested ledger is between
// 1 and 19 inclusive ledgers ahead of the valid ledger this
// node has not built it yet, but it's possible/likely it
// has the tx's necessary to build it and get caught up.
// Plus it might not become validated. On the other hand, if
// it's more than 20 in the future, this node should request
// it so that it can jump ahead and get caught up.
LedgerIndex const validSeq = app_.getLedgerMaster().getValidLedgerIndex();
constexpr std::size_t lagLeeway = 20;
bool const nearFuture = (seq > validSeq) && (seq < validSeq + lagLeeway);
// If everything else is ok, don't try to acquire the ledger
// if the request is related to consensus. (Note that
// consensus calls usually pass a seq of 0, so nearFuture
// will be false other than on a brand new network.)
bool const consensus = reason == InboundLedger::Reason::CONSENSUS;
ss << " Evaluating whether to broadcast requests to peers"
<< ". full: " << (isFull ? "true" : "false") << ". ledger sequence " << seq
<< ". Valid sequence: " << validSeq << ". Lag leeway: " << lagLeeway
<< ". request for near future ledger: " << (nearFuture ? "true" : "false")
<< ". Consensus: " << (consensus ? "true" : "false");
// If the node is not synced, send requests.
if (!isFull)
return true;
// If the ledger is in the near future, do NOT send requests.
// This node is probably about to build it.
if (nearFuture)
return false;
// If the request is because of consensus, do NOT send requests.
// This node is probably about to build it.
if (consensus)
return false;
return true;
}();
ss << ". Would broadcast to peers? " << (shouldBroadcast ? "true." : "false.");
if (!shouldAcquire)
{
JLOG(j_.debug()) << "Abort(rule): " << ss.str();
return {};
}
bool isNew = true;
std::shared_ptr<InboundLedger> inbound;
@@ -136,35 +202,25 @@ public:
}
void
acquireAsync(
JobType type,
std::string const& name,
uint256 const& hash,
std::uint32_t seq,
InboundLedger::Reason reason) override
acquireAsync(uint256 const& hash, std::uint32_t seq, InboundLedger::Reason reason) override
{
if (auto check = std::make_shared<CanProcess const>(acquiresMutex_, pendingAcquires_, hash);
*check)
if (CanProcess const check{acquiresMutex_, pendingAcquires_, hash})
{
app_.getJobQueue().addJob(type, name, [check, name, hash, seq, reason, this]() {
JLOG(j_.debug()) << "JOB acquireAsync " << name << " started ";
try
{
acquire(hash, seq, reason);
}
catch (std::exception const& e)
{
JLOG(j_.warn()) << "Exception thrown for acquiring new "
"inbound ledger "
<< hash << ": " << e.what();
}
catch (...)
{
JLOG(j_.warn()) << "Unknown exception thrown for acquiring new "
"inbound ledger "
<< hash;
}
});
try
{
acquire(hash, seq, reason);
}
catch (std::exception const& e)
{
JLOG(j_.warn()) << "Exception thrown for acquiring new inbound ledger " << hash
<< ": " << e.what();
}
catch (...)
{
JLOG(j_.warn()) << "Unknown exception thrown for acquiring new "
"inbound ledger "
<< hash;
}
}
}

View File

@@ -12,10 +12,10 @@ BasicApp::BasicApp(std::size_t numberOfThreads)
work_.emplace(boost::asio::make_work_guard(io_context_));
threads_.reserve(numberOfThreads);
for (std::size_t i = 0; i < numberOfThreads; ++i)
while ((numberOfThreads--) != 0u)
{
threads_.emplace_back([this, i]() {
beast::setCurrentThreadName("io svc #" + std::to_string(i));
threads_.emplace_back([this, numberOfThreads]() {
beast::setCurrentThreadName("io svc #" + std::to_string(numberOfThreads));
this->io_context_.run();
});
}

View File

@@ -17,6 +17,7 @@ enum class ProtocolFeature {
ValidatorListPropagation,
ValidatorList2Propagation,
LedgerReplay,
LedgerDataCookies
};
/** Represents a peer connection in the overlay. */
@@ -116,6 +117,13 @@ public:
[[nodiscard]] virtual bool
txReduceRelayEnabled() const = 0;
//
// Messages
//
virtual std::set<std::optional<uint64_t>>
releaseRequestCookies(uint256 const& requestHash) = 0;
};
} // namespace xrpl

View File

@@ -113,6 +113,8 @@ std::chrono::seconds constexpr peerTimerInterval{60};
/** The timeout for a shutdown timer */
std::chrono::seconds constexpr shutdownTimerInterval{5};
/** How often we process duplicate incoming TMGetLedger messages */
std::chrono::seconds constexpr getledgerInterval{15};
} // namespace
// TODO: Remove this exclusion once unit tests are added after the hotfix
@@ -567,6 +569,8 @@ PeerImp::supportsFeature(ProtocolFeature f) const
return protocol_ >= make_protocol(2, 2);
case ProtocolFeature::LedgerReplay:
return ledgerReplayEnabled_;
case ProtocolFeature::LedgerDataCookies:
return protocol_ >= make_protocol(2, 3);
}
return false;
}
@@ -1548,8 +1552,9 @@ PeerImp::handleTransaction(
void
PeerImp::onMessage(std::shared_ptr<protocol::TMGetLedger> const& m)
{
auto badData = [&](std::string const& msg) {
fee_.update(Resource::feeInvalidData, "get_ledger " + msg);
auto badData = [&](std::string const& msg, bool chargefee = true) {
if (chargefee)
fee_.update(Resource::feeInvalidData, "get_ledger " + msg);
JLOG(p_journal_.warn()) << "TMGetLedger: " << msg;
};
auto const itype{m->itype()};
@@ -1647,11 +1652,68 @@ PeerImp::onMessage(std::shared_ptr<protocol::TMGetLedger> const& m)
}
}
// Drop duplicate requests from the same peer for at least
// `getLedgerInterval` seconds.
// Append a little junk to prevent the hash of an incoming messsage
// from matching the hash of the same outgoing message.
// `shouldProcessForPeer` does not distingish between incoming and
// outgoing, and some of the message relay logic checks the hash to see
// if the message has been relayed already. If the hashes are the same,
// a duplicate will be detected when sending the message is attempted,
// so it will fail.
auto const messageHash = sha512Half(*m, nullptr);
// Request cookies are not included in the hash. Track them here.
auto const requestCookie = [&m]() -> std::optional<uint64_t> {
if (m->has_requestcookie())
return m->requestcookie();
return std::nullopt;
}();
auto const [inserted, pending] = [&] {
std::lock_guard lock{cookieLock_};
auto& cookies = messageRequestCookies_[messageHash];
bool const pending = !cookies.empty();
return std::pair{cookies.emplace(requestCookie).second, pending};
}();
// Check if the request has been seen from this peer.
if (!app_.getHashRouter().shouldProcessForPeer(messageHash, id_, getledgerInterval))
{
// This request has already been seen from this peer.
// Has it been seen with this request cookie (or lack thereof)?
if (inserted)
{
// This is a duplicate request, but with a new cookie. When a
// response is ready, one will be sent for each request cookie.
JLOG(p_journal_.debug())
<< "TMGetLedger: duplicate request with new request cookie: "
<< requestCookie.value_or(0) << ". Job pending: " << (pending ? "yes" : "no")
<< ": " << messageHash;
if (pending)
{
// Don't bother queueing up a new job if other requests are
// already pending. This should limit entries in the job queue
// to one per peer per unique request.
JLOG(p_journal_.debug()) << "TMGetLedger: Suppressing recvGetLedger job, since one "
"is pending: "
<< messageHash;
return;
}
}
else
{
// Don't punish nodes that don't know any better
return badData(
"duplicate request: " + to_string(messageHash),
supportsFeature(ProtocolFeature::LedgerDataCookies));
}
}
// Queue a job to process the request
JLOG(p_journal_.debug()) << "TMGetLedger: Adding recvGetLedger job: " << messageHash;
std::weak_ptr<PeerImp> const weak = shared_from_this();
app_.getJobQueue().addJob(jtLEDGER_REQ, "RcvGetLedger", [weak, m]() {
app_.getJobQueue().addJob(jtLEDGER_REQ, "RcvGetLedger", [weak, m, messageHash]() {
if (auto peer = weak.lock())
peer->processLedgerRequest(m);
peer->processLedgerRequest(m, messageHash);
});
}
@@ -1758,8 +1820,9 @@ PeerImp::onMessage(std::shared_ptr<protocol::TMReplayDeltaResponse> const& m)
void
PeerImp::onMessage(std::shared_ptr<protocol::TMLedgerData> const& m)
{
auto badData = [&](std::string const& msg) {
fee_.update(Resource::feeInvalidData, msg);
auto badData = [&](std::string const& msg, bool charge = true) {
if (charge)
fee_.update(Resource::feeInvalidData, msg);
JLOG(p_journal_.warn()) << "TMLedgerData: " << msg;
};
@@ -1816,23 +1879,96 @@ PeerImp::onMessage(std::shared_ptr<protocol::TMLedgerData> const& m)
return;
}
// If there is a request cookie, attempt to relay the message
if (m->has_requestcookie())
auto const messageHash = sha512Half(*m);
if (!app_.getHashRouter().addSuppressionPeer(messageHash, id_))
{
if (auto peer = overlay_.findPeerByShortID(m->requestcookie()))
// Don't punish nodes that don't know any better
return badData(
"Duplicate message: " + to_string(messageHash),
supportsFeature(ProtocolFeature::LedgerDataCookies));
}
bool const routed =
m->has_directresponse() || m->responsecookies_size() || m->has_requestcookie();
{
// Check if this message needs to be forwarded to one or more peers.
// Maximum of one of the relevant fields should be populated.
XRPL_ASSERT(
!m->has_requestcookie() || !m->responsecookies_size(),
"ripple::PeerImp::onMessage(TMLedgerData) : valid cookie fields");
// Make a copy of the response cookies, then wipe the list so it can be
// forwarded cleanly
auto const responseCookies = m->responsecookies();
m->clear_responsecookies();
// Flag indicating if this response should be processed locally,
// possibly in addition to being forwarded.
bool const directResponse = m->has_directresponse() && m->directresponse();
m->clear_directresponse();
auto const relay = [this, m, &messageHash](auto const cookie) {
if (auto peer = overlay_.findPeerByShortID(cookie))
{
XRPL_ASSERT(
!m->has_requestcookie() && !m->responsecookies_size(),
"ripple::PeerImp::onMessage(TMLedgerData) relay : no "
"cookies");
if (peer->supportsFeature(ProtocolFeature::LedgerDataCookies))
// Setting this flag is not _strictly_ necessary for peers
// that support it if there are no cookies included in the
// message, but it is more accurate.
m->set_directresponse(true);
else
m->clear_directresponse();
peer->send(std::make_shared<Message>(*m, protocol::mtLEDGER_DATA));
}
else
JLOG(p_journal_.info()) << "Unable to route TX/ledger data reply to peer ["
<< cookie << "]: " << messageHash;
};
// If there is a request cookie, attempt to relay the message
if (m->has_requestcookie())
{
XRPL_ASSERT(
responseCookies.empty(),
"ripple::PeerImp::onMessage(TMLedgerData) : no response "
"cookies");
m->clear_requestcookie();
peer->send(std::make_shared<Message>(*m, protocol::mtLEDGER_DATA));
relay(m->requestcookie());
if (!directResponse && responseCookies.empty())
return;
}
else
// If there's a list of request cookies, attempt to relay the message to
// all of them.
if (responseCookies.size())
{
JLOG(p_journal_.info()) << "Unable to route TX/ledger data reply";
for (auto const cookie : responseCookies)
relay(cookie);
if (!directResponse)
return;
}
}
// Now that any forwarding is done check the base message (data only, no
// routing info for duplicates)
if (routed)
{
m->clear_directresponse();
XRPL_ASSERT(
!m->has_requestcookie() && !m->responsecookies_size(),
"ripple::PeerImp::onMessage(TMLedgerData) : no cookies");
auto const baseMessageHash = sha512Half(*m);
if (!app_.getHashRouter().addSuppressionPeer(baseMessageHash, id_))
{
// Don't punish nodes that don't know any better
return badData(
"Duplicate message: " + to_string(baseMessageHash),
supportsFeature(ProtocolFeature::LedgerDataCookies));
}
return;
}
uint256 const ledgerHash{m->ledgerhash()};
// Otherwise check if received data for a candidate transaction set
if (m->type() == protocol::liTS_CANDIDATE)
{
@@ -3159,16 +3295,21 @@ PeerImp::checkValidation(
// the TX tree with the specified root hash.
//
static std::shared_ptr<PeerImp>
getPeerWithTree(OverlayImpl& ov, uint256 const& rootHash, PeerImp const* skip)
getPeerWithTree(
OverlayImpl& ov,
uint256 const& rootHash,
PeerImp const* skip,
std::function<bool(Peer::id_t)> shouldProcessCallback)
{
std::shared_ptr<PeerImp> ret;
int retScore = 0;
XRPL_ASSERT(shouldProcessCallback, "ripple::getPeerWithTree : callback provided");
ov.for_each([&](std::shared_ptr<PeerImp>&& p) {
if (p->hasTxSet(rootHash) && p.get() != skip)
{
auto score = p->getScore(true);
if (!ret || (score > retScore))
if (!ret || (score > retScore && shouldProcessCallback(p->id())))
{
ret = std::move(p);
retScore = score;
@@ -3187,16 +3328,18 @@ getPeerWithLedger(
OverlayImpl& ov,
uint256 const& ledgerHash,
LedgerIndex ledger,
PeerImp const* skip)
PeerImp const* skip,
std::function<bool(Peer::id_t)> shouldProcessCallback)
{
std::shared_ptr<PeerImp> ret;
int retScore = 0;
XRPL_ASSERT(shouldProcessCallback, "ripple::getPeerWithLedger : callback provided");
ov.for_each([&](std::shared_ptr<PeerImp>&& p) {
if (p->hasLedger(ledgerHash, ledger) && p.get() != skip)
{
auto score = p->getScore(true);
if (!ret || (score > retScore))
if (!ret || (score > retScore && shouldProcessCallback(p->id())))
{
ret = std::move(p);
retScore = score;
@@ -3210,7 +3353,8 @@ getPeerWithLedger(
void
PeerImp::sendLedgerBase(
std::shared_ptr<Ledger const> const& ledger,
protocol::TMLedgerData& ledgerData)
protocol::TMLedgerData& ledgerData,
PeerCookieMap const& destinations)
{
JLOG(p_journal_.trace()) << "sendLedgerBase: Base data";
@@ -3240,14 +3384,92 @@ PeerImp::sendLedgerBase(
}
}
auto message{std::make_shared<Message>(ledgerData, protocol::mtLEDGER_DATA)};
send(message);
sendToMultiple(ledgerData, destinations);
}
void
PeerImp::sendToMultiple(protocol::TMLedgerData& ledgerData, PeerCookieMap const& destinations)
{
bool foundSelf = false;
for (auto const& [peer, cookies] : destinations)
{
if (peer.get() == this)
foundSelf = true;
bool const multipleCookies = peer->supportsFeature(ProtocolFeature::LedgerDataCookies);
std::vector<std::uint64_t> sendCookies;
bool directResponse = false;
if (!multipleCookies)
{
JLOG(p_journal_.debug()) << "sendToMultiple: Sending " << cookies.size()
<< " TMLedgerData messages to peer [" << peer->id()
<< "]: " << sha512Half(ledgerData);
}
for (auto const& cookie : cookies)
{
// Unfortunately, need a separate Message object for every
// combination
if (cookie)
{
if (multipleCookies)
{
// Save this one for later to send a single message
sendCookies.emplace_back(*cookie);
continue;
}
// Feature not supported, so send a single message with a
// single cookie
ledgerData.set_requestcookie(*cookie);
}
else
{
if (multipleCookies)
{
// Set this flag later on the single message
directResponse = true;
continue;
}
ledgerData.clear_requestcookie();
}
XRPL_ASSERT(
!multipleCookies,
"ripple::PeerImp::sendToMultiple : ledger data cookies "
"unsupported");
auto message{std::make_shared<Message>(ledgerData, protocol::mtLEDGER_DATA)};
peer->send(message);
}
if (multipleCookies)
{
// Send a single message with all the cookies and/or the direct
// response flag, so the receiver can farm out the single message to
// multiple peers and/or itself
XRPL_ASSERT(
sendCookies.size() || directResponse,
"ripple::PeerImp::sendToMultiple : valid response options");
ledgerData.clear_requestcookie();
ledgerData.clear_responsecookies();
ledgerData.set_directresponse(directResponse);
for (auto const& cookie : sendCookies)
ledgerData.add_responsecookies(cookie);
auto message{std::make_shared<Message>(ledgerData, protocol::mtLEDGER_DATA)};
peer->send(message);
JLOG(p_journal_.debug())
<< "sendToMultiple: Sent 1 TMLedgerData message to peer [" << peer->id()
<< "]: including " << (directResponse ? "the direct response flag and " : "")
<< sendCookies.size() << " response cookies. "
<< ": " << sha512Half(ledgerData);
}
}
XRPL_ASSERT(foundSelf, "ripple::PeerImp::sendToMultiple : current peer included");
}
std::shared_ptr<Ledger const>
PeerImp::getLedger(std::shared_ptr<protocol::TMGetLedger> const& m)
PeerImp::getLedger(std::shared_ptr<protocol::TMGetLedger> const& m, uint256 const& mHash)
{
JLOG(p_journal_.trace()) << "getLedger: Ledger";
JLOG(p_journal_.trace()) << "getLedger: Ledger " << mHash;
std::shared_ptr<Ledger const> ledger;
@@ -3263,16 +3485,30 @@ PeerImp::getLedger(std::shared_ptr<protocol::TMGetLedger> const& m)
if (m->has_querytype() && !m->has_requestcookie())
{
// Attempt to relay the request to a peer
// Note repeated messages will not relay to the same peer
// before `getLedgerInterval` seconds. This prevents one
// peer from getting flooded, and distributes the request
// load. If a request has been relayed to all eligible
// peers, then this message will not be relayed.
if (auto const peer = getPeerWithLedger(
overlay_, ledgerHash, m->has_ledgerseq() ? m->ledgerseq() : 0, this))
overlay_,
ledgerHash,
m->has_ledgerseq() ? m->ledgerseq() : 0,
this,
[&](Peer::id_t id) {
return app_.getHashRouter().shouldProcessForPeer(
mHash, id, getledgerInterval);
}))
{
m->set_requestcookie(id());
peer->send(std::make_shared<Message>(*m, protocol::mtGET_LEDGER));
JLOG(p_journal_.debug()) << "getLedger: Request relayed to peer";
JLOG(p_journal_.debug())
<< "getLedger: Request relayed to peer [" << peer->id() << "]: " << mHash;
return ledger;
}
JLOG(p_journal_.trace()) << "getLedger: Failed to find peer to relay request";
JLOG(p_journal_.trace())
<< "getLedger: Don't have ledger with hash " << ledgerHash << ": " << mHash;
}
}
}
@@ -3281,15 +3517,15 @@ PeerImp::getLedger(std::shared_ptr<protocol::TMGetLedger> const& m)
// Attempt to find ledger by sequence
if (m->ledgerseq() < app_.getLedgerMaster().getEarliestFetch())
{
JLOG(p_journal_.debug()) << "getLedger: Early ledger sequence request";
JLOG(p_journal_.debug()) << "getLedger: Early ledger sequence request " << mHash;
}
else
{
ledger = app_.getLedgerMaster().getLedgerBySeq(m->ledgerseq());
if (!ledger)
{
JLOG(p_journal_.debug())
<< "getLedger: Don't have ledger with sequence " << m->ledgerseq();
JLOG(p_journal_.debug()) << "getLedger: Don't have ledger with sequence "
<< m->ledgerseq() << ": " << mHash;
}
}
}
@@ -3311,27 +3547,29 @@ PeerImp::getLedger(std::shared_ptr<protocol::TMGetLedger> const& m)
charge(Resource::feeMalformedRequest, "get_ledger ledgerSeq");
ledger.reset();
JLOG(p_journal_.warn()) << "getLedger: Invalid ledger sequence " << ledgerSeq;
JLOG(p_journal_.warn())
<< "getLedger: Invalid ledger sequence " << ledgerSeq << ": " << mHash;
}
}
else if (ledgerSeq < app_.getLedgerMaster().getEarliestFetch())
{
ledger.reset();
JLOG(p_journal_.debug()) << "getLedger: Early ledger sequence request " << ledgerSeq;
JLOG(p_journal_.debug())
<< "getLedger: Early ledger sequence request " << ledgerSeq << ": " << mHash;
}
}
else
{
JLOG(p_journal_.debug()) << "getLedger: Unable to find ledger";
JLOG(p_journal_.debug()) << "getLedger: Unable to find ledger " << mHash;
}
return ledger;
}
std::shared_ptr<SHAMap const>
PeerImp::getTxSet(std::shared_ptr<protocol::TMGetLedger> const& m) const
PeerImp::getTxSet(std::shared_ptr<protocol::TMGetLedger> const& m, uint256 const& mHash) const
{
JLOG(p_journal_.trace()) << "getTxSet: TX set";
JLOG(p_journal_.trace()) << "getTxSet: TX set " << mHash;
uint256 const txSetHash{m->ledgerhash()};
std::shared_ptr<SHAMap> shaMap{app_.getInboundTransactions().getSet(txSetHash, false)};
@@ -3340,20 +3578,28 @@ PeerImp::getTxSet(std::shared_ptr<protocol::TMGetLedger> const& m) const
if (m->has_querytype() && !m->has_requestcookie())
{
// Attempt to relay the request to a peer
if (auto const peer = getPeerWithTree(overlay_, txSetHash, this))
// Note repeated messages will not relay to the same peer
// before `getLedgerInterval` seconds. This prevents one
// peer from getting flooded, and distributes the request
// load. If a request has been relayed to all eligible
// peers, then this message will not be relayed.
if (auto const peer = getPeerWithTree(overlay_, txSetHash, this, [&](Peer::id_t id) {
return app_.getHashRouter().shouldProcessForPeer(mHash, id, getledgerInterval);
}))
{
m->set_requestcookie(id());
peer->send(std::make_shared<Message>(*m, protocol::mtGET_LEDGER));
JLOG(p_journal_.debug()) << "getTxSet: Request relayed";
JLOG(p_journal_.debug())
<< "getTxSet: Request relayed to peer [" << peer->id() << "]: " << mHash;
}
else
{
JLOG(p_journal_.debug()) << "getTxSet: Failed to find relay peer";
JLOG(p_journal_.debug()) << "getTxSet: Failed to find relay peer: " << mHash;
}
}
else
{
JLOG(p_journal_.debug()) << "getTxSet: Failed to find TX set";
JLOG(p_journal_.debug()) << "getTxSet: Failed to find TX set " << mHash;
}
}
@@ -3361,7 +3607,7 @@ PeerImp::getTxSet(std::shared_ptr<protocol::TMGetLedger> const& m) const
}
void
PeerImp::processLedgerRequest(std::shared_ptr<protocol::TMGetLedger> const& m)
PeerImp::processLedgerRequest(std::shared_ptr<protocol::TMGetLedger> const& m, uint256 const& mHash)
{
// Do not resource charge a peer responding to a relay
if (!m->has_requestcookie())
@@ -3374,9 +3620,72 @@ PeerImp::processLedgerRequest(std::shared_ptr<protocol::TMGetLedger> const& m)
bool fatLeaves{true};
auto const itype{m->itype()};
auto getDestinations = [&] {
// If a ledger data message is generated, it's going to be sent to every
// peer that is waiting for it.
PeerCookieMap result;
std::size_t numCookies = 0;
{
// Don't do the work under this peer if this peer is not waiting for
// any replies
auto myCookies = releaseRequestCookies(mHash);
if (myCookies.empty())
{
JLOG(p_journal_.debug()) << "TMGetLedger: peer is no longer "
"waiting for response to request: "
<< mHash;
return result;
}
numCookies += myCookies.size();
result[shared_from_this()] = myCookies;
}
std::set<HashRouter::PeerShortID> const peers = app_.getHashRouter().getPeers(mHash);
for (auto const peerID : peers)
{
// This loop does not need to be done under the HashRouter
// lock because findPeerByShortID and releaseRequestCookies
// are thread safe, and everything else is local
if (auto p = overlay_.findPeerByShortID(peerID))
{
auto cookies = p->releaseRequestCookies(mHash);
numCookies += cookies.size();
if (result.contains(p))
{
// Unlikely, but if a request came in to this peer while
// iterating, add the items instead of copying /
// overwriting.
XRPL_ASSERT(
p.get() == this,
"ripple::PeerImp::processLedgerRequest : found self in "
"map");
for (auto const& cookie : cookies)
result[p].emplace(cookie);
}
else if (cookies.size())
result[p] = cookies;
}
}
JLOG(p_journal_.debug()) << "TMGetLedger: Processing request for " << result.size()
<< " peers. Will send " << numCookies
<< " messages if successful: " << mHash;
return result;
};
// Will only populate this if we're going to do work.
PeerCookieMap destinations;
if (itype == protocol::liTS_CANDIDATE)
{
if (sharedMap = getTxSet(m); !sharedMap)
destinations = getDestinations();
if (destinations.empty())
// Nowhere to send the response!
return;
if (sharedMap = getTxSet(m, mHash); !sharedMap)
return;
map = sharedMap.get();
@@ -3384,8 +3693,6 @@ PeerImp::processLedgerRequest(std::shared_ptr<protocol::TMGetLedger> const& m)
ledgerData.set_ledgerseq(0);
ledgerData.set_ledgerhash(m->ledgerhash());
ledgerData.set_type(protocol::liTS_CANDIDATE);
if (m->has_requestcookie())
ledgerData.set_requestcookie(m->requestcookie());
// We'll already have most transactions
fatLeaves = false;
@@ -3403,7 +3710,12 @@ PeerImp::processLedgerRequest(std::shared_ptr<protocol::TMGetLedger> const& m)
return;
}
if (ledger = getLedger(m); !ledger)
destinations = getDestinations();
if (destinations.empty())
// Nowhere to send the response!
return;
if (ledger = getLedger(m, mHash); !ledger)
return;
// Fill out the reply
@@ -3411,13 +3723,11 @@ PeerImp::processLedgerRequest(std::shared_ptr<protocol::TMGetLedger> const& m)
ledgerData.set_ledgerhash(ledgerHash.begin(), ledgerHash.size());
ledgerData.set_ledgerseq(ledger->header().seq);
ledgerData.set_type(itype);
if (m->has_requestcookie())
ledgerData.set_requestcookie(m->requestcookie());
switch (itype)
{
case protocol::liBASE:
sendLedgerBase(ledger, ledgerData);
sendLedgerBase(ledger, ledgerData, destinations);
return;
case protocol::liTX_NODE:
@@ -3528,7 +3838,7 @@ PeerImp::processLedgerRequest(std::shared_ptr<protocol::TMGetLedger> const& m)
if (ledgerData.nodes_size() == 0)
return;
send(std::make_shared<Message>(ledgerData, protocol::mtLEDGER_DATA));
sendToMultiple(ledgerData, destinations);
}
int
@@ -3580,6 +3890,19 @@ PeerImp::isHighLatency() const
return latency_ >= peerHighLatency;
}
std::set<std::optional<uint64_t>>
PeerImp::releaseRequestCookies(uint256 const& requestHash)
{
std::set<std::optional<uint64_t>> result;
std::lock_guard lock(cookieLock_);
if (messageRequestCookies_.contains(requestHash))
{
std::swap(result, messageRequestCookies_[requestHash]);
messageRequestCookies_.erase(requestHash);
}
return result;
};
void
PeerImp::Metrics::add_message(std::uint64_t bytes)
{

View File

@@ -247,6 +247,13 @@ private:
bool ledgerReplayEnabled_ = false;
LedgerReplayMsgHandler ledgerReplayMsgHandler_;
// Track message requests and responses
// TODO: Use an expiring cache or something
using MessageCookieMap = std::map<uint256, std::set<std::optional<uint64_t>>>;
using PeerCookieMap = std::map<std::shared_ptr<Peer>, std::set<std::optional<uint64_t>>>;
std::mutex mutable cookieLock_;
MessageCookieMap messageRequestCookies_;
friend class OverlayImpl;
class Metrics
@@ -488,6 +495,13 @@ public:
return txReduceRelayEnabled_;
}
//
// Messages
//
std::set<std::optional<uint64_t>>
releaseRequestCookies(uint256 const& requestHash) override;
private:
/**
* @brief Handles a failure associated with a specific error code.
@@ -783,16 +797,22 @@ private:
std::shared_ptr<protocol::TMValidation> const& packet);
void
sendLedgerBase(std::shared_ptr<Ledger const> const& ledger, protocol::TMLedgerData& ledgerData);
std::shared_ptr<Ledger const>
getLedger(std::shared_ptr<protocol::TMGetLedger> const& m);
std::shared_ptr<SHAMap const>
getTxSet(std::shared_ptr<protocol::TMGetLedger> const& m) const;
sendLedgerBase(
std::shared_ptr<Ledger const> const& ledger,
protocol::TMLedgerData& ledgerData,
PeerCookieMap const& destinations);
void
processLedgerRequest(std::shared_ptr<protocol::TMGetLedger> const& m);
sendToMultiple(protocol::TMLedgerData& ledgerData, PeerCookieMap const& destinations);
std::shared_ptr<Ledger const>
getLedger(std::shared_ptr<protocol::TMGetLedger> const& m, uint256 const& mHash);
std::shared_ptr<SHAMap const>
getTxSet(std::shared_ptr<protocol::TMGetLedger> const& m, uint256 const& mHash) const;
void
processLedgerRequest(std::shared_ptr<protocol::TMGetLedger> const& m, uint256 const& mHash);
};
//------------------------------------------------------------------------------

View File

@@ -7,6 +7,9 @@
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/core/HashRouter.h>
#include <xrpl/core/JobQueue.h>
#include <xrpl/protocol/digest.h>
#include <google/protobuf/message.h>
@@ -97,16 +100,45 @@ PeerSetImpl::sendRequest(
std::shared_ptr<Peer> const& peer)
{
auto packet = std::make_shared<Message>(message, type);
auto const messageHash = [&]() {
auto const packetBuffer = packet->getBuffer(compression::Compressed::Off);
return sha512Half(Slice(packetBuffer.data(), packetBuffer.size()));
}();
// Allow messages to be re-sent to the same peer after a delay
using namespace std::chrono_literals;
constexpr std::chrono::seconds interval = 30s;
if (peer)
{
peer->send(packet);
if (app_.getHashRouter().shouldProcessForPeer(messageHash, peer->id(), interval))
{
JLOG(journal_.trace()) << "Sending " << protocolMessageName(type) << " message to ["
<< peer->id() << "]: " << messageHash;
peer->send(packet);
}
else
JLOG(journal_.debug()) << "Suppressing sending duplicate " << protocolMessageName(type)
<< " message to [" << peer->id() << "]: " << messageHash;
return;
}
for (auto id : peers_)
{
if (auto p = app_.getOverlay().findPeerByShortID(id))
p->send(packet);
{
if (app_.getHashRouter().shouldProcessForPeer(messageHash, p->id(), interval))
{
JLOG(journal_.trace()) << "Sending " << protocolMessageName(type) << " message to ["
<< p->id() << "]: " << messageHash;
p->send(packet);
}
else
JLOG(journal_.debug())
<< "Suppressing sending duplicate " << protocolMessageName(type)
<< " message to [" << p->id() << "]: " << messageHash;
}
}
}

View File

@@ -23,6 +23,12 @@ protocolMessageType(protocol::TMGetLedger const&)
return protocol::mtGET_LEDGER;
}
inline protocol::MessageType
protocolMessageType(protocol::TMLedgerData const&)
{
return protocol::mtLEDGER_DATA;
}
inline protocol::MessageType
protocolMessageType(protocol::TMReplayDeltaRequest const&)
{
@@ -434,3 +440,63 @@ invokeProtocolMessage(Buffers const& buffers, Handler& handler, std::size_t& hin
}
} // namespace xrpl
namespace protocol {
template <class Hasher>
void
hash_append(Hasher& h, TMGetLedger const& msg)
{
using beast::hash_append;
using namespace xrpl;
hash_append(h, safe_cast<int>(protocolMessageType(msg)));
hash_append(h, safe_cast<int>(msg.itype()));
if (msg.has_ltype())
hash_append(h, safe_cast<int>(msg.ltype()));
if (msg.has_ledgerhash())
hash_append(h, msg.ledgerhash());
if (msg.has_ledgerseq())
hash_append(h, msg.ledgerseq());
for (auto const& nodeId : msg.nodeids())
hash_append(h, nodeId);
hash_append(h, msg.nodeids_size());
// Do NOT include the request cookie. It does not affect the content of the
// request, but only where to route the results.
// if (msg.has_requestcookie())
// hash_append(h, msg.requestcookie());
if (msg.has_querytype())
hash_append(h, safe_cast<int>(msg.querytype()));
if (msg.has_querydepth())
hash_append(h, msg.querydepth());
}
template <class Hasher>
void
hash_append(Hasher& h, TMLedgerData const& msg)
{
using beast::hash_append;
using namespace xrpl;
hash_append(h, safe_cast<int>(protocolMessageType(msg)));
hash_append(h, msg.ledgerhash());
hash_append(h, msg.ledgerseq());
hash_append(h, safe_cast<int>(msg.type()));
for (auto const& node : msg.nodes())
{
hash_append(h, node.nodedata());
if (node.has_nodeid())
hash_append(h, node.nodeid());
}
hash_append(h, msg.nodes_size());
if (msg.has_requestcookie())
hash_append(h, msg.requestcookie());
if (msg.has_error())
hash_append(h, safe_cast<int>(msg.error()));
}
} // namespace protocol

View File

@@ -28,6 +28,8 @@ namespace xrpl {
constexpr ProtocolVersion const supportedProtocolList[]{
{2, 1},
{2, 2},
// Adds TMLedgerData::responseCookies and directResponse
{2, 3},
};
// This ugly construct ensures that supportedProtocolList is sorted in strictly

View File

@@ -8,9 +8,6 @@
namespace xrpl::PeerFinder {
/** Direction of a slot count adjustment. */
enum class CountAdjustment : int { Decrement = -1, Increment = 1 };
/** Manages the count of available connections for the various slots. */
class Counts
{
@@ -19,14 +16,14 @@ public:
void
add(Slot const& s)
{
adjust(s, CountAdjustment::Increment);
adjust(s, 1);
}
/** Removes the slot state and properties from the slot counts. */
void
remove(Slot const& s)
{
adjust(s, CountAdjustment::Decrement);
adjust(s, -1);
}
/** Returns `true` if the slot can become active. */
@@ -210,40 +207,21 @@ public:
//--------------------------------------------------------------------------
private:
/** Increments or decrements a counter based on the adjustment direction. */
template <typename T>
static void
adjustCounter(T& counter, CountAdjustment dir)
{
switch (dir)
{
case CountAdjustment::Increment:
++counter;
break;
case CountAdjustment::Decrement:
--counter;
break;
}
}
// Adjusts counts based on the specified slot, in the direction indicated.
// Using ++/-- instead of += on std::size_t counters avoids UBSan
// unsigned-integer-overflow from implicit conversion of -1 to SIZE_MAX.
// A decrement on a zero counter is a real bug that UBSan should catch.
void
adjust(Slot const& s, CountAdjustment const dir)
adjust(Slot const& s, int const n)
{
if (s.fixed())
adjustCounter(m_fixed, dir);
m_fixed += n;
if (s.reserved())
adjustCounter(m_reserved, dir);
m_reserved += n;
switch (s.state())
{
case Slot::accept:
XRPL_ASSERT(s.inbound(), "xrpl::PeerFinder::Counts::adjust : input is inbound");
adjustCounter(m_acceptCount, dir);
m_acceptCount += n;
break;
case Slot::connect:
@@ -252,28 +230,28 @@ private:
!s.inbound(),
"xrpl::PeerFinder::Counts::adjust : input is not "
"inbound");
adjustCounter(m_attempts, dir);
m_attempts += n;
break;
case Slot::active:
if (s.fixed())
adjustCounter(m_fixed_active, dir);
m_fixed_active += n;
if (!s.fixed() && !s.reserved())
{
if (s.inbound())
{
adjustCounter(m_in_active, dir);
m_in_active += n;
}
else
{
adjustCounter(m_out_active, dir);
m_out_active += n;
}
}
adjustCounter(m_active, dir);
m_active += n;
break;
case Slot::closing:
adjustCounter(m_closingCount, dir);
m_closingCount += n;
break;
// LCOV_EXCL_START