Compare commits

..

22 Commits

Author SHA1 Message Date
Nicholas Dudfield
2260e3b0c9 fix(ledger): draw inbound peer samples from default_prng
dev has no Application::getPrng. Peer selection is outside the harness
schedule, so use the per-thread default engine. Draws still go through
rand_int.
2026-09-23 18:01:05 +07:00
Nicholas Dudfield
fe43214c4b fix(test): suffix multi-runner ipc names with the parent pid
Every unittest process, including one job, removes and recreates the
shared-memory object and message queue. Give that pair a parent-pid
suffix and publish the names in the environment so spawned children
and the in-process child open only that pair.
2026-09-23 17:58:15 +07:00
Nicholas Dudfield
bf6fa019ba ci: tripwire for implementation-defined random algorithms 2026-09-23 17:35:10 +07:00
Nicholas Dudfield
1f8e30537f fix(basics): allow zero-bit groups in random word extraction 2026-09-23 17:35:10 +07:00
Nicholas Dudfield
54604e7a9a fix(basics): size randomU64 from the engine range
numeric_limits digits is the storage width, not how many values the
engine emits. A 32-bit engine in a 64-bit result type was treated as
a full word, which biases a range and can reject forever. A full
64-bit range is still one draw. Other power-of-two ranges are
concatenated. The rest follow the standard independent_bits rejection.
2026-09-23 17:35:09 +07:00
Nicholas Dudfield
d3d5e86dec fix(ledger): sample inbound peers with Knuth's Algorithm S
std::sample is not the same on libstdc++ and libc++, so saturated
acquisition cells diverged on Linux after rand_int was made portable.
Selection-sample the id-sorted peer list with rand_int, and take a
must-keep tail without a zero-width draw.
2026-09-23 17:35:07 +07:00
Nicholas Dudfield
99465e29e1 fix(basics): map rand_int without the standard distribution
std::uniform_int_distribution is not the same on libc++ and libstdc++,
so a seeded engine produced different harness event streams on Linux.
Rejection-sample a closed range from the engine's own 64-bit output.
Seeding overloads are unchanged.
2026-09-23 17:32:52 +07:00
Nicholas Dudfield
d8096685ee test: drop ConcurrentLedgerSave_test from the dev backport
It depends on a jtx helper (saveValidatedLedger) that exists only on the
feature-export-rng lineage. The production fix it covers stays; the test
can return with the helper if that is ported.
2026-09-23 10:02:51 +07:00
Nicholas Dudfield
52f112d52b fix(overlay): bound protobuf parsing to the current frame 2026-09-23 09:46:23 +07:00
Nicholas Dudfield
cf2ed8c299 fix(ledger): count publication as progress while running 2026-09-23 09:46:22 +07:00
Nicholas Dudfield
15eed3a497 fix(app): persist genesis successor state tree 2026-09-23 09:46:22 +07:00
Nicholas Dudfield
3d7c3236fc fix(app): keep first-ledger publication state per application 2026-09-23 09:46:21 +07:00
Nicholas Dudfield
1da084b3b8 fix(txq): keep parent-hash ordering keys per candidate 2026-09-23 09:46:21 +07:00
Nicholas Dudfield
541edc48c9 fix(overlay): isolate relay duplicate tracking per Slots instance 2026-09-23 09:46:20 +07:00
Nicholas Dudfield
cd9fb29eec fix(consensus): isolate validation keep-range refresh deadlines 2026-09-23 09:46:20 +07:00
Nicholas Dudfield
fd85c5d122 fix(consensus): serialize consensus phase publication 2026-09-23 09:46:19 +07:00
Nicholas Dudfield
723f4f28b6 fix(tx): synchronize mutable transaction response state 2026-09-23 09:46:19 +07:00
Nicholas Dudfield
9e7805e5aa fix(ledger): synchronize acquire state access and received-data admission 2026-09-23 09:46:18 +07:00
Nicholas Dudfield
cff7ea906c fix(ledger): lock acquire completion state before reporting it 2026-09-23 09:46:18 +07:00
Nicholas Dudfield
15739bf13d fix(ledger): synchronize ledger-age log throttles 2026-09-23 09:46:18 +07:00
Nicholas Dudfield
e08de61273 fix(rdb): keep ledger-save SQL formatters local to each call 2026-09-23 09:46:17 +07:00
tequ
902ed9b492 Use normal consequences factory for standard transactions (#774)
Replace redundant custom consequence factories that returned normal consequences with the built-in Normal factory.
2026-09-21 21:12:15 +10:00
42 changed files with 946 additions and 1638 deletions

25
.github/workflows/rng-tripwire.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: rng-tripwire
on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Check for vendor-defined random algorithms
run: python3 Builds/rng_tripwire/rng_tripwire.py
- name: What happened?
if: failure()
env:
MESSAGE: |
A standard-library random algorithm showed up outside the
allowlist. These algorithms are not the same on every standard
library, so a new call site can change a deterministic trace.
Either draw through ripple::rand_int, or add the path to
Builds/rng_tripwire/allowlist.txt with a one-line justification.
Comments are ignored. See Builds/rng_tripwire/allowlist.txt.
run: |
echo "${MESSAGE}"
exit 1

View File

@@ -0,0 +1,13 @@
# path-prefix justification
# A hit under src/ or include/ is accepted only when its path starts with one
# of these prefixes. The justification is for reviewers; the script ignores it.
src/test/ Tests are not on the consensus schedule.
include/xrpl/basics/random.h Seeding overloads only. rand_int does not call these distributions.
include/xrpl/basics/hardened_hash.h One-time hash salt drawn from OS entropy.
include/xrpl/crypto/csprng.h CSPRNG declaration. OS entropy, not a consensus draw.
src/libxrpl/crypto/csprng.cpp OS entropy for the CSPRNG.
src/xrpld/peerfinder/ default_prng shuffles. Not the harness engine.
src/xrpld/overlay/detail/PeerImp.cpp Fault-injection seeds. RuntimeConfig only, not the harness engine.
src/xrpld/app/consensus/ConsensusExtensions.cpp Fault-injection seeds. RuntimeConfig only, not the harness engine.
src/xrpld/overlay/detail/OverlayImpl.cpp std::shuffle of relay peers. Runs only when TX_REDUCE_RELAY_ENABLE is on, and that flag is off. Uses app_.getPrng when it does run.

View File

@@ -0,0 +1,125 @@
#!/usr/bin/env python3
"""Fail if a vendor-defined <random> algorithm appears outside the allowlist.
Scans src/ and include/ for the standard algorithms whose mapping is not
portable across standard libraries. Comments are ignored. A hit is accepted
only when its path is listed in allowlist.txt. Unlisted hits are printed as
file:line and the process exits non-zero.
"""
import re
import sys
from pathlib import Path
ALGORITHMS = (
"uniform_int_distribution",
"uniform_real_distribution",
"bernoulli_distribution",
"binomial_distribution",
"negative_binomial_distribution",
"geometric_distribution",
"poisson_distribution",
"exponential_distribution",
"gamma_distribution",
"weibull_distribution",
"extreme_value_distribution",
"normal_distribution",
"lognormal_distribution",
"chi_squared_distribution",
"cauchy_distribution",
"fisher_f_distribution",
"student_t_distribution",
"discrete_distribution",
"piecewise_constant_distribution",
"piecewise_linear_distribution",
"sample",
"shuffle",
"generate_canonical",
"random_device",
)
HIT = re.compile(r"\bstd::(?:" + "|".join(ALGORITHMS) + r")\b")
SUFFIXES = {".h", ".hh", ".hpp", ".cpp", ".cc", ".cxx", ".ipp", ".inc"}
def repo_root() -> Path:
here = Path(__file__).resolve().parent
for candidate in (here, *here.parents):
if (candidate / "src").is_dir() and (candidate / "include").is_dir():
return candidate
sys.exit("rng_tripwire: cannot find the repository root")
def load_allowlist(path: Path) -> list[tuple[str, str]]:
entries = []
for raw in path.read_text().splitlines():
line = raw.split("#", 1)[0].strip()
if not line:
continue
prefix, _, why = line.partition(" ")
entries.append((prefix.strip(), why.strip()))
return entries
def allowed(rel: str, entries: list[tuple[str, str]]) -> bool:
for prefix, _why in entries:
if rel == prefix or rel.startswith(prefix):
return True
return False
def strip_comments(text: str) -> list[str]:
lines = []
in_block = False
for line in text.splitlines():
out = []
i = 0
while i < len(line):
if in_block:
end = line.find("*/", i)
if end < 0:
i = len(line)
break
in_block = False
i = end + 2
continue
if line.startswith("//", i):
break
if line.startswith("/*", i):
in_block = True
i += 2
continue
out.append(line[i])
i += 1
lines.append("".join(out))
return lines
def main() -> int:
root = repo_root()
allow_path = Path(__file__).resolve().parent / "allowlist.txt"
entries = load_allowlist(allow_path)
hits = []
for base in ("src", "include"):
for path in sorted((root / base).rglob("*")):
if not path.is_file() or path.suffix not in SUFFIXES:
continue
rel = path.relative_to(root).as_posix()
try:
text = path.read_text(encoding="utf-8")
except UnicodeError:
text = path.read_text(encoding="latin-1")
for number, line in enumerate(strip_comments(text), start=1):
if HIT.search(line):
hits.append((allowed(rel, entries), f"{rel}:{number}"))
unlisted = [item for ok, item in hits if not ok]
for ok, item in hits:
print(("ALLOW " if ok else "HIT ") + item)
print(f"{len(hits)} hits, {len(unlisted)} unlisted")
for item in unlisted:
print(item)
return 1 if unlisted else 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -105,7 +105,6 @@
#define sfMPTAmount ((3U << 16U) + 26U)
#define sfIssuerNode ((3U << 16U) + 27U)
#define sfSubjectNode ((3U << 16U) + 28U)
#define sfTime ((3U << 16U) + 96U)
#define sfTouchCount ((3U << 16U) + 97U)
#define sfAccountIndex ((3U << 16U) + 98U)
#define sfAccountCount ((3U << 16U) + 99U)
@@ -223,7 +222,6 @@
#define sfProvider ((7U << 16U) + 30U)
#define sfMPTokenMetadata ((7U << 16U) + 31U)
#define sfCredentialType ((7U << 16U) + 32U)
#define sfJsonTxDelta ((7U << 16U) + 96U)
#define sfHookName ((7U << 16U) + 97U)
#define sfRemarkValue ((7U << 16U) + 98U)
#define sfRemarkName ((7U << 16U) + 99U)

View File

@@ -22,6 +22,7 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/beast/xor_shift_engine.h>
#include <bit>
#include <cstddef>
#include <cstdint>
#include <cstring>
@@ -49,6 +50,87 @@ namespace detail {
// Determines if a type can be called like an Engine
template <class Engine, class Result = typename Engine::result_type>
using is_engine = std::is_invocable_r<Result, Engine>;
// 64 bits from the engine. Width comes from max()-min(), not from the
// storage type: a 32-bit engine may use a 64-bit result_type.
template <class Engine>
std::uint64_t
randomU64(Engine& engine)
{
static_assert(std::is_unsigned_v<typename Engine::result_type>);
static_assert(
std::numeric_limits<typename Engine::result_type>::digits <= 64);
static_assert(Engine::min() < Engine::max());
auto const draw = [&engine]() -> std::uint64_t {
return static_cast<std::uint64_t>(engine() - Engine::min());
};
// A wrapped cardinality of 0 means 2^64 values: one full-range draw.
constexpr auto span =
static_cast<std::uint64_t>(Engine::max() - Engine::min());
constexpr std::uint64_t range = span + 1u;
constexpr bool full = range == 0;
constexpr bool powerOfTwo = full || (range & (range - 1u)) == 0;
if constexpr (powerOfTwo)
{
constexpr int width = full ? 64 : std::bit_width(range) - 1;
if constexpr (width >= 64)
return draw();
std::uint64_t value = 0;
int filled = 0;
while (filled < 64)
{
auto const take = width < (64 - filled) ? width : (64 - filled);
auto const mask = (std::uint64_t{1} << take) - 1;
value |= (draw() & mask) << filled;
filled += take;
}
return value;
}
else
{
// [rand.adapt.ibits] for w = 64. R is not a power of two.
constexpr int m = std::bit_width(range) - 1;
constexpr int nCeil = (64 + m - 1) / m;
constexpr int w0Try = 64 / nCeil;
constexpr auto y0Try = (std::uint64_t{1} << w0Try) * (range >> w0Try);
constexpr bool bump =
(range - y0Try) > (y0Try / static_cast<std::uint64_t>(nCeil));
constexpr int n = bump ? nCeil + 1 : nCeil;
constexpr int w0 = 64 / n;
constexpr int n0 = n - (64 % n);
constexpr auto y0 = (std::uint64_t{1} << w0) * (range >> w0);
constexpr auto y1 =
(std::uint64_t{1} << (w0 + 1)) * (range >> (w0 + 1));
// R == 3 gives n == 65 and w0 == 0. The first group still
// consumes its draw even though it contributes no output bits.
static_assert(w0 >= 0 && w0 < 63);
std::uint64_t word = 0;
for (int k = 0; k != n0; ++k)
{
std::uint64_t u;
do
{
u = draw();
} while (u >= y0);
word = (word << w0) + (u & ((std::uint64_t{1} << w0) - 1));
}
for (int k = n0; k != n; ++k)
{
std::uint64_t u;
do
{
u = draw();
} while (u >= y1);
constexpr int bits = w0 + 1;
word = (word << bits) + (u & ((std::uint64_t{1} << bits) - 1));
}
return word;
}
}
} // namespace detail
/** Return the default random engine.
@@ -116,10 +198,24 @@ rand_int(Engine& engine, Integral min, Integral max)
{
XRPL_ASSERT(max > min, "ripple::rand_int : max over min inputs");
// This should have no state and constructing it should
// be very cheap. If that turns out not to be the case
// it could be hand-optimized.
return std::uniform_int_distribution<Integral>(min, max)(engine);
// Closed interval. Rejection sampling keeps the result uniform and the
// same on libc++ and libstdc++ for a given engine sequence.
using U = std::make_unsigned_t<Integral>;
auto const span = static_cast<U>(static_cast<U>(max) - static_cast<U>(min));
auto const count = static_cast<std::uint64_t>(span) + 1u;
if (count == 0)
return static_cast<Integral>(detail::randomU64(engine));
// Values below this threshold are the leftover that would bias x % count.
auto const slack = static_cast<std::uint64_t>(-count) % count;
std::uint64_t draw;
do
{
draw = detail::randomU64(engine);
} while (draw < slack);
auto const offset = static_cast<U>(draw % count);
return static_cast<Integral>(static_cast<U>(static_cast<U>(min) + offset));
}
template <class Integral>

View File

@@ -1,739 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2012-2014 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.
*/
//==============================================================================
#ifndef RIPPLE_PROTOCOL_JSONTXSIGNATURES_H_INCLUDED
#define RIPPLE_PROTOCOL_JSONTXSIGNATURES_H_INCLUDED
#include <xrpl/json/json_reader.h>
#include <xrpl/json/json_writer.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <xrpl/protocol/STTx.h>
#include <boost/algorithm/string.hpp>
#include <algorithm>
#include <charconv>
#include <cmath>
#include <cstdint>
#include <functional>
#include <map>
#include <stdexcept>
#include <string>
#include <string_view>
#include <unordered_map>
#include <utility>
#include <vector>
namespace ripple {
//------------------------------------------------------------------------------
// jsontx: plaintext-JSON signing support (featureJsonTx)
//
// The delta is attacker-controlled: it arrives over the wire beside a binary
// transaction and is not covered by the signature it helps reconstruct. Every
// bound below is therefore explicit, and unsanitize_jsontx accepts only the
// exact encoding sanitize_jsontx would have produced.
//------------------------------------------------------------------------------
// Every constant below decides whether a transaction is valid, so each is a
// consensus rule from the moment featureJsonTx activates and cannot be tuned
// afterwards without a further amendment. jsontx_min_copy and jsontx_max_cand
// are normative encoder parameters: sanitize_jsontx's output is compared byte
// for byte in jsontx_verify, so changing either changes which deltas verify.
//
// jsontx_max_diff bounds what an ordinary signer can send, not just what a
// node will accept. A pretty-printed document costs roughly 8 bytes and 2.3
// ops per line of delta (12 bytes / 2.3 ops at 4-space indent with CRLF), so
// 2048 bytes covers about 170 lines - a payment with a dozen memos, indented.
// sanitize_jsontx enforces both caps itself so that a document the encoder
// accepts is never one unsanitize_jsontx then refuses.
inline constexpr std::size_t jsontx_max_text = 8192; // canonical and original
inline constexpr std::size_t jsontx_max_diff = 2048; // delta bytes
inline constexpr std::size_t jsontx_max_ops =
jsontx_max_diff / 2; // delta instructions
inline constexpr std::size_t jsontx_min_copy = 4; // encoder match threshold
inline constexpr std::size_t jsontx_max_cand = 64; // encoder candidate cap
// Case-insensitive field-name -> canonical SField. Built once from
// SField::knownCodeToField, the same table doServerDefinitions publishes, using
// its serializability filter (useful, binary, non-pseudo). sfInvalid if
// unknown.
inline SField const&
jsontx_field(std::string const& name)
{
static auto const tbl = [] {
std::unordered_map<std::string, SField const*> m;
for (auto const& [code, f] : SField::knownCodeToField)
if (f->isUseful() && f->isBinary() && f->fieldType < 10000 &&
!f->fieldName.empty())
m.emplace(boost::algorithm::to_lower_copy(f->fieldName), f);
return m;
}();
auto const i = tbl.find(boost::algorithm::to_lower_copy(name));
return i == tbl.end() ? sfInvalid : *i->second;
}
// Civil calendar arithmetic (Howard Hinnant's algorithm), used in both
// directions. Pure integer maths - no strptime, no timegm, no locale, no
// tzdata - because these conversions decide whether a signature verifies and
// so must give the same answer on every node forever.
inline constexpr std::int64_t
jsontx_days(int y, unsigned m, unsigned d) // days from 1970-01-01
{
y -= m <= 2;
std::int64_t const era = (y >= 0 ? y : y - 399) / 400;
unsigned const yoe = static_cast<unsigned>(y - era * 400);
unsigned const doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1;
unsigned const doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
return era * 146097 + doe - 719468;
}
inline constexpr std::int64_t jsontx_epoch_day = jsontx_days(2000, 1, 1);
// 9999-12-31T23:59:59.999Z: past this toISOString() switches to expanded years
// (+275760-09-13T...) and the fixed 24 character shape no longer holds
inline constexpr std::uint64_t jsontx_max_time =
(static_cast<std::uint64_t>(jsontx_days(9999, 12, 31) - jsontx_epoch_day) *
86400 +
86399) *
1000 +
999;
static_assert(jsontx_epoch_day == 10957); // matches chrono.h epoch_offset
// Strict Date().toISOString() -> milliseconds since the ripple epoch. Exactly
// YYYY-MM-DDTHH:MM:SS.sssZ, always UTC, always three fractional digits.
inline std::uint64_t
jsontx_iso(std::string const& s)
{
static constexpr char pat[] = "0000-00-00T00:00:00.000Z";
if (s.size() != 24)
throw std::runtime_error("jsontx: Time must be an ISO 8601 instant");
// not std::isdigit: that consults the locale, and this comparison decides
// whether a signature verifies
auto const digit = [](char c) { return c >= '0' && c <= '9'; };
for (std::size_t i = 0; i < 24; ++i)
if (pat[i] == '0' ? !digit(s[i]) : s[i] != pat[i])
throw std::runtime_error("jsontx: malformed Time");
auto const n = [&s](std::size_t i, std::size_t c) {
int v = 0;
while (c--)
v = v * 10 + (s[i++] - '0');
return v;
};
int const y = n(0, 4), mo = n(5, 2), d = n(8, 2), h = n(11, 2),
mi = n(14, 2), se = n(17, 2), ms = n(20, 3);
if (mo < 1 || mo > 12)
throw std::runtime_error("jsontx: Time month out of range");
bool const leap = (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
int const dim =
mo == 2 ? (leap ? 29 : 28) : ((mo % 2 == 1) == (mo <= 7) ? 31 : 30);
// 60 is rejected: JS cannot emit a leap second and the ledger cannot
// represent one
if (d < 1 || d > dim || h > 23 || mi > 59 || se > 59)
throw std::runtime_error("jsontx: Time out of range");
std::int64_t const t = (jsontx_days(y, mo, d) - jsontx_epoch_day) * 86400 +
h * 3600 + mi * 60 + se;
if (t < 0)
throw std::runtime_error("jsontx: Time precedes the ripple epoch");
return static_cast<std::uint64_t>(t) * 1000 + ms;
}
// The exact inverse. Total over [0, jsontx_max_time] and injective, so sfTime
// and its ISO spelling are two views of one value and the delta carries
// nothing for the field.
inline std::string
jsontx_iso_str(std::uint64_t ms)
{
if (ms > jsontx_max_time)
throw std::runtime_error("jsontx: Time out of range");
std::int64_t const z =
static_cast<std::int64_t>(ms / 86400000) + jsontx_epoch_day + 719468;
unsigned const tod = static_cast<unsigned>(ms / 1000 % 86400);
std::int64_t const era = (z >= 0 ? z : z - 146096) / 146097;
unsigned const doe = static_cast<unsigned>(z - era * 146097);
unsigned const yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
unsigned const doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
unsigned const mp = (5 * doy + 2) / 153;
unsigned const d = doy - (153 * mp + 2) / 5 + 1;
unsigned const m = mp + (mp < 10 ? 3 : -9);
std::int64_t const y =
static_cast<std::int64_t>(yoe) + era * 400 + (m <= 2);
// jsontx_max_time already bounds y to [2000, 9999], so every field below
// fits its width. Hand-rolled rather than formatted: this spelling is part
// of the signature preimage and a library's padding rules are not.
char buf[24];
auto const pad = [&buf](std::size_t at, std::uint64_t v, std::size_t w) {
while (w--)
{
buf[at + w] = static_cast<char>('0' + v % 10);
v /= 10;
}
};
pad(0, static_cast<std::uint64_t>(y), 4);
buf[4] = '-';
pad(5, m, 2);
buf[7] = '-';
pad(8, d, 2);
buf[10] = 'T';
pad(11, tod / 3600, 2);
buf[13] = ':';
pad(14, tod / 60 % 60, 2);
buf[16] = ':';
pad(17, tod % 60, 2);
buf[19] = '.';
pad(20, ms % 1000, 3);
buf[23] = 'Z';
return std::string(buf, sizeof(buf));
}
//------------------------------------------------------------------------------
// jsoncpp compatibility
//
// This is xrpl's vendored jsoncpp, which predates JSON_HAS_INT64: Json::Int is
// int and Json::UInt is unsigned int, both 32 bit, and there is no isInt64 /
// asInt64 / asUInt64. Reader::decodeNumber yields intValue or uintValue only
// while the digits fit in 32 bits; on overflow, and for any token carrying a
// '.' or an exponent, it falls through to decodeDouble and the number arrives
// as a realValue.
//
// So a large integer is not lost, but it is no longer held as an integer, and
// the digits the signer wrote are recoverable only while the double is an
// exact integer view of them. That holds to 2^53; above it consecutive doubles
// are more than 1 apart and distinct decimal integers collapse onto the same
// double. Past that point the value is refused rather than guessed at.
//------------------------------------------------------------------------------
inline constexpr double jsontx_exact_max = 9007199254740992.0; // 2^53
// Exact integer view of a json number. False if v is not a number at all, or
// is one this build cannot reproduce digit for digit.
inline bool
jsontx_exact(Json::Value const& v, std::int64_t& out)
{
switch (v.type())
{
case Json::intValue:
out = v.asInt();
return true;
case Json::uintValue:
out = static_cast<std::int64_t>(v.asUInt());
return true;
case Json::realValue:
break;
default: // including booleanValue, which isIntegral() would admit
return false;
}
double const d = v.asDouble();
if (!std::isfinite(d) || d != std::trunc(d) || d <= -jsontx_exact_max ||
d >= jsontx_exact_max)
return false;
out = static_cast<std::int64_t>(d);
return true;
}
// STI_UINT64 renders as a fixed 16 digit hex string and so needs the value
// unsigned. A negative is refused rather than wrapped: the old
// static_cast<std::uint64_t>(v.asDouble()) was undefined for a negative or
// oversized double, and a silent wrap would let -1 and 18446744073709551615
// canonicalize to the same bytes.
inline std::uint64_t
jsontx_u64(Json::Value const& v)
{
std::int64_t n = 0;
if (!jsontx_exact(v, n) || n < 0)
throw std::runtime_error(
"jsontx: UInt64 must be an exact non-negative integer");
return static_cast<std::uint64_t>(n);
}
// STUInt64::getJson renders through std::to_chars, so: lowercase, unpadded,
// and base ten for sMD_BaseTen fields. Any other spelling is one the node can
// never re-derive from its own transaction, which would stop the canonical
// form being a fixed point.
inline std::string
jsontx_u64_str(SField const& f, std::uint64_t v)
{
char buf[20];
auto const r = std::to_chars(
buf, buf + sizeof(buf), v, f.shouldMeta(SField::sMD_BaseTen) ? 10 : 16);
return std::string(buf, r.ptr);
}
// Renders a javascript number as an exact integer. Anything this build cannot
// reproduce digit for digit - a fraction, an infinity, a magnitude past 2^53 -
// is rejected outright: shortest-round-trip rendering of a double is not
// portable enough to sit in a consensus preimage, and nothing in a transaction
// needs one. Fractional amounts arrive as strings, which is what the ledger
// wants anyway.
inline std::string
jsontx_num(Json::Value const& v)
{
std::int64_t n = 0;
if (!jsontx_exact(v, n))
throw std::runtime_error("jsontx: number must be an exact integer");
return std::to_string(n);
}
// The vendored jsoncpp accepts both comment styles, and stops looking once it
// has a root value rather than requiring end of input. So
//
// {"TransactionType":"Payment",...} /* sign this to log in */
//
// parses, and the trailing text is reproduced verbatim by the delta and so
// sits inside the signed preimage. For a feature whose whole premise is that
// the signer reads what they sign, text outside the object cannot be allowed
// to ride along. Reject it before the parser ever sees the document.
//
// This validates framing only - one bracketed value, no comments, nothing
// after it. Whether the contents are legal json is still the parser's job.
inline void
jsontx_strict(std::string_view raw)
{
std::size_t depth = 0;
bool str = false, esc = false, closed = false;
for (std::size_t i = 0; i < raw.size(); ++i)
{
char const c = raw[i];
if (str)
{
if (esc)
{
if (c == 'u')
throw std::runtime_error(
"jsontx: \\u escapes are not allowed");
esc = false;
}
else if (c == '\\')
esc = true;
else if (c == '"')
str = false;
continue;
}
switch (c)
{
// json's whitespace set, exactly
case ' ':
case '\t':
case '\r':
case '\n':
continue;
case '"':
str = true;
break;
case '{':
case '[':
++depth;
break;
case '}':
case ']':
if (depth == 0)
throw std::runtime_error("jsontx: unbalanced document");
if (--depth == 0)
closed = true;
continue;
case '/':
// a bare '/' is not legal json either way, so leave that to
// the parser and reject only what the parser would accept
if (i + 1 < raw.size() &&
(raw[i + 1] == '/' || raw[i + 1] == '*'))
throw std::runtime_error(
"jsontx: comments are not allowed");
break;
default:
break;
}
if (closed)
throw std::runtime_error("jsontx: trailing data after document");
}
if (str || depth || !closed)
throw std::runtime_error("jsontx: malformed json");
}
// Returns { sanitized, diff }. `sanitized` is the canonical form: whitespace
// stripped, field names capitalized to their xahau spelling, members reordered
// by field code, numbers reformatted per field type. `diff` is a binary delta
// which, applied to `sanitized` by unsanitize_jsontx, reproduces `raw` byte for
// byte. Throws on anything it cannot canonicalize.
inline std::pair<std::string, std::string>
sanitize_jsontx(std::string_view raw)
{
if (raw.size() > jsontx_max_text)
throw std::runtime_error("jsontx: document too large");
jsontx_strict(raw);
Json::Value jv;
if (Json::Reader r;
!r.parse(raw.data(), raw.data() + raw.size(), jv) || !jv.isObject())
throw std::runtime_error("jsontx: malformed json");
// (a plain recursive lambda; deducing-this would drop the std::function)
std::function<void(
Json::Value const&, SerializedTypeID, SField const*, std::string&)>
emit = [&](Json::Value const& v,
SerializedTypeID ty,
SField const* fld,
std::string& o) {
if (v.isObject())
{
auto keys = v.getMemberNames();
o += '{';
if (ty == STI_OBJECT) // keys are xahau fields
{
std::vector<std::pair<SField const*, std::string>> ks;
for (auto const& k : keys)
{
auto const& f = jsontx_field(k);
if (f == sfInvalid)
throw std::runtime_error(
"jsontx: unknown field '" + k + "'");
ks.emplace_back(&f, k);
}
std::sort(
ks.begin(), ks.end(), [](auto const& a, auto const& b) {
return a.first->fieldCode < b.first->fieldCode;
});
for (std::size_t n = 0; n < ks.size(); ++n)
{
auto const& [f, k] = ks[n];
if (n && f == ks[n - 1].first) // e.g. "Fee" and "fee"
throw std::runtime_error(
"jsontx: duplicate field '" + f->fieldName +
"'");
if (o.back() != '{')
o += ',';
o += Json::valueToQuotedString(f->fieldName.c_str()) +
':';
emit(v[k], f->fieldType, f, o);
}
}
else // amount / issue style subobject: lexicographic, quoted
{
std::sort(keys.begin(), keys.end());
for (auto const& k : keys)
{
if (o.back() != '{')
o += ',';
o += Json::valueToQuotedString(k.c_str()) + ':';
emit(v[k], STI_NOTPRESENT, nullptr, o);
}
}
o += '}';
}
else if (v.isArray())
{
o += '[';
for (auto const& e : v)
{
if (o.back() != '[')
o += ',';
emit(e, STI_OBJECT, nullptr, o);
}
o += ']';
}
else if (v.isString())
{
// Time is spelled Date().toISOString() in the preimage and
// stored as an sfTime u64 of milliseconds. Re-emitting the
// round-tripped spelling rather than the input is what makes
// the canonical form a fixed point: any string that is not
// exactly what jsontx_iso_str produces is rejected here.
if (fld && *fld == sfTime)
o += Json::valueToQuotedString(
jsontx_iso_str(jsontx_iso(v.asString())).c_str());
else
{
// valueToQuotedString takes a char const* and so stops at
// an embedded NUL, which would let two different strings
// canonicalize to the same bytes. Nothing in a
// transaction needs one, so refuse rather than truncate.
auto const t = v.asString();
if (t.find('\0') != std::string::npos)
throw std::runtime_error("jsontx: NUL in string value");
o += Json::valueToQuotedString(t.c_str());
}
}
else if (v.isBool())
o += v.asBool() ? "true" : "false";
else if (v.isNull())
throw std::runtime_error("jsontx: null value");
else if (ty == STI_UINT8 || ty == STI_UINT16 || ty == STI_UINT32)
o += jsontx_num(v); // small ints stay bare
else if (ty == STI_UINT64)
{
if (!fld)
throw std::runtime_error("jsontx: unnamed UInt64");
o += '"' + jsontx_u64_str(*fld, jsontx_u64(v)) + '"';
}
else // amounts, u64, everything else the ledger wants as a string
o += Json::valueToQuotedString(jsontx_num(v).c_str());
};
std::string out;
emit(jv, STI_OBJECT, nullptr, out);
// Greedy copy/insert delta over `raw`, sourcing from `out`.
// op 0x00 <varint len> <bytes> literal
// op 0x01 <varint off> <varint len> copy from sanitized
std::string diff, lit;
std::size_t ops = 0;
auto const gram = [](std::string_view s, std::size_t i) {
return std::uint32_t(std::uint8_t(s[i])) << 24 |
std::uint32_t(std::uint8_t(s[i + 1])) << 16 |
std::uint32_t(std::uint8_t(s[i + 2])) << 8 |
std::uint32_t(std::uint8_t(s[i + 3]));
};
auto const varint = [](std::string& o, std::uint64_t v) {
do
{
std::uint8_t const c = v & 0x7F;
v >>= 7;
o += static_cast<char>(c | (v ? 0x80 : 0));
} while (v);
};
auto const flush = [&] {
if (lit.empty())
return;
diff += char(0);
varint(diff, lit.size());
diff += lit;
lit.clear();
++ops;
};
// This encoder is normative - unsanitize_jsontx only accepts its exact
// output - so it must emit identical bytes on every node and every stdlib.
// An unordered container would not: bucket order for equal keys is
// unspecified, and with a candidate cap that changes which match wins.
std::map<std::uint32_t, std::vector<std::uint32_t>> idx;
for (std::size_t i = 0; i + jsontx_min_copy <= out.size(); ++i)
idx[gram(out, i)].push_back(i);
for (std::size_t i = 0; i < raw.size();)
{
std::size_t bo = 0, bl = 0;
if (i + jsontx_min_copy <= raw.size())
if (auto const it = idx.find(gram(raw, i)); it != idx.end())
{
std::size_t tried = 0;
for (auto const off : it->second) // ascending, so ties keep
{ // the lowest offset
if (++tried > jsontx_max_cand)
break;
std::size_t l = 0;
while (i + l < raw.size() && off + l < out.size() &&
out[off + l] == raw[i + l])
++l;
if (l > bl)
bl = l, bo = off;
}
}
if (bl >= jsontx_min_copy)
{
flush();
diff += char(1);
varint(diff, bo);
varint(diff, bl);
++ops;
i += bl;
}
else
lit += raw[i++];
}
flush();
// The same two bounds unsanitize_jsontx applies, applied here so that a
// document this function accepts is never one the verifier then refuses.
// Almost always this means "too much whitespace to encode": the canonical
// form itself is well inside jsontx_max_text.
if (diff.size() > jsontx_max_diff || ops > jsontx_max_ops)
throw std::runtime_error(
"jsontx: formatting differs too much from canonical form");
return {std::move(out), std::move(diff)};
}
// Applies an UNTRUSTED delta to a canonical form the node derived itself.
// Copies read only from `sanitized`, never from the output being built, so a
// short delta cannot expand geometrically. Offsets and lengths are range
// checked before use, varints are length- and minimality-bounded, and the two
// encodings the encoder can never emit - an unmerged literal run, and a copy
// abutting the previous copy in the source - are rejected. Throws on anything
// else.
inline std::string
unsanitize_jsontx(std::string_view sanitized, std::string_view diff)
{
if (sanitized.size() > jsontx_max_text || diff.size() > jsontx_max_diff)
throw std::runtime_error("jsontx: oversize delta input");
std::string out;
std::size_t p = 0, ops = 0, prevEnd = 0;
int prev = -1;
auto const varint = [&](std::uint64_t max) -> std::uint64_t {
std::uint64_t v = 0;
for (int s = 0; s <= 21; s += 7) // four bytes; caps far under a shift
{ // wide enough to be undefined
if (p >= diff.size())
throw std::runtime_error("jsontx: truncated delta");
std::uint8_t const c = diff[p++];
v |= std::uint64_t(c & 0x7F) << s;
if (c & 0x80)
continue;
if (s && !(c & 0x7F))
throw std::runtime_error("jsontx: non-minimal varint");
if (v > max)
throw std::runtime_error("jsontx: delta value out of range");
return v;
}
throw std::runtime_error("jsontx: overlong varint");
};
while (p < diff.size())
{
if (++ops > jsontx_max_ops)
throw std::runtime_error("jsontx: too many delta ops");
std::uint8_t const op = diff[p++];
if (op > 1)
throw std::runtime_error("jsontx: unknown delta op");
std::size_t n = 0;
if (op == 0) // literal
{
if (prev == 0)
throw std::runtime_error("jsontx: unmerged literal run");
n = varint(jsontx_max_text);
if (n == 0 || n > diff.size() - p)
throw std::runtime_error("jsontx: bad literal length");
if (out.size() + n > jsontx_max_text)
throw std::runtime_error("jsontx: delta expands too far");
out += diff.substr(p, n);
p += n;
}
else // copy from the canonical form
{
auto const off = varint(sanitized.size());
n = varint(sanitized.size() - off);
if (n < jsontx_min_copy)
throw std::runtime_error("jsontx: undersize copy");
if (prev == 1 && off == prevEnd)
throw std::runtime_error("jsontx: unmerged copy run");
if (out.size() + n > jsontx_max_text)
throw std::runtime_error("jsontx: delta expands too far");
out += sanitized.substr(off, n);
prevEnd = off + n;
}
prev = op;
}
if (out.empty())
throw std::runtime_error("jsontx: empty delta");
return out;
}
// The complete untrusted-side check, in one place so the RPC path and the
// relay/consensus path cannot drift. Takes the transaction exactly as it came
// off the wire and returns the reconstructed preimage.
inline std::string
jsontx_verify(STTx const& stx, std::string_view diff)
{
if (!stx.isFieldPresent(sfTxnSignature) || stx.isFieldPresent(sfSigners))
throw std::runtime_error("jsontx: expects a lone TxnSignature");
// Out comes everything the signer did not have in front of them: the
// signature, and the delta carrier. SigningPubKey stays. It being inside
// the preimage is what binds key to signature and stops a third party
// re-signing a captured preimage under their own key.
auto txj = stx.STObject::getJson(JsonOptions::none);
txj.removeMember(sfTxnSignature.fieldName);
txj.removeMember(sfJsonTxDelta.fieldName);
// sfTime is a u64 of milliseconds on the wire and an ISO 8601 instant in
// the preimage. The two are a bijection over the representable range, so
// this is a rewrite rather than a reconstruction and the delta carries
// nothing for the field.
if (stx.isFieldPresent(sfTime))
txj[sfTime.fieldName] = jsontx_iso_str(stx.getFieldU64(sfTime));
auto const pkb = stx.getSigningPubKey();
if (publicKeyType(makeSlice(pkb)) != KeyType::ed25519)
throw std::runtime_error("jsontx: SigningPubKey must be ed25519");
// canonical form, derived only from data the node has already validated
auto const san = sanitize_jsontx(Json::FastWriter{}.write(txj)).first;
// reconstruct the signed preimage under the caps above
auto const raw = unsanitize_jsontx(san, diff);
// Bind the preimage to the transaction. This is the load-bearing check,
// not a sanity check: a delta of pure literals can reconstruct ANY text,
// so without it any ed25519 signature the key ever produced over anything
// at all would authorise this transaction. Comparing the delta too - not
// just the canonical form - makes the delta a pure function of the
// preimage, which rules out a second delta reconstructing the same bytes
// and yielding a second valid transaction id.
auto const [san2, diff2] = sanitize_jsontx(raw);
if (san2 != san || diff2 != diff)
throw std::runtime_error("jsontx: preimage does not match transaction");
if (!verify(
PublicKey(makeSlice(pkb)),
makeSlice(raw),
makeSlice(stx.getFieldVL(sfTxnSignature))))
throw std::runtime_error("jsontx: signature does not verify");
return raw;
}
// The relay and consensus entry point: same check, with the delta taken from
// the transaction rather than passed alongside it. This is what checkValidity
// calls in place of STTx::checkSign for a transaction carrying a delta.
inline std::string
jsontx_verify(STTx const& stx)
{
if (!stx.isFieldPresent(sfJsonTxDelta))
throw std::runtime_error("jsontx: no delta");
auto const delta = stx.getFieldVL(sfJsonTxDelta);
return jsontx_verify(
stx,
std::string_view(
reinterpret_cast<char const*>(delta.data()), delta.size()));
}
} // namespace ripple
#endif

View File

@@ -34,7 +34,6 @@
// If you add an amendment here, then do not forget to increment `numFeatures`
// in include/xrpl/protocol/Feature.h.
XRPL_FEATURE(JsonTx, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(OnChainManifests, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (HookMap, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FIX (GuardDepth32, Supported::yes, VoteBehavior::DefaultNo)

View File

@@ -153,7 +153,6 @@ TYPED_SFIELD(sfOutstandingAmount, UINT64, 25, SField::sMD_BaseTen|SFie
TYPED_SFIELD(sfMPTAmount, UINT64, 26, SField::sMD_BaseTen|SField::sMD_Default)
TYPED_SFIELD(sfIssuerNode, UINT64, 27)
TYPED_SFIELD(sfSubjectNode, UINT64, 28)
TYPED_SFIELD(sfTime, UINT64, 96)
TYPED_SFIELD(sfTouchCount, UINT64, 97)
TYPED_SFIELD(sfAccountIndex, UINT64, 98)
TYPED_SFIELD(sfAccountCount, UINT64, 99)
@@ -295,7 +294,6 @@ TYPED_SFIELD(sfAssetClass, VL, 29)
TYPED_SFIELD(sfProvider, VL, 30)
TYPED_SFIELD(sfMPTokenMetadata, VL, 31)
TYPED_SFIELD(sfCredentialType, VL, 32)
TYPED_SFIELD(sfJsonTxDelta, VL, 96, SField::sMD_Default, SField::notSigning)
TYPED_SFIELD(sfHookName, VL, 97)
TYPED_SFIELD(sfRemarkValue, VL, 98)
TYPED_SFIELD(sfRemarkName, VL, 99)

View File

@@ -631,7 +631,6 @@ JSS(server_status); // out: NetworkOPs
JSS(server_version); // out: NetworkOPs
JSS(settle_delay); // out: AccountChannels
JSS(severity); // in: LogLevel
JSS(sig);
JSS(signature); // out: NetworkOPs, ChannelAuthorize
JSS(signature_verified); // out: ChannelVerify
JSS(signing_key); // out: NetworkOPs

View File

@@ -49,8 +49,6 @@ TxFormats::TxFormats()
{sfNetworkID, soeOPTIONAL},
{sfHookParameters, soeOPTIONAL},
{sfHookName, soeOPTIONAL},
{sfTime, soeOPTIONAL},
{sfJsonTxDelta, soeOPTIONAL},
};
#pragma push_macro("UNWRAP")

View File

@@ -0,0 +1,113 @@
#include <test/jtx.h>
#include <xrpld/app/misc/Transaction.h>
#include <xrpld/rpc/CTID.h>
#include <future>
#include <thread>
namespace ripple {
namespace test {
class TransactionState_test : public beast::unit_test::suite
{
void
run() override
{
testcase(
"concurrent response readers observe coherent locator snapshots");
using namespace jtx;
Env env(*this, envconfig([](std::unique_ptr<Config> config) {
config->NETWORK_ID = 11;
return config;
}));
Account const alice("alice");
env.fund(XRP(1000), alice);
env.close();
env(noop(alice));
if (!BEAST_EXPECT(!env.tx()->isFieldPresent(sfNetworkID)))
return;
std::string reason;
Transaction transaction(env.tx(), reason, env.app());
BEAST_EXPECT(reason.empty());
auto const& reader = transaction;
auto const ctidA = RPC::encodeCTID(101, 7, 11);
auto const ctidB = RPC::encodeCTID(202, 9, 22);
if (!BEAST_EXPECT(ctidA && ctidB))
return;
auto setState = [&](bool second) {
std::uint32_t const ledger = second ? 202 : 101;
transaction.setStatus(
COMMITTED, ledger, second ? 9 : 7, second ? 22 : 11);
transaction.setCurrentLedgerState(
ledger, XRPAmount{ledger}, ledger + 1, ledger + 2);
transaction.setResult(second ? TER{terQUEUED} : TER{tesSUCCESS});
transaction.clearSubmitResult();
transaction.setApplied();
transaction.setQueued();
transaction.setBroadcast();
transaction.setKept();
};
setState(false);
std::promise<void> ready;
auto start = ready.get_future().share();
constexpr int iterations = 10000;
int snapshots = 0;
int inconsistent = 0;
std::thread writer([&] {
start.wait();
for (int i = 0; i < iterations; ++i)
setState(i % 2 != 0);
});
std::thread observer([&] {
start.wait();
for (int i = 0; i < iterations; ++i)
{
auto const json = reader.getJson(JsonOptions::none);
auto const ledger = json[jss::ledger_index].asUInt();
auto const ctid = json[jss::ctid].asString();
if (!((ledger == 101 && ctid == *ctidA) ||
(ledger == 202 && ctid == *ctidB)))
++inconsistent;
auto const state = reader.getCurrentLedgerState();
if (!state ||
state->minFeeRequired !=
XRPAmount{state->validatedLedger} ||
state->accountSeqNext != state->validatedLedger + 1 ||
state->accountSeqAvail != state->validatedLedger + 2)
++inconsistent;
auto const result = reader.getResult();
if (result != tesSUCCESS && result != terQUEUED)
++inconsistent;
if (!reader.isValidated() || reader.getStatus() != COMMITTED)
++inconsistent;
auto const flags = reader.getSubmitResult();
(void)flags;
++snapshots;
}
});
ready.set_value();
writer.join();
observer.join();
BEAST_EXPECT(snapshots == iterations);
BEAST_EXPECT(inconsistent == 0);
BEAST_EXPECT(reader.getLedger() == 202);
BEAST_EXPECT(reader.getResult() == terQUEUED);
auto flags = reader.getSubmitResult();
BEAST_EXPECT(
flags.applied && flags.queued && flags.broadcast && flags.kept);
flags.clear();
BEAST_EXPECT(reader.getSubmitResult().any());
transaction.clearSubmitResult();
BEAST_EXPECT(!reader.getSubmitResult().any());
}
};
BEAST_DEFINE_TESTSUITE(TransactionState, app, ripple);
} // namespace test
} // namespace ripple

View File

@@ -0,0 +1,299 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2012, 2013 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/random.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/beast/xor_shift_engine.h>
#include <array>
#include <cstdint>
#include <limits>
#include <random>
namespace ripple {
namespace test {
namespace {
// 32-bit output stored in a 32-bit result.
struct Bits32
{
using result_type = std::uint32_t;
result_type n = 0;
static constexpr result_type
min()
{
return 0;
}
static constexpr result_type
max()
{
return 0xffffffffu;
}
result_type
operator()()
{
return n++;
}
};
// 32-bit output stored in a 64-bit result. This is the Linux mt19937 shape.
struct Bits32In64
{
using result_type = std::uint64_t;
result_type n = 0;
static constexpr result_type
min()
{
return 0;
}
static constexpr result_type
max()
{
return 0xffffffffu;
}
result_type
operator()()
{
return n++;
}
};
// Nonzero minimum, power-of-two range of 256 values.
struct NonzeroMin
{
using result_type = std::uint32_t;
result_type n = 0;
static constexpr result_type
min()
{
return 5;
}
static constexpr result_type
max()
{
return 5 + 255;
}
result_type
operator()()
{
return static_cast<result_type>(min() + (n++ % 256));
}
};
// A valid three-value engine exercises the zero-bit first group in
// independent_bits_engine. Cycling values make draw consumption explicit.
template <class Result, Result Minimum = 0>
struct ThreeValues
{
using result_type = Result;
std::size_t calls = 0;
static constexpr result_type
min()
{
return Minimum;
}
static constexpr result_type
max()
{
return Minimum + 2;
}
result_type
operator()()
{
return static_cast<result_type>(Minimum + (calls++ % 3));
}
};
} // namespace
class random_test : public beast::unit_test::suite
{
// 8-bit model of reject-then-modulo. slack = 2^w % count, drop the
// low slack words, then modulo. Every accepted result must have the
// same number of preimages.
void
testReducedMapping()
{
testcase("8-bit reject-then-modulo is uniform");
constexpr int universe = 256;
for (int count = 1; count <= universe; ++count)
{
int const slack = universe % count;
std::array<int, 256> hits{};
int accepted = 0;
for (int raw = 0; raw < universe; ++raw)
{
if (raw < slack)
continue;
++hits[raw % count];
++accepted;
}
BEAST_EXPECT(accepted % count == 0);
auto const each = accepted / count;
for (int result = 0; result < count; ++result)
BEAST_EXPECT(hits[result] == each);
}
}
template <class Engine, class Integral>
void
expectInRange(Engine& engine, Integral min, Integral max, int samples)
{
for (int i = 0; i < samples; ++i)
{
auto const value = rand_int(engine, min, max);
BEAST_EXPECT(value >= min);
BEAST_EXPECT(value <= max);
}
}
public:
void
testEngineVectors()
{
testcase("engine range vectors and draw consumption");
beast::xor_shift_engine full{1};
beast::xor_shift_engine fullTwin{1};
BEAST_EXPECT(detail::randomU64(full) == fullTwin());
BEAST_EXPECT(full() == fullTwin());
Bits32 narrow;
Bits32 narrowTwin;
auto const narrowWord = detail::randomU64(narrow);
auto const nLow = static_cast<std::uint64_t>(narrowTwin());
auto const nHigh = static_cast<std::uint64_t>(narrowTwin());
BEAST_EXPECT(narrowWord == (nLow | (nHigh << 32)));
BEAST_EXPECT(narrow() == narrowTwin());
Bits32In64 wideStore;
Bits32In64 wideStoreTwin;
auto const wideWord = detail::randomU64(wideStore);
auto const wLow = wideStoreTwin();
auto const wHigh = wideStoreTwin();
BEAST_EXPECT(wideWord == (wLow | (wHigh << 32)));
BEAST_EXPECT(wideStore() == wideStoreTwin());
NonzeroMin shifted;
NonzeroMin shiftedTwin;
std::uint64_t composed = 0;
for (int i = 0; i < 8; ++i)
{
auto const piece =
static_cast<std::uint64_t>(shiftedTwin() - NonzeroMin::min());
composed |= piece << (8 * i);
}
BEAST_EXPECT(detail::randomU64(shifted) == composed);
BEAST_EXPECT(shifted() == shiftedTwin());
std::minstd_rand uneven{12345};
std::minstd_rand unevenCopy{12345};
std::independent_bits_engine<std::minstd_rand, 64, std::uint64_t> ibits{
unevenCopy};
for (int i = 0; i < 8; ++i)
BEAST_EXPECT(detail::randomU64(uneven) == ibits());
}
void
testZeroBitGroup()
{
testcase("three-value engines retain the zero-bit group draw");
auto const check = [&](auto engine) {
using Engine = decltype(engine);
std::independent_bits_engine<Engine, 64, std::uint64_t> reference{
engine};
// R=3: n=65, w0=0, n0=1, y0=3, y1=2. Discard the
// first draw, then take 64 bits, rejecting normalized value 2.
// For this cycle the accepted bits are 1010...10 (97 draws).
auto const first = detail::randomU64(engine);
BEAST_EXPECT(first == 0xaaaaaaaaaaaaaaaaULL);
BEAST_EXPECT(engine.calls == 97);
BEAST_EXPECT(first == reference());
BEAST_EXPECT(engine.calls == reference.base().calls);
for (int i = 0; i < 16; ++i)
{
BEAST_EXPECT(detail::randomU64(engine) == reference());
BEAST_EXPECT(engine.calls == reference.base().calls);
}
// Also exercise the public closed-range mapper with the same
// normalized stream, including the runtime fault-hook range.
for (auto const count : {10u, 10'000u})
{
auto const n = static_cast<std::uint64_t>(count);
auto const slack = static_cast<std::uint64_t>(-n) % n;
std::uint64_t expected;
do
{
expected = reference();
} while (expected < slack);
BEAST_EXPECT(rand_int(engine, 0u, count - 1) == expected % n);
BEAST_EXPECT(engine.calls == reference.base().calls);
}
};
check(ThreeValues<std::uint32_t>{});
check(ThreeValues<std::uint64_t>{});
check(ThreeValues<std::uint32_t, 5>{});
check(ThreeValues<
std::uint64_t,
std::numeric_limits<std::uint64_t>::max() - 2>{});
}
void
testBounds()
{
testcase("closed bounds, including signed endpoints and 2^40");
auto check = [&](auto engine) {
expectInRange(engine, 0, 10, 64);
expectInRange(engine, 0, 9999, 64);
expectInRange(engine, -20, 20, 64);
expectInRange(engine, -5, 15, 64);
expectInRange(
engine, std::uint64_t{0}, (std::uint64_t{1} << 40) + 123u, 8);
};
check(beast::xor_shift_engine{7});
check(Bits32{});
check(Bits32In64{});
check(NonzeroMin{});
check(std::minstd_rand{7});
check(std::mt19937{7});
}
void
run() override
{
testReducedMapping();
testEngineVectors();
testZeroBitGroup();
testBounds();
}
};
BEAST_DEFINE_TESTSUITE(random, basics, ripple);
} // namespace test
} // namespace ripple

View File

@@ -1,558 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2012-2014 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/beast/unit_test.h>
#include <xrpl/protocol/JSONTxSignatures.h>
#include <cstdint>
#include <exception>
#include <limits>
#include <string>
#include <string_view>
namespace ripple {
/**
* Unit tests for JsonTx plaintext transaction signature support.
*
* Covers:
* - jsontx_strict: document framing (no comments, no \u, nothing outside)
* - jsontx_iso / jsontx_iso_str: ISO 8601 <-> ripple-epoch milliseconds
* - sanitize_jsontx / unsanitize_jsontx: canonical form + delta round-trip
* - unsanitize_jsontx: rejection of every delta the encoder cannot emit
* - jsontx_exact / jsontx_num / jsontx_u64 / jsontx_u64_str: number handling
* - jsontx_field: case-insensitive canonical field lookup
*
* All of these report failure by throwing rather than by returning a status,
* so the helpers below adapt them to the boolean BEAST_EXPECT wants.
*/
class JSONTxSignatures_test : public beast::unit_test::suite
{
// True if f() threw. Used in place of a throws-macro so the expectation
// still reports the failing file and line through BEAST_EXPECT.
template <class F>
static bool
threw(F&& f)
{
try
{
f();
}
catch (std::exception const&)
{
return true;
}
return false;
}
static bool
strictOk(std::string_view s)
{
return !threw([&] { jsontx_strict(s); });
}
// sanitize then unsanitize must reproduce the input byte for byte; this is
// the property jsontx_verify rests on.
static bool
roundTrips(std::string const& raw)
{
auto const [san, diff] = sanitize_jsontx(raw);
return unsanitize_jsontx(san, diff) == raw;
}
static bool
unsanitizeThrows(std::string_view san, std::string const& diff)
{
return threw([&] { (void)unsanitize_jsontx(san, diff); });
}
void
testStrictAccepts()
{
testcase("jsontx_strict accepts");
BEAST_EXPECT(strictOk("{}"));
BEAST_EXPECT(strictOk(R"({"key":"value"})"));
BEAST_EXPECT(strictOk(R"({"n":0})"));
BEAST_EXPECT(strictOk(R"({"n":12345})"));
BEAST_EXPECT(strictOk(R"({"n":-1})"));
BEAST_EXPECT(strictOk(R"({"outer":{"inner":true}})"));
BEAST_EXPECT(strictOk(R"({"a":[1,2,3]})"));
BEAST_EXPECT(strictOk(R"({"b":true})"));
BEAST_EXPECT(strictOk(R"({"b":false})"));
BEAST_EXPECT(strictOk(R"({"n":null})"));
// json's whitespace set, between tokens and around the document
BEAST_EXPECT(strictOk(" { \"key\" : \"val\" } "));
BEAST_EXPECT(strictOk("{\r\n\t\"key\":\"val\"\r\n}"));
// an escaped backslash ends the escape, so the 'u' after it is an
// ordinary character and not a \u escape
BEAST_EXPECT(strictOk(R"({"key":"\\utest"})"));
// a solidus that does not open a comment is the parser's business
BEAST_EXPECT(strictOk(R"({"key":"a/b"})"));
// This validates framing only. Trailing commas, single quotes,
// unquoted keys and non-object roots are all rejected too, but by the
// parser or by sanitize_jsontx rather than here.
BEAST_EXPECT(strictOk(R"({"key":"val",})"));
BEAST_EXPECT(strictOk(R"({'key':'val'})"));
BEAST_EXPECT(strictOk(R"({key:"val"})"));
BEAST_EXPECT(strictOk(R"([1,2,3])"));
}
void
testStrictRejects()
{
testcase("jsontx_strict rejects");
// comments outside the document, in either style
BEAST_EXPECT(!strictOk("{\"key\":\"value\"}\n// real comment"));
BEAST_EXPECT(!strictOk("{\"key\":\"value\"} /* real comment */"));
BEAST_EXPECT(!strictOk("// leading comment\n{\"key\":\"value\"}"));
// ...but a comment marker inside a string is just text
BEAST_EXPECT(strictOk(R"({"key":"value // not a comment"})"));
// \u escapes never appear in a preimage: the signer has to be able to
// read what they are signing
BEAST_EXPECT(!strictOk(R"({"key":"\u0048ello"})"));
BEAST_EXPECT(!strictOk(R"({"key":"hello\u0041"})"));
BEAST_EXPECT(!strictOk(R"({"key":"\u0000"})"));
// nothing may ride along after the document
BEAST_EXPECT(!strictOk(R"({"a":"b"} "trailing")"));
BEAST_EXPECT(!strictOk(R"({"a":"b"} {"c":"d"})"));
// unbalanced brackets
BEAST_EXPECT(!strictOk(R"({"a":"b")"));
BEAST_EXPECT(!strictOk(R"({"a":"b"}})"));
// unterminated string
BEAST_EXPECT(!strictOk(R"({"a":"b})"));
// a bare scalar is not a bracketed document
BEAST_EXPECT(!strictOk(R"("just a string")"));
BEAST_EXPECT(!strictOk("42"));
BEAST_EXPECT(!strictOk("true"));
BEAST_EXPECT(!strictOk("null"));
BEAST_EXPECT(!strictOk(""));
BEAST_EXPECT(!strictOk(" "));
}
void
testIsoRoundTrip()
{
testcase("ISO 8601 round-trip");
BEAST_EXPECT(jsontx_iso("2000-01-01T00:00:00.000Z") == 0);
BEAST_EXPECT(jsontx_iso_str(0) == "2000-01-01T00:00:00.000Z");
auto rt = [](char const* s) {
return jsontx_iso_str(jsontx_iso(s)) == s;
};
BEAST_EXPECT(rt("2000-01-01T00:00:00.000Z"));
BEAST_EXPECT(rt("2025-06-15T12:30:45.123Z"));
BEAST_EXPECT(rt("2000-02-29T00:00:00.000Z")); // leap year
BEAST_EXPECT(rt("2400-02-29T00:00:00.000Z")); // 400-year leap year
BEAST_EXPECT(rt("9999-12-31T23:59:59.999Z"));
// the last instant with a 24 character spelling
BEAST_EXPECT(jsontx_iso("9999-12-31T23:59:59.999Z") == jsontx_max_time);
BEAST_EXPECT(
jsontx_iso_str(jsontx_max_time) == "9999-12-31T23:59:59.999Z");
BEAST_EXPECT(threw([] { (void)jsontx_iso_str(jsontx_max_time + 1); }));
// milliseconds are carried, not truncated
BEAST_EXPECT(jsontx_iso("2000-01-01T00:00:00.001Z") == 1);
BEAST_EXPECT(jsontx_iso("2000-01-02T00:00:00.000Z") == 86400000);
}
void
testIsoRejects()
{
testcase("ISO 8601 rejection");
auto bad = [](char const* s) {
return threw([&] { (void)jsontx_iso(s); });
};
// the shape is exactly YYYY-MM-DDTHH:MM:SS.sssZ, always 24 characters
BEAST_EXPECT(bad("2000-01-01T00:00:00")); // no fraction, no Z
BEAST_EXPECT(bad("2000-01-01T00:00:00.Z")); // no fraction
BEAST_EXPECT(bad("2000-01-01T00:00:00.000")); // no Z
BEAST_EXPECT(bad("2000-01-01T00:00:00.000+00:00")); // offset
BEAST_EXPECT(bad("2000-01-01 00:00:00.000Z")); // space for T
BEAST_EXPECT(bad("2000/01/01T00:00:00.000Z"));
BEAST_EXPECT(bad("not-a-date"));
BEAST_EXPECT(bad(""));
// out of range components
BEAST_EXPECT(bad("2000-00-01T00:00:00.000Z")); // month 0
BEAST_EXPECT(bad("2000-13-01T00:00:00.000Z")); // month 13
BEAST_EXPECT(bad("2000-01-00T00:00:00.000Z")); // day 0
BEAST_EXPECT(bad("2000-02-30T00:00:00.000Z")); // Feb 30
BEAST_EXPECT(bad("2001-02-29T00:00:00.000Z")); // non-leap Feb 29
BEAST_EXPECT(bad("1900-02-29T00:00:00.000Z")); // century non-leap
BEAST_EXPECT(bad("2000-01-01T24:00:00.000Z")); // hour 24
BEAST_EXPECT(bad("2000-01-01T00:60:00.000Z")); // minute 60
BEAST_EXPECT(bad("2000-01-01T00:00:60.000Z")); // leap second
// before the ripple epoch
BEAST_EXPECT(bad("1999-12-31T23:59:59.999Z"));
}
void
testSanitizeRoundTrip()
{
testcase("sanitize/unsanitize round-trip");
// already canonical
BEAST_EXPECT(roundTrips(
R"({"TransactionType":"Payment","Sequence":1,"Fee":"10","SigningPubKey":"ED0000","Account":"rTest"})"));
// source order is irrelevant: the canonical form sorts by field code
BEAST_EXPECT(roundTrips(
R"({"Account":"rTest","Fee":"10","Sequence":1,"SigningPubKey":"ED0000","TransactionType":"Payment"})"));
// sfTime is an ISO instant in the preimage and a u64 on the wire
BEAST_EXPECT(roundTrips(
R"({"Account":"rTest","Fee":"10","Sequence":1,"SigningPubKey":"ED0000","Time":"2000-01-01T00:00:00.000Z","TransactionType":"Payment"})"));
// interior whitespace is carried by the delta
BEAST_EXPECT(roundTrips(R"({ "Account" : "rTest" , "Fee" : "10" })"));
// pretty-printed, which is what a signer actually reads
BEAST_EXPECT(
roundTrips("{\n"
" \"Account\": \"rTest\",\n"
" \"Fee\": \"10\",\n"
" \"Sequence\": 1,\n"
" \"SigningPubKey\": \"ED\",\n"
" \"Time\": \"2000-01-01T00:00:00.000Z\",\n"
" \"TransactionType\": \"Payment\"\n"
"}"));
// CRLF, which is what a browser textarea produces
BEAST_EXPECT(
roundTrips("{\r\n"
" \"Account\": \"rTest\",\r\n"
" \"Fee\": \"10\",\r\n"
" \"TransactionType\": \"Payment\"\r\n"
"}"));
// nested objects and arrays
BEAST_EXPECT(roundTrips(
R"({"Account":"rTest","Fee":"10","Memos":[{"Memo":{"MemoData":"48656C6C6F"}}],"Sequence":1,"TransactionType":"Payment"})"));
// a u32 above the signed range stays bare and exact
BEAST_EXPECT(roundTrips(
R"({"Account":"rTest","Fee":"10","Flags":2147483648,"Sequence":1})"));
// field names match case-insensitively and re-emit canonically
BEAST_EXPECT(roundTrips(R"({"account":"rTest","FEE":"10"})"));
}
void
testCanonicalForm()
{
testcase("canonical form");
auto const compact =
sanitize_jsontx(R"({"Account":"rTest","Fee":"10"})");
auto const spaced =
sanitize_jsontx(R"({ "Account" : "rTest" , "Fee" : "10" })").first;
auto const reordered =
sanitize_jsontx(R"({"Fee":"10","Account":"rTest"})").first;
auto const cased =
sanitize_jsontx(R"({"account":"rTest","fee":"10"})").first;
// whitespace, member order and spelling all wash out
BEAST_EXPECT(compact.first == spaced);
BEAST_EXPECT(compact.first == reordered);
BEAST_EXPECT(compact.first == cased);
// the canonical form is a fixed point, which is what lets jsontx_verify
// compare a node-derived form against a signer-derived one
BEAST_EXPECT(sanitize_jsontx(compact.first).first == compact.first);
// and both halves sit inside the caps the verifier applies
BEAST_EXPECT(compact.first.size() <= jsontx_max_text);
BEAST_EXPECT(compact.second.size() <= jsontx_max_diff);
}
void
testSanitizeRejects()
{
testcase("sanitize_jsontx rejection");
auto bad = [](std::string const& raw) {
return threw([&] { (void)sanitize_jsontx(raw); });
};
// not json at all
BEAST_EXPECT(bad("not json"));
BEAST_EXPECT(bad(""));
// a preimage is an object
BEAST_EXPECT(bad(R"("just a string")"));
BEAST_EXPECT(bad(R"([1,2,3])"));
// every member must name a serializable field
BEAST_EXPECT(bad(R"({"Account":"rTest","Fee":"10","BogusField":"x"})"));
// two spellings of one field would canonicalize to the same member
BEAST_EXPECT(bad(R"({"Fee":"10","fee":"20"})"));
// null has no canonical spelling as a field value
BEAST_EXPECT(bad(R"({"Account":"rTest","Fee":null})"));
// framing violations reach sanitize_jsontx through jsontx_strict
BEAST_EXPECT(bad("{\"Account\":\"rTest\"} // hi"));
BEAST_EXPECT(
bad(R"({"Account":"rTest","Fee":"10","Domain":"\u0041"})"));
// over the text cap
{
std::string large = "{\"Account\":\"";
large.append(jsontx_max_text, 'r');
large += "\"}";
BEAST_EXPECT(bad(large));
}
// an sfTime that is not exactly what jsontx_iso_str would produce
BEAST_EXPECT(bad(R"({"Account":"rTest","Time":"2000-01-01"})"));
}
void
testDeltaRejection()
{
testcase("unsanitize_jsontx rejection");
// unsanitize_jsontx never parses its first argument, so any text will
// do as a source for the decoder's own error paths
std::string const san = "abcdefghij";
// a delta must reconstruct something
BEAST_EXPECT(unsanitizeThrows(san, ""));
// only two ops exist
BEAST_EXPECT(unsanitizeThrows(san, std::string("\x02", 1)));
BEAST_EXPECT(unsanitizeThrows(san, std::string("\x7f", 1)));
// truncated: an op with no varint after it
BEAST_EXPECT(unsanitizeThrows(san, std::string("\x00", 1)));
// zero-length literal
BEAST_EXPECT(unsanitizeThrows(san, std::string("\x00\x00", 2)));
// literal claiming more bytes than the delta holds
BEAST_EXPECT(
unsanitizeThrows(san, std::string("\x00\x81\x01", 3) + "short"));
// non-minimal varint: a continuation byte followed by zero
BEAST_EXPECT(unsanitizeThrows(san, std::string("\x00\x80\x00", 3)));
// overlong varint: more than four bytes
BEAST_EXPECT(
unsanitizeThrows(san, std::string("\x00\xff\xff\xff\xff\x01", 6)));
// two literals in a row; the encoder always merges them
BEAST_EXPECT(unsanitizeThrows(
san,
std::string("\x00\x02", 2) + "ab" + std::string("\x00\x02", 2) +
"cd"));
// a copy shorter than the encoder's match threshold
BEAST_EXPECT(unsanitizeThrows(san, std::string("\x01\x00\x01", 3)));
// a copy abutting the previous copy; the encoder always merges them
BEAST_EXPECT(
unsanitizeThrows(san, std::string("\x01\x00\x04\x01\x04\x04", 6)));
// offset past the end of the canonical form
BEAST_EXPECT(unsanitizeThrows(san, std::string("\x01\xff\xff\x04", 4)));
// length running past the end from a valid offset
BEAST_EXPECT(unsanitizeThrows(san, std::string("\x01\x08\x08", 3)));
// oversize inputs are refused before any decoding
BEAST_EXPECT(
unsanitizeThrows(san, std::string(jsontx_max_diff + 1, '\x00')));
BEAST_EXPECT(unsanitizeThrows(
std::string(jsontx_max_text + 1, 'x'),
std::string("\x01\x00\x04", 3)));
// a well-formed copy is accepted, so the cases above fail for their
// own reasons and not because the fixture itself is malformed
BEAST_EXPECT(
unsanitize_jsontx(san, std::string("\x01\x00\x05", 3)) == "abcde");
}
void
testNumbers()
{
testcase("number handling");
std::int64_t out = 0;
// small integers arrive as intValue / uintValue
BEAST_EXPECT(jsontx_exact(Json::Value(0), out) && out == 0);
BEAST_EXPECT(jsontx_exact(Json::Value(-1), out) && out == -1);
BEAST_EXPECT(jsontx_exact(Json::Value(42u), out) && out == 42);
// larger ones fall through to realValue and stay exact below 2^53
BEAST_EXPECT(
jsontx_exact(Json::Value(9007199254740991.0), out) &&
out == 9007199254740991LL);
BEAST_EXPECT(
jsontx_exact(Json::Value(-9007199254740991.0), out) &&
out == -9007199254740991LL);
// at and past 2^53 distinct decimal integers share a double
BEAST_EXPECT(!jsontx_exact(Json::Value(9007199254740992.0), out));
BEAST_EXPECT(!jsontx_exact(Json::Value(-9007199254740992.0), out));
// fractions, infinities and non-numbers are not integers
BEAST_EXPECT(!jsontx_exact(Json::Value(1.5), out));
BEAST_EXPECT(!jsontx_exact(
Json::Value(std::numeric_limits<double>::infinity()), out));
BEAST_EXPECT(!jsontx_exact(Json::Value(true), out));
BEAST_EXPECT(!jsontx_exact(Json::Value("7"), out));
// jsontx_num renders what jsontx_exact accepts
BEAST_EXPECT(jsontx_num(Json::Value(0)) == "0");
BEAST_EXPECT(jsontx_num(Json::Value(1)) == "1");
BEAST_EXPECT(jsontx_num(Json::Value(-1)) == "-1");
BEAST_EXPECT(jsontx_num(Json::Value(42)) == "42");
BEAST_EXPECT(jsontx_num(Json::Value(1000000)) == "1000000");
BEAST_EXPECT(
jsontx_num(Json::Value(9007199254740991.0)) == "9007199254740991");
BEAST_EXPECT(
threw([] { (void)jsontx_num(Json::Value(9007199254740992.0)); }));
BEAST_EXPECT(threw([] { (void)jsontx_num(Json::Value(1.5)); }));
// jsontx_u64 additionally refuses negatives rather than wrapping them
BEAST_EXPECT(jsontx_u64(Json::Value(0)) == 0);
BEAST_EXPECT(jsontx_u64(Json::Value(100)) == 100);
BEAST_EXPECT(threw([] { (void)jsontx_u64(Json::Value(-1)); }));
BEAST_EXPECT(threw([] { (void)jsontx_u64(Json::Value(1.5)); }));
BEAST_EXPECT(threw([] {
(void)jsontx_u64(
Json::Value(std::numeric_limits<double>::infinity()));
}));
// STUInt64::getJson renders hex, unpadded and lowercase, except for
// the sMD_BaseTen fields which render base ten
BEAST_EXPECT(jsontx_u64_str(sfTime, 0) == "0");
BEAST_EXPECT(jsontx_u64_str(sfTime, 255) == "ff");
BEAST_EXPECT(jsontx_u64_str(sfTime, 1000) == "3e8");
BEAST_EXPECT(jsontx_u64_str(sfMaximumAmount, 255) == "255");
BEAST_EXPECT(jsontx_u64_str(sfMaximumAmount, 1000) == "1000");
}
void
testFieldLookup()
{
testcase("jsontx_field");
auto const* account = &jsontx_field("Account");
BEAST_EXPECT(account->fieldName == "Account");
BEAST_EXPECT(account->fieldCode > 0);
// the lookup is case-insensitive and always yields the one canonical
// SField, which is what makes the canonical spelling well defined
BEAST_EXPECT(&jsontx_field("account") == account);
BEAST_EXPECT(&jsontx_field("ACCOUNT") == account);
BEAST_EXPECT(&jsontx_field("AcCoUnT") == account);
BEAST_EXPECT(jsontx_field("Fee").fieldName == "Fee");
BEAST_EXPECT(
jsontx_field("TransactionType").fieldName == "TransactionType");
BEAST_EXPECT(jsontx_field("Time") == sfTime);
BEAST_EXPECT(jsontx_field("JsonTxDelta") == sfJsonTxDelta);
// anything unknown is sfInvalid, which sanitize_jsontx turns into a
// rejection rather than a silently dropped member
BEAST_EXPECT(jsontx_field("NoSuchField") == sfInvalid);
BEAST_EXPECT(jsontx_field("") == sfInvalid);
}
void
testBounds()
{
testcase("consensus bounds");
// These are consensus rules from the moment featureJsonTx activates,
// so pin them: changing one changes which deltas verify.
BEAST_EXPECT(jsontx_max_text == 8192);
BEAST_EXPECT(jsontx_max_diff == 2048);
BEAST_EXPECT(jsontx_max_ops == 1024);
BEAST_EXPECT(jsontx_min_copy == 4);
BEAST_EXPECT(jsontx_max_cand == 64);
BEAST_EXPECT(jsontx_epoch_day == 10957);
// sanitize_jsontx applies the delta caps itself, so a document it
// accepts is never one unsanitize_jsontx would then refuse. This is
// reached well before the text cap: indentation costs delta bytes
// that the canonical form does not pay.
auto indented = [](int memos) {
std::string raw =
"{\n \"Account\": \"rTest\",\n \"Memos\": [";
for (int i = 0; i < memos; ++i)
raw += std::string(i ? "," : "") +
"\n {\n"
" \"Memo\": {\n"
" \"MemoData\": \"AA\"\n"
" }\n"
" }";
raw += "\n ]\n}";
return raw;
};
BEAST_EXPECT(roundTrips(indented(20)));
auto const tooFormatted = indented(40);
BEAST_EXPECT(tooFormatted.size() < jsontx_max_text);
BEAST_EXPECT(threw([&] { (void)sanitize_jsontx(tooFormatted); }));
}
public:
void
run() override
{
testStrictAccepts();
testStrictRejects();
testIsoRoundTrip();
testIsoRejects();
testSanitizeRoundTrip();
testCanonicalForm();
testSanitizeRejects();
testDeltaRejection();
testNumbers();
testFieldLookup();
testBounds();
}
};
BEAST_DEFINE_TESTSUITE(JSONTxSignatures, protocol, ripple);
} // namespace ripple

View File

@@ -24,9 +24,13 @@
#include <boost/lexical_cast.hpp>
#include <algorithm>
#include <cstdlib>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <stdexcept>
#include <string>
#include <unistd.h>
#include <vector>
namespace ripple {
@@ -234,9 +238,40 @@ multi_runner_base<IsParent>::multi_runner_base()
{
if (IsParent)
{
// cleanup any leftover state for any previous failed runs
boost::interprocess::shared_memory_object::remove(shared_mem_name_);
boost::interprocess::message_queue::remove(message_queue_name_);
// One name pair per parent process. Spawned children inherit the
// environment; the in-process single-job child reads it too.
auto const pid =
std::to_string(static_cast<unsigned long long>(::getpid()));
shared_mem_name_ = std::string(shared_mem_prefix_) + "." + pid;
message_queue_name_ =
std::string(message_queue_prefix_) + "." + pid;
if (::setenv(shared_mem_env_, shared_mem_name_.c_str(), 1) != 0 ||
::setenv(message_queue_env_, message_queue_name_.c_str(), 1) !=
0)
{
throw std::runtime_error(
"failed to publish unit-test ipc names");
}
// Drop a leftover object for this pid. Do not touch another
// process's names.
boost::interprocess::shared_memory_object::remove(
shared_mem_name_.c_str());
boost::interprocess::message_queue::remove(
message_queue_name_.c_str());
}
else
{
char const* const shm = std::getenv(shared_mem_env_);
char const* const mq = std::getenv(message_queue_env_);
if (shm == nullptr || shm[0] == '\0' || mq == nullptr ||
mq[0] == '\0')
{
throw std::runtime_error(
"unit-test child missing parent ipc names");
}
shared_mem_name_ = shm;
message_queue_name_ = mq;
}
shared_mem_ = boost::interprocess::shared_memory_object{
@@ -244,7 +279,7 @@ multi_runner_base<IsParent>::multi_runner_base()
IsParent,
boost::interprocess::create_only_t,
boost::interprocess::open_only_t>{},
shared_mem_name_,
shared_mem_name_.c_str(),
boost::interprocess::read_write};
if (IsParent)
@@ -253,7 +288,7 @@ multi_runner_base<IsParent>::multi_runner_base()
message_queue_ =
std::make_unique<boost::interprocess::message_queue>(
boost::interprocess::create_only,
message_queue_name_,
message_queue_name_.c_str(),
/*max messages*/ 16,
/*max message size*/ 1 << 20);
}
@@ -261,7 +296,8 @@ multi_runner_base<IsParent>::multi_runner_base()
{
message_queue_ =
std::make_unique<boost::interprocess::message_queue>(
boost::interprocess::open_only, message_queue_name_);
boost::interprocess::open_only,
message_queue_name_.c_str());
}
region_ = boost::interprocess::mapped_region{
@@ -275,8 +311,12 @@ multi_runner_base<IsParent>::multi_runner_base()
{
if (IsParent)
{
boost::interprocess::shared_memory_object::remove(shared_mem_name_);
boost::interprocess::message_queue::remove(message_queue_name_);
if (!shared_mem_name_.empty())
boost::interprocess::shared_memory_object::remove(
shared_mem_name_.c_str());
if (!message_queue_name_.empty())
boost::interprocess::message_queue::remove(
message_queue_name_.c_str());
}
throw;
}
@@ -288,8 +328,9 @@ multi_runner_base<IsParent>::~multi_runner_base()
if (IsParent)
{
inner_->~inner();
boost::interprocess::shared_memory_object::remove(shared_mem_name_);
boost::interprocess::message_queue::remove(message_queue_name_);
boost::interprocess::shared_memory_object::remove(
shared_mem_name_.c_str());
boost::interprocess::message_queue::remove(message_queue_name_.c_str());
}
}

View File

@@ -152,11 +152,20 @@ class multi_runner_base
print_results(S& s);
};
static constexpr const char* shared_mem_name_ = "RippledUnitTestSharedMem";
// name of the message queue a multi_runner_child will use to communicate
// with multi_runner_parent
static constexpr const char* message_queue_name_ =
// Prefixes only. The parent appends ".<pid>" and publishes the full names
// in the environment before a child is constructed or spawned. Fixed
// names collided: every --unittest process, including one job, removes
// and recreates these objects.
static constexpr char const* shared_mem_prefix_ =
"RippledUnitTestSharedMem";
static constexpr char const* message_queue_prefix_ =
"RippledUnitTestMessageQueue";
static constexpr char const* shared_mem_env_ = "XRPLD_UNIT_TEST_SHARED_MEM";
static constexpr char const* message_queue_env_ =
"XRPLD_UNIT_TEST_MESSAGE_QUEUE";
std::string shared_mem_name_;
std::string message_queue_name_;
// `inner_` will be created in shared memory
inner* inner_;

View File

@@ -64,6 +64,7 @@ public:
bool
isComplete() const
{
ScopedLockType const sl(mtx_);
return complete_;
}
@@ -71,18 +72,21 @@ public:
bool
isFailed() const
{
ScopedLockType const sl(mtx_);
return failed_;
}
std::shared_ptr<Ledger const>
getLedger() const
{
ScopedLockType const sl(mtx_);
return mLedger;
}
std::uint32_t
getSeq() const
{
ScopedLockType const sl(mtx_);
return mSeq;
}
@@ -109,12 +113,14 @@ public:
void
touch()
{
ScopedLockType const sl(mtx_);
mLastAction = m_clock.now();
}
clock_type::time_point
getLastAction() const
{
ScopedLockType const sl(mtx_);
return mLastAction;
}

View File

@@ -28,14 +28,12 @@
#include <xrpld/overlay/Overlay.h>
#include <xrpld/shamap/SHAMapNodeID.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/random.h>
#include <xrpl/protocol/HashPrefix.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/resource/Fees.h>
#include <boost/iterator/function_output_iterator.hpp>
#include <algorithm>
#include <random>
namespace ripple {
@@ -434,6 +432,8 @@ InboundLedger::pmDowncast()
void
InboundLedger::done()
{
ScopedLockType const sl(mtx_);
if (mSignaled)
return;
@@ -471,14 +471,14 @@ InboundLedger::done()
// We hold the PeerSet lock, so must dispatch
app_.getJobQueue().addJob(
jtLEDGER_DATA, "AcquisitionDone", [self = shared_from_this()]() {
if (self->complete_ && !self->failed_)
if (self->isComplete() && !self->isFailed())
{
self->app_.getLedgerMaster().checkAccept(self->getLedger());
self->app_.getLedgerMaster().tryAdvance();
}
else
self->app_.getInboundLedgers().logFailure(
self->hash_, self->mSeq);
self->hash_, self->getSeq());
});
}
@@ -1039,11 +1039,12 @@ InboundLedger::gotData(
std::weak_ptr<Peer> peer,
std::shared_ptr<protocol::TMLedgerData> const& data)
{
std::lock_guard sl(mReceivedDataLock);
ScopedLockType const stateLock(mtx_);
if (isDone())
return false;
std::lock_guard sl(mReceivedDataLock);
mReceivedData.emplace_back(peer, data);
if (mReceiveDispatched)
@@ -1213,32 +1214,43 @@ struct PeerDataCounts
// call F with the `peer` parameter with a random sample of at most n values
// of the counts vector.
template <class F>
template <class F, class URBG>
void
sampleN(std::size_t n, F&& f)
sampleN(std::size_t n, F&& f, URBG&& rng)
{
if (counts.empty())
return;
auto outFunc = [&f](auto&& v) { f(v.first); };
std::minstd_rand rng{std::random_device{}()};
#if _MSC_VER
// Id order makes the walk independent of map iteration.
std::vector<std::pair<std::shared_ptr<Peer>, int>> population(
counts.begin(), counts.end());
std::sort(
population.begin(),
population.end(),
[](auto const& a, auto const& b) {
return a.first->id() < b.first->id();
});
std::vector<std::pair<std::shared_ptr<Peer>, int>> s;
s.reserve(n);
std::sample(
counts.begin(), counts.end(), std::back_inserter(s), n, rng);
for (auto& v : s)
// Knuth's Algorithm S. std::sample's selection differs between
// libstdc++ and libc++. When every remaining peer must be kept,
// take it without rand_int: that call rejects a zero-width range.
auto need = std::min(n, population.size());
auto remaining = population.size();
for (auto& item : population)
{
outFunc(v);
if (need == 0)
break;
if (need == remaining ||
rand_int(rng, std::size_t{0}, remaining - 1) < need)
{
s.push_back(std::move(item));
--need;
}
--remaining;
}
#else
std::sample(
counts.begin(),
counts.end(),
boost::make_function_output_iterator(outFunc),
n,
rng);
#endif
for (auto& v : s)
f(v.first);
}
};
} // namespace detail
@@ -1288,9 +1300,14 @@ InboundLedger::runData()
// Select a random sample of the peers that gives us the most nodes that are
// useful
dataCounts.prune();
dataCounts.sampleN(maxUsefulPeers, [&](std::shared_ptr<Peer> const& peer) {
trigger(peer, TriggerReason::reply);
});
// dev has no Application::getPrng. Peer selection is outside the
// harness schedule, so draw from the per-thread default engine.
dataCounts.sampleN(
maxUsefulPeers,
[&](std::shared_ptr<Peer> const& peer) {
trigger(peer, TriggerReason::reply);
},
default_prng());
}
Json::Value

View File

@@ -181,12 +181,15 @@ LedgerMaster::getPublishedLedgerAge()
std::chrono::seconds ret = app_.timeKeeper().closeTime().time_since_epoch();
ret -= pubClose;
ret = (ret > 0s) ? ret : 0s;
static std::chrono::seconds lastRet = -1s;
static std::atomic<std::chrono::seconds::rep> lastRet{-1};
auto const retCount = ret.count();
auto observedLastRet = lastRet.load(std::memory_order_relaxed);
if (ret != lastRet)
if (retCount != observedLastRet &&
lastRet.compare_exchange_strong(
observedLastRet, retCount, std::memory_order_relaxed))
{
JLOG(m_journal.trace()) << "Published ledger age is " << ret.count();
lastRet = ret;
JLOG(m_journal.trace()) << "Published ledger age is " << retCount;
}
return ret;
}
@@ -206,12 +209,15 @@ LedgerMaster::getValidatedLedgerAge()
std::chrono::seconds ret = app_.timeKeeper().closeTime().time_since_epoch();
ret -= valClose;
ret = (ret > 0s) ? ret : 0s;
static std::chrono::seconds lastRet = -1s;
static std::atomic<std::chrono::seconds::rep> lastRet{-1};
auto const retCount = ret.count();
auto observedLastRet = lastRet.load(std::memory_order_relaxed);
if (ret != lastRet)
if (retCount != observedLastRet &&
lastRet.compare_exchange_strong(
observedLastRet, retCount, std::memory_order_relaxed))
{
JLOG(m_journal.trace()) << "Validated ledger age is " << ret.count();
lastRet = ret;
JLOG(m_journal.trace()) << "Validated ledger age is " << retCount;
}
return ret;
}
@@ -2271,7 +2277,11 @@ LedgerMaster::doAdvance(std::unique_lock<std::recursive_mutex>& sl)
}
app_.getOPs().clearNeedNetworkLedger();
progress = newPFWork("pf:newLedger", sl);
// Publishing is progress even without pathfinding clients. Keep
// the shutdown guard so this loop cannot re-enter history work
// after the Application starts stopping.
newPFWork("pf:newLedger", sl);
progress = !app_.isStopping();
}
if (progress)
mAdvanceWork = true;

View File

@@ -1741,6 +1741,10 @@ ApplicationImp::startGenesisLedger()
auto const next =
std::make_shared<Ledger>(*genesis, timeKeeper().closeTime());
next->updateSkipList();
// Consensus-built ledgers flush their state trees, but this genesis
// successor bypasses that path. Persist its state before it can be
// advertised as complete or loaded by a restarted node.
next->stateMap().flushDirty(hotACCOUNT_NODE);
XRPL_ASSERT(
next->read(keylet::fees()),
"ripple::ApplicationImp::startGenesisLedger : valid ledger fees");

View File

@@ -302,6 +302,9 @@ runUnitTests(
args.emplace_back("--unittest-child");
}
// Children inherit XRPLD_UNIT_TEST_SHARED_MEM and
// XRPLD_UNIT_TEST_MESSAGE_QUEUE, published by multi_runner_parent.
// Do not pass an environment that drops those variables.
for (std::size_t i = 0; i < num_jobs; ++i)
children.emplace_back(
boost::process::exe = exe_name, boost::process::args = args);

View File

@@ -383,6 +383,11 @@ public:
reportFeeChange() override;
void
reportConsensusStateChange(ConsensusPhase phase);
void
reportConsensusStateChangeIfNeeded(
ConsensusPhase phase,
std::unique_ptr<std::stringstream> const& clog,
bool logPhase);
void
updateLocalTx(ReadView const& view) override;
@@ -674,12 +679,14 @@ private:
RCLConsensus mConsensus;
ConsensusPhase mLastConsensusPhase;
std::mutex lastConsensusPhaseMutex_;
ConsensusPhase mLastConsensusPhase{ConsensusPhase::open};
LedgerMaster& m_ledgerMaster;
SubInfoMapType mSubAccount;
SubInfoMapType mSubRTAccount;
bool firstLedgerPublished_{true}; // Guarded by mSubLock.
subRpcMapType mRpcSubMap;
@@ -1010,14 +1017,8 @@ NetworkOPsImp::processHeartbeatTimer()
mConsensus.timerEntry(app_.timeKeeper().closeTime(), clog.ss());
CLOG(clog.ss()) << "consensus phase " << to_string(mLastConsensusPhase);
const ConsensusPhase currPhase = mConsensus.phase();
if (mLastConsensusPhase != currPhase)
{
reportConsensusStateChange(currPhase);
mLastConsensusPhase = currPhase;
CLOG(clog.ss()) << " changed to " << to_string(mLastConsensusPhase);
}
reportConsensusStateChangeIfNeeded(currPhase, clog.ss(), true);
CLOG(clog.ss()) << ". ";
setHeartbeatTimer();
@@ -2094,11 +2095,7 @@ NetworkOPsImp::beginConsensus(
clog);
const ConsensusPhase currPhase = mConsensus.phase();
if (mLastConsensusPhase != currPhase)
{
reportConsensusStateChange(currPhase);
mLastConsensusPhase = currPhase;
}
reportConsensusStateChangeIfNeeded(currPhase, clog, false);
JLOG(m_journal.debug()) << "Initiating consensus engine";
return true;
@@ -3168,11 +3165,10 @@ NetworkOPsImp::pubLedger(std::shared_ptr<ReadView const> const& lpAccepted)
}
{
static bool firstTime = true;
if (firstTime)
if (firstLedgerPublished_)
{
// First validated ledger, start delayed SubAccountHistory
firstTime = false;
firstLedgerPublished_ = false;
for (auto& outer : mSubAccountHistory)
{
for (auto& inner : outer.second)
@@ -3225,6 +3221,24 @@ NetworkOPsImp::reportConsensusStateChange(ConsensusPhase phase)
[this, phase]() { pubConsensus(phase); });
}
void
NetworkOPsImp::reportConsensusStateChangeIfNeeded(
ConsensusPhase phase,
std::unique_ptr<std::stringstream> const& clog,
bool logPhase)
{
std::scoped_lock const lock(lastConsensusPhaseMutex_);
if (logPhase)
CLOG(clog) << "consensus phase " << to_string(mLastConsensusPhase);
if (mLastConsensusPhase != phase)
{
reportConsensusStateChange(phase);
mLastConsensusPhase = phase;
if (logPhase)
CLOG(clog) << " changed to " << to_string(mLastConsensusPhase);
}
}
inline void
NetworkOPsImp::updateLocalTx(ReadView const& view)
{

View File

@@ -29,6 +29,7 @@
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxMeta.h>
#include <mutex>
#include <optional>
#include <variant>
@@ -100,30 +101,35 @@ public:
LedgerIndex
getLedger() const
{
std::scoped_lock const lock(mutableStateMutex_);
return mLedgerIndex;
}
bool
isValidated() const
{
std::scoped_lock const lock(mutableStateMutex_);
return mLedgerIndex != 0;
}
TransStatus
getStatus() const
{
std::scoped_lock const lock(mutableStateMutex_);
return mStatus;
}
TER
getResult()
getResult() const
{
std::scoped_lock const lock(mutableStateMutex_);
return mResult;
}
void
setResult(TER terResult)
{
std::scoped_lock const lock(mutableStateMutex_);
mResult = terResult;
}
@@ -137,12 +143,14 @@ public:
void
setStatus(TransStatus status)
{
std::scoped_lock const lock(mutableStateMutex_);
mStatus = status;
}
void
setLedger(LedgerIndex ledger)
{
std::scoped_lock const lock(mutableStateMutex_);
mLedgerIndex = ledger;
}
@@ -212,6 +220,7 @@ public:
SubmitResult
getSubmitResult() const
{
std::scoped_lock const lock(mutableStateMutex_);
return submitResult_;
}
@@ -221,6 +230,7 @@ public:
void
clearSubmitResult()
{
std::scoped_lock const lock(mutableStateMutex_);
submitResult_.clear();
}
@@ -230,6 +240,7 @@ public:
void
setApplied()
{
std::scoped_lock const lock(mutableStateMutex_);
submitResult_.applied = true;
}
@@ -239,6 +250,7 @@ public:
void
setQueued()
{
std::scoped_lock const lock(mutableStateMutex_);
submitResult_.queued = true;
}
@@ -248,6 +260,7 @@ public:
void
setBroadcast()
{
std::scoped_lock const lock(mutableStateMutex_);
submitResult_.broadcast = true;
}
@@ -257,6 +270,7 @@ public:
void
setKept()
{
std::scoped_lock const lock(mutableStateMutex_);
submitResult_.kept = true;
}
@@ -289,6 +303,7 @@ public:
std::optional<CurrentLedgerState>
getCurrentLedgerState() const
{
std::scoped_lock const lock(mutableStateMutex_);
return currentLedgerState_;
}
@@ -306,6 +321,7 @@ public:
std::uint32_t accountSeq,
std::uint32_t availableSeq)
{
std::scoped_lock const lock(mutableStateMutex_);
currentLedgerState_.emplace(
validatedLedger, fee, accountSeq, availableSeq);
}
@@ -391,6 +407,9 @@ private:
uint256 mTransactionID;
// Response and locator state is read by RPC/relay while NetworkOPs
// updates it. Snapshot the related locator fields together for JSON.
mutable std::mutex mutableStateMutex_;
LedgerIndex mLedgerIndex = 0;
std::optional<uint32_t> mTxnSeq;
std::optional<uint16_t> mNetworkID;

View File

@@ -534,6 +534,8 @@ private:
FeeLevel64 const feeLevel;
/// Transaction ID.
TxID const txID;
// Updated only while absent from the intrusive byFee_ index.
uint256 parentHashSortKey;
/// Account submitting the transaction.
AccountID const account;
/// Expiration ledger for the transaction
@@ -589,16 +591,6 @@ private:
*/
static constexpr int retriesAllowed = 10;
/** The hash of the parent ledger.
This is used to pseudo-randomize the transaction order when
populating byFee_, by XORing it with the transaction hash (txID).
Using a single static and doing the XOR operation every time was
tested to be as fast or faster than storing the computed "sort key",
and obviously uses less memory.
*/
static LedgerHash parentHashComp;
public:
/// Constructor
MaybeTx(
@@ -608,6 +600,12 @@ private:
ApplyFlags const flags,
PreflightResult const& pfresult);
void
setParentHashSortKey(LedgerHash const& parentHash)
{
parentHashSortKey = txID ^ parentHash;
}
/// Attempt to apply the queued transaction to the open ledger.
ApplyResult
apply(Application& app, OpenView& view, beast::Journal j);
@@ -663,8 +661,7 @@ private:
operator()(const MaybeTx& lhs, const MaybeTx& rhs) const
{
if (lhs.feeLevel == rhs.feeLevel)
return (lhs.txID ^ MaybeTx::parentHashComp) <
(rhs.txID ^ MaybeTx::parentHashComp);
return lhs.parentHashSortKey < rhs.parentHashSortKey;
return lhs.feeLevel > rhs.feeLevel;
}
};
@@ -798,9 +795,7 @@ private:
*/
std::optional<size_t> maxSize_;
/**
parentHash_ used for logging only
*/
/// Parent hash used to salt newly queued candidates.
LedgerHash parentHash_{beast::zero};
/** Most queue operations are done under the master lock,

View File

@@ -64,6 +64,7 @@ Transaction::setStatus(
std::optional<std::uint32_t> tseq,
std::optional<std::uint16_t> netID)
{
std::scoped_lock const lock(mutableStateMutex_);
mStatus = ts;
mLedgerIndex = lseq;
if (tseq)
@@ -167,37 +168,47 @@ Transaction::getJson(JsonOptions options, bool binary) const
Json::Value ret(
mTransaction->getJson(options & ~JsonOptions::include_date, binary));
LedgerIndex ledgerIndex;
std::optional<std::uint32_t> transactionSeq;
std::optional<std::uint16_t> networkID;
{
std::scoped_lock const lock(mutableStateMutex_);
ledgerIndex = mLedgerIndex;
transactionSeq = mTxnSeq;
networkID = mNetworkID;
}
// NOTE Binary STTx::getJson output might not be a JSON object
if (ret.isObject() && mLedgerIndex)
if (ret.isObject() && ledgerIndex)
{
if (!(options & JsonOptions::disable_API_prior_V2))
{
// Behaviour before API version 2
ret[jss::inLedger] = mLedgerIndex;
ret[jss::inLedger] = ledgerIndex;
}
// TODO: disable_API_prior_V3 to disable output of both `date` and
// `ledger_index` elements (taking precedence over include_date)
ret[jss::ledger_index] = mLedgerIndex;
ret[jss::ledger_index] = ledgerIndex;
if (options & JsonOptions::include_date)
{
auto ct = mApp.getLedgerMaster().getCloseTimeBySeq(mLedgerIndex);
auto ct = mApp.getLedgerMaster().getCloseTimeBySeq(ledgerIndex);
if (ct)
ret[jss::date] = ct->time_since_epoch().count();
}
// compute outgoing CTID
// override local network id if it's explicitly in the txn
std::optional netID = mNetworkID;
auto netID = networkID;
if (mTransaction->isFieldPresent(sfNetworkID))
netID = mTransaction->getFieldU32(sfNetworkID);
if (mTxnSeq && netID && *mTxnSeq <= 0xFFFFU && *netID < 0xFFFFU &&
mLedgerIndex < 0xFFFFFFFUL)
if (transactionSeq && netID && *transactionSeq <= 0xFFFFU &&
*netID < 0xFFFFU && ledgerIndex < 0xFFFFFFFUL)
{
std::optional<std::string> ctid =
RPC::encodeCTID(mLedgerIndex, *mTxnSeq, *netID);
RPC::encodeCTID(ledgerIndex, *transactionSeq, *netID);
if (ctid)
ret[jss::ctid] = *ctid;
}

View File

@@ -292,8 +292,6 @@ TxQ::FeeMetrics::escalatedSeriesFeeLevel(
return {totalFeeLevel.has_value(), *totalFeeLevel};
}
LedgerHash TxQ::MaybeTx::parentHashComp{};
TxQ::MaybeTx::MaybeTx(
std::shared_ptr<STTx const> const& txn_,
TxID const& txID_,
@@ -303,6 +301,7 @@ TxQ::MaybeTx::MaybeTx(
: txn(txn_)
, feeLevel(feeLevel_)
, txID(txID_)
, parentHashSortKey(txID_)
, account(txn_->getAccountID(sfAccount))
, firstValid(getFirstLedgerSequence(*txn_))
, lastValid(getLastLedgerSequence(*txn_))
@@ -1365,6 +1364,7 @@ TxQ::apply(
{tx, transactionID, feeLevelPaid, flags, pfresult});
// Then index it into the byFee lookup.
candidate.setParentHashSortKey(parentHash_);
byFee_.insert(candidate);
JLOG(j_.debug()) << "Added transaction " << candidate.txID
<< " with result " << transToken(pfresult.ter) << " from "
@@ -1850,12 +1850,11 @@ TxQ::accept(Application& app, OpenView& view)
// time, create a new list and merge the old list into it.
byFee_.clear();
MaybeTx::parentHashComp = parentHash;
for (auto& [_, account] : byAccount_)
{
for (auto& [_, candidate] : account.transactions)
{
candidate.setParentHashSortKey(parentHash);
byFee_.insert(candidate);
}
}

View File

@@ -297,13 +297,12 @@ saveValidatedLedger(
}
{
static boost::format deleteLedger(
"DELETE FROM Ledgers WHERE LedgerSeq = %u;");
static boost::format deleteTrans1(
boost::format deleteLedger("DELETE FROM Ledgers WHERE LedgerSeq = %u;");
boost::format deleteTrans1(
"DELETE FROM Transactions WHERE LedgerSeq = %u;");
static boost::format deleteTrans2(
boost::format deleteTrans2(
"DELETE FROM AccountTransactions WHERE LedgerSeq = %u;");
static boost::format deleteAcctTrans(
boost::format deleteAcctTrans(
"DELETE FROM AccountTransactions WHERE TransID = '%s';");
{

View File

@@ -30,12 +30,6 @@
namespace ripple {
TxConsequences
ClaimReward::makeTxConsequences(PreflightContext const& ctx)
{
return TxConsequences{ctx.tx, TxConsequences::normal};
}
NotTEC
ClaimReward::preflight(PreflightContext const& ctx)
{

View File

@@ -32,15 +32,12 @@ namespace ripple {
class ClaimReward : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit ClaimReward(ApplyContext& ctx) : Transactor(ctx)
{
}
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);

View File

@@ -30,12 +30,6 @@
namespace ripple {
TxConsequences
Cron::makeTxConsequences(PreflightContext const& ctx)
{
return TxConsequences{ctx.tx, TxConsequences::normal};
}
NotTEC
Cron::preflight(PreflightContext const& ctx)
{

View File

@@ -30,7 +30,7 @@ namespace ripple {
class Cron : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit Cron(ApplyContext& ctx) : Transactor(ctx)
{
@@ -39,9 +39,6 @@ public:
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);

View File

@@ -27,12 +27,6 @@
namespace ripple {
TxConsequences
CronSet::makeTxConsequences(PreflightContext const& ctx)
{
return TxConsequences{ctx.tx, TxConsequences::normal};
}
NotTEC
CronSet::preflight(PreflightContext const& ctx)
{

View File

@@ -29,7 +29,7 @@ namespace ripple {
class CronSet : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit CronSet(ApplyContext& ctx) : Transactor(ctx)
{
@@ -38,9 +38,6 @@ public:
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);

View File

@@ -26,12 +26,6 @@
namespace ripple {
TxConsequences
Invoke::makeTxConsequences(PreflightContext const& ctx)
{
return TxConsequences{ctx.tx, TxConsequences::normal};
}
NotTEC
Invoke::preflight(PreflightContext const& ctx)
{

View File

@@ -30,7 +30,7 @@ namespace ripple {
class Invoke : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit Invoke(ApplyContext& ctx) : Transactor(ctx)
{
@@ -39,9 +39,6 @@ public:
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);

View File

@@ -30,12 +30,6 @@
namespace ripple {
TxConsequences
SetRemarks::makeTxConsequences(PreflightContext const& ctx)
{
return TxConsequences{ctx.tx, TxConsequences::normal};
}
NotTEC
SetRemarks::validateRemarks(STArray const& remarks, beast::Journal const& j)
{

View File

@@ -30,7 +30,7 @@ namespace ripple {
class SetRemarks : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit SetRemarks(ApplyContext& ctx) : Transactor(ctx)
{
@@ -39,9 +39,6 @@ public:
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);

View File

@@ -37,7 +37,6 @@
#include <xrpl/json/to_string.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/JSONTxSignatures.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/STAccount.h>
#include <xrpl/protocol/UintTypes.h>
@@ -103,22 +102,6 @@ preflight1(PreflightContext const& ctx)
return temMALFORMED;
}
// sfTime and sfJsonTxDelta are common fields, so every transaction type
// can carry them the moment this binary ships. A node on an older build
// cannot parse them, so accepting one into a ledger before the amendment
// activates would split consensus.
if (ctx.tx.isFieldPresent(sfTime) || ctx.tx.isFieldPresent(sfJsonTxDelta))
{
if (!ctx.rules.enabled(featureJsonTx))
return temDISABLED;
// unsanitize_jsontx refuses anything larger, so a bigger delta is
// only ever ledger weight that can never verify
if (ctx.tx.isFieldPresent(sfJsonTxDelta) &&
ctx.tx.getFieldVL(sfJsonTxDelta).size() > jsontx_max_diff)
return temMALFORMED;
}
auto const ret = preflight0(ctx);
if (!isTesSuccess(ret))
return ret;

View File

@@ -23,7 +23,6 @@
#include <xrpld/app/tx/applySteps.h>
#include <xrpl/basics/Log.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/JSONTxSignatures.h>
namespace ripple {
@@ -98,44 +97,6 @@ checkValidity(
return {Validity::Valid, ""};
}
if (rules.enabled(featureJsonTx) && tx.isFieldPresent(sfJsonTxDelta))
{
// A JsonTx signs a plaintext preimage, so STTx::checkSign below -- a
// check against the binary signing hash -- can never succeed for it.
// The delta cannot be stripped to downgrade to a binary check either:
// that leaves a TxnSignature over text the binary hash does not match.
//
// This proves only that the key signed the preimage. Tying that key to
// sfAccount is still Transactor::checkSingleSign's job, exactly as for
// a binary-signed transaction.
// jsontx_verify canonicalizes twice and then verifies ed25519, and
// checkValidity runs on every relay, so honour the cache the binary
// path below uses rather than redoing that work per peer.
if (flags & SF_SIGBAD)
return {Validity::SigBad, "Transaction has bad signature."};
if (!(flags & SF_SIGGOOD))
{
try
{
(void)jsontx_verify(tx);
}
catch (std::exception const& e)
{
router.setFlags(id, SF_SIGBAD);
return {Validity::SigBad, e.what()};
}
router.setFlags(id, SF_SIGGOOD);
}
std::string reason;
if (!passesLocalChecks(tx, reason))
return {Validity::SigGoodOnly, reason};
return {Validity::Valid, ""};
}
if (flags & SF_SIGBAD)
// Signature is known bad
return {Validity::SigBad, "Transaction has bad signature."};

View File

@@ -310,6 +310,9 @@ class Validations
// Sequence of the largest validation received from each node
hash_map<NodeID, SeqEnforcer<Seq>> seqEnforcers_;
// Each validation store owns its keep-range refresh schedule.
std::chrono::steady_clock::time_point refreshTime_{};
//! Validations from listed nodes, indexed by ledger id (partial and full)
beast::aged_unordered_map<
ID,
@@ -735,13 +738,12 @@ public:
{
// We only need to refresh the keep range when it's just about
// to expire. Track the next time we need to refresh.
static std::chrono::steady_clock::time_point refreshTime;
if (auto const now = byLedger_.clock().now();
refreshTime <= now)
refreshTime_ <= now)
{
// The next refresh time is shortly before the expiration
// time from now.
refreshTime = now + parms_.validationSET_EXPIRES -
refreshTime_ = now + parms_.validationSET_EXPIRES -
parms_.validationFRESHNESS;
for (auto i = byLedger_.begin(); i != byLedger_.end(); ++i)

View File

@@ -658,8 +658,7 @@ private:
// to discard duplicate message from the same peer. A message
// is aged after IDLED seconds. A message received IDLED seconds
// after it was relayed is ignored by PeerImp.
inline static messages peersWithMessage_{
beast::get_abstract_clock<clock_type>()};
messages peersWithMessage_{beast::get_abstract_clock<clock_type>()};
};
template <typename clock_type>

View File

@@ -285,7 +285,8 @@ parseMessageContent(MessageHeader const& header, Buffers const& buffers)
if (payloadSize == 0 || !m->ParseFromArray(payload.data(), payloadSize))
return {};
}
else if (!m->ParseFromZeroCopyStream(&stream))
else if (!m->ParseFromBoundedZeroCopyStream(
&stream, static_cast<int>(header.payload_wire_size)))
return {};
return m;

View File

@@ -30,15 +30,9 @@
#include <xrpld/rpc/detail/RPCHelpers.h>
#include <xrpld/rpc/detail/TransactionSign.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/json/json_reader.h>
#include <xrpl/json/json_writer.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/JSONTxSignatures.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/RPCErr.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <xrpl/resource/Fees.h>
namespace ripple {
@@ -94,10 +88,8 @@ doInject(RPC::JsonContext& context)
}
// {
// tx_blob: <string>
// OR tx: <json text> together with sig: <hex>
// OR manifest: <hex>
// OR tx_json: <object> together with secret: <secret> (deprecated)
// tx_blob: <string> XOR tx_json: <object>,
// secret: <secret>
// }
Json::Value
doSubmit(RPC::JsonContext& context)
@@ -106,37 +98,17 @@ doSubmit(RPC::JsonContext& context)
context.loadType = Resource::feeMediumBurdenRPC;
auto const view = context.app.openLedger().current();
bool const isJsonTx =
context.params.isMember(jss::tx) && context.params.isMember(jss::sig);
bool const hasManifest = context.params.isMember(jss::manifest);
bool const hasTxBlob = context.params.isMember(jss::tx_blob);
// Both of these carry authority that only their amendment teaches the
// network to honour, so without the amendment the submitter is told their
// transaction is unsigned, which reads as their mistake. It isn't -- the
// feature is not live yet -- so say so before touching the payload at all.
if (isJsonTx && !view->rules().enabled(featureJsonTx))
return RPC::make_error(
rpcNOT_ENABLED,
"The JsonTx amendment is not enabled on this network. "
"Plaintext-JSON submission will work once it activates; nothing "
"is wrong with this request.");
if (hasManifest && !view->rules().enabled(featureOnChainManifests))
return RPC::make_error(
rpcNOT_ENABLED,
"The OnChainManifests amendment is not enabled on this "
"network. Manifest submission will work once it activates; "
"nothing is wrong with this request.");
int const count =
(hasTxBlob ? 1 : 0) + (isJsonTx ? 1 : 0) + (hasManifest ? 1 : 0);
if (!count)
if (hasManifest && hasTxBlob)
{
return RPC::make_error(
rpcINVALID_PARAMS,
"Specify exactly one of either `tx_blob` or `manifest`");
}
else if (!hasTxBlob && !hasManifest)
{
// legacy signing code
auto const failType = getFailHard(context);
if (context.role != Role::ADMIN && !context.app.config().canSign())
@@ -160,22 +132,30 @@ doSubmit(RPC::JsonContext& context)
return ret;
}
else if (count != 1)
{
return RPC::make_error(
rpcINVALID_PARAMS,
"Specify exactly one of `tx_blob`, `manifest`, or `tx` together "
"with `sig`");
}
// execution to here means exactly one of isJsonTx, hasManifest or
// hasTxBlob is true
std::string txBlob =
hasTxBlob ? context.params[jss::tx_blob].asString() : "";
if (hasManifest)
{
// OnChainManifests amendment accepts a manifest submission here; turn
// it into the transaction that carries it and drop through to normal
// tx_blob processing below.
auto const view = context.app.openLedger().current();
// The transaction built below carries no account signature; its
// authority is the manifest's own master and ephemeral signatures,
// which checkValidity() only honours once the amendment is active.
// Without this the submitter is told their transaction is unsigned,
// which reads as their mistake. It isn't -- the feature is not live
// yet -- so say so before touching the manifest at all.
if (!view->rules().enabled(featureOnChainManifests))
return RPC::make_error(
rpcNOT_ENABLED,
"The OnChainManifests amendment is not enabled on this "
"network. Manifest submission will work once it activates; "
"nothing is wrong with this request.");
auto const raw = strUnHex(context.params[jss::manifest].asString());
if (!raw || raw->empty())
return rpcError(rpcINVALID_PARAMS);
@@ -195,84 +175,18 @@ doSubmit(RPC::JsonContext& context)
txBlob = *hex;
}
std::optional<Blob> ret;
auto ret = strUnHex(txBlob);
if (!isJsonTx)
{
ret = strUnHex(txBlob);
if (!ret || !ret->size())
return rpcError(rpcINVALID_PARAMS);
if (!ret || ret->empty())
return rpcError(rpcINVALID_PARAMS);
}
SerialIter sitTrans(makeSlice(*ret));
std::shared_ptr<STTx const> stTx;
try
{
if (!isJsonTx)
{
SerialIter sitTrans(makeSlice(*ret));
stTx = std::make_shared<STTx const>(std::ref(sitTrans));
}
else
{
if (!context.params.isMember(jss::sig))
throw std::runtime_error("JsonTx: missing sig parameter");
std::string const raw = context.params[jss::tx].asString();
auto const [san, diff] = sanitize_jsontx(raw);
auto const sig = strUnHex(context.params[jss::sig].asString());
if (!sig || sig->empty())
throw std::runtime_error("JsonTx: bad signature");
Json::Value jv;
if (Json::Reader r; !r.parse(san, jv))
throw std::runtime_error("JsonTx: unparsable canonical form");
// The preimage carries the key but not the signature over itself.
for (auto const& n :
{sfTxnSignature.fieldName, sfSigners.fieldName})
if (jv.isMember(n))
throw std::runtime_error(
"JsonTx: " + n + " must not appear in tx");
if (!jv.isMember(sfSigningPubKey.fieldName))
throw std::runtime_error("JsonTx: tx must carry SigningPubKey");
// Hand the parser the u64 rather than teaching STUInt64 a second
// spelling; the ISO form only ever exists in the preimage.
std::optional<std::uint64_t> ms;
if (jv.isMember(sfTime.fieldName))
{
ms = jsontx_iso(jv[sfTime.fieldName].asString());
jv.removeMember(sfTime.fieldName);
}
STParsedJSONObject parsed("tx_json", jv);
if (!parsed.object)
throw std::runtime_error(
parsed.error[jss::error_message].asString());
if (ms)
parsed.object->setFieldU64(sfTime, *ms);
parsed.object->setFieldVL(sfTxnSignature, *sig);
// The delta rides along in the transaction. Without it a relaying
// node has nothing to reconstruct the preimage from, the binary
// TxnSignature check fails there, and the transaction never
// propagates past this node.
parsed.object->setFieldVL(sfJsonTxDelta, makeSlice(diff));
stTx = std::make_shared<STTx const>(std::move(*parsed.object));
// Round-trip the binary codec and run the check a relaying node
// will run, so this path cannot accept anything the network would
// later reject -- and so the caller gets the real reason rather
// than a bare "fails local checks" from checkValidity below.
Serializer ser;
stTx->add(ser);
SerialIter si(ser.slice());
STTx const rt{si};
if (jsontx_verify(rt) != raw)
throw std::runtime_error("JsonTx: does not round-trip");
}
stTx = std::make_shared<STTx const>(std::ref(sitTrans));
}
catch (std::exception& e)
{