mirror of
https://github.com/Xahau/xahaud.git
synced 2026-10-22 03:37:08 +00:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e0b9cc8aa | ||
|
|
505c28768a | ||
|
|
d7f320bd17 | ||
|
|
22a94c4429 | ||
|
|
42a33a224d | ||
|
|
ee33d825fc | ||
|
|
9ef66a65b8 | ||
|
|
98fdaa4afa | ||
|
|
d9cc66afe9 |
No files matched your search
@@ -96,6 +96,7 @@ test.overlay > test.toplevel
|
||||
test.overlay > test.unit_test
|
||||
test.overlay > xrpl.basics
|
||||
test.overlay > xrpld.app
|
||||
test.overlay > xrpld.core
|
||||
test.overlay > xrpld.overlay
|
||||
test.overlay > xrpld.peerfinder
|
||||
test.overlay > xrpld.shamap
|
||||
@@ -191,6 +192,7 @@ xrpld.overlay > xrpl.basics
|
||||
xrpld.overlay > xrpld.core
|
||||
xrpld.overlay > xrpld.peerfinder
|
||||
xrpld.overlay > xrpld.perflog
|
||||
xrpld.overlay > xrpld.shamap
|
||||
xrpld.overlay > xrpl.json
|
||||
xrpld.overlay > xrpl.protocol
|
||||
xrpld.overlay > xrpl.resource
|
||||
|
||||
+23
-23
@@ -22,29 +22,29 @@ APPLY_HOOK="$SCRIPT_DIR/../include/xrpl/hook/hook_api.macro"
|
||||
return s;
|
||||
}
|
||||
|
||||
{
|
||||
line = $0;
|
||||
|
||||
# Skip block comments
|
||||
if (line ~ /\/\*/) {
|
||||
next;
|
||||
}
|
||||
|
||||
# Look for comment lines that start with // and contain function signature
|
||||
if (line ~ /^[[:space:]]*\/\/[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*[[:space:]]+[a-zA-Z_][a-zA-Z0-9_]*[[:space:]]*\(/) {
|
||||
# Remove leading // and trim
|
||||
sub(/^[[:space:]]*\/\/[[:space:]]*/, "", line);
|
||||
line = trim(line);
|
||||
|
||||
# Check if function name is "_g" to add attribute
|
||||
if (line ~ /[[:space:]]+_g[[:space:]]*\(/) {
|
||||
# Insert __attribute__((noduplicate)) before _g
|
||||
sub(/[[:space:]]+_g/, " __attribute__((noduplicate)) _g", line);
|
||||
}
|
||||
|
||||
# printf("\n");
|
||||
|
||||
printf("extern %s\n\n", line);
|
||||
# Each entry is HOOK_API_DEFINITION(RET, NAME, (PARAMS), AMENDMENT),
|
||||
# possibly spread over several lines. Collect until the parentheses
|
||||
# balance, then split on the first two commas; the parameter tuple
|
||||
# is the first parenthesised group.
|
||||
function emit(s, i, ret, name, p, q, params) {
|
||||
sub(/^[[:space:]]*HOOK_API_DEFINITION[[:space:]]*\(/, "", s);
|
||||
gsub(/[[:space:]]+/, " ", s);
|
||||
i = index(s, ","); ret = trim(substr(s, 1, i - 1)); s = substr(s, i + 1);
|
||||
i = index(s, ","); name = trim(substr(s, 1, i - 1)); s = substr(s, i + 1);
|
||||
p = index(s, "("); q = index(s, ")");
|
||||
params = trim(substr(s, p + 1, q - p - 1));
|
||||
if (name == "_g")
|
||||
name = "__attribute__((noduplicate)) _g";
|
||||
printf("extern %s %s(%s);\n\n", ret, name, params);
|
||||
}
|
||||
|
||||
/^[[:space:]]*HOOK_API_DEFINITION[[:space:]]*\(/ { buf = ""; collecting = 1; }
|
||||
|
||||
collecting {
|
||||
buf = buf " " $0;
|
||||
if (gsub(/\(/, "(", buf) == gsub(/\)/, ")", buf)) {
|
||||
collecting = 0;
|
||||
emit(buf);
|
||||
}
|
||||
}
|
||||
' "$APPLY_HOOK"
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
#pragma once
|
||||
|
||||
#include <shared_mutex>
|
||||
|
||||
// On Linux (glibc), std::shared_mutex wraps pthread_rwlock_t initialised
|
||||
// with PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP. This means a
|
||||
// pending exclusive lock() blocks new shared (reader) acquisitions,
|
||||
// causing reader starvation when writers contend frequently.
|
||||
//
|
||||
// On macOS / ARM (libc++), std::shared_mutex is already reader-preferring,
|
||||
// so the same code behaves differently across platforms.
|
||||
//
|
||||
// This header provides ReaderPreferringSharedMutex:
|
||||
// - On Linux it wraps pthread_rwlock_t initialised with
|
||||
// PTHREAD_RWLOCK_PREFER_READER_NP, matching macOS semantics.
|
||||
// - On all other platforms it is a type alias for std::shared_mutex.
|
||||
//
|
||||
// The interface is identical to std::shared_mutex, so it works with
|
||||
// std::shared_lock and std::unique_lock.
|
||||
|
||||
#if defined(__linux__)
|
||||
|
||||
#include <cerrno>
|
||||
#include <pthread.h>
|
||||
#include <system_error>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
class ReaderPreferringSharedMutex
|
||||
{
|
||||
pthread_rwlock_t rwlock_;
|
||||
|
||||
public:
|
||||
ReaderPreferringSharedMutex()
|
||||
{
|
||||
pthread_rwlockattr_t attr;
|
||||
pthread_rwlockattr_init(&attr);
|
||||
pthread_rwlockattr_setkind_np(&attr, PTHREAD_RWLOCK_PREFER_READER_NP);
|
||||
int rc = pthread_rwlock_init(&rwlock_, &attr);
|
||||
pthread_rwlockattr_destroy(&attr);
|
||||
if (rc != 0)
|
||||
throw std::system_error(
|
||||
rc, std::system_category(), "pthread_rwlock_init");
|
||||
}
|
||||
|
||||
~ReaderPreferringSharedMutex()
|
||||
{
|
||||
pthread_rwlock_destroy(&rwlock_);
|
||||
}
|
||||
|
||||
ReaderPreferringSharedMutex(ReaderPreferringSharedMutex const&) = delete;
|
||||
ReaderPreferringSharedMutex&
|
||||
operator=(ReaderPreferringSharedMutex const&) = delete;
|
||||
|
||||
// Exclusive (writer) locking
|
||||
void
|
||||
lock()
|
||||
{
|
||||
pthread_rwlock_wrlock(&rwlock_);
|
||||
}
|
||||
|
||||
bool
|
||||
try_lock()
|
||||
{
|
||||
return pthread_rwlock_trywrlock(&rwlock_) == 0;
|
||||
}
|
||||
|
||||
void
|
||||
unlock()
|
||||
{
|
||||
pthread_rwlock_unlock(&rwlock_);
|
||||
}
|
||||
|
||||
// Shared (reader) locking
|
||||
void
|
||||
lock_shared()
|
||||
{
|
||||
pthread_rwlock_rdlock(&rwlock_);
|
||||
}
|
||||
|
||||
bool
|
||||
try_lock_shared()
|
||||
{
|
||||
return pthread_rwlock_tryrdlock(&rwlock_) == 0;
|
||||
}
|
||||
|
||||
void
|
||||
unlock_shared()
|
||||
{
|
||||
pthread_rwlock_unlock(&rwlock_);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#else // !__linux__
|
||||
|
||||
namespace ripple {
|
||||
|
||||
// macOS, Windows, etc. — std::shared_mutex is already reader-preferring.
|
||||
using ReaderPreferringSharedMutex = std::shared_mutex;
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -77,7 +77,7 @@ strUnHex(std::size_t strSize, Iterator begin, Iterator end)
|
||||
|
||||
if (strSize & 1)
|
||||
{
|
||||
int c = unxtab[*iter++];
|
||||
int c = unxtab[static_cast<unsigned char>(*iter++)];
|
||||
|
||||
if (c < 0)
|
||||
return {};
|
||||
@@ -87,12 +87,12 @@ strUnHex(std::size_t strSize, Iterator begin, Iterator end)
|
||||
|
||||
while (iter != end)
|
||||
{
|
||||
int cHigh = unxtab[*iter++];
|
||||
int cHigh = unxtab[static_cast<unsigned char>(*iter++)];
|
||||
|
||||
if (cHigh < 0)
|
||||
return {};
|
||||
|
||||
int cLow = unxtab[*iter++];
|
||||
int cLow = unxtab[static_cast<unsigned char>(*iter++)];
|
||||
|
||||
if (cLow < 0)
|
||||
return {};
|
||||
|
||||
+14
-12
@@ -404,6 +404,18 @@ const double fee_base_multiplier = 1.1f;
|
||||
|
||||
using APIWhitelist = std::map<std::string, std::vector<uint8_t>>;
|
||||
|
||||
namespace detail {
|
||||
template <class T>
|
||||
constexpr uint8_t wasmTypeCode = sizeof(T) == 8 ? 0x7EU : 0x7FU;
|
||||
|
||||
template <class R, class... Args>
|
||||
std::vector<uint8_t>
|
||||
wasmSignature(R (*)(Args...))
|
||||
{
|
||||
return {wasmTypeCode<R>, wasmTypeCode<Args>...};
|
||||
}
|
||||
} // namespace detail
|
||||
|
||||
// RH NOTE: Find descriptions of api functions in ./impl/applyHook.cpp and
|
||||
// hookapi.h (include for hooks) this is a map of the api name to its return
|
||||
// code (vec[0] and its parameters vec[>0]) as wasm type codes
|
||||
@@ -415,25 +427,15 @@ getImportWhitelist(Rules const& rules)
|
||||
#pragma push_macro("HOOK_API_DEFINITION")
|
||||
#undef HOOK_API_DEFINITION
|
||||
|
||||
#define int64_t 0x7EU
|
||||
#define int32_t 0x7FU
|
||||
#define uint32_t 0x7FU
|
||||
|
||||
#define HOOK_WRAP_PARAMS(...) __VA_ARGS__
|
||||
|
||||
#define HOOK_API_DEFINITION( \
|
||||
RETURN_TYPE, FUNCTION_NAME, PARAMS_TUPLE, AMENDMENT) \
|
||||
if (AMENDMENT == uint256{} || rules.enabled(AMENDMENT)) \
|
||||
whitelist[#FUNCTION_NAME] = { \
|
||||
RETURN_TYPE, HOOK_WRAP_PARAMS PARAMS_TUPLE};
|
||||
whitelist[#FUNCTION_NAME] = \
|
||||
detail::wasmSignature((RETURN_TYPE(*) PARAMS_TUPLE) nullptr);
|
||||
|
||||
#include "hook_api.macro"
|
||||
|
||||
#undef HOOK_API_DEFINITION
|
||||
#undef HOOK_WRAP_PARAMS
|
||||
#undef int64_t
|
||||
#undef int32_t
|
||||
#undef uint32_t
|
||||
#pragma pop_macro("HOOK_API_DEFINITION")
|
||||
|
||||
return whitelist;
|
||||
|
||||
@@ -1,374 +1,301 @@
|
||||
// int32_t _g(uint32_t guard_id, uint32_t maxiter);
|
||||
// clang-format off
|
||||
HOOK_API_DEFINITION(
|
||||
int32_t, _g, (uint32_t, uint32_t),
|
||||
int32_t, _g, (uint32_t guard_id, uint32_t maxiter),
|
||||
uint256{})
|
||||
|
||||
// int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, accept, (uint32_t, uint32_t, int64_t),
|
||||
int64_t, accept, (uint32_t read_ptr, uint32_t read_len, int64_t error_code),
|
||||
uint256{})
|
||||
|
||||
// int64_t rollback(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, rollback, (uint32_t, uint32_t, int64_t),
|
||||
int64_t, rollback, (uint32_t read_ptr, uint32_t read_len, int64_t error_code),
|
||||
uint256{})
|
||||
|
||||
// int64_t util_raddr(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, util_raddr, (uint32_t, uint32_t, uint32_t, uint32_t),
|
||||
int64_t, util_raddr, (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len),
|
||||
uint256{})
|
||||
|
||||
// int64_t util_accid(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, util_accid, (uint32_t, uint32_t, uint32_t, uint32_t),
|
||||
int64_t, util_accid, (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len),
|
||||
uint256{})
|
||||
|
||||
// int64_t util_verify(uint32_t dread_ptr, uint32_t dread_len, uint32_t sread_ptr, uint32_t sread_len, uint32_t kread_ptr, uint32_t kread_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, util_verify, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t),
|
||||
int64_t, util_verify, (uint32_t dread_ptr, uint32_t dread_len, uint32_t sread_ptr, uint32_t sread_len, uint32_t kread_ptr, uint32_t kread_len),
|
||||
uint256{})
|
||||
|
||||
// int64_t util_sha512h(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, util_sha512h, (uint32_t, uint32_t, uint32_t, uint32_t),
|
||||
int64_t, util_sha512h, (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len),
|
||||
uint256{})
|
||||
|
||||
// int64_t util_keylet(uint32_t write_ptr, uint32_t write_len, uint32_t keylet_type, uint32_t a, uint32_t b, uint32_t c, uint32_t d, uint32_t e, uint32_t f);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, util_keylet, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t),
|
||||
int64_t, util_keylet, (uint32_t write_ptr, uint32_t write_len, uint32_t keylet_type, uint32_t a, uint32_t b, uint32_t c, uint32_t d, uint32_t e, uint32_t f),
|
||||
uint256{})
|
||||
|
||||
// int64_t sto_validate(uint32_t tread_ptr, uint32_t tread_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, sto_validate, (uint32_t, uint32_t),
|
||||
int64_t, sto_validate, (uint32_t tread_ptr, uint32_t tread_len),
|
||||
uint256{})
|
||||
|
||||
// int64_t sto_subfield(uint32_t read_ptr, uint32_t read_len, uint32_t field_id);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, sto_subfield, (uint32_t, uint32_t, uint32_t),
|
||||
int64_t, sto_subfield, (uint32_t read_ptr, uint32_t read_len, uint32_t field_id),
|
||||
uint256{})
|
||||
|
||||
// int64_t sto_subarray(uint32_t read_ptr, uint32_t read_len, uint32_t array_id);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, sto_subarray, (uint32_t, uint32_t, uint32_t),
|
||||
int64_t, sto_subarray, (uint32_t read_ptr, uint32_t read_len, uint32_t array_id),
|
||||
uint256{})
|
||||
|
||||
// int64_t sto_emplace(uint32_t write_ptr, uint32_t write_len, uint32_t sread_ptr, uint32_t sread_len, uint32_t fread_ptr, uint32_t fread_len, uint32_t field_id);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, sto_emplace, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t),
|
||||
int64_t, sto_emplace, (uint32_t write_ptr, uint32_t write_len, uint32_t sread_ptr, uint32_t sread_len, uint32_t fread_ptr, uint32_t fread_len, uint32_t field_id),
|
||||
uint256{})
|
||||
|
||||
// int64_t sto_erase(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len, uint32_t field_id);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, sto_erase, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t),
|
||||
int64_t, sto_erase, (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len, uint32_t field_id),
|
||||
uint256{})
|
||||
|
||||
// int64_t etxn_burden();
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, etxn_burden, (),
|
||||
uint256{})
|
||||
|
||||
// int64_t etxn_details(uint32_t write_ptr, uint32_t write_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, etxn_details, (uint32_t, uint32_t),
|
||||
int64_t, etxn_details, (uint32_t write_ptr, uint32_t write_len),
|
||||
uint256{})
|
||||
|
||||
// int64_t etxn_fee_base(uint32_t read_ptr, uint32_t read_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, etxn_fee_base, (uint32_t, uint32_t),
|
||||
int64_t, etxn_fee_base, (uint32_t read_ptr, uint32_t read_len),
|
||||
uint256{})
|
||||
|
||||
// int64_t etxn_reserve(uint32_t count);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, etxn_reserve, (uint32_t),
|
||||
int64_t, etxn_reserve, (uint32_t count),
|
||||
uint256{})
|
||||
|
||||
// int64_t etxn_generation();
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, etxn_generation, (),
|
||||
uint256{})
|
||||
|
||||
// int64_t etxn_nonce(uint32_t write_ptr, uint32_t write_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, etxn_nonce, (uint32_t, uint32_t),
|
||||
int64_t, etxn_nonce, (uint32_t write_ptr, uint32_t write_len),
|
||||
uint256{})
|
||||
|
||||
// int64_t emit(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, emit, (uint32_t, uint32_t, uint32_t, uint32_t),
|
||||
int64_t, emit, (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len),
|
||||
uint256{})
|
||||
|
||||
// int64_t float_set(int32_t exponent, int64_t mantissa);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, float_set, (int32_t, int64_t),
|
||||
int64_t, float_set, (int32_t exponent, int64_t mantissa),
|
||||
uint256{})
|
||||
|
||||
// int64_t float_multiply(int64_t float1, int64_t float2);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, float_multiply, (int64_t, int64_t),
|
||||
int64_t, float_multiply, (int64_t float1, int64_t float2),
|
||||
uint256{})
|
||||
|
||||
// int64_t float_mulratio(int64_t float1, uint32_t round_up, uint32_t numerator, uint32_t denominator);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, float_mulratio, (int64_t, uint32_t, uint32_t, uint32_t),
|
||||
int64_t, float_mulratio, (int64_t float1, uint32_t round_up, uint32_t numerator, uint32_t denominator),
|
||||
uint256{})
|
||||
|
||||
// int64_t float_negate(int64_t float1);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, float_negate, (int64_t),
|
||||
int64_t, float_negate, (int64_t float1),
|
||||
uint256{})
|
||||
|
||||
// int64_t float_compare(int64_t float1, int64_t float2, uint32_t mode);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, float_compare, (int64_t, int64_t, uint32_t),
|
||||
int64_t, float_compare, (int64_t float1, int64_t float2, uint32_t mode),
|
||||
uint256{})
|
||||
|
||||
// int64_t float_sum(int64_t float1, int64_t float2);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, float_sum, (int64_t, int64_t),
|
||||
int64_t, float_sum, (int64_t float1, int64_t float2),
|
||||
uint256{})
|
||||
|
||||
// int64_t float_sto(uint32_t write_ptr, uint32_t write_len, uint32_t cread_ptr, uint32_t cread_len, uint32_t iread_ptr, uint32_t iread_len, int64_t float1, uint32_t field_code);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, float_sto, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, int64_t, uint32_t),
|
||||
int64_t, float_sto, (uint32_t write_ptr, uint32_t write_len, uint32_t cread_ptr, uint32_t cread_len, uint32_t iread_ptr, uint32_t iread_len, int64_t float1, uint32_t field_code),
|
||||
uint256{})
|
||||
|
||||
// int64_t float_sto_set(uint32_t read_ptr, uint32_t read_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, float_sto_set, (uint32_t, uint32_t),
|
||||
int64_t, float_sto_set, (uint32_t read_ptr, uint32_t read_len),
|
||||
uint256{})
|
||||
|
||||
// int64_t float_invert(int64_t float1);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, float_invert, (int64_t),
|
||||
int64_t, float_invert, (int64_t float1),
|
||||
uint256{})
|
||||
|
||||
// int64_t float_divide(int64_t float1, int64_t float2);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, float_divide, (int64_t, int64_t),
|
||||
int64_t, float_divide, (int64_t float1, int64_t float2),
|
||||
uint256{})
|
||||
|
||||
// int64_t float_one();
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, float_one, (),
|
||||
uint256{})
|
||||
|
||||
// int64_t float_mantissa(int64_t float1);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, float_mantissa, (int64_t),
|
||||
int64_t, float_mantissa, (int64_t float1),
|
||||
uint256{})
|
||||
|
||||
// int64_t float_sign(int64_t float1);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, float_sign, (int64_t),
|
||||
int64_t, float_sign, (int64_t float1),
|
||||
uint256{})
|
||||
|
||||
// int64_t float_int(int64_t float1, uint32_t decimal_places, uint32_t abs);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, float_int, (int64_t, uint32_t, uint32_t),
|
||||
int64_t, float_int, (int64_t float1, uint32_t decimal_places, uint32_t abs),
|
||||
uint256{})
|
||||
|
||||
// int64_t float_log(int64_t float1);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, float_log, (int64_t),
|
||||
int64_t, float_log, (int64_t float1),
|
||||
uint256{})
|
||||
|
||||
// int64_t float_root(int64_t float1, uint32_t n);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, float_root, (int64_t, uint32_t),
|
||||
int64_t, float_root, (int64_t float1, uint32_t n),
|
||||
uint256{})
|
||||
|
||||
// int64_t fee_base();
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, fee_base, (),
|
||||
uint256{})
|
||||
|
||||
// int64_t ledger_seq();
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, ledger_seq, (),
|
||||
uint256{})
|
||||
|
||||
// int64_t ledger_last_time();
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, ledger_last_time, (),
|
||||
uint256{})
|
||||
|
||||
// int64_t ledger_last_hash(uint32_t write_ptr, uint32_t write_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, ledger_last_hash, (uint32_t, uint32_t),
|
||||
int64_t, ledger_last_hash, (uint32_t write_ptr, uint32_t write_len),
|
||||
uint256{})
|
||||
|
||||
// int64_t ledger_nonce(uint32_t write_ptr, uint32_t write_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, ledger_nonce, (uint32_t, uint32_t),
|
||||
int64_t, ledger_nonce, (uint32_t write_ptr, uint32_t write_len),
|
||||
uint256{})
|
||||
|
||||
// int64_t ledger_keylet(uint32_t write_ptr, uint32_t write_len, uint32_t lread_ptr, uint32_t lread_len, uint32_t hread_ptr, uint32_t hread_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, ledger_keylet, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t),
|
||||
int64_t, ledger_keylet, (uint32_t write_ptr, uint32_t write_len, uint32_t lread_ptr, uint32_t lread_len, uint32_t hread_ptr, uint32_t hread_len),
|
||||
uint256{})
|
||||
|
||||
// int64_t hook_account(uint32_t write_ptr, uint32_t write_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, hook_account, (uint32_t, uint32_t),
|
||||
int64_t, hook_account, (uint32_t write_ptr, uint32_t write_len),
|
||||
uint256{})
|
||||
|
||||
// int64_t hook_hash(uint32_t write_ptr, uint32_t write_len, int32_t hook_no);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, hook_hash, (uint32_t, uint32_t, int32_t),
|
||||
int64_t, hook_hash, (uint32_t write_ptr, uint32_t write_len, int32_t hook_no),
|
||||
uint256{})
|
||||
|
||||
// int64_t hook_param_set(uint32_t read_ptr, uint32_t read_len, uint32_t kread_ptr, uint32_t kread_len, uint32_t hread_ptr, uint32_t hread_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, hook_param_set, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t),
|
||||
int64_t, hook_param_set, (uint32_t read_ptr, uint32_t read_len, uint32_t kread_ptr, uint32_t kread_len, uint32_t hread_ptr, uint32_t hread_len),
|
||||
uint256{})
|
||||
|
||||
// int64_t hook_param(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, hook_param, (uint32_t, uint32_t, uint32_t, uint32_t),
|
||||
int64_t, hook_param, (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len),
|
||||
uint256{})
|
||||
|
||||
// int64_t hook_again();
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, hook_again, (),
|
||||
uint256{})
|
||||
|
||||
// int64_t hook_skip(uint32_t read_ptr, uint32_t read_len, uint32_t flags);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, hook_skip, (uint32_t, uint32_t, uint32_t),
|
||||
int64_t, hook_skip, (uint32_t read_ptr, uint32_t read_len, uint32_t flags),
|
||||
uint256{})
|
||||
|
||||
// int64_t hook_pos();
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, hook_pos, (),
|
||||
uint256{})
|
||||
|
||||
// int64_t slot(uint32_t write_ptr, uint32_t write_len, uint32_t slot);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, slot, (uint32_t, uint32_t, uint32_t),
|
||||
int64_t, slot, (uint32_t write_ptr, uint32_t write_len, uint32_t slot),
|
||||
uint256{})
|
||||
|
||||
// int64_t slot_clear(uint32_t slot);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, slot_clear, (uint32_t),
|
||||
int64_t, slot_clear, (uint32_t slot),
|
||||
uint256{})
|
||||
|
||||
// int64_t slot_count(uint32_t slot);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, slot_count, (uint32_t),
|
||||
int64_t, slot_count, (uint32_t slot),
|
||||
uint256{})
|
||||
|
||||
// int64_t slot_set(uint32_t read_ptr, uint32_t read_len, uint32_t slot);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, slot_set, (uint32_t, uint32_t, uint32_t),
|
||||
int64_t, slot_set, (uint32_t read_ptr, uint32_t read_len, uint32_t slot),
|
||||
uint256{})
|
||||
|
||||
// int64_t slot_size(uint32_t slot);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, slot_size, (uint32_t),
|
||||
int64_t, slot_size, (uint32_t slot),
|
||||
uint256{})
|
||||
|
||||
// int64_t slot_subarray(uint32_t parent_slot, uint32_t array_id, uint32_t new_slot);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, slot_subarray, (uint32_t, uint32_t, uint32_t),
|
||||
int64_t, slot_subarray, (uint32_t parent_slot, uint32_t array_id, uint32_t new_slot),
|
||||
uint256{})
|
||||
|
||||
// int64_t slot_subfield(uint32_t parent_slot, uint32_t field_id, uint32_t new_slot);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, slot_subfield, (uint32_t, uint32_t, uint32_t),
|
||||
int64_t, slot_subfield, (uint32_t parent_slot, uint32_t field_id, uint32_t new_slot),
|
||||
uint256{})
|
||||
|
||||
// int64_t slot_type(uint32_t slot_no, uint32_t flags);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, slot_type, (uint32_t, uint32_t),
|
||||
int64_t, slot_type, (uint32_t slot_no, uint32_t flags),
|
||||
uint256{})
|
||||
|
||||
// int64_t slot_float(uint32_t slot_no);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, slot_float, (uint32_t),
|
||||
int64_t, slot_float, (uint32_t slot_no),
|
||||
uint256{})
|
||||
|
||||
// int64_t state_set(uint32_t read_ptr, uint32_t read_len, uint32_t kread_ptr, uint32_t kread_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, state_set, (uint32_t, uint32_t, uint32_t, uint32_t),
|
||||
int64_t, state_set, (uint32_t read_ptr, uint32_t read_len, uint32_t kread_ptr, uint32_t kread_len),
|
||||
uint256{})
|
||||
|
||||
// int64_t state_foreign_set(uint32_t read_ptr, uint32_t read_len, uint32_t kread_ptr, uint32_t kread_len, uint32_t nread_ptr, uint32_t nread_len, uint32_t aread_ptr, uint32_t aread_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, state_foreign_set, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t),
|
||||
int64_t, state_foreign_set, (uint32_t read_ptr, uint32_t read_len, uint32_t kread_ptr, uint32_t kread_len, uint32_t nread_ptr, uint32_t nread_len, uint32_t aread_ptr, uint32_t aread_len),
|
||||
uint256{})
|
||||
|
||||
// int64_t state(uint32_t write_ptr, uint32_t write_len, uint32_t kread_ptr, uint32_t kread_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, state, (uint32_t, uint32_t, uint32_t, uint32_t),
|
||||
int64_t, state, (uint32_t write_ptr, uint32_t write_len, uint32_t kread_ptr, uint32_t kread_len),
|
||||
uint256{})
|
||||
|
||||
// int64_t state_foreign(uint32_t write_ptr, uint32_t write_len, uint32_t kread_ptr, uint32_t kread_len, uint32_t nread_ptr, uint32_t nread_len, uint32_t aread_ptr, uint32_t aread_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, state_foreign, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t),
|
||||
int64_t, state_foreign, (uint32_t write_ptr, uint32_t write_len, uint32_t kread_ptr, uint32_t kread_len, uint32_t nread_ptr, uint32_t nread_len, uint32_t aread_ptr, uint32_t aread_len),
|
||||
uint256{})
|
||||
|
||||
// int64_t trace(uint32_t mread_ptr, uint32_t mread_len, uint32_t dread_ptr, uint32_t dread_len, uint32_t as_hex);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, trace, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t),
|
||||
int64_t, trace, (uint32_t mread_ptr, uint32_t mread_len, uint32_t dread_ptr, uint32_t dread_len, uint32_t as_hex),
|
||||
uint256{})
|
||||
|
||||
// int64_t trace_num(uint32_t read_ptr, uint32_t read_len, int64_t number);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, trace_num, (uint32_t, uint32_t, int64_t),
|
||||
int64_t, trace_num, (uint32_t read_ptr, uint32_t read_len, int64_t number),
|
||||
uint256{})
|
||||
|
||||
// int64_t trace_float(uint32_t read_ptr, uint32_t read_len, int64_t float1);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, trace_float, (uint32_t, uint32_t, int64_t),
|
||||
int64_t, trace_float, (uint32_t read_ptr, uint32_t read_len, int64_t float1),
|
||||
uint256{})
|
||||
|
||||
// int64_t otxn_burden();
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, otxn_burden, (),
|
||||
uint256{})
|
||||
|
||||
// int64_t otxn_field(uint32_t write_ptr, uint32_t write_len, uint32_t field_id);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, otxn_field, (uint32_t, uint32_t, uint32_t),
|
||||
int64_t, otxn_field, (uint32_t write_ptr, uint32_t write_len, uint32_t field_id),
|
||||
uint256{})
|
||||
|
||||
// int64_t otxn_generation();
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, otxn_generation, (),
|
||||
uint256{})
|
||||
|
||||
// int64_t otxn_id(uint32_t write_ptr, uint32_t write_len, uint32_t flags);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, otxn_id, (uint32_t, uint32_t, uint32_t),
|
||||
int64_t, otxn_id, (uint32_t write_ptr, uint32_t write_len, uint32_t flags),
|
||||
uint256{})
|
||||
|
||||
// int64_t otxn_type();
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, otxn_type, (),
|
||||
uint256{})
|
||||
|
||||
// int64_t otxn_slot(uint32_t slot_no);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, otxn_slot, (uint32_t),
|
||||
int64_t, otxn_slot, (uint32_t slot_no),
|
||||
uint256{})
|
||||
|
||||
// int64_t otxn_param(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, otxn_param, (uint32_t, uint32_t, uint32_t, uint32_t),
|
||||
int64_t, otxn_param, (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len),
|
||||
uint256{})
|
||||
|
||||
// int64_t meta_slot(uint32_t slot_no);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, meta_slot, (uint32_t),
|
||||
int64_t, meta_slot, (uint32_t slot_no),
|
||||
uint256{})
|
||||
|
||||
// int64_t xpop_slot(uint32_t slot_no_tx, uint32_t slot_no_meta);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, xpop_slot, (uint32_t, uint32_t),
|
||||
int64_t, xpop_slot, (uint32_t slot_no_tx, uint32_t slot_no_meta),
|
||||
featureHooksUpdate1)
|
||||
|
||||
// int64_t prepare(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, prepare, (uint32_t, uint32_t, uint32_t, uint32_t),
|
||||
int64_t, prepare, (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len),
|
||||
featureHooksUpdate2)
|
||||
// clang-format on
|
||||
@@ -47,6 +47,7 @@ JSS(AL_hit_rate); // out: GetCounts
|
||||
JSS(AcceptedCredentials); // out: AccountObjects
|
||||
JSS(Account); // in: TransactionSign; field.
|
||||
JSS(AMMID); // field
|
||||
JSS(API); // out: RPC server_definitions (HOOK)
|
||||
JSS(Amount); // in: TransactionSign; field.
|
||||
JSS(Authorize); // field
|
||||
JSS(Amount2); // in/out: AMM IOU/XRP pool, deposit, withdraw amount
|
||||
@@ -68,6 +69,9 @@ JSS(Destination); // in: TransactionSign; field.
|
||||
JSS(EPrice); // in: AMM Deposit option
|
||||
JSS(Fee); // in/out: TransactionSign; field.
|
||||
JSS(FIELDS); // out: RPC server_definitions
|
||||
JSS(HOOK); // out: RPC server_definitions
|
||||
JSS(KEYLET_TYPES); // out: RPC server_definitions (HOOK)
|
||||
JSS(RETURN_CODES); // out: RPC server_definitions (HOOK)
|
||||
JSS(Flags); // in/out: TransactionSign; field.
|
||||
JSS(GenesisMints);
|
||||
JSS(GovernanceMarks);
|
||||
|
||||
@@ -545,7 +545,7 @@ b58_to_b256_be(std::string_view input, std::span<std::uint8_t> out)
|
||||
XRPL_ASSERT(
|
||||
num_b_58_10_coeffs <= b_58_10_coeff.size(),
|
||||
"ripple::b58_fast::detail::b58_to_b256_be : maximum coeff");
|
||||
for (auto c : input.substr(0, partial_coeff_len))
|
||||
for (unsigned char const c : input.substr(0, partial_coeff_len))
|
||||
{
|
||||
auto cur_val = ::ripple::alphabetReverse[c];
|
||||
if (cur_val < 0)
|
||||
@@ -559,7 +559,7 @@ b58_to_b256_be(std::string_view input, std::span<std::uint8_t> out)
|
||||
{
|
||||
for (int j = 0; j < num_full_coeffs; ++j)
|
||||
{
|
||||
auto c = input[partial_coeff_len + j * 10 + i];
|
||||
unsigned char const c = input[partial_coeff_len + j * 10 + i];
|
||||
auto cur_val = ::ripple::alphabetReverse[c];
|
||||
if (cur_val < 0)
|
||||
{
|
||||
@@ -580,6 +580,12 @@ b58_to_b256_be(std::string_view input, std::span<std::uint8_t> out)
|
||||
{
|
||||
std::uint64_t const c = b_58_10_coeff[i];
|
||||
|
||||
// Each digit is < 58, so at most 4 words are in use here and the
|
||||
// span below always fits `result`. Guard the invariant anyway rather
|
||||
// than letting a violation index past the end of the stack array.
|
||||
if (cur_result_size >= result.size())
|
||||
return Unexpected(TokenCodecErrc::inputTooLarge);
|
||||
|
||||
{
|
||||
auto code = ripple::b58_fast::detail::inplace_bigint_mul(
|
||||
std::span(&result[0], cur_result_size + 1), B_58_10);
|
||||
|
||||
@@ -590,6 +590,134 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
test_emitted_txn_entry(FeatureBitset features)
|
||||
{
|
||||
testcase("Test emitted txn ledger entry");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace hook;
|
||||
using namespace hook_api;
|
||||
|
||||
auto const alice = Account{"alice"};
|
||||
|
||||
for (bool const withFix : {true, false})
|
||||
{
|
||||
Env env{*this, withFix ? features : features - fix20261005};
|
||||
|
||||
// emissions are only written into non-open views, so apply on
|
||||
// top of the last closed ledger the way consensus does
|
||||
auto const closed = env.closed();
|
||||
OpenView ov{&*closed};
|
||||
BEAST_EXPECT(!ov.open());
|
||||
|
||||
STTx const invokeTx = STTx(ttINVOKE, [&](STObject& obj) {});
|
||||
ApplyContext applyCtx{
|
||||
env.app(),
|
||||
ov,
|
||||
invokeTx,
|
||||
tesSUCCESS,
|
||||
env.current()->fees().base,
|
||||
tapNONE,
|
||||
env.journal};
|
||||
|
||||
STTx const emitTx = STTx(ttINVOKE, [&](STObject& obj) {
|
||||
obj[sfAccount] = alice.id();
|
||||
obj[sfSequence] = 0;
|
||||
obj[sfSigningPubKey] = Slice{};
|
||||
obj[sfFirstLedgerSequence] = closed->seq() + 1;
|
||||
obj[sfLastLedgerSequence] = closed->seq() + 5;
|
||||
obj[sfFee] = closed->fees().base;
|
||||
|
||||
auto& emitDetails = obj.peekFieldObject(sfEmitDetails);
|
||||
emitDetails[sfEmitGeneration] = 1;
|
||||
emitDetails[sfEmitBurden] = 1;
|
||||
emitDetails[sfEmitParentTxnID] = invokeTx.getTransactionID();
|
||||
emitDetails[sfEmitNonce] = uint256();
|
||||
emitDetails[sfEmitHookHash] = uint256();
|
||||
});
|
||||
|
||||
std::string reason;
|
||||
auto const emitted = std::make_shared<ripple::Transaction>(
|
||||
std::make_shared<STTx const>(emitTx), reason, env.app());
|
||||
std::queue<std::shared_ptr<ripple::Transaction>> emittedTxn;
|
||||
emittedTxn.push(emitted);
|
||||
|
||||
auto hookCtx = makeStubHookContext(
|
||||
applyCtx,
|
||||
alice.id(),
|
||||
alice.id(),
|
||||
{.result = {.emittedTxn = emittedTxn}});
|
||||
|
||||
BEAST_EXPECT(
|
||||
hook::finalizeHookResult(hookCtx.result, applyCtx, true) ==
|
||||
tesSUCCESS);
|
||||
|
||||
auto const kl = keylet::emittedTxn(emitted->getID());
|
||||
auto const sle = applyCtx.view().peek(kl);
|
||||
if (!BEAST_EXPECT(sle))
|
||||
continue;
|
||||
|
||||
Serializer expected;
|
||||
emitTx.add(expected);
|
||||
|
||||
// in memory: with the fix sfEmittedTxn occupies its template
|
||||
// slot exactly once, without it a duplicate is appended behind
|
||||
// the (non-present) template slot and the field reads absent
|
||||
auto const entries =
|
||||
std::count_if(sle->begin(), sle->end(), [](STBase const& f) {
|
||||
return f.getFName() == sfEmittedTxn;
|
||||
});
|
||||
BEAST_EXPECT(entries == (withFix ? 1 : 2));
|
||||
BEAST_EXPECT(sle->isFieldPresent(sfEmittedTxn) == withFix);
|
||||
|
||||
// serialized: identical either way, re-reading the entry always
|
||||
// yields exactly the emitted transaction
|
||||
{
|
||||
Serializer s;
|
||||
sle->add(s);
|
||||
SerialIter sit(s.slice());
|
||||
STLedgerEntry const reread(sit, kl.key);
|
||||
BEAST_EXPECT(reread.isFieldPresent(sfEmittedTxn));
|
||||
Serializer txn;
|
||||
reread.peekAtField(sfEmittedTxn).add(txn);
|
||||
BEAST_EXPECT(txn.peekData() == expected.peekData());
|
||||
}
|
||||
|
||||
// a hook running later in the same ledger can tell them apart:
|
||||
// slot_subfield only finds sfEmittedTxn with the fix
|
||||
auto readerCtx =
|
||||
makeStubHookContext(applyCtx, alice.id(), alice.id(), {});
|
||||
auto& api = readerCtx.api();
|
||||
|
||||
Bytes klBytes{
|
||||
static_cast<uint8_t>((kl.type >> 8) & 0xFFU),
|
||||
static_cast<uint8_t>(kl.type & 0xFFU)};
|
||||
klBytes.insert(klBytes.end(), kl.key.begin(), kl.key.end());
|
||||
|
||||
auto const slotNo = api.slot_set(klBytes, 0);
|
||||
if (!BEAST_EXPECT(slotNo.has_value()))
|
||||
continue;
|
||||
|
||||
auto const sub =
|
||||
api.slot_subfield(slotNo.value(), sfEmittedTxn.getCode(), 0);
|
||||
if (!withFix)
|
||||
{
|
||||
BEAST_EXPECT(!sub.has_value() && sub.error() == DOESNT_EXIST);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!BEAST_EXPECT(sub.has_value()))
|
||||
continue;
|
||||
auto const field = api.slot(sub.value());
|
||||
if (!BEAST_EXPECT(field.has_value()))
|
||||
continue;
|
||||
Serializer s;
|
||||
field.value()->add(s);
|
||||
BEAST_EXPECT(s.peekData() == expected.peekData());
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
test_etxn_details(FeatureBitset features)
|
||||
{
|
||||
@@ -1828,11 +1956,31 @@ public:
|
||||
BEAST_EXPECT(api.float_set(-50, 0).value() == 0);
|
||||
BEAST_EXPECT(api.float_set(0, 0).value() == 0);
|
||||
|
||||
// an exponent lower than -96 should produce an invalid float error
|
||||
BEAST_EXPECT(api.float_set(-97, 1).error() == INVALID_FLOAT);
|
||||
// fix20261005: exponent out of range is reported as
|
||||
// EXPONENT_UNDERSIZED / EXPONENT_OVERSIZED; before the fix both
|
||||
// cases were reported as INVALID_FLOAT
|
||||
bool const fixExpCodes = env.current()->rules().enabled(fix20261005);
|
||||
auto const undersized =
|
||||
fixExpCodes ? EXPONENT_UNDERSIZED : INVALID_FLOAT;
|
||||
auto const oversized = fixExpCodes ? EXPONENT_OVERSIZED : INVALID_FLOAT;
|
||||
|
||||
// an exponent larger than +96 should produce an invalid float error
|
||||
BEAST_EXPECT(api.float_set(+97, 1).error() == INVALID_FLOAT);
|
||||
// an exponent lower than -96 should produce an underflow error
|
||||
BEAST_EXPECT(api.float_set(-97, 1).error() == undersized);
|
||||
|
||||
// an exponent larger than +96 should produce an overflow error
|
||||
BEAST_EXPECT(api.float_set(+97, 1).error() == oversized);
|
||||
|
||||
// the -96..80 range applies to the exponent after the mantissa has
|
||||
// been normalized to 16 digits (1 -> 1e15, exponent - 15)
|
||||
auto const oneE15 = [](int32_t exp) {
|
||||
return hook::hook_float::make_float(1000000000000000ULL, exp, false)
|
||||
.value();
|
||||
};
|
||||
BEAST_EXPECT(api.float_set(81, 1).value() == oneE15(66));
|
||||
BEAST_EXPECT(api.float_set(95, 1).value() == oneE15(80));
|
||||
BEAST_EXPECT(api.float_set(96, 1).error() == oversized);
|
||||
BEAST_EXPECT(api.float_set(-81, 1).value() == oneE15(-96));
|
||||
BEAST_EXPECT(api.float_set(-82, 1).error() == undersized);
|
||||
|
||||
// clang-format off
|
||||
std::vector<std::tuple<int32_t, int64_t, uint64_t>> tests = {
|
||||
@@ -4324,8 +4472,20 @@ public:
|
||||
makeStubHookContext(applyCtx, alice.id(), alice.id(), {});
|
||||
auto& api = hookCtx.api();
|
||||
|
||||
BEAST_EXPECT(api.sto_validate(Bytes{}).error() == TOO_SMALL);
|
||||
BEAST_EXPECT(api.sto_validate(Bytes{0x00}).error() == TOO_SMALL);
|
||||
// a buffer shorter than two bytes can't be a valid STObject:
|
||||
// fix20261005 reports it as invalid rather than as TOO_SMALL
|
||||
if (env.closed()->rules().enabled(fix20261005))
|
||||
{
|
||||
BEAST_EXPECT(api.sto_validate(Bytes{}).value() == false);
|
||||
BEAST_EXPECT(api.sto_validate(Bytes{0x00}).value() == false);
|
||||
BEAST_EXPECT(api.sto_validate(Bytes{0xE1}).value() == false);
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(api.sto_validate(Bytes{}).error() == TOO_SMALL);
|
||||
BEAST_EXPECT(api.sto_validate(Bytes{0x00}).error() == TOO_SMALL);
|
||||
BEAST_EXPECT(api.sto_validate(Bytes{0xE1}).error() == TOO_SMALL);
|
||||
}
|
||||
|
||||
// { Memo: {MemoData: "BEEF"} }
|
||||
auto const memos = *strUnHex("EA7D02BEEFE1");
|
||||
@@ -4399,6 +4559,22 @@ public:
|
||||
BEAST_EXPECT(accid.has_value());
|
||||
auto aliceid = alice.id();
|
||||
BEAST_EXPECT(accid.value() == Bytes(aliceid.begin(), aliceid.end()));
|
||||
|
||||
// Bytes outside the base58 alphabet, including >= 0x80, must be
|
||||
// rejected wherever they appear (signed-char lookup regression)
|
||||
{
|
||||
std::string const addr = alice.human();
|
||||
for (std::size_t pos = 0; pos < addr.size(); ++pos)
|
||||
{
|
||||
for (int const b : {0x00, 0x7f, 0x80, 0xc2, 0xff})
|
||||
{
|
||||
std::string bad = addr;
|
||||
bad[pos] = static_cast<char>(b);
|
||||
auto const r = api.util_accid(bad);
|
||||
BEAST_EXPECT(!r && r.error() == INVALID_ARGUMENT);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -4768,6 +4944,7 @@ public:
|
||||
|
||||
test_prepare(features);
|
||||
test_emit(features);
|
||||
test_emitted_txn_entry(features);
|
||||
test_etxn_burden(features);
|
||||
test_etxn_generation(features);
|
||||
test_otxn_burden(features);
|
||||
@@ -4796,6 +4973,7 @@ public:
|
||||
test_float_one(features);
|
||||
test_float_root(features);
|
||||
test_float_set(features);
|
||||
test_float_set(features - fix20261005);
|
||||
test_float_sign(features);
|
||||
test_float_sto(features);
|
||||
test_float_sto_set(features);
|
||||
@@ -4847,6 +5025,7 @@ public:
|
||||
test_sto_subfield(features);
|
||||
test_sto_subfield(features - fix20260929);
|
||||
test_sto_validate(features);
|
||||
test_sto_validate(features - fix20261005);
|
||||
|
||||
test_trace(features);
|
||||
test_trace_float(features);
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 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 <test/jtx/Env.h>
|
||||
#include <test/jtx/envconfig.h>
|
||||
#include <xrpld/app/main/LoadManager.h>
|
||||
#include <xrpld/app/misc/LoadFeeTrack.h>
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
#include <cstdint>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
/**
|
||||
* Unit tests for LoadFeeTrack fee adjustment mechanics.
|
||||
*
|
||||
* These tests document the expected per-second behaviour of
|
||||
* raiseLocalFee() and lowerLocalFee() as called by LoadManager::run().
|
||||
*
|
||||
* Background: prior to the fix in this PR, the fee adjustment block in
|
||||
* LoadManager::run() was placed *outside* the while loop, meaning
|
||||
* raiseLocalFee()/lowerLocalFee() fired only once on shutdown rather
|
||||
* than every second during normal operation. localTxnLoadFee_ (and thus
|
||||
* load_factor_local in server_info) was therefore never adjusted by
|
||||
* local job queue load during normal operation.
|
||||
*/
|
||||
class LoadManager_test : public beast::unit_test::suite
|
||||
{
|
||||
public:
|
||||
// The normal/minimum fee factor (kLftNormalFee in LoadFeeTrack)
|
||||
static constexpr std::uint32_t kNormalFee = 256;
|
||||
|
||||
void
|
||||
testRaiseHysteresis()
|
||||
{
|
||||
testcase("raiseLocalFee requires two consecutive calls");
|
||||
|
||||
LoadFeeTrack track;
|
||||
|
||||
// First call: raiseCount_ goes from 0 to 1, returns false (hysteresis)
|
||||
BEAST_EXPECT(!track.raiseLocalFee());
|
||||
BEAST_EXPECT(track.getLocalFee() == kNormalFee);
|
||||
|
||||
// Second call: raiseCount_ reaches 2, fee is raised
|
||||
BEAST_EXPECT(track.raiseLocalFee());
|
||||
BEAST_EXPECT(track.getLocalFee() > kNormalFee);
|
||||
}
|
||||
|
||||
void
|
||||
testLowerDecaysToBaseline()
|
||||
{
|
||||
testcase("lowerLocalFee decays elevated fee back to baseline");
|
||||
|
||||
LoadFeeTrack track;
|
||||
|
||||
// Raise the fee above baseline (requires two calls due to hysteresis)
|
||||
track.raiseLocalFee();
|
||||
track.raiseLocalFee();
|
||||
auto const elevated = track.getLocalFee();
|
||||
BEAST_EXPECT(elevated > kNormalFee);
|
||||
|
||||
// lowerLocalFee() should decay it each call
|
||||
// Each call reduces by 1/kLftFeeDecFraction (1/4), so a few
|
||||
// iterations should bring it back to kNormalFee
|
||||
bool changed = false;
|
||||
for (int i = 0; i < 100; ++i)
|
||||
{
|
||||
changed |= track.lowerLocalFee();
|
||||
if (track.getLocalFee() == kNormalFee)
|
||||
break;
|
||||
}
|
||||
|
||||
BEAST_EXPECT(changed);
|
||||
BEAST_EXPECT(track.getLocalFee() == kNormalFee);
|
||||
}
|
||||
|
||||
void
|
||||
testLowerAtBaselineIsNoop()
|
||||
{
|
||||
testcase("lowerLocalFee at baseline returns false");
|
||||
|
||||
LoadFeeTrack track;
|
||||
|
||||
// Already at baseline — should return false (no change)
|
||||
BEAST_EXPECT(!track.lowerLocalFee());
|
||||
BEAST_EXPECT(track.getLocalFee() == kNormalFee);
|
||||
}
|
||||
|
||||
void
|
||||
testRaiseResetsOnLower()
|
||||
{
|
||||
testcase("lowerLocalFee resets raiseCount");
|
||||
|
||||
LoadFeeTrack track;
|
||||
|
||||
// One raise call (hysteresis: count=1, no change yet)
|
||||
BEAST_EXPECT(!track.raiseLocalFee());
|
||||
|
||||
// Lower resets raiseCount_ to 0
|
||||
track.lowerLocalFee();
|
||||
|
||||
// Next raise call starts from 0 again — still needs two calls
|
||||
BEAST_EXPECT(!track.raiseLocalFee());
|
||||
BEAST_EXPECT(track.getLocalFee() == kNormalFee);
|
||||
}
|
||||
|
||||
void
|
||||
testIsLoadedLocal()
|
||||
{
|
||||
testcase("isLoadedLocal reflects fee state correctly");
|
||||
|
||||
LoadFeeTrack track;
|
||||
|
||||
// At baseline, not loaded
|
||||
BEAST_EXPECT(!track.isLoadedLocal());
|
||||
|
||||
// After one raise (hysteresis, fee unchanged) — raiseCount_ != 0
|
||||
track.raiseLocalFee();
|
||||
BEAST_EXPECT(track.isLoadedLocal());
|
||||
|
||||
// After lower, raiseCount_ reset and fee back at baseline
|
||||
track.lowerLocalFee();
|
||||
BEAST_EXPECT(!track.isLoadedLocal());
|
||||
}
|
||||
|
||||
void
|
||||
testLoadManagerLoop()
|
||||
{
|
||||
testcase("LoadManager loop raises and decays load_factor_local");
|
||||
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env(*this, envconfig());
|
||||
|
||||
// Stop the real LoadManager thread so we can drive the fee track
|
||||
// manually without races. On stock code the fee adjustment block
|
||||
// is outside the while loop and never fires during normal operation,
|
||||
// so getFeeTrack().getLocalFee() stays at kNormalFee (256) regardless.
|
||||
env.app().getLoadManager().stop();
|
||||
|
||||
auto& feeTrack = env.app().getFeeTrack();
|
||||
|
||||
// Confirm baseline
|
||||
BEAST_EXPECT(feeTrack.getLocalFee() == kNormalFee);
|
||||
BEAST_EXPECT(!feeTrack.isLoadedLocal());
|
||||
|
||||
// raiseLocalFee() requires two consecutive calls (hysteresis guard).
|
||||
// This is the exact sequence LoadManager::run() executes each tick
|
||||
// when the job queue is overloaded.
|
||||
BEAST_EXPECT(!feeTrack.raiseLocalFee()); // tick 1: count=1, no change
|
||||
BEAST_EXPECT(feeTrack.raiseLocalFee()); // tick 2: count=2, fee raised
|
||||
BEAST_EXPECT(feeTrack.getLocalFee() > kNormalFee);
|
||||
BEAST_EXPECT(feeTrack.isLoadedLocal());
|
||||
|
||||
// Now simulate the LoadManager loop calling lowerLocalFee() each tick
|
||||
// until the fee decays back to baseline. On stock code this block
|
||||
// never runs during normal operation so the fee would stay elevated.
|
||||
bool decayed = false;
|
||||
for (int i = 0; i < 100; ++i)
|
||||
{
|
||||
feeTrack.lowerLocalFee();
|
||||
if (feeTrack.getLocalFee() == kNormalFee)
|
||||
{
|
||||
decayed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
BEAST_EXPECT(decayed);
|
||||
BEAST_EXPECT(feeTrack.getLocalFee() == kNormalFee);
|
||||
BEAST_EXPECT(!feeTrack.isLoadedLocal());
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testRaiseHysteresis();
|
||||
testLowerDecaysToBaseline();
|
||||
testLowerAtBaselineIsNoop();
|
||||
testRaiseResetsOnLower();
|
||||
testIsLoadedLocal();
|
||||
testLoadManagerLoop();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(LoadManager, app, ripple);
|
||||
|
||||
} // namespace ripple
|
||||
@@ -26,11 +26,17 @@
|
||||
#include <xrpld/app/misc/detail/OnlineDeleteRanges.h>
|
||||
#include <xrpld/app/rdb/backend/SQLiteDatabase.h>
|
||||
#include <xrpld/core/ConfigSections.h>
|
||||
#include <xrpld/nodestore/NodeObject.h>
|
||||
#include <xrpld/nodestore/detail/DatabaseRotatingImp.h>
|
||||
#include <xrpl/beast/utility/temp_dir.h>
|
||||
#include <xrpl/protocol/digest.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
#include <exception>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
@@ -47,6 +53,19 @@ class SHAMapStore_test : public beast::unit_test::suite
|
||||
return cfg;
|
||||
}
|
||||
|
||||
// Same as onlineDelete, but on a node store that persists nodes, so
|
||||
// SHAMapStoreImp builds DatabaseRotatingImp and really rotates.
|
||||
// envconfig defaults to type=rwdb, which is the null node store.
|
||||
static auto
|
||||
onlineDeleteStoring(std::unique_ptr<Config> cfg, std::string const& path)
|
||||
{
|
||||
cfg = onlineDelete(std::move(cfg));
|
||||
auto& section = cfg->section(ConfigSection::nodeDatabase());
|
||||
section.set("type", "memory");
|
||||
section.set("path", path);
|
||||
return cfg;
|
||||
}
|
||||
|
||||
static auto
|
||||
advisoryDelete(std::unique_ptr<Config> cfg)
|
||||
{
|
||||
@@ -308,13 +327,23 @@ public:
|
||||
}
|
||||
|
||||
void
|
||||
testAutomatic()
|
||||
testAutomatic(bool storing)
|
||||
{
|
||||
testcase("automatic online_delete");
|
||||
testcase(
|
||||
storing ? "automatic online_delete (rotating node store)"
|
||||
: "automatic online_delete (null node store)");
|
||||
using namespace jtx;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
Env env(*this, envconfig(onlineDelete));
|
||||
beast::temp_dir nodeDir;
|
||||
Env env(
|
||||
*this,
|
||||
storing ? envconfig(onlineDeleteStoring, nodeDir.path())
|
||||
: envconfig(onlineDelete));
|
||||
BEAST_EXPECT(
|
||||
storing ==
|
||||
(dynamic_cast<NodeStore::DatabaseRotating*>(
|
||||
&env.app().getNodeStore()) != nullptr));
|
||||
auto& store = env.app().getSHAMapStore();
|
||||
|
||||
auto ledgerSeq = waitForReady(env);
|
||||
@@ -572,6 +601,412 @@ public:
|
||||
return backend;
|
||||
}
|
||||
|
||||
std::unique_ptr<NodeStore::Backend>
|
||||
makeMemoryBackend(
|
||||
jtx::Env& env,
|
||||
NodeStoreScheduler& scheduler,
|
||||
std::string const& name)
|
||||
{
|
||||
Section section;
|
||||
section.set("type", "memory");
|
||||
section.set("path", name);
|
||||
auto backend = NodeStore::Manager::instance().make_Backend(
|
||||
section,
|
||||
megabytes(env.app().config().getValueFor(
|
||||
SizedItem::burstSize, std::nullopt)),
|
||||
scheduler,
|
||||
env.app().logs().journal("NodeStoreTest"));
|
||||
backend->open();
|
||||
return backend;
|
||||
}
|
||||
|
||||
// Ordinary fetch (duplicate == false) must copy an archived object
|
||||
// into the writable backend. The following rotation deletes the
|
||||
// archive. Without that copy, a transaction node read between
|
||||
// rotations disappears while callers can still name it.
|
||||
void
|
||||
testArchiveReadSurvivesRotation()
|
||||
{
|
||||
testcase("archive read survives rotation");
|
||||
|
||||
using namespace jtx;
|
||||
Env env(*this, envconfig(onlineDelete));
|
||||
NodeStoreScheduler scheduler(env.app().getJobQueue());
|
||||
|
||||
Section nscfg;
|
||||
nscfg.set("type", "memory");
|
||||
nscfg.set("path", "rotating");
|
||||
|
||||
auto dbr = std::make_unique<NodeStore::DatabaseRotatingImp>(
|
||||
env.app(),
|
||||
scheduler,
|
||||
1,
|
||||
makeMemoryBackend(env, scheduler, "writable"),
|
||||
makeMemoryBackend(env, scheduler, "archive"),
|
||||
nscfg,
|
||||
env.app().logs().journal("NodeStoreTest"));
|
||||
|
||||
Blob const expected(32, 0xab);
|
||||
auto const hash = sha512Half(makeSlice(expected));
|
||||
{
|
||||
Blob data = expected;
|
||||
dbr->store(hotTRANSACTION_NODE, std::move(data), hash, 1);
|
||||
}
|
||||
|
||||
auto const matches = [&](std::shared_ptr<NodeObject> const& object) {
|
||||
return object && object->getType() == hotTRANSACTION_NODE &&
|
||||
object->getData() == expected;
|
||||
};
|
||||
|
||||
// Public fetch defaults duplicate to false. The private override
|
||||
// is the rotation copy pass.
|
||||
NodeStore::Database& db = *dbr;
|
||||
BEAST_EXPECT(matches(db.fetchNodeObject(hash)));
|
||||
|
||||
auto const nop = [](std::string const&, std::string const&) {};
|
||||
// Writable becomes the archive. The object now lives only there.
|
||||
dbr->rotate(makeMemoryBackend(env, scheduler, "writable-2"), nop);
|
||||
|
||||
BEAST_EXPECT(matches(db.fetchNodeObject(hash)));
|
||||
|
||||
dbr->rotate(makeMemoryBackend(env, scheduler, "writable-3"), nop);
|
||||
BEAST_EXPECT(matches(db.fetchNodeObject(hash)));
|
||||
}
|
||||
|
||||
// getWriteLoad, sync, storeLedger, importDatabase and for_each changed
|
||||
// lock type. In null mode no Env builds a DatabaseRotatingImp, so
|
||||
// drive them directly.
|
||||
void
|
||||
testRotatingAccessors()
|
||||
{
|
||||
testcase("rotating database accessors");
|
||||
|
||||
using namespace jtx;
|
||||
// Source ledger must come from a node store that holds its nodes.
|
||||
Env env(*this, envconfig([](std::unique_ptr<Config> cfg) {
|
||||
auto& section = cfg->section(ConfigSection::nodeDatabase());
|
||||
section.set("type", "memory");
|
||||
section.set("path", "rotating-accessors-app");
|
||||
return cfg;
|
||||
}));
|
||||
env.fund(XRP(10000), Account{"alice"});
|
||||
env.close();
|
||||
|
||||
NodeStoreScheduler scheduler(env.app().getJobQueue());
|
||||
Section nscfg;
|
||||
nscfg.set("type", "memory");
|
||||
nscfg.set("path", "rotating-accessors");
|
||||
auto makeDb = [&](std::string const& prefix) {
|
||||
return std::make_unique<NodeStore::DatabaseRotatingImp>(
|
||||
env.app(),
|
||||
scheduler,
|
||||
1,
|
||||
makeMemoryBackend(env, scheduler, prefix + "-writable"),
|
||||
makeMemoryBackend(env, scheduler, prefix + "-archive"),
|
||||
nscfg,
|
||||
env.app().logs().journal("NodeStoreTest"));
|
||||
};
|
||||
|
||||
auto src = makeDb("rotating-accessors-src");
|
||||
BEAST_EXPECT(src->getName() == "rotating-accessors-src-writable");
|
||||
BEAST_EXPECT(src->getWriteLoad() >= 0);
|
||||
src->sync();
|
||||
|
||||
auto const ledger = env.app().getLedgerMaster().getClosedLedger();
|
||||
if (!BEAST_EXPECT(ledger))
|
||||
return;
|
||||
BEAST_EXPECT(src->storeLedger(ledger));
|
||||
|
||||
NodeStore::Database& srcDb = *src;
|
||||
auto const stateRoot = ledger->stateMap().getHash().as_uint256();
|
||||
BEAST_EXPECT(srcDb.fetchNodeObject(ledger->info().hash));
|
||||
BEAST_EXPECT(srcDb.fetchNodeObject(stateRoot));
|
||||
|
||||
// importDatabase walks the source with for_each.
|
||||
auto dst = makeDb("rotating-accessors-dst");
|
||||
dst->importDatabase(*src);
|
||||
NodeStore::Database& dstDb = *dst;
|
||||
BEAST_EXPECT(dstDb.fetchNodeObject(ledger->info().hash));
|
||||
BEAST_EXPECT(dstDb.fetchNodeObject(stateRoot));
|
||||
}
|
||||
|
||||
static auto
|
||||
nullBackend(std::unique_ptr<Config> cfg)
|
||||
{
|
||||
cfg->LEDGER_HISTORY = 8;
|
||||
auto& section = cfg->section(ConfigSection::nodeDatabase());
|
||||
section.set("type", "rwdb");
|
||||
section.set("path", "main");
|
||||
return cfg;
|
||||
}
|
||||
|
||||
static auto
|
||||
nullBackendFullHistory(std::unique_ptr<Config> cfg)
|
||||
{
|
||||
cfg = nullBackend(std::move(cfg));
|
||||
cfg->LEDGER_HISTORY = std::numeric_limits<std::uint32_t>::max();
|
||||
return cfg;
|
||||
}
|
||||
|
||||
static auto
|
||||
nullBackendDeleteOff(std::unique_ptr<Config> cfg)
|
||||
{
|
||||
cfg = nullBackend(std::move(cfg));
|
||||
cfg->LEDGER_HISTORY = 256;
|
||||
cfg->section(ConfigSection::nodeDatabase()).set("online_delete", "0");
|
||||
return cfg;
|
||||
}
|
||||
|
||||
// Payments keep applying after more closes than ledger_history,
|
||||
// with no node-store record of the state tree. Close time comes
|
||||
// from the resident ledger, not a node-store header walk.
|
||||
void
|
||||
testNullModeLedgerProgression()
|
||||
{
|
||||
testcase("null mode ledger progression");
|
||||
|
||||
using namespace jtx;
|
||||
Env env(*this, envconfig(nullBackend));
|
||||
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
env.fund(XRP(10000), alice, bob);
|
||||
env.close();
|
||||
|
||||
auto const bobBefore = env.balance(bob);
|
||||
constexpr int payments = 12;
|
||||
for (int i = 0; i < payments; ++i)
|
||||
{
|
||||
env(pay(alice, bob, XRP(1)));
|
||||
env.close();
|
||||
}
|
||||
|
||||
BEAST_EXPECT(env.balance(bob) == bobBefore + XRP(payments));
|
||||
BEAST_EXPECT(env.seq(alice) > 1);
|
||||
|
||||
auto const info = env.rpc(
|
||||
"json",
|
||||
"account_info",
|
||||
std::string{"{\"account\": \""} + alice.human() + "\"}");
|
||||
BEAST_EXPECT(
|
||||
info[jss::result].isMember(jss::account_data) &&
|
||||
info[jss::result][jss::account_data][jss::Account] ==
|
||||
alice.human());
|
||||
|
||||
auto const ledger = env.app().getLedgerMaster().getClosedLedger();
|
||||
if (!BEAST_EXPECT(ledger))
|
||||
return;
|
||||
BEAST_EXPECT(ledger->read(keylet::account(alice.id())));
|
||||
|
||||
auto const closeTime = env.app().getLedgerMaster().getCloseTimeByHash(
|
||||
ledger->info().hash, ledger->info().seq);
|
||||
BEAST_EXPECT(closeTime && *closeTime == ledger->info().closeTime);
|
||||
|
||||
auto const validated = env.rpc("ledger", "validated");
|
||||
BEAST_EXPECT(goodLedger(
|
||||
env, validated, std::to_string(ledger->info().seq), true));
|
||||
}
|
||||
|
||||
// ledger_history=full would make the retained-ledger window grow
|
||||
// without bound, since nothing else can drop a null-mode tree.
|
||||
void
|
||||
testNullModeRejectsFullHistory()
|
||||
{
|
||||
testcase("RWDB null mode rejects ledger_history=full");
|
||||
|
||||
using namespace jtx;
|
||||
try
|
||||
{
|
||||
Env env(*this, envconfig(nullBackendFullHistory));
|
||||
fail("Env should throw when ledger_history is full");
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
std::string(e.what()).find("ledger_history") !=
|
||||
std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
// online_delete=0 is an explicit disable. It must not be treated
|
||||
// as a missing key and replaced with ledger_history.
|
||||
void
|
||||
testExplicitOnlineDeleteZero()
|
||||
{
|
||||
testcase("explicit online_delete zero stays off");
|
||||
|
||||
using namespace jtx;
|
||||
Env env(*this, envconfig(nullBackendDeleteOff));
|
||||
|
||||
Account const alice{"alice"};
|
||||
env.fund(XRP(10000), alice);
|
||||
env.close();
|
||||
auto const balance = env.balance(alice);
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.app().getSHAMapStore().getLastRotated() == 0);
|
||||
BEAST_EXPECT(env.balance(alice) == balance);
|
||||
}
|
||||
|
||||
void
|
||||
testNullModeRequiresHistory()
|
||||
{
|
||||
testcase("RWDB null mode requires ledger_history > 0");
|
||||
|
||||
using namespace jtx;
|
||||
try
|
||||
{
|
||||
Env env(*this, envconfig([](std::unique_ptr<Config> cfg) {
|
||||
cfg->LEDGER_HISTORY = 0;
|
||||
auto& section = cfg->section(ConfigSection::nodeDatabase());
|
||||
section.set("type", "rwdb");
|
||||
section.set("path", "main");
|
||||
return cfg;
|
||||
}));
|
||||
fail("Env should throw when ledger_history is 0");
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
std::string(e.what()).find("ledger_history") !=
|
||||
std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testRetainedLedgerCloseTime()
|
||||
{
|
||||
testcase("retained ledger close time survives later closes");
|
||||
|
||||
using namespace jtx;
|
||||
Env env(*this, envconfig(nullBackend));
|
||||
|
||||
Account const alice{"alice"};
|
||||
env.fund(XRP(10000), alice);
|
||||
env.close();
|
||||
|
||||
auto const first = env.app().getLedgerMaster().getClosedLedger();
|
||||
if (!BEAST_EXPECT(first))
|
||||
return;
|
||||
auto const firstHash = first->info().hash;
|
||||
auto const firstSeq = first->info().seq;
|
||||
auto const firstClose = first->info().closeTime;
|
||||
|
||||
for (int i = 0; i < 12; ++i)
|
||||
{
|
||||
env(pay(alice, env.master, XRP(1)));
|
||||
env.close();
|
||||
}
|
||||
|
||||
auto const closeTime =
|
||||
env.app().getLedgerMaster().getCloseTimeByHash(firstHash, firstSeq);
|
||||
BEAST_EXPECT(closeTime && *closeTime == firstClose);
|
||||
BEAST_EXPECT(env.balance(alice) < XRP(10000));
|
||||
}
|
||||
|
||||
void
|
||||
testNullBackendIsPerConfig()
|
||||
{
|
||||
testcase("null backend flag is per configuration");
|
||||
|
||||
Config rwdbCfg;
|
||||
Config upperCfg;
|
||||
Config nudbCfg;
|
||||
Config emptyCfg;
|
||||
rwdbCfg.section(ConfigSection::nodeDatabase()).set("type", "rwdb");
|
||||
upperCfg.section(ConfigSection::nodeDatabase()).set("type", "RWDB");
|
||||
nudbCfg.section(ConfigSection::nodeDatabase()).set("type", "NuDB");
|
||||
|
||||
// Evaluated per Config object, not latched process-wide.
|
||||
BEAST_EXPECT(rwdbCfg.nullBackend());
|
||||
BEAST_EXPECT(upperCfg.nullBackend());
|
||||
BEAST_EXPECT(!nudbCfg.nullBackend());
|
||||
BEAST_EXPECT(!emptyCfg.nullBackend());
|
||||
BEAST_EXPECT(rwdbCfg.nullBackend());
|
||||
}
|
||||
|
||||
// Resident lookups are used with peer-supplied values. They must find
|
||||
// in-memory ledgers but never load or edit the complete-ledger set.
|
||||
void
|
||||
testResidentLedgerLookup()
|
||||
{
|
||||
testcase("resident ledger lookup does not load or clear");
|
||||
|
||||
using namespace jtx;
|
||||
Env env(*this, envconfig(nullBackend));
|
||||
|
||||
Account const alice{"alice"};
|
||||
env.fund(XRP(10000), alice);
|
||||
env.close();
|
||||
for (int i = 0; i < 4; ++i)
|
||||
env.close();
|
||||
|
||||
auto& lm = env.app().getLedgerMaster();
|
||||
auto const closed = lm.getClosedLedger();
|
||||
if (!BEAST_EXPECT(closed))
|
||||
return;
|
||||
|
||||
auto const complete = lm.getCompleteLedgers();
|
||||
|
||||
auto const bySeq = lm.getResidentLedgerBySeq(closed->info().seq);
|
||||
BEAST_EXPECT(bySeq && bySeq->info().hash == closed->info().hash);
|
||||
|
||||
auto const byHash = lm.getResidentLedgerByHash(closed->info().hash);
|
||||
BEAST_EXPECT(byHash && byHash->info().seq == closed->info().seq);
|
||||
|
||||
// A recent ledger still inside the retained window.
|
||||
auto const prior = lm.getResidentLedgerBySeq(closed->info().seq - 2);
|
||||
BEAST_EXPECT(prior && prior->info().seq == closed->info().seq - 2);
|
||||
|
||||
BEAST_EXPECT(!lm.getResidentLedgerByHash(uint256{12345}));
|
||||
BEAST_EXPECT(!lm.getResidentLedgerBySeq(closed->info().seq + 1000));
|
||||
|
||||
BEAST_EXPECT(lm.getCompleteLedgers() == complete);
|
||||
|
||||
// Drop every ledger from the history cache. Null mode must still
|
||||
// reach recent ledgers through the closed/validated holders and
|
||||
// the retained window, without loading anything.
|
||||
env.app().getJobQueue().rendezvous();
|
||||
auto const older = lm.getResidentLedgerBySeq(closed->info().seq - 3);
|
||||
if (!BEAST_EXPECT(older))
|
||||
return;
|
||||
lm.clearLedgerCachePrior(closed->info().seq + 1);
|
||||
|
||||
auto const closedAgain = lm.getResidentLedgerBySeq(closed->info().seq);
|
||||
BEAST_EXPECT(
|
||||
closedAgain && closedAgain->info().hash == closed->info().hash);
|
||||
auto const closedByHash =
|
||||
lm.getResidentLedgerByHash(closed->info().hash);
|
||||
BEAST_EXPECT(
|
||||
closedByHash && closedByHash->info().seq == closed->info().seq);
|
||||
|
||||
auto const retainedBySeq = lm.getResidentLedgerBySeq(older->info().seq);
|
||||
BEAST_EXPECT(
|
||||
retainedBySeq && retainedBySeq->info().hash == older->info().hash);
|
||||
auto const retainedByHash =
|
||||
lm.getResidentLedgerByHash(older->info().hash);
|
||||
BEAST_EXPECT(
|
||||
retainedByHash && retainedByHash->info().seq == older->info().seq);
|
||||
|
||||
BEAST_EXPECT(!lm.getResidentLedgerByHash(uint256{12345}));
|
||||
BEAST_EXPECT(lm.getCompleteLedgers() == complete);
|
||||
}
|
||||
|
||||
void
|
||||
testNullFactoryDropsWrites()
|
||||
{
|
||||
testcase("type=rwdb uses NullFactory and drops node-store writes");
|
||||
|
||||
using namespace jtx;
|
||||
Env env(*this, envconfig(nullBackend));
|
||||
|
||||
auto& db = env.app().getNodeStore();
|
||||
uint256 const hash{2};
|
||||
Blob data(32, 7);
|
||||
db.store(hotACCOUNT_NODE, std::move(data), hash, 1);
|
||||
BEAST_EXPECT(!db.fetchNodeObject(hash, 1));
|
||||
}
|
||||
|
||||
void
|
||||
testRotate()
|
||||
{
|
||||
@@ -852,8 +1287,19 @@ public:
|
||||
{
|
||||
testComputeOnlineDeleteTargets();
|
||||
testClear();
|
||||
testAutomatic();
|
||||
testAutomatic(false);
|
||||
testAutomatic(true);
|
||||
testRotatingAccessors();
|
||||
testCanDelete();
|
||||
testArchiveReadSurvivesRotation();
|
||||
testNullModeLedgerProgression();
|
||||
testNullModeRejectsFullHistory();
|
||||
testExplicitOnlineDeleteZero();
|
||||
testNullModeRequiresHistory();
|
||||
testRetainedLedgerCloseTime();
|
||||
testNullBackendIsPerConfig();
|
||||
testResidentLedgerLookup();
|
||||
testNullFactoryDropsWrites();
|
||||
testRotate();
|
||||
testPinnedRangeRestoreRequiresPinnedData();
|
||||
}
|
||||
|
||||
@@ -6482,7 +6482,10 @@ public:
|
||||
{
|
||||
testcase("Test float_set");
|
||||
using namespace jtx;
|
||||
Env env{*this, features};
|
||||
// this hook asserts the pre-fix20261005 INVALID_FLOAT result for an
|
||||
// out of range exponent; the amended EXPONENT_UNDERSIZED/OVERSIZED
|
||||
// behaviour is covered in HookAPI_test
|
||||
Env env{*this, features - fix20261005};
|
||||
|
||||
auto const alice = Account{"alice"};
|
||||
auto const bob = Account{"bob"};
|
||||
@@ -13262,7 +13265,9 @@ public:
|
||||
testcase("Test sto_validate");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
// this hook asserts the pre-fix20261005 TOO_SMALL result for a short
|
||||
// buffer; the amended behaviour is covered by the param hook below
|
||||
Env env{*this, features - fix20261005};
|
||||
|
||||
auto const bob = Account{"bob"};
|
||||
auto const alice = Account{"alice"};
|
||||
@@ -13518,6 +13523,45 @@ public:
|
||||
testSTI(
|
||||
"011A0000000000000000000000005553440000000000", hasEnabled);
|
||||
}
|
||||
|
||||
// a buffer shorter than two bytes can't be a valid STObject:
|
||||
// fix20261005 returns 0 (invalid) instead of TOO_SMALL
|
||||
for (bool const withFix : {true, false})
|
||||
{
|
||||
Env env{
|
||||
*this,
|
||||
withFix ? features | fix20261005 : features - fix20261005};
|
||||
|
||||
env.fund(XRP(10000), alice, bob);
|
||||
env.close();
|
||||
|
||||
// install the hook on alice
|
||||
env(ripple::test::jtx::hook(
|
||||
alice, {{hso(hook, overrideFlag)}}, 0),
|
||||
M("set sto_validate"),
|
||||
HSFEE);
|
||||
env.close();
|
||||
|
||||
auto payJv = pay(bob, alice, XRP(1));
|
||||
Json::Value params{Json::arrayValue};
|
||||
auto& param = params[0U][jss::HookParameter];
|
||||
param[jss::HookParameterName] = strHex(std::string("V"));
|
||||
param[jss::HookParameterValue] = "E1";
|
||||
payJv[jss::HookParameters] = params;
|
||||
|
||||
env(payJv, M("test sto_validate short buffer"), fee(XRP(1)));
|
||||
env.close();
|
||||
|
||||
auto const result =
|
||||
env.meta()->getFieldArray(sfHookExecutions)[0].getFieldU64(
|
||||
sfHookReturnCode);
|
||||
|
||||
// negative exit codes are recorded with the MSB set
|
||||
BEAST_EXPECT(
|
||||
result ==
|
||||
(withFix ? 0ULL
|
||||
: 0x8000000000000000ULL + 4ULL /*TOO_SMALL*/));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 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/ReaderPreferringSharedMutex.h>
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
class ReaderPreferringSharedMutex_test : public beast::unit_test::suite
|
||||
{
|
||||
void
|
||||
testExclusiveAndShared()
|
||||
{
|
||||
testcase("exclusive and shared locking");
|
||||
|
||||
ReaderPreferringSharedMutex mutex;
|
||||
|
||||
BEAST_EXPECT(mutex.try_lock());
|
||||
BEAST_EXPECT(!mutex.try_lock());
|
||||
BEAST_EXPECT(!mutex.try_lock_shared());
|
||||
mutex.unlock();
|
||||
|
||||
mutex.lock_shared();
|
||||
BEAST_EXPECT(mutex.try_lock_shared());
|
||||
BEAST_EXPECT(!mutex.try_lock());
|
||||
mutex.unlock_shared();
|
||||
mutex.unlock_shared();
|
||||
|
||||
{
|
||||
std::shared_lock shared{mutex};
|
||||
BEAST_EXPECT(!mutex.try_lock());
|
||||
BEAST_EXPECT(mutex.try_lock_shared());
|
||||
mutex.unlock_shared();
|
||||
}
|
||||
|
||||
{
|
||||
std::unique_lock exclusive{mutex};
|
||||
BEAST_EXPECT(!mutex.try_lock());
|
||||
BEAST_EXPECT(!mutex.try_lock_shared());
|
||||
}
|
||||
|
||||
mutex.lock();
|
||||
BEAST_EXPECT(!mutex.try_lock_shared());
|
||||
mutex.unlock();
|
||||
BEAST_EXPECT(mutex.try_lock_shared());
|
||||
mutex.unlock_shared();
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testExclusiveAndShared();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(ReaderPreferringSharedMutex, ripple_basics, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -59,6 +59,17 @@ public:
|
||||
testUnHexFailure("123X");
|
||||
testUnHexFailure("V");
|
||||
testUnHexFailure("XAH");
|
||||
|
||||
// Bytes >= 0x80 (negative when char is signed) must be rejected, not
|
||||
// used to index the lookup table out of bounds
|
||||
for (int b = 0x80; b <= 0xff; ++b)
|
||||
{
|
||||
std::string const c(1, static_cast<char>(b));
|
||||
testUnHexFailure(c);
|
||||
testUnHexFailure("0" + c);
|
||||
testUnHexFailure(c + "0");
|
||||
testUnHexFailure("00" + c + "00");
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -462,11 +462,102 @@ class base58_test : public beast::unit_test::suite
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testDecodeInvalidChars()
|
||||
{
|
||||
testcase("decode_invalid_chars");
|
||||
|
||||
// Every byte outside the base58 alphabet, including bytes >= 0x80
|
||||
// (negative when char is signed), must be rejected at any position.
|
||||
// The fast decoder reads the leading `size % 10` characters and the
|
||||
// remaining characters in two separate loops; varying the length and
|
||||
// the position of the bad byte exercises both.
|
||||
constexpr std::string_view alphabet =
|
||||
"rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz";
|
||||
std::array<bool, 256> inAlphabet{};
|
||||
for (unsigned char const c : alphabet)
|
||||
inAlphabet[c] = true;
|
||||
|
||||
auto const invalidCode =
|
||||
make_error_code(TokenCodecErrc::invalidEncodingChar);
|
||||
|
||||
// 52 is the longest input the fast decoder accepts
|
||||
for (std::size_t len = 1; len <= 52; ++len)
|
||||
{
|
||||
std::string base;
|
||||
for (std::size_t i = 0; i < len; ++i)
|
||||
base.push_back(alphabet[(i * 7 + len) % alphabet.size()]);
|
||||
|
||||
for (std::size_t pos = 0; pos < len; ++pos)
|
||||
{
|
||||
for (int b = 0; b < 256; ++b)
|
||||
{
|
||||
if (inAlphabet[b])
|
||||
continue;
|
||||
|
||||
std::string s = base;
|
||||
s[pos] = static_cast<char>(b);
|
||||
|
||||
std::array<std::uint8_t, 64> outBuf;
|
||||
auto const r = b58_fast::detail::b58_to_b256_be(s, outBuf);
|
||||
BEAST_EXPECT(!r && r.error() == invalidCode);
|
||||
BEAST_EXPECT(b58_ref::detail::decodeBase58(s).empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Same through the token API, using a valid AccountID as the base
|
||||
constexpr TokenType tokType = TokenType::AccountID;
|
||||
std::string const addr = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh";
|
||||
BEAST_EXPECT(!decodeBase58Token(addr, tokType).empty());
|
||||
for (std::size_t pos = 0; pos < addr.size(); ++pos)
|
||||
{
|
||||
for (int b = 0; b < 256; ++b)
|
||||
{
|
||||
if (inAlphabet[b])
|
||||
continue;
|
||||
|
||||
std::string s = addr;
|
||||
s[pos] = static_cast<char>(b);
|
||||
|
||||
std::array<std::uint8_t, 64> outBuf;
|
||||
auto const r = b58_fast::decodeBase58Token(tokType, s, outBuf);
|
||||
BEAST_EXPECT(!r && r.error() == invalidCode);
|
||||
BEAST_EXPECT(b58_ref::decodeBase58Token(s, tokType).empty());
|
||||
BEAST_EXPECT(decodeBase58Token(s, tokType).empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testDecodeMaxValue()
|
||||
{
|
||||
testcase("decode_max_value");
|
||||
|
||||
// The largest value of every accepted length ('z' is digit 57) must
|
||||
// decode identically in both implementations. At 52 chars this is the
|
||||
// widest intermediate the fast decoder's bigint ever has to hold.
|
||||
for (std::size_t len = 1; len <= 52; ++len)
|
||||
{
|
||||
std::string const s(len, 'z');
|
||||
|
||||
std::array<std::uint8_t, 64> outBuf;
|
||||
auto const r = b58_fast::detail::b58_to_b256_be(s, outBuf);
|
||||
std::string const ref = b58_ref::detail::decodeBase58(s);
|
||||
if (!BEAST_EXPECT(r))
|
||||
continue;
|
||||
BEAST_EXPECT(
|
||||
std::string(r.value().begin(), r.value().end()) == ref);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testMultiprecision();
|
||||
testFastMatchesRef();
|
||||
testDecodeInvalidChars();
|
||||
testDecodeMaxValue();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1203,6 +1203,96 @@ class UNLReportVoteNewValidator_test : public beast::unit_test::suite
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Regression test: UNLReport proposals must not be gated on the N-UNL local
|
||||
* reliability threshold.
|
||||
*
|
||||
* The local node (validator 0) validates exactly `myVals` of the 256 scored
|
||||
* ledgers, validator 1 validates none (so it is an N-UNL disable candidate and
|
||||
* absent from the report), every other validator validates all of them.
|
||||
*
|
||||
* == myVals <= 128: not active ourselves -> no UNLReport, no UNLModify
|
||||
* == 129 .. 229: UNLReport proposed, not reliable enough to vote on N-UNL
|
||||
* == >= 230: UNLReport and one UNLModify (230 used to fall into the
|
||||
* "Too many!" branch and fail)
|
||||
*/
|
||||
class UNLReportVoteLocalReliability_test : public beast::unit_test::suite
|
||||
{
|
||||
void
|
||||
testDoVoting()
|
||||
{
|
||||
testcase("Do Voting with local reliability below N-UNL threshold");
|
||||
|
||||
struct Case
|
||||
{
|
||||
std::uint32_t myVals;
|
||||
bool expectReport;
|
||||
bool expectModify;
|
||||
};
|
||||
|
||||
std::array<Case, 6> const cases = {{
|
||||
{128, false, false},
|
||||
{129, true, false},
|
||||
{229, true, false},
|
||||
{230, true, true},
|
||||
{231, true, true},
|
||||
{256, true, true},
|
||||
}};
|
||||
|
||||
for (bool const withVLImport : {true, false})
|
||||
{
|
||||
for (auto const& c : cases)
|
||||
{
|
||||
std::uint32_t const numNodes = 20;
|
||||
URNetworkHistory history = {
|
||||
*this, {numNodes, 0, false, false, withVLImport, {}}};
|
||||
BEAST_EXPECT(history.goodHistory);
|
||||
if (!history.goodHistory)
|
||||
continue;
|
||||
|
||||
// scored ledgers are [lastSeq - 256, lastSeq - 1]
|
||||
auto const lastSeq = history.lastLedger()->seq();
|
||||
history.walkHistoryAndAddValidations(
|
||||
[&](std::shared_ptr<Ledger const> const& l,
|
||||
std::size_t idx) -> bool {
|
||||
if (idx == 1)
|
||||
return false;
|
||||
if (idx != 0)
|
||||
return true;
|
||||
return l->seq() + 256 < lastSeq + c.myVals;
|
||||
});
|
||||
|
||||
NegativeUNLVote vote(
|
||||
history.UNLNodeIDs[0],
|
||||
history.env.journal,
|
||||
history.env.app());
|
||||
auto txSet = std::make_shared<SHAMap>(
|
||||
SHAMapType::TRANSACTION, history.env.app().getNodeFamily());
|
||||
vote.doVoting(
|
||||
history.lastLedger(),
|
||||
history.UNLKeySet,
|
||||
history.validations,
|
||||
txSet);
|
||||
|
||||
std::size_t const expectReport =
|
||||
c.expectReport ? numNodes - 1 + (withVLImport ? 1 : 0) : 0;
|
||||
std::size_t const expectModify = c.expectModify ? 1 : 0;
|
||||
std::string const ctx = "myVals=" + std::to_string(c.myVals) +
|
||||
" withVLImport=" + std::to_string(withVLImport);
|
||||
|
||||
BEAST_EXPECTS(countUNLRTx(txSet) == expectReport, ctx);
|
||||
BEAST_EXPECTS(unl::countTx(txSet) == expectModify, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testDoVoting();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(UNLReport, ledger, ripple);
|
||||
BEAST_DEFINE_TESTSUITE(UNLReportNoAmendment, ledger, ripple);
|
||||
BEAST_DEFINE_TESTSUITE(UNLReportFork, consensus, ripple);
|
||||
@@ -1215,6 +1305,7 @@ BEAST_DEFINE_TESTSUITE_PRIO(
|
||||
ripple,
|
||||
1);
|
||||
BEAST_DEFINE_TESTSUITE(UNLReportVoteNewValidator, consensus, ripple);
|
||||
BEAST_DEFINE_TESTSUITE(UNLReportVoteLocalReliability, consensus, ripple);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -1102,23 +1102,24 @@ trustthesevalidators.gov
|
||||
Config cfg;
|
||||
/* NOTE: this string includes some explicit
|
||||
* space chars in order to verify proper trimming */
|
||||
std::string toLoad(R"(
|
||||
std::string toLoad(
|
||||
R"(
|
||||
[port_rpc])"
|
||||
"\x20"
|
||||
R"(
|
||||
"\x20"
|
||||
R"(
|
||||
# comment
|
||||
# indented comment
|
||||
)"
|
||||
"\x20\x20"
|
||||
R"(
|
||||
"\x20\x20"
|
||||
R"(
|
||||
[ips])"
|
||||
"\x20"
|
||||
R"(
|
||||
"\x20"
|
||||
R"(
|
||||
r.ripple.com 51235
|
||||
|
||||
[ips_fixed])"
|
||||
"\x20\x20"
|
||||
R"(
|
||||
"\x20\x20"
|
||||
R"(
|
||||
# COMMENT
|
||||
s1.ripple.com 51235
|
||||
s2.ripple.com 51235
|
||||
@@ -1144,23 +1145,24 @@ r.ripple.com 51235
|
||||
Config cfg;
|
||||
/* NOTE: this string includes some explicit
|
||||
* space chars in order to verify proper trimming */
|
||||
std::string toLoad(R"(
|
||||
std::string toLoad(
|
||||
R"(
|
||||
[port_rpc])"
|
||||
"\x20"
|
||||
R"(
|
||||
"\x20"
|
||||
R"(
|
||||
# comment
|
||||
# indented comment
|
||||
)"
|
||||
"\x20\x20"
|
||||
R"(
|
||||
"\x20\x20"
|
||||
R"(
|
||||
[ips])"
|
||||
"\x20"
|
||||
R"(
|
||||
"\x20"
|
||||
R"(
|
||||
r.ripple.com:51235
|
||||
|
||||
[ips_fixed])"
|
||||
"\x20\x20"
|
||||
R"(
|
||||
"\x20\x20"
|
||||
R"(
|
||||
# COMMENT
|
||||
s1.ripple.com:51235
|
||||
s2.ripple.com 51235
|
||||
@@ -1447,8 +1449,8 @@ r.ripple.com:51235
|
||||
}
|
||||
}
|
||||
|
||||
// Test 2: RWDB without online_delete NOT in standalone mode (should
|
||||
// throw)
|
||||
// Test 2: RWDB without online_delete NOT in standalone mode
|
||||
// (now allowed — SHAMapStoreImp defaults it to ledger_history)
|
||||
{
|
||||
Config c;
|
||||
std::string toLoad =
|
||||
@@ -1459,16 +1461,12 @@ r.ripple.com:51235
|
||||
try
|
||||
{
|
||||
c.loadFromString(toLoad);
|
||||
fail("Expected exception for RWDB without online_delete");
|
||||
}
|
||||
catch (std::runtime_error const& e)
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
std::string(e.what()).find(
|
||||
"RWDB (in-memory backend) requires online_delete") !=
|
||||
std::string::npos);
|
||||
pass();
|
||||
}
|
||||
catch (std::runtime_error const&)
|
||||
{
|
||||
fail("Should not throw for RWDB without online_delete");
|
||||
}
|
||||
}
|
||||
|
||||
// Test 3: RWDB with online_delete NOT in standalone mode (should
|
||||
@@ -1613,7 +1611,33 @@ r.ripple.com:51235
|
||||
}
|
||||
}
|
||||
|
||||
// Test 5: pinned_type without online_delete (should throw)
|
||||
// Test 5: type=rwdb with pinned_type (should throw, even in
|
||||
// standalone). The rwdb node store is tree-only, so pinned data
|
||||
// would be accepted and then dropped.
|
||||
for (bool const standalone : {false, true})
|
||||
{
|
||||
Config c;
|
||||
std::string toLoad =
|
||||
"[node_db]\n"
|
||||
"type=rwdb\n"
|
||||
"online_delete=256\n"
|
||||
"pinned_type=NuDB\n"
|
||||
"pinned_path=pinned\n";
|
||||
c.setupControl(true, true, standalone);
|
||||
try
|
||||
{
|
||||
c.loadFromString(toLoad);
|
||||
fail("Expected exception for pinned_type with type=rwdb");
|
||||
}
|
||||
catch (std::runtime_error const& e)
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
std::string(e.what()).find("type=rwdb") !=
|
||||
std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
// Test 6: pinned_type without online_delete (should throw)
|
||||
{
|
||||
Config c;
|
||||
std::string toLoad =
|
||||
|
||||
@@ -82,6 +82,35 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
if (type == "rwdb")
|
||||
{
|
||||
std::unique_ptr<Backend> backend = Manager::instance().make_Backend(
|
||||
params, megabytes(4), scheduler, journal);
|
||||
backend->open();
|
||||
try
|
||||
{
|
||||
backend->open();
|
||||
fail("rwdb open twice should throw");
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
pass();
|
||||
}
|
||||
|
||||
storeBatch(*backend, batch);
|
||||
backend->close();
|
||||
BEAST_EXPECT(!backend->isOpen());
|
||||
|
||||
std::shared_ptr<NodeObject> fetched;
|
||||
BEAST_EXPECT(
|
||||
backend->fetch(batch.front()->getHash().data(), &fetched) ==
|
||||
notFound);
|
||||
backend->store(batch.front());
|
||||
int visited = 0;
|
||||
backend->for_each([&visited](auto const&) { ++visited; });
|
||||
BEAST_EXPECT(visited == 0);
|
||||
}
|
||||
|
||||
// rwdb backend does not keep table/data after close
|
||||
if (type != "rwdb")
|
||||
{
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include <xrpld/nodestore/detail/DatabasePinnedImp.h>
|
||||
#include <xrpl/basics/Buffer.h>
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/beast/utility/temp_dir.h>
|
||||
#include <xrpl/protocol/digest.h>
|
||||
|
||||
namespace ripple {
|
||||
@@ -33,13 +34,16 @@ namespace test {
|
||||
class DatabasePinned_test : public beast::unit_test::suite
|
||||
{
|
||||
// Build an Env whose nodestore is wired up as DatabasePinnedImp.
|
||||
// pinned_type=rwdb is allowed in standalone mode (the catalogue
|
||||
// tests do the same).
|
||||
// The rotating store must persist (type=memory). type=rwdb is
|
||||
// NullFactory and never builds DatabasePinnedImp. pinned_type=rwdb
|
||||
// is allowed in standalone mode.
|
||||
static std::unique_ptr<Config>
|
||||
pinnedEnvconfig()
|
||||
pinnedEnvconfig(std::string const& nodePath)
|
||||
{
|
||||
auto cfg = jtx::envconfig();
|
||||
auto& nodeDb = cfg->section(ConfigSection::nodeDatabase());
|
||||
nodeDb.set("type", "memory");
|
||||
nodeDb.set("path", nodePath);
|
||||
nodeDb.set("pinned_type", "rwdb");
|
||||
nodeDb.set("online_delete", "256");
|
||||
return cfg;
|
||||
@@ -84,7 +88,8 @@ class DatabasePinned_test : public beast::unit_test::suite
|
||||
testcase("store routing by NodeObjectType");
|
||||
|
||||
using namespace jtx;
|
||||
Env env{*this, pinnedEnvconfig()};
|
||||
beast::temp_dir td;
|
||||
Env env{*this, pinnedEnvconfig(td.path())};
|
||||
auto* db = getPinnedDb(env);
|
||||
if (!db)
|
||||
return;
|
||||
@@ -136,7 +141,8 @@ class DatabasePinned_test : public beast::unit_test::suite
|
||||
testcase("store/fetch round-trip across both backends");
|
||||
|
||||
using namespace jtx;
|
||||
Env env{*this, pinnedEnvconfig()};
|
||||
beast::temp_dir td;
|
||||
Env env{*this, pinnedEnvconfig(td.path())};
|
||||
auto& nodeStore = env.app().getNodeStore();
|
||||
|
||||
// Store one object of each type with distinct content so the
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 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 <test/jtx.h>
|
||||
#include <xrpld/core/ConfigSections.h>
|
||||
#include <xrpld/overlay/Compression.h>
|
||||
#include <xrpld/overlay/Message.h>
|
||||
#include <xrpld/overlay/detail/OverlayImpl.h>
|
||||
#include <xrpld/overlay/detail/PeerImp.h>
|
||||
#include <xrpld/shamap/SHAMapInnerNode.h>
|
||||
#include <xrpld/shamap/SHAMapNodeID.h>
|
||||
#include <xrpl/basics/make_SSLContext.h>
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
#include <xrpl/protocol/messages.h>
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
class GetObjectByHash_test : public beast::unit_test::suite
|
||||
{
|
||||
using socket_type = boost::asio::ip::tcp::socket;
|
||||
using middle_type = boost::beast::tcp_stream;
|
||||
using stream_type = boost::beast::ssl_stream<middle_type>;
|
||||
|
||||
class PeerTest : public PeerImp
|
||||
{
|
||||
public:
|
||||
std::vector<std::shared_ptr<Message>> sent;
|
||||
|
||||
PeerTest(
|
||||
Application& app,
|
||||
std::shared_ptr<PeerFinder::Slot> const& slot,
|
||||
http_request_type&& request,
|
||||
PublicKey const& publicKey,
|
||||
ProtocolVersion protocol,
|
||||
Resource::Consumer consumer,
|
||||
std::unique_ptr<GetObjectByHash_test::stream_type>&& stream_ptr,
|
||||
OverlayImpl& overlay)
|
||||
: PeerImp(
|
||||
app,
|
||||
1,
|
||||
slot,
|
||||
std::move(request),
|
||||
publicKey,
|
||||
protocol,
|
||||
consumer,
|
||||
std::move(stream_ptr),
|
||||
overlay)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
send(std::shared_ptr<Message> const& m) override
|
||||
{
|
||||
sent.push_back(m);
|
||||
}
|
||||
};
|
||||
|
||||
static auto
|
||||
nullBackend(std::unique_ptr<Config> cfg)
|
||||
{
|
||||
cfg->LEDGER_HISTORY = 8;
|
||||
auto& section = cfg->section(ConfigSection::nodeDatabase());
|
||||
section.set("type", "rwdb");
|
||||
section.set("path", "main");
|
||||
return cfg;
|
||||
}
|
||||
|
||||
std::shared_ptr<PeerTest>
|
||||
makePeer(jtx::Env& env)
|
||||
{
|
||||
auto& overlay = dynamic_cast<OverlayImpl&>(env.app().overlay());
|
||||
http_request_type request;
|
||||
auto stream_ptr = std::make_unique<stream_type>(
|
||||
socket_type(env.app().getIOContext()), *context_);
|
||||
beast::IP::Endpoint const local(
|
||||
boost::asio::ip::make_address("172.16.0.1"));
|
||||
beast::IP::Endpoint const remote(
|
||||
boost::asio::ip::make_address("172.16.0.2"));
|
||||
PublicKey const key(std::get<0>(randomKeyPair(KeyType::ed25519)));
|
||||
auto consumer = overlay.resourceManager().newInboundEndpoint(remote);
|
||||
auto slot = overlay.peerFinder().new_inbound_slot(local, remote);
|
||||
auto peer = std::make_shared<PeerTest>(
|
||||
env.app(),
|
||||
slot,
|
||||
std::move(request),
|
||||
key,
|
||||
ProtocolVersion{1, 7},
|
||||
consumer,
|
||||
std::move(stream_ptr),
|
||||
overlay);
|
||||
overlay.add_active(peer);
|
||||
return peer;
|
||||
}
|
||||
|
||||
std::optional<protocol::TMGetObjectByHash>
|
||||
parseReply(std::shared_ptr<Message> const& message)
|
||||
{
|
||||
if (!message)
|
||||
return {};
|
||||
auto const& buf = message->getBuffer(compression::Compressed::Off);
|
||||
if (buf.size() <= compression::headerBytes)
|
||||
return {};
|
||||
protocol::TMGetObjectByHash reply;
|
||||
if (!reply.ParseFromArray(
|
||||
buf.data() + compression::headerBytes,
|
||||
static_cast<int>(buf.size() - compression::headerBytes)))
|
||||
return {};
|
||||
return reply;
|
||||
}
|
||||
|
||||
std::shared_ptr<boost::asio::ssl::context> context_ = make_SSLContext("");
|
||||
|
||||
public:
|
||||
void
|
||||
testServesLinkedAndCachedNodes()
|
||||
{
|
||||
testcase(
|
||||
"GetObjectByHash serves cache, linked tree, and ledger header");
|
||||
|
||||
using namespace jtx;
|
||||
Env env(*this, envconfig(nullBackend));
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
env.fund(XRP(10000), alice, bob);
|
||||
env(pay(alice, bob, XRP(1)));
|
||||
env.close();
|
||||
|
||||
auto const ledger = env.app().getLedgerMaster().getClosedLedger();
|
||||
if (!BEAST_EXPECT(ledger))
|
||||
return;
|
||||
|
||||
auto const stateHash = ledger->stateMap().getHash().as_uint256();
|
||||
auto const txHash = ledger->txMap().getHash().as_uint256();
|
||||
auto const headerHash = ledger->info().hash;
|
||||
auto const seq = ledger->info().seq;
|
||||
auto const rootId = SHAMapNodeID{}.getRawString();
|
||||
|
||||
auto peer = makePeer(env);
|
||||
auto sendQuery = [&](auto&& fill) {
|
||||
peer->sent.clear();
|
||||
auto packet = std::make_shared<protocol::TMGetObjectByHash>();
|
||||
packet->set_query(true);
|
||||
fill(*packet);
|
||||
// Production dispatch resets fee_ in onMessageBegin before
|
||||
// onMessage. Calling onMessage alone leaves a moderate burden
|
||||
// charge in place and Debug XRPL_ASSERT fires on a later
|
||||
// malformed request with a smaller fee.
|
||||
auto const size = packet->ByteSizeLong();
|
||||
peer->onMessageBegin(
|
||||
protocol::mtGET_OBJECTS, packet, size, size, false);
|
||||
peer->onMessage(packet);
|
||||
peer->onMessageEnd(protocol::mtGET_OBJECTS, packet);
|
||||
};
|
||||
|
||||
// TreeNodeCache hit: hash only, no nodeid.
|
||||
sendQuery([&](protocol::TMGetObjectByHash& packet) {
|
||||
packet.set_type(protocol::TMGetObjectByHash::otSTATE_NODE);
|
||||
auto& obj = *packet.add_objects();
|
||||
obj.set_hash(stateHash.begin(), stateHash.size());
|
||||
obj.set_ledgerseq(seq);
|
||||
});
|
||||
if (auto const cached =
|
||||
env.app().getNodeFamily().getTreeNodeCache()->fetch(stateHash))
|
||||
{
|
||||
if (!BEAST_EXPECT(peer->sent.size() == 1))
|
||||
return;
|
||||
auto const reply = parseReply(peer->sent.front());
|
||||
if (!BEAST_EXPECT(reply && reply->objects_size() == 1))
|
||||
return;
|
||||
BEAST_EXPECT(uint256{reply->objects(0).hash()} == stateHash);
|
||||
}
|
||||
|
||||
// After dropping the interned cache, the same request needs a nodeid
|
||||
// so fetchLinkedTreeNode can walk the retained SHAMap.
|
||||
env.app().getNodeFamily().getTreeNodeCache()->reset();
|
||||
sendQuery([&](protocol::TMGetObjectByHash& packet) {
|
||||
packet.set_type(protocol::TMGetObjectByHash::otSTATE_NODE);
|
||||
auto& obj = *packet.add_objects();
|
||||
obj.set_hash(stateHash.begin(), stateHash.size());
|
||||
obj.set_ledgerseq(seq);
|
||||
});
|
||||
{
|
||||
if (!BEAST_EXPECT(peer->sent.size() == 1))
|
||||
return;
|
||||
auto const reply = parseReply(peer->sent.front());
|
||||
if (!BEAST_EXPECT(reply))
|
||||
return;
|
||||
BEAST_EXPECT(reply->objects_size() == 0);
|
||||
}
|
||||
|
||||
sendQuery([&](protocol::TMGetObjectByHash& packet) {
|
||||
packet.set_type(protocol::TMGetObjectByHash::otSTATE_NODE);
|
||||
auto& obj = *packet.add_objects();
|
||||
obj.set_hash(stateHash.begin(), stateHash.size());
|
||||
obj.set_nodeid(rootId);
|
||||
obj.set_ledgerseq(seq);
|
||||
});
|
||||
{
|
||||
if (!BEAST_EXPECT(peer->sent.size() == 1))
|
||||
return;
|
||||
auto const reply = parseReply(peer->sent.front());
|
||||
if (!BEAST_EXPECT(reply && reply->objects_size() == 1))
|
||||
return;
|
||||
BEAST_EXPECT(uint256{reply->objects(0).hash()} == stateHash);
|
||||
BEAST_EXPECT(reply->objects(0).index() == rootId);
|
||||
BEAST_EXPECT(reply->objects(0).ledgerseq() == seq);
|
||||
}
|
||||
|
||||
// Transaction map root, found after the state-map miss.
|
||||
sendQuery([&](protocol::TMGetObjectByHash& packet) {
|
||||
packet.set_type(protocol::TMGetObjectByHash::otTRANSACTION_NODE);
|
||||
auto& obj = *packet.add_objects();
|
||||
obj.set_hash(txHash.begin(), txHash.size());
|
||||
obj.set_nodeid(rootId);
|
||||
obj.set_ledgerseq(seq);
|
||||
});
|
||||
{
|
||||
if (!BEAST_EXPECT(peer->sent.size() == 1))
|
||||
return;
|
||||
auto const reply = parseReply(peer->sent.front());
|
||||
if (!BEAST_EXPECT(reply && reply->objects_size() == 1))
|
||||
return;
|
||||
BEAST_EXPECT(uint256{reply->objects(0).hash()} == txHash);
|
||||
}
|
||||
|
||||
// Linked walk finds the node, hash does not match, object omitted.
|
||||
uint256 const wrong{2};
|
||||
sendQuery([&](protocol::TMGetObjectByHash& packet) {
|
||||
packet.set_type(protocol::TMGetObjectByHash::otSTATE_NODE);
|
||||
auto& obj = *packet.add_objects();
|
||||
obj.set_hash(wrong.begin(), wrong.size());
|
||||
obj.set_nodeid(rootId);
|
||||
obj.set_ledgerseq(seq);
|
||||
});
|
||||
{
|
||||
if (!BEAST_EXPECT(peer->sent.size() == 1))
|
||||
return;
|
||||
auto const reply = parseReply(peer->sent.front());
|
||||
if (!BEAST_EXPECT(reply))
|
||||
return;
|
||||
BEAST_EXPECT(reply->objects_size() == 0);
|
||||
}
|
||||
|
||||
// Ledger header fallback when the node store and tree caches miss.
|
||||
sendQuery([&](protocol::TMGetObjectByHash& packet) {
|
||||
packet.set_type(protocol::TMGetObjectByHash::otLEDGER);
|
||||
auto& obj = *packet.add_objects();
|
||||
obj.set_hash(headerHash.begin(), headerHash.size());
|
||||
obj.set_ledgerseq(seq);
|
||||
});
|
||||
{
|
||||
if (!BEAST_EXPECT(peer->sent.size() == 1))
|
||||
return;
|
||||
auto const reply = parseReply(peer->sent.front());
|
||||
if (!BEAST_EXPECT(reply && reply->objects_size() == 1))
|
||||
return;
|
||||
BEAST_EXPECT(uint256{reply->objects(0).hash()} == headerHash);
|
||||
BEAST_EXPECT(!reply->objects(0).data().empty());
|
||||
}
|
||||
|
||||
// Malformed ledger hash is rejected before a reply is sent.
|
||||
sendQuery([&](protocol::TMGetObjectByHash& packet) {
|
||||
packet.set_type(protocol::TMGetObjectByHash::otLEDGER);
|
||||
packet.set_ledgerhash("short");
|
||||
auto& obj = *packet.add_objects();
|
||||
obj.set_hash(headerHash.begin(), headerHash.size());
|
||||
});
|
||||
BEAST_EXPECT(peer->sent.empty());
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testServesLinkedAndCachedNodes();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(GetObjectByHash, overlay, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -103,6 +103,35 @@ public:
|
||||
BEAST_EXPECT(fieldNames.insert(name).second);
|
||||
}
|
||||
}
|
||||
|
||||
// hook api, keylet types and return codes
|
||||
{
|
||||
auto const& hook = result[jss::result][jss::HOOK];
|
||||
BEAST_EXPECT(hook.isMember(jss::API));
|
||||
BEAST_EXPECT(hook.isMember(jss::KEYLET_TYPES));
|
||||
BEAST_EXPECT(hook.isMember(jss::RETURN_CODES));
|
||||
|
||||
auto const& accept = hook[jss::API]["accept"];
|
||||
BEAST_EXPECT(accept["return"] == "int64_t");
|
||||
BEAST_EXPECT(accept["params"].size() == 3);
|
||||
BEAST_EXPECT(accept["params"][0u][jss::type] == "uint32_t");
|
||||
BEAST_EXPECT(accept["params"][0u][jss::name] == "read_ptr");
|
||||
BEAST_EXPECT(accept["params"][2u][jss::type] == "int64_t");
|
||||
BEAST_EXPECT(accept["params"][2u][jss::name] == "error_code");
|
||||
BEAST_EXPECT(!accept.isMember("amendment"));
|
||||
|
||||
auto const& burden = hook[jss::API]["etxn_burden"];
|
||||
BEAST_EXPECT(burden["params"].size() == 0);
|
||||
|
||||
auto const& xpop = hook[jss::API]["xpop_slot"];
|
||||
BEAST_EXPECT(xpop["amendment"] == "HooksUpdate1");
|
||||
|
||||
BEAST_EXPECT(hook[jss::KEYLET_TYPES]["HOOK"] == 1);
|
||||
BEAST_EXPECT(hook[jss::KEYLET_TYPES]["MANIFEST"] == 37);
|
||||
BEAST_EXPECT(hook[jss::RETURN_CODES]["SUCCESS"] == 0);
|
||||
BEAST_EXPECT(hook[jss::RETURN_CODES]["INVALID_FLOAT"] == -10024);
|
||||
BEAST_EXPECT(hook[jss::RETURN_CODES]["TOO_MANY_NAMESPACES"] == -45);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -132,6 +161,7 @@ public:
|
||||
BEAST_EXPECT(
|
||||
!result[jss::result].isMember(jss::TRANSACTION_FLAGS_INDICES));
|
||||
BEAST_EXPECT(!result[jss::result].isMember(jss::TYPES));
|
||||
BEAST_EXPECT(!result[jss::result].isMember(jss::HOOK));
|
||||
BEAST_EXPECT(result[jss::result].isMember(jss::hash));
|
||||
}
|
||||
|
||||
@@ -156,6 +186,7 @@ public:
|
||||
BEAST_EXPECT(
|
||||
result[jss::result].isMember(jss::TRANSACTION_FLAGS_INDICES));
|
||||
BEAST_EXPECT(result[jss::result].isMember(jss::TYPES));
|
||||
BEAST_EXPECT(result[jss::result].isMember(jss::HOOK));
|
||||
BEAST_EXPECT(result[jss::result].isMember(jss::hash));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -847,6 +847,14 @@ public:
|
||||
Account alice("alice");
|
||||
Account bob("bob");
|
||||
Account carol("carol");
|
||||
|
||||
// Account history loads ledgers by seq. type=rwdb is tree-only
|
||||
// and cannot reconstruct those maps from the node store.
|
||||
auto storing = [] {
|
||||
auto cfg = envconfig();
|
||||
cfg->section(ConfigSection::nodeDatabase()).set("type", "memory");
|
||||
return cfg;
|
||||
};
|
||||
Account david("david");
|
||||
///////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -1015,7 +1023,7 @@ public:
|
||||
*
|
||||
* also test subscribe to the account before it is created
|
||||
*/
|
||||
Env env(*this);
|
||||
Env env(*this, storing());
|
||||
auto wscTxHistory = makeWSClient(env.app().config());
|
||||
Json::Value request;
|
||||
request[jss::account_history_tx_stream] = Json::objectValue;
|
||||
@@ -1061,7 +1069,7 @@ public:
|
||||
* subscribe genesis account tx history without txns
|
||||
* subscribe to bob's account after it is created
|
||||
*/
|
||||
Env env(*this);
|
||||
Env env(*this, storing());
|
||||
auto wscTxHistory = makeWSClient(env.app().config());
|
||||
Json::Value request;
|
||||
request[jss::account_history_tx_stream] = Json::objectValue;
|
||||
@@ -1140,7 +1148,7 @@ public:
|
||||
* subscribe account and subscribe account tx history
|
||||
* and compare txns streamed
|
||||
*/
|
||||
Env env(*this);
|
||||
Env env(*this, storing());
|
||||
auto wscAccount = makeWSClient(env.app().config());
|
||||
auto wscTxHistory = makeWSClient(env.app().config());
|
||||
|
||||
@@ -1211,7 +1219,7 @@ public:
|
||||
* alice issues USD to carol
|
||||
* mix USD and XRP payments
|
||||
*/
|
||||
Env env(*this);
|
||||
Env env(*this, storing());
|
||||
auto const USD_a = alice["USD"];
|
||||
|
||||
std::array<Account, 2> accounts = {alice, carol};
|
||||
@@ -1250,7 +1258,7 @@ public:
|
||||
/*
|
||||
* long transaction history
|
||||
*/
|
||||
Env env(*this);
|
||||
Env env(*this, storing());
|
||||
std::array<Account, 2> accounts = {alice, carol};
|
||||
env.fund(XRP(444444), accounts);
|
||||
env.close();
|
||||
|
||||
@@ -20,11 +20,13 @@
|
||||
#include <test/shamap/common.h>
|
||||
#include <test/unit_test/SuiteJournal.h>
|
||||
#include <xrpld/shamap/SHAMap.h>
|
||||
#include <xrpld/shamap/SHAMapInnerNode.h>
|
||||
#include <xrpld/shamap/SHAMapItem.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/basics/random.h>
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
#include <xrpl/beast/xor_shift_engine.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace tests {
|
||||
@@ -84,12 +86,53 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
testNullBackendIgnoresFullBelow(beast::Journal const& journal)
|
||||
{
|
||||
testcase("null backend does not skip children via FullBelowCache");
|
||||
|
||||
TestNodeFamily sourceFamily(journal);
|
||||
TestNodeFamily destFamily(journal);
|
||||
destFamily.setNullBackend(true);
|
||||
|
||||
SHAMap source(SHAMapType::FREE, sourceFamily);
|
||||
for (int i = 0; i < 32; ++i)
|
||||
source.addItem(SHAMapNodeType::tnACCOUNT_STATE, makeRandomAS());
|
||||
source.setImmutable();
|
||||
auto const rootHash = source.getHash();
|
||||
|
||||
auto const root = source.getLinkedNode(SHAMapNodeID{});
|
||||
if (!BEAST_EXPECT(root && root->isInner()))
|
||||
return;
|
||||
auto* inner = static_cast<SHAMapInnerNode*>(root.get());
|
||||
for (int b = 0; b < SHAMapInnerNode::branchFactor; ++b)
|
||||
{
|
||||
if (!inner->isEmptyBranch(b))
|
||||
{
|
||||
destFamily.getFullBelowCache()->insert(
|
||||
inner->getChildHash(b).as_uint256());
|
||||
}
|
||||
}
|
||||
|
||||
SHAMap destination(SHAMapType::FREE, destFamily);
|
||||
destination.setSynching();
|
||||
Serializer s;
|
||||
source.serializeRoot(s);
|
||||
BEAST_EXPECT(
|
||||
destination.addRootNode(rootHash, s.slice(), nullptr).isGood());
|
||||
|
||||
auto const missing = destination.getMissingNodes(2048, nullptr);
|
||||
BEAST_EXPECT(!missing.empty());
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
using namespace beast::severities;
|
||||
test::SuiteJournal journal("SHAMapSync_test", *this);
|
||||
|
||||
testNullBackendIgnoresFullBelow(journal);
|
||||
|
||||
TestNodeFamily f(journal), f2(journal);
|
||||
SHAMap source(SHAMapType::FREE, f);
|
||||
SHAMap destination(SHAMapType::FREE, f2);
|
||||
|
||||
@@ -19,11 +19,17 @@
|
||||
|
||||
#include <test/shamap/common.h>
|
||||
#include <test/unit_test/SuiteJournal.h>
|
||||
#include <xrpld/nodestore/NodeObject.h>
|
||||
#include <xrpld/shamap/SHAMap.h>
|
||||
#include <xrpld/shamap/SHAMapInnerNode.h>
|
||||
#include <xrpld/shamap/SHAMapTreeNode.h>
|
||||
#include <xrpl/basics/Blob.h>
|
||||
#include <xrpl/basics/Buffer.h>
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace ripple {
|
||||
namespace tests {
|
||||
@@ -122,6 +128,224 @@ public:
|
||||
|
||||
run(true, journal);
|
||||
run(false, journal);
|
||||
testCanonicalizeMergesInnerChildren(journal);
|
||||
testCanonicalizeSharesDuplicateContent(journal);
|
||||
testGetLinkedNode(journal);
|
||||
}
|
||||
|
||||
void
|
||||
testCanonicalizeSharesDuplicateContent(beast::Journal const& journal)
|
||||
{
|
||||
testcase("canonicalize shares nodes of identical maps");
|
||||
|
||||
tests::TestNodeFamily f(journal);
|
||||
SHAMap a{SHAMapType::FREE, f};
|
||||
SHAMap b{SHAMapType::FREE, f};
|
||||
|
||||
constexpr uint256 h1(
|
||||
"092891fe4ef6cee585fdc6fda0e09eb4d386363158ec3321b8123e5a772c6ca7");
|
||||
constexpr uint256 h2(
|
||||
"436ccbac3347baa1f1e53baeef1f43334da88f1f6d70d963b833afd6dfa289fe");
|
||||
for (auto* map : {&a, &b})
|
||||
{
|
||||
BEAST_EXPECT(map->addItem(
|
||||
SHAMapNodeType::tnTRANSACTION_NM,
|
||||
make_shamapitem(h1, IntToVUC(1))));
|
||||
BEAST_EXPECT(map->addItem(
|
||||
SHAMapNodeType::tnTRANSACTION_NM,
|
||||
make_shamapitem(h2, IntToVUC(2))));
|
||||
}
|
||||
|
||||
// Do not call getHash() first: it unshares the map and flushDirty
|
||||
// then has nothing to write.
|
||||
auto const harvestedBefore = SHAMap::canonicalInnerBranchesHarvested();
|
||||
a.flushDirty(hotTRANSACTION_NODE);
|
||||
b.flushDirty(hotTRANSACTION_NODE);
|
||||
|
||||
// b's root and leaves were canonicalized onto a's. a's root was
|
||||
// already fully linked, so there was nothing to harvest, and the
|
||||
// leaf duplicates take the non-inner early return.
|
||||
auto const rootA = a.getLinkedNode(SHAMapNodeID{});
|
||||
auto const rootB = b.getLinkedNode(SHAMapNodeID{});
|
||||
if (!BEAST_EXPECT(rootA && rootB && rootA->isInner()))
|
||||
return;
|
||||
BEAST_EXPECT(rootA.get() == rootB.get());
|
||||
BEAST_EXPECT(
|
||||
SHAMap::canonicalInnerBranchesHarvested() == harvestedBefore);
|
||||
|
||||
auto* inner = static_cast<SHAMapInnerNode*>(rootA.get());
|
||||
int shared = 0;
|
||||
for (int branch = 0; branch < SHAMapInnerNode::branchFactor; ++branch)
|
||||
{
|
||||
if (inner->isEmptyBranch(branch))
|
||||
continue;
|
||||
auto const id = SHAMapNodeID{}.getChildNodeID(branch);
|
||||
auto const leafA = a.getLinkedNode(id);
|
||||
auto const leafB = b.getLinkedNode(id);
|
||||
BEAST_EXPECT(leafA && leafB && leafA.get() == leafB.get());
|
||||
++shared;
|
||||
}
|
||||
BEAST_EXPECT(shared == 2);
|
||||
}
|
||||
|
||||
void
|
||||
testCanonicalizeMergesInnerChildren(beast::Journal const& journal)
|
||||
{
|
||||
testcase("canonicalize harvests inner children onto cache winner");
|
||||
|
||||
tests::TestNodeFamily f(journal);
|
||||
SHAMap map{SHAMapType::FREE, f};
|
||||
|
||||
constexpr uint256 h1(
|
||||
"092891fe4ef6cee585fdc6fda0e09eb4d386363158ec3321b8123e5a772c6ca7");
|
||||
constexpr uint256 h2(
|
||||
"436ccbac3347baa1f1e53baeef1f43334da88f1f6d70d963b833afd6dfa289fe");
|
||||
constexpr uint256 h3(
|
||||
"b92891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8");
|
||||
constexpr uint256 h4(
|
||||
"c92891fe4ef6cee585fdc6fda2e09eb4d386363158ec3321b8123e5a772c6ca8");
|
||||
|
||||
BEAST_EXPECT(map.addItem(
|
||||
SHAMapNodeType::tnTRANSACTION_NM,
|
||||
make_shamapitem(h1, IntToVUC(1))));
|
||||
BEAST_EXPECT(map.addItem(
|
||||
SHAMapNodeType::tnTRANSACTION_NM,
|
||||
make_shamapitem(h2, IntToVUC(2))));
|
||||
BEAST_EXPECT(map.addItem(
|
||||
SHAMapNodeType::tnTRANSACTION_NM,
|
||||
make_shamapitem(h3, IntToVUC(3))));
|
||||
BEAST_EXPECT(map.addItem(
|
||||
SHAMapNodeType::tnTRANSACTION_NM,
|
||||
make_shamapitem(h4, IntToVUC(4))));
|
||||
|
||||
// getHash() unshares the map (cowid=0), which makes flushDirty a
|
||||
// no-op. Snapshot first so the original stays dirty and still
|
||||
// canonicalizes onto the cache shell.
|
||||
auto snap = map.snapShot(false);
|
||||
auto const rootHash = snap->getHash();
|
||||
Serializer s;
|
||||
snap->serializeRoot(s);
|
||||
auto shell = SHAMapTreeNode::makeFromWire(s.slice());
|
||||
if (!BEAST_EXPECT(
|
||||
shell && shell->isInner() && shell->getHash() == rootHash))
|
||||
return;
|
||||
|
||||
auto innerShell = std::static_pointer_cast<SHAMapInnerNode>(shell);
|
||||
int linkedBefore = 0;
|
||||
for (int b = 0; b < SHAMapInnerNode::branchFactor; ++b)
|
||||
{
|
||||
if (!innerShell->isEmptyBranch(b) && innerShell->getChildPointer(b))
|
||||
++linkedBefore;
|
||||
}
|
||||
BEAST_EXPECT(linkedBefore == 0);
|
||||
|
||||
f.getTreeNodeCache()->canonicalize_replace_client(
|
||||
rootHash.as_uint256(), shell);
|
||||
|
||||
auto const harvestedBefore = SHAMap::canonicalInnerBranchesHarvested();
|
||||
map.flushDirty(hotTRANSACTION_NODE);
|
||||
|
||||
auto cached = f.getTreeNodeCache()->fetch(rootHash.as_uint256());
|
||||
if (!BEAST_EXPECT(cached && cached->isInner()))
|
||||
return;
|
||||
auto* inner = static_cast<SHAMapInnerNode*>(cached.get());
|
||||
int linkedAfter = 0;
|
||||
for (int b = 0; b < SHAMapInnerNode::branchFactor; ++b)
|
||||
{
|
||||
if (!inner->isEmptyBranch(b) && inner->getChildPointer(b))
|
||||
++linkedAfter;
|
||||
}
|
||||
BEAST_EXPECT(linkedAfter > 0);
|
||||
BEAST_EXPECT(
|
||||
SHAMap::canonicalInnerBranchesHarvested() > harvestedBefore);
|
||||
|
||||
auto const rootNode = map.getLinkedNode(SHAMapNodeID{});
|
||||
if (!BEAST_EXPECT(rootNode && rootNode->isInner()))
|
||||
return;
|
||||
auto* liveInner = static_cast<SHAMapInnerNode*>(rootNode.get());
|
||||
int liveLinked = 0;
|
||||
for (int b = 0; b < SHAMapInnerNode::branchFactor; ++b)
|
||||
{
|
||||
if (liveInner->isEmptyBranch(b))
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
!map.getLinkedNode(SHAMapNodeID{}.getChildNodeID(b)));
|
||||
continue;
|
||||
}
|
||||
auto const child =
|
||||
map.getLinkedNode(SHAMapNodeID{}.getChildNodeID(b));
|
||||
if (liveInner->getChildPointer(b))
|
||||
{
|
||||
++liveLinked;
|
||||
BEAST_EXPECT(
|
||||
child && child->getHash() == liveInner->getChildHash(b));
|
||||
}
|
||||
}
|
||||
BEAST_EXPECT(liveLinked > 0);
|
||||
}
|
||||
|
||||
void
|
||||
testGetLinkedNode(beast::Journal const& journal)
|
||||
{
|
||||
testcase("getLinkedNode walks only already-linked children");
|
||||
|
||||
tests::TestNodeFamily f(journal);
|
||||
SHAMap map{SHAMapType::FREE, f};
|
||||
|
||||
constexpr uint256 h1(
|
||||
"092891fe4ef6cee585fdc6fda0e09eb4d386363158ec3321b8123e5a772c6ca7");
|
||||
constexpr uint256 h2(
|
||||
"436ccbac3347baa1f1e53baeef1f43334da88f1f6d70d963b833afd6dfa289fe");
|
||||
constexpr uint256 h3(
|
||||
"b92891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8");
|
||||
constexpr uint256 h4(
|
||||
"c92891fe4ef6cee585fdc6fda2e09eb4d386363158ec3321b8123e5a772c6ca8");
|
||||
|
||||
BEAST_EXPECT(map.addItem(
|
||||
SHAMapNodeType::tnTRANSACTION_NM,
|
||||
make_shamapitem(h1, IntToVUC(1))));
|
||||
BEAST_EXPECT(map.addItem(
|
||||
SHAMapNodeType::tnTRANSACTION_NM,
|
||||
make_shamapitem(h2, IntToVUC(2))));
|
||||
BEAST_EXPECT(map.addItem(
|
||||
SHAMapNodeType::tnTRANSACTION_NM,
|
||||
make_shamapitem(h3, IntToVUC(3))));
|
||||
BEAST_EXPECT(map.addItem(
|
||||
SHAMapNodeType::tnTRANSACTION_NM,
|
||||
make_shamapitem(h4, IntToVUC(4))));
|
||||
|
||||
auto const root = map.getLinkedNode(SHAMapNodeID{});
|
||||
if (!BEAST_EXPECT(root && root->isInner()))
|
||||
return;
|
||||
|
||||
auto* inner = static_cast<SHAMapInnerNode*>(root.get());
|
||||
std::optional<int> leafBranch;
|
||||
std::optional<int> emptyBranch;
|
||||
for (int b = 0; b < SHAMapInnerNode::branchFactor; ++b)
|
||||
{
|
||||
if (inner->isEmptyBranch(b))
|
||||
{
|
||||
if (!emptyBranch)
|
||||
emptyBranch = b;
|
||||
continue;
|
||||
}
|
||||
auto const child =
|
||||
map.getLinkedNode(SHAMapNodeID{}.getChildNodeID(b));
|
||||
BEAST_EXPECT(child);
|
||||
if (child && !child->isInner() && !leafBranch)
|
||||
leafBranch = b;
|
||||
}
|
||||
if (BEAST_EXPECT(emptyBranch))
|
||||
{
|
||||
BEAST_EXPECT(!map.getLinkedNode(
|
||||
SHAMapNodeID{}.getChildNodeID(*emptyBranch)));
|
||||
}
|
||||
if (BEAST_EXPECT(leafBranch))
|
||||
{
|
||||
// A leaf has no children. Asking for a grandchild must miss.
|
||||
auto const leafId = SHAMapNodeID{}.getChildNodeID(*leafBranch);
|
||||
BEAST_EXPECT(!map.getLinkedNode(leafId.getChildNodeID(0)));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -120,11 +120,26 @@ public:
|
||||
tnCache_->reset();
|
||||
}
|
||||
|
||||
bool
|
||||
isNullBackend() const override
|
||||
{
|
||||
return nullBackend_;
|
||||
}
|
||||
|
||||
void
|
||||
setNullBackend(bool v)
|
||||
{
|
||||
nullBackend_ = v;
|
||||
}
|
||||
|
||||
beast::manual_clock<std::chrono::steady_clock>
|
||||
clock()
|
||||
{
|
||||
return clock_;
|
||||
}
|
||||
|
||||
private:
|
||||
bool nullBackend_ = false;
|
||||
};
|
||||
|
||||
} // namespace tests
|
||||
|
||||
@@ -348,6 +348,17 @@ RCLConsensus::Adaptor::onClose(
|
||||
make_shamapitem(tx.first->getTransactionID(), s.slice()));
|
||||
}
|
||||
|
||||
// Keep enough validation history for the next flag ledger's score table
|
||||
// even when we don't vote this round (not proposing / wrong LCL).
|
||||
// Otherwise toKeep_ goes stale, validations older than
|
||||
// validationSET_EXPIRES (10 min, < 256 ledgers) get swept, and we fail
|
||||
// the next flag's score table too.
|
||||
if (prevLedger->isVotingLedger())
|
||||
{
|
||||
auto const seq = prevLedger->info().seq + 1;
|
||||
app_.getValidations().setSeqToKeep(seq - 1, seq + FLAG_LEDGER_INTERVAL);
|
||||
}
|
||||
|
||||
// Add pseudo-transactions to the set
|
||||
if (app_.config().standalone() || (proposing && !wrongLCL))
|
||||
{
|
||||
|
||||
@@ -69,7 +69,13 @@ Expected<bool, HookReturnCode>
|
||||
HookAPI::sto_validate(Bytes const& data) const
|
||||
{
|
||||
if (data.size() < 2)
|
||||
{
|
||||
// fix20261005: a buffer this short can never hold a valid STObject,
|
||||
// so report it as invalid (0) as documented, rather than TOO_SMALL
|
||||
if (hookCtx.applyCtx.view().rules().enabled(fix20261005))
|
||||
return false;
|
||||
return Unexpected(TOO_SMALL);
|
||||
}
|
||||
|
||||
unsigned char* start = const_cast<unsigned char*>(data.data());
|
||||
unsigned char* upto = start;
|
||||
@@ -1029,15 +1035,21 @@ HookAPI::float_set(int32_t exponent, int64_t mantissa) const
|
||||
auto normalized = hook_float::normalize_xfl(mantissa, exponent);
|
||||
|
||||
// the above function will underflow into a canonical 0
|
||||
// but this api must report that underflow
|
||||
// but this api must report that underflow.
|
||||
// The range check is on the exponent *after* the mantissa is normalized
|
||||
// to 16 digits. fix20261005: report overflow / underflow as
|
||||
// EXPONENT_OVERSIZED / EXPONENT_UNDERSIZED; before the fix both were
|
||||
// reported as INVALID_FLOAT.
|
||||
bool const fixExpCodes =
|
||||
hookCtx.applyCtx.view().rules().enabled(fix20261005);
|
||||
if (!normalized)
|
||||
{
|
||||
if (normalized.error() == XFL_OVERFLOW)
|
||||
return Unexpected(INVALID_FLOAT);
|
||||
return Unexpected(fixExpCodes ? EXPONENT_OVERSIZED : INVALID_FLOAT);
|
||||
return Unexpected(normalized.error());
|
||||
}
|
||||
if (normalized.value() == 0)
|
||||
return Unexpected(INVALID_FLOAT);
|
||||
return Unexpected(fixExpCodes ? EXPONENT_UNDERSIZED : INVALID_FLOAT);
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
@@ -1546,7 +1546,20 @@ hook::finalizeHookResult(
|
||||
ptr->add(s);
|
||||
SerialIter sit(s.slice());
|
||||
|
||||
sleEmitted->emplace_back(ripple::STObject(sit, sfEmittedTxn));
|
||||
ripple::STObject emittedTxn(sit, sfEmittedTxn);
|
||||
|
||||
// ltEMITTED_TXN has an SOTemplate, so the new SLE already
|
||||
// holds a non-present sfEmittedTxn slot. emplace_back()
|
||||
// appends a second entry behind it, and lookups (which go
|
||||
// through the template index) keep reporting the field as
|
||||
// absent until the SLE is serialized and re-read. set()
|
||||
// replaces the template slot. The serialized entry is the
|
||||
// same either way, but a hook slotting the entry later in
|
||||
// the same ledger can observe the difference, so gate it.
|
||||
if (applyCtx.view().rules().enabled(fix20261005))
|
||||
sleEmitted->set(std::move(emittedTxn));
|
||||
else
|
||||
sleEmitted->emplace_back(std::move(emittedTxn));
|
||||
auto page = applyCtx.view().dirInsert(
|
||||
keylet::emittedDir(), emittedId, [&](SLE::ref sle) {
|
||||
(*sle)[sfFlags] = lsfEmittedDir;
|
||||
|
||||
@@ -155,6 +155,12 @@ LedgerHistory::getLedgerByHash(LedgerHash const& hash)
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::shared_ptr<Ledger const>
|
||||
LedgerHistory::getCachedLedger(LedgerHash const& hash)
|
||||
{
|
||||
return m_ledgers_by_hash.fetch(hash);
|
||||
}
|
||||
|
||||
static void
|
||||
log_one(
|
||||
ReadView const& ledger,
|
||||
|
||||
@@ -63,6 +63,13 @@ public:
|
||||
std::shared_ptr<Ledger const>
|
||||
getLedgerByHash(LedgerHash const& ledgerHash);
|
||||
|
||||
/** Return a ledger already in the hash cache.
|
||||
|
||||
Does not load from the database or wire the SHAMap.
|
||||
*/
|
||||
std::shared_ptr<Ledger const>
|
||||
getCachedLedger(LedgerHash const& ledgerHash);
|
||||
|
||||
/** Get a ledger's hash given its sequence number
|
||||
@param ledgerIndex The sequence number of the desired ledger
|
||||
@return The hash of the specified ledger
|
||||
|
||||
@@ -37,6 +37,8 @@
|
||||
#include <xrpl/protocol/RippleLedgerHash.h>
|
||||
#include <xrpl/protocol/STValidation.h>
|
||||
#include <xrpl/protocol/messages.h>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
|
||||
#include <mutex>
|
||||
@@ -186,6 +188,21 @@ public:
|
||||
std::shared_ptr<Ledger const>
|
||||
getLedgerByHash(uint256 const& hash);
|
||||
|
||||
/** Return a ledger that is already resident in memory.
|
||||
|
||||
Looks in the ledger history cache, the closed and validated
|
||||
ledgers and the null-mode retained window. Unlike getLedgerByHash
|
||||
and getLedgerBySeq this never loads from SQL or the node store and
|
||||
never edits the complete-ledger set, so it is safe to call with
|
||||
values supplied by a peer.
|
||||
*/
|
||||
std::shared_ptr<Ledger const>
|
||||
getResidentLedgerByHash(uint256 const& hash);
|
||||
|
||||
/** Sequence-number form of getResidentLedgerByHash. */
|
||||
std::shared_ptr<Ledger const>
|
||||
getResidentLedgerBySeq(std::uint32_t seq);
|
||||
|
||||
void
|
||||
setLedgerRangePresent(
|
||||
std::uint32_t minV,
|
||||
@@ -323,6 +340,10 @@ private:
|
||||
void
|
||||
doAdvance(std::unique_lock<std::recursive_mutex>&);
|
||||
|
||||
// Closed, validated, then retained ledgers; never loads.
|
||||
std::shared_ptr<Ledger const>
|
||||
findResidentLedger(std::function<bool(LedgerInfo const&)> const& match);
|
||||
|
||||
std::vector<std::shared_ptr<Ledger const>>
|
||||
findNewLedgersToPublish(std::unique_lock<std::recursive_mutex>&);
|
||||
|
||||
@@ -354,6 +375,14 @@ private:
|
||||
// The last ledger we handled fetching history
|
||||
std::shared_ptr<Ledger const> mHistLedger;
|
||||
|
||||
// Sliding window of the last ledger_history published ledgers, pinned
|
||||
// so their SHAMaps stay reachable via shared_ptr. Only populated when
|
||||
// [node_db] type=rwdb (null node store), where nothing else can bring
|
||||
// a dropped tree back. Has its own lock so peer requests that consult
|
||||
// it do not contend on m_mutex.
|
||||
std::mutex mutable mRetainedLock;
|
||||
std::deque<std::shared_ptr<Ledger const>> mRetainedLedgers;
|
||||
|
||||
// Fully validated ledger, whether or not we have the ledger resident.
|
||||
std::pair<uint256, LedgerIndex> mLastValidLedger{uint256(), 0};
|
||||
|
||||
@@ -401,6 +430,10 @@ private:
|
||||
// How much history do we want to keep
|
||||
std::uint32_t const ledger_history_;
|
||||
|
||||
// [node_db] type=rwdb: the node store keeps nothing, so recent
|
||||
// ledgers are retained in memory (see mRetainedLedgers).
|
||||
bool const nullBackend_;
|
||||
|
||||
std::uint32_t const ledger_fetch_size_;
|
||||
|
||||
TaggedCache<uint256, Blob> fetch_packs_;
|
||||
|
||||
@@ -113,6 +113,7 @@ LedgerMaster::LedgerMaster(
|
||||
, fetch_depth_(
|
||||
app_.getSHAMapStore().clampFetchDepth(app_.config().FETCH_DEPTH))
|
||||
, ledger_history_(app_.config().LEDGER_HISTORY)
|
||||
, nullBackend_(app_.config().nullBackend())
|
||||
, ledger_fetch_size_(app_.config().getValueFor(SizedItem::ledgerFetch))
|
||||
, fetch_packs_(
|
||||
"FetchPack",
|
||||
@@ -937,6 +938,20 @@ LedgerMaster::setFullLedger(
|
||||
mCompleteLedgers.insert(ledger->info().seq);
|
||||
}
|
||||
|
||||
// Null node store only: pin a sliding window of recently validated
|
||||
// current ledgers so their SHAMap state trees stay resident via
|
||||
// shared_ptr. A persistent node store must not do this. It would pin
|
||||
// ledger_history full trees for nothing, and ledger_history=full
|
||||
// (uint32 max) would never trim. SHAMapStoreImp rejects both zero and
|
||||
// "full" ledger_history for type=rwdb.
|
||||
if (nullBackend_ && isCurrent)
|
||||
{
|
||||
std::lock_guard rl(mRetainedLock);
|
||||
mRetainedLedgers.push_back(ledger);
|
||||
while (mRetainedLedgers.size() > ledger_history_)
|
||||
mRetainedLedgers.pop_front();
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard ml(m_mutex);
|
||||
|
||||
@@ -1852,6 +1867,12 @@ LedgerMaster::getCloseTimeByHash(
|
||||
LedgerHash const& ledgerHash,
|
||||
std::uint32_t index)
|
||||
{
|
||||
// Resident ledgers first (getLedgerByHash would load a cache miss).
|
||||
// Close time is a header field, so fall through to the node-store
|
||||
// header when nothing is already in memory.
|
||||
if (auto const ledger = getResidentLedgerByHash(ledgerHash))
|
||||
return ledger->info().closeTime;
|
||||
|
||||
auto nodeObject = app_.getNodeStore().fetchNodeObject(ledgerHash, index);
|
||||
if (nodeObject && (nodeObject->getData().size() >= 120))
|
||||
{
|
||||
@@ -1996,6 +2017,44 @@ LedgerMaster::getLedgerByHash(uint256 const& hash)
|
||||
return {};
|
||||
}
|
||||
|
||||
std::shared_ptr<Ledger const>
|
||||
LedgerMaster::findResidentLedger(
|
||||
std::function<bool(LedgerInfo const&)> const& match)
|
||||
{
|
||||
for (auto const& ledger : {mClosedLedger.get(), mValidLedger.get()})
|
||||
{
|
||||
if (ledger && match(ledger->info()))
|
||||
return ledger;
|
||||
}
|
||||
|
||||
std::lock_guard lock(mRetainedLock);
|
||||
auto const it = std::find_if(
|
||||
mRetainedLedgers.rbegin(),
|
||||
mRetainedLedgers.rend(),
|
||||
[&](auto const& ledger) { return match(ledger->info()); });
|
||||
return it == mRetainedLedgers.rend() ? nullptr : *it;
|
||||
}
|
||||
|
||||
std::shared_ptr<Ledger const>
|
||||
LedgerMaster::getResidentLedgerByHash(uint256 const& hash)
|
||||
{
|
||||
if (auto ret = mLedgerHistory.getCachedLedger(hash))
|
||||
return ret;
|
||||
return findResidentLedger(
|
||||
[&](LedgerInfo const& info) { return info.hash == hash; });
|
||||
}
|
||||
|
||||
std::shared_ptr<Ledger const>
|
||||
LedgerMaster::getResidentLedgerBySeq(std::uint32_t seq)
|
||||
{
|
||||
// getLedgerHash only reads the in-memory index; a zero hash misses.
|
||||
if (auto ret =
|
||||
mLedgerHistory.getCachedLedger(mLedgerHistory.getLedgerHash(seq)))
|
||||
return ret;
|
||||
return findResidentLedger(
|
||||
[&](LedgerInfo const& info) { return info.seq == seq; });
|
||||
}
|
||||
|
||||
void
|
||||
LedgerMaster::setLedgerRangePresent(
|
||||
std::uint32_t minV,
|
||||
|
||||
@@ -170,26 +170,25 @@ LoadManager::run()
|
||||
LogicError("Deadlock detected");
|
||||
}
|
||||
}
|
||||
}
|
||||
bool change;
|
||||
|
||||
bool change;
|
||||
if (app_.getJobQueue().isOverloaded())
|
||||
{
|
||||
JLOG(journal_.info()) << "Raising local fee (JQ overload): "
|
||||
<< app_.getJobQueue().getJson(0);
|
||||
change = app_.getFeeTrack().raiseLocalFee();
|
||||
}
|
||||
else
|
||||
{
|
||||
change = app_.getFeeTrack().lowerLocalFee();
|
||||
}
|
||||
|
||||
if (app_.getJobQueue().isOverloaded())
|
||||
{
|
||||
JLOG(journal_.info()) << "Raising local fee (JQ overload): "
|
||||
<< app_.getJobQueue().getJson(0);
|
||||
change = app_.getFeeTrack().raiseLocalFee();
|
||||
}
|
||||
else
|
||||
{
|
||||
change = app_.getFeeTrack().lowerLocalFee();
|
||||
}
|
||||
|
||||
if (change)
|
||||
{
|
||||
// VFALCO TODO replace this with a Listener / observer and
|
||||
// subscribe in NetworkOPs or Application.
|
||||
app_.getOPs().reportFeeChange();
|
||||
if (change)
|
||||
{
|
||||
// VFALCO TODO replace this with a Listener / observer and
|
||||
// subscribe in NetworkOPs or Application.
|
||||
app_.getOPs().reportFeeChange();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -58,9 +58,19 @@ NegativeUNLVote::doVoting(
|
||||
unlNodeIDs.emplace(nid);
|
||||
}
|
||||
|
||||
// Build a reliability score table of validators
|
||||
if (std::optional<hash_map<NodeID, std::uint32_t>> scoreTable =
|
||||
buildScoreTable(prevLedger, unlNodeIDs, validations))
|
||||
// Build a reliability score table of validators. This only fails if
|
||||
// there isn't enough ledger history; it does NOT apply the local
|
||||
// reliability gate (that is N-UNL specific, see below).
|
||||
auto const scoreTable =
|
||||
buildRawScoreTable(prevLedger, unlNodeIDs, validations);
|
||||
if (!scoreTable)
|
||||
return;
|
||||
|
||||
auto const seq = prevLedger->info().seq + 1;
|
||||
|
||||
// N-UNL voting: only vote to disable/re-enable others if we ourselves
|
||||
// have been reliable over the last flag ledger interval.
|
||||
if (localNodeReliableForNUNL(*scoreTable, seq))
|
||||
{
|
||||
// build next negUnl
|
||||
auto negUnlKeys = prevLedger->negativeUNL();
|
||||
@@ -82,7 +92,6 @@ NegativeUNLVote::doVoting(
|
||||
}
|
||||
}
|
||||
|
||||
auto const seq = prevLedger->info().seq + 1;
|
||||
purgeNewValidators(seq);
|
||||
|
||||
// Process the table and find all candidates to disable or to re-enable
|
||||
@@ -109,14 +118,33 @@ NegativeUNLVote::doVoting(
|
||||
"ripple::NegativeUNLVote::doVoting : found node to enable");
|
||||
addTx(seq, nidToKeyMap.at(n), ToReEnable, initialSet);
|
||||
}
|
||||
}
|
||||
|
||||
// do reporting when enabled
|
||||
if (prevLedger->rules().enabled(featureXahauGenesis) &&
|
||||
scoreTable->size() > 0)
|
||||
// UNLReport. Previously this sat behind the N-UNL local reliability gate
|
||||
// (>= 90% of our own validations on-chain in the last 256 ledgers). Every
|
||||
// ttUNL_REPORT is its own deterministic txn and is voted on individually
|
||||
// in consensus, so one proposer's skewed view can't change the outcome;
|
||||
// but gating the whole batch per-proposer made the reports all-or-nothing:
|
||||
// whenever fewer than ~half the UNL cleared the 90% bar the entire report
|
||||
// lost the dispute and the flag ledger carried no UNLReport at all.
|
||||
// Use the same bar the report itself uses for "active".
|
||||
if (prevLedger->rules().enabled(featureXahauGenesis) &&
|
||||
!scoreTable->empty())
|
||||
{
|
||||
auto const it = scoreTable->find(myId_);
|
||||
auto const myScore = it == scoreTable->end() ? 0u : it->second;
|
||||
if (myScore > (FLAG_LEDGER_INTERVAL >> 1))
|
||||
{
|
||||
addReportingTx(seq, *scoreTable, nidToKeyMap, initialSet);
|
||||
addImportVLTx(seq, initialSet);
|
||||
}
|
||||
else
|
||||
{
|
||||
JLOG(j_.debug())
|
||||
<< "R-UNL: ledger " << seq << ". Local node only issued "
|
||||
<< myScore << " validations in last " << FLAG_LEDGER_INTERVAL
|
||||
<< " ledgers, not proposing UNLReport.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,7 +305,7 @@ NegativeUNLVote::choose(
|
||||
}
|
||||
|
||||
std::optional<hash_map<NodeID, std::uint32_t>>
|
||||
NegativeUNLVote::buildScoreTable(
|
||||
NegativeUNLVote::buildRawScoreTable(
|
||||
std::shared_ptr<Ledger const> const& prevLedger,
|
||||
hash_set<NodeID> const& unl,
|
||||
RCLValidations& validations)
|
||||
@@ -327,6 +355,14 @@ NegativeUNLVote::buildScoreTable(
|
||||
}
|
||||
}
|
||||
|
||||
return scoreTable;
|
||||
}
|
||||
|
||||
bool
|
||||
NegativeUNLVote::localNodeReliableForNUNL(
|
||||
hash_map<NodeID, std::uint32_t> const& scoreTable,
|
||||
LedgerIndex seq) const
|
||||
{
|
||||
// Return false if the validation message history or local node's
|
||||
// participation in the history is not good.
|
||||
auto const myValidationCount = [&]() -> std::uint32_t {
|
||||
@@ -341,23 +377,33 @@ NegativeUNLVote::buildScoreTable(
|
||||
<< " validations in last " << FLAG_LEDGER_INTERVAL
|
||||
<< " ledgers."
|
||||
<< " The reliability measurement could be wrong.";
|
||||
return {};
|
||||
return false;
|
||||
}
|
||||
else if (
|
||||
myValidationCount > negativeUNLMinLocalValsToVote &&
|
||||
myValidationCount <= FLAG_LEDGER_INTERVAL)
|
||||
{
|
||||
return scoreTable;
|
||||
}
|
||||
else
|
||||
if (myValidationCount > FLAG_LEDGER_INTERVAL)
|
||||
{
|
||||
// cannot happen because validations.getTrustedForLedger does not
|
||||
// return multiple validations of the same ledger from a validator.
|
||||
JLOG(j_.error()) << "N-UNL: ledger " << seq << ". Local node issued "
|
||||
<< myValidationCount << " validations in last "
|
||||
<< FLAG_LEDGER_INTERVAL << " ledgers. Too many!";
|
||||
return {};
|
||||
return false;
|
||||
}
|
||||
// NB: the old code used `>` here, so exactly 230 fell through to the
|
||||
// "Too many!" branch.
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<hash_map<NodeID, std::uint32_t>>
|
||||
NegativeUNLVote::buildScoreTable(
|
||||
std::shared_ptr<Ledger const> const& prevLedger,
|
||||
hash_set<NodeID> const& unl,
|
||||
RCLValidations& validations)
|
||||
{
|
||||
auto scoreTable = buildRawScoreTable(prevLedger, unl, validations);
|
||||
if (scoreTable &&
|
||||
!localNodeReliableForNUNL(*scoreTable, prevLedger->info().seq + 1))
|
||||
return {};
|
||||
return scoreTable;
|
||||
}
|
||||
|
||||
NegativeUNLVote::Candidates const
|
||||
|
||||
@@ -192,6 +192,26 @@ private:
|
||||
NodeID
|
||||
choose(uint256 const& randomPadData, std::vector<NodeID> const& candidates);
|
||||
|
||||
/**
|
||||
* Count trusted full validations per UNL validator over the last
|
||||
* FLAG_LEDGER_INTERVAL ledgers. Fails only on insufficient ledger
|
||||
* history; does not apply the local reliability gate.
|
||||
*/
|
||||
std::optional<hash_map<NodeID, std::uint32_t>>
|
||||
buildRawScoreTable(
|
||||
std::shared_ptr<Ledger const> const& prevLedger,
|
||||
hash_set<NodeID> const& unl,
|
||||
RCLValidations& validations);
|
||||
|
||||
/**
|
||||
* True if the local node issued enough validations in the score table
|
||||
* to be trusted to vote on N-UNL changes.
|
||||
*/
|
||||
bool
|
||||
localNodeReliableForNUNL(
|
||||
hash_map<NodeID, std::uint32_t> const& scoreTable,
|
||||
LedgerIndex seq) const;
|
||||
|
||||
/**
|
||||
* Build a reliability measurement score table of validators' validation
|
||||
* messages in the last flag ledger period.
|
||||
|
||||
@@ -704,7 +704,10 @@ private:
|
||||
|
||||
std::array<SubMapType, SubTypes::sLastEntry> mStreamMaps;
|
||||
|
||||
ServerFeeSummary mLastFeeSummary;
|
||||
ServerFeeSummary mLastFeeSummary; ///< Guarded by mFeeSummaryMutex_.
|
||||
std::mutex mFeeSummaryMutex_; ///< Guards mLastFeeSummary only. Kept
|
||||
///< separate from mSubLock to avoid
|
||||
///< lock-ordering hazards with masterMutex.
|
||||
|
||||
JobQueue& m_job_queue;
|
||||
|
||||
@@ -2348,7 +2351,10 @@ NetworkOPsImp::pubServer()
|
||||
else
|
||||
jvObj[jss::load_factor] = f.loadFactorServer;
|
||||
|
||||
mLastFeeSummary = f;
|
||||
{
|
||||
std::lock_guard fsl(mFeeSummaryMutex_);
|
||||
mLastFeeSummary = f;
|
||||
}
|
||||
|
||||
for (auto i = mStreamMaps[sServer].begin();
|
||||
i != mStreamMaps[sServer].end();)
|
||||
@@ -3233,14 +3239,23 @@ NetworkOPsImp::reportFeeChange()
|
||||
app_.getTxQ().getMetrics(*app_.openLedger().current()),
|
||||
app_.getFeeTrack()};
|
||||
|
||||
// only schedule the job if something has changed
|
||||
if (f != mLastFeeSummary)
|
||||
{
|
||||
m_job_queue.addJob(
|
||||
jtCLIENT_FEE_CHANGE, "reportFeeChange->pubServer", [this]() {
|
||||
pubServer();
|
||||
});
|
||||
}
|
||||
// Guard mLastFeeSummary under mFeeSummaryMutex_ to prevent concurrent
|
||||
// threads from simultaneously passing the check and queuing duplicate
|
||||
// jtCLIENT_FEE_CHANGE jobs (data race fix).
|
||||
// Also fixes the no-subscriber case where mLastFeeSummary was
|
||||
// never updated by pubServer(), causing endless job queuing.
|
||||
// mFeeSummaryMutex_ is used instead of mSubLock to avoid a
|
||||
// lock-ordering hazard: reportFeeChange() is called with masterMutex
|
||||
// held, and pubServer() holds mSubLock across the subscriber fan-out.
|
||||
if (std::lock_guard sl(mFeeSummaryMutex_); f != mLastFeeSummary)
|
||||
mLastFeeSummary = f;
|
||||
else
|
||||
return;
|
||||
|
||||
m_job_queue.addJob(
|
||||
jtCLIENT_FEE_CHANGE, "reportFeeChange->pubServer", [this]() {
|
||||
pubServer();
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
@@ -4290,10 +4305,26 @@ NetworkOPsImp::subServer(
|
||||
jvResult[jss::pubkey_node] =
|
||||
toBase58(TokenType::NodePublic, app_.nodeIdentity().first);
|
||||
|
||||
std::lock_guard sl(mSubLock);
|
||||
return mStreamMaps[sServer]
|
||||
.emplace(isrListener->getSeq(), isrListener)
|
||||
.second;
|
||||
bool added;
|
||||
bool isFirstSubscriber = false;
|
||||
{
|
||||
std::lock_guard sl(mSubLock);
|
||||
added = mStreamMaps[sServer]
|
||||
.emplace(isrListener->getSeq(), isrListener)
|
||||
.second;
|
||||
isFirstSubscriber = added && mStreamMaps[sServer].size() == 1;
|
||||
}
|
||||
if (isFirstSubscriber)
|
||||
{
|
||||
// First subscriber on an otherwise-quiet node: reset mLastFeeSummary
|
||||
// so the next reportFeeChange() tick publishes a full serverStatus
|
||||
// with base_fee and load_factor_* fields. Skip if a PubFee job is
|
||||
// already queued — it will publish to the new subscriber anyway,
|
||||
// and resetting here would cause a duplicate notification.
|
||||
std::lock_guard fsl(mFeeSummaryMutex_);
|
||||
mLastFeeSummary = {};
|
||||
}
|
||||
return added;
|
||||
}
|
||||
|
||||
// <-- bool: true=erased, false=was not there
|
||||
|
||||
@@ -134,7 +134,39 @@ SHAMapStoreImp::SHAMapStoreImp(
|
||||
section.set("filter_bits", "10");
|
||||
}
|
||||
|
||||
get_if_exists(section, "online_delete", deleteInterval_);
|
||||
// An explicit online_delete=0 disables cleanup. Only a missing
|
||||
// key is defaulted below. get_if_exists cannot tell those apart.
|
||||
bool const onlineDeleteConfigured = section.exists("online_delete");
|
||||
if (onlineDeleteConfigured)
|
||||
get_if_exists(section, "online_delete", deleteInterval_);
|
||||
isNullBackend_ = config.nullBackend();
|
||||
|
||||
if (isNullBackend_)
|
||||
{
|
||||
// Ledgers stay resident through LedgerMaster's retained window,
|
||||
// which is ledger_history long. Zero keeps nothing alive, and
|
||||
// "full" (uint32 max) never trims, so memory grows without bound.
|
||||
if (config.LEDGER_HISTORY == 0 ||
|
||||
config.LEDGER_HISTORY == std::numeric_limits<std::uint32_t>::max())
|
||||
{
|
||||
Throw<std::runtime_error>(
|
||||
"RWDB null mode requires a finite ledger_history > 0");
|
||||
}
|
||||
JLOG(journal_.info())
|
||||
<< "RWDB null mode: node store is ephemeral, " << "retaining "
|
||||
<< config.LEDGER_HISTORY << " ledgers in memory";
|
||||
}
|
||||
|
||||
// For RWDB, default online_delete to ledger_history only when the
|
||||
// key is absent. Clamp to the minimum so an implicit value never
|
||||
// triggers the "online_delete must be at least …" throw.
|
||||
if (isNullBackend_ && !onlineDeleteConfigured)
|
||||
{
|
||||
auto const minInterval = config.standalone()
|
||||
? minimumDeletionIntervalSA_
|
||||
: minimumDeletionInterval_;
|
||||
deleteInterval_ = std::max(config.LEDGER_HISTORY, minInterval);
|
||||
}
|
||||
|
||||
// Always initialize state database for pinned ranges persistence
|
||||
state_db_.init(config, dbName_);
|
||||
@@ -175,7 +207,7 @@ SHAMapStoreImp::SHAMapStoreImp(
|
||||
std::to_string(config.LEDGER_HISTORY) + ")");
|
||||
}
|
||||
|
||||
if (!config.mem_backend())
|
||||
if (!isNullBackend_)
|
||||
dbPaths();
|
||||
}
|
||||
}
|
||||
@@ -200,7 +232,25 @@ SHAMapStoreImp::makeNodeStore(int readThreads)
|
||||
|
||||
std::unique_ptr<NodeStore::Database> db;
|
||||
|
||||
if (deleteInterval_)
|
||||
if (isNullBackend_)
|
||||
{
|
||||
// Tree-only: use the existing NullFactory (type=none). No
|
||||
// DatabaseRotatingImp, no rotation thread, no object cache.
|
||||
// A cached header would look like a full acquire and cannot
|
||||
// be rebuilt from this backend.
|
||||
nscfg.set("type", "none");
|
||||
nscfg.set("cache_size", "0");
|
||||
nscfg.set("cache_age", "0");
|
||||
db = NodeStore::Manager::instance().make_Database(
|
||||
megabytes(
|
||||
app_.config().getValueFor(SizedItem::burstSize, std::nullopt)),
|
||||
scheduler_,
|
||||
readThreads,
|
||||
nscfg,
|
||||
app_.logs().journal(nodeStoreName_));
|
||||
fdRequired_ += db->fdRequired();
|
||||
}
|
||||
else if (deleteInterval_)
|
||||
{
|
||||
SavedState state = state_db_.getState();
|
||||
|
||||
@@ -421,8 +471,10 @@ SHAMapStoreImp::run()
|
||||
state_db_.setLastRotated(lastRotated);
|
||||
}
|
||||
|
||||
bool const readyToRotate =
|
||||
validatedSeq >= lastRotated + deleteInterval_ &&
|
||||
// Widen the sum: an online_delete near uint32 max would wrap a
|
||||
// 32-bit add and make this true on every ledger.
|
||||
bool const readyToRotate = std::uint64_t{validatedSeq} >=
|
||||
std::uint64_t{lastRotated} + deleteInterval_ &&
|
||||
canDelete_ >= lastRotated - 1 && healthWait() == keepGoing;
|
||||
|
||||
// will delete up to (not including) lastRotated
|
||||
@@ -439,6 +491,18 @@ SHAMapStoreImp::run()
|
||||
if (healthWait() == stopping)
|
||||
return;
|
||||
|
||||
if (isNullBackend_)
|
||||
{
|
||||
// Null node store: nothing to copy or rotate. Resident
|
||||
// trees are kept alive by LedgerMaster's retained window,
|
||||
// so only the SQL cleanup above applies.
|
||||
lastRotated = validatedSeq;
|
||||
state_db_.setLastRotated(lastRotated);
|
||||
JLOG(journal_.info())
|
||||
<< "finished null-mode cleanup " << validatedSeq;
|
||||
continue;
|
||||
}
|
||||
|
||||
JLOG(journal_.debug()) << "copying ledger " << validatedSeq;
|
||||
std::uint64_t nodeCount = 0;
|
||||
|
||||
|
||||
@@ -107,6 +107,7 @@ private:
|
||||
|
||||
std::uint32_t deleteInterval_ = 0;
|
||||
bool advisoryDelete_ = false;
|
||||
bool isNullBackend_ = false;
|
||||
std::uint32_t deleteBatch_ = 100;
|
||||
std::chrono::milliseconds backOff_{100};
|
||||
std::chrono::seconds ageThreshold_{60};
|
||||
|
||||
@@ -1658,11 +1658,9 @@ TxQ::accept(Application& app, OpenView& view)
|
||||
[seq, txnHash, emitDetails](auto& obj) {
|
||||
obj[sfLedgerSequence] = seq;
|
||||
obj[sfTransactionHash] = txnHash;
|
||||
obj.emplace_back(emitDetails);
|
||||
/*std::unique_ptr<STBase> ed =
|
||||
std::make_unique<STBase>(emitDetails);
|
||||
ed->setFName(sfEmitDetails);
|
||||
obj.set(std::move(ed));*/
|
||||
// replace the template slot rather than
|
||||
// appending a duplicate (bytes unchanged)
|
||||
obj.set(STObject(emitDetails));
|
||||
});
|
||||
|
||||
uint256 txID = efTx.getTransactionID();
|
||||
|
||||
@@ -375,6 +375,21 @@ public:
|
||||
return isMem;
|
||||
}
|
||||
|
||||
/** True when [node_db] type=rwdb.
|
||||
|
||||
The app node store is the existing NullFactory (type=none).
|
||||
Nodes stay alive through Ledger and SHAMap pointers.
|
||||
The result is read from this configuration, so two
|
||||
applications in one process do not share it.
|
||||
*/
|
||||
bool
|
||||
nullBackend() const
|
||||
{
|
||||
auto const& node = section(ConfigSection::nodeDatabase());
|
||||
return !node.empty() &&
|
||||
boost::beast::iequals(get(node, "type"), "rwdb");
|
||||
}
|
||||
|
||||
bool
|
||||
useTxTables() const
|
||||
{
|
||||
|
||||
@@ -118,7 +118,7 @@ sizedItems
|
||||
{SizedItem::txnDBCache, {{ 4, 12, 24, 64, 128 }}},
|
||||
{SizedItem::lgrDBCache, {{ 4, 8, 16, 32, 128 }}},
|
||||
{SizedItem::openFinalLimit, {{ 8, 16, 32, 64, 128 }}},
|
||||
{SizedItem::burstSize, {{ 4, 8, 16, 32, 64*1024*1024 }}},
|
||||
{SizedItem::burstSize, {{ 4, 8, 16, 32, 64 }}},
|
||||
{SizedItem::ramSizeGB, {{ 8, 12, 16, 24, 32 }}},
|
||||
{SizedItem::accountIdCacheSize, {{ 20047, 50053, 77081, 150061, 300007 }}}
|
||||
}};
|
||||
@@ -1060,24 +1060,6 @@ Config::loadFromString(std::string const& fileContents)
|
||||
"the maximum number of allowed peers (peers_max)");
|
||||
}
|
||||
}
|
||||
|
||||
if (!RUN_STANDALONE)
|
||||
{
|
||||
auto db_section = section(ConfigSection::nodeDatabase());
|
||||
if (auto type = get(db_section, "type", ""); type == "rwdb")
|
||||
{
|
||||
if (auto delete_interval = get(db_section, "online_delete", 0);
|
||||
delete_interval == 0)
|
||||
{
|
||||
Throw<std::runtime_error>(
|
||||
"RWDB (in-memory backend) requires online_delete to "
|
||||
"prevent OOM "
|
||||
"Exception: standalone mode (used by tests) doesn't need "
|
||||
"online_delete");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DatabasePinned validation (applies to all modes including standalone)
|
||||
auto db_section = section(ConfigSection::nodeDatabase());
|
||||
if (db_section.exists("pinned_type"))
|
||||
@@ -1089,6 +1071,16 @@ Config::loadFromString(std::string const& fileContents)
|
||||
"type must be specified when using pinned_type");
|
||||
}
|
||||
|
||||
// type=rwdb is the tree-only null node store. SHAMapStore never
|
||||
// builds DatabasePinned for it, so pinned (and catalogue) data
|
||||
// would be accepted and then silently dropped.
|
||||
if (nullBackend())
|
||||
{
|
||||
Throw<std::runtime_error>(
|
||||
"pinned_type is not supported with [node_db] type=rwdb "
|
||||
"(the rwdb node store does not persist nodes)");
|
||||
}
|
||||
|
||||
// Ensure pinned_path is specified for filesystem-backed types.
|
||||
// In-memory backends (rwdb) don't need a path.
|
||||
auto pinnedTypeForPath = get(db_section, "pinned_type", "");
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
#include <xrpld/nodestore/detail/DecodedBlob.h>
|
||||
#include <xrpld/nodestore/detail/EncodedBlob.h>
|
||||
#include <xrpld/nodestore/detail/codec.h>
|
||||
#include <xrpl/basics/ReaderPreferringSharedMutex.h>
|
||||
#include <xrpl/basics/contract.h>
|
||||
#include <boost/beast/core/string.hpp>
|
||||
#include <boost/core/ignore_unused.hpp>
|
||||
#include <boost/unordered/concurrent_flat_map.hpp>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
|
||||
namespace ripple {
|
||||
namespace NodeStore {
|
||||
@@ -34,8 +36,7 @@ private:
|
||||
using DataStore =
|
||||
std::map<uint256, std::vector<std::uint8_t>>; // Store compressed blob
|
||||
// data
|
||||
mutable std::recursive_mutex
|
||||
mutex_; // Only needed for std::map implementation
|
||||
mutable ReaderPreferringSharedMutex mutex_;
|
||||
|
||||
DataStore table_;
|
||||
|
||||
@@ -65,7 +66,7 @@ public:
|
||||
void
|
||||
open(bool createIfMissing) override
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
std::unique_lock lock(mutex_);
|
||||
if (isOpen_)
|
||||
Throw<std::runtime_error>("already open");
|
||||
isOpen_ = true;
|
||||
@@ -74,26 +75,31 @@ public:
|
||||
bool
|
||||
isOpen() override
|
||||
{
|
||||
std::shared_lock lock(mutex_);
|
||||
return isOpen_;
|
||||
}
|
||||
|
||||
void
|
||||
close() override
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
table_.clear();
|
||||
isOpen_ = false;
|
||||
DataStore old;
|
||||
{
|
||||
std::unique_lock lock(mutex_);
|
||||
isOpen_ = false;
|
||||
old.swap(table_); // O(1) swap; release lock before destructor runs
|
||||
}
|
||||
// 'old' is now destroyed outside the lock — no fetch() can be
|
||||
// blocked by the (potentially millions-of-entries) map destructor.
|
||||
}
|
||||
|
||||
Status
|
||||
fetch(void const* key, std::shared_ptr<NodeObject>* pObject) override
|
||||
{
|
||||
if (!isOpen_)
|
||||
return notFound;
|
||||
|
||||
uint256 const hash(uint256::fromVoid(key));
|
||||
|
||||
std::lock_guard lock(mutex_);
|
||||
std::shared_lock lock(mutex_);
|
||||
if (!isOpen_)
|
||||
return notFound;
|
||||
auto it = table_.find(hash);
|
||||
if (it == table_.end())
|
||||
return notFound;
|
||||
@@ -128,9 +134,6 @@ public:
|
||||
void
|
||||
store(std::shared_ptr<NodeObject> const& object) override
|
||||
{
|
||||
if (!isOpen_)
|
||||
return;
|
||||
|
||||
if (!object)
|
||||
return;
|
||||
|
||||
@@ -143,7 +146,9 @@ public:
|
||||
static_cast<const std::uint8_t*>(result.first),
|
||||
static_cast<const std::uint8_t*>(result.first) + result.second);
|
||||
|
||||
std::lock_guard lock(mutex_);
|
||||
std::unique_lock lock(mutex_);
|
||||
if (!isOpen_)
|
||||
return;
|
||||
table_[object->getHash()] = std::move(compressed);
|
||||
}
|
||||
|
||||
@@ -162,10 +167,9 @@ public:
|
||||
void
|
||||
for_each(std::function<void(std::shared_ptr<NodeObject>)> f) override
|
||||
{
|
||||
std::shared_lock lock(mutex_);
|
||||
if (!isOpen_)
|
||||
return;
|
||||
|
||||
std::lock_guard lock(mutex_);
|
||||
for (const auto& entry : table_)
|
||||
{
|
||||
nudb::detail::buffer bf;
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
#include <xrpld/nodestore/detail/DatabaseRotatingImp.h>
|
||||
#include <xrpl/protocol/HashPrefix.h>
|
||||
|
||||
#include <shared_mutex>
|
||||
|
||||
namespace ripple {
|
||||
namespace NodeStore {
|
||||
|
||||
@@ -59,10 +61,13 @@ DatabaseRotatingImp::rotate(
|
||||
// deleted.
|
||||
std::shared_ptr<NodeStore::Backend> oldArchiveBackend;
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
std::unique_lock const lock(mutex_);
|
||||
|
||||
// Mark the archive backend for deletion
|
||||
archiveBackend_->setDeletePath();
|
||||
// Do NOT call setDeletePath() inside this lock. For in-memory
|
||||
// backends, setDeletePath() calls close() which destructs the entire
|
||||
// table_ map while the lock is held and starves consensus reads.
|
||||
// Pinned history is preserved by DatabasePinned, not by copying
|
||||
// ledgers into the writable backend during rotation.
|
||||
oldArchiveBackend = std::move(archiveBackend_);
|
||||
|
||||
// Complete the rotation
|
||||
@@ -72,20 +77,23 @@ DatabaseRotatingImp::rotate(
|
||||
writableBackend_ = std::move(newBackend);
|
||||
}
|
||||
|
||||
// Lock released — clear the old archive now without blocking fetches.
|
||||
oldArchiveBackend->setDeletePath();
|
||||
|
||||
f(newWritableBackendName, newArchiveBackendName);
|
||||
}
|
||||
|
||||
std::string
|
||||
DatabaseRotatingImp::getName() const
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
std::shared_lock const lock(mutex_);
|
||||
return writableBackend_->getName();
|
||||
}
|
||||
|
||||
std::int32_t
|
||||
DatabaseRotatingImp::getWriteLoad() const
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
std::shared_lock const lock(mutex_);
|
||||
return writableBackend_->getWriteLoad();
|
||||
}
|
||||
|
||||
@@ -93,7 +101,7 @@ void
|
||||
DatabaseRotatingImp::importDatabase(Database& source)
|
||||
{
|
||||
auto const backend = [&] {
|
||||
std::lock_guard lock(mutex_);
|
||||
std::shared_lock const lock(mutex_);
|
||||
return writableBackend_;
|
||||
}();
|
||||
|
||||
@@ -104,7 +112,7 @@ bool
|
||||
DatabaseRotatingImp::storeLedger(std::shared_ptr<Ledger const> const& srcLedger)
|
||||
{
|
||||
auto const backend = [&] {
|
||||
std::lock_guard lock(mutex_);
|
||||
std::shared_lock const lock(mutex_);
|
||||
return writableBackend_;
|
||||
}();
|
||||
|
||||
@@ -114,7 +122,7 @@ DatabaseRotatingImp::storeLedger(std::shared_ptr<Ledger const> const& srcLedger)
|
||||
void
|
||||
DatabaseRotatingImp::sync()
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
std::unique_lock const lock(mutex_);
|
||||
writableBackend_->sync();
|
||||
}
|
||||
|
||||
@@ -128,7 +136,7 @@ DatabaseRotatingImp::store(
|
||||
auto nObj = NodeObject::createObject(type, std::move(data), hash);
|
||||
|
||||
auto const backend = [&] {
|
||||
std::lock_guard lock(mutex_);
|
||||
std::shared_lock const lock(mutex_);
|
||||
return writableBackend_;
|
||||
}();
|
||||
|
||||
@@ -182,7 +190,7 @@ DatabaseRotatingImp::fetchNodeObject(
|
||||
std::shared_ptr<NodeObject> nodeObject;
|
||||
|
||||
auto [writable, archive] = [&] {
|
||||
std::lock_guard lock(mutex_);
|
||||
std::shared_lock const lock(mutex_);
|
||||
return std::make_pair(writableBackend_, archiveBackend_);
|
||||
}();
|
||||
|
||||
@@ -196,12 +204,17 @@ DatabaseRotatingImp::fetchNodeObject(
|
||||
{
|
||||
{
|
||||
// Refresh the writable backend pointer
|
||||
std::lock_guard lock(mutex_);
|
||||
std::shared_lock const lock(mutex_);
|
||||
writable = writableBackend_;
|
||||
}
|
||||
|
||||
// Update writable backend with data from the archive backend
|
||||
// if (duplicate)
|
||||
// Promote every archive hit into the writable backend.
|
||||
// Ordinary reads pass duplicate=false; the rotation copy
|
||||
// pass passes true. Both must copy. The next rotation
|
||||
// deletes this archive, and copyNode only walks the state
|
||||
// tree, so an unpromoted header or transaction node
|
||||
// disappears while SQL still names it.
|
||||
(void)duplicate;
|
||||
writable->store(nodeObject);
|
||||
}
|
||||
}
|
||||
@@ -217,7 +230,7 @@ DatabaseRotatingImp::for_each(
|
||||
std::function<void(std::shared_ptr<NodeObject>)> f)
|
||||
{
|
||||
auto [writable, archive] = [&] {
|
||||
std::lock_guard lock(mutex_);
|
||||
std::shared_lock const lock(mutex_);
|
||||
return std::make_pair(writableBackend_, archiveBackend_);
|
||||
}();
|
||||
|
||||
|
||||
@@ -22,7 +22,10 @@
|
||||
|
||||
#include <xrpld/nodestore/DatabaseRotating.h>
|
||||
|
||||
#include <xrpl/basics/ReaderPreferringSharedMutex.h>
|
||||
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
|
||||
namespace ripple {
|
||||
namespace NodeStore {
|
||||
@@ -93,7 +96,7 @@ public:
|
||||
private:
|
||||
std::shared_ptr<Backend> writableBackend_;
|
||||
std::shared_ptr<Backend> archiveBackend_;
|
||||
mutable std::mutex mutex_;
|
||||
mutable ReaderPreferringSharedMutex mutex_;
|
||||
|
||||
std::shared_ptr<NodeObject>
|
||||
fetchNodeObject(
|
||||
|
||||
@@ -32,11 +32,16 @@
|
||||
#include <xrpld/overlay/detail/PeerImp.h>
|
||||
#include <xrpld/overlay/detail/Tuning.h>
|
||||
#include <xrpld/perflog/PerfLog.h>
|
||||
#include <xrpld/shamap/Family.h>
|
||||
#include <xrpld/shamap/SHAMap.h>
|
||||
#include <xrpld/shamap/SHAMapNodeID.h>
|
||||
#include <xrpld/shamap/SHAMapTreeNode.h>
|
||||
#include <xrpl/basics/UptimeClock.h>
|
||||
#include <xrpl/basics/base64.h>
|
||||
#include <xrpl/basics/random.h>
|
||||
#include <xrpl/basics/safe_cast.h>
|
||||
#include <xrpl/beast/core/LexicalCast.h>
|
||||
#include <xrpl/protocol/HashPrefix.h>
|
||||
#include <xrpl/protocol/digest.h>
|
||||
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
@@ -46,6 +51,7 @@
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <numeric>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
@@ -58,10 +64,48 @@ std::chrono::milliseconds constexpr peerHighLatency{300};
|
||||
|
||||
/** How often we PING the peer to check for latency and sendq probe */
|
||||
std::chrono::seconds constexpr peerTimerInterval{60};
|
||||
} // namespace
|
||||
|
||||
// TODO: Remove this exclusion once unit tests are added after the hotfix
|
||||
// release.
|
||||
// Null node store (type=rwdb) only: find a requested tree node by walking
|
||||
// the child links of resident ledgers. Uses resident-only lookups, since
|
||||
// seq and hash come from the peer and must not trigger SQL / node-store
|
||||
// loads or edit the complete-ledger set.
|
||||
std::shared_ptr<SHAMapTreeNode>
|
||||
fetchLinkedTreeNode(
|
||||
Application& app,
|
||||
uint256 const& hash,
|
||||
std::uint32_t seq,
|
||||
std::optional<SHAMapNodeID> const& nodeId)
|
||||
{
|
||||
if (!nodeId || !app.getNodeFamily().isNullBackend())
|
||||
return {};
|
||||
|
||||
auto findInMap = [&](SHAMap const& map) -> std::shared_ptr<SHAMapTreeNode> {
|
||||
auto node = map.getLinkedNode(*nodeId);
|
||||
if (node && node->getHash().as_uint256() == hash)
|
||||
return node;
|
||||
return {};
|
||||
};
|
||||
|
||||
auto findInLedger = [&](std::shared_ptr<Ledger const> const& ledger)
|
||||
-> std::shared_ptr<SHAMapTreeNode> {
|
||||
if (!ledger)
|
||||
return {};
|
||||
if (auto node = findInMap(ledger->stateMap()))
|
||||
return node;
|
||||
return findInMap(ledger->txMap());
|
||||
};
|
||||
|
||||
if (seq)
|
||||
{
|
||||
if (auto node =
|
||||
findInLedger(app.getLedgerMaster().getResidentLedgerBySeq(seq)))
|
||||
return node;
|
||||
}
|
||||
if (auto node = findInLedger(app.getLedgerMaster().getClosedLedger()))
|
||||
return node;
|
||||
return findInLedger(app.getLedgerMaster().getValidatedLedger());
|
||||
}
|
||||
} // namespace
|
||||
|
||||
PeerImp::PeerImp(
|
||||
Application& app,
|
||||
@@ -2478,13 +2522,65 @@ PeerImp::onMessage(std::shared_ptr<protocol::TMGetObjectByHash> const& m)
|
||||
// need to inject the NodeStore interfaces.
|
||||
std::uint32_t seq{obj.has_ledgerseq() ? obj.ledgerseq() : 0};
|
||||
auto nodeObject{app_.getNodeStore().fetchNodeObject(hash, seq)};
|
||||
|
||||
void const* dataPtr = nullptr;
|
||||
std::size_t dataSize = 0;
|
||||
Blob treeBlob;
|
||||
|
||||
if (nodeObject)
|
||||
{
|
||||
dataPtr = nodeObject->getData().data();
|
||||
dataSize = nodeObject->getData().size();
|
||||
}
|
||||
else if (
|
||||
auto treeNode =
|
||||
app_.getNodeFamily().getTreeNodeCache()->fetch(hash))
|
||||
{
|
||||
Serializer s;
|
||||
treeNode->serializeWithPrefix(s);
|
||||
treeBlob = std::move(s.modData());
|
||||
dataPtr = treeBlob.data();
|
||||
dataSize = treeBlob.size();
|
||||
}
|
||||
else if (
|
||||
auto treeNode = fetchLinkedTreeNode(
|
||||
app_,
|
||||
hash,
|
||||
seq,
|
||||
obj.has_nodeid() ? deserializeSHAMapNodeID(obj.nodeid())
|
||||
: std::nullopt))
|
||||
{
|
||||
Serializer s;
|
||||
treeNode->serializeWithPrefix(s);
|
||||
treeBlob = std::move(s.modData());
|
||||
dataPtr = treeBlob.data();
|
||||
dataSize = treeBlob.size();
|
||||
}
|
||||
else if (
|
||||
packet.type() == protocol::TMGetObjectByHash::otLEDGER &&
|
||||
app_.getNodeFamily().isNullBackend())
|
||||
{
|
||||
// Null node store: serve the header of a resident
|
||||
// ledger in the same wire format the node store uses.
|
||||
// Resident-only, so a peer cannot trigger a load.
|
||||
if (auto ledger =
|
||||
app_.getLedgerMaster().getResidentLedgerByHash(
|
||||
hash))
|
||||
{
|
||||
Serializer s(sizeof(LedgerInfo) + 4);
|
||||
s.add32(HashPrefix::ledgerMaster);
|
||||
addRaw(ledger->info(), s);
|
||||
treeBlob = std::move(s.modData());
|
||||
dataPtr = treeBlob.data();
|
||||
dataSize = treeBlob.size();
|
||||
}
|
||||
}
|
||||
|
||||
if (dataPtr)
|
||||
{
|
||||
protocol::TMIndexedObject& newObj = *reply.add_objects();
|
||||
newObj.set_hash(hash.begin(), hash.size());
|
||||
newObj.set_data(
|
||||
&nodeObject->getData().front(),
|
||||
nodeObject->getData().size());
|
||||
newObj.set_data(dataPtr, dataSize);
|
||||
|
||||
if (obj.has_nodeid())
|
||||
newObj.set_index(obj.nodeid());
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <xrpld/app/misc/NetworkOPs.h>
|
||||
#include <xrpld/core/Config.h>
|
||||
#include <xrpld/rpc/detail/TransactionSign.h>
|
||||
#include <xrpl/hook/Enum.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/json/json_writer.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
@@ -60,6 +61,14 @@ MAGIC_ENUM_FLAG(ripple::NFTokenCreateOfferFlags);
|
||||
MAGIC_ENUM_FLAG(ripple::ClaimRewardFlags);
|
||||
MAGIC_ENUM_16(ripple::AccountFlags);
|
||||
|
||||
// hook_return_code spans [-10024, 0] because of INVALID_FLOAT
|
||||
template <>
|
||||
struct magic_enum::customize::enum_range<hook_api::hook_return_code>
|
||||
{
|
||||
static constexpr int min = -10024;
|
||||
static constexpr int max = 0;
|
||||
};
|
||||
|
||||
namespace ripple {
|
||||
|
||||
class Definitions
|
||||
@@ -83,6 +92,50 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
template <typename EnumType>
|
||||
void
|
||||
addEnumToJson(Json::Value& json)
|
||||
{
|
||||
json = Json::objectValue;
|
||||
// No structured binding: STR() is a lambda, and capturing a
|
||||
// binding needs P1091 (Clang 16+).
|
||||
for (auto const& entry : magic_enum::enum_entries<EnumType>())
|
||||
{
|
||||
auto const name = entry.second;
|
||||
json[STR(name)] = static_cast<Json::Int>(entry.first);
|
||||
}
|
||||
}
|
||||
|
||||
// params is the stringified C parameter list, e.g.
|
||||
// "(uint32_t read_ptr, uint32_t read_len)"
|
||||
void
|
||||
addHookApi(
|
||||
Json::Value& json,
|
||||
std::string const& returnType,
|
||||
std::string const& name,
|
||||
std::string params,
|
||||
uint256 const& amendment)
|
||||
{
|
||||
Json::Value& fn = json[name];
|
||||
fn["return"] = returnType;
|
||||
fn["params"] = Json::arrayValue;
|
||||
boost::trim_if(params, boost::is_any_of("() "));
|
||||
std::vector<std::string> parts;
|
||||
if (!params.empty())
|
||||
boost::split(parts, params, boost::is_any_of(","));
|
||||
for (auto& part : parts)
|
||||
{
|
||||
boost::trim(part);
|
||||
auto const sp = part.rfind(' ');
|
||||
Json::Value p = Json::objectValue;
|
||||
p[jss::type] = part.substr(0, sp);
|
||||
p[jss::name] = part.substr(sp + 1);
|
||||
fn["params"].append(p);
|
||||
}
|
||||
if (amendment != uint256{})
|
||||
fn["amendment"] = featureToName(amendment);
|
||||
}
|
||||
|
||||
Json::Value
|
||||
generate()
|
||||
{
|
||||
@@ -385,6 +438,29 @@ private:
|
||||
|
||||
ret[jss::native_currency_code] = systemCurrencyCode();
|
||||
|
||||
// Hook API: enough to regenerate hook/extern.h, hookapi.h keylet
|
||||
// constants and error.h without the xahaud source tree.
|
||||
Json::Value& hook = ret[jss::HOOK];
|
||||
hook = Json::objectValue;
|
||||
hook[jss::API] = Json::objectValue;
|
||||
#pragma push_macro("HOOK_API_DEFINITION")
|
||||
#undef HOOK_API_DEFINITION
|
||||
#define HOOK_API_DEFINITION( \
|
||||
RETURN_TYPE, FUNCTION_NAME, PARAMS_TUPLE, AMENDMENT) \
|
||||
addHookApi( \
|
||||
hook[jss::API], \
|
||||
#RETURN_TYPE, \
|
||||
#FUNCTION_NAME, \
|
||||
#PARAMS_TUPLE, \
|
||||
AMENDMENT);
|
||||
#include <xrpl/hook/hook_api.macro>
|
||||
#undef HOOK_API_DEFINITION
|
||||
#pragma pop_macro("HOOK_API_DEFINITION")
|
||||
|
||||
addEnumToJson<hook_api::keylet_code::keylet_code>(
|
||||
hook[jss::KEYLET_TYPES]);
|
||||
addEnumToJson<hook_api::hook_return_code>(hook[jss::RETURN_CODES]);
|
||||
|
||||
// generate hash
|
||||
{
|
||||
const std::string out = Json::FastWriter().write(ret);
|
||||
|
||||
@@ -82,6 +82,14 @@ public:
|
||||
|
||||
virtual void
|
||||
reset() = 0;
|
||||
|
||||
/** True when this family's node store does not persist nodes.
|
||||
|
||||
type=rwdb uses NullFactory. Tree nodes live only through
|
||||
in-memory SHAMap pointers. The flag is per family.
|
||||
*/
|
||||
virtual bool
|
||||
isNullBackend() const = 0;
|
||||
};
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
@@ -87,8 +87,15 @@ public:
|
||||
acquire(hash, seq);
|
||||
}
|
||||
|
||||
bool
|
||||
isNullBackend() const override
|
||||
{
|
||||
return nullBackend_;
|
||||
}
|
||||
|
||||
private:
|
||||
Application& app_;
|
||||
bool const nullBackend_;
|
||||
NodeStore::Database& db_;
|
||||
beast::Journal const j_;
|
||||
|
||||
|
||||
@@ -367,6 +367,18 @@ public:
|
||||
void
|
||||
invariants() const;
|
||||
|
||||
/** Child pointers copied from a discarded same-hash inner onto the
|
||||
TreeNodeCache winner during canonicalize.
|
||||
*/
|
||||
static std::uint64_t
|
||||
canonicalInnerBranchesHarvested();
|
||||
|
||||
/** Return the node at id if every parent on the path already has a
|
||||
child pointer. Does not fetch from the node store.
|
||||
*/
|
||||
std::shared_ptr<SHAMapTreeNode>
|
||||
getLinkedNode(SHAMapNodeID const& id) const;
|
||||
|
||||
private:
|
||||
using SharedPtrNodeStack =
|
||||
std::stack<std::pair<std::shared_ptr<SHAMapTreeNode>, SHAMapNodeID>>;
|
||||
|
||||
@@ -27,6 +27,7 @@ namespace ripple {
|
||||
|
||||
NodeFamily::NodeFamily(Application& app, CollectorManager& cm)
|
||||
: app_(app)
|
||||
, nullBackend_(app.config().nullBackend())
|
||||
, db_(app.getNodeStore())
|
||||
, j_(app.journal("NodeFamily"))
|
||||
, fbCache_(std::make_shared<FullBelowCache>(
|
||||
|
||||
@@ -19,14 +19,53 @@
|
||||
|
||||
#include <xrpld/shamap/SHAMap.h>
|
||||
#include <xrpld/shamap/SHAMapAccountStateLeafNode.h>
|
||||
#include <xrpld/shamap/SHAMapInnerNode.h>
|
||||
#include <xrpld/shamap/SHAMapNodeID.h>
|
||||
#include <xrpld/shamap/SHAMapSyncFilter.h>
|
||||
#include <xrpld/shamap/SHAMapTxLeafNode.h>
|
||||
#include <xrpld/shamap/SHAMapTxPlusMetaLeafNode.h>
|
||||
#include <xrpl/basics/contract.h>
|
||||
|
||||
#include <atomic>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
namespace {
|
||||
|
||||
std::atomic<std::uint64_t> canonicalInnerBranchesHarvested_{0};
|
||||
|
||||
/** Join linked children from a discarded same-hash inner onto the cache winner.
|
||||
|
||||
Same-hash inners have identical branch masks and child hashes. Installing a
|
||||
child the winner lacks can only add linkage; it cannot change content.
|
||||
*/
|
||||
void
|
||||
mergeCanonicalInner(
|
||||
std::shared_ptr<SHAMapTreeNode> const& canonical,
|
||||
std::shared_ptr<SHAMapTreeNode> const& incoming)
|
||||
{
|
||||
// Same hash implies same node type, so checking one side suffices.
|
||||
if (!canonical->isInner())
|
||||
return;
|
||||
|
||||
auto* cached = static_cast<SHAMapInnerNode*>(canonical.get());
|
||||
auto* other = static_cast<SHAMapInnerNode*>(incoming.get());
|
||||
|
||||
for (int branch = 0; branch < SHAMapInnerNode::branchFactor; ++branch)
|
||||
{
|
||||
if (cached->isEmptyBranch(branch) || cached->getChildPointer(branch))
|
||||
continue;
|
||||
auto child = other->getChild(branch);
|
||||
if (!child)
|
||||
continue;
|
||||
cached->canonicalizeChild(branch, std::move(child));
|
||||
canonicalInnerBranchesHarvested_.fetch_add(
|
||||
1, std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
[[nodiscard]] std::shared_ptr<SHAMapLeafNode>
|
||||
makeTypedLeaf(
|
||||
SHAMapNodeType type,
|
||||
@@ -1279,7 +1318,35 @@ SHAMap::canonicalize(
|
||||
node->getHash() == hash,
|
||||
"ripple::SHAMap::canonicalize : node hash do match");
|
||||
|
||||
auto incoming = node;
|
||||
f_.getTreeNodeCache()->canonicalize_replace_client(hash.as_uint256(), node);
|
||||
if (node != incoming)
|
||||
mergeCanonicalInner(node, incoming);
|
||||
}
|
||||
|
||||
std::uint64_t
|
||||
SHAMap::canonicalInnerBranchesHarvested()
|
||||
{
|
||||
return canonicalInnerBranchesHarvested_.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
std::shared_ptr<SHAMapTreeNode>
|
||||
SHAMap::getLinkedNode(SHAMapNodeID const& id) const
|
||||
{
|
||||
auto node = root_;
|
||||
SHAMapNodeID current;
|
||||
while (node && current.getDepth() < id.getDepth())
|
||||
{
|
||||
if (!node->isInner())
|
||||
return {};
|
||||
auto* inner = static_cast<SHAMapInnerNode*>(node.get());
|
||||
auto const branch = selectBranch(current, id.getNodeID());
|
||||
if (inner->isEmptyBranch(branch))
|
||||
return {};
|
||||
node = inner->getChild(branch);
|
||||
current = current.getChildNodeID(branch);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -191,7 +191,7 @@ SHAMap::gmn_ProcessNodes(MissingNodes& mn, MissingNodes::StackEntry& se)
|
||||
fullBelow = false;
|
||||
}
|
||||
else if (
|
||||
!backed_ ||
|
||||
!backed_ || f_.isNullBackend() ||
|
||||
!f_.getFullBelowCache()->touch_if_exists(childHash.as_uint256()))
|
||||
{
|
||||
bool pending = false;
|
||||
@@ -248,7 +248,7 @@ SHAMap::gmn_ProcessNodes(MissingNodes& mn, MissingNodes::StackEntry& se)
|
||||
if (fullBelow)
|
||||
{ // No partial node encountered below this node
|
||||
node->setFullBelowGen(mn.generation_);
|
||||
if (backed_)
|
||||
if (backed_ && !f_.isNullBackend())
|
||||
{
|
||||
f_.getFullBelowCache()->insert(node->getHash().as_uint256());
|
||||
}
|
||||
@@ -605,7 +605,8 @@ SHAMap::addKnownNode(
|
||||
}
|
||||
|
||||
auto childHash = inner->getChildHash(branch);
|
||||
if (f_.getFullBelowCache()->touch_if_exists(childHash.as_uint256()))
|
||||
if (!f_.isNullBackend() &&
|
||||
f_.getFullBelowCache()->touch_if_exists(childHash.as_uint256()))
|
||||
{
|
||||
return SHAMapAddNode::duplicate();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user