Compare commits

..
Author SHA1 Message Date
Richard Holland 783deb3e80 Merge branch 'dev' into jsontx 2026-10-21 12:31:51 +10:00
Richard Holland 3ae33f77fa Merge branch 'dev' into jsontx 2026-10-20 13:00:24 +10:00
Richard Holland 8fd39009cc fix testcase race condition 2026-10-20 14:00:04 +11:00
Richard Holland 424526807e test fix 2026-10-16 17:13:22 +11:00
Richard Holland 0074cf9e95 Merge dev into jsontx; move sfJsonTxDelta to VL 95 (96 taken by sfAppLoader) 2026-10-15 10:38:24 +11:00
Richard Holland de7afa9895 Merge branch 'dev' into jsontx 2026-10-08 23:12:33 +10:00
Richard Holland 983c5dfe0c fix consensus ordering for sfTime 2026-10-09 00:12:04 +11:00
tequ 47b00e9a65 Merge branch 'dev' into jsontx 2026-10-07 15:48:17 +09:00
Richard Holland ee1d44f7d6 Merge branch 'dev' into jsontx 2026-10-04 10:52:56 +10:00
Richard Holland 5995cc2d00 Merge branch 'dev' into jsontx 2026-10-03 09:46:40 +10:00
Richard Holland e9cc9e6fcb edge cases for seq=0, update fix name 2026-10-01 08:28:38 +10:00
Richard Holland 350347c615 Merge branch 'dev' into jsontx 2026-10-01 05:43:23 +10:00
Richard Holland 7112b80d8f Merge branch 'dev' into jsontx 2026-09-30 14:20:36 +10:00
Richard Holland 3de9cc3ba7 Merge branch 'dev' into jsontx 2026-09-29 04:38:01 +10:00
Richard Holland e35ae4af20 seq=0 safety 2026-09-29 04:37:36 +10:00
Richard Holland d916f108da implied seq=0 when time present or ticketseq 2026-09-29 04:20:57 +10:00
Richard Holland a09c180a93 sfTime fixes, integer bounds, etc 2026-09-29 03:58:14 +10:00
Richard Holland 33939f0b02 improvements 2026-09-26 20:50:30 +10:00
Richard Holland 779a07804b Merge branch 'dev' into jsontx 2026-09-26 16:16:01 +10:00
Richard Holland 60caaea495 try again 2026-09-13 23:40:06 +10:00
RichardAHBot 4bbcc2d953 fix: regenerate hook/sfcodes.h on jsontx branch
Adds sfTime (UINT32/96), sfJsonTxDelta (VL/96), and sfAppLoader/sfAppLoaderID
which were added to sfields.macro but not propagated to the generated hook
header, causing verify-generated-headers CI failure.
2026-09-13 22:39:31 +10:00
RichardAHBot 72d793d912 style: clang-format-18 for jsontx files 2026-09-13 22:39:31 +10:00
RichardAHBot db25f9799c test: add 15 more error path tests for delta decoder and timestamp parser
Cover unsanitize_jsontx error paths: truncated delta, unmerged literal/run,
empty delta, undersize copy, unmerged copy run, bad literal length, overlong
varint, and delta value out of range. Also add timestamp edge cases: malformed
ISO format, out-of-range month/day, non-leap Feb 29, and before-epoch dates.
2026-09-13 22:39:31 +10:00
RichardAHBot e470d8a70b fix: tighten jsontx_exact integer boundary at 2^53
jsontx_exact: change the double boundary check from exclusive to inclusive
(d > max -> d >= max, d < -max -> d <= -max). Doubles cannot uniquely
represent odd integers at or above 2^53, so values like 2^53+1 silently
round to 2^53, corrupting the canonical form. The safe ceiling for
round-trip-exact integers is 2^53 - 1.

tests: add coverage for the 2^53 boundary (positive and negative sides,
and the silent rounding case).
2026-09-13 22:39:31 +10:00
RichardAHBot 4fb88f5f80 fix: reject \u escapes in jsontx_strict; expand tests
jsontx_strict: the comment claimed \u sequences were rejected but the
code only tracked backslashes without checking if they introduced a
unicode escape. Now properly throws on \u inside strings, which also
covers NUL-byte injection (\u0000) and prevents canonical form
mismatches caused by jsoncpp silently decoding \uXXXX.

tests: add coverage for \u rejection, jsontx_u64 negative/non-integer/
infinity rejection, delta size scaling with transaction complexity,
and sanitize rejection of unicode escapes.

Fixes a subtle security concern: without this check, a signer could
send a preimage with \uXXXX that jsoncpp would decode into a
different character, causing the canonical form to diverge from what
the signer actually signed while still passing the delta round-trip.
2026-09-13 22:39:31 +10:00
RichardAHBot b2cdcf429e improve: add sig parameter check and expand json-tx test coverage
Submit.cpp: validate jss::sig presence before use so missing-signature
errors are reported clearly rather than as a misleading 'bad signature'.

JSONTxSignatures_test: 40 test sections covering:
- strict JSON parsing (comments, unicode, trailing commas, single quotes)
- ISO-8601 timestamp round-trips and overflow
- sanitize/unsanitize delta encoding round-trips
- unknown field and duplicate field rejection
- NUL byte rejection in string values
- document size limit enforcement
- nested objects and canonical field reordering
- delta decoding edge cases (varint bounds, too many ops, copy past end,
  unknown ops, non-minimal varints)
- multiple round-trip stability
- u64 formatting edge cases
2026-09-13 22:39:31 +10:00
RichardAHBot 3fd6ee472a add: unit tests for JSONTxSignatures
Comprehensive test coverage for jsontx_strict, jsontx_iso round-tripping,
sanitize/unsanitize delta encoding, jsontx_num edge cases, jsontx_field
lookup, and full pipeline integration tests.

Tests cover:
- Valid and invalid strict JSON parsing (comments, unicode, syntax)
- ISO-8601 timestamp round-trips and overflow rejection
- Delta encoding round-trips for various transaction shapes
- Bounds checking on malformed deltas
- Multiple round-trip stability
- Whitespace normalization producing identical canonical forms
- Delta size bounded by jsontx_max_diff
2026-09-13 22:39:31 +10:00
Richard Holland b2aeb1cd92 more 2026-09-11 19:06:34 +10:00
Richard Holland 0f56d2d807 Merge branch 'dev' into jsontx 2026-09-11 16:18:41 +10:00
Richard Holland e904290e4e jsontx stuff 2026-08-05 12:43:25 +10:00
Richard Holland 723103150c init jsontxsig amendment 2026-07-31 12:36:39 +10:00
30 changed files with 4847 additions and 245 deletions

No files matched your search

+3
View File
@@ -108,6 +108,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)
@@ -227,6 +229,7 @@
#define sfProvider ((7U << 16U) + 30U)
#define sfMPTokenMetadata ((7U << 16U) + 31U)
#define sfCredentialType ((7U << 16U) + 32U)
#define sfJsonTxDelta ((7U << 16U) + 95U)
#define sfAppLoader ((7U << 16U) + 96U)
#define sfHookName ((7U << 16U) + 97U)
#define sfRemarkValue ((7U << 16U) + 98U)
+182
View 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
+15
View File
@@ -173,6 +173,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
+12
View File
@@ -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_FEATURE(RNG, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(PWALoader, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (20261005, Supported::yes, VoteBehavior::DefaultYes)
@@ -300,6 +300,7 @@ LEDGER_ENTRY(ltACCOUNT_ROOT, 0x0061, AccountRoot, account, ({
{sfAMMID, soeOPTIONAL},
{sfAppLoaderSequence, soeOPTIONAL},
{sfManifestID, soeOPTIONAL},
{sfLastTxnTime, soeOPTIONAL},
}))
/** A ledger object which contains a list of object identifiers.
@@ -156,6 +156,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)
@@ -300,6 +302,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, 95, SField::sMD_Default, SField::notSigning)
TYPED_SFIELD(sfAppLoader, VL, 96)
TYPED_SFIELD(sfHookName, VL, 97)
TYPED_SFIELD(sfRemarkValue, VL, 98)
+1 -167
View File
@@ -24,21 +24,11 @@
/**
* TRANSACTION(tag, value, name, fields)
*
* To ease maintenance, you may replace any unneeded values with "..."
* e.g. #define TRANSACTION(tag, value, name, ...)
*
* You must define a transactor class in the `ripple` namespace named `name`,
* and include its header alongside the TRANSACTOR definition using this
* format:
* #if TRANSACTION_INCLUDE
* # include <xrpld/app/tx/detail/HEADER.h>
* #endif
* and include its header in `src/xrpld/app/tx/detail/applySteps.cpp`.
*/
/** This transaction type executes a payment. */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/Payment.h>
#endif
TRANSACTION(ttPAYMENT, 0, Payment, ({
{sfDestination, soeREQUIRED},
{sfAmount, soeREQUIRED, soeMPTSupported},
@@ -51,9 +41,6 @@ TRANSACTION(ttPAYMENT, 0, Payment, ({
}))
/** This transaction type creates an escrow object. */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/Escrow.h>
#endif
TRANSACTION(ttESCROW_CREATE, 1, EscrowCreate, ({
{sfDestination, soeREQUIRED},
{sfAmount, soeREQUIRED},
@@ -75,9 +62,6 @@ TRANSACTION(ttESCROW_FINISH, 2, EscrowFinish, ({
/** This transaction type adjusts various account settings. */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/SetAccount.h>
#endif
TRANSACTION(ttACCOUNT_SET, 3, AccountSet, ({
{sfEmailHash, soeOPTIONAL},
{sfWalletLocator, soeOPTIONAL},
@@ -101,9 +85,6 @@ TRANSACTION(ttESCROW_CANCEL, 4, EscrowCancel, ({
}))
/** This transaction type sets or clears an account's "regular key". */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/SetRegularKey.h>
#endif
TRANSACTION(ttREGULAR_KEY_SET, 5, SetRegularKey, ({
{sfRegularKey, soeOPTIONAL},
}))
@@ -111,9 +92,6 @@ TRANSACTION(ttREGULAR_KEY_SET, 5, SetRegularKey, ({
// 6 deprecated
/** This transaction type creates an offer to trade one asset for another. */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/CreateOffer.h>
#endif
TRANSACTION(ttOFFER_CREATE, 7, OfferCreate, ({
{sfTakerPays, soeREQUIRED},
{sfTakerGets, soeREQUIRED},
@@ -123,9 +101,6 @@ TRANSACTION(ttOFFER_CREATE, 7, OfferCreate, ({
}))
/** This transaction type cancels existing offers to trade one asset for another. */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/CancelOffer.h>
#endif
TRANSACTION(ttOFFER_CANCEL, 8, OfferCancel, ({
{sfOfferSequence, soeOPTIONAL},
{sfOfferID, soeOPTIONAL}, // keylet as alternative to offerseq
@@ -134,9 +109,6 @@ TRANSACTION(ttOFFER_CANCEL, 8, OfferCancel, ({
// 9 deprecated
/** This transaction type creates a new set of tickets. */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/CreateTicket.h>
#endif
TRANSACTION(ttTICKET_CREATE, 10, TicketCreate, ({
{sfTicketCount, soeREQUIRED},
}))
@@ -146,18 +118,12 @@ TRANSACTION(ttTICKET_CREATE, 10, TicketCreate, ({
/** This transaction type modifies the signer list associated with an account. */
// The SignerEntries are optional because a SignerList is deleted by
// setting the SignerQuorum to zero and omitting SignerEntries.
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/SetSignerList.h>
#endif
TRANSACTION(ttSIGNER_LIST_SET, 12, SignerListSet, ({
{sfSignerQuorum, soeREQUIRED},
{sfSignerEntries, soeOPTIONAL},
}))
/** This transaction type creates a new unidirectional XRP payment channel. */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/PayChan.h>
#endif
TRANSACTION(ttPAYCHAN_CREATE, 13, PaymentChannelCreate, ({
{sfDestination, soeREQUIRED},
{sfAmount, soeREQUIRED},
@@ -185,9 +151,6 @@ TRANSACTION(ttPAYCHAN_CLAIM, 15, PaymentChannelClaim, ({
}))
/** This transaction type creates a new check. */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/CreateCheck.h>
#endif
TRANSACTION(ttCHECK_CREATE, 16, CheckCreate, ({
{sfDestination, soeREQUIRED},
{sfSendMax, soeREQUIRED},
@@ -197,9 +160,6 @@ TRANSACTION(ttCHECK_CREATE, 16, CheckCreate, ({
}))
/** This transaction type cashes an existing check. */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/CashCheck.h>
#endif
TRANSACTION(ttCHECK_CASH, 17, CheckCash, ({
{sfCheckID, soeREQUIRED},
{sfAmount, soeOPTIONAL},
@@ -207,17 +167,11 @@ TRANSACTION(ttCHECK_CASH, 17, CheckCash, ({
}))
/** This transaction type cancels an existing check. */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/CancelCheck.h>
#endif
TRANSACTION(ttCHECK_CANCEL, 18, CheckCancel, ({
{sfCheckID, soeREQUIRED},
}))
/** This transaction type grants or revokes authorization to transfer funds. */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/DepositPreauth.h>
#endif
TRANSACTION(ttDEPOSIT_PREAUTH, 19, DepositPreauth, ({
{sfAuthorize, soeOPTIONAL},
{sfUnauthorize, soeOPTIONAL},
@@ -226,9 +180,6 @@ TRANSACTION(ttDEPOSIT_PREAUTH, 19, DepositPreauth, ({
}))
/** This transaction type modifies a trustline between two accounts. */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/SetTrust.h>
#endif
TRANSACTION(ttTRUST_SET, 20, TrustSet, ({
{sfLimitAmount, soeOPTIONAL},
{sfQualityIn, soeOPTIONAL},
@@ -236,9 +187,6 @@ TRANSACTION(ttTRUST_SET, 20, TrustSet, ({
}))
/** This transaction type deletes an existing account. */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/DeleteAccount.h>
#endif
TRANSACTION(ttACCOUNT_DELETE, 21, AccountDelete, ({
{sfDestination, soeREQUIRED},
{sfDestinationTag, soeOPTIONAL},
@@ -246,17 +194,11 @@ TRANSACTION(ttACCOUNT_DELETE, 21, AccountDelete, ({
}))
/** This transaction type installs a hook. */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/SetHook.h>
#endif
TRANSACTION(ttHOOK_SET, 22, SetHook, ({
{sfHooks, soeREQUIRED},
}))
/** This transaction mints a new NFT. */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/NFTokenMint.h>
#endif
TRANSACTION(ttNFTOKEN_MINT, 25, NFTokenMint, ({
{sfNFTokenTaxon, soeREQUIRED},
{sfTransferFee, soeOPTIONAL},
@@ -268,18 +210,12 @@ TRANSACTION(ttNFTOKEN_MINT, 25, NFTokenMint, ({
}))
/** This transaction burns (i.e. destroys) an existing NFT. */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/NFTokenBurn.h>
#endif
TRANSACTION(ttNFTOKEN_BURN, 26, NFTokenBurn, ({
{sfNFTokenID, soeREQUIRED},
{sfOwner, soeOPTIONAL},
}))
/** This transaction creates a new offer to buy or sell an NFT. */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/NFTokenCreateOffer.h>
#endif
TRANSACTION(ttNFTOKEN_CREATE_OFFER, 27, NFTokenCreateOffer, ({
{sfNFTokenID, soeREQUIRED},
{sfAmount, soeREQUIRED},
@@ -289,17 +225,11 @@ TRANSACTION(ttNFTOKEN_CREATE_OFFER, 27, NFTokenCreateOffer, ({
}))
/** This transaction cancels an existing offer to buy or sell an existing NFT. */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/NFTokenCancelOffer.h>
#endif
TRANSACTION(ttNFTOKEN_CANCEL_OFFER, 28, NFTokenCancelOffer, ({
{sfNFTokenOffers, soeREQUIRED},
}))
/** This transaction accepts an existing offer to buy or sell an existing NFT. */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/NFTokenAcceptOffer.h>
#endif
TRANSACTION(ttNFTOKEN_ACCEPT_OFFER, 29, NFTokenAcceptOffer, ({
{sfNFTokenBuyOffer, soeOPTIONAL},
{sfNFTokenSellOffer, soeOPTIONAL},
@@ -307,18 +237,12 @@ TRANSACTION(ttNFTOKEN_ACCEPT_OFFER, 29, NFTokenAcceptOffer, ({
}))
/** This transaction claws back issued tokens. */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/Clawback.h>
#endif
TRANSACTION(ttCLAWBACK, 30, Clawback, ({
{sfAmount, soeREQUIRED, soeMPTSupported},
{sfHolder, soeOPTIONAL},
}))
/** This transaction claws back tokens from an AMM pool. */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/AMMClawback.h>
#endif
TRANSACTION(ttAMM_CLAWBACK, 31, AMMClawback, ({
{sfHolder, soeREQUIRED},
{sfAsset, soeREQUIRED},
@@ -327,9 +251,6 @@ TRANSACTION(ttAMM_CLAWBACK, 31, AMMClawback, ({
}))
/** This transaction type creates an AMM instance */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/AMMCreate.h>
#endif
TRANSACTION(ttAMM_CREATE, 35, AMMCreate, ({
{sfAmount, soeREQUIRED},
{sfAmount2, soeREQUIRED},
@@ -337,9 +258,6 @@ TRANSACTION(ttAMM_CREATE, 35, AMMCreate, ({
}))
/** This transaction type deposits into an AMM instance */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/AMMDeposit.h>
#endif
TRANSACTION(ttAMM_DEPOSIT, 36, AMMDeposit, ({
{sfAsset, soeREQUIRED},
{sfAsset2, soeREQUIRED},
@@ -351,9 +269,6 @@ TRANSACTION(ttAMM_DEPOSIT, 36, AMMDeposit, ({
}))
/** This transaction type withdraws from an AMM instance */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/AMMWithdraw.h>
#endif
TRANSACTION(ttAMM_WITHDRAW, 37, AMMWithdraw, ({
{sfAsset, soeREQUIRED},
{sfAsset2, soeREQUIRED},
@@ -364,9 +279,6 @@ TRANSACTION(ttAMM_WITHDRAW, 37, AMMWithdraw, ({
}))
/** This transaction type votes for the trading fee */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/AMMVote.h>
#endif
TRANSACTION(ttAMM_VOTE, 38, AMMVote, ({
{sfAsset, soeREQUIRED},
{sfAsset2, soeREQUIRED},
@@ -374,9 +286,6 @@ TRANSACTION(ttAMM_VOTE, 38, AMMVote, ({
}))
/** This transaction type bids for the auction slot */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/AMMBid.h>
#endif
TRANSACTION(ttAMM_BID, 39, AMMBid, ({
{sfAsset, soeREQUIRED},
{sfAsset2, soeREQUIRED},
@@ -386,18 +295,12 @@ TRANSACTION(ttAMM_BID, 39, AMMBid, ({
}))
/** This transaction type deletes AMM in the empty state */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/AMMDelete.h>
#endif
TRANSACTION(ttAMM_DELETE, 40, AMMDelete, ({
{sfAsset, soeREQUIRED},
{sfAsset2, soeREQUIRED},
}))
/** This transaction mints/burns/buys/sells a URI TOKEN */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/URIToken.h>
#endif
TRANSACTION(ttURITOKEN_MINT, 45, URITokenMint, ({
{sfURI, soeREQUIRED},
{sfDigest, soeOPTIONAL},
@@ -425,9 +328,6 @@ TRANSACTION(ttURITOKEN_CANCEL_SELL_OFFER, 49, URITokenCancelSellOffer, ({
}))
/** This transactions creates a crosschain sequence number */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/XChainBridge.h>
#endif
TRANSACTION(ttXCHAIN_CREATE_CLAIM_ID, 50, XChainCreateClaimID, ({
{sfXChainBridge, soeREQUIRED},
{sfSignatureReward, soeREQUIRED},
@@ -507,9 +407,6 @@ TRANSACTION(ttXCHAIN_CREATE_BRIDGE, 57, XChainCreateBridge, ({
}))
/** This transaction type creates or updates a DID */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/DID.h>
#endif
TRANSACTION(ttDID_SET, 58, DIDSet, ({
{sfDIDDocument, soeOPTIONAL},
{sfURI, soeOPTIONAL},
@@ -520,9 +417,6 @@ TRANSACTION(ttDID_SET, 58, DIDSet, ({
TRANSACTION(ttDID_DELETE, 59, DIDDelete, ({}))
/** This transaction type creates an Oracle instance */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/SetOracle.h>
#endif
TRANSACTION(ttORACLE_SET, 60, OracleSet, ({
{sfOracleDocumentID, soeREQUIRED},
{sfProvider, soeOPTIONAL},
@@ -533,26 +427,17 @@ TRANSACTION(ttORACLE_SET, 60, OracleSet, ({
}))
/** This transaction type deletes an Oracle instance */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/DeleteOracle.h>
#endif
TRANSACTION(ttORACLE_DELETE, 61, OracleDelete, ({
{sfOracleDocumentID, soeREQUIRED},
}))
/** This transaction type fixes a problem in the ledger state */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/LedgerStateFix.h>
#endif
TRANSACTION(ttLEDGER_STATE_FIX, 62, LedgerStateFix, ({
{sfLedgerFixType, soeREQUIRED},
{sfOwner, soeOPTIONAL},
}))
/** This transaction type creates a MPTokensIssuance instance */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/MPTokenIssuanceCreate.h>
#endif
TRANSACTION(ttMPTOKEN_ISSUANCE_CREATE, 63, MPTokenIssuanceCreate, ({
{sfAssetScale, soeOPTIONAL},
{sfTransferFee, soeOPTIONAL},
@@ -561,35 +446,23 @@ TRANSACTION(ttMPTOKEN_ISSUANCE_CREATE, 63, MPTokenIssuanceCreate, ({
}))
/** This transaction type destroys a MPTokensIssuance instance */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/MPTokenIssuanceDestroy.h>
#endif
TRANSACTION(ttMPTOKEN_ISSUANCE_DESTROY, 64, MPTokenIssuanceDestroy, ({
{sfMPTokenIssuanceID, soeREQUIRED},
}))
/** This transaction type sets flags on a MPTokensIssuance or MPToken instance */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/MPTokenIssuanceSet.h>
#endif
TRANSACTION(ttMPTOKEN_ISSUANCE_SET, 65, MPTokenIssuanceSet, ({
{sfMPTokenIssuanceID, soeREQUIRED},
{sfHolder, soeOPTIONAL},
}))
/** This transaction type authorizes a MPToken instance */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/MPTokenAuthorize.h>
#endif
TRANSACTION(ttMPTOKEN_AUTHORIZE, 66, MPTokenAuthorize, ({
{sfMPTokenIssuanceID, soeREQUIRED},
{sfHolder, soeOPTIONAL},
}))
/** This transaction type create an Credential instance */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/Credentials.h>
#endif
TRANSACTION(ttCREDENTIAL_CREATE, 67, CredentialCreate, ({
{sfSubject, soeREQUIRED},
{sfCredentialType, soeREQUIRED},
@@ -611,9 +484,6 @@ TRANSACTION(ttCREDENTIAL_DELETE, 69, CredentialDelete, ({
}))
/** This transaction type modify a NFToken */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/NFTokenModify.h>
#endif
TRANSACTION(ttNFTOKEN_MODIFY, 70, NFTokenModify, ({
{sfNFTokenID, soeREQUIRED},
{sfOwner, soeOPTIONAL},
@@ -621,18 +491,12 @@ TRANSACTION(ttNFTOKEN_MODIFY, 70, NFTokenModify, ({
}))
/** This transaction type creates or modifies a Permissioned Domain */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/PermissionedDomainSet.h>
#endif
TRANSACTION(ttPERMISSIONED_DOMAIN_SET, 71, PermissionedDomainSet, ({
{sfDomainID, soeOPTIONAL},
{sfAcceptedCredentials, soeREQUIRED},
}))
/** This transaction type deletes a Permissioned Domain */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/PermissionedDomainDelete.h>
#endif
TRANSACTION(ttPERMISSIONED_DOMAIN_DELETE, 72, PermissionedDomainDelete, ({
{sfDomainID, soeREQUIRED},
}))
@@ -646,9 +510,6 @@ TRANSACTION(ttPERMISSIONED_DOMAIN_DELETE, 72, PermissionedDomainDelete, ({
open ledger, so a reveal only becomes public after the revealing
validator's transaction set is locked.
*/
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/Change.h>
#endif
TRANSACTION(ttENTROPY, 89, Entropy, ({
{sfLedgerSequence, soeREQUIRED},
{sfPublicKey, soeREQUIRED},
@@ -658,26 +519,17 @@ TRANSACTION(ttENTROPY, 89, Entropy, ({
/* sfAccount is supplied by TxFormats::commonFields; listing it here as well
makes the SOTemplate reject the format at construction. */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/SetManifest.h>
#endif
TRANSACTION(ttMANIFEST_SET, 91, SetManifest, ({
{sfManifest, soeREQUIRED},
}))
/* A pseudo-txn alarm signal for invoking a hook, emitted by validators after alarm set conditions are met */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/Cron.h>
#endif
TRANSACTION(ttCRON, 92, Cron, ({
{sfOwner, soeREQUIRED},
{sfLedgerSequence, soeREQUIRED},
}))
/* Sechedule an alarm for later */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/CronSet.h>
#endif
TRANSACTION(ttCRON_SET, 93, CronSet, ({
{sfDelaySeconds, soeOPTIONAL},
{sfRepeatCount, soeOPTIONAL},
@@ -685,9 +537,6 @@ TRANSACTION(ttCRON_SET, 93, CronSet, ({
}))
/* A note attaching transactor that allows the owner or issuer (on a object by object basis) to attach remarks */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/SetRemarks.h>
#endif
TRANSACTION(ttREMARKS_SET, 94, SetRemarks, ({
{sfObjectID, soeREQUIRED},
{sfRemarks, soeREQUIRED},
@@ -695,9 +544,6 @@ TRANSACTION(ttREMARKS_SET, 94, SetRemarks, ({
/* A payment transactor that delivers only the exact amounts specified, creating accounts and TLs as needed
* that the sender pays for. */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/Remit.h>
#endif
TRANSACTION(ttREMIT, 95, Remit, ({
{sfDestination, soeREQUIRED},
{sfAmounts, soeOPTIONAL},
@@ -712,18 +558,12 @@ TRANSACTION(ttREMIT, 95, Remit, ({
/** This transaction can only be used by the genesis account, which is controlled exclusively by
* rewards/governance hooks, to print new XRP to be delivered directly to an array of destinations,
* according to reward schedule */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/GenesisMint.h>
#endif
TRANSACTION(ttGENESIS_MINT, 96, GenesisMint, ({
{sfGenesisMints, soeREQUIRED},
}))
/** This transaction accepts a proof of burn from an external network as a basis
* for minting according to featureImport */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/Import.h>
#endif
TRANSACTION(ttIMPORT, 97, Import, ({
{sfBlob, soeREQUIRED},
{sfIssuer, soeOPTIONAL},
@@ -731,18 +571,12 @@ TRANSACTION(ttIMPORT, 97, Import, ({
/** This transaction resets accumulator/counters and claims a reward for holding an average balance
* from a specified hook */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/ClaimReward.h>
#endif
TRANSACTION(ttCLAIM_REWARD, 98, ClaimReward, ({
{sfIssuer, soeOPTIONAL},
{sfClaimCurrency, soeOPTIONAL},
}))
/** This transaction invokes a hook, providing arbitrary data. Essentially as a 0 drop payment. **/
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/Invoke.h>
#endif
TRANSACTION(ttINVOKE, 99, Invoke, ({
{sfBlob, soeOPTIONAL},
{sfDestination, soeOPTIONAL},
+1
View File
@@ -641,6 +641,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
View 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
+16
View File
@@ -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
+2
View File
@@ -49,6 +49,8 @@ TxFormats::TxFormats()
{sfNetworkID, soeOPTIONAL},
{sfHookParameters, soeOPTIONAL},
{sfHookName, soeOPTIONAL},
{sfTime, soeOPTIONAL},
{sfJsonTxDelta, soeOPTIONAL},
};
#pragma push_macro("UNWRAP")
File diff suppressed because it is too large. Load diff
+32 -14
View File
@@ -718,6 +718,31 @@ public:
replayer.deltas_.size() == deltas;
}
/**
* Sweep finished tasks and wait until all sub-tasks are gone.
*
* A finished task can still be briefly referenced by a JobQueue worker
* (e.g. the timer job that delivered the final reply is still unwinding
* and holds a locked shared_ptr to the sub-task). The replayer only
* tracks sub-tasks by weak_ptr, so a single sweep racing with that
* worker can leave an entry behind until the next sweep. Production
* sweeps periodically, so retry here instead of asserting on one sweep.
*/
bool
sweepAndWaitForEmpty()
{
int totalRound = 100;
for (int i = 0; i < totalRound; ++i)
{
replayer.sweep();
if (countsAsExpected(0, 0, 0))
return true;
if (i < totalRound - 1)
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
return false;
}
std::shared_ptr<SkipListAcquire>
findSkipListAcquire(uint256 const& hash)
{
@@ -1175,8 +1200,7 @@ struct LedgerReplayer_test : public beast::unit_test::suite
deltaStatuses));
// sweep
net.client.replayer.sweep();
BEAST_EXPECT(net.client.countsAsExpected(0, 0, 0));
BEAST_EXPECT(net.client.sweepAndWaitForEmpty());
}
void
@@ -1205,8 +1229,7 @@ struct LedgerReplayer_test : public beast::unit_test::suite
deltaStatuses));
// sweep
net.client.replayer.sweep();
BEAST_EXPECT(net.client.countsAsExpected(0, 0, 0));
BEAST_EXPECT(net.client.sweepAndWaitForEmpty());
}
void
@@ -1257,8 +1280,7 @@ struct LedgerReplayer_test : public beast::unit_test::suite
BEAST_EXPECT(net.client.waitForLedgers(finalHash, totalReplay));
// sweep
net.client.replayer.sweep();
BEAST_EXPECT(net.client.countsAsExpected(0, 0, 0));
BEAST_EXPECT(net.client.sweepAndWaitForEmpty());
}
void
@@ -1453,8 +1475,7 @@ struct LedgerReplayer_test : public beast::unit_test::suite
BEAST_EXPECT(net.client.countsAsExpected(5, 3, totalReplay * 3 - 1));
// sweep
net.client.replayer.sweep();
BEAST_EXPECT(net.client.countsAsExpected(0, 0, 0));
BEAST_EXPECT(net.client.sweepAndWaitForEmpty());
}
void
@@ -1509,8 +1530,7 @@ struct LedgerReplayerTimeout_test : public beast::unit_test::suite
// sweep
BEAST_EXPECT(net.client.countsAsExpected(1, 1, 0));
net.client.replayer.sweep();
BEAST_EXPECT(net.client.countsAsExpected(0, 0, 0));
BEAST_EXPECT(net.client.sweepAndWaitForEmpty());
}
void
@@ -1543,8 +1563,7 @@ struct LedgerReplayerTimeout_test : public beast::unit_test::suite
// sweep
BEAST_EXPECT(net.client.countsAsExpected(1, 1, totalReplay - 1));
net.client.replayer.sweep();
BEAST_EXPECT(net.client.countsAsExpected(0, 0, 0));
BEAST_EXPECT(net.client.sweepAndWaitForEmpty());
}
void
@@ -1607,8 +1626,7 @@ struct LedgerReplayerLong_test : public beast::unit_test::suite
rounds, rounds, rounds * (totalReplay - 1)));
// sweep
net.client.replayer.sweep();
BEAST_EXPECT(net.client.countsAsExpected(0, 0, 0));
BEAST_EXPECT(net.client.sweepAndWaitForEmpty());
}
};
File diff suppressed because it is too large. Load diff
+32 -6
View File
@@ -34,6 +34,7 @@
#include <xrpld/rpc/ServerHandler.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/base64.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/beast/test/yield_to.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/SystemParameters.h>
@@ -47,6 +48,7 @@
#include <boost/beast/http.hpp>
#include <array>
#include <chrono>
#include <map>
#include <mutex>
#include <sstream>
@@ -802,7 +804,14 @@ class PWAServe_test : public beast::unit_test::suite,
Account const nobody{"nobody"};
env.fund(XRP(1000), nobody);
env.close();
// The resource clock has one-second granularity and each tick
// decays ~1/32 of the balance, far more than the few-miss margin
// left below the drop threshold. Arm the client just after a tick
// so the misses normally land before the next one.
auto nearDrop = [&](std::string const& ip, int misses) {
auto const t0 = stopwatch().now();
while (stopwatch().now() == t0)
std::this_thread::sleep_for(std::chrono::milliseconds(1));
auto c = env.app().getResourceManager().newInboundEndpoint(
beast::IP::Endpoint::from_string(ip));
c.charge(Resource::Charge(
@@ -811,8 +820,10 @@ class PWAServe_test : public beast::unit_test::suite,
(20 + 400), // feeReferenceRPC + feeMediumBurdenRPC
"test"));
};
// Throttled (503) after at least one miss (404), well before the
// ~60 misses a decay tick would require.
auto throttles = [&](std::string const& client) {
for (int i = 0; i < 50; ++i)
for (int i = 0; i < 10; ++i)
{
auto const s =
getDoc(env, nobody, yield, {{"X-Forwarded-For", client}})
@@ -824,14 +835,29 @@ class PWAServe_test : public beast::unit_test::suite,
}
return false;
};
nearDrop("198.51.100.9", 3);
BEAST_EXPECT(throttles("198.51.100.9"));
// On a loaded runner a tick can still land mid-attempt; retry with
// a fresh client (an armed one cannot be re-armed without starting
// over the threshold). A real regression fails every attempt.
auto eventually = [](auto&& attempt) {
for (int n = 0; n < 5; ++n)
if (attempt(n))
return true;
return false;
};
BEAST_EXPECT(eventually([&](int n) {
auto const ip = "198.51.100." + std::to_string(9 + n);
nearDrop(ip, 3);
return throttles(ip);
}));
BEAST_EXPECT(
with({{"X-Forwarded-For", "203.0.113.7"}}) ==
200); // others unaffected
nearDrop("2001:db8:1:2::", 3);
BEAST_EXPECT(throttles("2001:db8:1:2:dead:beef::1")); // same /64
BEAST_EXPECT(with({{"X-Forwarded-For", "2001:db8:1:3::1"}}) == 200);
BEAST_EXPECT(eventually([&](int n) {
auto const prefix = "2001:db8:1:" + std::to_string(2 + n) + ":";
nearDrop(prefix + ":", 3);
return throttles(prefix + "dead:beef::1"); // same /64
}));
BEAST_EXPECT(with({{"X-Forwarded-For", "2001:db8:2::1"}}) == 200);
}
void
+12
View File
@@ -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();
+27 -3
View File
@@ -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)
+33 -3
View File
@@ -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;
+15 -1
View File
@@ -789,6 +789,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();
@@ -1968,7 +1975,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]);
+10
View File
@@ -69,6 +69,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))
{
+8
View File
@@ -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))
{
+27
View File
@@ -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;
@@ -330,6 +339,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
+4 -1
View File
@@ -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_));
+117
View File
@@ -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;
@@ -621,6 +680,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) &&
@@ -703,6 +768,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;
@@ -766,6 +871,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())
{
+6
View File
@@ -326,6 +326,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();
}
+57
View File
@@ -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
+57 -16
View File
@@ -18,21 +18,61 @@
//==============================================================================
#include <xrpld/app/tx/applySteps.h>
#pragma push_macro("TRANSACTION")
#undef TRANSACTION
// Do nothing
#define TRANSACTION(...)
#define TRANSACTION_INCLUDE 1
#include <xrpl/protocol/detail/transactions.macro>
#undef TRANSACTION
#pragma pop_macro("TRANSACTION")
// DO NOT INCLUDE TRANSACTOR HEADER FILES HERE.
// See the instructions at the top of transactions.macro instead.
#include <xrpld/app/tx/detail/AMMBid.h>
#include <xrpld/app/tx/detail/AMMClawback.h>
#include <xrpld/app/tx/detail/AMMCreate.h>
#include <xrpld/app/tx/detail/AMMDelete.h>
#include <xrpld/app/tx/detail/AMMDeposit.h>
#include <xrpld/app/tx/detail/AMMVote.h>
#include <xrpld/app/tx/detail/AMMWithdraw.h>
#include <xrpld/app/tx/detail/ApplyContext.h>
#include <xrpld/app/tx/detail/CancelCheck.h>
#include <xrpld/app/tx/detail/CancelOffer.h>
#include <xrpld/app/tx/detail/CashCheck.h>
#include <xrpld/app/tx/detail/Change.h>
#include <xrpld/app/tx/detail/ClaimReward.h>
#include <xrpld/app/tx/detail/Clawback.h>
#include <xrpld/app/tx/detail/CreateCheck.h>
#include <xrpld/app/tx/detail/CreateOffer.h>
#include <xrpld/app/tx/detail/CreateTicket.h>
#include <xrpld/app/tx/detail/Credentials.h>
#include <xrpld/app/tx/detail/Cron.h>
#include <xrpld/app/tx/detail/CronSet.h>
#include <xrpld/app/tx/detail/DID.h>
#include <xrpld/app/tx/detail/DeleteAccount.h>
#include <xrpld/app/tx/detail/DeleteOracle.h>
#include <xrpld/app/tx/detail/DepositPreauth.h>
#include <xrpld/app/tx/detail/Escrow.h>
#include <xrpld/app/tx/detail/GenesisMint.h>
#include <xrpld/app/tx/detail/Import.h>
#include <xrpld/app/tx/detail/Invoke.h>
#include <xrpld/app/tx/detail/LedgerStateFix.h>
#include <xrpld/app/tx/detail/MPTokenAuthorize.h>
#include <xrpld/app/tx/detail/MPTokenIssuanceCreate.h>
#include <xrpld/app/tx/detail/MPTokenIssuanceDestroy.h>
#include <xrpld/app/tx/detail/MPTokenIssuanceSet.h>
#include <xrpld/app/tx/detail/NFTokenAcceptOffer.h>
#include <xrpld/app/tx/detail/NFTokenBurn.h>
#include <xrpld/app/tx/detail/NFTokenCancelOffer.h>
#include <xrpld/app/tx/detail/NFTokenCreateOffer.h>
#include <xrpld/app/tx/detail/NFTokenMint.h>
#include <xrpld/app/tx/detail/NFTokenModify.h>
#include <xrpld/app/tx/detail/PayChan.h>
#include <xrpld/app/tx/detail/Payment.h>
#include <xrpld/app/tx/detail/PermissionedDomainDelete.h>
#include <xrpld/app/tx/detail/PermissionedDomainSet.h>
#include <xrpld/app/tx/detail/Remit.h>
#include <xrpld/app/tx/detail/SetAccount.h>
#include <xrpld/app/tx/detail/SetHook.h>
#include <xrpld/app/tx/detail/SetManifest.h>
#include <xrpld/app/tx/detail/SetOracle.h>
#include <xrpld/app/tx/detail/SetRegularKey.h>
#include <xrpld/app/tx/detail/SetRemarks.h>
#include <xrpld/app/tx/detail/SetSignerList.h>
#include <xrpld/app/tx/detail/SetTrust.h>
#include <xrpld/app/tx/detail/URIToken.h>
#include <xrpld/app/tx/detail/XChainBridge.h>
#include <xrpld/app/tx/detail/XahauGenesis.h>
#include <xrpl/protocol/TxFormats.h>
#include <stdexcept>
@@ -222,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)
{
}
+146 -31
View File
@@ -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)
{