mirror of
https://github.com/Xahau/xahaud.git
synced 2026-10-11 22:38:02 +00:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de7afa9895 | ||
|
|
983c5dfe0c | ||
|
|
47b00e9a65 | ||
|
|
ee1d44f7d6 | ||
|
|
5995cc2d00 | ||
|
|
e9cc9e6fcb | ||
|
|
350347c615 | ||
|
|
7112b80d8f | ||
|
|
3de9cc3ba7 | ||
|
|
e35ae4af20 | ||
|
|
d916f108da | ||
|
|
a09c180a93 | ||
|
|
33939f0b02 | ||
|
|
779a07804b | ||
|
|
60caaea495 | ||
|
|
4bbcc2d953 | ||
|
|
72d793d912 | ||
|
|
db25f9799c | ||
|
|
e470d8a70b | ||
|
|
4fb88f5f80 | ||
|
|
b2cdcf429e | ||
|
|
3fd6ee472a | ||
|
|
b2aeb1cd92 | ||
|
|
0f56d2d807 | ||
|
|
e904290e4e | ||
|
|
723103150c |
@@ -105,6 +105,8 @@
|
||||
#define sfMPTAmount ((3U << 16U) + 26U)
|
||||
#define sfIssuerNode ((3U << 16U) + 27U)
|
||||
#define sfSubjectNode ((3U << 16U) + 28U)
|
||||
#define sfLastTxnTime ((3U << 16U) + 95U)
|
||||
#define sfTime ((3U << 16U) + 96U)
|
||||
#define sfTouchCount ((3U << 16U) + 97U)
|
||||
#define sfAccountIndex ((3U << 16U) + 98U)
|
||||
#define sfAccountCount ((3U << 16U) + 99U)
|
||||
@@ -222,6 +224,7 @@
|
||||
#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)
|
||||
|
||||
182
include/xrpl/protocol/JSONTxSignatures.h
Normal file
182
include/xrpl/protocol/JSONTxSignatures.h
Normal file
@@ -0,0 +1,182 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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/protocol/SField.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// jsontx: plaintext-JSON signing support (featureJsonTx)
|
||||
//
|
||||
// A JsonTx is authorised by an ed25519 signature over
|
||||
//
|
||||
// jsontx_sign_prefix || preimage
|
||||
//
|
||||
// where the preimage is the exact JSON text the signer read. The node rebuilds
|
||||
// the preimage from the binary transaction plus sfJsonTxDelta, a small delta
|
||||
// against a canonical form the node derives itself. The delta is
|
||||
// attacker-controlled: it 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.
|
||||
//
|
||||
// CONSENSUS SURFACE. Once featureJsonTx activates, everything that decides
|
||||
// whether a preimage verifies is a consensus rule and can only change behind
|
||||
// a further amendment. That is:
|
||||
//
|
||||
// - every constant and every function in this file, including the delta
|
||||
// encoder's exact output (jsontx_verify compares it byte for byte);
|
||||
// - STObject::getJson(JsonOptions::none) and the getJson of every ST type
|
||||
// that can appear in a transaction (STAmount, STUInt64, STPathSet,
|
||||
// STIssue, STVector256, ...), because the node's canonical form is
|
||||
// derived from it;
|
||||
// - Json::FastWriter, which serializes that JSON before canonicalization;
|
||||
// - STParsedJSON, only in so far as the submit RPC builds the transaction
|
||||
// from the canonical form: a change there changes which preimages can be
|
||||
// submitted, not which verify.
|
||||
//
|
||||
// None of that code was consensus-critical before. A change to how any field
|
||||
// renders as JSON - including one merged from upstream rippled - changes
|
||||
// which signatures verify and must be amendment-gated. JSONTxSignatures_test
|
||||
// pins this for every field of every transaction format.
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
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
|
||||
inline constexpr std::size_t jsontx_max_depth = 32; // JSON nesting
|
||||
|
||||
// The largest magnitude a bare JSON integer may have: 2^53 - 1, the range
|
||||
// RFC 8259 section 6 calls interoperable and RFC 7493 (I-JSON) section 2.2
|
||||
// requires. Past it an IEEE 754 double - which is what JSON.parse and most
|
||||
// other parsers produce - can no longer hold every integer, so the number a
|
||||
// wallet shows after parsing the preimage may not be the number that
|
||||
// executes. Larger values are written as strings, which is how getJson
|
||||
// renders every amount and UInt64 anyway.
|
||||
inline constexpr std::uint64_t jsontx_max_int = (std::uint64_t{1} << 53) - 1;
|
||||
|
||||
// Domain separation. Without it the signed message is bare JSON text, and any
|
||||
// wallet feature that signs a user-visible text message with the account key
|
||||
// (several do, over the raw bytes) could be driven to sign a live Payment
|
||||
// presented as a "log in" message. 0xFF can never occur in UTF-8, so no text
|
||||
// signer can produce these bytes; the rest follows HashPrefix convention.
|
||||
inline constexpr std::string_view jsontx_sign_prefix{
|
||||
"\xFF"
|
||||
"JTX",
|
||||
4};
|
||||
|
||||
// The exact message a JsonTx signer signs for `preimage`.
|
||||
std::string
|
||||
jsontx_signing_data(std::string_view preimage);
|
||||
|
||||
// ASCII-only case folding; never consults the locale.
|
||||
std::string
|
||||
jsontx_lower(std::string_view s);
|
||||
|
||||
// Case-insensitive field-name -> canonical SField, over the serializable
|
||||
// fields doServerDefinitions publishes. sfInvalid if unknown.
|
||||
SField const&
|
||||
jsontx_field(std::string const& name);
|
||||
|
||||
// days from 1970-01-01 (Howard Hinnant's civil calendar algorithm)
|
||||
constexpr std::int64_t
|
||||
jsontx_days(int y, unsigned m, unsigned d)
|
||||
{
|
||||
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);
|
||||
static_assert(jsontx_epoch_day == 10957); // matches chrono.h epoch_offset
|
||||
|
||||
// 9999-12-31T23:59:59.999Z: past this toISOString() switches to expanded years
|
||||
// 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;
|
||||
|
||||
// Strict Date().toISOString() -> milliseconds since the ripple epoch.
|
||||
std::uint64_t
|
||||
jsontx_iso(std::string_view s);
|
||||
|
||||
// The exact inverse over [0, jsontx_max_time].
|
||||
std::string
|
||||
jsontx_iso_str(std::uint64_t ms);
|
||||
|
||||
// How STUInt64::getJson spells v for field f (hex, or base ten for
|
||||
// sMD_BaseTen fields).
|
||||
std::string
|
||||
jsontx_u64_str(SField const& f, std::uint64_t v);
|
||||
|
||||
// Validates that `raw` is a JsonTx document: exactly one JSON object, nothing
|
||||
// before or after it but whitespace, printable ASCII only, no escapes, no
|
||||
// comments, no booleans or nulls, no duplicate keys, integers spelled
|
||||
// -?(0|[1-9][0-9]*), never -0, and no larger in magnitude than
|
||||
// jsontx_max_int, nesting at most jsontx_max_depth. Throws.
|
||||
void
|
||||
jsontx_strict(std::string_view raw);
|
||||
|
||||
// The canonical form of `raw` alone: whitespace stripped, field names in
|
||||
// their Xahau spelling, members ordered by field code, numbers formatted per
|
||||
// field type, and - at the root only - an omitted Sequence written as
|
||||
// "Sequence":0 when the document has a Time or a TicketSequence. Throws on
|
||||
// anything it cannot canonicalize.
|
||||
std::string
|
||||
jsontx_canonical(std::string_view raw);
|
||||
|
||||
// Returns { canonical, delta } where applying delta to canonical with
|
||||
// unsanitize_jsontx reproduces `raw` byte for byte. Throws.
|
||||
std::pair<std::string, std::string>
|
||||
sanitize_jsontx(std::string_view raw);
|
||||
|
||||
// Applies an UNTRUSTED delta to a canonical form the node derived itself.
|
||||
std::string
|
||||
unsanitize_jsontx(std::string_view sanitized, std::string_view diff);
|
||||
|
||||
// The complete untrusted-side check, shared by the submit RPC and the
|
||||
// relay/consensus path. Returns the reconstructed preimage or throws.
|
||||
std::string
|
||||
jsontx_verify(STTx const& stx, std::string_view diff);
|
||||
|
||||
// As above with the delta taken from sfJsonTxDelta. This is what
|
||||
// checkValidity calls in place of STTx::checkSign for a transaction
|
||||
// carrying a delta.
|
||||
std::string
|
||||
jsontx_verify(STTx const& stx);
|
||||
|
||||
} // namespace ripple
|
||||
#endif
|
||||
@@ -154,6 +154,21 @@ std::size_t constexpr maxPriceScale = 20;
|
||||
*/
|
||||
std::size_t constexpr maxTrim = 25;
|
||||
|
||||
/** sfTime validity window (featureJsonTx).
|
||||
|
||||
A transaction carrying sfTime (milliseconds since the ripple epoch) is
|
||||
accepted only while the parent ledger's close time lies within
|
||||
[Time - txTimeMaxFuture, Time + txTimeMaxAge]. The upper bound is the
|
||||
stand-in for LastLedgerSequence: once a validated ledger closes later
|
||||
than Time + txTimeMaxAge the transaction can never apply. The lower bound
|
||||
keeps the validity period of a signed transaction short and limits how
|
||||
far ahead of the network a fast client clock can push sfLastTxnTime.
|
||||
|
||||
Both are consensus rules once featureJsonTx activates.
|
||||
*/
|
||||
std::uint64_t constexpr txTimeMaxAgeMs = 300'000;
|
||||
std::uint64_t constexpr txTimeMaxFutureMs = 120'000;
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
|
||||
@@ -101,6 +101,18 @@ public:
|
||||
SeqProxy
|
||||
getSeqProxy() const;
|
||||
|
||||
/** True if replay protection comes from sfTime rather than a sequence.
|
||||
|
||||
That is: sfTime is present, Sequence is 0 and there is no
|
||||
TicketSequence. Such a transaction neither checks nor consumes the
|
||||
account's Sequence; it must instead carry a Time strictly greater
|
||||
than the account's sfLastTxnTime, and records its Time there. Its
|
||||
SeqProxy is sequence(0), so anything that derives an object id from
|
||||
the SeqProxy must use seqID() instead.
|
||||
*/
|
||||
bool
|
||||
isTimeSequenced() const;
|
||||
|
||||
boost::container::flat_set<AccountID>
|
||||
getMentionedAccounts() const;
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
// 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_FIX (20261005, Supported::yes, VoteBehavior::DefaultYes)
|
||||
XRPL_FEATURE(EscrowDestinationCancel, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (20260929, Supported::yes, VoteBehavior::DefaultNo)
|
||||
|
||||
@@ -263,6 +263,7 @@ LEDGER_ENTRY(ltACCOUNT_ROOT, 0x0061, AccountRoot, account, ({
|
||||
{sfCron, soeOPTIONAL},
|
||||
{sfAMMID, soeOPTIONAL},
|
||||
{sfManifestID, soeOPTIONAL},
|
||||
{sfLastTxnTime, soeOPTIONAL},
|
||||
}))
|
||||
|
||||
/** A ledger object which contains a list of object identifiers.
|
||||
|
||||
@@ -153,6 +153,8 @@ 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(sfLastTxnTime, UINT64, 95)
|
||||
TYPED_SFIELD(sfTime, UINT64, 96)
|
||||
TYPED_SFIELD(sfTouchCount, UINT64, 97)
|
||||
TYPED_SFIELD(sfAccountIndex, UINT64, 98)
|
||||
TYPED_SFIELD(sfAccountCount, UINT64, 99)
|
||||
@@ -294,6 +296,7 @@ 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)
|
||||
|
||||
@@ -633,6 +633,7 @@ 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
|
||||
|
||||
992
src/libxrpl/protocol/JSONTxSignatures.cpp
Normal file
992
src/libxrpl/protocol/JSONTxSignatures.cpp
Normal file
@@ -0,0 +1,992 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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/basics/contract.h>
|
||||
#include <xrpl/json/json_writer.h>
|
||||
#include <xrpl/protocol/JSONTxSignatures.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
#include <stdexcept>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
namespace {
|
||||
|
||||
[[noreturn]] void
|
||||
fail(std::string const& why)
|
||||
{
|
||||
Throw<std::runtime_error>("jsontx: " + why);
|
||||
}
|
||||
|
||||
// Not std::isdigit: that consults the locale, and every comparison in this
|
||||
// file decides whether a signature verifies.
|
||||
constexpr bool
|
||||
digit(char c)
|
||||
{
|
||||
return c >= '0' && c <= '9';
|
||||
}
|
||||
|
||||
// json's whitespace set, exactly
|
||||
constexpr bool
|
||||
space(char c)
|
||||
{
|
||||
return c == ' ' || c == '\t' || c == '\r' || c == '\n';
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// The preimage parser.
|
||||
//
|
||||
// Not Json::Reader. The vendored jsoncpp reader accepts comments, stops once
|
||||
// it has a root value rather than requiring end of input, keeps the last of
|
||||
// two identical keys, and holds numbers as 32-bit ints or doubles - so a bare
|
||||
// integer past 2^32 is refused outright and a fractional token is rounded by
|
||||
// sscanf. Each of those is a way for the text a signer reads to differ from
|
||||
// what executes, or for a valid transaction to be unsignable. This parser
|
||||
// accepts a deliberately small language instead:
|
||||
//
|
||||
// - exactly one object, with only whitespace around it;
|
||||
// - printable ASCII only (0x20-0x7E) inside strings, and no backslash at
|
||||
// all. Every string a transaction renders through getJson is plain
|
||||
// printable ASCII (hex, base58, decimal, currency codes, type names), and
|
||||
// jsontx_verify requires the preimage's strings to equal those byte for
|
||||
// byte, so nothing an escape or a non-ASCII byte could spell would ever
|
||||
// verify. Refusing them outright also rules out bidi overrides,
|
||||
// homoglyphs and terminal control sequences in the text being approved;
|
||||
// - integers spelled -?(0|[1-9][0-9]*), never -0, and within
|
||||
// +/-jsontx_max_int, kept as their exact digits rather than converted to
|
||||
// anything;
|
||||
// - no true, false or null: no transaction field renders as one;
|
||||
// - no two members of one object with the same name.
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
struct Node
|
||||
{
|
||||
enum class Kind : std::uint8_t { object, array, string, number };
|
||||
|
||||
Kind kind = Kind::object;
|
||||
std::string text; // string contents, or the number token
|
||||
std::vector<std::string> keys; // object member names, in source order
|
||||
std::vector<Node> items; // object member values, or array items
|
||||
};
|
||||
|
||||
class Reader
|
||||
{
|
||||
std::string_view s_;
|
||||
std::size_t p_ = 0;
|
||||
|
||||
void
|
||||
skip()
|
||||
{
|
||||
while (p_ < s_.size() && space(s_[p_]))
|
||||
++p_;
|
||||
}
|
||||
|
||||
char
|
||||
peek() const
|
||||
{
|
||||
if (p_ >= s_.size())
|
||||
fail("unexpected end of document");
|
||||
return s_[p_];
|
||||
}
|
||||
|
||||
void
|
||||
expect(char c, char const* what)
|
||||
{
|
||||
if (peek() != c)
|
||||
fail(what);
|
||||
++p_;
|
||||
}
|
||||
|
||||
std::string
|
||||
string()
|
||||
{
|
||||
++p_; // opening quote
|
||||
std::size_t const b = p_;
|
||||
for (;;)
|
||||
{
|
||||
if (p_ >= s_.size())
|
||||
fail("unterminated string");
|
||||
auto const c = static_cast<unsigned char>(s_[p_]);
|
||||
if (c == '"')
|
||||
break;
|
||||
if (c == '\\')
|
||||
fail("escape sequences are not allowed");
|
||||
if (c < 0x20 || c > 0x7E)
|
||||
fail("strings must be printable ASCII");
|
||||
++p_;
|
||||
}
|
||||
std::string r(s_.substr(b, p_ - b));
|
||||
++p_; // closing quote
|
||||
return r;
|
||||
}
|
||||
|
||||
std::string
|
||||
number()
|
||||
{
|
||||
std::size_t const b = p_;
|
||||
if (s_[p_] == '-')
|
||||
++p_;
|
||||
if (p_ >= s_.size() || !digit(s_[p_]))
|
||||
fail("number must be a plain integer");
|
||||
if (s_[p_] == '0')
|
||||
++p_;
|
||||
else
|
||||
while (p_ < s_.size() && digit(s_[p_]))
|
||||
++p_;
|
||||
// a fraction, an exponent, a leading zero or anything else glued on
|
||||
if (p_ < s_.size() && !space(s_[p_]) && s_[p_] != ',' &&
|
||||
s_[p_] != '}' && s_[p_] != ']')
|
||||
fail("number must be a plain integer");
|
||||
std::string tok(s_.substr(b, p_ - b));
|
||||
if (tok == "-0")
|
||||
fail("number must be a plain integer");
|
||||
|
||||
// The interoperable range, symmetric about zero. Sixteen digits is
|
||||
// the most 2^53 - 1 has, so the length test also keeps from_chars
|
||||
// well away from overflow.
|
||||
std::string_view const mag =
|
||||
std::string_view(tok).substr(tok.front() == '-' ? 1 : 0);
|
||||
std::uint64_t v = 0;
|
||||
if (mag.size() > 16 ||
|
||||
std::from_chars(mag.data(), mag.data() + mag.size(), v).ec !=
|
||||
std::errc{} ||
|
||||
v > jsontx_max_int)
|
||||
fail(
|
||||
"integer outside +/-(2^53 - 1); write larger values as "
|
||||
"strings");
|
||||
return tok;
|
||||
}
|
||||
|
||||
void
|
||||
value(Node& n, std::size_t depth)
|
||||
{
|
||||
switch (peek())
|
||||
{
|
||||
case '{':
|
||||
object(n, depth + 1);
|
||||
return;
|
||||
case '[':
|
||||
array(n, depth + 1);
|
||||
return;
|
||||
case '"':
|
||||
n.kind = Node::Kind::string;
|
||||
n.text = string();
|
||||
return;
|
||||
case 't':
|
||||
case 'f':
|
||||
fail("boolean values are not allowed");
|
||||
case 'n':
|
||||
fail("null values are not allowed");
|
||||
case '/':
|
||||
fail("comments are not allowed");
|
||||
default:
|
||||
if (peek() == '-' || digit(peek()))
|
||||
{
|
||||
n.kind = Node::Kind::number;
|
||||
n.text = number();
|
||||
return;
|
||||
}
|
||||
fail("unexpected character");
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
object(Node& n, std::size_t depth)
|
||||
{
|
||||
if (depth > jsontx_max_depth)
|
||||
fail("document nested too deeply");
|
||||
n.kind = Node::Kind::object;
|
||||
++p_; // '{'
|
||||
skip();
|
||||
if (peek() != '}')
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
skip();
|
||||
if (peek() != '"')
|
||||
fail("member name must be a string");
|
||||
n.keys.push_back(string());
|
||||
skip();
|
||||
expect(':', "expected ':'");
|
||||
skip();
|
||||
value(n.items.emplace_back(), depth);
|
||||
skip();
|
||||
if (peek() == ',')
|
||||
{
|
||||
++p_;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect('}', "expected ',' or '}'");
|
||||
|
||||
// Two identical names are one member to jsoncpp (the last wins) and
|
||||
// may be either to whatever the signer's wallet parsed.
|
||||
std::vector<std::string_view> ks(n.keys.begin(), n.keys.end());
|
||||
std::sort(ks.begin(), ks.end());
|
||||
if (std::adjacent_find(ks.begin(), ks.end()) != ks.end())
|
||||
fail("duplicate member name");
|
||||
}
|
||||
|
||||
void
|
||||
array(Node& n, std::size_t depth)
|
||||
{
|
||||
if (depth > jsontx_max_depth)
|
||||
fail("document nested too deeply");
|
||||
n.kind = Node::Kind::array;
|
||||
++p_; // '['
|
||||
skip();
|
||||
if (peek() != ']')
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
skip();
|
||||
value(n.items.emplace_back(), depth);
|
||||
skip();
|
||||
if (peek() == ',')
|
||||
{
|
||||
++p_;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(']', "expected ',' or ']'");
|
||||
}
|
||||
|
||||
public:
|
||||
explicit Reader(std::string_view s) : s_(s)
|
||||
{
|
||||
}
|
||||
|
||||
Node
|
||||
parse()
|
||||
{
|
||||
Node root;
|
||||
skip();
|
||||
if (p_ >= s_.size() || s_[p_] != '{')
|
||||
fail("document must be an object");
|
||||
object(root, 1);
|
||||
skip();
|
||||
if (p_ != s_.size())
|
||||
fail(
|
||||
s_[p_] == '/' ? "comments are not allowed"
|
||||
: "trailing data after document");
|
||||
return root;
|
||||
}
|
||||
};
|
||||
|
||||
Node
|
||||
parse(std::string_view raw)
|
||||
{
|
||||
if (raw.size() > jsontx_max_text)
|
||||
fail("document too large");
|
||||
return Reader(raw).parse();
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Canonicalization, directed by field type.
|
||||
//
|
||||
// Keys of a transaction or inner object are SField names, matched
|
||||
// case-insensitively and re-emitted in their canonical spelling, ordered by
|
||||
// field code. A field's type decides what its value may be and how it is
|
||||
// written. The handful of non-field objects a transaction renders (an issued
|
||||
// amount, an Issue, a path step) have their keys sorted bytewise and hold
|
||||
// only scalars.
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
void
|
||||
quoted(std::string& o, std::string_view s)
|
||||
{
|
||||
o += '"';
|
||||
o += s;
|
||||
o += '"';
|
||||
}
|
||||
|
||||
std::uint64_t
|
||||
unsignedValue(Node const& v, SField const& f)
|
||||
{
|
||||
std::uint64_t n = 0;
|
||||
auto const& t = v.text;
|
||||
auto const r = std::from_chars(t.data(), t.data() + t.size(), n);
|
||||
if (t.front() == '-' || r.ec != std::errc{} || r.ptr != t.data() + t.size())
|
||||
fail("'" + f.fieldName + "' is out of range");
|
||||
return n;
|
||||
}
|
||||
|
||||
char const*
|
||||
kindName(Node::Kind k)
|
||||
{
|
||||
switch (k)
|
||||
{
|
||||
case Node::Kind::object:
|
||||
return "an object";
|
||||
case Node::Kind::array:
|
||||
return "an array";
|
||||
case Node::Kind::string:
|
||||
return "a string";
|
||||
case Node::Kind::number:
|
||||
return "a number";
|
||||
}
|
||||
return "?"; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
void
|
||||
require(Node const& v, Node::Kind k, std::string const& what)
|
||||
{
|
||||
if (v.kind != k)
|
||||
fail(what + " must be " + kindName(k) + ", not " + kindName(v.kind));
|
||||
}
|
||||
|
||||
void
|
||||
emitFields(Node const& n, std::string& o, bool root = false);
|
||||
|
||||
// A string or a number, where the ledger wants a string: numbers become the
|
||||
// quoted spelling of their exact digits.
|
||||
void
|
||||
emitScalar(Node const& v, std::string& o, std::string const& what)
|
||||
{
|
||||
if (v.kind != Node::Kind::string && v.kind != Node::Kind::number)
|
||||
fail(what + " must be a string or a number");
|
||||
quoted(o, v.text);
|
||||
}
|
||||
|
||||
// {"currency":..,"issuer":..,"value":..} and the like
|
||||
void
|
||||
emitPlain(Node const& n, std::string const& what, std::string& o)
|
||||
{
|
||||
require(n, Node::Kind::object, what);
|
||||
std::vector<std::size_t> idx(n.keys.size());
|
||||
for (std::size_t i = 0; i < idx.size(); ++i)
|
||||
idx[i] = i;
|
||||
std::sort(idx.begin(), idx.end(), [&n](auto a, auto b) {
|
||||
return n.keys[a] < n.keys[b];
|
||||
});
|
||||
o += '{';
|
||||
for (auto const i : idx)
|
||||
{
|
||||
if (o.back() != '{')
|
||||
o += ',';
|
||||
quoted(o, n.keys[i]);
|
||||
o += ':';
|
||||
emitScalar(n.items[i], o, what + "." + n.keys[i]);
|
||||
}
|
||||
o += '}';
|
||||
}
|
||||
|
||||
void
|
||||
emitField(SField const& f, Node const& v, std::string& o)
|
||||
{
|
||||
std::string const& name = f.fieldName;
|
||||
|
||||
switch (f.fieldType)
|
||||
{
|
||||
case STI_OBJECT:
|
||||
case STI_XCHAIN_BRIDGE: // renders its members under field names
|
||||
require(v, Node::Kind::object, "'" + name + "'");
|
||||
emitFields(v, o);
|
||||
return;
|
||||
|
||||
case STI_ARRAY:
|
||||
require(v, Node::Kind::array, "'" + name + "'");
|
||||
o += '[';
|
||||
for (auto const& e : v.items)
|
||||
{
|
||||
if (o.back() != '[')
|
||||
o += ',';
|
||||
// each element is {"InnerFieldName": {...}}
|
||||
require(e, Node::Kind::object, "an element of '" + name + "'");
|
||||
if (e.keys.size() != 1)
|
||||
fail(
|
||||
"an element of '" + name +
|
||||
"' must have exactly one member");
|
||||
emitFields(e, o);
|
||||
}
|
||||
o += ']';
|
||||
return;
|
||||
|
||||
case STI_PATHSET:
|
||||
require(v, Node::Kind::array, "'" + name + "'");
|
||||
o += '[';
|
||||
for (auto const& path : v.items)
|
||||
{
|
||||
if (o.back() != '[')
|
||||
o += ',';
|
||||
require(path, Node::Kind::array, "a path in '" + name + "'");
|
||||
o += '[';
|
||||
for (auto const& step : path.items)
|
||||
{
|
||||
if (o.back() != '[')
|
||||
o += ',';
|
||||
emitPlain(step, "a path step in '" + name + "'", o);
|
||||
}
|
||||
o += ']';
|
||||
}
|
||||
o += ']';
|
||||
return;
|
||||
|
||||
case STI_VECTOR256:
|
||||
require(v, Node::Kind::array, "'" + name + "'");
|
||||
o += '[';
|
||||
for (auto const& e : v.items)
|
||||
{
|
||||
if (o.back() != '[')
|
||||
o += ',';
|
||||
require(e, Node::Kind::string, "an element of '" + name + "'");
|
||||
quoted(o, e.text);
|
||||
}
|
||||
o += ']';
|
||||
return;
|
||||
|
||||
case STI_AMOUNT:
|
||||
case STI_ISSUE:
|
||||
if (v.kind == Node::Kind::object)
|
||||
emitPlain(v, "'" + name + "'", o);
|
||||
else
|
||||
emitScalar(v, o, "'" + name + "'");
|
||||
return;
|
||||
|
||||
case STI_UINT8:
|
||||
case STI_UINT16:
|
||||
case STI_UINT32:
|
||||
if (v.kind == Node::Kind::string) // e.g. TransactionType
|
||||
{
|
||||
quoted(o, v.text);
|
||||
return;
|
||||
}
|
||||
require(v, Node::Kind::number, "'" + name + "'");
|
||||
{
|
||||
std::uint64_t const max = f.fieldType == STI_UINT8 ? 0xFFu
|
||||
: f.fieldType == STI_UINT16 ? 0xFFFFu
|
||||
: 0xFFFF'FFFFu;
|
||||
if (unsignedValue(v, f) > max)
|
||||
fail("'" + name + "' is out of range");
|
||||
}
|
||||
o += v.text; // already minimal: the parser refuses leading zeros
|
||||
return;
|
||||
|
||||
case STI_UINT64:
|
||||
if (f == sfTime)
|
||||
{
|
||||
// Spelled Date().toISOString() in the preimage and stored as
|
||||
// milliseconds. Re-emitting the round-tripped spelling rather
|
||||
// than the input is what makes this a fixed point.
|
||||
require(v, Node::Kind::string, "'" + name + "'");
|
||||
quoted(o, jsontx_iso_str(jsontx_iso(v.text)));
|
||||
return;
|
||||
}
|
||||
if (v.kind == Node::Kind::string)
|
||||
{
|
||||
quoted(o, v.text);
|
||||
return;
|
||||
}
|
||||
require(v, Node::Kind::number, "'" + name + "'");
|
||||
quoted(o, jsontx_u64_str(f, unsignedValue(v, f)));
|
||||
return;
|
||||
|
||||
default: // hashes, blobs, accounts, currencies, numbers
|
||||
emitScalar(v, o, "'" + name + "'");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
emitFields(Node const& n, std::string& o, bool root)
|
||||
{
|
||||
std::vector<std::pair<SField const*, Node const*>> ks;
|
||||
ks.reserve(n.keys.size() + 1);
|
||||
for (std::size_t i = 0; i < n.keys.size(); ++i)
|
||||
{
|
||||
auto const& f = jsontx_field(n.keys[i]);
|
||||
if (f == sfInvalid)
|
||||
fail("unknown field '" + n.keys[i] + "'");
|
||||
ks.emplace_back(&f, &n.items[i]);
|
||||
}
|
||||
|
||||
// An omitted Sequence is Sequence 0 when something else sequences the
|
||||
// transaction: a Time (time-sequenced) or a TicketSequence. Both need
|
||||
// Sequence 0, and neither can mean anything else by it. The canonical
|
||||
// form always carries the field, because the node's own rendering does:
|
||||
// getJson emits every required field. Written this way the two spellings
|
||||
// of such a preimage - with and without "Sequence": 0 - canonicalize to
|
||||
// the same bytes, and the delta simply skips the ones the signer left
|
||||
// out.
|
||||
//
|
||||
// Only a constant may be implied here, never anything read from the
|
||||
// ledger such as the account's next sequence: the transaction must be a
|
||||
// pure function of the preimage, or the binding check means nothing.
|
||||
//
|
||||
// With neither field, nothing is implied: an absent Sequence is left
|
||||
// absent, and the submit RPC refuses the document for want of one.
|
||||
if (root)
|
||||
{
|
||||
auto const has = [&ks](SField const& f) {
|
||||
return std::any_of(ks.begin(), ks.end(), [&f](auto const& k) {
|
||||
return *k.first == f;
|
||||
});
|
||||
};
|
||||
if (!has(sfSequence) && (has(sfTime) || has(sfTicketSequence)))
|
||||
{
|
||||
static Node const zero{Node::Kind::number, "0", {}, {}};
|
||||
ks.emplace_back(&sfSequence, &zero);
|
||||
}
|
||||
}
|
||||
|
||||
// field codes are unique, so this order is total
|
||||
std::sort(ks.begin(), ks.end(), [](auto const& a, auto const& b) {
|
||||
return a.first->fieldCode < b.first->fieldCode;
|
||||
});
|
||||
|
||||
o += '{';
|
||||
for (std::size_t j = 0; j < ks.size(); ++j)
|
||||
{
|
||||
auto const& [f, v] = ks[j];
|
||||
if (j && f == ks[j - 1].first) // e.g. "Fee" and "fee"
|
||||
fail("duplicate field '" + f->fieldName + "'");
|
||||
if (o.back() != '{')
|
||||
o += ',';
|
||||
quoted(o, f->fieldName);
|
||||
o += ':';
|
||||
emitField(*f, *v, o);
|
||||
}
|
||||
o += '}';
|
||||
}
|
||||
|
||||
std::string
|
||||
canonical(Node const& root)
|
||||
{
|
||||
std::string out;
|
||||
emitFields(root, out, true);
|
||||
// Usually far smaller than the input, but a bare number in a field the
|
||||
// ledger wants as a string gains two quote characters, so a document of
|
||||
// bare amounts can grow past the cap. unsanitize_jsontx refuses a
|
||||
// canonical form that large, so refuse it here too.
|
||||
if (out.size() > jsontx_max_text)
|
||||
fail("canonical form too large");
|
||||
return out;
|
||||
}
|
||||
|
||||
void
|
||||
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);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
std::string
|
||||
jsontx_signing_data(std::string_view preimage)
|
||||
{
|
||||
std::string r;
|
||||
r.reserve(jsontx_sign_prefix.size() + preimage.size());
|
||||
r += jsontx_sign_prefix;
|
||||
r += preimage;
|
||||
return r;
|
||||
}
|
||||
|
||||
// Not boost::algorithm::to_lower_copy or std::tolower: both consult the global
|
||||
// locale, under which a byte such as 0xC9 folds on one node and not another.
|
||||
std::string
|
||||
jsontx_lower(std::string_view s)
|
||||
{
|
||||
std::string r(s);
|
||||
for (auto& c : r)
|
||||
if (c >= 'A' && c <= 'Z')
|
||||
c = static_cast<char>(c - 'A' + 'a');
|
||||
return r;
|
||||
}
|
||||
|
||||
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())
|
||||
{
|
||||
// Two fields folding to one key would make the lookup depend
|
||||
// on iteration order. None do today; keep it that way.
|
||||
if (!m.emplace(jsontx_lower(f->fieldName), f).second)
|
||||
LogicError(
|
||||
"jsontx: field names collide case-insensitively: " +
|
||||
f->fieldName);
|
||||
}
|
||||
return m;
|
||||
}();
|
||||
|
||||
auto const i = tbl.find(jsontx_lower(name));
|
||||
return i == tbl.end() ? sfInvalid : *i->second;
|
||||
}
|
||||
|
||||
std::uint64_t
|
||||
jsontx_iso(std::string_view s)
|
||||
{
|
||||
static constexpr char pat[] = "0000-00-00T00:00:00.000Z";
|
||||
if (s.size() != 24)
|
||||
fail("Time must be an ISO 8601 instant");
|
||||
for (std::size_t i = 0; i < 24; ++i)
|
||||
if (pat[i] == '0' ? !digit(s[i]) : s[i] != pat[i])
|
||||
fail("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)
|
||||
fail("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 refused: JS cannot emit a leap second and the ledger cannot
|
||||
// represent one
|
||||
if (d < 1 || d > dim || h > 23 || mi > 59 || se > 59)
|
||||
fail("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)
|
||||
fail("Time precedes the ripple epoch");
|
||||
return static_cast<std::uint64_t>(t) * 1000 + ms;
|
||||
}
|
||||
|
||||
std::string
|
||||
jsontx_iso_str(std::uint64_t ms)
|
||||
{
|
||||
if (ms > jsontx_max_time)
|
||||
fail("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 bounds y to [2000, 9999], so every field fits its
|
||||
// width. Hand-rolled: this spelling is part of the signed 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));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
void
|
||||
jsontx_strict(std::string_view raw)
|
||||
{
|
||||
(void)parse(raw);
|
||||
}
|
||||
|
||||
std::string
|
||||
jsontx_canonical(std::string_view raw)
|
||||
{
|
||||
return canonical(parse(raw));
|
||||
}
|
||||
|
||||
std::pair<std::string, std::string>
|
||||
sanitize_jsontx(std::string_view raw)
|
||||
{
|
||||
std::string out = canonical(parse(raw));
|
||||
|
||||
// Greedy copy/insert delta over `raw`, sourcing from `out`.
|
||||
// op 0x00 <varint len> <bytes> literal
|
||||
// op 0x01 <varint off> <varint len> copy from canonical
|
||||
//
|
||||
// This encoder is normative - unsanitize_jsontx only accepts its exact
|
||||
// output - so it must emit identical bytes on every node and stdlib.
|
||||
// Candidates for a 4-gram are tried in ascending offset order and ties
|
||||
// keep the lowest offset. The index is a sorted vector of
|
||||
// (gram << 32 | offset): offsets are unique, so the order is total and
|
||||
// std::sort is deterministic.
|
||||
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 flush = [&] {
|
||||
if (lit.empty())
|
||||
return;
|
||||
diff += char(0);
|
||||
varint(diff, lit.size());
|
||||
diff += lit;
|
||||
lit.clear();
|
||||
++ops;
|
||||
};
|
||||
|
||||
std::vector<std::uint64_t> idx;
|
||||
if (out.size() >= jsontx_min_copy)
|
||||
{
|
||||
idx.reserve(out.size() - jsontx_min_copy + 1);
|
||||
for (std::size_t i = 0; i + jsontx_min_copy <= out.size(); ++i)
|
||||
idx.push_back(std::uint64_t(gram(out, i)) << 32 | i);
|
||||
std::sort(idx.begin(), idx.end());
|
||||
}
|
||||
|
||||
for (std::size_t i = 0; i < raw.size();)
|
||||
{
|
||||
std::size_t bo = 0, bl = 0;
|
||||
if (i + jsontx_min_copy <= raw.size())
|
||||
{
|
||||
std::uint64_t const g = gram(raw, i);
|
||||
auto it = std::lower_bound(idx.begin(), idx.end(), g << 32);
|
||||
for (std::size_t tried = 0;
|
||||
it != idx.end() && (*it >> 32) == g && tried < jsontx_max_cand;
|
||||
++it, ++tried)
|
||||
{
|
||||
std::size_t const off = *it & 0xFFFF'FFFFu;
|
||||
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 bounds unsanitize_jsontx applies, applied here so that a document
|
||||
// this accepts is never one the verifier then refuses.
|
||||
if (diff.size() > jsontx_max_diff || ops > jsontx_max_ops)
|
||||
fail("formatting differs too much from canonical form");
|
||||
|
||||
return {std::move(out), std::move(diff)};
|
||||
}
|
||||
|
||||
// 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 refused.
|
||||
std::string
|
||||
unsanitize_jsontx(std::string_view sanitized, std::string_view diff)
|
||||
{
|
||||
if (sanitized.size() > jsontx_max_text || diff.size() > jsontx_max_diff)
|
||||
fail("oversize delta input");
|
||||
|
||||
std::string out;
|
||||
std::size_t p = 0, ops = 0, prevEnd = 0;
|
||||
int prev = -1;
|
||||
|
||||
auto const read = [&](std::uint64_t max) -> std::uint64_t {
|
||||
std::uint64_t v = 0;
|
||||
for (int s = 0; s <= 21; s += 7) // four bytes at most
|
||||
{
|
||||
if (p >= diff.size())
|
||||
fail("truncated delta");
|
||||
std::uint8_t const c = diff[p++];
|
||||
v |= std::uint64_t(c & 0x7F) << s;
|
||||
if (c & 0x80)
|
||||
continue;
|
||||
if (s && !(c & 0x7F))
|
||||
fail("non-minimal varint");
|
||||
if (v > max)
|
||||
fail("delta value out of range");
|
||||
return v;
|
||||
}
|
||||
fail("overlong varint");
|
||||
};
|
||||
|
||||
while (p < diff.size())
|
||||
{
|
||||
if (++ops > jsontx_max_ops)
|
||||
fail("too many delta ops");
|
||||
|
||||
std::uint8_t const op = diff[p++];
|
||||
if (op > 1)
|
||||
fail("unknown delta op");
|
||||
|
||||
if (op == 0) // literal
|
||||
{
|
||||
if (prev == 0)
|
||||
fail("unmerged literal run");
|
||||
auto const n = read(jsontx_max_text);
|
||||
if (n == 0 || n > diff.size() - p)
|
||||
fail("bad literal length");
|
||||
if (out.size() + n > jsontx_max_text)
|
||||
fail("delta expands too far");
|
||||
out += diff.substr(p, n);
|
||||
p += n;
|
||||
}
|
||||
else // copy from the canonical form
|
||||
{
|
||||
auto const off = read(sanitized.size());
|
||||
auto const n = read(sanitized.size() - off);
|
||||
if (n < jsontx_min_copy)
|
||||
fail("undersize copy");
|
||||
if (prev == 1 && off == prevEnd)
|
||||
fail("unmerged copy run");
|
||||
if (out.size() + n > jsontx_max_text)
|
||||
fail("delta expands too far");
|
||||
out += sanitized.substr(off, n);
|
||||
prevEnd = off + n;
|
||||
}
|
||||
prev = op;
|
||||
}
|
||||
|
||||
if (out.empty())
|
||||
fail("empty delta");
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string
|
||||
jsontx_verify(STTx const& stx, std::string_view diff)
|
||||
{
|
||||
// The cheap refusals first: this runs on every relay of a transaction
|
||||
// carrying a delta, before any signature is known to be good.
|
||||
if (!stx.isFieldPresent(sfTxnSignature) || stx.isFieldPresent(sfSigners))
|
||||
fail("expects a lone TxnSignature");
|
||||
|
||||
auto const pkb = stx.getSigningPubKey();
|
||||
if (publicKeyType(makeSlice(pkb)) != KeyType::ed25519)
|
||||
fail("SigningPubKey must be ed25519");
|
||||
|
||||
if (diff.size() > jsontx_max_diff)
|
||||
fail("oversize delta");
|
||||
|
||||
// No transaction serializes to more than twice its canonical text
|
||||
// (JSONTxSignatures_test checks every field of every format; the worst
|
||||
// is a bare three-letter currency code, about 1.3x). Allowing twice that
|
||||
// again, plus the delta and signature this counts but the canonical form
|
||||
// does not, loses nothing that could verify, and spares getJson on an
|
||||
// arbitrarily large object.
|
||||
if (stx.getSerializer().size() > 4 * jsontx_max_text + jsontx_max_diff)
|
||||
fail("transaction too large");
|
||||
|
||||
// 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 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 milliseconds on the wire and an ISO 8601 instant in the
|
||||
// preimage: a bijection over the representable range, so the delta
|
||||
// carries nothing for the field.
|
||||
if (stx.isFieldPresent(sfTime))
|
||||
txj[sfTime.fieldName] = jsontx_iso_str(stx.getFieldU64(sfTime));
|
||||
|
||||
// canonical form, derived only from data the node already holds
|
||||
auto const san = jsontx_canonical(Json::FastWriter{}.write(txj));
|
||||
|
||||
// reconstruct the signed preimage under the caps above
|
||||
auto const raw = unsanitize_jsontx(san, diff);
|
||||
|
||||
// The signature before the binding check purely for cost: the binding
|
||||
// check re-canonicalizes and re-encodes, and without the key an attacker
|
||||
// cannot get past this line. Both must pass.
|
||||
if (!verify(
|
||||
PublicKey(makeSlice(pkb)),
|
||||
makeSlice(jsontx_signing_data(raw)),
|
||||
makeSlice(stx.getFieldVL(sfTxnSignature))))
|
||||
fail("signature does not verify");
|
||||
|
||||
// Bind the preimage to the transaction. This is the load-bearing check:
|
||||
// a delta of pure literals can reconstruct ANY text, so without it every
|
||||
// JsonTx signature the key ever produced would authorise this
|
||||
// transaction. Comparing the delta too makes it 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)
|
||||
fail("preimage does not match transaction");
|
||||
|
||||
return raw;
|
||||
}
|
||||
|
||||
std::string
|
||||
jsontx_verify(STTx const& stx)
|
||||
{
|
||||
if (!stx.isFieldPresent(sfJsonTxDelta))
|
||||
fail("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
|
||||
@@ -198,6 +198,13 @@ STTx::getSeqProxy() const
|
||||
return SeqProxy{SeqProxy::ticket, *ticketSeq};
|
||||
}
|
||||
|
||||
bool
|
||||
STTx::isTimeSequenced() const
|
||||
{
|
||||
return isFieldPresent(sfTime) && getFieldU32(sfSequence) == 0 &&
|
||||
!isFieldPresent(sfTicketSequence);
|
||||
}
|
||||
|
||||
void
|
||||
STTx::sign(PublicKey const& publicKey, SecretKey const& secretKey)
|
||||
{
|
||||
@@ -214,6 +221,15 @@ STTx::checkSign(
|
||||
RequireFullyCanonicalSig requireCanonicalSig,
|
||||
Rules const& rules) const
|
||||
{
|
||||
// sfJsonTxDelta is a non-signing field, so the binary signing hash does
|
||||
// not cover it: a binary signature stays valid with any delta appended.
|
||||
// Were that accepted, anyone relaying a binary-signed transaction could
|
||||
// mint as many new transaction ids for it as there are deltas. A delta
|
||||
// means the signature is over the JSON preimage, which jsontx_verify
|
||||
// checks; a binary signature never authorises one.
|
||||
if (isFieldPresent(sfJsonTxDelta))
|
||||
return Unexpected("Binary signature cannot authorise a JsonTx.");
|
||||
|
||||
try
|
||||
{
|
||||
// Determine whether we're single- or multi-signing by looking
|
||||
|
||||
@@ -49,6 +49,8 @@ TxFormats::TxFormats()
|
||||
{sfNetworkID, soeOPTIONAL},
|
||||
{sfHookParameters, soeOPTIONAL},
|
||||
{sfHookName, soeOPTIONAL},
|
||||
{sfTime, soeOPTIONAL},
|
||||
{sfJsonTxDelta, soeOPTIONAL},
|
||||
};
|
||||
|
||||
#pragma push_macro("UNWRAP")
|
||||
|
||||
1514
src/test/app/JsonTx_test.cpp
Normal file
1514
src/test/app/JsonTx_test.cpp
Normal file
File diff suppressed because it is too large
Load Diff
1510
src/test/protocol/JSONTxSignatures_test.cpp
Normal file
1510
src/test/protocol/JSONTxSignatures_test.cpp
Normal file
File diff suppressed because it is too large
Load Diff
@@ -161,6 +161,18 @@ public:
|
||||
if (!sleAcct)
|
||||
return false;
|
||||
|
||||
// A time-sequenced transaction's SeqProxy is sequence(0), which
|
||||
// the sequence test below would call past and sweep at once. It
|
||||
// is spent once the account has recorded its Time or a later one
|
||||
// - applied, or superseded - and otherwise lives out holdLedgers
|
||||
// like anything else (preclaim refuses it once it expires).
|
||||
if (auto const time = txn.getTX()->at(~sfTime);
|
||||
time && txn.getTX()->isTimeSequenced())
|
||||
{
|
||||
auto const last = sleAcct->at(~sfLastTxnTime);
|
||||
return last && *last >= *time;
|
||||
}
|
||||
|
||||
SeqProxy const acctSeq =
|
||||
SeqProxy::sequence(sleAcct->getFieldU32(sfSequence));
|
||||
SeqProxy const seqProx = txn.getSeqProxy();
|
||||
|
||||
@@ -30,6 +30,11 @@ operator<(CanonicalTXSet::Key const& lhs, CanonicalTXSet::Key const& rhs)
|
||||
if (lhs.account_ > rhs.account_)
|
||||
return false;
|
||||
|
||||
// Empty for everything but a time-sequenced transaction, so this sorts
|
||||
// those after the rest of the account's and leaves the rest alone.
|
||||
if (lhs.time_ != rhs.time_)
|
||||
return lhs.time_ < rhs.time_;
|
||||
|
||||
if (lhs.seqProxy_ < rhs.seqProxy_)
|
||||
return true;
|
||||
|
||||
@@ -48,12 +53,28 @@ CanonicalTXSet::accountKey(AccountID const& account)
|
||||
return ret;
|
||||
}
|
||||
|
||||
CanonicalTXSet::Key
|
||||
CanonicalTXSet::makeKey(
|
||||
uint256 const& account,
|
||||
STTx const& tx,
|
||||
uint256 const& id)
|
||||
{
|
||||
return Key(
|
||||
account,
|
||||
tx.getSeqProxy(),
|
||||
tx.isTimeSequenced()
|
||||
? std::optional<std::uint64_t>(tx.getFieldU64(sfTime))
|
||||
: std::nullopt,
|
||||
id);
|
||||
}
|
||||
|
||||
void
|
||||
CanonicalTXSet::insert(std::shared_ptr<STTx const> const& txn)
|
||||
{
|
||||
map_.insert(std::make_pair(
|
||||
Key(accountKey(txn->getAccountID(sfAccount)),
|
||||
txn->getSeqProxy(),
|
||||
makeKey(
|
||||
accountKey(txn->getAccountID(sfAccount)),
|
||||
*txn,
|
||||
txn->getTransactionID()),
|
||||
txn));
|
||||
}
|
||||
@@ -71,10 +92,13 @@ CanonicalTXSet::popAcctTransaction(std::shared_ptr<STTx const> const& tx)
|
||||
//
|
||||
// 3. After handling all transactions with Sequences, return Tickets
|
||||
// with the lowest Ticket ID first.
|
||||
//
|
||||
// 4. After those, return time-sequenced transactions with the oldest
|
||||
// Time first.
|
||||
std::shared_ptr<STTx const> result;
|
||||
uint256 const effectiveAccount{accountKey(tx->getAccountID(sfAccount))};
|
||||
|
||||
Key const after(effectiveAccount, tx->getSeqProxy(), beast::zero);
|
||||
Key const after = makeKey(effectiveAccount, *tx, beast::zero);
|
||||
auto const itrNext{map_.lower_bound(after)};
|
||||
if (itrNext != map_.end() &&
|
||||
itrNext->first.getAccount() == effectiveAccount)
|
||||
|
||||
@@ -25,13 +25,31 @@
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/SeqProxy.h>
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
/** Holds transactions which were deferred to the next pass of consensus.
|
||||
|
||||
"Canonical" refers to the order in which transactions are applied.
|
||||
|
||||
- Puts transactions from the same account in SeqProxy order
|
||||
- Puts transactions from the same account in SeqProxy order: Sequences,
|
||||
then Tickets
|
||||
- Then the account's time-sequenced transactions (featureJsonTx), oldest
|
||||
Time first
|
||||
|
||||
Without a time-sequenced transaction in the set this is the SeqProxy
|
||||
order it always was. A Time on a transaction with a Sequence or a Ticket
|
||||
changes nothing: only time-sequenced transactions check or record
|
||||
sfLastTxnTime, so oldest first is all their order has to satisfy.
|
||||
|
||||
They go after the Tickets for the reason Tickets go after Sequences. A
|
||||
time-sequenced transaction does not say which Sequence it followed, but
|
||||
what it does can depend on it - a TicketCreate numbers its Tickets from
|
||||
the account Sequence. A transaction with a Sequence or a Ticket that
|
||||
really came after it fails terPRE_SEQ or terPRE_TICKET here and is
|
||||
retried; one that came before it, applied second, could have its
|
||||
Sequence or Ticket taken and fail for good.
|
||||
|
||||
*/
|
||||
// VFALCO TODO rename to SortedTxSet
|
||||
@@ -41,8 +59,11 @@ private:
|
||||
class Key
|
||||
{
|
||||
public:
|
||||
Key(uint256 const& account, SeqProxy seqProx, uint256 const& id)
|
||||
: account_(account), txId_(id), seqProxy_(seqProx)
|
||||
Key(uint256 const& account,
|
||||
SeqProxy seqProx,
|
||||
std::optional<std::uint64_t> time,
|
||||
uint256 const& id)
|
||||
: account_(account), txId_(id), seqProxy_(seqProx), time_(time)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -95,6 +116,11 @@ private:
|
||||
uint256 account_;
|
||||
uint256 txId_;
|
||||
SeqProxy seqProxy_;
|
||||
// sfTime of a time-sequenced transaction, and nothing for any other.
|
||||
// Compared before seqProxy_: an empty optional is less than every
|
||||
// Time, so time-sequenced transactions come after the rest of their
|
||||
// account's, and among themselves oldest first.
|
||||
std::optional<std::uint64_t> time_;
|
||||
};
|
||||
|
||||
friend bool
|
||||
@@ -104,6 +130,10 @@ private:
|
||||
uint256
|
||||
accountKey(AccountID const& account);
|
||||
|
||||
// The Key for tx under the salted account key, id breaking ties
|
||||
static Key
|
||||
makeKey(uint256 const& account, STTx const& tx, uint256 const& id);
|
||||
|
||||
public:
|
||||
using const_iterator =
|
||||
std::map<Key, std::shared_ptr<STTx const>>::const_iterator;
|
||||
|
||||
@@ -788,6 +788,13 @@ TxQ::apply(
|
||||
return {terNO_ACCOUNT, false};
|
||||
}
|
||||
|
||||
// The queue is keyed by SeqProxy, and every time-sequenced transaction
|
||||
// has SeqProxy sequence(0): two of them would be taken for replacements
|
||||
// of one another. tryDirectApply already applied it if its fee was high
|
||||
// enough, so the only thing left is to say it cannot wait.
|
||||
if (view.rules().enabled(featureJsonTx) && tx->isTimeSequenced())
|
||||
return {telCAN_NOT_QUEUE, false};
|
||||
|
||||
// If the transaction needs a Ticket is that Ticket in the ledger?
|
||||
SeqProxy const acctSeqProx = SeqProxy::sequence((*sleAccount)[sfSequence]);
|
||||
SeqProxy const txSeqProx = tx->getSeqProxy();
|
||||
@@ -1967,7 +1974,14 @@ TxQ::tryDirectApply(
|
||||
|
||||
std::optional<SeqProxy> txSeqProx;
|
||||
|
||||
if (!bypassQueue)
|
||||
// A time-sequenced transaction has no sequence to match, and is never in
|
||||
// the queue (TxQ::apply refuses to queue one), so there is no queued
|
||||
// entry to replace either. Unlike a manifest it still has to pay the
|
||||
// open ledger fee: it can be spammed like any signed transaction.
|
||||
bool const timeSequenced =
|
||||
view.rules().enabled(featureJsonTx) && tx->isTimeSequenced();
|
||||
|
||||
if (!bypassQueue && !timeSequenced)
|
||||
{
|
||||
SeqProxy const acctSeqProx =
|
||||
SeqProxy::sequence((*sleAccount)[sfSequence]);
|
||||
|
||||
@@ -67,6 +67,16 @@ Change::preflight(PreflightContext const& ctx)
|
||||
return temBAD_SIGNATURE;
|
||||
}
|
||||
|
||||
// Pseudo-transactions skip preflight1, where every other transaction
|
||||
// type has these gated on featureJsonTx. They carry no signature for a
|
||||
// delta to reconstruct, and a node on an older build cannot parse either
|
||||
// field, so a validator proposing one would split the network.
|
||||
if (ctx.tx.isFieldPresent(sfTime) || ctx.tx.isFieldPresent(sfJsonTxDelta))
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Change: JsonTx fields on a pseudo-transaction";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
if (ctx.tx.getFieldU32(sfSequence) != 0 ||
|
||||
ctx.tx.isFieldPresent(sfPreviousTxnID))
|
||||
{
|
||||
|
||||
@@ -62,6 +62,14 @@ Cron::preflight(PreflightContext const& ctx)
|
||||
return temBAD_SIGNATURE;
|
||||
}
|
||||
|
||||
// Cron skips preflight1, where every other transaction type has these
|
||||
// gated on featureJsonTx; same reasoning as Change::preflight.
|
||||
if (ctx.tx.isFieldPresent(sfTime) || ctx.tx.isFieldPresent(sfJsonTxDelta))
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Cron: JsonTx fields on a pseudo-transaction";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
if (ctx.tx.getFieldU32(sfSequence) != 0 ||
|
||||
ctx.tx.isFieldPresent(sfPreviousTxnID))
|
||||
{
|
||||
|
||||
@@ -57,6 +57,15 @@ DeleteAccount::preflight(PreflightContext const& ctx)
|
||||
// An account cannot be deleted and give itself the resulting XRP.
|
||||
return temDST_IS_SRC;
|
||||
|
||||
// Deleting an account also deletes its sfLastTxnTime, which is the only
|
||||
// thing standing between a time-sequenced transaction and a replay. For
|
||||
// earlier transactions preclaim waits out their window; this one is
|
||||
// necessarily still inside its own, so once the account is re-created it
|
||||
// could be applied again. A Sequence or Ticket is not lost that way: a
|
||||
// re-created account starts its Sequence at the current ledger index.
|
||||
if (ctx.tx.isTimeSequenced())
|
||||
return temBAD_SEQUENCE;
|
||||
|
||||
if (auto const err = credentials::checkFields(ctx); !isTesSuccess(err))
|
||||
return err;
|
||||
|
||||
@@ -321,6 +330,24 @@ DeleteAccount::preclaim(PreclaimContext const& ctx)
|
||||
if ((*sleAccount)[sfSequence] + seqDelta > ctx.view.seq())
|
||||
return tecTOO_SOON;
|
||||
|
||||
// The same protection for sfTime. A re-created account has no
|
||||
// sfLastTxnTime, so any time-sequenced transaction whose Time is still
|
||||
// inside the validity window could be applied to it a second time. Every
|
||||
// one this account has applied had a Time at most sfLastTxnTime, so once
|
||||
// that is older than txTimeMaxAgeMs none of them can ever apply again.
|
||||
// (A Time on a transaction sequenced by a Sequence or a Ticket is never
|
||||
// recorded and needs no such care: the Sequence restarts and the Tickets
|
||||
// are gone.)
|
||||
if (auto const last = (*sleAccount)[~sfLastTxnTime])
|
||||
{
|
||||
std::uint64_t const close =
|
||||
static_cast<std::uint64_t>(
|
||||
ctx.view.parentCloseTime().time_since_epoch().count()) *
|
||||
1000;
|
||||
if (close <= *last || close - *last <= txTimeMaxAgeMs)
|
||||
return tecTOO_SOON;
|
||||
}
|
||||
|
||||
// do not allow the account to be removed if there are hooks installed or
|
||||
// one or more hook states when these fields are completely empty the field
|
||||
// is made absent so this test is sufficient these fields cannot be
|
||||
|
||||
@@ -262,9 +262,12 @@ NFTokenMint::doApply()
|
||||
{
|
||||
std::uint32_t const acctSeq = root->at(sfSequence);
|
||||
|
||||
// A time-sequenced transaction, like a Ticket, leaves the
|
||||
// Sequence untouched.
|
||||
root->at(sfFirstNFTokenSequence) =
|
||||
ctx_.tx.isFieldPresent(sfIssuer) ||
|
||||
ctx_.tx.getSeqProxy().isTicket()
|
||||
ctx_.tx.getSeqProxy().isTicket() ||
|
||||
ctx_.tx.isTimeSequenced()
|
||||
? acctSeq
|
||||
: acctSeq - 1;
|
||||
}
|
||||
|
||||
@@ -125,14 +125,22 @@ PermissionedDomainSet::doApply()
|
||||
if (balance < reserve)
|
||||
return tecINSUFFICIENT_RESERVE;
|
||||
|
||||
Keylet const pdKeylet = keylet::permissionedDomain(
|
||||
account_, ctx_.tx.getFieldU32(sfSequence));
|
||||
// The raw Sequence is 0 for a transaction that uses a Ticket, so
|
||||
// before fix20260929 every ticketed domain an account created was
|
||||
// keyed (account, 0): the second collided with the first, and an
|
||||
// insert over an existing key is a LogicError when the ledger is
|
||||
// built. The SeqProxy value is the Ticket number for those, which is
|
||||
// what every other sequence-keyed object already uses.
|
||||
std::uint32_t const seq = ctx_.view().rules().enabled(fix20260929)
|
||||
? ctx_.tx.getSeqProxy().value()
|
||||
: ctx_.tx.getFieldU32(sfSequence);
|
||||
Keylet const pdKeylet = keylet::permissionedDomain(account_, seq);
|
||||
auto slePd = std::make_shared<SLE>(pdKeylet);
|
||||
if (!slePd)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
slePd->setAccountID(sfOwner, account_);
|
||||
slePd->setFieldU32(sfSequence, ctx_.tx.getFieldU32(sfSequence));
|
||||
slePd->setFieldU32(sfSequence, seq);
|
||||
slePd->peekFieldArray(sfAcceptedCredentials) = std::move(sortedLE);
|
||||
auto const page = view().dirInsert(
|
||||
keylet::ownerDir(account_), pdKeylet, describeOwnerDir(account_));
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
#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>
|
||||
@@ -102,6 +103,64 @@ 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;
|
||||
|
||||
// Emitted transactions carry their own replay protection
|
||||
// (sfEmitDetails) and never a signature, so neither field has a
|
||||
// meaning on one.
|
||||
if (ctx.tx.isFieldPresent(sfEmitDetails))
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
if (auto const time = ctx.tx[~sfTime])
|
||||
{
|
||||
// Past this sfTime has no preimage spelling, and nothing in the
|
||||
// validity window is anywhere near it; refusing it here keeps an
|
||||
// absurd Time from being held and retried as terPRE_SEQ.
|
||||
if (*time > jsontx_max_time)
|
||||
return temMALFORMED;
|
||||
|
||||
// These already pin Sequence to 0 for reasons of their own - a first
|
||||
// Import creates its account, a manifest is derived from the
|
||||
// manifest alone - so an sfTime would silently reinterpret them as
|
||||
// time-sequenced.
|
||||
if (ctx.tx.getTxnType() == ttIMPORT ||
|
||||
ctx.tx.getTxnType() == ttMANIFEST_SET)
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
// MPTokenIssuanceCreate and PermissionedDomainSet key the object they
|
||||
// create by the raw sequence value, (sequence, account). Every other
|
||||
// creator goes through seqID(), which falls back to the transaction id
|
||||
// when the SeqProxy is sequence(0) - an emitted or a time-sequenced
|
||||
// transaction - but these two ids are fixed formats with no room for one.
|
||||
// Two such transactions from one account would name the same object,
|
||||
// and inserting over an existing key is a LogicError on every node that
|
||||
// builds the ledger. So they need a real Sequence or a Ticket.
|
||||
//
|
||||
// Time-sequenced transactions are new with featureJsonTx and refused
|
||||
// unconditionally. Refusing the emitted case changes existing behaviour,
|
||||
// so it waits for fix20260929. (Neither feature is supported yet, so
|
||||
// neither case is reachable on a live network today.)
|
||||
if ((ctx.tx.getTxnType() == ttMPTOKEN_ISSUANCE_CREATE ||
|
||||
ctx.tx.getTxnType() == ttPERMISSIONED_DOMAIN_SET) &&
|
||||
ctx.tx.getSeqProxy() == SeqProxy::sequence(0) &&
|
||||
(ctx.tx.isTimeSequenced() || ctx.rules.enabled(fix20260929)))
|
||||
return temBAD_SEQUENCE;
|
||||
|
||||
auto const ret = preflight0(ctx);
|
||||
if (!isTesSuccess(ret))
|
||||
return ret;
|
||||
@@ -619,6 +678,12 @@ Transactor::checkSeqProxy(
|
||||
if (tx.isFieldPresent(sfFirstLedgerSequence))
|
||||
return tefINTERNAL;
|
||||
|
||||
// Replay protection comes from sfTime against sfLastTxnTime, which
|
||||
// checkPriorTxAndLastLedger enforces; the account Sequence is neither
|
||||
// checked here nor consumed.
|
||||
if (view.rules().enabled(featureJsonTx) && tx.isTimeSequenced())
|
||||
return tesSUCCESS;
|
||||
|
||||
if (t_seqProx.isSeq())
|
||||
{
|
||||
if (tx.isFieldPresent(sfTicketSequence) &&
|
||||
@@ -701,6 +766,46 @@ Transactor::checkPriorTxAndLastLedger(PreclaimContext const& ctx)
|
||||
(ctx.view.seq() > ctx.tx.getFieldU32(sfLastLedgerSequence)))
|
||||
return tefMAX_LEDGER;
|
||||
|
||||
// sfTime stands in for LastLedgerSequence on every transaction carrying
|
||||
// it and, on a time-sequenced one, for the account Sequence too.
|
||||
if (auto const time = ctx.tx[~sfTime])
|
||||
{
|
||||
// milliseconds since the ripple epoch, like sfTime
|
||||
std::uint64_t const close =
|
||||
static_cast<std::uint64_t>(
|
||||
ctx.view.parentCloseTime().time_since_epoch().count()) *
|
||||
1000;
|
||||
|
||||
// Expired: from here on this transaction can never apply, exactly as
|
||||
// for a LastLedgerSequence in the past. Written without adding to
|
||||
// *time, which arrives off the wire and could overflow.
|
||||
if (close > *time && close - *time > txTimeMaxAgeMs)
|
||||
return tefMAX_LEDGER;
|
||||
|
||||
// Not valid yet. Retriable, as for a future sequence: a client clock
|
||||
// a little ahead of the network's is the common case.
|
||||
if (*time > close && *time - close > txTimeMaxFutureMs)
|
||||
return terPRE_SEQ;
|
||||
|
||||
// Replay, for a time-sequenced transaction only. Strictly greater, so
|
||||
// no two of them share a Time, and the Time of every one ever applied
|
||||
// is at most sfLastTxnTime. With the window above, each is applied at
|
||||
// most once.
|
||||
//
|
||||
// A Sequence or a Ticket is replay protection already, so a
|
||||
// transaction sequenced by one is neither checked here nor recorded
|
||||
// in consumeSeqProxy: its Time is only a validity window. Were it
|
||||
// checked, it would have to apply in Time order among the account's
|
||||
// time-sequenced transactions as well as in Sequence order, and no
|
||||
// CanonicalTXSet order satisfies both for every set the open ledger
|
||||
// can accept, so consensus would drop transactions the open ledger
|
||||
// took.
|
||||
if (ctx.tx.isTimeSequenced() && sle &&
|
||||
sle->isFieldPresent(sfLastTxnTime) &&
|
||||
*time <= sle->getFieldU64(sfLastTxnTime))
|
||||
return tefPAST_SEQ;
|
||||
}
|
||||
|
||||
if (ctx.view.txExists(ctx.tx.getTransactionID()))
|
||||
return tefALREADY;
|
||||
|
||||
@@ -764,6 +869,18 @@ Transactor::consumeSeqProxy(SLE::pointer const& sleAccount)
|
||||
ctx_.tx.getTxnType() == ttMANIFEST_SET)
|
||||
return tesSUCCESS;
|
||||
|
||||
// A time-sequenced transaction records its Time, which is what
|
||||
// checkPriorTxAndLastLedger's replay check reads; nothing else does (see
|
||||
// there). This runs in apply() and again in reset(), so a tec result is
|
||||
// covered too. It has Sequence 0 and no Ticket, so falling through would
|
||||
// write Sequence = 0 + 1 and make every sequence this account ever used
|
||||
// replayable.
|
||||
if (ctx_.tx.isTimeSequenced())
|
||||
{
|
||||
sleAccount->setFieldU64(sfLastTxnTime, ctx_.tx.getFieldU64(sfTime));
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
SeqProxy const seqProx = ctx_.tx.getSeqProxy();
|
||||
if (seqProx.isSeq())
|
||||
{
|
||||
|
||||
@@ -321,6 +321,12 @@ seqID(C const& ctx_)
|
||||
ctx_.tx.isFieldPresent(sfEmitDetails))
|
||||
return ctx_.tx.getTransactionID();
|
||||
|
||||
// Every time-sequenced transaction has Sequence 0, so its SeqProxy
|
||||
// cannot tell one object from the next. Its transaction id can: sfTime
|
||||
// is strictly increasing per account, so no two ever share an id.
|
||||
if (ctx_.view().rules().enabled(featureJsonTx) && ctx_.tx.isTimeSequenced())
|
||||
return ctx_.tx.getTransactionID();
|
||||
|
||||
return ctx_.tx.getSeqProxy().value();
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include <xrpld/app/tx/detail/SetManifest.h>
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/JSONTxSignatures.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
@@ -50,6 +51,62 @@ checkValidity(
|
||||
auto const id = tx.getTransactionID();
|
||||
auto const flags = router.getFlags(id);
|
||||
|
||||
// A delta decides which signature check applies, so it is examined before
|
||||
// anything else - including the emitted and manifest branches below, which
|
||||
// would otherwise accept a delta as inert ballast and give anyone who
|
||||
// relays such a transaction a free way to mint new transaction ids for it.
|
||||
if (tx.isFieldPresent(sfJsonTxDelta))
|
||||
{
|
||||
// Answered without touching the router. Every cached flag must be a
|
||||
// function of the transaction alone, because nodes that disagree on
|
||||
// one disagree on whether a transaction in the agreed set applies.
|
||||
// Were this cached, a JsonTx relayed across the activation boundary -
|
||||
// PeerImp checks against the validated ledger's rules, consensus
|
||||
// against the open ledger's - would be SIGBAD on some nodes and
|
||||
// SIGGOOD on others.
|
||||
if (!rules.enabled(featureJsonTx))
|
||||
return {Validity::SigBad, "JsonTx is not enabled."};
|
||||
|
||||
// A JsonTx signs a plaintext preimage, so STTx::checkSign -- a
|
||||
// check against the binary signing hash -- can never succeed for it,
|
||||
// and STTx::checkSign refuses any transaction carrying a delta. 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. That is safe
|
||||
// only because nothing but this branch (and forceValidity, from
|
||||
// trusted sources) ever sets SIGGOOD or SIGBAD on an id carrying a
|
||||
// delta: the binary path below is unreachable for one.
|
||||
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 (rules.enabled(featureHooks) && tx.isFieldPresent(sfEmitDetails))
|
||||
{
|
||||
// emitted transactions do not contain signatures
|
||||
|
||||
@@ -262,7 +262,8 @@ TxConsequences::TxConsequences(STTx const& tx)
|
||||
: beast::zero)
|
||||
, potentialSpend_(beast::zero)
|
||||
, seqProx_(tx.getSeqProxy())
|
||||
, sequencesConsumed_(tx.getSeqProxy().isSeq() ? 1 : 0)
|
||||
, sequencesConsumed_(
|
||||
tx.getSeqProxy().isSeq() && !tx.isTimeSequenced() ? 1 : 0)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -30,9 +30,15 @@
|
||||
#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 {
|
||||
@@ -88,9 +94,15 @@ doInject(RPC::JsonContext& context)
|
||||
}
|
||||
|
||||
// {
|
||||
// tx_blob: <string> XOR tx_json: <object>,
|
||||
// secret: <secret>
|
||||
// tx_blob: <string>
|
||||
// OR tx: <json text> together with sig: <hex>
|
||||
// OR manifest: <hex>
|
||||
// OR tx_json: <object> together with secret: <secret> (deprecated)
|
||||
// }
|
||||
//
|
||||
// For tx + sig (featureJsonTx), `tx` is the exact JSON text the signer read
|
||||
// and `sig` is their ed25519 signature over jsontx_signing_data(tx): the
|
||||
// four bytes jsontx_sign_prefix followed by that text.
|
||||
Json::Value
|
||||
doSubmit(RPC::JsonContext& context)
|
||||
{
|
||||
@@ -98,17 +110,43 @@ 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);
|
||||
|
||||
// Half a JsonTx would otherwise fall through to the legacy signing path
|
||||
// and fail there with an unrelated complaint about tx_json.
|
||||
if (context.params.isMember(jss::tx) != context.params.isMember(jss::sig))
|
||||
return RPC::make_param_error(
|
||||
"JsonTx submission needs both `tx` and `sig`");
|
||||
bool const hasTxBlob = context.params.isMember(jss::tx_blob);
|
||||
|
||||
if (hasManifest && hasTxBlob)
|
||||
{
|
||||
// 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(
|
||||
rpcINVALID_PARAMS,
|
||||
"Specify exactly one of either `tx_blob` or `manifest`");
|
||||
}
|
||||
else if (!hasTxBlob && !hasManifest)
|
||||
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)
|
||||
{
|
||||
// legacy signing code
|
||||
auto const failType = getFailHard(context);
|
||||
|
||||
if (context.role != Role::ADMIN && !context.app.config().canSign())
|
||||
@@ -132,30 +170,22 @@ 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);
|
||||
@@ -175,18 +205,103 @@ doSubmit(RPC::JsonContext& context)
|
||||
txBlob = *hex;
|
||||
}
|
||||
|
||||
auto ret = strUnHex(txBlob);
|
||||
std::optional<Blob> ret;
|
||||
|
||||
if (!ret || !ret->size())
|
||||
return rpcError(rpcINVALID_PARAMS);
|
||||
if (!isJsonTx)
|
||||
{
|
||||
ret = strUnHex(txBlob);
|
||||
|
||||
SerialIter sitTrans(makeSlice(*ret));
|
||||
if (!ret || ret->empty())
|
||||
return rpcError(rpcINVALID_PARAMS);
|
||||
}
|
||||
|
||||
std::shared_ptr<STTx const> stTx;
|
||||
|
||||
try
|
||||
{
|
||||
stTx = std::make_shared<STTx const>(std::ref(sitTrans));
|
||||
if (!isJsonTx)
|
||||
{
|
||||
SerialIter sitTrans(makeSlice(*ret));
|
||||
stTx = std::make_shared<STTx const>(std::ref(sitTrans));
|
||||
}
|
||||
else
|
||||
{
|
||||
// the preimage is the signer's exact bytes, so it arrives as a
|
||||
// string; an object would already have been re-serialized by
|
||||
// someone other than the signer
|
||||
if (!context.params[jss::tx].isString() ||
|
||||
!context.params[jss::sig].isString())
|
||||
throw std::runtime_error(
|
||||
"JsonTx: tx and sig must both be strings");
|
||||
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,
|
||||
// nor the delta, which is derived from it.
|
||||
for (auto const& n :
|
||||
{sfTxnSignature.fieldName,
|
||||
sfSigners.fieldName,
|
||||
sfJsonTxDelta.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");
|
||||
|
||||
// The canonical form already wrote an omitted Sequence as 0 if
|
||||
// the document has a Time or a TicketSequence, so an absent one
|
||||
// here means it has neither. Say so, rather than let the template
|
||||
// check report a missing field. And there is deliberately no
|
||||
// autofill: the account's next sequence is ledger state, and the
|
||||
// signer's text has to determine the transaction by itself.
|
||||
if (!jv.isMember(sfSequence.fieldName))
|
||||
throw std::runtime_error(
|
||||
"JsonTx: no Sequence: add a Time, a TicketSequence, or a "
|
||||
"Sequence");
|
||||
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user