Compare commits

..
Author SHA1 Message Date
Nik Bougalis c5aa162322 Make ApplyFlags a scoped enumeration
Convert ApplyFlags to a scoped enum, drop the now-redundant `Tap`
prefix from its enumerators and replace the hand-written bitwise
operators by opting in to enum_bitops.

No functional change.
2026-10-20 14:05:13 -07:00
Nik Bougalis 82421bc2a1 Make the path element type a scoped enumeration
STPathElement stored its type as an unsigned int, returned it as a
std::uint32_t, and defined the flags as an unscoped enum. The type
has always been serialized as a single byte.

Introduce STPathElementType, a scoped enum with std::uint8_t as its
fixed underlying type, opted into the bitwise operators from
enum_bitops.h:

- Store the type as STPathElementType, and use it for the
  constructor parameter and the return type of getNodeType().
- Rename call sites from STPathElement::TypeAccount and friends to
  STPathElementType::Account and friends.
- Replace raw mask tests in the serializer, JSON output, strand
  construction and pathfinder with the existing predicates.
- Validate the type byte once when deserializing, before any field
  is read, and restructure the loop around that.
- Remove the cached hashValue_ and getHash(). Their only consumer
  was a fast-reject prefilter in operator==, whose operands are
  equal in practice (hash-set lookups after a hash match, serdes
  self-checks), so the hash was computed on every construction and
  never rejected anything. operator== keeps the same semantics
  (TypeAccount bit plus the three fields) as a hidden friend.
- Give type_ and isOffer_ default member initializers, default the
  default constructor, and move isOffer_ next to type_ so the two
  one-byte members share a word.
- Mark the trivial accessors noexcept.

sizeof(STPathElement) drops from 88 to 72 bytes and its alignment
from 8 to 4. Every parsed path and every pathfinder candidate is a
vector of these.

No functional changes are intended. Observable differences are
confined to malformed input:

- The deserializer no longer logs rejected elements at error level
  through debugLog() before throwing. The exception carries the
  reason, and logging peer-supplied garbage at error level was a
  log-spam vector.
- The exception text changed: "empty path" is now "bad path element:
  empty path", and an element with undefined type bits reports the
  byte in hex.
- If the currency and MPT bits are both set and the input is
  truncated within the account field, the invalid bit combination
  is now detected before the short read; previously the short read
  was detected first. Such elements are rejected either way.
2026-10-20 14:05:08 -07:00
Nik Bougalis 969d9035aa Introduce opt-in bitwise operators for scoped enumerations
Scoped enumerations do not support bitwise operators, so using an
`enum class` as a set of flags requires extensive casting at call
sites or a hand-written set of operators for each type.

Add `enum_bitops.h`, which provides `&`, `|`, `^`, `~` along with
the compound assignment forms for any scoped enumeration that has
an unsigned underlying type and is opted in:

    enum class Flags : std::uint8_t { a = 1, b = 2 };

    // Opt into bitwise operations
    template <>
    struct enum_bitops::OptIn<Flags> : std::true_type {};

All synthesized operators are constexpr and noexcept, and preserve
the enumeration type.
2026-10-20 14:05:05 -07:00
Nik Bougalis 7939339a65 chore: Fix an issue that breaks CLion (#8619) 2026-10-17 22:43:53 +00:00
4ea2db2eda feat: Add TransactionProposal invariants for Cosign (#8130)
Co-authored-by: Shawn Xie <[email protected]>
Co-authored-by: Kassaking7 <[email protected]>
Co-authored-by: Shawn Xie <[email protected]>
Co-authored-by: Zhiyuan Wang <[email protected]>
Co-authored-by: Claude Fable 5.1 <[email protected]>
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
2026-10-16 20:59:53 +00:00
Ayaz Salikhov d4ffe32534 ci: Fix release/staging branches running conditions (#8615) 2026-10-16 17:06:19 +00:00
Gregory Tsipenyuk 7354816f65 fix: Assorted MPT/DEX offer related fixes - 1 (#8468) 2026-10-16 16:28:05 +00:00
Gregory Tsipenyuk 12cb3932f3 fix: Check vault share freeze at MPT strand endpoints (#8478) 2026-10-16 15:26:00 +00:00
Gregory Tsipenyuk dc64a8daac fix: Allow IOU rounding in Clawback invariant balance check (#8475) 2026-10-16 15:25:03 +00:00
Nik Bougalis dc524e9cc6 refactor: Optimize the binary layout of Number (#8550) 2026-10-16 15:20:01 +00:00
Alex KremerandAyaz Salikhov 4060b1bcd4 refactor: Migrate handlers to rpc-spec (D) (#8573)
Co-authored-by: Ayaz Salikhov <[email protected]>
2026-10-16 14:06:30 +00:00
Mayukha Vadari bfbb440fc2 feat: Handle Smart Escrow's Bytecode, Data and Gas fields (#8215) 2026-10-16 13:03:48 +00:00
126 changed files with 4578 additions and 1898 deletions

No files matched your search

+1
View File
@@ -8,6 +8,7 @@ on:
branches:
- "develop"
- "release/*"
- "staging/*"
paths:
- "crates/**/Cargo.toml"
- "crates/Cargo.lock"
+1 -1
View File
@@ -243,7 +243,7 @@ jobs:
- should-run
- build-test
# Only run when committing to a PR that targets a release branch.
if: ${{ github.repository == 'XRPLF/rippled' && needs.should-run.outputs.go == 'true' && github.event_name == 'pull_request' && startsWith(github.event.pull_request.base.ref, 'release') }}
if: ${{ github.repository == 'XRPLF/rippled' && needs.should-run.outputs.go == 'true' && github.event_name == 'pull_request' && startsWith(github.event.pull_request.base.ref, 'release/') }}
uses: ./.github/workflows/reusable-upload-recipe.yml
secrets:
remote_username: ${{ secrets.NEXUS_REMOTE_USERNAME }}
+8 -6
View File
@@ -1,14 +1,16 @@
# This workflow runs all workflows to build and test the code on various Linux
# flavors, as well as on MacOS and Windows, on a scheduled basis, on merge into
# the 'develop' or 'release*' branches, or when requested manually. Upon pushes
# to the develop branch it also uploads the libxrpl recipe to the Conan remote.
# the 'develop', 'release/*' or 'staging/*' branches, or when requested
# manually. Upon pushes to the develop branch, it also uploads the libxrpl
# recipe to the Conan remote.
name: Trigger
on:
push:
branches:
- "develop"
- "release*"
- "release/*"
- "staging/*"
paths:
# These paths are unique to `on-trigger.yml`.
- ".github/workflows/on-trigger.yml"
@@ -62,9 +64,9 @@ on:
workflow_dispatch:
concurrency:
# When a PR is merged into the develop branch it will be assigned a unique
# When a PR is merged into the develop branch, it will be assigned a unique
# group identifier, so execution will continue even if another PR is merged
# while it is still running. In all other cases the group identifier is shared
# while it is still running. In all other cases, the group identifier is shared
# per branch, so that any in-progress runs are cancelled when a new commit is
# pushed.
group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' && github.sha || github.ref }}
@@ -98,7 +100,7 @@ jobs:
# However, we do not enable ccache for events targeting a release branch,
# to protect against the rare case that the output produced by ccache is
# not identical to a regular compilation.
ccache_enabled: ${{ github.repository_owner == 'XRPLF' && !startsWith(github.ref, 'refs/heads/release') }}
ccache_enabled: ${{ github.repository_owner == 'XRPLF' && !startsWith(github.ref, 'refs/heads/release/') }}
os: ${{ matrix.os }}
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
+2 -1
View File
@@ -8,7 +8,8 @@ on:
push:
branches:
- "develop"
- "release*"
- "release/*"
- "staging/*"
workflow_dispatch:
jobs:
+1
View File
@@ -37,6 +37,7 @@ Version 3.5.0 is not yet released.
- `channel_verify`: The `channel_id` and `signature` fields now return an `invalidParams` error if the value is not a string. [#7582](https://github.com/XRPLF/rippled/pull/7582)
- `account_currencies`, `account_nfts`, `account_offers`: A request with more than one problem may now report a different one of them. An invalid `ledger_hash` or `ledger_index` is reported before an invalid or missing `account`. An invalid `limit` or `marker` is reported before `lgrNotFound` or `actNotFound`, and for `account_offers` also before `actMalformed`. A request with a single problem returns the same error as before. [#8561](https://github.com/XRPLF/rippled/pull/8561)
- `deposit_authorized`: A `credentials` entry that is all zeros (for example `"0"` or 64 `0` characters) now returns a `badCredentials` error instead of looking up a credential that can never exist.
- `account_info`, `account_lines`, `account_channels`: A request with more than one problem may now report a different one of them. An invalid `ledger_hash` or `ledger_index` is reported before a missing or invalid `account`, or `ident` for `account_info`. For `account_lines`, a `peer` that is not a string, or an invalid `limit` or `marker`, is reported before `lgrNotFound`, `actNotFound` or a malformed `account`, and an invalid `limit` or `marker` also before a malformed `peer`. For `account_channels`, a malformed `account` is reported before `lgrNotFound`, and an invalid `destination_account`, `limit` or `marker` before `lgrNotFound` or `actNotFound`. A request with a single problem returns the same error as before. [#8573](https://github.com/XRPLF/rippled/pull/8573)
### Bugfixes in 3.5.0
+3
View File
@@ -297,7 +297,10 @@ if(xrpld)
VALUE_TYPE "::json::Value"
VIEW_HEADER "xrpld/rpc/detail/JsonObjectView.hpp"
HANDLERS
account_channels
account_currencies
account_info
account_lines
account_nfts
account_offers
book_changes
+1 -1
View File
@@ -3,7 +3,7 @@
"requires": [
"zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1782392402.122708",
"xxhash/0.8.3#681d36a0a6111fc56e5e45ea182c19cc%1782392402.420688",
"xrpl-rpc-spec/0.1.23#56e8946f5b3fb949a3732e1a51135ca2%1790977482.197086",
"xrpl-rpc-spec/0.1.25#2daa763e2d86ccc4ad0a0fa4d1983e9f%1791907455.579992",
"sqlite3/3.53.0#324ada52333108388a9a6108bfa96734%1782392403.185447",
"soci/4.0.3#e726491a03468795453f7c83fc924a96%1782392402.679521",
"snappy/1.1.10#968fef506ff261592ec30c574d4a7809%1782307151.633168",
+1 -1
View File
@@ -40,7 +40,7 @@ class Xrpl(ConanFile):
"nudb/2.0.9",
"openssl/3.6.3",
"soci/4.0.3",
"xrpl-rpc-spec/0.1.23",
"xrpl-rpc-spec/0.1.25",
"zlib/1.3.2",
]
+6 -2
View File
@@ -352,9 +352,9 @@ class Number final
using rep = std::int64_t;
using InternalRep = MantissaRange::rep;
bool negative_{false};
InternalRep mantissa_{0};
int exponent_{std::numeric_limits<int>::lowest()};
bool negative_{false};
public:
// The range for the exponent when normalized
@@ -650,8 +650,12 @@ private:
shiftExponent(int exponentDelta) const;
};
// The number class currently has 3 tail padding bytes. If you are adding new
// members, try to fit them there before expanding the size of the class.
static_assert(sizeof(Number) == 16);
constexpr Number::Number(bool negative, InternalRep mantissa, int exponent, Unchecked) noexcept
: negative_(negative), mantissa_{mantissa}, exponent_{exponent}
: mantissa_{mantissa}, exponent_{exponent}, negative_(negative)
{
}
+200
View File
@@ -0,0 +1,200 @@
#pragma once
#include <concepts>
#include <type_traits>
#include <utility>
namespace xrpl {
/**
* Opt-in bitwise operators for scoped enumerations.
*
* Provides `&`, `|`, `^`, `~` and the corresponding compound assignment forms
* for any scoped enumeration with an unsigned underlying type that opts in by
* specializing @ref OptIn:
*
* @code
* namespace xrpl {
*
* enum class MyFlags : std::uint32_t { a = 1, b = 2, c = 4 };
*
* template <>
* struct enum_bitops::OptIn<MyFlags> : std::true_type {};
*
* } // namespace xrpl
* @endcode
*
* The operators are constexpr and noexcept, and return the enumeration type.
*
* @par Where the specialization may be declared
* In namespace xrpl, or at global scope with full qualification, and after
* the enumeration is defined but before the operators are first used. This
* cannot be done in a nested namespace or at class scope; for enumerations
* nested in a class this means after the class definition.
*
* @par Where the operators are found
* The operators are declared in namespace xrpl, so argument-dependent lookup
* will find them only for enumerations declared in xrpl or nested in a class
* declared directly in xrpl. Enumerations in a nested namespace will only be
* found from code inside xrpl, and only if no enclosing scope declares an
* operator of the same name; elsewhere they require using-declarations.
*/
namespace enum_bitops {
/**
* Types that may be opted in to the bitwise operators.
*
* Satisfied by a scoped enumeration whose underlying type is an unsigned
* integer type.
*
* @tparam T The type to test.
*/
template <typename T>
concept Eligible = std::is_scoped_enum_v<T> && std::unsigned_integral<std::underlying_type_t<T>>;
/**
* Opt-in switch for the bitwise operators.
*
* The primary template derives from std::false_type. Specialize it to derive
* from std::true_type to enable the operators for an @ref Eligible enumeration.
*
* @tparam T The scoped enumeration to opt in.
*/
template <Eligible T>
struct OptIn : std::false_type
{
};
/**
* Enumerations for which the bitwise operators are enabled.
*
* Satisfied when @p T satisfies @ref Eligible and @ref OptIn has been
* specialized for it to derive from std::true_type.
*
* @tparam T The type to test.
*/
template <typename T>
concept Candidate = Eligible<T> && OptIn<T>::value;
} // namespace enum_bitops
// These are declared in xrpl, not in enum_bitops, so that argument-dependent
// lookup finds them for enumerations whose associated namespace is xrpl.
/**
* @name Bitwise operators for opted-in scoped enumerations
*
* Each operator applies the corresponding built-in operator to the
* underlying values and converts the result back to the enumeration type.
* Both operands must have the same enumeration type; there is no implicit
* conversion to or from the underlying type.
*/
/** @{ */
/**
* Bitwise AND.
*
* @param lhs The left operand.
* @param rhs The right operand.
* @return The bits set in both @p lhs and @p rhs.
*/
template <enum_bitops::Candidate T>
constexpr T
operator&(T lhs, T rhs) noexcept
{
return static_cast<T>(std::to_underlying(lhs) & std::to_underlying(rhs));
}
/**
* Bitwise OR.
*
* @param lhs The left operand.
* @param rhs The right operand.
* @return The bits set in @p lhs, in @p rhs, or in both.
*/
template <enum_bitops::Candidate T>
constexpr T
operator|(T lhs, T rhs) noexcept
{
return static_cast<T>(std::to_underlying(lhs) | std::to_underlying(rhs));
}
/**
* Bitwise exclusive OR.
*
* @param lhs The left operand.
* @param rhs The right operand.
* @return The bits set in exactly one of @p lhs and @p rhs.
*/
template <enum_bitops::Candidate T>
constexpr T
operator^(T lhs, T rhs) noexcept
{
return static_cast<T>(std::to_underlying(lhs) ^ std::to_underlying(rhs));
}
/**
* Bitwise complement.
*
* The result has every bit of the underlying type that is clear in
* @p val, including bits that no enumerator names. It is intended for
* clearing flags, as in `flags & ~flag`.
*
* @param val The operand.
* @return The complement of @p val.
*/
template <enum_bitops::Candidate T>
constexpr T
operator~(T val) noexcept
{
return static_cast<T>(~std::to_underlying(val));
}
/**
* Bitwise AND assignment.
*
* @param lhs The value to modify.
* @param rhs The right operand.
* @return A reference to @p lhs.
*/
template <enum_bitops::Candidate T>
constexpr T&
operator&=(T& lhs, T rhs) noexcept
{
lhs = lhs & rhs;
return lhs;
}
/**
* Bitwise OR assignment.
*
* @param lhs The value to modify.
* @param rhs The right operand.
* @return A reference to @p lhs.
*/
template <enum_bitops::Candidate T>
constexpr T&
operator|=(T& lhs, T rhs) noexcept
{
lhs = lhs | rhs;
return lhs;
}
/**
* Bitwise exclusive OR assignment.
*
* @param lhs The value to modify.
* @param rhs The right operand.
* @return A reference to @p lhs.
*/
template <enum_bitops::Candidate T>
constexpr T&
operator^=(T& lhs, T rhs) noexcept
{
lhs = lhs ^ rhs;
return lhs;
}
/** @} */
} // namespace xrpl
+12 -53
View File
@@ -1,7 +1,7 @@
#pragma once
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/safe_cast.h>
#include <xrpl/basics/enum_bitops.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/OwnerCounts.h>
#include <xrpl/ledger/ReadView.h>
@@ -22,80 +22,39 @@
namespace xrpl {
// Bitwise flag enum with existing operator overloads
// NOLINTNEXTLINE(cppcoreguidelines-use-enum-class)
enum ApplyFlags : std::uint32_t {
TapNone = 0x00,
enum class ApplyFlags : std::uint32_t {
None = 0x00,
// This is a local transaction with the
// fail_hard flag set.
TapFailHard = 0x10,
FailHard = 0x10,
// This is not the transaction's last pass
// Transaction can be retried, soft failures allowed
TapRetry = 0x20,
Retry = 0x20,
// Transaction came from a privileged source
TapUnlimited = 0x400,
Unlimited = 0x400,
// Transaction is executing as part of a batch
TapBatch = 0x800,
Batch = 0x800,
// Transaction shouldn't be applied
// Signatures shouldn't be checked
TapDryRun = 0x1000,
DryRun = 0x1000,
// Transaction is being preflighted as the payload of a
// TransactionProposalCreate. Its signatures are collected on-ledger
// afterward, so signature-presence checks (e.g. Batch signer matching)
// are skipped at proposal-creation time (On-Chain Cosigner spec
// §5.3.1.2).
TapProposal = 0x2000,
Proposal = 0x2000,
};
constexpr ApplyFlags
operator|(ApplyFlags const& lhs, ApplyFlags const& rhs)
template <>
struct enum_bitops::OptIn<ApplyFlags> : std::true_type
{
return safeCast<ApplyFlags>(
safeCast<std::underlying_type_t<ApplyFlags>>(lhs) |
safeCast<std::underlying_type_t<ApplyFlags>>(rhs));
}
static_assert((TapFailHard | TapRetry) == safeCast<ApplyFlags>(0x30u), "ApplyFlags operator |");
static_assert((TapRetry | TapFailHard) == safeCast<ApplyFlags>(0x30u), "ApplyFlags operator |");
constexpr ApplyFlags
operator&(ApplyFlags const& lhs, ApplyFlags const& rhs)
{
return safeCast<ApplyFlags>(
safeCast<std::underlying_type_t<ApplyFlags>>(lhs) &
safeCast<std::underlying_type_t<ApplyFlags>>(rhs));
}
static_assert((TapFailHard & TapRetry) == TapNone, "ApplyFlags operator &");
static_assert((TapRetry & TapFailHard) == TapNone, "ApplyFlags operator &");
constexpr ApplyFlags
operator~(ApplyFlags const& flags)
{
return safeCast<ApplyFlags>(~safeCast<std::underlying_type_t<ApplyFlags>>(flags));
}
static_assert(~TapRetry == safeCast<ApplyFlags>(0xFFFFFFDFu), "ApplyFlags operator ~");
inline ApplyFlags
operator|=(ApplyFlags& lhs, ApplyFlags const& rhs)
{
lhs = lhs | rhs;
return lhs;
}
inline ApplyFlags
operator&=(ApplyFlags& lhs, ApplyFlags const& rhs)
{
lhs = lhs & rhs;
return lhs;
}
};
//------------------------------------------------------------------------------
@@ -14,6 +14,7 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Concepts.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Fees.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/Keylet.h>
@@ -27,6 +28,8 @@
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/UintTypes.h>
#include <cstdint>
namespace xrpl {
/**
@@ -560,4 +563,54 @@ escrowUnlockApplyHelper<MPTIssue>(
journal);
}
/**
* Smart escrow kill switches, driven by the voted FeeSettings.
*
* Zeroing `bytecodeSizeLimit` stops new uploads while leaving existing escrows
* finishable without forcing holders to wait for `CancelAfter`.
* Zeroing `gasLimit` stops both.
*/
/** @{ */
inline bool
isBytecodeUploadDisabled(Fees const& fees)
{
return fees.bytecodeSizeLimit == 0 || fees.gasLimit == 0;
}
inline bool
isBytecodeExecutionDisabled(Fees const& fees)
{
return fees.gasLimit == 0;
}
/** @} */
/**
* Owner count an escrow costs.
*
* An escrow costs one increment per started 500 bytes of bytecode, and one
* increment if it carries no bytecode.
*/
template <class T>
int32_t
calculateAdditionalReserve(T const& finishFunction)
{
static auto constexpr kBytecodeReserveIncrement = 500;
if (!finishFunction)
return 1;
auto const size = finishFunction->size();
if (size == 0)
{
// LCOV_EXCL_START
// Empty Bytecode is refused at create, so no escrow carries it. Ceiling
// division would answer 0, which would subtract less than the create added.
UNREACHABLE("xrpl::calculateAdditionalReserve : empty Bytecode");
return 1;
// LCOV_EXCL_STOP
}
return static_cast<int32_t>((size + kBytecodeReserveIncrement - 1) / kBytecodeReserveIncrement);
}
} // namespace xrpl
+5
View File
@@ -398,6 +398,11 @@ constexpr std::uint32_t kMaxInvestmentPeriod = std::chrono::seconds{std::chrono:
*/
constexpr std::uint8_t kMaxAssetCheckDepth = 5;
/**
* Maximum length of a Data field in Escrow object that can be updated by WASM code.
*/
constexpr std::size_t kMaxWasmDataLength = 1 * 1024; // 1KB
/**
* A ledger index.
*/
+80 -107
View File
@@ -2,6 +2,8 @@
#include <xrpl/basics/CountedObject.h>
#include <xrpl/basics/UnorderedContainers.h>
#include <xrpl/basics/enum_bitops.h>
#include <xrpl/beast/hash/hash_append.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/AccountID.h>
@@ -15,37 +17,43 @@
#include <cstdint>
#include <memory>
#include <optional>
#include <type_traits>
#include <utility>
#include <vector>
namespace xrpl {
enum class STPathElementType : std::uint8_t {
// clang-format off
None = 0x00, ///< No element; also terminates the serialized path set.
Account = 0x01, ///< Rippling through an account (vs taking an offer).
Currency = 0x10, ///< Currency follows.
Issuer = 0x20, ///< Issuer follows.
Mpt = 0x40, ///< MPT follows.
Boundary = 0xFF, ///< Boundary between alternate paths.
Asset = Currency | Mpt, ///< Either asset kind.
All = Account | Currency | Issuer | Mpt, ///< Every value-carrying bit.
// clang-format on
};
template <>
struct enum_bitops::OptIn<STPathElementType> : std::true_type
{
};
class STPathElement final : public CountedObject<STPathElement>
{
unsigned int type_;
STPathElementType type_ = STPathElementType::None;
bool isOffer_ = true;
AccountID accountID_;
PathAsset assetID_;
AccountID issuerID_;
bool isOffer_;
std::size_t hashValue_;
public:
// Bitwise values (typeCurrency | typeMPT)
// NOLINTNEXTLINE(cppcoreguidelines-use-enum-class)
enum Type {
TypeNone = 0x00,
TypeAccount = 0x01, // Rippling through an account (vs taking an offer).
TypeCurrency = 0x10, // Currency follows.
TypeIssuer = 0x20, // Issuer follows.
TypeMpt = 0x40, // MPT follows.
TypeBoundary = 0xFF, // Boundary between alternate paths.
TypeAsset = TypeCurrency | TypeMpt,
TypeAll = TypeAccount | TypeCurrency | TypeIssuer | TypeMpt,
// Combination of all types.
};
STPathElement();
STPathElement() = default;
STPathElement(STPathElement const&) = default;
STPathElement&
operator=(STPathElement const&) = default;
@@ -62,42 +70,42 @@ public:
bool forceAsset = false);
STPathElement(
unsigned int uType,
STPathElementType type,
AccountID const& account,
PathAsset const& asset,
AccountID const& issuer);
[[nodiscard]] std::uint32_t
getNodeType() const;
[[nodiscard]] STPathElementType
getNodeType() const noexcept;
[[nodiscard]] bool
isOffer() const;
isOffer() const noexcept;
[[nodiscard]] bool
isAccount() const;
isAccount() const noexcept;
[[nodiscard]] bool
hasIssuer() const;
hasIssuer() const noexcept;
[[nodiscard]] bool
hasCurrency() const;
hasCurrency() const noexcept;
[[nodiscard]] bool
hasMPT() const;
hasMPT() const noexcept;
[[nodiscard]] bool
hasAsset() const;
hasAsset() const noexcept;
[[nodiscard]] bool
isNone() const;
isNone() const noexcept;
// Nodes are either an account ID or a offer prefix. Offer prefixs denote a
// class of offers.
[[nodiscard]] AccountID const&
getAccountID() const;
getAccountID() const noexcept;
[[nodiscard]] PathAsset const&
getPathAsset() const;
getPathAsset() const noexcept;
[[nodiscard]] Currency const&
getCurrency() const;
@@ -106,17 +114,18 @@ public:
getMPTID() const;
[[nodiscard]] AccountID const&
getIssuerID() const;
getIssuerID() const noexcept;
[[nodiscard]] bool
isType(Type const& pe) const;
isType(STPathElementType pe) const noexcept;
bool
operator==(STPathElement const& t) const;
private:
static std::size_t
getHash(STPathElement const& element);
friend bool
operator==(STPathElement const& lhs, STPathElement const& rhs) noexcept
{
return lhs.isType(STPathElementType::Account) == rhs.isType(STPathElementType::Account) &&
lhs.accountID_ == rhs.accountID_ && lhs.assetID_ == rhs.assetID_ &&
lhs.issuerID_ == rhs.issuerID_;
}
};
template <class Hasher>
@@ -124,7 +133,7 @@ void
hash_append(Hasher& h, STPathElement const& e) noexcept
{
using beast::hash_append;
hash_append(h, (e.getNodeType() & STPathElement::TypeAccount) != 0u);
hash_append(h, e.isType(STPathElementType::Account));
hash_append(h, e.getAccountID());
hash_append(h, e.getPathAsset());
hash_append(h, e.getIssuerID());
@@ -337,29 +346,16 @@ private:
// ------------ STPathElement ------------
inline STPathElement::STPathElement() : type_(TypeNone), isOffer_(true)
{
// hashValue_ is derived from the whole object, so it is computed in the body
// once every other member is initialized (as in the other constructors).
// NOLINTNEXTLINE(cppcoreguidelines-prefer-member-initializer)
hashValue_ = getHash(*this);
}
inline STPathElement::STPathElement(
std::optional<AccountID> const& account,
std::optional<PathAsset> const& asset,
std::optional<AccountID> const& issuer)
: type_(TypeNone)
{
if (!account)
{
isOffer_ = true;
}
else
if (account)
{
isOffer_ = false;
accountID_ = *account;
type_ |= TypeAccount;
type_ |= STPathElementType::Account;
XRPL_ASSERT(
accountID_ != noAccount(), "xrpl::STPathElement::STPathElement : account is set");
}
@@ -367,17 +363,15 @@ inline STPathElement::STPathElement(
if (asset)
{
assetID_ = *asset;
type_ |= assetID_.holds<Currency>() ? TypeCurrency : TypeMpt;
type_ |= assetID_.holds<Currency>() ? STPathElementType::Currency : STPathElementType::Mpt;
}
if (issuer)
{
issuerID_ = *issuer;
type_ |= TypeIssuer;
type_ |= STPathElementType::Issuer;
XRPL_ASSERT(issuerID_ != noAccount(), "xrpl::STPathElement::STPathElement : issuer is set");
}
hashValue_ = getHash(*this);
}
inline STPathElement::STPathElement(
@@ -385,108 +379,94 @@ inline STPathElement::STPathElement(
PathAsset const& asset,
AccountID const& issuer,
bool forceAsset)
: type_(TypeNone)
, accountID_(account)
, assetID_(asset)
, issuerID_(issuer)
, isOffer_(isXRP(accountID_))
: isOffer_(isXRP(account)), accountID_(account), assetID_(asset), issuerID_(issuer)
{
if (!isOffer_)
type_ |= TypeAccount;
type_ |= STPathElementType::Account;
if (forceAsset || !isXRP(assetID_))
type_ |= asset.holds<Currency>() ? TypeCurrency : TypeMpt;
type_ |= asset.holds<Currency>() ? STPathElementType::Currency : STPathElementType::Mpt;
if (!isXRP(issuer))
type_ |= TypeIssuer;
hashValue_ = getHash(*this);
type_ |= STPathElementType::Issuer;
}
inline STPathElement::STPathElement(
unsigned int uType,
STPathElementType type,
AccountID const& account,
PathAsset const& asset,
AccountID const& issuer)
: type_(uType)
, accountID_(account)
, assetID_(asset)
, issuerID_(issuer)
, isOffer_(isXRP(accountID_))
: type_(type), isOffer_(isXRP(account)), accountID_(account), assetID_(asset), issuerID_(issuer)
{
assetID_.visit(
[&](Currency const&) { type_ = type_ & (~Type::TypeMpt); },
[&](MPTID const&) { type_ = type_ & (~Type::TypeCurrency); });
// hashValue_ must be computed after type_ is adjusted above, so this cannot
// be a member initializer.
// NOLINTNEXTLINE(cppcoreguidelines-prefer-member-initializer)
hashValue_ = getHash(*this);
[&](Currency const&) { type_ &= ~STPathElementType::Mpt; },
[&](MPTID const&) { type_ &= ~STPathElementType::Currency; });
}
inline std::uint32_t
STPathElement::getNodeType() const
inline STPathElementType
STPathElement::getNodeType() const noexcept
{
return type_;
}
inline bool
STPathElement::isOffer() const
STPathElement::isOffer() const noexcept
{
return isOffer_;
}
inline bool
STPathElement::isAccount() const
STPathElement::isAccount() const noexcept
{
return !isOffer();
}
inline bool
STPathElement::isType(Type const& pe) const
STPathElement::isType(STPathElementType pe) const noexcept
{
return (type_ & pe) != 0u;
return (type_ & pe) != STPathElementType::None;
}
inline bool
STPathElement::hasIssuer() const
STPathElement::hasIssuer() const noexcept
{
return isType(STPathElement::TypeIssuer);
return isType(STPathElementType::Issuer);
}
inline bool
STPathElement::hasCurrency() const
STPathElement::hasCurrency() const noexcept
{
return isType(STPathElement::TypeCurrency);
return isType(STPathElementType::Currency);
}
inline bool
STPathElement::hasMPT() const
STPathElement::hasMPT() const noexcept
{
return isType(STPathElement::TypeMpt);
return isType(STPathElementType::Mpt);
}
inline bool
STPathElement::hasAsset() const
STPathElement::hasAsset() const noexcept
{
return isType(STPathElement::TypeAsset);
return isType(STPathElementType::Asset);
}
inline bool
STPathElement::isNone() const
STPathElement::isNone() const noexcept
{
return getNodeType() == STPathElement::TypeNone;
return getNodeType() == STPathElementType::None;
}
// Nodes are either an account ID or a offer prefix. Offer prefixs denote a
// class of offers.
inline AccountID const&
STPathElement::getAccountID() const
STPathElement::getAccountID() const noexcept
{
return accountID_;
}
inline PathAsset const&
STPathElement::getPathAsset() const
STPathElement::getPathAsset() const noexcept
{
return assetID_;
}
@@ -504,18 +484,11 @@ STPathElement::getMPTID() const
}
inline AccountID const&
STPathElement::getIssuerID() const
STPathElement::getIssuerID() const noexcept
{
return issuerID_;
}
inline bool
STPathElement::operator==(STPathElement const& t) const
{
return (type_ & TypeAccount) == (t.type_ & TypeAccount) && hashValue_ == t.hashValue_ &&
accountID_ == t.accountID_ && assetID_ == t.assetID_ && issuerID_ == t.issuerID_;
}
// ------------ STPath ------------
inline STPath::STPath(std::vector<STPathElement> p) : path_(std::move(p))
+2 -1
View File
@@ -133,7 +133,6 @@ enum TEMcodes : TERUnderlyingType {
temBAD_MPT,
temBAD_CIPHERTEXT,
temINVALID_BYTECODE,
temTEMP_DISABLED,
};
//------------------------------------------------------------------------------
@@ -184,6 +183,8 @@ enum TEFcodes : TERUnderlyingType {
tefBAD_PATH_COUNT,
tefNO_BYTECODE,
tefBYTECODE_NOT_INCLUDED,
tefTEMP_DISABLED,
tefEXCEEDS_LIMIT,
};
//------------------------------------------------------------------------------
+7 -13
View File
@@ -312,17 +312,12 @@ public:
lowerBound(UInt256 const& id) const;
/**
* Visit every node in this SHAMap.
* Visit every node in this SHAMap
*
* The walk ends at the first node it cannot read, so a caller that needs a
* complete walk to be correct must check the return value.
*
* @param function Called with every node visited. Returning false from it
* ends the walk, which on its own keeps the result true.
* @return True when the walk read every node it reached. False when a node
* could not be read.
* @param function called with every node visited.
* If function returns false, visitNodes exits.
*/
bool
void
visitNodes(std::function<bool(SHAMapTreeNode&)> const& function) const;
/**
@@ -336,12 +331,11 @@ public:
visitDifferences(SHAMap const* have, std::function<bool(SHAMapTreeNode const&)> const&) const;
/**
* Visit every leaf node in this SHAMap.
* Visit every leaf node in this SHAMap
*
* @param function Called with every non-inner node visited.
* @return What visitNodes reports about the walk.
* @param function called with every non inner node visited.
*/
bool
void
visitLeaves(std::function<void(boost::intrusive_ptr<SHAMapItem const> const&)> const&) const;
// comparison/sync functions
+2 -1
View File
@@ -51,7 +51,8 @@ public:
beast::Journal journal = beast::Journal{beast::Journal::getNullSink()})
: ApplyContext(registry, base, std::nullopt, tx, preclaimResult, baseFee, flags, journal)
{
XRPL_ASSERT((flags & TapBatch) == 0, "Batch apply flag should not be set");
XRPL_ASSERT(
(flags & ApplyFlags::Batch) == ApplyFlags::None, "Batch apply flag should not be set");
}
std::reference_wrapper<ServiceRegistry> registry;
+11 -8
View File
@@ -59,10 +59,11 @@ public:
, parentBatchId(parentBatchId)
, j(j)
{
XRPL_ASSERT((flags & TapBatch) == TapBatch, "Batch apply flag should be set");
XRPL_ASSERT(
(flags & ApplyFlags::Batch) == ApplyFlags::Batch, "Batch apply flag should be set");
XRPL_ASSERT_IF(
(flags & TapProposal) != TapNone,
(flags & TapDryRun) != TapNone,
(flags & ApplyFlags::Proposal) != ApplyFlags::None,
(flags & ApplyFlags::DryRun) != ApplyFlags::None,
"xrpl::PreflightContext : proposal preflight implies dry run");
}
@@ -74,10 +75,11 @@ public:
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: registry(registry), tx(tx), rules(std::move(rules)), flags(flags), j(j)
{
XRPL_ASSERT((flags & TapBatch) == 0, "Batch apply flag should not be set");
XRPL_ASSERT(
(flags & ApplyFlags::Batch) == ApplyFlags::None, "Batch apply flag should not be set");
XRPL_ASSERT_IF(
(flags & TapProposal) != TapNone,
(flags & TapDryRun) != TapNone,
(flags & ApplyFlags::Proposal) != ApplyFlags::None,
(flags & ApplyFlags::DryRun) != ApplyFlags::None,
"xrpl::PreflightContext : proposal preflight implies dry run");
}
@@ -116,7 +118,7 @@ public:
, j(j)
{
XRPL_ASSERT(
parentBatchId.has_value() == ((flags & TapBatch) == TapBatch),
parentBatchId.has_value() == ((flags & ApplyFlags::Batch) == ApplyFlags::Batch),
"Parent Batch ID should be set if batch apply flag is set");
}
@@ -129,7 +131,8 @@ public:
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
: PreclaimContext(registry, view, preflightResult, tx, flags, std::nullopt, j)
{
XRPL_ASSERT((flags & TapBatch) == 0, "Batch apply flag should not be set");
XRPL_ASSERT(
(flags & ApplyFlags::Batch) == ApplyFlags::None, "Batch apply flag should not be set");
}
PreclaimContext&
+1 -1
View File
@@ -41,7 +41,7 @@ struct ApplyResult
inline bool
isTecClaimHardFail(TER ter, ApplyFlags flags)
{
return isTecClaim(ter) && ((flags & TapRetry) == 0u);
return isTecClaim(ter) && ((flags & ApplyFlags::Retry) == ApplyFlags::None);
}
/**
@@ -0,0 +1,52 @@
#pragma once
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <cstdint>
#include <set>
#include <vector>
namespace xrpl {
class ReadView;
/**
* @brief Invariants for TransactionProposal ledger entries.
*
* - Only transaction types implementing proposal lifecycle operations may
* create or modify proposals, and a failed one leaves no proposal effect.
* - A proposal is deleted only by TransactionProposalCancel or together with
* the Ticket it is keyed to, and a deleted Ticket takes the proposal keyed
* to it along.
* - Collected signer arrays remain sorted, unique, and bounded.
*/
class ValidTransactionProposal
{
// The Ticket each deleted proposal was keyed to; a proposal may go with
// its Ticket, or by Cancel, and no other way.
std::vector<uint256> ticketsOfDeletedProposals_;
// The proposal each deleted Ticket was keyed to, whether or not one
// existed; none may survive its Ticket.
std::vector<Keylet> proposalsOfDeletedTickets_;
std::set<uint256> deletedTickets_;
std::uint32_t created_ = 0;
std::uint32_t modified_ = 0;
std::uint32_t deleted_ = 0;
bool invalidSignerArrays_ = false;
public:
void
visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after);
[[nodiscard]] bool
finalize(STTx const& tx, TER result, XRPAmount, ReadView const& view, beast::Journal const& j)
const;
};
} // namespace xrpl
@@ -9,6 +9,7 @@
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/invariants/AMMInvariant.h>
#include <xrpl/tx/invariants/CosignerInvariant.h>
#include <xrpl/tx/invariants/DirectoryInvariant.h>
#include <xrpl/tx/invariants/FreezeInvariant.h>
#include <xrpl/tx/invariants/LoanBrokerInvariant.h>
@@ -483,6 +484,7 @@ using InvariantChecks = std::tuple<
ValidAmounts,
ValidMPTTransfer,
ObjectHasPseudoAccount,
ValidTransactionProposal,
SponsorshipOwnerCountsMatch,
SponsorshipAccountCountMatchesField>;
@@ -20,11 +20,14 @@ public:
{
}
static bool
checkExtraFeatures(PreflightContext const& ctx);
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
static bool
checkExtraFeatures(PreflightContext const& ctx);
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
static NotTEC
preflight(PreflightContext const& ctx);
@@ -43,6 +43,9 @@ constexpr auto kProposalTxTypes = std::to_array<TxType>({
bool
isValidProposalTxnType(STObject const& proposedTx)
{
if (!proposedTx.isFieldPresent(sfTransactionType))
return false;
auto const type = proposedTx.getFieldU16(sfTransactionType);
if (std::ranges::find(kProposalTxTypes, type) != kProposalTxTypes.end())
@@ -4,6 +4,7 @@
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/EscrowHelpers.h>
#include <xrpl/ledger/helpers/OracleHelpers.h>
#include <xrpl/ledger/helpers/ProposalHelpers.h>
#include <xrpl/protocol/AccountID.h>
@@ -319,6 +320,8 @@ getLedgerEntryOwnerCount(SLE const& sle)
UNREACHABLE("AccountRoots are not supported by object sponsorship.");
return 0;
// LCOV_EXCL_STOP
case ltESCROW:
return calculateAdditionalReserve(sle[~sfBytecode]);
default:
return 1;
}
+62 -95
View File
@@ -1,11 +1,10 @@
#include <xrpl/protocol/STPathSet.h>
#include <xrpl/basics/CountedObject.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/UnorderedContainers.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/contract.h>
#include <xrpl/beast/hash/uhash.h>
#include <xrpl/basics/safe_cast.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/AccountID.h>
@@ -17,7 +16,9 @@
#include <xrpl/protocol/jss.h>
#include <algorithm>
#include <cstddef>
#include <concepts>
#include <cstdint>
#include <format>
#include <memory>
#include <stdexcept>
#include <utility>
@@ -25,41 +26,6 @@
namespace xrpl {
std::size_t
STPathElement::getHash(STPathElement const& element)
{
std::size_t hashAccount = 2654435761;
std::size_t hashCurrency = 2654435761;
std::size_t hashIssuer = 2654435761;
// NIKB NOTE: This doesn't have to be a secure hash as speed is more
// important. We don't even really need to fully hash the whole
// base_uint here, as a few bytes would do for our use.
//
// The note above is only true because the result of this function reaches
// nothing but STPathElement::operator==, where it is a fast-reject
// prefilter ahead of the field comparisons that decide the answer. Do not
// use it to key a container.
for (auto const x : element.getAccountID())
hashAccount += (hashAccount * 257) ^ x;
// Check pathAsset type instead of element's type_
// In some cases type_ might be account but the asset
// is still set to either MPT or currency (see Pathfinder::addLink())
element.getPathAsset().visit(
[&](MPTID const& mpt) { hashCurrency += beast::Uhash<>{}(mpt); },
[&](Currency const& currency) {
for (auto const x : currency)
hashCurrency += (hashCurrency * 509) ^ x;
});
for (auto const x : element.getIssuerID())
hashIssuer += (hashIssuer * 911) ^ x;
return (hashAccount ^ hashCurrency ^ hashIssuer);
}
// For guidance on deciding which option to pursue:
// 1. Try to decrease the size of the STPathSet first. For instance, if a std::optional was
// injected into the type, could you get the same functionality using a std::unique_ptr instead?
@@ -108,62 +74,57 @@ STPathSet::operator=(STPathSet const& other)
STPathSet::STPathSet(SerialIter& sit, SField const& name) : STBase(name)
{
std::vector<STPathElement> path;
for (;;)
{
int const iType = sit.get8();
if (iType == STPathElement::TypeNone || iType == STPathElement::TypeBoundary)
STPathElementType type = STPathElementType::None;
do
{
type = safeCast<STPathElementType>(sit.get8());
if (type == STPathElementType::None || type == STPathElementType::Boundary)
{
if (path.empty())
{
JLOG(debugLog().error()) << "Empty path in pathset";
Throw<std::runtime_error>("empty path");
}
Throw<std::runtime_error>("bad path element: empty path");
// Move rather than converting the vector to an STPath by copy.
value_.emplace_back(std::move(path));
path.clear();
if (iType == STPathElement::TypeNone)
return;
continue;
}
else if ((iType & ~STPathElement::TypeAll) != 0)
{
JLOG(debugLog().error()) << "Bad path element " << iType << " in pathset";
Throw<std::runtime_error>("bad path element");
}
else
{
auto const hasAccount = (iType & STPathElement::TypeAccount) != 0u;
auto const hasCurrency = (iType & STPathElement::TypeCurrency) != 0u;
auto const hasIssuer = (iType & STPathElement::TypeIssuer) != 0u;
auto const hasMPT = (iType & STPathElement::TypeMpt) != 0u;
AccountID account;
PathAsset asset;
AccountID issuer;
if ((type & ~STPathElementType::All) != STPathElementType::None)
Throw<std::runtime_error>(
std::format("bad path element: <{:#04x}>", std::to_underlying(type)));
if (hasAccount)
account = sit.get160();
bool const hasAccount = (type & STPathElementType::Account) == STPathElementType::Account;
bool const hasCurrency =
(type & STPathElementType::Currency) == STPathElementType::Currency;
bool const hasMPT = (type & STPathElementType::Mpt) == STPathElementType::Mpt;
bool const hasIssuer = (type & STPathElementType::Issuer) == STPathElementType::Issuer;
if (hasCurrency && hasMPT)
{
JLOG(debugLog().error()) << "Bad path element MPT and Currency in pathset";
Throw<std::runtime_error>("bad path element: MPT and Currency");
}
if (hasCurrency && hasMPT)
Throw<std::runtime_error>("bad path element: MPT and Currency");
if (hasCurrency)
asset = Currency::fromRaw(sit.get160());
AccountID account;
PathAsset asset;
AccountID issuer;
if (hasMPT)
asset = sit.get192();
if (hasAccount)
account = sit.get160();
if (hasIssuer)
issuer = sit.get160();
if (hasCurrency)
asset = Currency::fromRaw(sit.get160());
path.emplace_back(account, asset, issuer, hasCurrency || hasMPT);
}
}
if (hasMPT)
asset = sit.get192();
if (hasIssuer)
issuer = sit.get160();
path.emplace_back(account, asset, issuer, hasCurrency || hasMPT);
} while (type != STPathElementType::None);
}
STBase*
@@ -217,24 +178,22 @@ STPath::getJson(JsonOptions) const
for (auto const& it : path_)
{
json::Value elem(json::ValueType::Object);
auto const iType = it.getNodeType();
elem[jss::type] = iType;
elem[jss::type] = safeCast<std::uint32_t>(it.getNodeType());
if ((iType & STPathElement::TypeAccount) != 0u)
if (it.isType(STPathElementType::Account))
elem[jss::account] = to_string(it.getAccountID());
XRPL_ASSERT(
((iType & STPathElement::TypeCurrency) == 0u) ||
((iType & STPathElement::TypeMpt) == 0u),
!(it.hasCurrency() && it.hasMPT()),
"xrpl::STPath::getJson : not type Currency and MPT");
if ((iType & STPathElement::TypeCurrency) != 0u)
if (it.hasCurrency())
elem[jss::currency] = to_string(it.getCurrency());
if ((iType & STPathElement::TypeMpt) != 0u)
if (it.hasMPT())
elem[jss::mpt_issuance_id] = to_string(it.getMPTID());
if ((iType & STPathElement::TypeIssuer) != 0u)
if (it.hasIssuer())
elem[jss::issuer] = to_string(it.getIssuerID());
ret.append(elem);
@@ -266,34 +225,42 @@ STPathSet::add(Serializer& s) const
XRPL_ASSERT(getFName().fieldType == STI_PATHSET, "xrpl::STPathSet::add : valid field type");
bool first = true;
// Workaround, since `Serializer` does not accept strongly-typed enums
// yet. When support is added, the static_assert will trigger, and the
// lambda can be removed entirely.
auto const toByte = [](std::same_as<STPathElementType> auto t) noexcept {
static_assert(
!requires(Serializer& ser) { ser.add8(t); },
"Serializer::add8 now accepts STPathElementType directly; remove toByte.");
return std::to_underlying(t);
};
for (auto const& spPath : value_)
{
if (!first)
s.add8(STPathElement::TypeBoundary);
s.add8(toByte(STPathElementType::Boundary));
for (auto const& speElement : spPath)
{
int const iType = speElement.getNodeType();
s.add8(toByte(speElement.getNodeType()));
s.add8(iType);
if ((iType & STPathElement::TypeAccount) != 0u)
if (speElement.isType(STPathElementType::Account))
s.addBitString(speElement.getAccountID());
if ((iType & STPathElement::TypeMpt) != 0u)
if (speElement.hasMPT())
s.addBitString(speElement.getMPTID());
if ((iType & STPathElement::TypeCurrency) != 0u)
if (speElement.hasCurrency())
s.addBitString(speElement.getCurrency());
if ((iType & STPathElement::TypeIssuer) != 0u)
if (speElement.hasIssuer())
s.addBitString(speElement.getIssuerID());
}
first = false;
}
s.add8(STPathElement::TypeNone);
s.add8(toByte(STPathElementType::None));
}
} // namespace xrpl
+2 -1
View File
@@ -137,6 +137,8 @@ transResults()
MAKE_ERROR(tefBAD_PATH_COUNT, "Malformed: Too many paths."),
MAKE_ERROR(tefNO_BYTECODE, "There is no WASM code to run, but a WASM-specific field was included."),
MAKE_ERROR(tefBYTECODE_NOT_INCLUDED, "WASM code requires a field that was not included."),
MAKE_ERROR(tefTEMP_DISABLED, "The transaction requires logic that is currently temporarily disabled."),
MAKE_ERROR(tefEXCEEDS_LIMIT, "The transaction exceeds a limit set by fee voting."),
MAKE_ERROR(telLOCAL_ERROR, "Local failure."),
MAKE_ERROR(telBAD_DOMAIN, "Domain too long."),
@@ -209,7 +211,6 @@ transResults()
MAKE_ERROR(temINVALID_INNER_BATCH, "Malformed: Invalid inner batch transaction."),
MAKE_ERROR(temBAD_CIPHERTEXT, "Malformed: Invalid ciphertext."),
MAKE_ERROR(temINVALID_BYTECODE, "Malformed: Provided byte code is invalid."),
MAKE_ERROR(temTEMP_DISABLED, "The transaction requires logic that is currently temporarily disabled."),
MAKE_ERROR(terRETRY, "Retry transaction."),
MAKE_ERROR(terFUNDS_SPENT, "DEPRECATED."),
+4 -1
View File
@@ -343,6 +343,9 @@ SHAMap::descend(SHAMapInnerNode& parent, unsigned int branch) const
return node;
node = fetchNode(parent.getChildHash(branch));
if (!node)
return {};
node = parent.canonicalizeChild(branch, std::move(node));
return node;
}
@@ -354,7 +357,7 @@ SHAMap::descendNoStore(SHAMapInnerNode& parent, unsigned int branch) const
{
SHAMapTreeNodePtr ret = parent.getChild(branch);
if (!ret && backed_)
ret = fetchNodeNT(parent.getChildHash(branch));
ret = fetchNode(parent.getChildHash(branch));
return ret;
}
+14 -58
View File
@@ -13,11 +13,9 @@
#include <boost/smart_ptr/intrusive_ptr.hpp>
#include <array>
#include <exception>
#include <mutex>
#include <sstream>
#include <stack>
#include <string>
#include <thread>
#include <utility>
#include <vector>
@@ -285,36 +283,21 @@ bool
SHAMap::walkMapParallel(std::vector<SHAMapMissingNode>& missingNodes, int maxMissing) const
{
if (!root_->isInner()) // root_ is only node, and we have it
return true;
// Only the nodes this call records count towards the result, so remember what the
// caller already had.
auto const initialMissing = missingNodes.size();
return false;
using StackEntry = intr_ptr::SharedPtr<SHAMapInnerNode>;
std::array<SHAMapTreeNodePtr, SHAMapInnerNode::kBranchFactor> topChildren;
{
// This loop runs before the workers start, so it needs no lock.
auto const& innerRoot = intr_ptr::staticPointerCast<SHAMapInnerNode>(root_);
for (auto i = 0u; i < SHAMapInnerNode::kBranchFactor; ++i)
{
if (innerRoot->isEmptyBranch(i))
continue;
topChildren[i] = descendNoStore(*innerRoot, i);
if (!topChildren[i])
{
// A root child that cannot be read hides its whole subtree. Record it here,
// because the loop below skips a null child without visiting it.
missingNodes.emplace_back(type_, innerRoot->getChildHash(i));
if (--maxMissing <= 0)
return false;
}
if (!innerRoot->isEmptyBranch(i))
topChildren[i] = descendNoStore(*innerRoot, i);
}
}
std::vector<std::thread> workers;
workers.reserve(SHAMapInnerNode::kBranchFactor);
std::vector<std::string> exceptions;
std::vector<SHAMapMissingNode> exceptions;
exceptions.reserve(SHAMapInnerNode::kBranchFactor);
std::array<std::stack<StackEntry, std::vector<StackEntry>>, SHAMapInnerNode::kBranchFactor>
@@ -363,14 +346,6 @@ SHAMap::walkMapParallel(std::vector<SHAMapMissingNode>& missingNodes, int maxMis
else
{
std::scoped_lock const l{m};
// Another worker may have spent the budget while this one
// was walking, so test it before adding to the list and not
// only after. Testing only after lets every worker still
// running add one more node than the caller asked for.
if (maxMissing <= 0)
return;
missingNodes.emplace_back(type_, node->getChildHash(i));
if (--maxMissing <= 0)
return;
@@ -378,14 +353,10 @@ SHAMap::walkMapParallel(std::vector<SHAMapMissingNode>& missingNodes, int maxMis
}
}
}
catch (std::exception const& e)
catch (SHAMapMissingNode const& e)
{
// LCOV_EXCL_START
// A worker must not let an exception leave its thread, so record it
// and let the join below report it.
std::scoped_lock const l(m);
exceptions.emplace_back(e.what());
// LCOV_EXCL_STOP
exceptions.push_back(e);
}
},
std::move(nodeStacks[rootChildIndex]));
@@ -395,29 +366,14 @@ SHAMap::walkMapParallel(std::vector<SHAMapMissingNode>& missingNodes, int maxMis
worker.join();
std::scoped_lock const l(m);
if (!exceptions.empty())
{
// LCOV_EXCL_START
std::stringstream ss;
ss << "Exception(s) in ledger load: ";
for (auto const& e : exceptions)
ss << e << ", ";
JLOG(journal_.error()) << ss.str();
return false;
// LCOV_EXCL_STOP
}
// A node the workers could not read is recorded in `missingNodes` rather than thrown,
// so the result has to consult it too.
auto const found = missingNodes.size() - initialMissing;
if (found != 0)
{
JLOG(journal_.error()) << "Missing node(s) in ledger load: " << found
<< ", first: " << missingNodes[initialMissing].what();
return false;
}
return true;
if (exceptions.empty())
return true;
std::stringstream ss;
ss << "Exception(s) in ledger load: ";
for (auto const& e : exceptions)
ss << e.what() << ", ";
JLOG(journal_.error()) << ss.str();
return false;
}
} // namespace xrpl
+7 -26
View File
@@ -50,36 +50,28 @@ isLeafDepth(unsigned int depth)
} // namespace
bool
void
SHAMap::visitLeaves(
std::function<void(boost::intrusive_ptr<SHAMapItem const> const& item)> const& leafFunction)
const
{
return visitNodes([&leafFunction](SHAMapTreeNode& node) {
visitNodes([&leafFunction](SHAMapTreeNode& node) {
if (!node.isInner())
leafFunction(safeDowncast<SHAMapLeafNode&>(node).peekItem());
return true;
});
}
bool
void
SHAMap::visitNodes(std::function<bool(SHAMapTreeNode&)> const& function) const
{
if (!root_)
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::SHAMap::visitNodes : non-null root");
return true;
// LCOV_EXCL_STOP
}
return;
// The root is readable by definition, so a stop here still reports a complete walk.
if (!function(*root_))
return true;
function(*root_);
// A map whose root is a leaf holds one node, and this walk just visited it.
if (!root_->isInner())
return true;
return;
using StackEntry = std::pair<unsigned int, intr_ptr::SharedPtr<SHAMapInnerNode>>;
std::stack<StackEntry, std::vector<StackEntry>> stack;
@@ -94,17 +86,8 @@ SHAMap::visitNodes(std::function<bool(SHAMapTreeNode&)> const& function) const
if (!node->isEmptyBranch(pos))
{
SHAMapTreeNodePtr const child = descendNoStore(*node, pos);
if (!child)
{
// No caller can use a partial walk, so end it here rather than pay
// for the rest of the map. The callers that act on the result log
// it at a level an operator reads, so keep this one at debug.
JLOG(journal_.debug())
<< "visitNodes: unreadable child node " << node->getChildHash(pos);
return false;
}
if (!function(*child))
return true;
return;
if (child->isLeaf())
{
@@ -139,8 +122,6 @@ SHAMap::visitNodes(std::function<bool(SHAMapTreeNode&)> const& function) const
std::tie(pos, node) = stack.top();
stack.pop();
}
return true;
}
void
+4 -3
View File
@@ -38,7 +38,7 @@ ApplyContext::ApplyContext(
, parentBatchId_(parentBatchId)
{
XRPL_ASSERT(
parentBatchId.has_value() == ((flags_ & TapBatch) == TapBatch),
parentBatchId.has_value() == ((flags_ & ApplyFlags::Batch) == ApplyFlags::Batch),
"Parent Batch ID should be set if batch apply flag is set");
view_.emplace(&base_, flags_);
}
@@ -53,13 +53,14 @@ std::optional<TxMeta>
ApplyContext::apply(TER ter)
{
// NOLINTNEXTLINE(bugprone-unchecked-optional-access) view_ emplaced in constructor
return view_->apply(base_, tx, ter, parentBatchId_, (flags_ & TapDryRun) != 0u, journal);
return view_->apply(
base_, tx, ter, parentBatchId_, (flags_ & ApplyFlags::DryRun) != ApplyFlags::None, journal);
}
void
ApplyContext::addOrderBook(Book const& book)
{
if ((flags_ & TapDryRun) == TapNone)
if ((flags_ & ApplyFlags::DryRun) == ApplyFlags::None)
registry.get().getOrderBookDB().addOrderBook(book);
}
+9 -7
View File
@@ -135,7 +135,7 @@ preflightCheckSigningKey(STObject const& sigObject, beast::Journal j)
std::optional<NotTEC>
preflightCheckSimulateKeys(ApplyFlags flags, STObject const& sigObject, beast::Journal j)
{
if ((flags & TapDryRun) != 0u) // simulation
if ((flags & ApplyFlags::DryRun) != ApplyFlags::None) // simulation
{
std::optional<Slice> const signature = sigObject[~sfTxnSignature];
if (signature && !signature->empty())
@@ -510,7 +510,8 @@ Transactor::minimumFee(
Fees const& fees,
ApplyFlags flags)
{
return scaleFeeLoad(baseFee, registry.getFeeTrack(), fees, (flags & TapUnlimited) != 0u);
return scaleFeeLoad(
baseFee, registry.getFeeTrack(), fees, (flags & ApplyFlags::Unlimited) != ApplyFlags::None);
}
TER
@@ -521,7 +522,7 @@ Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee)
auto const feePaid = ctx.tx[sfFee].xrp();
if ((ctx.flags & TapBatch) != 0u)
if ((ctx.flags & ApplyFlags::Batch) != ApplyFlags::None)
{
if (feePaid == beast::kZero)
return tesSUCCESS;
@@ -984,7 +985,8 @@ Transactor::checkSign(
return tesSUCCESS;
}
if (((flags & TapDryRun) != 0u) && pkSigner.empty() && !sigObject.isFieldPresent(sfSigners))
if (((flags & ApplyFlags::DryRun) != ApplyFlags::None) && pkSigner.empty() &&
!sigObject.isFieldPresent(sfSigners))
{
// simulate: skip signature validation when neither SigningPubKey nor
// Signers are provided
@@ -1155,7 +1157,7 @@ Transactor::checkMultiSign(
}
XRPL_ASSERT(
(flags & TapDryRun) || !spk.empty(),
(flags & ApplyFlags::DryRun) != ApplyFlags::None || !spk.empty(),
"xrpl::Transactor::checkMultiSign : non-empty signer or "
"simulation");
AccountID const signingAcctIDFromPubKey =
@@ -1643,7 +1645,7 @@ Transactor::operator()()
if (ctx_.size() > kOversizeMetaDataCap)
result = tecOVERSIZE;
if (isTecClaim(result) && ((view().flags() & TapFailHard) != 0u))
if (isTecClaim(result) && ((view().flags() & ApplyFlags::FailHard) != ApplyFlags::None))
{
// If the TapFailHard flag is set, a tec result
// must not do anything
@@ -1723,7 +1725,7 @@ Transactor::operator()()
// Once we call apply, we will no longer be able to look at view()
metadata = ctx_.apply(result);
if ((ctx_.flags() & TapDryRun) != 0u)
if ((ctx_.flags() & ApplyFlags::DryRun) != ApplyFlags::None)
return logger(result, false, std::move(metadata));
return logger(result, canApply, std::move(metadata));
+2 -2
View File
@@ -215,7 +215,7 @@ applyBatchTransactions(
auto const applyOneTransaction = [&registry, &j, &parentBatchId, &batchView](STTx const& tx) {
OpenView perTxBatchView(kBatchView, batchView);
auto const ret = apply(registry, perTxBatchView, parentBatchId, tx, TapBatch, j);
auto const ret = apply(registry, perTxBatchView, parentBatchId, tx, ApplyFlags::Batch, j);
XRPL_ASSERT(
ret.applied == (isTesSuccess(ret.ter) || isTecClaim(ret.ter)),
"Inner transaction should not be applied");
@@ -274,7 +274,7 @@ applyTransaction(
{
// Returns false if the transaction has need not be retried.
if (retryAssured)
flags = flags | TapRetry;
flags = flags | ApplyFlags::Retry;
JLOG(j.debug()) << "TXN " << txn.getTransactionID() << (retryAssured ? "/retry" : "/final");
+6 -2
View File
@@ -31,6 +31,8 @@
#include <xrpl/protocol/detail/transactions.macro>
#undef TRANSACTION
#undef TRANSACTION_INCLUDE
#pragma pop_macro("TRANSACTION")
// DO NOT INCLUDE TRANSACTOR HEADER FILES HERE.
@@ -346,7 +348,8 @@ preflight(
// builds; re-check the same invariant here so a release build can't
// silently skip a proposed transaction's signature-presence checks
// outside of a dry run.
if ((flags & TapProposal) != TapNone && (flags & TapDryRun) == TapNone)
if ((flags & ApplyFlags::Proposal) != ApplyFlags::None &&
(flags & ApplyFlags::DryRun) == ApplyFlags::None)
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "apply (preflight): TapProposal set without TapDryRun.";
@@ -377,7 +380,8 @@ preflight(
PreflightContext const pfCtx(registry, tx, parentBatchId, rules, flags, j);
// See the comment in the other preflight() overload above.
if ((flags & TapProposal) != TapNone && (flags & TapDryRun) == TapNone)
if ((flags & ApplyFlags::Proposal) != ApplyFlags::None &&
(flags & ApplyFlags::DryRun) == ApplyFlags::None)
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "apply (preflight): TapProposal set without TapDryRun.";
@@ -0,0 +1,225 @@
#include <xrpl/tx/invariants/CosignerInvariant.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/SeqProxy.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/XRPAmount.h>
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <optional>
namespace xrpl {
namespace {
bool
validSignerArray(STArray const& signers, std::size_t limit, SField const& elementName)
{
if (signers.size() > limit)
return false;
std::optional<AccountID> previous;
for (auto const& signer : signers)
{
if (signer.getFName() != elementName || !signer.isFieldPresent(sfAccount))
return false;
auto const account = signer.getAccountID(sfAccount);
if (previous && account <= *previous)
return false;
previous = account;
}
return true;
}
bool
validNestedSigners(STObject const& signature)
{
return !signature.isFieldPresent(sfSigners) ||
validSignerArray(signature.getFieldArray(sfSigners), STTx::kMaxMultiSigners, sfSigner);
}
bool
validSignerArrays(STObject const& proposedTx)
{
if (!validNestedSigners(proposedTx))
return false;
for (auto const* field :
std::array<SField const*, 2>{&sfCounterpartySignature, &sfSponsorSignature})
{
if (proposedTx.isFieldPresent(*field) &&
!validNestedSigners(proposedTx.getFieldObject(*field)))
return false;
}
if (!proposedTx.isFieldPresent(sfBatchSigners))
return true;
auto const& batchSigners = proposedTx.getFieldArray(sfBatchSigners);
if (!validSignerArray(batchSigners, kMaxBatchSigners, sfBatchSigner))
return false;
return std::ranges::all_of(
batchSigners, [](auto const& batchSigner) { return validNestedSigners(batchSigner); });
}
// The Ticket a proposal is keyed to: the one its proposed transaction spends.
uint256
proposalTicketKey(SLE const& proposal)
{
auto const& proposedTx = proposal.getFieldObject(sfProposedTransaction);
return keylet::ticket(
proposedTx.getAccountID(sfAccount),
SeqProxy::rawTicket(proposedTx.getFieldU32(sfTicketSequence)))
.key;
}
} // namespace
void
ValidTransactionProposal::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after)
{
// `after` is always present (on deletion it holds the final state), so it
// alone determines the entry type. LedgerEntryTypesMatch owns malformed
// type transitions; skip them rather than read one side with the other's
// schema.
if (!after || (before && before->getType() != after->getType()))
return;
auto const type = after->getType();
// Consuming a Ticket deletes the proposal keyed to it (XLS-0103 §4.5).
// Remember each Ticket that went, so finalize can tell a proposal deleted
// with its Ticket from one that simply vanished, and the proposal each
// Ticket was keyed to, so finalize can check that none stayed behind.
if (type == ltTICKET)
{
if (isDelete)
{
deletedTickets_.insert(after->key());
proposalsOfDeletedTickets_.push_back(
keylet::txProposal(
after->getAccountID(sfAccount), after->getFieldU32(sfTicketSequence)));
}
return;
}
if (type != ltTRANSACTION_PROPOSAL)
return;
if (!before)
{
++created_;
}
else if (isDelete)
{
++deleted_;
ticketsOfDeletedProposals_.push_back(proposalTicketKey(*before));
}
else
{
++modified_;
}
// A live proposal's signer lists must stay submittable: within size
// limits, sorted by account, and free of duplicates.
if (!isDelete && !validSignerArrays(after->getFieldObject(sfProposedTransaction)))
invalidSignerArrays_ = true;
}
bool
ValidTransactionProposal::finalize(
STTx const& tx,
TER result,
XRPAmount,
ReadView const& view,
beast::Journal const& j) const
{
if (!view.rules().enabled(featureCosign))
return true;
if (invalidSignerArrays_)
{
JLOG(j.fatal()) << "Invariant failed: TransactionProposal signer arrays are not canonical.";
return false;
}
// A proposal leaves the ledger in two ways (XLS-0103 §4.5): a
// TransactionProposalCancel deletes the proposal it names, and consuming
// the Ticket a proposal is keyed to deletes the proposal with it, whether
// the proposed transaction executed (or failed with a claimed-fee tec) or
// AccountDelete swept the target's Tickets.
std::uint32_t deletedWithTicket = 0;
for (auto const& ticketKey : ticketsOfDeletedProposals_)
{
if (deletedTickets_.contains(ticketKey))
++deletedWithTicket;
}
bool const succeeded = isTesSuccess(result);
bool effectsMatch = false;
if (tx.getTxnType() == ttTRANSACTION_PROPOSAL_CREATE)
{
effectsMatch = created_ == (succeeded ? 1u : 0u) && modified_ == 0 && deleted_ == 0;
}
else if (tx.getTxnType() == ttTRANSACTION_PROPOSAL_CANCEL)
{
// A Cancel deletes one proposal and nothing else. It cannot
// spend a reserved Ticket, so no proposal goes with its Ticket here.
effectsMatch = created_ == 0 && modified_ == 0 && deletedWithTicket == 0 &&
deleted_ == (succeeded ? 1u : 0u);
}
else
{
// Any other transaction deletes a proposal only together with its
// Ticket, and modifies at most one, only as a successful
// SponsorshipTransfer.
// TODO: extend this when TransactionProposalSign is implemented.
std::uint32_t const modifiable =
tx.getTxnType() == ttSPONSORSHIP_TRANSFER && succeeded ? 1 : 0;
effectsMatch = created_ == 0 && modified_ <= modifiable && deleted_ == deletedWithTicket;
}
if (!effectsMatch)
{
JLOG(j.fatal())
<< "Invariant failed: TransactionProposal changes do not match transaction result.";
return false;
}
// The other direction of the deletion rule: ticketDelete deletes the
// proposal keyed to a Ticket along with it (XLS-0103 §4.5), so no
// proposal may outlive its Ticket, orphaned on a key that can no longer
// be spent.
bool const orphaned = std::ranges::any_of(
proposalsOfDeletedTickets_, [&view](Keylet const& keylet) { return view.exists(keylet); });
if (orphaned)
{
JLOG(j.fatal())
<< "Invariant failed: TransactionProposal outlived the Ticket it is keyed to.";
return false;
}
return true;
}
} // namespace xrpl
+5 -1
View File
@@ -935,8 +935,12 @@ ValidClawback::finalize(
return !mptV2Enabled;
}
// IOU balances keep 16 significant digits, so a clawback
// much smaller than the balance changes it by a rounded
// amount, possibly zero. Compare with the same rounded
// subtraction the balance update performs.
if (*afterBalance > *beforeBalance ||
(*beforeBalance - *afterBalance) != std::min(*beforeBalance, clawAmount))
*afterBalance != *beforeBalance - std::min(*beforeBalance, clawAmount))
{
JLOG(j.fatal())
<< "Invariant failed: trustline clawback balance change is invalid";
+57 -16
View File
@@ -254,6 +254,15 @@ private:
TAmounts<TIn, TOut> const& stepAmt,
TOut const& ownerGives) const;
template <template <typename, typename> typename Offer>
void
tryConsumeOffer(
PaymentSandbox& sb,
Offer<TIn, TOut>& offer,
TAmounts<TIn, TOut> const& ofrAmt,
TAmounts<TIn, TOut> const& stepAmt,
TOut const& ownerGives) const;
// If clobQuality is available and has a better quality then return nullopt,
// otherwise if amm liquidity is available return AMM offer adjusted based
// on clobQuality.
@@ -810,10 +819,11 @@ BookStep<TIn, TOut, TDerived>::forEachOffer(
auto const amtIn = [&](TIn const& in) { return capInMPT && *maxIn_ < in ? *maxIn_ : in; };
try
{
// All arithmetic in this block runs before the offer is consumed.
// A crafted MPTokensV2 offer can overflow while transfer rates or
// crossing limits are applied; remove that unusable offer instead
// of letting it persist as a tecINTERNAL source.
// crossing limits are applied, here or in the callback's
// limitStepIn()/limitStepOut(); remove that unusable offer instead
// of letting it persist as a tecINTERNAL source. Overflows while
// consuming the offer are handled by tryConsumeOffer().
stpAmt.in = mulRatio(amtIn(ofrAmt.in), ofrInRate, QUALITY_ONE, /*roundUp*/ true);
// owner pays the transfer fee. An MPT gross-up that doesn't fit in
@@ -941,16 +951,9 @@ BookStep<TIn, TOut, TDerived>::forEachOffer(
removeOffer("Removing offer with overflowing amount calculation");
return true;
}
// An overflow can only be produced by a crafted MPT offer, and MPT
// offers require featureMPTokensV2 (enforced at OfferCreate
// preflight). So the amendment is always enabled when we get here
// and this legacy re-throw is unreachable in practice.
// LCOV_EXCL_START
XRPL_ASSERT(
sb.rules().enabled(featureMPTokensV2),
"xrpl::BookStep::forEachOffer : overflow implies MPTokensV2");
// Legacy behavior: the exception escapes flow() and the
// transaction fails with tecINTERNAL.
throw;
// LCOV_EXCL_STOP
}
};
@@ -991,6 +994,32 @@ BookStep<TIn, TOut, TDerived>::forEachOffer(
return {offers.permToRemove(), counter.count()};
}
template <class TIn, class TOut, class TDerived>
template <template <typename, typename> typename Offer>
void
BookStep<TIn, TOut, TDerived>::tryConsumeOffer(
PaymentSandbox& sb,
Offer<TIn, TOut>& offer,
TAmounts<TIn, TOut> const& ofrAmt,
TAmounts<TIn, TOut> const& stepAmt,
TOut const& ownerGives) const
{
try
{
// Under MPTokensV2, an overflow while consuming the offer fails the
// strand instead of being handled by forEachOffer().
consumeOffer(sb, offer, ofrAmt, stepAmt, ownerGives);
}
catch (std::overflow_error const&)
{
if (sb.rules().enabled(featureMPTokensV2))
{
Throw<FlowException>(tecINTERNAL, "Overflow while consuming offer");
}
throw;
}
}
template <class TIn, class TOut, class TDerived>
template <template <typename, typename> typename Offer>
void
@@ -1072,7 +1101,7 @@ std::optional<std::variant<Quality, AMMOffer<TIn, TOut>>>
BookStep<TIn, TOut, TDerived>::tip(ReadView const& view) const
{
// This can be simplified (and sped up) if directories are never empty.
Sandbox sb(&view, TapNone);
Sandbox sb(&view, ApplyFlags::None);
BookTip bt(sb, book_);
auto const lobQuality = bt.step(j_) ? std::optional<Quality>(bt.quality()) : std::nullopt;
// Multi-path offer generates an offer with the quality
@@ -1192,7 +1221,7 @@ BookStep<TIn, TOut, TDerived>::revImp(
savedOuts.insert(stpAmt.out);
result = TAmounts<TIn, TOut>(sum(savedIns), sum(savedOuts));
remainingOut = out - result.out;
this->consumeOffer(sb, offer, ofrAmt, stpAmt, ownerGives);
this->tryConsumeOffer(sb, offer, ofrAmt, stpAmt, ownerGives);
// return true b/c even if the payment is satisfied,
// we need to consume the offer. An offer limited by what the
// issuer can issue is not consumed, though: its remainder is still
@@ -1224,7 +1253,7 @@ BookStep<TIn, TOut, TDerived>::revImp(
savedOuts.insert(remainingOut);
result.in = sum(savedIns);
result.out = out;
this->consumeOffer(sb, offer, ofrAdjAmt, stpAdjAmt, ownerGivesAdj);
this->tryConsumeOffer(sb, offer, ofrAdjAmt, stpAdjAmt, ownerGivesAdj);
// Explicitly check whether the offer is funded. Given that we have
// (stpAmt.out > remainingOut), it's natural to assume the offer
@@ -1408,7 +1437,7 @@ BookStep<TIn, TOut, TDerived>::fwdImp(
savedOuts = std::move(savedOutsAdj);
result = resultAdj;
remainingIn = in - result.in;
this->consumeOffer(sb, offer, ofrAdjAmt, stpAdjAmt, ownerGivesAdj);
this->tryConsumeOffer(sb, offer, ofrAdjAmt, stpAdjAmt, ownerGivesAdj);
// When the mantissas of two iou amounts differ by less than ten, then
// subtracting them leaves a result of zero. This can cause the check
@@ -1512,6 +1541,18 @@ BookStep<TIn, TOut, TDerived>::check(StrandContext const& ctx) const
return temBAD_PATH;
}
// An issuer paying with its own MPT issues it into the first book. A
// holder-sourced strand records the MPT in the first MPTEndpointStep and
// so rejects a later book that outputs it again; do the same here. The
// step after such a book issues the MPT a second time and the payment
// engine cannot keep the two within MaximumAmount once StrandFlow
// rebuilds the sandbox on a limiting step.
if (ctx.isFirst && book_.in.holds<MPTIssue>() && book_.in.getIssuer() == ctx.strandSrc &&
ctx.view.rules().enabled(featureMPTokensV2))
{
ctx.seenDirectAssets[0].insert(book_.in);
}
// Do not allow two books to output the same issue. This may cause offers on
// one step to unfund offers in another step.
if (!ctx.seenBookOuts.insert(book_.out).second ||
+6 -2
View File
@@ -6,6 +6,7 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/PaymentSandbox.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AccountID.h>
@@ -874,15 +875,18 @@ MPTEndpointStep<TDerived>::check(StrandContext const& ctx) const
// pure issue/redeem can't be frozen (issuer/holder)
// For the first step: check global freeze of the step's own asset.
// For the last step: check only the per-holder MPToken lock.
// For the last step: check only the per-holder lock.
// Global freeze of the deliver asset is not checked here
// because MPT semantics allow issuer<->holder transfers even when globally
// locked — only holder-to-holder DEX paths are restricted.
// For both steps: a vault share inherits the freeze of the vault's
// underlying asset.
if (!(ctx.isLast && ctx.isFirst))
{
auto const& account = ctx.isFirst ? src_ : dst_;
bool const frozen = (ctx.isFirst && isGlobalFrozen(ctx.view, mptIssue_)) ||
isIndividualFrozen(ctx.view, account, mptIssue_);
isIndividualFrozen(ctx.view, account, mptIssue_) ||
isVaultPseudoAccountFrozen(ctx.view, account, mptIssue_, 0);
if (frozen)
return terLOCKED;
}
+18 -32
View File
@@ -57,10 +57,8 @@ checkNear(IOUAmount const& expected, IOUAmount const& actual)
static bool
isXRPAccount(STPathElement const& pe)
{
if (pe.getNodeType() != STPathElement::TypeAccount)
return false;
return isXRP(pe.getAccountID());
};
return pe.getNodeType() == STPathElementType::Account && isXRP(pe.getAccountID());
}
static std::pair<TER, std::unique_ptr<Step>>
toStep(
@@ -71,11 +69,8 @@ toStep(
{
auto& j = ctx.j;
if (ctx.isFirst && e1->isAccount() &&
((e1->getNodeType() & STPathElement::TypeCurrency) != 0u) && e1->getPathAsset().isXRP())
{
if (ctx.isFirst && e1->isAccount() && e1->hasCurrency() && e1->getPathAsset().isXRP())
return makeXrpEndpointStep(ctx, e1->getAccountID());
}
if (ctx.isLast && isXRPAccount(*e1) && e2->isAccount())
return makeXrpEndpointStep(ctx, e2->getAccountID());
@@ -117,15 +112,9 @@ toStep(
// LCOV_EXCL_STOP
}
XRPL_ASSERT(
(e2->getNodeType() & STPathElement::TypeAsset) ||
(e2->getNodeType() & STPathElement::TypeIssuer),
"xrpl::toStep : currency or issuer");
PathAsset const outAsset =
((e2->getNodeType() & STPathElement::TypeAsset) != 0u) ? e2->getPathAsset() : curAsset;
auto const outIssuer = ((e2->getNodeType() & STPathElement::TypeIssuer) != 0u)
? e2->getIssuerID()
: curAsset.getIssuer();
XRPL_ASSERT(e2->hasAsset() || e2->hasIssuer(), "xrpl::toStep : currency or issuer");
PathAsset const outAsset = e2->hasAsset() ? e2->getPathAsset() : curAsset;
auto const outIssuer = e2->hasIssuer() ? e2->getIssuerID() : curAsset.getIssuer();
if (isXRP(curAsset) && outAsset.isXRP())
{
@@ -197,16 +186,15 @@ toStrand(
for (std::size_t i = 0; i < path.size(); ++i)
{
auto const& pe = path[i];
auto const t = pe.getNodeType();
if (((t & ~STPathElement::TypeAll) != 0u) || (t == 0u))
if ((pe.getNodeType() & ~STPathElementType::All) != STPathElementType::None || pe.isNone())
return {temBAD_PATH, Strand{}};
bool const hasAccount = (t & STPathElement::TypeAccount) != 0u;
bool const hasIssuer = (t & STPathElement::TypeIssuer) != 0u;
bool const hasCurrency = (t & STPathElement::TypeCurrency) != 0u;
bool const hasMPT = (t & STPathElement::TypeMpt) != 0u;
bool const hasAsset = (t & STPathElement::TypeAsset) != 0u;
bool const hasAccount = pe.isType(STPathElementType::Account);
bool const hasIssuer = pe.hasIssuer();
bool const hasCurrency = pe.hasCurrency();
bool const hasMPT = pe.hasMPT();
bool const hasAsset = pe.hasAsset();
if (hasAccount && (hasIssuer || hasCurrency))
return {temBAD_PATH, Strand{}};
@@ -250,9 +238,7 @@ toStrand(
}();
// Currency or MPT
auto const hasAsset = [](STPathElement const pe) {
return pe.getNodeType() & STPathElement::TypeAsset;
};
auto const hasAsset = [](STPathElement const& pe) { return pe.hasAsset(); };
std::vector<STPathElement> normPath;
// reserve enough for the path, the implied source, destination,
@@ -263,10 +249,10 @@ toStrand(
// transaction, as defined by the transaction's Account field. The Asset
// is either SendMax or Deliver.
auto const t = [&] {
auto const t = STPathElement::TypeAccount | STPathElement::TypeIssuer;
auto const t = STPathElementType::Account | STPathElementType::Issuer;
return curAsset.visit(
[&](MPTIssue const&) { return t | STPathElement::TypeMpt; },
[&](Issue const&) { return t | STPathElement::TypeCurrency; });
[&](MPTIssue const&) { return t | STPathElementType::Mpt; },
[&](Issue const&) { return t | STPathElementType::Currency; });
}();
// If MPT then the issuer is the actual issuer, it is never the source
// account.
@@ -435,7 +421,7 @@ toStrand(
return {msr.first, Strand{}};
result.push_back(std::move(msr.second));
impliedPE.emplace(
STPathElement::TypeAccount, curAsset.getIssuer(), xrpCurrency(), xrpAccount());
STPathElementType::Account, curAsset.getIssuer(), xrpCurrency(), xrpAccount());
cur = &*impliedPE;
}
}
@@ -450,7 +436,7 @@ toStrand(
return {msr.first, Strand{}};
result.push_back(std::move(msr.second));
impliedPE.emplace(
STPathElement::TypeAccount, curAsset.getIssuer(), xrpCurrency(), xrpAccount());
STPathElementType::Account, curAsset.getIssuer(), xrpCurrency(), xrpAccount());
cur = &*impliedPE;
}
}
@@ -187,7 +187,8 @@ AccountSet::preclaim(PreclaimContext const& ctx)
if (!dirIsEmpty(ctx.view, keylet::ownerDir(id)))
{
JLOG(ctx.j.trace()) << "Retry: Owner directory not empty.";
return ((ctx.flags & TapRetry) != 0u) ? TER{terOWNERS} : TER{tecOWNERS};
return ((ctx.flags & ApplyFlags::Retry) != ApplyFlags::None) ? TER{terOWNERS}
: TER{tecOWNERS};
}
}
@@ -297,7 +297,8 @@ OfferCreate::checkAcceptAsset(
JLOG(j.debug()) << "delay: can't receive IOUs from non-existent issuer: "
<< to_string(asset.getIssuer());
return ((flags & TapRetry) != 0u) ? TER{terNO_ACCOUNT} : TER{tecNO_ISSUER};
return ((flags & ApplyFlags::Retry) != ApplyFlags::None) ? TER{terNO_ACCOUNT}
: TER{tecNO_ISSUER};
}
// An account cannot create a trustline to itself, so no line can exist
@@ -320,7 +321,8 @@ OfferCreate::checkAcceptAsset(
{
JLOG(j.debug()) << "delay: can't receive IOUs from issuer with "
"DisallowIncomingTrustline set";
return ((flags & TapRetry) != 0u) ? TER{terNO_LINE} : TER{tecNO_LINE};
return ((flags & ApplyFlags::Retry) != ApplyFlags::None) ? TER{terNO_LINE}
: TER{tecNO_LINE};
}
}
@@ -328,7 +330,8 @@ OfferCreate::checkAcceptAsset(
{
if (!trustLine)
{
return ((flags & TapRetry) != 0u) ? TER{terNO_LINE} : TER{tecNO_LINE};
return ((flags & ApplyFlags::Retry) != ApplyFlags::None) ? TER{terNO_LINE}
: TER{tecNO_LINE};
}
// Entries have a canonical representation, determined by a
@@ -344,7 +347,8 @@ OfferCreate::checkAcceptAsset(
JLOG(j.debug()) << "delay: can't receive IOUs from "
"issuer without auth.";
return ((flags & TapRetry) != 0u) ? TER{terNO_AUTH} : TER{tecNO_AUTH};
return ((flags & ApplyFlags::Retry) != ApplyFlags::None) ? TER{terNO_AUTH}
: TER{tecNO_AUTH};
}
}
@@ -110,11 +110,16 @@ EscrowCancel::doApply()
auto const sle = ctx_.view().peek(keylet::account(account));
STAmount const amount = escrow->getFieldAmount(sfAmount);
auto const reserveToSubtract = calculateAdditionalReserve((*escrow)[~sfBytecode]);
// The return can re-create a holding the owner deleted while the escrow
// was pending; the removed escrow must not be counted against its reserve.
bool const recycleReserve = ctx_.view().rules().enabled(fixCleanup3_4_0);
if (recycleReserve)
decreaseOwnerCountForObject(ctx_.view(), sle, escrow.mutableRawSle(), 1, ctx_.journal);
{
decreaseOwnerCountForObject(
ctx_.view(), sle, escrow.mutableRawSle(), reserveToSubtract, ctx_.journal);
}
// Transfer amount back to the owner
if (isXRP(amount))
@@ -160,7 +165,10 @@ EscrowCancel::doApply()
}
if (!recycleReserve)
decreaseOwnerCountForObject(ctx_.view(), sle, escrow.mutableRawSle(), 1, ctx_.journal);
{
decreaseOwnerCountForObject(
ctx_.view(), sle, escrow.mutableRawSle(), reserveToSubtract, ctx_.journal);
}
// Remove escrow from ledger
escrow.erase();
@@ -4,8 +4,8 @@
#include <xrpl/basics/chrono.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/conditions/Condition.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/entries/EscrowEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
@@ -16,6 +16,7 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Concepts.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Fees.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/Keylet.h>
@@ -130,6 +131,19 @@ escrowCreatePreflightHelper<MPTIssue>(PreflightContext const& ctx)
return tesSUCCESS;
}
XRPAmount
EscrowCreate::calculateBaseFee(ReadView const& view, STTx const& tx)
{
XRPAmount txnFees{Transactor::calculateBaseFee(view, tx)};
if (tx.isFieldPresent(sfBytecode))
{
// Ten base fees in all: `Transactor::calculateBaseFee` charges one,
// this adds nine. Plus 5 drops per byte.
txnFees += 9 * view.fees().base + 5 * tx[sfBytecode].size();
}
return txnFees;
}
NotTEC
EscrowCreate::preflight(PreflightContext const& ctx)
{
@@ -161,12 +175,19 @@ EscrowCreate::preflight(PreflightContext const& ctx)
ctx.tx[sfCancelAfter] <= ctx.tx[sfFinishAfter])
return temBAD_EXPIRATION;
if (ctx.tx.isFieldPresent(sfBytecode) && !ctx.tx.isFieldPresent(sfCancelAfter))
return temBAD_EXPIRATION;
// In the absence of a FinishAfter, the escrow can be finished
// immediately, which can be confusing. When creating an escrow,
// we want to ensure that either a FinishAfter time is explicitly
// specified or a completion condition is attached.
if (!ctx.tx[~sfFinishAfter] && !ctx.tx[~sfCondition])
if (!ctx.tx[~sfFinishAfter] && !ctx.tx[~sfCondition] && !ctx.tx[~sfBytecode])
{
JLOG(ctx.j.debug()) << "Must have at least one of FinishAfter, "
"Condition, or Bytecode.";
return temMALFORMED;
}
if (auto const cb = ctx.tx[~sfCondition])
{
@@ -182,6 +203,33 @@ EscrowCreate::preflight(PreflightContext const& ctx)
}
}
if (ctx.tx.isFieldPresent(sfData))
{
if (!ctx.tx.isFieldPresent(sfBytecode))
{
JLOG(ctx.j.debug()) << "EscrowCreate with Data requires Bytecode";
return temMALFORMED;
}
auto const data = ctx.tx.getFieldVL(sfData);
if (data.size() > kMaxWasmDataLength)
{
JLOG(ctx.j.debug()) << "EscrowCreate.Data bad size " << data.size();
return temMALFORMED;
}
}
// amendment was checked by checkExtraFeatures
if (ctx.tx.isFieldPresent(sfBytecode))
{
auto const code = ctx.tx.getFieldVL(sfBytecode);
// Protocol ceiling only; `preflight` has no view.
if (code.empty() || code.size() > kMaxBytecodeSizeLimit)
{
JLOG(ctx.j.debug()) << "EscrowCreate.Bytecode bad size " << code.size();
return temMALFORMED;
}
}
return tesSUCCESS;
}
@@ -216,6 +264,30 @@ EscrowCreate::preclaim(PreclaimContext const& ctx)
!isTesSuccess(ret))
return ret;
}
// The limits are voted, so they can change between ledgers. A `tef`, unlike
// a `tem`, does not mark the transaction bad for the hold time.
if (ctx.tx.isFieldPresent(sfBytecode))
{
auto const& fees = ctx.view.fees();
if (isBytecodeUploadDisabled(fees))
{
JLOG(ctx.j.debug()) << "WASM runtime deactivated by fee voting";
return tefTEMP_DISABLED;
}
auto const code = ctx.tx.getFieldVL(sfBytecode);
if (code.size() > fees.bytecodeSizeLimit)
{
JLOG(ctx.j.debug()) << "EscrowCreate.Bytecode over voted limit " << code.size();
return tefEXCEEDS_LIMIT;
}
// TODO(SmartEscrow): screen the module itself here - that it compiles,
// and that it imports and exports what the engine can serve - once the
// WASM engine lands. Until then any blob of the right size is taken.
}
return tesSUCCESS;
}
@@ -236,6 +308,7 @@ EscrowCreate::doApply()
// Check reserve and funds availability
STAmount const amount{ctx_.tx[sfAmount]};
auto const reserveToAdd = calculateAdditionalReserve(ctx_.tx[~sfBytecode]);
auto const balance = sle->getFieldAmount(sfBalance).xrp();
// First check: whoever is on the hook for the new owner increment
@@ -243,8 +316,8 @@ EscrowCreate::doApply()
// validates the sponsor's reserve + remaining credit. When
// unsponsored this hits the source branch and validates the
// source's pre-lock balance against base + (currentOC+1)*increment.
if (auto const ret =
checkReserve(ctx_.getApplyViewContext(), sle, balance, {.ownerCountDelta = 1}, j_);
if (auto const ret = checkReserve(
ctx_.getApplyViewContext(), sle, balance, {.ownerCountDelta = reserveToAdd}, j_);
!isTesSuccess(ret))
return ret;
@@ -258,11 +331,15 @@ EscrowCreate::doApply()
// checkReserve) because that helper diverts to the sponsor's balance
// when a sponsor is present and would ignore the source's post-lock
// balance entirely. ownerCountDelta differs by case:
// - sponsored: 0 — sponsor covers the new owner increment, so the
// source only owes reserve for its current owners.
// - unsponsored: 1 — source owes reserve including the new increment.
// - sponsored: 0 — sponsor covers the new owner increment, so
// the source only owes reserve for its current owners.
// - unsponsored: reserveToAdd — source owes reserve including the new
// increment, which Bytecode can make larger than one unit.
auto const sourceReserve = accountReserve(
ctx_.view(), sle, j_, {.ownerCountDelta = getTxReserveSponsorID(ctx_.tx) ? 0 : 1});
ctx_.view(),
sle,
j_,
{.ownerCountDelta = getTxReserveSponsorID(ctx_.tx) ? 0 : reserveToAdd});
if (balance - STAmount(amount).xrp() < sourceReserve)
return tecUNFUNDED;
}
@@ -289,6 +366,8 @@ EscrowCreate::doApply()
(*escrow)[~sfCancelAfter] = ctx_.tx[~sfCancelAfter];
(*escrow)[~sfFinishAfter] = ctx_.tx[~sfFinishAfter];
(*escrow)[~sfDestinationTag] = ctx_.tx[~sfDestinationTag];
(*escrow)[~sfBytecode] = ctx_.tx[~sfBytecode];
(*escrow)[~sfData] = ctx_.tx[~sfData];
if (ctx_.view().rules().enabled(fixIncludeKeyletFields))
{
@@ -356,7 +435,7 @@ EscrowCreate::doApply()
}
// increment owner count
increaseOwnerCount(ctx_.getApplyViewContext(), sle, 1, ctx_.journal);
increaseOwnerCount(ctx_.getApplyViewContext(), sle, reserveToAdd, ctx_.journal);
addSponsorToLedgerEntry(ctx_.getApplyViewContext(), escrow.mutableRawSle());
ctx_.view().update(sle);
return tesSUCCESS;
@@ -16,6 +16,7 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Concepts.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Fees.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Rate.h>
#include <xrpl/protocol/SField.h>
@@ -27,6 +28,8 @@
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/Transactor.h>
#include <cstdint>
#include <optional>
#include <system_error>
#include <variant>
@@ -79,7 +82,24 @@ EscrowFinish::preflight(PreflightContext const& ctx)
// If you specify a condition, then you must also specify
// a fulfillment.
if (static_cast<bool>(cb) != static_cast<bool>(fb))
{
JLOG(ctx.j.debug()) << "Condition != Fulfillment";
return temMALFORMED;
}
if (auto const allowance = ctx.tx[~sfGas]; allowance)
{
// Protocol ceiling only; `preflight` has no view.
if (*allowance == 0)
{
return temBAD_LIMIT;
}
if (*allowance > kMaxGasLimit)
{
JLOG(ctx.j.debug()) << "Gas too large: " << *allowance;
return temBAD_LIMIT;
}
}
return tesSUCCESS;
}
@@ -148,7 +168,15 @@ EscrowFinish::calculateBaseFee(ReadView const& view, STTx const& tx)
{
extraFee += view.fees().base * (32 + (fb->size() / 16));
}
if (auto const allowance = tx[~sfGas]; allowance)
{
// The extra fee is the allowance in drops, rounded up to the nearest
// whole drop. Widen before multiplying: both factors are 32-bit.
std::uint64_t const allowanceFee =
((std::uint64_t{*allowance} * view.fees().gasPrice) + microDropsPerDrop - 1) /
microDropsPerDrop;
extraFee += allowanceFee;
}
return Transactor::calculateBaseFee(view, tx) + extraFee;
}
@@ -162,25 +190,66 @@ EscrowFinish::preclaim(PreclaimContext const& ctx)
return err;
}
if (ctx.view.rules().enabled(featureTokenEscrow))
if (ctx.view.rules().enabled(featureTokenEscrow) ||
ctx.view.rules().enabled(featureSmartEscrow))
{
// this check is done in doApply before this amendment is enabled
auto const seqProxy = SeqProxy::rawSequence(ctx.tx[sfOfferSequence]);
EscrowEntryR const escrow(ctx.tx[sfOwner], seqProxy, ctx.view);
if (!escrow)
return tecNO_TARGET;
AccountID const dest = (*escrow)[sfDestination];
STAmount const amount = (*escrow)[sfAmount];
if (!isXRP(amount))
if (ctx.view.rules().enabled(featureSmartEscrow))
{
if (auto const ret = std::visit(
[&]<typename T>(T const&) {
return escrowUnlockPreclaimHelper<T>(ctx.view, dest, amount);
},
amount.asset().value());
!isTesSuccess(ret))
return ret;
if (escrow->isFieldPresent(sfBytecode))
{
if (!ctx.tx.isFieldPresent(sfGas))
{
JLOG(ctx.j.debug()) << "Bytecode requires Gas";
return tefBYTECODE_NOT_INCLUDED;
}
// The limits are voted, so they can change between ledgers.
// A `tef`, unlike a `tem`, does not mark the transaction bad
// for the hold time.
auto const& fees = ctx.view.fees();
if (isBytecodeExecutionDisabled(fees))
{
JLOG(ctx.j.debug()) << "WASM runtime deactivated by fee voting";
return tefTEMP_DISABLED;
}
if (ctx.tx[sfGas] > fees.gasLimit)
{
JLOG(ctx.j.debug()) << "Gas over voted limit: " << ctx.tx[sfGas];
return tefEXCEEDS_LIMIT;
}
}
else
{
if (ctx.tx.isFieldPresent(sfGas))
{
JLOG(ctx.j.debug()) << "Bytecode not present, "
"Gas present";
return tefNO_BYTECODE;
}
}
}
if (ctx.view.rules().enabled(featureTokenEscrow))
{
AccountID const dest = (*escrow)[sfDestination];
STAmount const amount = (*escrow)[sfAmount];
if (!isXRP(amount))
{
if (auto const ret = std::visit(
[&]<typename T>(T const&) {
return escrowUnlockPreclaimHelper<T>(ctx.view, dest, amount);
},
amount.asset().value());
!isTesSuccess(ret))
return ret;
}
}
}
return tesSUCCESS;
@@ -193,7 +262,8 @@ EscrowFinish::doApply()
EscrowEntryW escrow(ctx_.tx[sfOwner], seqProxy, ctx_.view(), j_);
if (!escrow)
{
if (ctx_.view().rules().enabled(featureTokenEscrow))
if (ctx_.view().rules().enabled(featureTokenEscrow) ||
ctx_.view().rules().enabled(featureSmartEscrow))
return tecINTERNAL; // LCOV_EXCL_LINE
return tecNO_TARGET;
@@ -211,6 +281,20 @@ EscrowFinish::doApply()
if ((*escrow)[~sfCancelAfter] && after(now, (*escrow)[sfCancelAfter]))
return tecNO_PERMISSION;
AccountID const destID = (*escrow)[sfDestination];
auto const sled = ctx_.view().peek(keylet::account(destID));
if (ctx_.view().rules().enabled(featureSmartEscrow))
{
// NOTE: Escrow payments cannot be used to fund accounts.
if (!sled)
return tecNO_DST;
if (auto err =
verifyDepositPreauth(ctx_.tx, ctx_.view(), accountID_, destID, sled, ctx_.journal);
!isTesSuccess(err))
return err;
}
// Check cryptocondition fulfillment
{
auto const id = ctx_.tx.getTransactionID();
@@ -264,16 +348,33 @@ EscrowFinish::doApply()
return tecCRYPTOCONDITION_ERROR;
}
// NOTE: Escrow payments cannot be used to fund accounts.
AccountID const destID = (*escrow)[sfDestination];
auto const sled = ctx_.view().peek(keylet::account(destID));
if (!sled)
return tecNO_DST;
if (!ctx_.view().rules().enabled(featureSmartEscrow))
{
// NOTE: Escrow payments cannot be used to fund accounts.
if (!sled)
return tecNO_DST;
if (auto err =
verifyDepositPreauth(ctx_.tx, ctx_.view(), accountID_, destID, sled, ctx_.journal);
!isTesSuccess(err))
return err;
if (auto err =
verifyDepositPreauth(ctx_.tx, ctx_.view(), accountID_, destID, sled, ctx_.journal);
!isTesSuccess(err))
return err;
}
// Execute custom release function
if ((*escrow)[~sfBytecode])
{
// TODO(SmartEscrow): run the escrow's contract here once the WASM
// engine lands. That change also brings with it the reporting the run
// produces - the gas it consumed and the code it returned, in the
// transaction metadata - and the tecBYTECODE_REJECTED result that lets
// a rejected escrow survive with the data the contract wrote.
//
// Until then an escrow carrying bytecode can be created, funded and
// cancelled, but not finished. Reachable only under featureSmartEscrow,
// which is Supported::No.
JLOG(j_.debug()) << "EscrowFinish: no WASM engine to run the escrow's bytecode";
return tecFAILED_PROCESSING;
}
AccountID const account = (*escrow)[sfAccount];
@@ -301,13 +402,18 @@ EscrowFinish::doApply()
}
}
auto const reserveToSubtract = calculateAdditionalReserve((*escrow)[~sfBytecode]);
// Delivery can auto-create the destination's holding; the removed escrow
// must not be counted against its reserve. The two share a reserve payer
// for a self-escrow, or when one sponsor covers both.
bool const recycleReserve =
ctx_.view().rules().enabled(featureSponsor) || ctx_.view().rules().enabled(fixCleanup3_4_0);
if (recycleReserve)
decreaseOwnerCountForObject(ctx_.view(), account, escrow.mutableRawSle(), 1, ctx_.journal);
{
decreaseOwnerCountForObject(
ctx_.view(), account, escrow.mutableRawSle(), reserveToSubtract, ctx_.journal);
}
STAmount const amount = escrow->getFieldAmount(sfAmount);
// Transfer amount to destination
@@ -359,7 +465,10 @@ EscrowFinish::doApply()
ctx_.view().update(sled);
if (!recycleReserve)
decreaseOwnerCountForObject(ctx_.view(), account, escrow.mutableRawSle(), 1, ctx_.journal);
{
decreaseOwnerCountForObject(
ctx_.view(), account, escrow.mutableRawSle(), reserveToSubtract, ctx_.journal);
}
// Remove escrow from ledger
escrow.erase();
@@ -93,7 +93,8 @@ LoanSet::preflight(PreflightContext const& ctx)
// A proposed LoanSet is stored unsigned; its CounterpartySignature is
// collected on-ledger afterward, so its absence here is expected, not an
// error (On-Chain Cosigner spec §5.3.1.2).
if (!tx.isFlag(tfInnerBatchTxn) && !counterPartySig && (ctx.flags & TapProposal) == 0)
if (!tx.isFlag(tfInnerBatchTxn) && !counterPartySig &&
(ctx.flags & ApplyFlags::Proposal) == ApplyFlags::None)
{
JLOG(ctx.j.warn()) << "LoanSet transaction must have a CounterpartySignature.";
return temBAD_SIGNER;
@@ -37,7 +37,7 @@ TransactionProposalCancel::preclaim(PreclaimContext const& ctx)
// Never read the zero keylet. Preflight already rejected a zero
// ProposalID, and no real proposal can have one (every ProposalID is a
// hash), so reaching here with zero is an internal error. (and a mathematical rarity. )
// hash), so reaching here with zero is an internal error (and a mathematical rarity).
if (proposalID == beast::kZero)
return tefINTERNAL; // LCOV_EXCL_LINE
@@ -54,8 +54,8 @@ TransactionProposalCreate::preflight(PreflightContext const& ctx)
try
{
STTx const stx{STObject{proposedTx}};
auto const inner =
xrpl::preflight(ctx.registry, ctx.rules, stx, TapDryRun | TapProposal, ctx.j);
auto const inner = xrpl::preflight(
ctx.registry, ctx.rules, stx, ApplyFlags::DryRun | ApplyFlags::Proposal, ctx.j);
if (!isTesSuccess(inner.ter))
{
JLOG(ctx.j.debug()) << "TransactionProposalCreate: proposed txn "
@@ -205,7 +205,7 @@ SponsorshipTransfer::preflight(PreflightContext const& ctx)
// is collected on-ledger afterward, so its absence here is expected, not
// an error (On-Chain Cosigner spec §5.3.1.2).
if (isAccountReserveSponsorship && !ctx.tx.isFieldPresent(sfSponsorSignature) &&
(ctx.flags & TapProposal) == 0)
(ctx.flags & ApplyFlags::Proposal) == ApplyFlags::None)
{
JLOG(ctx.j.debug()) << "preflight: account sponsorship requires sfSponsorSignature";
return temMALFORMED;
+3 -2
View File
@@ -371,7 +371,8 @@ Batch::preflight(PreflightContext const& ctx)
// TapBatch — PreflightContext with a parentBatchId requires it.
// LoanSet already short-circuits on tfInnerBatchTxn; it is also in
// kDisabledTxTypes, so it never reaches this call.
ApplyFlags const innerFlags = TapBatch | (ctx.flags & (TapProposal | TapDryRun));
ApplyFlags const innerFlags =
ApplyFlags::Batch | (ctx.flags & (ApplyFlags::Proposal | ApplyFlags::DryRun));
if (auto const preflightResult =
xrpl::preflight(ctx.registry, ctx.rules, parentBatchId, stx, innerFlags, ctx.j);
!isTesSuccess(preflightResult.ter))
@@ -443,7 +444,7 @@ Batch::preflightSigValidated(PreflightContext const& ctx)
// A proposed Batch is stored unsigned; its BatchSigners are collected
// on-ledger afterward, so the signer-presence match belongs to submission
// time, not proposal creation (On-Chain Cosigner spec §5.3.1.2).
if ((ctx.flags & TapProposal) != 0)
if ((ctx.flags & ApplyFlags::Proposal) != ApplyFlags::None)
return tesSUCCESS;
auto const parentBatchId = ctx.tx.getTransactionID();
+3 -3
View File
@@ -1941,11 +1941,11 @@ private:
auto const flowResult = [&] {
STAmount const deliver(eth(51'000'000));
STAmount smax(btc(61'000'000));
PaymentSandbox sb(env.current().get(), TapNone);
PaymentSandbox sb(env.current().get(), ApplyFlags::None);
STPathSet paths;
auto const ipe = [](MPTTester const& iss) {
return STPathElement(
STPathElement::TypeMpt | STPathElement::TypeIssuer,
STPathElementType::Mpt | STPathElementType::Issuer,
xrpAccount(),
PathAsset{iss.issuanceID()},
iss.issuer());
@@ -1979,7 +1979,7 @@ private:
env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
if (flowResult.removableOffers.empty())
return false;
Sandbox sb(&view, TapNone);
Sandbox sb(&view, ApplyFlags::None);
for (auto const& o : flowResult.removableOffers)
{
if (auto const ok = sb.peek(keylet::offer(o)))
+3 -3
View File
@@ -2015,11 +2015,11 @@ private:
auto const flowResult = [&] {
STAmount const deliver(USD(51));
STAmount smax(BTC(61));
PaymentSandbox sb(env.current().get(), TapNone);
PaymentSandbox sb(env.current().get(), ApplyFlags::None);
STPathSet paths;
auto const ipe = [](Issue const& iss) {
return STPathElement(
STPathElement::TypeCurrency | STPathElement::TypeIssuer,
STPathElementType::Currency | STPathElementType::Issuer,
xrpAccount(),
iss.currency,
iss.account);
@@ -2053,7 +2053,7 @@ private:
env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
if (flowResult.removableOffers.empty())
return false;
Sandbox sb(&view, TapNone);
Sandbox sb(&view, ApplyFlags::None);
for (auto const& o : flowResult.removableOffers)
{
if (auto const ok = sb.peek(keylet::offer(o)))
+2 -2
View File
@@ -4207,7 +4207,7 @@ private:
auto const jtx = env.jt(tx, Seq(1), Fee(10));
env.app().config().features.erase(featureMPTokensV2);
PreflightContext const ctx(
env.app(), *jtx.stx, env.current()->rules(), TapNone, env.journal);
env.app(), *jtx.stx, env.current()->rules(), ApplyFlags::None, env.journal);
auto const pf = AMMBid::checkExtraFeatures(ctx);
BEAST_EXPECT(pf == false);
env.app().config().features.insert(featureMPTokensV2);
@@ -4219,7 +4219,7 @@ private:
jtx.jv["Asset2"].removeMember("mpt_issuance_id");
jtx.stx = env.ust(jtx);
PreflightContext const ctx(
env.app(), *jtx.stx, env.current()->rules(), TapNone, env.journal);
env.app(), *jtx.stx, env.current()->rules(), ApplyFlags::None, env.journal);
auto const pf = AMMBid::preflight(ctx);
BEAST_EXPECT(pf == temBAD_AMM_TOKENS);
}
+3 -3
View File
@@ -3194,7 +3194,7 @@ private:
auto const jtx = env.jt(tx, Seq(1), Fee(baseFee));
env.app().config().features.erase(featureAMM);
PreflightContext const pfCtx(
env.app(), *jtx.stx, env.current()->rules(), TapNone, env.journal);
env.app(), *jtx.stx, env.current()->rules(), ApplyFlags::None, env.journal);
auto const pf = Transactor::invokePreflight<AMMBid>(pfCtx);
BEAST_EXPECT(pf == temDISABLED);
env.app().config().features.insert(featureAMM);
@@ -3205,7 +3205,7 @@ private:
jtx.jv["TxnSignature"] = "deadbeef";
jtx.stx = env.ust(jtx);
PreflightContext const pfCtx(
env.app(), *jtx.stx, env.current()->rules(), TapNone, env.journal);
env.app(), *jtx.stx, env.current()->rules(), ApplyFlags::None, env.journal);
auto const pf = Transactor::invokePreflight<AMMBid>(pfCtx);
BEAST_EXPECT(!isTesSuccess(pf));
}
@@ -3216,7 +3216,7 @@ private:
jtx.jv["Asset2"].removeMember("issuer");
jtx.stx = env.ust(jtx);
PreflightContext const pfCtx(
env.app(), *jtx.stx, env.current()->rules(), TapNone, env.journal);
env.app(), *jtx.stx, env.current()->rules(), ApplyFlags::None, env.journal);
auto const pf = Transactor::invokePreflight<AMMBid>(pfCtx);
BEAST_EXPECT(pf == temBAD_AMM_TOKENS);
}
+1 -1
View File
@@ -597,7 +597,7 @@ public:
stx->at(sfSigningPubKey) = makeSlice(std::string("badkey"));
env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
auto const result = xrpl::apply(env.app(), view, *stx, TapNone, j);
auto const result = xrpl::apply(env.app(), view, *stx, ApplyFlags::None, j);
BEAST_EXPECT(result.ter == temBAD_SIGNATURE);
BEAST_EXPECT(!result.applied);
return result.applied;
+11 -6
View File
@@ -432,7 +432,7 @@ class Batch_test : public beast::unit_test::Suite
{
auto const jtx = env.jt(pay(alice, bob, XRP(1)), Txflags(tfInnerBatchTxn));
PreflightContext const pfCtx(
env.app(), *jtx.stx, env.current()->rules(), TapNone, env.journal);
env.app(), *jtx.stx, env.current()->rules(), ApplyFlags::None, env.journal);
auto const pf = Transactor::invokePreflight<Payment>(pfCtx);
BEAST_EXPECT(pf == temINVALID_INNER_BATCH);
}
@@ -447,7 +447,7 @@ class Batch_test : public beast::unit_test::Suite
disabledEnv.app(),
*jtx.stx,
disabledEnv.current()->rules(),
TapNone,
ApplyFlags::None,
disabledEnv.journal);
auto const pf = Transactor::invokePreflight<Payment>(pfCtx);
BEAST_EXPECT(pf == temINVALID_FLAG);
@@ -458,7 +458,12 @@ class Batch_test : public beast::unit_test::Suite
{
auto const jtx = env.jt(pay(alice, bob, XRP(1)));
PreflightContext const pfCtx(
env.app(), *jtx.stx, UInt256{1}, env.current()->rules(), TapBatch, env.journal);
env.app(),
*jtx.stx,
UInt256{1},
env.current()->rules(),
ApplyFlags::Batch,
env.journal);
auto const pf = Transactor::invokePreflight<Payment>(pfCtx);
BEAST_EXPECT(pf == temINVALID_INNER_BATCH);
}
@@ -1596,7 +1601,7 @@ class Batch_test : public beast::unit_test::Suite
batch::Sig(std::vector<Reg>(kMaxBatchSigners + 1, bob)));
env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
auto const result = xrpl::apply(env.app(), view, *jt.stx, TapNone, j);
auto const result = xrpl::apply(env.app(), view, *jt.stx, ApplyFlags::None, j);
BEAST_EXPECT(!result.applied && result.ter == temARRAY_TOO_LARGE);
return result.applied;
});
@@ -4716,7 +4721,7 @@ class Batch_test : public beast::unit_test::Suite
BEAST_EXPECT(!passesLocalChecks(stx, reason));
BEAST_EXPECT(reason == "Cannot submit pseudo transactions.");
env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
auto const result = xrpl::apply(env.app(), view, stx, TapNone, j);
auto const result = xrpl::apply(env.app(), view, stx, ApplyFlags::None, j);
BEAST_EXPECT(!result.applied && result.ter == temINVALID_FLAG);
return result.applied;
});
@@ -6016,7 +6021,7 @@ class Batch_test : public beast::unit_test::Suite
obj.setFieldU32(sfFlags, tfInnerBatchTxn);
});
auto const result = xrpl::apply(env.app(), view, stx, TapNone, j);
auto const result = xrpl::apply(env.app(), view, stx, ApplyFlags::None, j);
// Must NOT be applied — signature was never checked
BEAST_EXPECT(!result.applied);
return false;
+108
View File
@@ -1,4 +1,5 @@
#include <test/jtx/Account.h>
#include <test/jtx/CaptureLogs.h>
#include <test/jtx/Env.h>
#include <test/jtx/TestHelpers.h>
#include <test/jtx/amount.h>
@@ -14,13 +15,17 @@
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/UintTypes.h>
#include <array>
#include <cstdint>
#include <memory>
#include <stdexcept>
#include <string>
@@ -803,6 +808,108 @@ class Clawback_test : public beast::unit_test::Suite
BEAST_EXPECT(ownerCount(env, bob) == 0);
}
void
testSubResolutionAmount(FeatureBitset features)
{
testcase("Sub-resolution amount");
using namespace test::jtx;
// IOU balances keep 16 significant digits. When alice claws back an
// amount much smaller than bob's balance, the balance changes by the
// rounded amount, which can be zero, less, or more than requested. The
// clawback invariant must accept that instead of failing the
// transaction under MPTokensV2 or logging a failure without it.
Account const alice{"alice"};
Account const bob{"bob"};
struct Case
{
std::uint64_t balanceMantissa;
int balanceExponent;
std::uint64_t clawMantissa;
int clawExponent;
std::uint64_t afterMantissa;
int afterExponent;
};
std::array const cases{
// 1e7 - 1e-10 rounds back to 1e7
Case{
.balanceMantissa = 10'000'000,
.balanceExponent = 0,
.clawMantissa = 1,
.clawExponent = -10,
.afterMantissa = 10'000'000,
.afterExponent = 0,
},
// 1234567890.123456 - 0.0000005 rounds back to the balance
Case{
.balanceMantissa = 1'234'567'890'123'456,
.balanceExponent = -6,
.clawMantissa = 5,
.clawExponent = -7,
.afterMantissa = 1'234'567'890'123'456,
.afterExponent = -6,
},
// 0.0000014 removes 0.000001
Case{
.balanceMantissa = 1'234'567'890'123'456,
.balanceExponent = -6,
.clawMantissa = 14,
.clawExponent = -7,
.afterMantissa = 1'234'567'890'123'455,
.afterExponent = -6,
},
// 0.0000016 removes 0.000002
Case{
.balanceMantissa = 1'234'567'890'123'456,
.balanceExponent = -6,
.clawMantissa = 16,
.clawExponent = -7,
.afterMantissa = 1'234'567'890'123'454,
.afterExponent = -6,
},
};
for (auto const feat : {features, features - featureMPTokensV2})
{
for (auto const& c : cases)
{
std::string logs;
{
Env env(*this, feat, std::make_unique<test::CaptureLogs>(&logs));
env.fund(XRP(1000), alice, bob);
env.close();
auto const usd = alice["USD"];
env(fset(alice, asfAllowTrustLineClawback));
env.close();
env.trust(usd(10'000'000'000), bob);
env(pay(
alice, bob, STAmount{usd.issue(), c.balanceMantissa, c.balanceExponent}));
env.close();
env(claw(
alice,
STAmount{
Issue{usd.currency, bob.id()}, c.clawMantissa, c.clawExponent}),
Ter(tesSUCCESS));
env.close();
BEAST_EXPECT(
env.balance(bob, usd).value() ==
STAmount(usd.issue(), c.afterMantissa, c.afterExponent));
}
// Without MPTokensV2 a failed invariant only logs and the
// transaction still succeeds, so check the logs instead.
if (!feat[featureMPTokensV2])
BEAST_EXPECT(!logs.contains("Invariant failed"));
}
}
}
void
testTickets(FeatureBitset features)
{
@@ -872,6 +979,7 @@ class Clawback_test : public beast::unit_test::Suite
testDeleteDefaultLine(features);
testFrozenLine(features);
testAmountExceedsAvailable(features);
testSubResolutionAmount(features);
testTickets(features);
}
+2 -1
View File
@@ -1748,7 +1748,8 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
(*sle)[sfConfidentialBalanceVersion] = wrappedFrom;
view.rawReplace(sle);
auto const result = xrpl::apply(env.app(), view, *jt.stx, TapNone, env.journal);
auto const result =
xrpl::apply(env.app(), view, *jt.stx, ApplyFlags::None, env.journal);
BEAST_EXPECT(result.ter == tesSUCCESS);
return result.applied;
}));
+2 -2
View File
@@ -909,7 +909,7 @@ struct Credentials_test : public beast::unit_test::Suite
{
testcase("deleteSLE fail, bad SLE.");
auto const view =
std::make_shared<ApplyViewImpl>(env.current().get(), ApplyFlags::TapNone);
std::make_shared<ApplyViewImpl>(env.current().get(), ApplyFlags::None);
auto const ter = xrpl::credentials::deleteSLE(*view, {}, env.journal);
BEAST_EXPECT(ter == tecNO_ENTRY);
}
@@ -1132,7 +1132,7 @@ struct Credentials_test : public beast::unit_test::Suite
// Create an ApplyViewImpl on top of the current closed ledger
// and corrupt it by erasing the issuer's account SLE
auto const open = env.current();
ApplyViewImpl av(&*open, TapNone);
ApplyViewImpl av(&*open, ApplyFlags::None);
// Erase the issuer's account to simulate ledger corruption
auto const sleIssuer = av.peek(keylet::account(issuer.id()));
+787
View File
@@ -0,0 +1,787 @@
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/TestHelpers.h>
#include <test/jtx/amount.h>
#include <test/jtx/envconfig.h>
#include <test/jtx/escrow.h>
#include <test/jtx/fee.h>
#include <test/jtx/ter.h>
#include <xrpld/core/Config.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/config/Constants.h>
#include <xrpl/core/StartUpType.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Fees.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/SeqProxy.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <string>
namespace xrpl::test {
/**
* Smart Escrow's `Bytecode`, `Data` and `Gas` fields.
*
* The WASM engine that runs an escrow's bytecode is not in this build, so the
* `Bytecode` field is opaque here: `EscrowCreate` weighs its size and nothing
* reads it, and `EscrowFinish` has nothing to run it with. Everything around
* the run is testable, and is tested below - the preflight and preclaim rules,
* the fee the bytecode costs, the owner reserve it occupies, and the refund of
* that reserve.
*
* What waits on the engine: a successful finish, tecBYTECODE_REJECTED, the
* gas and return code the run reports in the transaction metadata, and the
* `Data` a rejected contract leaves behind.
*/
struct EscrowSmart_test : public beast::unit_test::Suite
{
// A blob of `bytes` bytes, as hex. Opens with the WASM preamble - "\0asm"
// and version 1 - so a reader can see what the field is meant to carry,
// but nothing in this build parses it.
static std::string
bytecodeOfSize(std::size_t bytes)
{
static std::string const kPreamble = "0061736D01000000";
std::string hex = kPreamble;
hex.append((bytes * 2) - kPreamble.size(), 'A');
return hex;
}
// What EscrowCreate charges for `bytecodeHex`: ten base fees, plus five
// drops per byte.
static XRPAmount
createFeeFor(jtx::Env const& env, std::string const& bytecodeHex)
{
return env.current()->fees().base * 10 + bytecodeHex.size() / 2 * 5;
}
// A validator's config that votes for `value` on the fee setting `key`.
static std::unique_ptr<Config>
votingFor(std::string const& key, std::uint32_t value)
{
auto cfg = jtx::envconfig(jtx::validator, "");
cfg->section(Sections::kVoting).set(key, std::to_string(value));
return cfg;
}
// Close past the flag ledger, so the vote lands in FeeSettings.
static void
closePastFlagLedger(jtx::Env& env)
{
for (auto i = env.current()->seq(); i <= 257; ++i)
env.close();
}
// What EscrowFinish charges for `gas`: the gas cost rounded up to the drop.
static XRPAmount
finishFeeFor(jtx::Env const& env, std::uint64_t gas)
{
return env.current()->fees().base +
((gas * env.current()->fees().gasPrice) + microDropsPerDrop - 1) / microDropsPerDrop;
}
void
testCreatePreflight(FeatureBitset features)
{
testcase("EscrowCreate preflight");
using namespace jtx;
using namespace std::chrono;
Account const alice{"alice"};
Account const carol{"carol"};
auto const bytecode = bytecodeOfSize(64);
{
// featureSmartEscrow disabled
Env env(*this, features - featureSmartEscrow);
env.fund(XRP(5000), alice, carol);
XRPAmount const txnFees = env.current()->fees().base + 1000;
auto const escrowCreate = escrow::create(alice, carol, XRP(1000));
env(escrowCreate,
escrow::Bytecode(bytecode),
escrow::kCancelTime(env.now() + 100s),
Fee(txnFees),
Ter(temDISABLED));
env.close();
env(escrowCreate,
escrow::Bytecode(bytecode),
escrow::kCancelTime(env.now() + 100s),
escrow::Data("00112233"),
Fee(txnFees),
Ter(temDISABLED));
env.close();
env(escrowCreate,
escrow::Data("00112233"),
escrow::kFinishTime(env.now() + 100s),
Fee(txnFees),
Ter(temDISABLED));
env.close();
}
{
// Bytecode > max length
Env env(*this, votingFor(Keys::kBytecodeSizeLimit, 10), features); // 10 bytes
closePastFlagLedger(env);
XRPAmount const txnFees = env.current()->fees().base + 1000;
env.fund(XRP(5000), alice, carol);
auto const escrowCreate = escrow::create(alice, carol, XRP(500));
// 11-byte string
std::string const longBytecode = "00112233445566778899AA";
env(escrowCreate,
escrow::Bytecode(longBytecode),
escrow::kCancelTime(env.now() + 100s),
Fee(txnFees),
Ter(tefEXCEEDS_LIMIT));
env.close();
}
{
// gas limit set to 0
Env env(*this, votingFor(Keys::kGasLimit, 0), features); // WASM runtime disabled
closePastFlagLedger(env);
XRPAmount const txnFees = env.current()->fees().base + 1000;
env.fund(XRP(5000), alice, carol);
auto const escrowCreate = escrow::create(alice, carol, XRP(500));
env(escrowCreate,
escrow::Bytecode(bytecode),
escrow::kCancelTime(env.now() + 100s),
Fee(txnFees),
Ter(tefTEMP_DISABLED));
env.close();
}
{
// size limit set to 0
Env env(
*this, votingFor(Keys::kBytecodeSizeLimit, 0), features); // WASM upload disabled
closePastFlagLedger(env);
XRPAmount const txnFees = env.current()->fees().base + 1000;
env.fund(XRP(5000), alice, carol);
auto const escrowCreate = escrow::create(alice, carol, XRP(500));
// 1-byte string
env(escrowCreate,
escrow::Bytecode("AA"),
escrow::kCancelTime(env.now() + 100s),
Fee(txnFees),
Ter(tefTEMP_DISABLED));
env.close();
env(escrowCreate,
escrow::Bytecode(bytecode),
escrow::kCancelTime(env.now() + 100s),
Fee(txnFees),
Ter(tefTEMP_DISABLED));
env.close();
}
{
// Data without Bytecode
Env env(*this, features);
XRPAmount const txnFees = env.current()->fees().base + 100000;
env.fund(XRP(5000), alice, carol);
auto const escrowCreate = escrow::create(alice, carol, XRP(500));
std::string const data(4, 'A');
env(escrowCreate,
escrow::Data(data),
escrow::kFinishTime(env.now() + 100s),
Fee(txnFees),
Ter(temMALFORMED));
env.close();
}
{
// Data > max length
Env env(*this, features);
XRPAmount const txnFees = env.current()->fees().base + 100000;
env.fund(XRP(5000), alice, carol);
auto const escrowCreate = escrow::create(alice, carol, XRP(500));
// string of length (kMaxWasmDataLength + 1) * 2
std::string const longData((kMaxWasmDataLength + 1) * 2, 'B');
env(escrowCreate,
escrow::Data(longData),
escrow::Bytecode(bytecode),
escrow::kCancelTime(env.now() + 100s),
Fee(txnFees),
Ter(temMALFORMED));
env.close();
}
// Bytecode joins FinishAfter and Condition as a way to say how the
// escrow completes, but it always needs a CancelAfter: nothing else
// can free the funds if the contract never accepts.
Env env(
*this,
envconfig([](std::unique_ptr<Config> cfg) {
cfg->startUp = StartUpType::Fresh;
return cfg;
}),
features);
env.fund(XRP(5000), alice, carol);
auto const escrowCreate = escrow::create(alice, carol, XRP(500));
XRPAmount const txnFees = createFeeFor(env, bytecode);
// Success situations
{
// Bytecode + CancelAfter
env(escrowCreate,
escrow::Bytecode(bytecode),
escrow::kCancelTime(env.now() + 20s),
Fee(txnFees));
env.close();
}
{
// Bytecode + Condition + CancelAfter
env(escrowCreate,
escrow::Bytecode(bytecode),
escrow::kCancelTime(env.now() + 30s),
escrow::kCondition(escrow::kCb1),
Fee(txnFees));
env.close();
}
{
// Bytecode + FinishAfter + CancelAfter
env(escrowCreate,
escrow::Bytecode(bytecode),
escrow::kCancelTime(env.now() + 40s),
escrow::kFinishTime(env.now() + 2s),
Fee(txnFees));
env.close();
}
{
// Bytecode + Data + CancelAfter
env(escrowCreate,
escrow::Bytecode(bytecode),
escrow::Data("00112233"),
escrow::kCancelTime(env.now() + 50s),
Fee(txnFees));
env.close();
}
// Failure situations (i.e. all other combinations)
{
// only Bytecode
env(escrowCreate, escrow::Bytecode(bytecode), Fee(txnFees), Ter(temBAD_EXPIRATION));
env.close();
}
{
// Bytecode + FinishAfter
env(escrowCreate,
escrow::Bytecode(bytecode),
escrow::kFinishTime(env.now() + 2s),
Fee(txnFees),
Ter(temBAD_EXPIRATION));
env.close();
}
{
// Bytecode + Condition
env(escrowCreate,
escrow::Bytecode(bytecode),
escrow::kCondition(escrow::kCb1),
Fee(txnFees),
Ter(temBAD_EXPIRATION));
env.close();
}
{
// Bytecode 0 length
env(escrowCreate,
escrow::Bytecode(""),
escrow::kCancelTime(env.now() + 60s),
Fee(txnFees),
Ter(temMALFORMED));
env.close();
}
{
// Not enough fees
env(escrowCreate,
escrow::Bytecode(bytecode),
escrow::kCancelTime(env.now() + 70s),
Fee(txnFees - 1),
Ter(telINSUF_FEE_P));
env.close();
}
}
void
testCreateFeeAndReserve(FeatureBitset features)
{
testcase("EscrowCreate fee and reserve");
using namespace jtx;
using namespace std::chrono;
Account const alice{"alice"};
Account const carol{"carol"};
{
// The fee scales with the bytecode: ten base fees plus five drops
// per byte, and a drop short of it is refused.
Env env(*this, features);
env.fund(XRP(5000), alice, carol);
env.close();
auto const bytecode = bytecodeOfSize(1'000);
auto const escrowCreate = escrow::create(alice, carol, XRP(500));
auto const fee = createFeeFor(env, bytecode);
BEAST_EXPECT(fee == env.current()->fees().base * 10 + 5'000);
env(escrowCreate,
escrow::Bytecode(bytecode),
escrow::kCancelTime(env.now() + 100s),
Fee(fee - 1),
Ter(telINSUF_FEE_P));
env.close();
auto const seq = env.seq(alice);
env(escrowCreate,
escrow::Bytecode(bytecode),
escrow::Data("00112233"),
escrow::kCancelTime(env.now() + 100s),
Fee(fee));
env.close();
// The ledger entry carries both fields.
auto const sle = env.le(keylet::escrow(alice, SeqProxy::rawSequence(seq)));
if (BEAST_EXPECT(sle))
{
BEAST_EXPECT(sle->isFieldPresent(sfBytecode));
BEAST_EXPECT(strHex(sle->getFieldVL(sfBytecode)) == bytecode);
BEAST_EXPECT(strHex(sle->getFieldVL(sfData)) == "00112233");
}
}
{
// The reserve is one owner increment per started 500 bytes of
// bytecode.
Env env(*this, features);
env.fund(XRP(5000), alice, carol);
env.close();
std::uint32_t expectedOwnerCount = 0;
for (auto const size : {
std::size_t{64},
std::size_t{500},
std::size_t{501},
std::size_t{1'000},
std::size_t{1'001},
})
{
auto const bytecode = bytecodeOfSize(size);
env(escrow::create(alice, carol, XRP(100)),
escrow::Bytecode(bytecode),
escrow::kCancelTime(env.now() + 1000s),
Fee(createFeeFor(env, bytecode)));
env.close();
expectedOwnerCount += (size + 499) / 500;
BEAST_EXPECTS(
env.ownerCount(alice) == expectedOwnerCount,
std::to_string(size) + " bytes: " + std::to_string(env.ownerCount(alice)));
}
}
{
// A bytecode escrow the owner cannot reserve is refused, where the
// same escrow without bytecode would have been accepted.
Env env(*this, features);
// Base 200 XRP + 50 XRP per owner object, from the unit-test
// config. Enough for one owner object, not for two.
env.fund(XRP(300), alice, carol);
env.close();
auto const bytecode = bytecodeOfSize(1'000);
env(escrow::create(alice, carol, XRP(20)),
escrow::Bytecode(bytecode),
escrow::kCancelTime(env.now() + 100s),
Fee(createFeeFor(env, bytecode)),
Ter(tecINSUFFICIENT_RESERVE));
env.close();
BEAST_EXPECT(env.ownerCount(alice) == 0);
// The same escrow without bytecode costs one increment and fits.
env(escrow::create(alice, carol, XRP(20)),
escrow::kFinishTime(env.now() + 10s),
escrow::kCancelTime(env.now() + 100s));
env.close();
BEAST_EXPECT(env.ownerCount(alice) == 1);
}
{
// After the lock, an XRP escrow's owner must still hold the
// reserve the bytecode costs, not the single increment a plain
// escrow costs.
Env env(*this, features);
Account const dave{"dave"};
Account const erin{"erin"};
env.fund(XRP(5000), carol);
env.close();
auto const bytecode = bytecodeOfSize(600); // two increments
auto const fee = createFeeFor(env, bytecode);
auto const amount = XRP(1'000);
auto const fees = env.current()->fees();
// One increment short after the lock.
env.fund(
STAmount{fees.accountReserve(1, 1) + amount.value().xrp() + fee}, noripple(dave));
// Exactly enough after the lock.
env.fund(
STAmount{fees.accountReserve(2, 1) + amount.value().xrp() + fee}, noripple(erin));
env.close();
env(escrow::create(dave, carol, amount),
escrow::Bytecode(bytecode),
escrow::kCancelTime(env.now() + 100s),
Fee(fee),
Ter(tecUNFUNDED));
env(escrow::create(erin, carol, amount),
escrow::Bytecode(bytecode),
escrow::kCancelTime(env.now() + 100s),
Fee(fee));
env.close();
BEAST_EXPECT(env.ownerCount(dave) == 0);
BEAST_EXPECT(env.ownerCount(erin) == 2);
}
}
void
testFinishPreflight(FeatureBitset features)
{
testcase("EscrowFinish preflight");
using namespace jtx;
using namespace std::chrono;
Account const alice{"alice"};
Account const carol{"carol"};
{
// featureSmartEscrow disabled
Env env(*this, features - featureSmartEscrow);
env.fund(XRP(5000), alice, carol);
env(escrow::finish(carol, alice, 1),
Fee(env.current()->fees().base + 1000),
escrow::Gas(4),
Ter(temDISABLED));
env.close();
}
{
// Gas > gas limit
Env env(*this, votingFor(Keys::kGasLimit, 1'000), features); // in gas
env.fund(XRP(5000), alice, carol);
env.close();
// Gas above the voted limit is checked against the escrow it
// would run, so there has to be one.
auto const bytecode = bytecodeOfSize(64);
auto const seq = env.seq(alice);
env(escrow::create(alice, carol, XRP(500)),
escrow::Bytecode(bytecode),
escrow::kCancelTime(env.now() + 100'000s),
Fee(createFeeFor(env, bytecode)));
closePastFlagLedger(env);
auto const gas = 1'001;
env(escrow::finish(carol, alice, seq),
Fee(finishFeeFor(env, gas)),
escrow::Gas(gas),
Ter(tefEXCEEDS_LIMIT));
}
{
// Gas of 0
Env env(*this, features);
env.fund(XRP(5000), alice, carol);
env.close();
env(escrow::finish(carol, alice, 1),
Fee(env.current()->fees().base + 1000),
escrow::Gas(0),
Ter(temBAD_LIMIT));
}
{
// Gas above the protocol ceiling, refused before any view is read
Env env(*this, features);
env.fund(XRP(5000), alice, carol);
env.close();
env(escrow::finish(carol, alice, 1),
Fee(env.current()->fees().base + 1000),
escrow::Gas(kMaxGasLimit + 1),
Ter(temBAD_LIMIT));
}
{
// WASM compute disabled after the escrow was created.
Env env(*this, votingFor(Keys::kGasLimit, 0), features);
env.fund(XRP(5000), alice, carol);
env.close();
auto const bytecode = bytecodeOfSize(64);
auto const seq = env.seq(alice);
env(escrow::create(alice, carol, XRP(500)),
escrow::Bytecode(bytecode),
escrow::kCancelTime(env.now() + 100'000s),
Fee(createFeeFor(env, bytecode)));
closePastFlagLedger(env);
auto const gas = 1'000;
env(escrow::finish(carol, alice, seq),
escrow::Gas(gas),
Fee(finishFeeFor(env, gas)),
Ter(tefTEMP_DISABLED));
}
}
void
testFinishBytecodePairing(FeatureBitset features)
{
testcase("EscrowFinish Bytecode and Gas must agree");
using namespace jtx;
using namespace std::chrono;
Account const alice{"alice"};
Account const carol{"carol"};
Env env(*this, features);
// Run past the flag ledger so that a Fee change vote occurs and
// updates FeeSettings. (It also activates all supported amendments.)
for (auto i = env.current()->seq(); i <= 257; ++i)
env.close();
env.fund(XRP(5000), alice, carol);
auto const bytecode = bytecodeOfSize(64);
auto const seq = env.seq(alice);
env(escrow::create(alice, carol, XRP(500)),
escrow::Bytecode(bytecode),
escrow::kCancelTime(env.now() + 100s),
Fee(createFeeFor(env, bytecode)));
env.close();
{
// Bytecode on the escrow, no Gas on the finish
env(escrow::finish(carol, alice, seq), Ter(tefBYTECODE_NOT_INCLUDED));
}
{
// Gas on the finish, no Bytecode on the escrow
auto const plainSeq = env.seq(alice);
env(escrow::create(alice, carol, XRP(500)),
escrow::kFinishTime(env.now() + 10s),
escrow::kCancelTime(env.now() + 100s));
env.close();
auto const gas = 100;
env(escrow::finish(carol, alice, plainSeq),
Fee(finishFeeFor(env, gas)),
escrow::Gas(gas),
Ter(tefNO_BYTECODE));
}
}
void
testFinishFee(FeatureBitset features)
{
testcase("EscrowFinish fee");
using namespace jtx;
using namespace std::chrono;
Account const alice{"alice"};
Account const carol{"carol"};
Env env(
*this,
envconfig([](std::unique_ptr<Config> cfg) {
cfg->fees.gasPrice = 1'000'000; // 1 drop per gas
return cfg;
}),
features);
// Run past the flag ledger so that a Fee change vote occurs and
// updates FeeSettings. (It also activates all supported amendments.)
for (auto i = env.current()->seq(); i <= 257; ++i)
env.close();
env.fund(XRP(5000), alice, carol);
auto const bytecode = bytecodeOfSize(64);
auto const seq = env.seq(alice);
env(escrow::create(alice, carol, XRP(1000)),
escrow::Bytecode(bytecode),
escrow::kCancelTime(env.now() + 100s),
Fee(createFeeFor(env, bytecode)));
env.close();
// A large gas allowance costs more than the allowance itself, and does
// not wrap.
auto const gas = 996'433;
auto const fee = finishFeeFor(env, gas);
BEAST_EXPECT(fee.drops() > gas);
// At one drop per gas the cost divides evenly, so no drop is added.
BEAST_EXPECT(fee == env.current()->fees().base + gas);
// Intentional low value to test overflow handling
env(escrow::finish(carol, alice, seq),
Fee(drops(30)),
escrow::Gas(gas),
Ter(telINSUF_FEE_P));
env(escrow::finish(carol, alice, seq), Fee(fee - 1), escrow::Gas(gas), Ter(telINSUF_FEE_P));
// TODO(SmartEscrow): tesSUCCESS once the WASM engine lands. Until then
// the fee is charged and the run reports that it could not happen.
env(escrow::finish(carol, alice, seq),
Fee(fee),
escrow::Gas(gas),
Ter(tecFAILED_PROCESSING));
{
// A gas cost of a fraction of a drop rounds up to a whole drop:
// two gas at one micro-drop each costs one drop, not zero, and not
// one drop per gas.
Env env(*this, votingFor(Keys::kGasPrice, 1), features);
env.fund(XRP(5000), alice, carol);
env.close();
auto const seq = env.seq(alice);
env(escrow::create(alice, carol, XRP(1000)),
escrow::Bytecode(bytecode),
escrow::kCancelTime(env.now() + 100'000s),
Fee(createFeeFor(env, bytecode)));
closePastFlagLedger(env);
BEAST_EXPECT(env.current()->fees().gasPrice == 1);
auto const base = env.current()->fees().base;
env(escrow::finish(carol, alice, seq), Fee(base), escrow::Gas(2), Ter(telINSUF_FEE_P));
env(escrow::finish(carol, alice, seq),
Fee(base + 1),
escrow::Gas(2),
Ter(tecFAILED_PROCESSING));
}
}
void
testFinishWithoutEngine(FeatureBitset features)
{
testcase("EscrowFinish has no WASM engine");
using namespace jtx;
using namespace std::chrono;
Account const alice{"alice"};
Account const carol{"carol"};
Env env(*this, features);
env.fund(XRP(5000), alice, carol);
env.close();
auto const bytecode = bytecodeOfSize(1'000);
auto const seq = env.seq(alice);
env(escrow::create(alice, carol, XRP(500)),
escrow::Bytecode(bytecode),
escrow::kCancelTime(env.now() + 20s),
Fee(createFeeFor(env, bytecode)));
env.close();
BEAST_EXPECT(env.ownerCount(alice) == 2);
// TODO(SmartEscrow): the contract decides this once the WASM engine
// lands - tesSUCCESS on accept, tecBYTECODE_REJECTED on reject.
auto const gas = 1'000;
env(escrow::finish(carol, alice, seq),
Fee(finishFeeFor(env, gas)),
escrow::Gas(gas),
Ter(tecFAILED_PROCESSING));
env.close();
// The escrow and its reserve survive.
BEAST_EXPECT(env.le(keylet::escrow(alice, SeqProxy::rawSequence(seq))));
BEAST_EXPECT(env.ownerCount(alice) == 2);
// Cancelling returns the whole reserve, not just one increment.
env.close(30s);
env(escrow::cancel(carol, alice, seq));
env.close();
BEAST_EXPECT(!env.le(keylet::escrow(alice, SeqProxy::rawSequence(seq))));
BEAST_EXPECT(env.ownerCount(alice) == 0);
}
void
testFinishReleasesReserve(FeatureBitset features)
{
testcase("EscrowFinish releases the reserve");
using namespace jtx;
using namespace std::chrono;
Account const alice{"alice"};
Account const carol{"carol"};
// Without featureSponsor and fixCleanup3_4_0, the reserve is released
// after delivery rather than before.
Env env(*this, features - featureSponsor - fixCleanup3_4_0);
env.fund(XRP(5000), alice, carol);
env.close();
auto const seq = env.seq(alice);
env(escrow::create(alice, carol, XRP(500)), escrow::kFinishTime(env.now() + 1s));
env.close();
BEAST_EXPECT(env.ownerCount(alice) == 1);
env(escrow::finish(carol, alice, seq));
env.close();
BEAST_EXPECT(!env.le(keylet::escrow(alice, SeqProxy::rawSequence(seq))));
BEAST_EXPECT(env.ownerCount(alice) == 0);
}
void
testWithFeats(FeatureBitset features)
{
testCreatePreflight(features);
testCreateFeeAndReserve(features);
testFinishPreflight(features);
testFinishBytecodePairing(features);
testFinishFee(features);
testFinishWithoutEngine(features);
testFinishReleasesReserve(features);
}
public:
void
run() override
{
using namespace test::jtx;
FeatureBitset const all{testableAmendments()};
testWithFeats(all);
}
};
BEAST_DEFINE_TESTSUITE(EscrowSmart, app, xrpl);
} // namespace xrpl::test
+2 -2
View File
@@ -2773,7 +2773,7 @@ struct EscrowToken_test : public beast::unit_test::Suite
auto const seq1 = env.seq(alice);
env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
Sandbox sb(&view, TapNone);
Sandbox sb(&view, ApplyFlags::None);
auto const sleNew =
std::make_shared<SLE>(keylet::escrow(alice, SeqProxy::rawSequence(seq1)));
MPTIssue const mpt{MPTIssue{makeMptID(1, AccountID(0x4985601))}};
@@ -3003,7 +3003,7 @@ struct EscrowToken_test : public beast::unit_test::Suite
auto const seq1 = env.seq(alice);
env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
Sandbox sb(&view, TapNone);
Sandbox sb(&view, ApplyFlags::None);
auto const sleNew =
std::make_shared<SLE>(keylet::escrow(alice, SeqProxy::rawSequence(seq1)));
MPTIssue const mpt{MPTIssue{makeMptID(1, AccountID(0x4985601))}};
+11 -9
View File
@@ -1126,8 +1126,8 @@ struct Escrow_test : public beast::unit_test::Suite
env(jt, Ter(fixEnabled ? TER{temMALFORMED} : TER{tecCRYPTOCONDITION_ERROR}));
// Run preflight on the same transaction again, now with the
// HashRouter flag cached. The result must not change.
auto const pf =
preflight(env.app(), env.current()->rules(), *jt.stx, TapNone, env.journal);
auto const pf = preflight(
env.app(), env.current()->rules(), *jt.stx, ApplyFlags::None, env.journal);
BEAST_EXPECT(pf.ter == (fixEnabled ? TER{temMALFORMED} : TER{tesSUCCESS}));
env.close();
@@ -1344,8 +1344,8 @@ struct Escrow_test : public beast::unit_test::Suite
escrow::kFinishTime(env.now() + 1s),
Seq(1),
Fee(baseFee));
auto const pf =
preflight(env.app(), env.current()->rules(), *jtx.stx, TapNone, env.journal);
auto const pf = preflight(
env.app(), env.current()->rules(), *jtx.stx, ApplyFlags::None, env.journal);
BEAST_EXPECT(isTesSuccess(pf.ter));
BEAST_EXPECT(!pf.consequences.isBlocker());
BEAST_EXPECT(pf.consequences.fee() == drops(baseFee));
@@ -1354,8 +1354,8 @@ struct Escrow_test : public beast::unit_test::Suite
{
auto const jtx = env.jt(escrow::cancel("bob", "alice", 3), Seq(1), Fee(baseFee));
auto const pf =
preflight(env.app(), env.current()->rules(), *jtx.stx, TapNone, env.journal);
auto const pf = preflight(
env.app(), env.current()->rules(), *jtx.stx, ApplyFlags::None, env.journal);
BEAST_EXPECT(isTesSuccess(pf.ter));
BEAST_EXPECT(!pf.consequences.isBlocker());
BEAST_EXPECT(pf.consequences.fee() == drops(baseFee));
@@ -1364,8 +1364,8 @@ struct Escrow_test : public beast::unit_test::Suite
{
auto const jtx = env.jt(escrow::finish("bob", "alice", 3), Seq(1), Fee(baseFee));
auto const pf =
preflight(env.app(), env.current()->rules(), *jtx.stx, TapNone, env.journal);
auto const pf = preflight(
env.app(), env.current()->rules(), *jtx.stx, ApplyFlags::None, env.journal);
BEAST_EXPECT(isTesSuccess(pf.ter));
BEAST_EXPECT(!pf.consequences.isBlocker());
BEAST_EXPECT(pf.consequences.fee() == drops(baseFee));
@@ -1524,7 +1524,7 @@ struct Escrow_test : public beast::unit_test::Suite
Account const alice{"alice"};
Account const bob{"bob"};
Account const carol{"carol"};
Account const dillon{"dillon "};
Account const dillon{"dillon"};
Account const zelda{"zelda"};
char const credType[] = "abcde";
@@ -1682,6 +1682,8 @@ public:
testWithFeats(all);
testWithFeats(all - featureTokenEscrow);
testWithFeats(all - fixCleanup3_5_0);
testWithFeats(all - featureSmartEscrow);
testWithFeats(all - featureTokenEscrow - featureSmartEscrow);
testTags(all - fixIncludeKeyletFields);
}
};
+2 -3
View File
@@ -160,7 +160,7 @@ createInvalidFeeTx(
TER
applyFeeAndTestResult(jtx::Env& env, OpenView& view, STTx const& tx)
{
auto const res = apply(env.app(), view, tx, ApplyFlags::TapNone, env.journal);
auto const res = apply(env.app(), view, tx, ApplyFlags::None, env.journal);
return res.ter;
}
@@ -1473,8 +1473,7 @@ class FeeVote_test : public beast::unit_test::Suite
OpenView accum(ledger.get());
BEAST_EXPECT(!accum.open());
auto const result =
apply(env.app(), accum, amendTx, ApplyFlags::TapNone, env.journal);
auto const result = apply(env.app(), accum, amendTx, ApplyFlags::None, env.journal);
BEAST_EXPECTS(result.applied && isTesSuccess(result.ter), transToken(result.ter));
accum.apply(*ledger);
}
+138 -3
View File
@@ -338,11 +338,11 @@ struct FlowMPT_test : public beast::unit_test::Suite
auto const flowResult = [&] {
STAmount const deliver(usd(5'100));
STAmount smax(btc(61));
PaymentSandbox sb(env.current().get(), TapNone);
PaymentSandbox sb(env.current().get(), ApplyFlags::None);
STPathSet paths;
auto const ipe = [](Asset const& asset) {
return STPathElement(
STPathElement::TypeAsset | STPathElement::TypeIssuer,
STPathElementType::Asset | STPathElementType::Issuer,
xrpAccount(),
asset,
asset.getIssuer());
@@ -376,7 +376,7 @@ struct FlowMPT_test : public beast::unit_test::Suite
env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
if (flowResult.removableOffers.empty())
return false;
Sandbox sb(&view, TapNone);
Sandbox sb(&view, ApplyFlags::None);
for (auto const& o : flowResult.removableOffers)
{
if (auto const ok = sb.peek(keylet::offer(o)))
@@ -1215,6 +1215,61 @@ struct FlowMPT_test : public beast::unit_test::Suite
BEAST_EXPECT(env.balance(bob, y) == y(0));
}
void
testConsumeOfferOverflow(FeatureBitset features)
{
// An overflow while consuming an offer fails the strand under
// MPTokensV2, and the transaction with tecINTERNAL otherwise. Either
// way no balances change and the offer stays on the book.
testcase("BookStep consumeOffer overflow");
using namespace jtx;
Account const gw("gateway");
Account const eurGW("eur_gateway");
Account const maker("maker");
Account const alice("alice");
Account const bob("bob");
for (auto const& testFeatures : {features, features - featureMPTokensV2})
{
Env env(*this, testFeatures);
auto const withV2 = testFeatures[featureMPTokensV2];
env.fund(XRP(10'000), gw, eurGW, maker, alice, bob);
env.close();
auto const usd = gw["USD"];
auto const eur = eurGW["EUR"];
STAmount const usdMax{usd.issue(), STAmount::kMaxValue, STAmount::kMaxOffset};
STAmount const usdIn{usd.issue(), 1, 90};
env.trust(usdMax, maker, alice);
env.trust(eur(1'000), maker, bob);
env.close();
env(pay(gw, maker, usdMax));
env(pay(gw, alice, usdIn));
env(pay(eurGW, maker, eur(100)));
env(offer(maker, usdIn, eur(100)));
env.close();
env(pay(alice, bob, eur(100)),
Path(~eur),
Sendmax(usdIn),
Ter(withV2 ? TER{tecPATH_PARTIAL} : TER{tecINTERNAL}));
env.close();
env.require(
Balance(bob, eur(0)),
Balance(maker, eur(100)),
Balance(maker, usdMax),
Balance(alice, usdIn));
BEAST_EXPECT(isOffer(env, maker, usdIn, eur(100)));
}
}
void
testFalseDry(FeatureBitset features)
{
@@ -3008,6 +3063,7 @@ struct FlowMPT_test : public beast::unit_test::Suite
testBookStepInputAboveMaximumAmount(features);
testBookStepInputAboveMaximumAmountWithFee(features);
testBookStepMissingIssuance(features);
testConsumeOfferOverflow(features);
testSelfPayment1(features);
testSelfPayment2(features);
testSelfFundedXRPEndpoint(false, features);
@@ -3016,6 +3072,85 @@ struct FlowMPT_test : public beast::unit_test::Suite
testReExecuteDirectStep(features);
testSelfPayLowQualityOffer(features);
testLockedMidPathHolder(features);
testIssuerSourceMPTLoop(features);
}
void
testIssuerSourceMPTLoop(FeatureBitset features)
{
// An issuer paying with its own MPT may not route it back through a
// book that outputs the same MPT. It would issue at the first book
// and again after the second, and with the issuer's own offer in
// the second book the engine could not keep the sum within
// MaximumAmount. Holders already get temBAD_PATH_LOOP for this.
testcase("Issuer-sourced payment looping through its own MPT");
using namespace jtx;
Account const gw{"gw"};
Account const alice{"alice"};
Account const c1{"c1"};
Account const e1{"e1"};
Account const dan{"dan"};
Env env(*this, features);
env.fund(XRP(1'000), gw, alice, c1, e1, dan);
env.close();
MPT const x =
MPTTester({.env = env, .issuer = gw, .holders = {alice, c1, e1, dan}, .maxAmt = 100});
auto const eur = gw["EUR"];
auto const usd = gw["USD"];
env(trust(e1, eur(1'000)));
env(trust(e1, usd(1'000)));
env(trust(dan, eur(1'000)));
env(pay(gw, e1, eur(1'000)));
env(pay(gw, e1, usd(1'000)));
env(pay(gw, alice, x(10)));
env.close();
// Liquidity for every path below, arranged so nothing crosses at
// creation.
env(offer(gw, XRP(60), x(30))); // issuer sells x for XRP
env(offer(gw, usd(100), x(100))); // issuer sells x for usd
env.close();
env(offer(c1, x(100), XRP(100))); // buys x for XRP
env(offer(e1, XRP(70), usd(70))); // sells usd for XRP
env(offer(e1, x(100), eur(100))); // sells eur for x
env.close();
BEAST_EXPECT(expectOffers(env, gw, 2));
BEAST_EXPECT(expectOffers(env, c1, 1));
BEAST_EXPECT(expectOffers(env, e1, 2));
auto const flags = Txflags(tfNoRippleDirect | tfPartialPayment);
// x -> XRP -> x, the issuer's own offer feeds the endpoint
env(pay(gw, dan, x(100)), Sendmax(x(100)), Path(~XRP, ~x), flags, Ter(temBAD_PATH_LOOP));
// x -> XRP -> x -> eur, the issuer's own offer feeds a third book
env(pay(gw, dan, eur(100)),
Sendmax(x(100)),
Path(~XRP, ~x, ~eur),
flags,
Ter(temBAD_PATH_LOOP));
// x -> XRP -> usd -> x, a limiting book before the issuer's own offer
env(pay(gw, dan, x(100)),
Sendmax(x(100)),
Path(~XRP, ~usd, ~x),
flags,
Ter(temBAD_PATH_LOOP));
// Same loop from a holder is already rejected
env(pay(alice, dan, x(10)), Sendmax(x(10)), Path(~XRP, ~x), flags, Ter(temBAD_PATH_LOOP));
env.close();
BEAST_EXPECT(env.balance(gw, x) == x(-10));
BEAST_EXPECT(env.balance(c1, x) == x(0));
BEAST_EXPECT(env.balance(dan, x) == x(0));
// The issuer paying with another asset may still take its own offer
env(pay(gw, dan, x(50)), Sendmax(usd(50)), Path(~x), flags);
env.close();
BEAST_EXPECT(env.balance(dan, x) == x(50));
BEAST_EXPECT(env.balance(gw, x) == x(-60));
}
void
+3 -3
View File
@@ -476,11 +476,11 @@ struct Flow_test : public beast::unit_test::Suite
auto const flowResult = [&] {
STAmount const deliver(usd(51));
STAmount smax(btc(61));
PaymentSandbox sb(env.current().get(), TapNone);
PaymentSandbox sb(env.current().get(), ApplyFlags::None);
STPathSet paths;
auto const ipe = [](Issue const& iss) {
return STPathElement(
STPathElement::TypeCurrency | STPathElement::TypeIssuer,
STPathElementType::Currency | STPathElementType::Issuer,
xrpAccount(),
iss.currency,
iss.account);
@@ -514,7 +514,7 @@ struct Flow_test : public beast::unit_test::Suite
env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
if (flowResult.removableOffers.empty())
return false;
Sandbox sb(&view, TapNone);
Sandbox sb(&view, ApplyFlags::None);
for (auto const& o : flowResult.removableOffers)
{
if (auto const ok = sb.peek(keylet::offer(o)))
+1 -1
View File
@@ -63,7 +63,7 @@ public:
if (stx)
{
OpenView accum(&*res);
applyTransaction(env.app(), accum, *stx, false, TapNone, env.journal);
applyTransaction(env.app(), accum, *stx, false, ApplyFlags::None, env.journal);
accum.apply(*res);
}
res->updateSkipList();
+2 -120
View File
@@ -28,28 +28,17 @@
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/random.h>
#include <xrpl/beast/net/IPAddress.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/xor_shift_engine.h>
#include <xrpl/json/json_value.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/Ledger.h>
#include <xrpl/nodestore/Database.h>
#include <xrpl/nodestore/NodeObject.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerHeader.h>
#include <xrpl/protocol/RippleLedgerHash.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/server/Handoff.h>
#include <xrpl/shamap/Family.h>
#include <xrpl/shamap/SHAMap.h>
#include <xrpl/shamap/SHAMapItem.h>
#include <xrpl/shamap/SHAMapMissingNode.h> // SHAMapType
#include <xrpl/shamap/SHAMapTreeNode.h>
#include <boost/asio/ip/address.hpp>
@@ -75,7 +64,6 @@
#include <string>
#include <thread>
#include <unordered_set>
#include <utility>
#include <vector>
namespace xrpl::test {
@@ -102,7 +90,7 @@ struct LedgerReplay_test : public beast::unit_test::Suite
auto const lastClosedParent = ledgerMaster.getLedgerByHash(lastClosed->header().parentHash);
auto const replayed = buildLedger(
LedgerReplay(lastClosedParent, lastClosed), TapNone, env.app(), env.journal);
LedgerReplay(lastClosedParent, lastClosed), ApplyFlags::None, env.app(), env.journal);
BEAST_EXPECT(replayed->header().hash == lastClosed->header().hash);
}
@@ -134,7 +122,7 @@ struct LedgerReplay_test : public beast::unit_test::Suite
auto const lastClosedParent = ledgerMaster.getLedgerByHash(lastClosed->header().parentHash);
auto const replayed = buildLedger(
LedgerReplay(lastClosedParent, lastClosed), TapNone, env.app(), env.journal);
LedgerReplay(lastClosedParent, lastClosed), ApplyFlags::None, env.app(), env.journal);
BEAST_EXPECT(replayed->header().hash == lastClosed->header().hash);
}
@@ -1102,111 +1090,6 @@ struct LedgerReplayer_test : public beast::unit_test::Suite
}
}
/**
* A ledger whose transaction map cannot be read in full must draw an error reply
* rather than a short transaction list.
*
* The two maps are built in memory and only part of them is written to the store: the
* whole state map, so the ledger loads, and the transaction map's root alone. The
* nodes below that root are never written, so the ledger the handler reads back from
* the store cannot reach them.
*/
void
testReplayDeltaIncompleteTxMap()
{
static constexpr auto kItems = 200;
static constexpr auto kSeed = 0xdeadbeefU;
// Far enough ahead of the server's own history that the missing-node handler finds
// no hash to acquire and returns at once.
static constexpr auto kSeqAhead = 1000;
testcase("ReplayDelta incomplete tx map");
LedgerServer server(*this, {.initLedgers = 1});
auto& family = server.app.getNodeFamily();
// The items only have to be well formed, because the walk stops before the handler
// reads any of them.
beast::XorShiftEngine engine{kSeed};
auto const makeItem = [&engine] {
static constexpr auto kWordsPerItem = 3;
Serializer s;
for (auto word = 0; word < kWordsPerItem; ++word)
s.add32(randInt<std::uint32_t>(engine));
return makeShamapitem(s.getSHA512Half(), s.slice());
};
SHAMap stateMap{SHAMapType::STATE, family};
SHAMap txMap{SHAMapType::TRANSACTION, family};
for (auto i = 0; i < kItems; ++i)
{
stateMap.addItem(SHAMapNodeType::TnAccountState, makeItem());
txMap.addItem(SHAMapNodeType::TnTransactionNm, makeItem());
}
BEAST_EXPECT(stateMap.setImmutable());
BEAST_EXPECT(txMap.setImmutable());
// Read both hashes before storing anything. getHash() is what computes the node
// hashes, through unshare(), so storing first would write every node under a stale
// hash and nothing would be findable afterwards.
LedgerHeader header = server.ledgerMaster.getClosedLedger()->header();
header.seq += kSeqAhead;
header.accountHash = stateMap.getHash().asUInt256();
header.txHash = txMap.getHash().asUInt256();
header.hash = calculateLedgerHash(header);
// Write a map's nodes into the store. visitNodes reports the root first, so
// rootOnly lets that one call through and stops at the next.
auto const storeMap = [&family](SHAMap const& map, bool rootOnly) {
auto stored = 0;
map.visitNodes([&family, rootOnly, &stored](SHAMapTreeNode& node) {
if (rootOnly && stored > 0)
return false;
Serializer s;
node.serializeWithPrefix(s);
family.db().store(
NodeObjectType::AccountNode,
std::move(s.modData()),
node.getHash().asUInt256(),
0);
++stored;
return true;
});
return stored;
};
BEAST_EXPECT(storeMap(stateMap, false) > 1);
BEAST_EXPECT(storeMap(txMap, true) == 1);
bool loaded = false;
auto const holed = std::make_shared<Ledger const>(
header,
loaded,
false,
server.ledgerMaster.getClosedLedger()->rules(),
server.ledgerMaster.getClosedLedger()->fees(),
family,
server.app.getLogs().journal("Ledger"));
// Both roots are readable, so the ledger loads and the handler accepts it.
BEAST_EXPECT(loaded);
BEAST_EXPECT(holed->isImmutable());
server.ledgerMaster.storeLedger(holed);
auto const request = std::make_shared<protocol::TMReplayDeltaRequest>();
request->set_ledgerhash(header.hash.data(), header.hash.size());
auto const reply = server.msgHandler.processReplayDeltaRequest(request);
// The reply names the missing nodes and carries no partial payload.
BEAST_EXPECT(reply.has_error());
BEAST_EXPECT(reply.error() == protocol::TMReplyError::reNO_NODE);
BEAST_EXPECT(reply.transaction_size() == 0);
BEAST_EXPECT(!reply.has_ledgerheader());
}
void
testTaskParameter()
{
@@ -1609,7 +1492,6 @@ struct LedgerReplayer_test : public beast::unit_test::Suite
{
testProofPath();
testReplayDelta();
testReplayDeltaIncompleteTxMap();
testTruncatedHeader();
testTaskParameter();
testConfig();
+4 -3
View File
@@ -994,8 +994,9 @@ class MPToken_test : public beast::unit_test::Suite
test::jtx::MPT const mpT1("MPT", makeMptID(env.seq(alice) + 10, alice));
err = !mpTokensV2 ? Ter(temMALFORMED) : Ter(tecOBJECT_NOT_FOUND);
env(pay(alice, carol, mpT1(100)), Sendmax(mpt(100)), err);
// "paths" is invalid in V1
err = !mpTokensV2 ? Ter(temMALFORMED) : Ter(tesSUCCESS);
// "paths" is invalid in V1. In V2 any path from the issuer back
// to its own MPT is a loop, like it is for a holder.
err = !mpTokensV2 ? Ter(temMALFORMED) : Ter(temBAD_PATH_LOOP);
env(pay(alice, carol, mpt(100)), Path(~usd), err);
}
@@ -4117,7 +4118,7 @@ class MPToken_test : public beast::unit_test::Suite
TER expectedTer,
std::optional<std::uint64_t> expectedOutstanding,
std::string const& label) {
ApplyViewImpl av(&*env.current(), TapNone);
ApplyViewImpl av(&*env.current(), ApplyFlags::None);
auto const ter =
accountSendMulti(av, issuer.id(), asset, receivers, env.app().getJournal("View"));
BEAST_EXPECTS(ter == expectedTer, label);
+14 -2
View File
@@ -777,7 +777,13 @@ class NFTokenBurn_test : public beast::unit_test::Suite
test::StreamSink sink{beast::Severity::Warning};
beast::Journal const jlog{sink};
ApplyContext ac{
env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
env.app(),
ov,
tx,
tesSUCCESS,
env.current()->fees().base,
ApplyFlags::None,
jlog};
// Verify that the last page is present and contains one NFT.
auto const lastNFTokenPage = ac.view().peek(keylet::nftokenPageMax(alice));
@@ -809,7 +815,13 @@ class NFTokenBurn_test : public beast::unit_test::Suite
test::StreamSink sink{beast::Severity::Warning};
beast::Journal const jlog{sink};
ApplyContext ac{
env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
env.app(),
ov,
tx,
tesSUCCESS,
env.current()->fees().base,
ApplyFlags::None,
jlog};
// Verify that the middle page is present.
auto const lastNFTokenPage = ac.view().peek(keylet::nftokenPageMax(alice));
+4 -4
View File
@@ -1964,9 +1964,9 @@ public:
auto const createPathElements = [](auto const& account1, auto const& account2) {
auto base = STPath{};
base.pushBack(
STPathElement{STPathElement::TypeAccount, account1, xrpCurrency(), account1});
STPathElement{STPathElementType::Account, account1, xrpCurrency(), account1});
auto const tail =
STPathElement{STPathElement::TypeAccount, account2, xrpCurrency(), account2};
STPathElement{STPathElementType::Account, account2, xrpCurrency(), account2};
return std::make_pair(base, tail);
};
@@ -2050,7 +2050,7 @@ public:
auto const makePath = [](AccountID const& account) {
auto p = STPath{};
p.pushBack(STPathElement{STPathElement::TypeAccount, account, xrpCurrency(), account});
p.pushBack(STPathElement{STPathElementType::Account, account, xrpCurrency(), account});
return p;
};
@@ -2174,7 +2174,7 @@ public:
},
// An explicit type mask vs. one derived from the populated fields.
{
STPathElement{STPathElement::TypeAccount, kAccount, xrpCurrency(), kIssuer},
STPathElement{STPathElementType::Account, kAccount, xrpCurrency(), kIssuer},
STPathElement{kAccount, PathAsset{xrpCurrency()}, kIssuer, false},
},
};
+76
View File
@@ -31,6 +31,7 @@
#include <xrpl/ledger/Dir.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/ApiVersion.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/KeyType.h>
@@ -1009,6 +1010,7 @@ struct PayChan_test : public beast::unit_test::Suite
auto const jrr =
env.rpc("json", "account_channels", to_string(params))[jss::result];
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'account'.");
};
@@ -1028,6 +1030,7 @@ struct PayChan_test : public beast::unit_test::Suite
auto const jrr =
env.rpc("json", "account_channels", to_string(params))[jss::result];
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'destination_account'.");
};
@@ -1038,6 +1041,54 @@ struct PayChan_test : public beast::unit_test::Suite
testInvalidDestAccountParam(json::Value(json::ValueType::Object));
testInvalidDestAccountParam(json::Value(json::ValueType::Array));
}
{
// An empty destination_account is no destination at all.
json::Value params;
params[jss::account] = alice.human();
params[jss::destination_account] = "";
auto const jrr = env.rpc("json", "account_channels", to_string(params))[jss::result];
BEAST_EXPECT(jrr[jss::channels].size() == 1);
BEAST_EXPECT(jrr[jss::channels][0u][jss::channel_id] == chan1Str);
BEAST_EXPECT(jrr[jss::channels][0u][jss::destination_account] == bob.human());
}
{
// A malformed account is reported without the ledger fields.
json::Value params;
params[jss::account] = "llIIOO";
auto const jrr = env.rpc("json", "account_channels", to_string(params))[jss::result];
BEAST_EXPECT(jrr[jss::error] == "actMalformed");
BEAST_EXPECT(jrr[jss::error_code] == RpcActMalformed);
BEAST_EXPECT(!jrr.isMember(jss::ledger_index));
BEAST_EXPECT(!jrr.isMember(jss::ledger_current_index));
}
{
// A bad limit is reported before an account that is not found.
json::Value params;
params[jss::account] = Account("bogie").human();
params[jss::limit] = 0;
auto const jrr = env.rpc("json", "account_channels", to_string(params))[jss::result];
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'limit'.");
}
{
// A non-string marker names the field; one that does not parse does not.
json::Value params;
params[jss::account] = alice.human();
params[jss::marker] = 1;
auto const notString =
env.rpc("json", "account_channels", to_string(params))[jss::result];
BEAST_EXPECT(notString[jss::error] == "invalidParams");
BEAST_EXPECT(notString[jss::error_code] == RpcInvalidParams);
BEAST_EXPECT(notString[jss::error_message] == "Invalid field 'marker', not string.");
params[jss::marker] = "NOT_A_MARKER";
auto const malformed =
env.rpc("json", "account_channels", to_string(params))[jss::result];
BEAST_EXPECT(malformed[jss::error] == "invalidParams");
BEAST_EXPECT(malformed[jss::error_code] == RpcInvalidParams);
BEAST_EXPECT(malformed[jss::error_message] == "Invalid parameters.");
}
{
auto const r = env.rpc("account_channels", alice.human(), bob.human());
BEAST_EXPECT(r[jss::result][jss::channels].size() == 1);
@@ -1183,6 +1234,31 @@ struct PayChan_test : public beast::unit_test::Suite
auto const r = testLimit(env, alice, 0);
BEAST_EXPECT(r.isMember(jss::error_message));
}
{
// The marker's start hint may have a leading '+', and anything after a second
// comma is ignored.
auto const first = testLimit(env, alice, 1);
auto const marker = first[jss::marker].asString();
auto const comma = marker.find(',');
BEAST_EXPECT(comma != std::string::npos);
auto const rest = testLimit(
env,
alice,
std::nullopt,
marker.substr(0, comma) + ",+" + marker.substr(comma + 1) + ",ignored");
BEAST_EXPECT(rest[jss::channels].size() == bobs.size() - 1);
std::set<std::string> found{
first[jss::channels][0u][jss::destination_account].asString(),
};
for (auto const& c : rest[jss::channels])
BEAST_EXPECT(found.insert(c[jss::destination_account].asString()).second);
BEAST_EXPECT(found == bobsB58);
}
}
void
+2 -2
View File
@@ -256,7 +256,7 @@ struct PayStrandMPT_test : public beast::unit_test::Suite
usd,
STPath({
STPathElement{
STPathElement::TypeCurrency, xrpAccount(), xrpCurrency(), xrpAccount()},
STPathElementType::Currency, xrpAccount(), xrpCurrency(), xrpAccount()},
}),
tesSUCCESS,
makeEndpointStep(alice, gw, usd),
@@ -583,7 +583,7 @@ struct PayStrandMPT_test : public beast::unit_test::Suite
inputs.defaultPathsAllowed = true;
try
{
PaymentSandbox sb{env.current().get(), TapNone};
PaymentSandbox sb{env.current().get(), ApplyFlags::None};
{
auto const r = ::xrpl::path::RippleCalc::rippleCalculate(
sb,
+5 -4
View File
@@ -225,7 +225,7 @@ public:
else
{
col.emplace_back(
STPathElement::Type::TypeBoundary,
STPathElementType::Boundary,
acc.value_or(AccountID{}),
cur.value_or(Currency{}),
iss.value_or(AccountID{}));
@@ -684,7 +684,7 @@ struct PayStrand_test : public beast::unit_test::Suite
usd,
STPath({
STPathElement{
STPathElement::TypeCurrency, xrpAccount(), xrpCurrency(), xrpAccount()},
STPathElementType::Currency, xrpAccount(), xrpCurrency(), xrpAccount()},
}),
tesSUCCESS,
D{.src = alice, .dst = gw, .currency = usdC},
@@ -770,7 +770,8 @@ struct PayStrand_test : public beast::unit_test::Suite
env,
usd,
std::nullopt,
STPath({STPathElement(0, xrpAccount(), xrpCurrency(), xrpAccount())}),
STPath({STPathElement(
STPathElementType::None, xrpAccount(), xrpCurrency(), xrpAccount())}),
temBAD_PATH);
// The same account can't appear more than once on a path
@@ -1078,7 +1079,7 @@ struct PayStrand_test : public beast::unit_test::Suite
inputs.defaultPathsAllowed = true;
try
{
PaymentSandbox sb{env.current().get(), TapNone};
PaymentSandbox sb{env.current().get(), ApplyFlags::None};
{
auto const r = ::xrpl::path::RippleCalc::rippleCalculate(
sb,
+1 -1
View File
@@ -88,7 +88,7 @@ struct PseudoTx_test : public beast::unit_test::Suite
BEAST_EXPECT(!passesLocalChecks(stx, reason));
BEAST_EXPECT(reason == "Cannot submit pseudo transactions.");
env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
auto const result = xrpl::apply(env.app(), view, stx, TapNone, j);
auto const result = xrpl::apply(env.app(), view, stx, ApplyFlags::None, j);
BEAST_EXPECT(!result.applied && result.ter == temINVALID);
return result.applied;
});
+4 -2
View File
@@ -103,7 +103,8 @@ struct Regression_test : public beast::unit_test::Suite
auto const jt = env.jt(pay(env.master, "alice", aliceAmount));
OpenView accum(&*next);
auto const result = xrpl::apply(env.app(), accum, *jt.stx, TapNone, env.journal);
auto const result =
xrpl::apply(env.app(), accum, *jt.stx, ApplyFlags::None, env.journal);
BEAST_EXPECT(isTesSuccess(result.ter));
BEAST_EXPECT(result.applied);
@@ -126,7 +127,8 @@ struct Regression_test : public beast::unit_test::Suite
OpenView accum(&*next);
auto const result = xrpl::apply(env.app(), accum, *jt.stx, TapNone, env.journal);
auto const result =
xrpl::apply(env.app(), accum, *jt.stx, ApplyFlags::None, env.journal);
BEAST_EXPECT(result.ter == tecINSUFF_FEE);
BEAST_EXPECT(result.applied);
+190 -8
View File
@@ -59,6 +59,7 @@
#include <xrpl/tx/apply.h>
#include <xrpl/tx/applySteps.h>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <limits>
@@ -989,8 +990,8 @@ public:
sponsor::SponseeAcc(alice),
Seq(1),
Fee(baseFee));
auto const pf =
preflight(env.app(), env.current()->rules(), *jt.stx, TapNone, env.journal);
auto const pf = preflight(
env.app(), env.current()->rules(), *jt.stx, ApplyFlags::None, env.journal);
BEAST_EXPECT(isTesSuccess(pf.ter));
BEAST_EXPECT(!pf.consequences.isBlocker());
BEAST_EXPECT(pf.consequences.fee() == drops(baseFee));
@@ -1005,8 +1006,8 @@ public:
sponsor::SponseeAcc(alice),
Seq(1),
Fee(baseFee));
auto const pf =
preflight(env.app(), env.current()->rules(), *jt.stx, TapNone, env.journal);
auto const pf = preflight(
env.app(), env.current()->rules(), *jt.stx, ApplyFlags::None, env.journal);
BEAST_EXPECT(isTesSuccess(pf.ter));
BEAST_EXPECT(!pf.consequences.isBlocker());
BEAST_EXPECT(pf.consequences.fee() == drops(baseFee));
@@ -1020,8 +1021,8 @@ public:
sponsor::SponseeAcc(alice),
Seq(1),
Fee(baseFee));
auto const pf =
preflight(env.app(), env.current()->rules(), *jt.stx, TapNone, env.journal);
auto const pf = preflight(
env.app(), env.current()->rules(), *jt.stx, ApplyFlags::None, env.journal);
BEAST_EXPECT(isTesSuccess(pf.ter));
BEAST_EXPECT(!pf.consequences.isBlocker());
BEAST_EXPECT(pf.consequences.fee() == drops(baseFee));
@@ -2317,7 +2318,8 @@ public:
Seq(env.seq(alice)),
sponsor::As(carol, spfSponsorFee));
auto const result = xrpl::apply(env.app(), overlay, *jt.stx, TapNone, env.journal);
auto const result =
xrpl::apply(env.app(), overlay, *jt.stx, ApplyFlags::None, env.journal);
BEAST_EXPECT(result.ter == tecINSUFF_FEE);
BEAST_EXPECT(result.applied);
@@ -2370,7 +2372,8 @@ public:
Fee(fixFee),
sponsor::As(sponsor, spfSponsorFee));
auto const pf = preflight(env.app(), overlay.rules(), *jt.stx, TapNone, env.journal);
auto const pf =
preflight(env.app(), overlay.rules(), *jt.stx, ApplyFlags::None, env.journal);
BEAST_EXPECT(isTesSuccess(pf.ter));
auto const pc = preclaim(pf, env.app(), overlay);
BEAST_EXPECT(isTesSuccess(pc.ter));
@@ -3550,6 +3553,101 @@ public:
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0);
}
{
// Bytecode Escrow: its reserve scales with the Bytecode size, so
// every leg of a sponsorship has to move the whole weight.
auto env = Env{*this, testableAmendments()};
env.fund(XRP(1000000), alice, bob, sponsor, sponsor2);
env.close();
auto const seq = env.seq(alice);
env(escrow::create(alice, bob, XRP(100)),
escrow::kCondition(escrow::kCb1),
escrow::kCancelTime(env.now() + 100s));
env.close();
auto const escrowKeylet = keylet::escrow(alice, SeqProxy::rawSequence(seq));
std::size_t const bytecodeSize = 1200;
std::uint32_t const bytecodeWeight = 3; // ceil(1200 / 500)
if (!cosigning)
{
env(sponsor::set_reserve(sponsor, 0, bytecodeWeight), sponsor::SponseeAcc(alice));
env(sponsor::set_reserve(sponsor2, 0, bytecodeWeight), sponsor::SponseeAcc(alice));
env.close();
}
// EscrowCreate's preclaim runs the WASM preflight, so a real
// 1200-byte contract cannot be assembled from here; the Bytecode
// field is injected directly and the owner's OwnerCount restored
// to the weight such a create would have charged.
env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal) -> bool {
auto const existing = view.read(escrowKeylet);
if (!existing)
{
return false;
}
auto const escrowSle = std::make_shared<SLE>(*existing);
auto const account = std::make_shared<SLE>(*view.read(keylet::account(alice.id())));
escrowSle->setFieldVL(sfBytecode, Blob(bytecodeSize, 0x00));
account->setFieldU32(sfOwnerCount, bytecodeWeight);
view.rawReplace(escrowSle);
view.rawReplace(account);
return true;
});
BEAST_EXPECT(ownerCount(env, alice) == bytecodeWeight);
std::optional<Sig> const sponsorSig =
cosigning ? std::optional<Sig>(Sig(sfSponsorSignature, sponsor)) : std::nullopt;
std::optional<Sig> const sponsor2Sig =
cosigning ? std::optional<Sig>(Sig(sfSponsorSignature, sponsor2)) : std::nullopt;
// create sponsor: charges the whole bytecode weight, not 1
if (sponsorSig)
{
env(sponsor::transfer(alice, tfSponsorshipCreate, escrowKeylet.key),
sponsor::As(sponsor, spfSponsorReserve),
*sponsorSig);
}
else
{
env(sponsor::transfer(alice, tfSponsorshipCreate, escrowKeylet.key),
sponsor::As(sponsor, spfSponsorReserve));
}
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == bytecodeWeight);
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == bytecodeWeight);
BEAST_EXPECT(env.le(escrowKeylet)->getAccountID(sfSponsor) == sponsor.id());
// transfer sponsor: moves the whole weight between the two sponsors
if (sponsor2Sig)
{
env(sponsor::transfer(alice, tfSponsorshipReassign, escrowKeylet.key),
sponsor::As(sponsor2, spfSponsorReserve),
*sponsor2Sig);
}
else
{
env(sponsor::transfer(alice, tfSponsorshipReassign, escrowKeylet.key),
sponsor::As(sponsor2, spfSponsorReserve));
}
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == bytecodeWeight);
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == bytecodeWeight);
BEAST_EXPECT(env.le(escrowKeylet)->getAccountID(sfSponsor) == sponsor2.id());
// end sponsor: hands the whole weight back to the owner
env(sponsor::transfer(alice, tfSponsorshipEnd, escrowKeylet.key));
BEAST_EXPECT(ownerCount(env, alice) == bytecodeWeight);
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0);
BEAST_EXPECT(!env.le(escrowKeylet)->isFieldPresent(sfSponsor));
}
Account const gw("gw");
auto const usd = gw["usd"];
{
@@ -4405,6 +4503,89 @@ public:
BEAST_EXPECT(!sle->isFieldPresent(sfRemainingOwnerCount));
}
// An escrow carrying Bytecode costs one owner count per 500 bytes, so
// sponsoring one must move that full weight rather than a single unit.
void
testBytecodeEscrowReserve()
{
testcase("Bytecode escrow sponsorship reserve");
using namespace test::jtx;
using namespace std::chrono_literals;
auto const alice = Account{"alice"};
auto const bob = Account{"bob"};
auto const sponsor = Account{"sponsor"};
auto env = Env{*this, testableAmendments()};
env.fund(XRP(1000000), alice, bob, sponsor);
env.close();
auto const seq = env.seq(alice);
env(escrow::create(alice, bob, XRP(100)),
escrow::kCondition(escrow::kCb1),
escrow::kCancelTime(env.now() + 100s));
env.close();
auto const escrowKeylet = keylet::escrow(alice, SeqProxy::rawSequence(seq));
auto const sponsorKeylet = keylet::sponsorship(sponsor.id(), alice.id());
std::size_t const bytecodeSize = 1200;
std::uint32_t const bytecodeWeight = 3; // ceil(1200 / 500)
BEAST_EXPECT(ownerCount(env, alice) == 1);
// Pre-fund exactly the bytecode weight
env(sponsor::set_reserve(sponsor, 0, bytecodeWeight), sponsor::SponseeAcc(alice));
env.close();
if (auto const sle = env.le(sponsorKeylet); BEAST_EXPECT(sle))
{
BEAST_EXPECT(sle->getFieldU32(sfRemainingOwnerCount) == bytecodeWeight);
}
// Synthesize a bytecode escrow. EscrowCreate's preclaim runs the WASM
// preflight, so a real 1200-byte contract cannot be assembled from
// here; the Bytecode field is injected directly and the owner's
// OwnerCount restored to the weight such a create would have charged.
env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal) -> bool {
auto const existing = view.read(escrowKeylet);
if (!existing)
{
return false;
}
auto const escrowSle = std::make_shared<SLE>(*existing);
auto const account = std::make_shared<SLE>(*view.read(keylet::account(alice.id())));
escrowSle->setFieldVL(sfBytecode, Blob(bytecodeSize, 0x00));
account->setFieldU32(sfOwnerCount, bytecodeWeight);
view.rawReplace(escrowSle);
view.rawReplace(account);
return true;
});
BEAST_EXPECT(ownerCount(env, alice) == bytecodeWeight);
// Create must charge the full bytecode weight (3), not 1: counting the
// escrow as a single unit would leave 2 pre-funded units unspent.
env(sponsor::transfer(alice, tfSponsorshipCreate, escrowKeylet.key),
sponsor::As(sponsor, spfSponsorReserve));
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == bytecodeWeight);
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == bytecodeWeight);
// All pre-funded units consumed (drained to absent).
if (auto const sle = env.le(sponsorKeylet); BEAST_EXPECT(sle))
{
BEAST_EXPECT(!sle->isFieldPresent(sfRemainingOwnerCount));
}
// End hands the same weight back; create bumped by the same amount, so
// the counters return to 0 (counting 1 here would underflow).
env(sponsor::transfer(alice, tfSponsorshipEnd, escrowKeylet.key));
BEAST_EXPECT(ownerCount(env, alice) == bytecodeWeight);
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
if (auto const sle = env.le(escrowKeylet); BEAST_EXPECT(sle))
{
BEAST_EXPECT(!sle->isFieldPresent(sfSponsor));
}
}
void
testSponsoredTrustLineNoFreeReserve()
{
@@ -5790,6 +5971,7 @@ protected:
testTransferSponsor(jtx::testableAmendments());
testTransferSponsor(jtx::testableAmendments() - fixCleanup3_4_0);
testLegacySignerListReserve();
testBytecodeEscrowReserve();
testSponsorFee();
testSponsorAccount();
+1 -1
View File
@@ -252,7 +252,7 @@ class TheoreticalQuality_test : public beast::unit_test::Suite
std::shared_ptr<ReadView const> closed,
std::optional<Quality> const& expectedQ = {})
{
PaymentSandbox const sb(closed.get(), TapNone);
PaymentSandbox const sb(closed.get(), ApplyFlags::None);
AMMContext ammContext(rcp.srcAccount, false);
auto const sendMaxIssue = [&rcp] -> std::optional<Asset> {
@@ -215,8 +215,9 @@ struct TransactionProposalCancel_test : public beast::unit_test::Suite
}
// The target account may refuse any proposal made for it, at any point in
// its lifecycle, without owning the object: anyone can create a proposal
// against any account and tie up one of its tickets (On-Chain Cosigner spec §7.2).
// its lifecycle, without owning the object: any of its signers, an
// authorized delegate, or that delegate's signers can create one and tie
// up one of its tickets (XLS-0103 §7.2).
void
testTargetCancel(FeatureBitset features)
{
@@ -683,6 +684,25 @@ struct TransactionProposalCancel_test : public beast::unit_test::Suite
BEAST_EXPECT(env.le(keylet::txProposal(proposalID)));
BEAST_EXPECT(ownerCount(env, alice) == proposal::kProposalOwnerCount);
}
// Paying with a ticket changes none of that. The failed cancel still
// consumes carol's ticket, and nothing else moves: the proposal and
// its reserve stay as they were.
{
uint256 const proposalID = makeProposal(env, alice, target, bob);
std::uint32_t const cancelTicket = proposal::createTicket(env, carol);
auto const aliceOwnerCount = ownerCount(env, alice);
env(proposal::cancel(carol, proposalID),
ticket::Use(cancelTicket),
Ter(tecNO_PERMISSION));
env.close();
BEAST_EXPECT(env.le(keylet::txProposal(proposalID)));
BEAST_EXPECT(!env.le(keylet::ticket(carol.id(), SeqProxy::rawTicket(cancelTicket))));
BEAST_EXPECT(ownerCount(env, alice) == aliceOwnerCount);
BEAST_EXPECT(ownerCount(env, carol) == 0);
}
}
// A Cancel works as an inner Batch transaction, where each inner runs its
@@ -1357,6 +1357,51 @@ struct TransactionProposalCreate_test : public beast::unit_test::Suite
env.close();
}
// A proposer may pay for the Create with one of its own Tickets. The
// Ticket's reserve goes as the proposal's arrives, so the proposer's owner
// count moves by the difference.
void
testTicketedCreate(FeatureBitset features)
{
testcase("create paid for with the proposer's own ticket");
using namespace jtx;
using namespace std::chrono_literals;
Env env{*this, features};
Account const alice{"alice"}; // the proposer
Account const target{"target"}; // the account the proposal is for
Account const bob{"bob"};
env.fund(XRP(10000), alice, target, bob);
env.close();
proposal::authorizeProposer(env, target, alice);
std::uint32_t const targetTicketSeq = proposal::createTicket(env, target);
std::uint32_t const aliceTicketSeq = proposal::createTicket(env, alice);
std::uint32_t const aliceSeq = env.seq(alice);
std::uint32_t const targetOwnerCount = ownerCount(env, target);
BEAST_EXPECT(ownerCount(env, alice) == 1);
// The proposal's reserve lands on alice less the Ticket she paid with,
// and her Sequence is untouched.
env(proposal::create(
alice,
proposal::unsignedPayload(env, pay(target, bob, XRP(1)), targetTicketSeq),
proposal::expiration(env, 100s)),
ticket::Use(aliceTicketSeq),
proposal::verify::create());
env.close();
BEAST_EXPECT(proposal::entry(env, target, targetTicketSeq));
BEAST_EXPECT(!env.le(keylet::ticket(alice.id(), SeqProxy::rawTicket(aliceTicketSeq))));
BEAST_EXPECT(env.seq(alice) == aliceSeq);
BEAST_EXPECT(ownerCount(env, alice) == proposal::kProposalOwnerCount);
// The target's Ticket, the one the proposal is keyed to, stays.
BEAST_EXPECT(ownerCount(env, target) == targetOwnerCount);
}
// The proposal's reserve can instead be sponsored: the reserve is charged
// to the sponsor's account, and the ledger object records the sponsor, the
// same as any other reserve-sponsorable object (TransactionProposalCreate
@@ -1728,6 +1773,7 @@ struct TransactionProposalCreate_test : public beast::unit_test::Suite
testOtherTransactionTypes(all);
testAuxiliaryCoSignatureTypes(all);
testReserve(all);
testTicketedCreate(all);
testSponsoredReserve(all);
testSponsorshipTransfer(all);
testFeeSponsored(all);
@@ -1603,7 +1603,7 @@ struct TransactionProposalReservedTicket_test : public beast::unit_test::Suite
auto const stx = rebuilt(mutated);
env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
auto const result = xrpl::apply(env.app(), view, *stx, TapNone, j);
auto const result = xrpl::apply(env.app(), view, *stx, ApplyFlags::None, j);
BEAST_EXPECT(result.ter == temBAD_SIGNATURE);
BEAST_EXPECT(!result.applied);
return result.applied;
@@ -1639,7 +1639,7 @@ struct TransactionProposalReservedTicket_test : public beast::unit_test::Suite
auto const stx = rebuilt(mutated);
env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
auto const result = xrpl::apply(env.app(), view, *stx, TapNone, j);
auto const result = xrpl::apply(env.app(), view, *stx, ApplyFlags::None, j);
BEAST_EXPECT(result.ter == temINVALID);
BEAST_EXPECT(!result.applied);
return result.applied;
+10 -9
View File
@@ -989,7 +989,8 @@ public:
Env::ParsedResult parsed;
env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
auto const result = xrpl::apply(env.app(), view, *jt.stx, TapNone, env.journal);
auto const result =
xrpl::apply(env.app(), view, *jt.stx, ApplyFlags::None, env.journal);
parsed.ter = result.ter;
return result.applied;
});
@@ -2428,8 +2429,8 @@ public:
env.memoize("carol");
{
auto const jtx = env.jt(offerCancel(alice, 3), Seq(5), Fee(10));
auto const pf =
preflight(env.app(), env.current()->rules(), *jtx.stx, TapNone, env.journal);
auto const pf = preflight(
env.app(), env.current()->rules(), *jtx.stx, ApplyFlags::None, env.journal);
BEAST_EXPECT(isTesSuccess(pf.ter));
BEAST_EXPECT(!pf.consequences.isBlocker());
BEAST_EXPECT(pf.consequences.fee() == drops(10));
@@ -2440,8 +2441,8 @@ public:
auto const usd = alice["USD"];
auto const jtx = env.jt(trust("carol", usd(50000000)), Seq(1), Fee(10));
auto const pf =
preflight(env.app(), env.current()->rules(), *jtx.stx, TapNone, env.journal);
auto const pf = preflight(
env.app(), env.current()->rules(), *jtx.stx, ApplyFlags::None, env.journal);
BEAST_EXPECT(isTesSuccess(pf.ter));
BEAST_EXPECT(!pf.consequences.isBlocker());
BEAST_EXPECT(pf.consequences.fee() == drops(10));
@@ -2450,8 +2451,8 @@ public:
{
auto const jtx = env.jt(ticket::create(alice, 1), Seq(1), Fee(10));
auto const pf =
preflight(env.app(), env.current()->rules(), *jtx.stx, TapNone, env.journal);
auto const pf = preflight(
env.app(), env.current()->rules(), *jtx.stx, ApplyFlags::None, env.journal);
BEAST_EXPECT(isTesSuccess(pf.ter));
BEAST_EXPECT(!pf.consequences.isBlocker());
BEAST_EXPECT(pf.consequences.fee() == drops(10));
@@ -4010,7 +4011,7 @@ public:
// be callable via RPC.)
env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
auto const tx = env.jt(noop(alice), Seq(aliceSeq), Fee(openLedgerCost(env)));
auto const result = xrpl::apply(env.app(), view, *tx.stx, TapUnlimited, j);
auto const result = xrpl::apply(env.app(), view, *tx.stx, ApplyFlags::Unlimited, j);
BEAST_EXPECT(isTesSuccess(result.ter) && result.applied);
return result.applied;
});
@@ -4078,7 +4079,7 @@ public:
// be callable via RPC.)
env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
auto const tx = env.jt(noop(alice), ticket::Use(tktSeq0 + 1), Fee(openLedgerCost(env)));
auto const result = xrpl::apply(env.app(), view, *tx.stx, TapUnlimited, j);
auto const result = xrpl::apply(env.app(), view, *tx.stx, ApplyFlags::Unlimited, j);
BEAST_EXPECT(isTesSuccess(result.ter) && result.applied);
return result.applied;
});
+2 -1
View File
@@ -122,7 +122,8 @@ InvariantsBase::doInvariantCheck(
OpenView ov{*env.current()};
test::StreamSink sink{beast::Severity::Warning};
beast::Journal const jlog{sink};
ApplyContext ac{env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
ApplyContext ac{
env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, ApplyFlags::None, jlog};
// Invariants normally run in the Transaction's "apply" (operator()) context, and can always
// access global Rules.
@@ -4,6 +4,7 @@
#include <test/jtx/Env.h>
#include <test/jtx/TestHelpers.h>
#include <test/jtx/amount.h>
#include <test/jtx/escrow.h>
#include <test/jtx/pay.h>
#include <test/jtx/token.h>
#include <test/jtx/trust.h>
@@ -844,7 +845,13 @@ class InvariantsMisc_test : public InvariantsBase
test::StreamSink sink{beast::Severity::Warning};
beast::Journal const jlog{sink};
ApplyContext ac{
env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
env.app(),
ov,
tx,
tesSUCCESS,
env.current()->fees().base,
ApplyFlags::None,
jlog};
CurrentTransactionRulesGuard const rulesGuard(ov.rules());
auto const sleLoan = ac.view().peek(loanKeylet);
@@ -899,7 +906,13 @@ class InvariantsMisc_test : public InvariantsBase
test::StreamSink sink{beast::Severity::Warning};
beast::Journal const jlog{sink};
ApplyContext ac{
env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
env.app(),
ov,
tx,
tesSUCCESS,
env.current()->fees().base,
ApplyFlags::None,
jlog};
CurrentTransactionRulesGuard const rulesGuard(ov.rules());
auto const sleLoan = ac.view().peek(loanKeylet);
@@ -1022,7 +1035,13 @@ class InvariantsMisc_test : public InvariantsBase
test::StreamSink sink{beast::Severity::Warning};
beast::Journal const jlog{sink};
ApplyContext ac{
env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
env.app(),
ov,
tx,
tesSUCCESS,
env.current()->fees().base,
ApplyFlags::None,
jlog};
CurrentTransactionRulesGuard const rulesGuard(ov.rules());
auto const sleLoan = ac.view().peek(loanKeylet);
@@ -1330,6 +1349,51 @@ class InvariantsMisc_test : public InvariantsBase
env(check::create(a1, a2, XRP(1)));
return true;
});
// An escrow's owner count scales with its Bytecode size, so
// sponsoring one has to move that whole weight. Counting it as a
// single unit leaves the sponsored counters short of the object's
// real weight, which is what this check exists to catch.
UInt256 escrowID;
doInvariantCheck(
{{expectMessage}},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
auto const escrow = ac.view().peek(keylet::unchecked(escrowID));
if (!escrow)
{
return false;
}
// ceil(1200 / 500) == 3 owner counts
escrow->setFieldVL(sfBytecode, Blob(1200, 0x00));
escrow->setAccountID(sfSponsor, a2.id());
ac.view().update(escrow);
// Move the counters by a single unit, as a sponsorship
// blind to the Bytecode weight would.
auto const sle1 = ac.view().peek(keylet::account(a1.id()));
auto const sle2 = ac.view().peek(keylet::account(a2.id()));
if (!sle1 || !sle2)
{
return false;
}
sle1->setFieldU32(sfSponsoredOwnerCount, 1);
sle2->setFieldU32(sfSponsoringOwnerCount, 1);
ac.view().update(sle1);
ac.view().update(sle2);
return true;
},
XRPAmount{},
STTx{ttACCOUNT_SET, [](STObject&) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
[&escrowID](Account const& a1, Account const& a2, Env& env) {
using namespace std::chrono_literals;
escrowID = keylet::escrow(a1.id(), SeqProxy::rawSequence(env.seq(a1))).key;
env(escrow::create(a1, a2, XRP(1)),
escrow::kCondition(escrow::kCb1),
escrow::kCancelTime(env.now() + 100s));
return true;
});
}
{
@@ -1461,7 +1525,7 @@ class InvariantsMisc_test : public InvariantsBase
test::StreamSink sink{beast::Severity::Warning};
beast::Journal const jlog{sink};
ApplyContext ac{
env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, ApplyFlags::None, jlog};
CurrentTransactionRulesGuard const rulesGuard(ov.rules());
auto const sle = ac.view().peek(vaultKeylet);
@@ -1528,7 +1592,7 @@ class InvariantsMisc_test : public InvariantsBase
test::StreamSink sink{beast::Severity::Warning};
beast::Journal const jlog{sink};
ApplyContext ac{
env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, ApplyFlags::None, jlog};
CurrentTransactionRulesGuard const rulesGuard(ov.rules());
// visitEntry only runs for entries the transaction touched, so
@@ -1585,7 +1649,8 @@ class InvariantsMisc_test : public InvariantsBase
STTx const tx{ttACCOUNT_SET, [](STObject&) {}};
test::StreamSink sink{beast::Severity::Warning};
beast::Journal const jlog{sink};
ApplyContext ac{env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
ApplyContext ac{
env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, ApplyFlags::None, jlog};
CurrentTransactionRulesGuard const rulesGuard(ov.rules());
FailingTxInvariantCheck failing;
@@ -681,7 +681,7 @@ class InvariantsPseudoAccount_test : public InvariantsBase
test::StreamSink sink{beast::Severity::Warning};
beast::Journal const jlog{sink};
ApplyContext ac{
env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, ApplyFlags::None, jlog};
CurrentTransactionRulesGuard const rulesGuard(ov.rules());
auto const sleBroker = ac.view().peek(brokerKeylet);
@@ -733,7 +733,7 @@ class InvariantsPseudoAccount_test : public InvariantsBase
test::StreamSink sink{beast::Severity::Warning};
beast::Journal const jlog{sink};
ApplyContext ac{
env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, ApplyFlags::None, jlog};
CurrentTransactionRulesGuard const rulesGuard(ov.rules());
auto const sleBroker = ac.view().peek(brokerKeylet);
@@ -781,7 +781,7 @@ class InvariantsPseudoAccount_test : public InvariantsBase
test::StreamSink sink{beast::Severity::Warning};
beast::Journal const jlog{sink};
ApplyContext ac{
env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, ApplyFlags::None, jlog};
CurrentTransactionRulesGuard const rulesGuard(ov.rules());
auto const sleBroker = ac.view().peek(brokerKeylet);
@@ -0,0 +1,549 @@
#include <test/app/invariants/InvariantsBase.h>
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/amount.h>
#include <test/jtx/pay.h>
#include <test/jtx/proposal.h>
#include <test/jtx/utility.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/helpers/ProposalHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/SeqProxy.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/ApplyContext.h>
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <initializer_list>
#include <memory>
#include <optional>
#include <source_location>
#include <string>
#include <utility>
#include <vector>
namespace xrpl::test {
class InvariantsTransactionProposal_test : public InvariantsBase
{
void
testTransactionProposalInvariants()
{
using namespace jtx;
using namespace std::chrono_literals;
// Submits a real proposal, so the entry is in the closed ledger before
// the precheck runs and the precheck can modify it.
std::optional<Keylet> liveProposalKeylet;
std::optional<std::uint32_t> liveTicketSeq;
Preclose const precloseLiveProposal =
[&](Account const& owner, Account const& destination, Env& env) {
auto const ticketSeq = proposal::createTicket(env, owner);
auto const proposedTx =
proposal::unsignedPayload(env, pay(owner, destination, XRP(1)), ticketSeq);
liveProposalKeylet = keylet::txProposal(owner.id(), ticketSeq);
liveTicketSeq = ticketSeq;
env(proposal::create(owner, proposedTx, proposal::expiration(env, 100s)));
return BEAST_EXPECT(env.le(*liveProposalKeylet));
};
auto peekLiveProposal = [&](ApplyContext& ac) -> SLE::pointer {
if (!liveProposalKeylet)
return nullptr;
return ac.view().peek(*liveProposalKeylet);
};
// Deletes the live proposal as deleteProposal would: out of the
// owner's directory, with the owner's reserve released.
auto deleteLiveProposal = [&](Account const& owner, ApplyContext& ac) {
auto const sle = peekLiveProposal(ac);
if (!sle)
return false;
auto const account = ac.view().peek(keylet::account(owner.id()));
if (!account)
return false;
account->at(sfOwnerCount) -=
xrpl::proposal::proposalOwnerCount(sle->getFieldObject(sfProposedTransaction));
ac.view().update(account);
if (!ac.view().dirRemove(
keylet::ownerDir(owner.id()), sle->getFieldU64(sfOwnerNode), sle->key(), false))
return false;
ac.view().erase(sle);
return true;
};
// Consumes one of the owner's Tickets as ticketDelete would.
auto deleteTicket = [&](Account const& owner, std::uint32_t ticketSeq, ApplyContext& ac) {
auto const ticket =
ac.view().peek(keylet::ticket(owner.id(), SeqProxy::rawTicket(ticketSeq)));
auto const account = ac.view().peek(keylet::account(owner.id()));
if (!ticket || !account)
return false;
if (!ac.view().dirRemove(
keylet::ownerDir(owner.id()),
ticket->getFieldU64(sfOwnerNode),
ticket->key(),
false))
return false;
if (auto const ticketCount = account->getFieldU32(sfTicketCount); ticketCount > 1)
{
account->setFieldU32(sfTicketCount, ticketCount - 1);
}
else
{
account->makeFieldAbsent(sfTicketCount);
}
account->at(sfOwnerCount) -= 1;
ac.view().update(account);
ac.view().erase(ticket);
return true;
};
// A transaction outside the whitelist may not modify a proposal at
// all; the invariant rejects it on the count, whatever field changed.
testcase("TransactionProposal modified outside the whitelist");
doInvariantCheck(
{{"TransactionProposal changes do not match transaction result"}},
[&](Account const&, Account const&, ApplyContext& ac) {
auto const sle = peekLiveProposal(ac);
if (!sle)
return false;
auto proposedTx = sle->getFieldObject(sfProposedTransaction);
proposedTx.setFieldU32(
sfTicketSequence, proposedTx.getFieldU32(sfTicketSequence) + 1);
sle->setFieldObject(sfProposedTransaction, proposedTx);
ac.view().update(sle);
return true;
},
XRPAmount{},
STTx{ttACCOUNT_SET, [](STObject&) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
precloseLiveProposal);
// Only stages the payload and the key it would live under; nothing
// reaches the ledger, so a precheck that inserts the entry by hand
// presents the invariant with a creation.
std::optional<STObject> stagedPayload;
std::optional<Keylet> stagedProposalKeylet;
Preclose const precloseStagedPayload =
[&](Account const& owner, Account const& destination, Env& env) {
auto const ticketSeq = proposal::createTicket(env, owner);
auto const proposedJson =
proposal::unsignedPayload(env, pay(owner, destination, XRP(1)), ticketSeq);
stagedPayload.emplace(parse(proposedJson));
stagedProposalKeylet = keylet::txProposal(owner.id(), ticketSeq);
return true;
};
// Inserts the staged proposal as doApply would, with the owner's
// reserve charged.
auto insertStagedProposal = [&](Account const& owner, ApplyContext& ac) {
if (!stagedPayload || !stagedProposalKeylet)
return false;
auto const sle = std::make_shared<SLE>(*stagedProposalKeylet);
sle->setAccountID(sfOwner, owner.id());
sle->setFieldObject(sfProposedTransaction, *stagedPayload);
sle->setFieldU32(sfExpiration, 1);
sle->setFieldU64(sfOwnerNode, 0);
ac.view().insert(sle);
auto const account = ac.view().peek(keylet::account(owner.id()));
if (!account)
return false;
account->at(sfOwnerCount) += xrpl::proposal::proposalOwnerCount(*stagedPayload);
ac.view().update(account);
return true;
};
testcase("TransactionProposal effect whitelist");
doInvariantCheck(
{{"TransactionProposal changes do not match transaction result"}},
[&](Account const& owner, Account const&, ApplyContext& ac) {
return insertStagedProposal(owner, ac);
},
XRPAmount{},
STTx{ttACCOUNT_SET, [](STObject&) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
precloseStagedPayload);
// The harness runs no transactor, so a failed result has to be fed in
// directly. A failed create must leave no trace.
testcase("TransactionProposal failed create still inserts");
doInvariantCheck(
{{"TransactionProposal changes do not match transaction result"}},
[&](Account const& owner, Account const&, ApplyContext& ac) {
return insertStagedProposal(owner, ac);
},
XRPAmount{},
STTx{ttTRANSACTION_PROPOSAL_CREATE, [](STObject&) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
precloseStagedPayload,
TxAccount::None,
std::source_location::current(),
tecNO_PERMISSION);
// Reassigns the live proposal's reserve to a sponsor as a
// SponsorshipTransfer would. The sponsored and sponsoring counts move
// with it so that SponsorshipOwnerCountsMatch stays quiet.
auto const reassignLiveProposal =
[&](Account const& owner, Account const& sponsor, ApplyContext& ac) {
auto const sle = peekLiveProposal(ac);
if (!sle)
return false;
auto const reserve =
xrpl::proposal::proposalOwnerCount(sle->getFieldObject(sfProposedTransaction));
sle->setAccountID(sfSponsor, sponsor.id());
ac.view().update(sle);
auto const account = ac.view().peek(keylet::account(owner.id()));
auto const sponsorRoot = ac.view().peek(keylet::account(sponsor.id()));
if (!account || !sponsorRoot)
return false;
account->at(sfSponsoredOwnerCount) += reserve;
ac.view().update(account);
sponsorRoot->at(sfSponsoringOwnerCount) += reserve;
ac.view().update(sponsorRoot);
return true;
};
// A failed transfer must not have reassigned anything. The counters are
// kept consistent so only the result mismatch is left to catch.
testcase("TransactionProposal failed sponsorship transfer still reassigns");
doInvariantCheck(
{{"TransactionProposal changes do not match transaction result"}},
reassignLiveProposal,
XRPAmount{},
STTx{ttSPONSORSHIP_TRANSFER, [](STObject&) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
precloseLiveProposal,
TxAccount::None,
std::source_location::current(),
tecNO_PERMISSION);
// A proposal leaves the ledger only through TransactionProposalCancel
// or together with the Ticket it is keyed to. The harness runs no
// transactor, so each deletion below is staged by hand, releasing the
// reserve as a real one would, to leave the deletion itself as the
// only possible violation.
testcase("TransactionProposal deletion outside the whitelist");
doInvariantCheck(
{{"TransactionProposal changes do not match transaction result"}},
[&](Account const& owner, Account const&, ApplyContext& ac) {
return deleteLiveProposal(owner, ac);
},
XRPAmount{},
STTx{ttACCOUNT_SET, [](STObject&) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
precloseLiveProposal);
// The owner holds a second Ticket, one no proposal is keyed to.
std::optional<std::uint32_t> spareTicketSeq;
Preclose const precloseLiveProposalAndSpareTicket =
[&](Account const& owner, Account const& destination, Env& env) {
spareTicketSeq = proposal::createTicket(env, owner);
return precloseLiveProposal(owner, destination, env);
};
// Consuming some other Ticket of the owner's is no reason for the
// proposal to go.
testcase("TransactionProposal deleted with an unrelated ticket");
doInvariantCheck(
{{"TransactionProposal changes do not match transaction result"}},
[&](Account const& owner, Account const&, ApplyContext& ac) {
return spareTicketSeq && deleteTicket(owner, *spareTicketSeq, ac) &&
deleteLiveProposal(owner, ac);
},
XRPAmount{},
STTx{ttACCOUNT_SET, [](STObject&) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
precloseLiveProposalAndSpareTicket);
// And a deleted Ticket takes the proposal keyed to it along: here the
// Ticket went and the proposal stayed behind.
testcase("TransactionProposal outlives its ticket");
doInvariantCheck(
{{"TransactionProposal outlived the Ticket it is keyed to"}},
[&](Account const& owner, Account const&, ApplyContext& ac) {
return liveTicketSeq && deleteTicket(owner, *liveTicketSeq, ac);
},
XRPAmount{},
STTx{ttACCOUNT_SET, [](STObject&) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
precloseLiveProposal);
// A Cancel names the live proposal and may pay with the owner's spare
// Ticket. Both exist only once the preclose has run, so the Env is
// built here rather than by the harness.
struct CancelOptions
{
// The Cancel pays with A1's spare Ticket.
bool paySpareTicket = false;
TER initialResult = tesSUCCESS;
};
auto const checkCancel = [&](std::vector<std::string> const& expectLogs,
Precheck const& precheck,
std::initializer_list<TER> ters,
Preclose const& preclose,
CancelOptions const& options = CancelOptions(),
std::source_location const& loc =
std::source_location::current()) {
Account const a1{"A1"};
Account const a2{"A2"};
Env env = makeEnv(testableAmendments());
env.fund(XRP(1000), a1, a2);
if (!BEAST_EXPECT(preclose(a1, a2, env)) || !BEAST_EXPECT(liveProposalKeylet))
return;
env.close();
STTx const tx{ttTRANSACTION_PROPOSAL_CANCEL, [&](STObject& obj) {
obj.setAccountID(sfAccount, a1.id());
obj.setFieldH256(sfProposalID, liveProposalKeylet->key);
if (options.paySpareTicket && spareTicketSeq)
{
obj.setFieldU32(sfSequence, 0);
obj.setFieldU32(sfTicketSequence, *spareTicketSeq);
}
}};
doInvariantCheck(
std::move(env),
a1,
a2,
expectLogs,
precheck,
XRPAmount{},
tx,
ters,
loc,
options.initialResult);
};
auto const cancelDeletesLiveProposal =
[&](Account const& owner, Account const&, ApplyContext& ac) {
return deleteLiveProposal(owner, ac);
};
// A Cancel that did not succeed deleted nothing.
testcase("TransactionProposal failed cancel still deletes");
{
CancelOptions options;
options.initialResult = tecNO_PERMISSION;
checkCancel(
{{"TransactionProposal changes do not match transaction result"}},
cancelDeletesLiveProposal,
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
precloseLiveProposal,
options);
}
// A Cancel cannot spend a reserved Ticket, so the proposal it deletes
// never goes with its Ticket.
testcase("TransactionProposal cancel consumes the reserved ticket");
checkCancel(
{{"TransactionProposal changes do not match transaction result"}},
[&](Account const& owner, Account const&, ApplyContext& ac) {
return liveTicketSeq && deleteTicket(owner, *liveTicketSeq, ac) &&
deleteLiveProposal(owner, ac);
},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
precloseLiveProposal);
// Distinct AccountIDs in ascending order, so an array built from them
// violates only the bound on its length.
auto ascendingAccounts = [](std::size_t count) {
std::vector<AccountID> accounts;
accounts.reserve(count);
for (std::size_t i = 0; i < count; ++i)
{
AccountID account{beast::kZero};
account.data()[0] = static_cast<std::uint8_t>(i + 1);
accounts.push_back(account);
}
return accounts;
};
auto signerArray = [](std::vector<AccountID> const& accounts) {
STArray signers{sfSigners};
for (auto const& account : accounts)
{
STObject signer{sfSigner};
signer.setAccountID(sfAccount, account);
signers.pushBack(signer);
}
return signers;
};
// No transaction type on this amendment branch collects signatures yet,
// so every rejection below has to be provoked by editing the stored
// payload directly.
auto const testBrokenSignerArray = [&](std::string const& name, auto const& breakPayload) {
testcase("TransactionProposal canonical signer arrays: " + name);
doInvariantCheck(
{{"TransactionProposal signer arrays are not canonical"}},
[&](Account const&, Account const& signerAccount, ApplyContext& ac) {
auto const sle = peekLiveProposal(ac);
if (!sle)
return false;
auto proposedTx = sle->getFieldObject(sfProposedTransaction);
breakPayload(proposedTx, signerAccount);
sle->setFieldObject(sfProposedTransaction, proposedTx);
ac.view().update(sle);
return true;
},
XRPAmount{},
STTx{ttACCOUNT_SET, [](STObject&) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
precloseLiveProposal);
};
testBrokenSignerArray("duplicate entries", [&](STObject& tx, Account const& signer) {
tx.setFieldArray(sfSigners, signerArray({signer.id(), signer.id()}));
});
testBrokenSignerArray("descending order", [&](STObject& tx, Account const&) {
auto accounts = ascendingAccounts(2);
std::ranges::reverse(accounts);
tx.setFieldArray(sfSigners, signerArray(accounts));
});
testBrokenSignerArray("past the multi-sign limit", [&](STObject& tx, Account const&) {
tx.setFieldArray(sfSigners, signerArray(ascendingAccounts(STTx::kMaxMultiSigners + 1)));
});
testBrokenSignerArray("entry without an Account", [](STObject& tx, Account const&) {
STArray signers{sfSigners};
signers.pushBack(STObject{sfSigner});
tx.setFieldArray(sfSigners, signers);
});
for (auto const* field :
std::array<SField const*, 2>{&sfCounterpartySignature, &sfSponsorSignature})
{
testBrokenSignerArray(
"duplicates under " + field->getName(), [&](STObject& tx, Account const& signer) {
STObject signature{*field};
signature.setFieldArray(sfSigners, signerArray({signer.id(), signer.id()}));
tx.setFieldObject(*field, signature);
});
}
testBrokenSignerArray("past the batch-signer limit", [&](STObject& tx, Account const&) {
STArray batchSigners{sfBatchSigners};
for (auto const& account : ascendingAccounts(kMaxBatchSigners + 1))
{
STObject batchSigner{sfBatchSigner};
batchSigner.setAccountID(sfAccount, account);
batchSigners.pushBack(batchSigner);
}
tx.setFieldArray(sfBatchSigners, batchSigners);
});
testBrokenSignerArray(
"duplicates under a BatchSigner", [&](STObject& tx, Account const& signer) {
STObject batchSigner{sfBatchSigner};
batchSigner.setAccountID(sfAccount, signer.id());
batchSigner.setFieldArray(sfSigners, signerArray({signer.id(), signer.id()}));
STArray batchSigners{sfBatchSigners};
batchSigners.pushBack(batchSigner);
tx.setFieldArray(sfBatchSigners, batchSigners);
});
// Negative controls. These share the setup of the failure cases above,
// so they confirm each of those failures came from the defect it names
// rather than from the hand-built ledger state.
std::initializer_list<TER> const passTers = {tesSUCCESS, tesSUCCESS};
testcase("TransactionProposal well-formed create");
doInvariantCheck(
{},
[&](Account const& owner, Account const&, ApplyContext& ac) {
return insertStagedProposal(owner, ac);
},
XRPAmount{},
STTx{ttTRANSACTION_PROPOSAL_CREATE, [](STObject&) {}},
passTers,
precloseStagedPayload);
testcase("TransactionProposal left untouched");
doInvariantCheck(
{},
[&](Account const&, Account const&, ApplyContext& ac) {
return liveProposalKeylet && ac.view().read(*liveProposalKeylet) != nullptr;
},
XRPAmount{},
STTx{ttACCOUNT_SET, [](STObject&) {}},
passTers,
precloseLiveProposal);
// A Cancel deletes the proposal it names.
testcase("TransactionProposal cancelled");
checkCancel({}, cancelDeletesLiveProposal, passTers, precloseLiveProposal);
// The Ticket a Cancel pays with is not the one its proposal is keyed
// to, so the proposal did not go with its Ticket.
testcase("TransactionProposal cancel paid with the canceller's own ticket");
{
CancelOptions options;
options.paySpareTicket = true;
checkCancel(
{},
[&](Account const& owner, Account const&, ApplyContext& ac) {
return spareTicketSeq && deleteTicket(owner, *spareTicketSeq, ac) &&
deleteLiveProposal(owner, ac);
},
passTers,
precloseLiveProposalAndSpareTicket,
options);
}
// A SponsorshipTransfer reassigns the Sponsor.
testcase("TransactionProposal sponsor reassigned");
doInvariantCheck(
{},
reassignLiveProposal,
XRPAmount{},
STTx{ttSPONSORSHIP_TRANSFER, [](STObject&) {}},
passTers,
precloseLiveProposal);
// The proposed transaction executing spends the proposal's Ticket and
// takes the proposal with it.
testcase("TransactionProposal deleted with its ticket");
doInvariantCheck(
{},
[&](Account const& owner, Account const&, ApplyContext& ac) {
return liveTicketSeq && deleteTicket(owner, *liveTicketSeq, ac) &&
deleteLiveProposal(owner, ac);
},
XRPAmount{},
STTx{ttACCOUNT_SET, [](STObject&) {}},
passTers,
precloseLiveProposal);
}
void
run() override
{
testTransactionProposalInvariants();
}
};
BEAST_DEFINE_TESTSUITE(InvariantsTransactionProposal, app, xrpl);
} // namespace xrpl::test
@@ -1131,7 +1131,13 @@ class InvariantsVault_test : public InvariantsBase
test::StreamSink sink{beast::Severity::Warning};
beast::Journal const jlog{sink};
ApplyContext ac{
env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
env.app(),
ov,
tx,
tesSUCCESS,
env.current()->fees().base,
ApplyFlags::None,
jlog};
CurrentTransactionRulesGuard const rulesGuard(ov.rules());
auto const sleLoan = ac.view().peek(loanKeylet);
@@ -1183,7 +1189,7 @@ class InvariantsVault_test : public InvariantsBase
test::StreamSink sink{beast::Severity::Warning};
beast::Journal const jlog{sink};
ApplyContext ac{
env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, ApplyFlags::None, jlog};
CurrentTransactionRulesGuard const rulesGuard(ov.rules());
auto const sleLoan = ac.view().peek(loanKeylet);
@@ -1360,7 +1366,13 @@ class InvariantsVault_test : public InvariantsBase
test::StreamSink sink{beast::Severity::Warning};
beast::Journal const jlog{sink};
ApplyContext ac{
env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
env.app(),
ov,
tx,
tesSUCCESS,
env.current()->fees().base,
ApplyFlags::None,
jlog};
CurrentTransactionRulesGuard const rulesGuard(ov.rules());
auto const sleLoan = ac.view().peek(loanKeylet);
@@ -1415,7 +1427,13 @@ class InvariantsVault_test : public InvariantsBase
test::StreamSink sink{beast::Severity::Warning};
beast::Journal const jlog{sink};
ApplyContext ac{
env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
env.app(),
ov,
tx,
tesSUCCESS,
env.current()->fees().base,
ApplyFlags::None,
jlog};
CurrentTransactionRulesGuard const rulesGuard(ov.rules());
auto const sleLoan = ac.view().peek(loanKeylet);
@@ -1519,7 +1537,7 @@ class InvariantsVault_test : public InvariantsBase
test::StreamSink sink{beast::Severity::Warning};
beast::Journal const jlog{sink};
ApplyContext ac{
env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, ApplyFlags::None, jlog};
CurrentTransactionRulesGuard const rulesGuard(ov.rules());
{
+3 -2
View File
@@ -1271,7 +1271,8 @@ class LoanBroker_test : public beast::unit_test::Suite
OpenView ov{*env.current()};
test::StreamSink sink{beast::Severity::Warning};
beast::Journal const jlog{sink};
ApplyContext ac{env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
ApplyContext ac{
env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, ApplyFlags::None, jlog};
if (auto const sleBroker = ac.view().peek(keylet::loanBroker(brokerKeylet.key)))
{
@@ -1284,7 +1285,7 @@ class LoanBroker_test : public beast::unit_test::Suite
// Invoke preclaim against the mutated (ApplyView) view; triggers
// nullptr deref
PreclaimContext const pctx{env.app(), ac.view(), tesSUCCESS, tx, TapNone, jlog};
PreclaimContext const pctx{env.app(), ac.view(), tesSUCCESS, tx, ApplyFlags::None, jlog};
(void)LoanBrokerCoverDeposit::preclaim(pctx);
}
+242 -4
View File
@@ -15,6 +15,7 @@
#include <test/jtx/ter.h>
#include <test/jtx/ticket.h>
#include <test/jtx/trust.h>
#include <test/jtx/txflags.h>
#include <test/jtx/vault.h>
#include <xrpl/basics/Number.h>
@@ -1609,26 +1610,263 @@ private:
env.close();
// Direct vault share payment inherits the underlying lock via
// sfReferenceHolding.
// sfReferenceHolding. Caught in the strand check, like a global
// or individual share lock.
BEAST_EXPECT(shareBalance(alice) == 499);
BEAST_EXPECT(shareBalance(bob) == 501);
env(pay(alice, bob, shares(1)), Ter{tecLOCKED});
env(pay(alice, bob, shares(1)), Ter{tecPATH_DRY});
env.close();
BEAST_EXPECT(shareBalance(alice) == 499);
BEAST_EXPECT(shareBalance(bob) == 501);
// The same inherited lock must also block DEX payment paths that
// would consume an offer selling vault shares.
// would consume an offer selling vault shares. bob, the
// destination, inherits the lock and is caught in the strand
// check.
env(pay(carol, bob, shares(1)),
Sendmax(XRP(1)),
Path(BookSpec{shares.raw()}),
Ter{tecPATH_PARTIAL});
Ter{tecPATH_DRY});
env.close();
BEAST_EXPECT(shareBalance(alice) == 499);
BEAST_EXPECT(shareBalance(bob) == 501);
BEAST_EXPECT(expectOffers(env, alice, 1));
}
// A holder individually frozen on the underlying can neither sell
// nor buy vault shares through an unfrozen holder's offer. The
// inherited freeze is caught when the strand is built, before the
// offer is consumed.
auto const testShareDEXEndpointFrozen =
[&](Env& env,
PrettyAsset const& asset,
Account const& owner,
Account const& alice,
Account const& bob,
Account const& carol,
std::function<void(bool)> const& setAliceFrozen) {
Vault const vault{env};
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
env(tx);
env.close();
env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = asset(500)}));
env(vault.deposit({.depositor = bob, .id = keylet.key, .amount = asset(500)}));
env.close();
PrettyAsset const shares = MPTIssue(env.le(keylet)->at(sfShareMPTID));
auto const shareMptID = shares.raw().get<MPTIssue>().getMptID();
auto const shareBalance = [&](Account const& account) {
auto const sle = env.le(keylet::mptoken(shareMptID, account));
return sle ? sle->at(sfMPTAmount) : 0;
};
// bob bids XRP for shares and asks more XRP for shares, so that
// his offers don't cross
env(offer(bob, shares(1), XRP(1)));
env(offer(bob, XRP(2), shares(1)));
env.close();
setAliceFrozen(true);
auto const aliceShares = shareBalance(alice);
auto const bobShares = shareBalance(bob);
auto const expectUnchanged = [&] {
BEAST_EXPECT(shareBalance(alice) == aliceShares);
BEAST_EXPECT(shareBalance(bob) == bobShares);
BEAST_EXPECT(expectOffers(env, bob, 2));
};
// alice sells shares: the first step's source is frozen
env(pay(alice, carol, XRP(1)),
Sendmax(shares(1)),
Path(~XRP),
Txflags(tfNoRippleDirect),
Ter{tecPATH_DRY});
env.close();
expectUnchanged();
// carol buys shares for alice: the last step's destination is
// frozen
env(pay(carol, alice, shares(1)),
Sendmax(XRP(2)),
Path(BookSpec{shares.raw()}),
Txflags(tfNoRippleDirect),
Ter{tecPATH_DRY});
env.close();
expectUnchanged();
// Both succeed once alice is unfrozen
setAliceFrozen(false);
env(pay(alice, carol, XRP(1)),
Sendmax(shares(1)),
Path(~XRP),
Txflags(tfNoRippleDirect));
env.close();
BEAST_EXPECT(shareBalance(bob) == bobShares + 1);
env(pay(carol, alice, shares(1)),
Sendmax(XRP(2)),
Path(BookSpec{shares.raw()}),
Txflags(tfNoRippleDirect));
env.close();
BEAST_EXPECT(shareBalance(alice) == aliceShares);
BEAST_EXPECT(shareBalance(bob) == bobShares);
BEAST_EXPECT(expectOffers(env, bob, 0));
};
// Vault shares as the intermediate asset between two books. Only the
// offer owners hold the shares in transit. An owner individually
// frozen on the underlying can neither give nor receive the shares.
auto const testShareDEXMiddleFrozen = [&](Env& env,
PrettyAsset const& asset,
Account const& owner,
Account const& alice,
Account const& bob,
Account const& carol,
std::function<void(bool)> const& setAliceFrozen) {
Vault const vault{env};
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
env(tx);
env.close();
env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = asset(500)}));
env(vault.deposit({.depositor = bob, .id = keylet.key, .amount = asset(500)}));
env.close();
PrettyAsset const shares = MPTIssue(env.le(keylet)->at(sfShareMPTID));
auto const shareMptID = shares.raw().get<MPTIssue>().getMptID();
auto const shareBalance = [&](Account const& account) {
auto const sle = env.le(keylet::mptoken(shareMptID, account));
return sle ? sle->at(sfMPTAmount) : 0;
};
// alice gives the shares: XRP -> shares (alice) -> asset (bob)
{
env(offer(alice, XRP(1), shares(1)));
env(offer(bob, shares(1), asset(1)));
env.close();
setAliceFrozen(true);
auto const aliceShares = shareBalance(alice);
auto const bobShares = shareBalance(bob);
env(pay(carol, owner, asset(1)),
Sendmax(XRP(1)),
Path(BookSpec{shares.raw()}, BookSpec{asset.raw()}),
Txflags(tfNoRippleDirect),
Ter{tecPATH_PARTIAL});
env.close();
BEAST_EXPECT(shareBalance(alice) == aliceShares);
BEAST_EXPECT(shareBalance(bob) == bobShares);
setAliceFrozen(false);
env(pay(carol, owner, asset(1)),
Sendmax(XRP(1)),
Path(BookSpec{shares.raw()}, BookSpec{asset.raw()}),
Txflags(tfNoRippleDirect));
env.close();
BEAST_EXPECT(shareBalance(alice) == aliceShares - 1);
BEAST_EXPECT(shareBalance(bob) == bobShares + 1);
BEAST_EXPECT(expectOffers(env, alice, 0));
BEAST_EXPECT(expectOffers(env, bob, 0));
}
// alice receives the shares: asset -> shares (bob) -> XRP (alice)
{
env(offer(bob, asset(1), shares(1)));
env(offer(alice, shares(1), XRP(1)));
env.close();
setAliceFrozen(true);
auto const aliceShares = shareBalance(alice);
auto const bobShares = shareBalance(bob);
env(pay(owner, carol, XRP(1)),
Sendmax(asset(1)),
Path(BookSpec{shares.raw()}, ~XRP),
Txflags(tfNoRippleDirect),
Ter{tecPATH_PARTIAL});
env.close();
BEAST_EXPECT(shareBalance(alice) == aliceShares);
BEAST_EXPECT(shareBalance(bob) == bobShares);
setAliceFrozen(false);
env(pay(owner, carol, XRP(1)),
Sendmax(asset(1)),
Path(BookSpec{shares.raw()}, ~XRP),
Txflags(tfNoRippleDirect));
env.close();
BEAST_EXPECT(shareBalance(alice) == aliceShares + 1);
BEAST_EXPECT(shareBalance(bob) == bobShares - 1);
BEAST_EXPECT(expectOffers(env, alice, 0));
BEAST_EXPECT(expectOffers(env, bob, 0));
}
};
{
testcase("MPT individually locked: vault share DEX inherits underlying lock");
Env env{*this, testableAmendments()};
Account const issuer{"issuer"};
Account const owner{"owner"};
Account const alice{"alice"};
Account const bob{"bob"};
Account const carol{"carol"};
env.fund(XRP(10'000), issuer, owner, alice, bob, carol);
env.close();
MPTTester asset{{
.env = env,
.issuer = issuer,
.holders = {owner, alice, bob},
.flags = tfMPTCanTransfer | tfMPTCanTrade | tfMPTCanLock,
}};
env(pay(issuer, owner, asset(1'000)));
env(pay(issuer, alice, asset(2'000)));
env(pay(issuer, bob, asset(2'000)));
env.close();
auto const setAliceFrozen = [&](bool frozen) {
asset.set({.holder = alice, .flags = frozen ? tfMPTLock : tfMPTUnlock});
env.close();
};
testShareDEXEndpointFrozen(env, asset, owner, alice, bob, carol, setAliceFrozen);
testShareDEXMiddleFrozen(env, asset, owner, alice, bob, carol, setAliceFrozen);
}
{
testcase("IOU individually frozen: vault share DEX inherits underlying freeze");
Env env{*this, testableAmendments()};
Account const issuer{"issuer"};
Account const owner{"owner"};
Account const alice{"alice"};
Account const bob{"bob"};
Account const carol{"carol"};
env.fund(XRP(10'000), issuer, owner, alice, bob, carol);
env.close();
// Deliver the IOU from a book to a holder
env(fset(issuer, asfDefaultRipple));
PrettyAsset const asset = issuer["IOU"];
env.trust(asset(10'000), owner, alice, bob);
env(pay(issuer, owner, asset(1'000)));
env(pay(issuer, alice, asset(2'000)));
env(pay(issuer, bob, asset(2'000)));
env.close();
auto const setAliceFrozen = [&](bool frozen) {
env(trust(issuer, asset(0), alice, frozen ? tfSetFreeze : tfClearFreeze));
env.close();
};
testShareDEXEndpointFrozen(env, asset, owner, alice, bob, carol, setAliceFrozen);
testShareDEXMiddleFrozen(env, asset, owner, alice, bob, carol, setAliceFrozen);
}
{
testcase("MPT CanTrade governance: share inherits underlying on DEX and AMM");
+1 -1
View File
@@ -114,7 +114,7 @@ private:
auto const peek = [keylet, &env, this](std::function<bool(SLE&, SLE&)> fn) -> bool {
return env.app().getOpenLedger().modify(
[&](OpenView& view, beast::Journal j) -> bool {
Sandbox sb(&view, TapNone);
Sandbox sb(&view, ApplyFlags::None);
auto const vault = sb.peek(keylet::vault(keylet.key));
if (!BEAST_EXPECT(vault))
return false;
+1 -1
View File
@@ -304,7 +304,7 @@ private:
env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
for (auto const field : encryptedBalanceFields)
{
Sandbox sb(&view, TapNone);
Sandbox sb(&view, ApplyFlags::None);
auto const token = sb.peek(tokenKeylet);
if (!BEAST_EXPECT(token))
return false;
+1 -1
View File
@@ -1939,7 +1939,7 @@ negUnlSizeTest(
bool
applyAndTestResult(jtx::Env& env, OpenView& view, STTx const& tx, bool pass)
{
auto const res = apply(env.app(), view, tx, ApplyFlags::TapNone, env.journal);
auto const res = apply(env.app(), view, tx, ApplyFlags::None, env.journal);
if (pass)
{
return isTesSuccess(res.ter);
+2 -2
View File
@@ -125,7 +125,7 @@ inline TestPath&
TestPath::pushBack(Issue const& iss)
{
path.emplaceBack(
STPathElement::TypeCurrency | STPathElement::TypeIssuer,
STPathElementType::Currency | STPathElementType::Issuer,
beast::kZero,
iss.currency,
iss.account);
@@ -136,7 +136,7 @@ inline TestPath&
TestPath::pushBack(MPTIssue const& iss)
{
path.emplaceBack(
STPathElement::TypeMpt | STPathElement::TypeIssuer,
STPathElementType::Mpt | STPathElementType::Issuer,
beast::kZero,
iss.getMptID(),
iss.getIssuer());
+57
View File
@@ -2,8 +2,11 @@
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/JTx.h>
#include <test/jtx/TestHelpers.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Rate.h>
@@ -11,7 +14,10 @@
#include <xrpl/protocol/STAmount.h>
#include <array>
#include <cstddef>
#include <cstdint>
#include <string>
#include <utility>
/**
* Escrow operations.
@@ -95,4 +101,55 @@ auto const kCondition = JTxFieldWrapper<BlobField>(sfCondition);
auto const kFulfillment = JTxFieldWrapper<BlobField>(sfFulfillment);
/**
* Set a hex-encoded blob field on a JTx
*/
template <SF_VL const& Field>
struct HexBlobField
{
private:
std::string value_;
public:
explicit HexBlobField(std::string hex) : value_(std::move(hex))
{
}
explicit HexBlobField(Slice const& blob) : value_(strHex(blob))
{
}
template <size_t N>
explicit HexBlobField(std::array<std::uint8_t, N> const& blob) : HexBlobField(makeSlice(blob))
{
}
void
operator()(Env&, JTx& jt) const
{
// clang-tidy reads a reference template parameter as parenthesized.
jt.jv[Field.jsonName] = value_; // NOLINT(readability-redundant-parentheses)
}
};
using Bytecode = HexBlobField<sfBytecode>;
using Data = HexBlobField<sfData>;
struct Gas
{
private:
std::uint32_t value_;
public:
explicit Gas(std::uint32_t const value) : value_(value)
{
}
void
operator()(Env&, JTx& jt) const
{
jt.jv[sfGas.jsonName] = value_;
}
};
} // namespace xrpl::test::jtx::escrow
+6 -6
View File
@@ -570,10 +570,10 @@ cpe(PathAsset const& pa)
{
return pa.visit(
[](Currency const& currency) {
return STPathElement(STPathElement::TypeCurrency, xrpAccount(), currency, xrpAccount());
return STPathElement(STPathElementType::Currency, xrpAccount(), currency, xrpAccount());
},
[](MPTID const& mpt) {
return STPathElement(STPathElement::TypeMpt, xrpAccount(), mpt, xrpAccount());
return STPathElement(STPathElementType::Mpt, xrpAccount(), mpt, xrpAccount());
});
};
@@ -590,14 +590,14 @@ ipe(Asset const& asset)
return asset.visit(
[](Issue const& issue) {
return STPathElement(
STPathElement::TypeCurrency | STPathElement::TypeIssuer,
STPathElementType::Currency | STPathElementType::Issuer,
xrpAccount(),
issue.currency,
issue.account);
},
[](MPTIssue const& issue) {
return STPathElement(
STPathElement::TypeMpt | STPathElement::TypeIssuer,
STPathElementType::Mpt | STPathElementType::Issuer,
xrpAccount(),
issue.getMptID(),
issue.getIssuer());
@@ -608,14 +608,14 @@ ipe(Asset const& asset)
STPathElement
iape(AccountID const& account)
{
return STPathElement(STPathElement::TypeIssuer, xrpAccount(), xrpCurrency(), account);
return STPathElement(STPathElementType::Issuer, xrpAccount(), xrpCurrency(), account);
};
// Account path element
STPathElement
ape(AccountID const& a)
{
return STPathElement(STPathElement::TypeAccount, a, xrpCurrency(), xrpAccount());
return STPathElement(STPathElementType::Account, a, xrpCurrency(), xrpAccount());
};
bool
+1 -1
View File
@@ -32,7 +32,7 @@ bumpLastPage(
{
std::expected<void, Error> res{};
env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) -> bool {
Sandbox sb(&view, TapNone);
Sandbox sb(&view, ApplyFlags::None);
// Find the root page
auto const sleRoot = sb.peek(directory);
+22 -4
View File
@@ -238,6 +238,13 @@ verify::Create::operator()(Env& env, JTx& jt) const
// Every entry the transaction could touch, read whole, so what follows can
// say that nothing moved rather than that the fields we named did not.
auto const& view = *env.current();
// A proposer paying with one of its own Tickets consumes it whenever the
// transaction is applied: on success, and on a claimed-fee tec.
std::optional<std::uint32_t> ownTicketSeq;
if (jt.jv.isMember(sfTicketSequence.jsonName))
ownTicketSeq = jt.jv[sfTicketSequence.jsonName].asUInt();
auto const proposalKeylet = keylet::txProposal(target, ticketSeq);
auto const ownerCountBefore = env.ownerCount(proposer);
auto const sponsoredOwnerCountBefore = env.sponsoredOwnerCount(proposer);
@@ -254,11 +261,15 @@ verify::Create::operator()(Env& env, JTx& jt) const
auto const& view = *applied.current();
auto const created = isTesSuccess(applied.ter());
bool const ticketConsumed = ownTicketSeq && (created || isTecClaim(applied.ter()));
std::uint32_t const ticketReturned = ticketConsumed ? 1 : 0;
// The proposer owns the proposal even when another account covers its
// reserve. A proposed Batch costs more owner-count increments.
// reserve. A proposed Batch costs more owner-count increments. A
// Ticket the proposer paid with gives one back.
test.expect(
applied.ownerCount(proposer) == ownerCountBefore + (created ? cost : 0),
applied.ownerCount(proposer) ==
ownerCountBefore + (created ? cost : 0) - ticketReturned,
"proposal reserve");
test.expect(
applied.sponsoredOwnerCount(proposer) ==
@@ -291,6 +302,14 @@ verify::Create::operator()(Env& env, JTx& jt) const
auto const sleProposal = view.read(proposalKeylet);
// The Ticket the proposer paid with has left its directory.
auto expectedDir = proposerDirBefore;
if (ticketConsumed)
{
expectedDir.erase(
keylet::ticket(proposer.id(), SeqProxy::rawTicket(*ownTicketSeq)).key);
}
if (!created)
{
// A create that did not succeed leaves the proposal as it found
@@ -300,7 +319,7 @@ verify::Create::operator()(Env& env, JTx& jt) const
// Nor did the directory gain a listing for an entry that does not
// exist.
test.expect(
ownerDirKeys(view, proposer.id()) == proposerDirBefore, "proposal owner directory");
ownerDirKeys(view, proposer.id()) == expectedDir, "proposal owner directory");
return;
}
@@ -353,7 +372,6 @@ verify::Create::operator()(Env& env, JTx& jt) const
// The proposal is listed in the proposer's directory on the page its
// OwnerNode names, and nothing else listed moved.
auto expectedDir = proposerDirBefore;
expectedDir.emplace(sleProposal->key(), sleProposal->getFieldU64(sfOwnerNode));
test.expect(ownerDirKeys(view, proposer.id()) == expectedDir, "proposal owner directory");
});
+3 -1
View File
@@ -77,7 +77,9 @@ public:
* @brief Check the ledger effects a TransactionProposalCreate must have,
* whatever its outcome: on tesSUCCESS a new proposal holding what was
* submitted, listed in the proposer's directory and paid for by its reserve;
* otherwise nothing moved. Nothing of the target's moves either way.
* otherwise nothing moved. Nothing of the target's moves either way. A Ticket
* the proposer pays with is expected to be consumed; attach ticket::Use()
* before this condition so it sees the TicketSequence.
*
* @code
* env(proposal::create(alice, payload, expiration), proposal::verify::create());
+3 -3
View File
@@ -313,7 +313,7 @@ struct Directory_test : public beast::unit_test::Suite
// All the offers have been cancelled, so the book
// should have no entries and be empty:
{
Sandbox const sb(env.closed().get(), TapNone);
Sandbox const sb(env.closed().get(), ApplyFlags::None);
UInt256 const bookBase = getBookBase({xrpIssue(), usd, std::nullopt});
BEAST_EXPECT(dirIsEmpty(sb, keylet::page(bookBase)));
@@ -352,7 +352,7 @@ struct Directory_test : public beast::unit_test::Suite
{
// Create a chain of three pages:
Sandbox sb(env.closed().get(), TapNone);
Sandbox sb(env.closed().get(), ApplyFlags::None);
makePages(sb, kBase, 3);
// Insert an item in the middle page:
@@ -376,7 +376,7 @@ struct Directory_test : public beast::unit_test::Suite
{
// Create a chain of four pages:
Sandbox sb(env.closed().get(), TapNone);
Sandbox sb(env.closed().get(), ApplyFlags::None);
makePages(sb, kBase, 4);
// Now add items on pages 1 and 2:
+11 -11
View File
@@ -131,7 +131,7 @@ class PaymentSandbox_test : public beast::unit_test::Suite
STAmount const toDebit(usdGw1(20));
{
// accountSend, no deferredCredits
ApplyViewImpl av(&*env.current(), TapNone);
ApplyViewImpl av(&*env.current(), ApplyFlags::None);
auto const iss = usdGw1;
auto const startingAmount =
@@ -156,7 +156,7 @@ class PaymentSandbox_test : public beast::unit_test::Suite
{
// directSendNoFee, no deferredCredits
ApplyViewImpl av(&*env.current(), TapNone);
ApplyViewImpl av(&*env.current(), ApplyFlags::None);
auto const iss = usdGw1;
auto const startingAmount =
@@ -177,7 +177,7 @@ class PaymentSandbox_test : public beast::unit_test::Suite
{
// accountSend, w/ deferredCredits
ApplyViewImpl av(&*env.current(), TapNone);
ApplyViewImpl av(&*env.current(), ApplyFlags::None);
PaymentSandbox pv(&av);
auto const iss = usdGw1;
@@ -205,7 +205,7 @@ class PaymentSandbox_test : public beast::unit_test::Suite
{
// directSendNoFee, w/ deferredCredits
ApplyViewImpl av(&*env.current(), TapNone);
ApplyViewImpl av(&*env.current(), ApplyFlags::None);
PaymentSandbox pv(&av);
auto const iss = usdGw1;
@@ -221,7 +221,7 @@ class PaymentSandbox_test : public beast::unit_test::Suite
{
// redeemIOU, w/ deferredCredits
ApplyViewImpl av(&*env.current(), TapNone);
ApplyViewImpl av(&*env.current(), ApplyFlags::None);
PaymentSandbox pv(&av);
auto const iss = usdGw1;
@@ -237,7 +237,7 @@ class PaymentSandbox_test : public beast::unit_test::Suite
{
// issueIOU, w/ deferredCredits
ApplyViewImpl av(&*env.current(), TapNone);
ApplyViewImpl av(&*env.current(), ApplyFlags::None);
PaymentSandbox pv(&av);
auto const iss = usdGw1;
@@ -253,7 +253,7 @@ class PaymentSandbox_test : public beast::unit_test::Suite
{
// accountSend, w/ deferredCredits and stacked views
ApplyViewImpl av(&*env.current(), TapNone);
ApplyViewImpl av(&*env.current(), ApplyFlags::None);
PaymentSandbox pv(&av);
auto const iss = usdGw1;
@@ -319,7 +319,7 @@ class PaymentSandbox_test : public beast::unit_test::Suite
STAmount const hugeAmt(
issue, STAmount::kMaxValue, STAmount::kMaxOffset - 1, false, STAmount::Unchecked{});
ApplyViewImpl av(&*env.current(), TapNone);
ApplyViewImpl av(&*env.current(), ApplyFlags::None);
PaymentSandbox pv(&av);
pv.creditHookIOU(gw, alice, hugeAmt, -tinyAmt);
BEAST_EXPECT(pv.balanceHookIOU(alice, gw, hugeAmt) == tinyAmt);
@@ -347,7 +347,7 @@ class PaymentSandbox_test : public beast::unit_test::Suite
env.fund(reserve(env, 1), alice);
env.close();
ApplyViewImpl av(&*env.current(), TapNone);
ApplyViewImpl av(&*env.current(), ApplyFlags::None);
PaymentSandbox sb(&av);
{
// Send alice an amount and spend it. The deferredCredits will cause
@@ -380,7 +380,7 @@ class PaymentSandbox_test : public beast::unit_test::Suite
auto const usd = gw["USD"];
Account const alice("alice");
ApplyViewImpl av(&*env.current(), TapNone);
ApplyViewImpl av(&*env.current(), ApplyFlags::None);
PaymentSandbox sb(&av);
// The currency we pass for the last argument mimics the currency that
@@ -412,7 +412,7 @@ class PaymentSandbox_test : public beast::unit_test::Suite
env.fund(XRP(10000), alice, sponsor);
env.close();
ApplyViewImpl av(&*env.current(), TapNone);
ApplyViewImpl av(&*env.current(), ApplyFlags::None);
PaymentSandbox sb(&av);
// Test basic owner count hook without sponsor
+9 -9
View File
@@ -189,7 +189,7 @@ class View_test : public beast::unit_test::Suite
Env env(*this);
wipe(env.app().getOpenLedger());
auto const open = env.current();
ApplyViewImpl v(&*open, TapNone);
ApplyViewImpl v(&*open, ApplyFlags::None);
succ(v, 0, std::nullopt);
v.insert(sle(1));
BEAST_EXPECT(v.exists(k(1)));
@@ -222,7 +222,7 @@ class View_test : public beast::unit_test::Suite
Env env(*this);
wipe(env.app().getOpenLedger());
auto const open = env.current();
ApplyViewImpl v0(&*open, TapNone);
ApplyViewImpl v0(&*open, ApplyFlags::None);
v0.insert(sle(1));
v0.insert(sle(2));
v0.insert(sle(4));
@@ -288,7 +288,7 @@ class View_test : public beast::unit_test::Suite
Env env(*this);
wipe(env.app().getOpenLedger());
auto const open = env.current();
ApplyViewImpl v0(&*open, TapNone);
ApplyViewImpl v0(&*open, ApplyFlags::None);
v0.rawInsert(sle(1, 1));
v0.rawInsert(sle(2, 2));
v0.rawInsert(sle(4, 4));
@@ -368,22 +368,22 @@ class View_test : public beast::unit_test::Suite
BEAST_EXPECT(v1.seq() == v0.seq());
BEAST_EXPECT(v1.parentCloseTime() == v1.parentCloseTime());
ApplyViewImpl v2(&v1, TapRetry);
ApplyViewImpl v2(&v1, ApplyFlags::Retry);
BEAST_EXPECT(v2.parentCloseTime() == v1.parentCloseTime());
BEAST_EXPECT(v2.seq() == v1.seq());
BEAST_EXPECT(v2.flags() == TapRetry);
BEAST_EXPECT(v2.flags() == ApplyFlags::Retry);
Sandbox const v3(&v2);
BEAST_EXPECT(v3.seq() == v2.seq());
BEAST_EXPECT(v3.parentCloseTime() == v2.parentCloseTime());
BEAST_EXPECT(v3.flags() == TapRetry);
BEAST_EXPECT(v3.flags() == ApplyFlags::Retry);
}
{
ApplyViewImpl v1(&v0, TapRetry);
ApplyViewImpl v1(&v0, ApplyFlags::Retry);
PaymentSandbox v2(&v1);
BEAST_EXPECT(v2.seq() == v0.seq());
BEAST_EXPECT(v2.parentCloseTime() == v0.parentCloseTime());
BEAST_EXPECT(v2.flags() == TapRetry);
BEAST_EXPECT(v2.flags() == ApplyFlags::Retry);
PaymentSandbox const v3(&v2);
BEAST_EXPECT(v3.seq() == v2.seq());
BEAST_EXPECT(v3.parentCloseTime() == v2.parentCloseTime());
@@ -1030,7 +1030,7 @@ class View_test : public beast::unit_test::Suite
wipe(*ledger);
ledger->rawInsert(sle(1));
ReadView const& v0 = *ledger;
ApplyViewImpl v1(&v0, TapNone);
ApplyViewImpl v1(&v0, ApplyFlags::None);
{
Sandbox v2(&v1);
v2.erase(v2.peek(k(1)));
+54 -4
View File
@@ -16,6 +16,7 @@
#include <array>
#include <cstdint>
#include <format>
#include <optional>
#include <string_view>
#include <utility>
@@ -34,6 +35,7 @@ public:
{
// account_info with no account.
auto const info = env.rpc("json", "account_info", "{ }");
BEAST_EXPECT(info[jss::result][jss::error_code] == RpcInvalidParams);
BEAST_EXPECT(info[jss::result][jss::error_message] == "Missing field 'account'.");
}
{
@@ -45,6 +47,8 @@ public:
"\"n94JNrQYkDrpt62bbSR7nVEhdyAvcJXRAsjEkFYyqRkh9SUTYEqV\"}");
BEAST_EXPECT(info[jss::result][jss::error_code] == RpcActMalformed);
BEAST_EXPECT(info[jss::result][jss::error_message] == "Account malformed.");
BEAST_EXPECT(info[jss::result].isMember(jss::ledger_current_index));
BEAST_EXPECT(info[jss::result][jss::validated] == false);
}
{
// account_info with an account that's not in the ledger.
@@ -54,10 +58,13 @@ public:
auto const info = env.rpc("json", "account_info", to_string(params));
BEAST_EXPECT(info[jss::result][jss::error_code] == RpcActNotFound);
BEAST_EXPECT(info[jss::result][jss::error_message] == "Account not found.");
BEAST_EXPECT(info[jss::result][jss::account] == bogie.human());
BEAST_EXPECT(info[jss::result].isMember(jss::ledger_current_index));
BEAST_EXPECT(info[jss::result][jss::validated] == false);
}
{
// Cannot use a seed as account
auto const info = env.rpc("json", "account_info", R"({"account": "foo"})");
auto const info = env.rpc("json", "account_info", R"JSON({"account": "foo"})JSON");
BEAST_EXPECT(info[jss::result][jss::error_code] == RpcActMalformed);
BEAST_EXPECT(info[jss::result][jss::error_message] == "Account malformed.");
}
@@ -69,6 +76,7 @@ public:
params[jss::account] = param;
auto jrr = env.rpc("json", "account_info", to_string(params))[jss::result];
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'account'.");
};
@@ -87,6 +95,7 @@ public:
params[jss::ident] = param;
auto jrr = env.rpc("json", "account_info", to_string(params))[jss::result];
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'ident'.");
};
@@ -97,6 +106,41 @@ public:
testInvalidIdentParam(json::Value(json::ValueType::Object));
testInvalidIdentParam(json::Value(json::ValueType::Array));
}
{
// `ident` is ignored when `account` is present
json::Value params;
params[jss::account] = env.master.human();
params[jss::ident] = 1;
auto const info = env.rpc("json", "account_info", to_string(params))[jss::result];
BEAST_EXPECT(!info.isMember(jss::error));
BEAST_EXPECT(info.isMember(jss::account_data));
BEAST_EXPECT(info[jss::account_data][sfAccount.jsonName] == env.master.human());
}
{
// an invalid ledger is reported before a missing account
json::Value params;
params[jss::ledger_hash] = 1;
auto const info = env.rpc("json", "account_info", to_string(params))[jss::result];
BEAST_EXPECT(info[jss::error] == "invalidParams");
BEAST_EXPECT(info[jss::error_code] == RpcInvalidParams);
BEAST_EXPECT(
info[jss::error_message] == "Invalid field 'ledger_hash', not hex string.");
}
{
// `queue` is read as a bool, so any non-empty string requests it, and the queue
// can't be requested from a closed ledger
env.close();
json::Value params;
params[jss::account] = env.master.human();
params[jss::ledger_index] = "closed";
params[jss::queue] = "false";
auto const info = env.rpc("json", "account_info", to_string(params))[jss::result];
BEAST_EXPECT(info[jss::error] == "invalidParams");
BEAST_EXPECT(info[jss::error_code] == RpcInvalidParams);
BEAST_EXPECT(info[jss::error_message] == "Invalid parameters.");
BEAST_EXPECT(info.isMember(jss::ledger_index));
BEAST_EXPECT(!info.isMember(jss::account_data));
}
}
// Test the "signer_lists" argument in account_info.
@@ -234,8 +278,9 @@ public:
withSigners[jss::account] = alice.human();
withSigners[jss::signer_lists] = true;
auto const withSignersAsString = std::string("{ ") + R"("api_version": 2, "account": ")" +
alice.human() + "\", " + "\"signer_lists\": asdfggh }";
auto const withSignersAsString = std::format(
R"JSON({{"api_version": 2, "account": "{}", "signer_lists": "yes"}})JSON",
alice.human());
// Alice has no SignerList yet.
{
@@ -285,9 +330,14 @@ public:
}
{
// account_info with "signer_lists" as not bool should error out
auto const info = env.rpc("json", "account_info", withSignersAsString);
auto const info = env.rpc("json", "account_info", withSignersAsString)[jss::result];
BEAST_EXPECT(info[jss::status] == "error");
BEAST_EXPECT(info[jss::error] == "invalidParams");
BEAST_EXPECT(info[jss::error_code] == RpcInvalidParams);
BEAST_EXPECT(info.isMember(jss::account_data));
BEAST_EXPECT(info[jss::account_data][sfAccount.jsonName] == alice.human());
BEAST_EXPECT(info.isMember(jss::ledger_current_index));
BEAST_EXPECT(!info.isMember(jss::signer_lists));
}
// Give alice a big signer list
+121 -6
View File
@@ -48,9 +48,14 @@ public:
using namespace test::jtx;
Env env(*this);
static constexpr std::size_t kLinesPerGateway = 26;
static constexpr std::size_t kAllLines = 2 * kLinesPerGateway;
{
// account_lines with no account.
auto const lines = env.rpc("json", "account_lines", "{ }");
BEAST_EXPECT(lines[jss::result][jss::error_code] == RpcInvalidParams);
BEAST_EXPECT(
lines[jss::result][jss::error_message] ==
rpc::missingFieldError(jss::account)[jss::error_message]);
@@ -60,9 +65,34 @@ public:
json::Value params;
params[jss::account] = "n9MJkEKHDhy5eTLuHUQeAAjo382frHNbFK4C8hcwN4nwM2SrLdBj";
auto const lines = env.rpc("json", "account_lines", to_string(params));
BEAST_EXPECT(lines[jss::result][jss::error_code] == RpcActMalformed);
BEAST_EXPECT(
lines[jss::result][jss::error_message] ==
rpc::makeError(RpcActMalformed)[jss::error_message]);
BEAST_EXPECT(lines[jss::result].isMember(jss::ledger_current_index));
BEAST_EXPECT(lines[jss::result][jss::validated] == false);
}
{
// An invalid ledger is reported before a missing account.
json::Value params;
params[jss::ledger_hash] = 1;
auto const lines = env.rpc("json", "account_lines", to_string(params))[jss::result];
BEAST_EXPECT(lines[jss::error] == "invalidParams");
BEAST_EXPECT(lines[jss::error_code] == RpcInvalidParams);
BEAST_EXPECT(
lines[jss::error_message] == "Invalid field 'ledger_hash', not hex string.");
}
{
// A bad limit is reported before an account that is not found.
json::Value params;
params[jss::account] = Account{"bogie"}.human();
params[jss::limit] = -1;
auto const lines = env.rpc("json", "account_lines", to_string(params))[jss::result];
BEAST_EXPECT(lines[jss::error] == "invalidParams");
BEAST_EXPECT(lines[jss::error_code] == RpcInvalidParams);
BEAST_EXPECT(
lines[jss::error_message] ==
rpc::expectedFieldMessage(jss::limit, "unsigned integer"));
}
{
// test account non-string
@@ -71,6 +101,7 @@ public:
params[jss::account] = param;
auto jrr = env.rpc("json", "account_lines", to_string(params))[jss::result];
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'account'.");
};
@@ -87,6 +118,7 @@ public:
json::Value params;
params[jss::account] = alice.human();
auto const lines = env.rpc("json", "account_lines", to_string(params));
BEAST_EXPECT(lines[jss::result][jss::error_code] == RpcActNotFound);
BEAST_EXPECT(
lines[jss::result][jss::error_message] ==
rpc::makeError(RpcActNotFound)[jss::error_message]);
@@ -104,6 +136,7 @@ public:
params[jss::peer] = param;
auto jrr = env.rpc("json", "account_lines", to_string(params))[jss::result];
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'peer'.");
};
@@ -128,6 +161,7 @@ public:
params[jss::account] = alice.human();
params[jss::ledger_index] = "nonsense";
auto const lines = env.rpc("json", "account_lines", to_string(params));
BEAST_EXPECT(lines[jss::result][jss::error_code] == RpcInvalidParams);
BEAST_EXPECT(
lines[jss::result][jss::error_message] ==
"Invalid field 'ledger_index', not string or number.");
@@ -138,6 +172,7 @@ public:
params[jss::account] = alice.human();
params[jss::ledger_index] = 50000;
auto const lines = env.rpc("json", "account_lines", to_string(params));
BEAST_EXPECT(lines[jss::result][jss::error_code] == RpcLgrNotFound);
BEAST_EXPECT(lines[jss::result][jss::error_message] == "ledgerNotFound");
}
// Create trust lines to share with alice.
@@ -191,7 +226,7 @@ public:
// A re-usable test for historic ledgers.
auto const testAccountLinesHistory =
[this, &env](Account const& account, LedgerHeader const& info, int count) {
[this, &env](Account const& account, LedgerHeader const& info, std::size_t count) {
// Get account_lines by ledger index.
json::Value paramsSeq;
paramsSeq[jss::account] = account.human();
@@ -213,10 +248,10 @@ public:
testAccountLinesHistory(alice, ledger3Info, 0);
// Alice should have 26 trust lines in ledger 4.
testAccountLinesHistory(alice, ledger4Info, 26);
testAccountLinesHistory(alice, ledger4Info, kLinesPerGateway);
// Alice should have 52 trust lines in ledger 58.
testAccountLinesHistory(alice, ledger58Info, 52);
testAccountLinesHistory(alice, ledger58Info, kAllLines);
{
// Invalid to specify both index and hash
@@ -226,6 +261,7 @@ public:
params[jss::ledger_index] = ledger58Info.seq;
auto const lines = env.rpc("json", "account_lines", to_string(params))[jss::result];
BEAST_EXPECT(lines[jss::error] == "invalidParams");
BEAST_EXPECT(lines[jss::error_code] == RpcInvalidParams);
BEAST_EXPECT(
lines[jss::error_message] ==
"Exactly one of 'ledger_hash' or 'ledger_index' can be "
@@ -238,6 +274,7 @@ public:
params[jss::ledger_index] = json::ValueType::Object;
auto const lines = env.rpc("json", "account_lines", to_string(params))[jss::result];
BEAST_EXPECT(lines[jss::error] == "invalidParams");
BEAST_EXPECT(lines[jss::error_code] == RpcInvalidParams);
BEAST_EXPECT(
lines[jss::error_message] == "Invalid field 'ledger_index', not string or number.");
}
@@ -247,7 +284,7 @@ public:
params[jss::account] = alice.human();
auto const lines = env.rpc("json", "account_lines", to_string(params));
BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
BEAST_EXPECT(lines[jss::result][jss::lines].size() == 52);
BEAST_EXPECT(lines[jss::result][jss::lines].size() == kAllLines);
}
{
// alice should have 26 trust lines with gw1.
@@ -256,7 +293,7 @@ public:
params[jss::peer] = gw1.human();
auto const lines = env.rpc("json", "account_lines", to_string(params));
BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
BEAST_EXPECT(lines[jss::result][jss::lines].size() == 26);
BEAST_EXPECT(lines[jss::result][jss::lines].size() == kLinesPerGateway);
// Check no ripple is not set for trustlines between alice and gw1
auto const& line = lines[jss::result][jss::lines][0u];
@@ -268,9 +305,38 @@ public:
params[jss::account] = alice.human();
params[jss::peer] = "n9MJkEKHDhy5eTLuHUQeAAjo382frHNbFK4C8hcwN4nwM2SrLdBj";
auto const lines = env.rpc("json", "account_lines", to_string(params));
BEAST_EXPECT(lines[jss::result][jss::error_code] == RpcActMalformed);
BEAST_EXPECT(
lines[jss::result][jss::error_message] ==
rpc::makeError(RpcActMalformed)[jss::error_message]);
BEAST_EXPECT(lines[jss::result].isMember(jss::ledger_current_index));
BEAST_EXPECT(lines[jss::result][jss::validated] == false);
}
{
// An empty peer is no peer at all.
json::Value params;
params[jss::account] = alice.human();
params[jss::peer] = "";
auto const lines = env.rpc("json", "account_lines", to_string(params));
params.removeMember(jss::peer);
auto const all = env.rpc("json", "account_lines", to_string(params));
BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
BEAST_EXPECT(lines[jss::result][jss::lines].size() == kAllLines);
BEAST_EXPECT(lines[jss::result][jss::lines] == all[jss::result][jss::lines]);
}
{
// ignore_default is read as a bool, so any non-empty string sets it.
json::Value params;
params[jss::account] = gw1.human();
auto const all = env.rpc("json", "account_lines", to_string(params));
params[jss::ignore_default] = true;
auto const asBool = env.rpc("json", "account_lines", to_string(params));
params[jss::ignore_default] = "false";
auto const asString = env.rpc("json", "account_lines", to_string(params));
BEAST_EXPECT(all[jss::result][jss::lines].size() == kLinesPerGateway);
BEAST_EXPECT(asBool[jss::result][jss::lines].size() == 0);
BEAST_EXPECT(!asString[jss::result].isMember(jss::error));
BEAST_EXPECT(asString[jss::result][jss::lines] == asBool[jss::result][jss::lines]);
}
{
// A negative limit should fail.
@@ -278,6 +344,7 @@ public:
params[jss::account] = alice.human();
params[jss::limit] = -1;
auto const lines = env.rpc("json", "account_lines", to_string(params));
BEAST_EXPECT(lines[jss::result][jss::error_code] == RpcInvalidParams);
BEAST_EXPECT(
lines[jss::result][jss::error_message] ==
rpc::expectedFieldMessage(jss::limit, "unsigned integer"));
@@ -298,7 +365,7 @@ public:
paramsB[jss::marker] = marker;
auto const linesB = env.rpc("json", "account_lines", to_string(paramsB));
BEAST_EXPECT(linesB[jss::result][jss::lines].isArray());
BEAST_EXPECT(linesB[jss::result][jss::lines].size() == 51);
BEAST_EXPECT(linesB[jss::result][jss::lines].size() == kAllLines - 1);
// Go again from where the marker left off, but set a limit of 3.
json::Value paramsC;
@@ -315,6 +382,7 @@ public:
paramsD[jss::account] = alice.human();
paramsD[jss::marker] = marker;
auto const linesD = env.rpc("json", "account_lines", to_string(paramsD));
BEAST_EXPECT(linesD[jss::result][jss::error_code] == RpcInvalidParams);
BEAST_EXPECT(
linesD[jss::result][jss::error_message] ==
rpc::makeError(RpcInvalidParams)[jss::error_message]);
@@ -325,10 +393,44 @@ public:
params[jss::account] = alice.human();
params[jss::marker] = true;
auto const lines = env.rpc("json", "account_lines", to_string(params));
BEAST_EXPECT(lines[jss::result][jss::error_code] == RpcInvalidParams);
BEAST_EXPECT(
lines[jss::result][jss::error_message] ==
rpc::expectedFieldMessage(jss::marker, "string"));
}
{
// A marker that does not parse is reported without naming the field.
json::Value params;
params[jss::account] = alice.human();
params[jss::marker] = "NOT_A_MARKER";
auto const lines = env.rpc("json", "account_lines", to_string(params));
BEAST_EXPECT(lines[jss::result][jss::error_code] == RpcInvalidParams);
BEAST_EXPECT(
lines[jss::result][jss::error_message] ==
rpc::makeError(RpcInvalidParams)[jss::error_message]);
}
{
// The marker's start hint may have a leading '+', and anything after a second
// comma is ignored.
json::Value params;
params[jss::account] = alice.human();
params[jss::limit] = 1;
auto const first = env.rpc("json", "account_lines", to_string(params));
auto const marker = first[jss::result][jss::marker].asString();
auto const comma = marker.find(',');
BEAST_EXPECT(comma != std::string::npos);
json::Value next;
next[jss::account] = alice.human();
next[jss::marker] = marker;
auto const plain = env.rpc("json", "account_lines", to_string(next));
next[jss::marker] =
marker.substr(0, comma) + ",+" + marker.substr(comma + 1) + ",ignored";
auto const lines = env.rpc("json", "account_lines", to_string(next));
BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
BEAST_EXPECT(lines[jss::result][jss::lines].size() == kAllLines - 1);
BEAST_EXPECT(lines[jss::result][jss::lines] == plain[jss::result][jss::lines]);
}
{
// Check that the flags we expect from alice to gw2 are present.
json::Value params;
@@ -514,6 +616,7 @@ public:
linesEndParams[jss::account] = alice.human();
linesEndParams[jss::marker] = linesBeg[jss::result][jss::marker];
auto const linesEnd = env.rpc("json", "account_lines", to_string(linesEndParams));
BEAST_EXPECT(linesEnd[jss::result][jss::error_code] == RpcInvalidParams);
BEAST_EXPECT(
linesEnd[jss::result][jss::error_message] ==
rpc::makeError(RpcInvalidParams)[jss::error_message]);
@@ -811,6 +914,7 @@ public:
request[jss::ripplerpc] = "2.0";
request[jss::id] = 5;
auto const lines = env.rpc("json2", to_string(request));
BEAST_EXPECT(lines[jss::error][jss::error_code] == RpcInvalidParams);
BEAST_EXPECT(
lines[jss::error][jss::message] ==
rpc::missingFieldError(jss::account)[jss::error_message]);
@@ -829,6 +933,7 @@ public:
request[jss::id] = 5;
request[jss::params] = params;
auto const lines = env.rpc("json2", to_string(request));
BEAST_EXPECT(lines[jss::error][jss::error_code] == RpcActMalformed);
BEAST_EXPECT(
lines[jss::error][jss::message] ==
rpc::makeError(RpcActMalformed)[jss::error_message]);
@@ -848,6 +953,7 @@ public:
request[jss::id] = 5;
request[jss::params] = params;
auto const lines = env.rpc("json2", to_string(request));
BEAST_EXPECT(lines[jss::error][jss::error_code] == RpcActNotFound);
BEAST_EXPECT(
lines[jss::error][jss::message] ==
rpc::makeError(RpcActNotFound)[jss::error_message]);
@@ -876,6 +982,7 @@ public:
auto const lines = env.rpc("json2", to_string(request));
BEAST_EXPECT(lines[jss::error][jss::error] == "invalidParams");
BEAST_EXPECT(lines[jss::error][jss::error_code] == RpcInvalidParams);
BEAST_EXPECT(lines[jss::error][jss::message] == "Invalid field 'peer'.");
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
@@ -918,6 +1025,7 @@ public:
request[jss::id] = 5;
request[jss::params] = params;
auto const lines = env.rpc("json2", to_string(request));
BEAST_EXPECT(lines[jss::error][jss::error_code] == RpcInvalidParams);
BEAST_EXPECT(
lines[jss::error][jss::message] ==
"Invalid field 'ledger_index', not string or number.");
@@ -937,6 +1045,7 @@ public:
request[jss::id] = 5;
request[jss::params] = params;
auto const lines = env.rpc("json2", to_string(request));
BEAST_EXPECT(lines[jss::error][jss::error_code] == RpcLgrNotFound);
BEAST_EXPECT(lines[jss::error][jss::message] == "ledgerNotFound");
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
@@ -1055,6 +1164,7 @@ public:
request[jss::params] = params;
auto const lines = env.rpc("json2", to_string(request));
BEAST_EXPECT(lines[jss::error][jss::error] == "invalidParams");
BEAST_EXPECT(lines[jss::error][jss::error_code] == RpcInvalidParams);
BEAST_EXPECT(
lines[jss::error][jss::message] ==
"Exactly one of 'ledger_hash' or 'ledger_index' can be "
@@ -1110,6 +1220,7 @@ public:
request[jss::id] = 5;
request[jss::params] = params;
auto const lines = env.rpc("json2", to_string(request));
BEAST_EXPECT(lines[jss::error][jss::error_code] == RpcActMalformed);
BEAST_EXPECT(
lines[jss::error][jss::message] ==
rpc::makeError(RpcActMalformed)[jss::error_message]);
@@ -1129,6 +1240,7 @@ public:
request[jss::id] = 5;
request[jss::params] = params;
auto const lines = env.rpc("json2", to_string(request));
BEAST_EXPECT(lines[jss::error][jss::error_code] == RpcInvalidParams);
BEAST_EXPECT(
lines[jss::error][jss::message] ==
rpc::expectedFieldMessage(jss::limit, "unsigned integer"));
@@ -1202,6 +1314,7 @@ public:
requestD[jss::id] = 5;
requestD[jss::params] = paramsD;
auto const linesD = env.rpc("json2", to_string(requestD));
BEAST_EXPECT(linesD[jss::error][jss::error_code] == RpcInvalidParams);
BEAST_EXPECT(
linesD[jss::error][jss::message] ==
rpc::makeError(RpcInvalidParams)[jss::error_message]);
@@ -1221,6 +1334,7 @@ public:
request[jss::id] = 5;
request[jss::params] = params;
auto const lines = env.rpc("json2", to_string(request));
BEAST_EXPECT(lines[jss::error][jss::error_code] == RpcInvalidParams);
BEAST_EXPECT(
lines[jss::error][jss::message] ==
rpc::expectedFieldMessage(jss::marker, "string"));
@@ -1378,6 +1492,7 @@ public:
linesEndRequest[jss::id] = 5;
linesEndRequest[jss::params] = linesEndParams;
auto const linesEnd = env.rpc("json2", to_string(linesEndRequest));
BEAST_EXPECT(linesEnd[jss::error][jss::error_code] == RpcInvalidParams);
BEAST_EXPECT(
linesEnd[jss::error][jss::message] ==
rpc::makeError(RpcInvalidParams)[jss::error_message]);
Loaded 100 of 126 files, more files were not shown because too many files have changed in this diff. Show more