Compare commits

...
Author SHA1 Message Date
Nik Bougalis f14395427e Modernize CountedObject infrastructure:
- Replace the lazy singleton with a constinit registry of per-type
  static counters. The `getInstance` method is removed.
- A concurrency bug that would corrupt the list of counters during
  insertion has been fixed.
- Debug asserts can detect incorrect accounting that can result in
  counter under- or overflow.
- `CountedObject` can only be used as a CRTP base for its own type
  parameter.
- Track the current and maximum values of counter. This results in
  a change in the output of `get_counts`, with individual counters
  now reported as sub-objects of a `counters` object.
2026-10-05 19:42:50 -07:00
Nik Bougalis b707bac74f Improve AccountID base58 conversion cache:
The previous cache guarded a configurably-sized vector with 64
packed spinlocks. Two aspects of that setup were not worth the
cost:

- Callers were forced to perform atomic RMW operations on a
  single word which was shared by all 64 locks; this caused
  the lock line to bounce between cores on every lookup.

- The configurable size and runtime initialization resulted
  in complexity that did not yield a meaningful improvement
  in performance.

This commit ditches the 64 packed spinlocks, replacing them
with per-entry sequence locks, allowing readers on the fast
path to avoid performing any stores at all.

Cache entries are now carefully sized and aligned to fit into
a typical cache line, which helps avoid false sharing and any
unnecessary coherence traffic.

The cache is now a fixed array of 65,536 entries. The size was
chosen to balance capacity against memory overhead and to keep
the indexing operation fast and simple: since AccountID values
are uniformly distributed, two bytes can serve directly as the
index without requiring additional hashing.

Note: The reader path will perform unsynchronized reads against
      concurrent writes; the sequence lock protocol detects and
      discards any data observed mid-update, so these races are
      benign. A sanitizer like TSAN can still flag them and the
      warnings are expected. A compile-time knob to disable the
      cache is available, if necessary.
2026-10-05 19:42:50 -07:00
Nik Bougalis da2b5d5eaf Remove beast::typeName
Replace Beast's demangling wrapper (originally by @HowardHinnant)
with calls to `boost::core::type_name`, eliminating the low-level
dependency and manual memory management from the codebase.
2026-10-05 19:42:50 -07:00
Nik Bougalis 4de4902200 Rework and clean up the safe_cast framework:
The SafeToCast concept transposed its signedness clause: it was written in
<Dest, Src> order but it declared <Src, Dest>. This introduced what can be
best described as a polarity bug. As a result:

* Signed-to-wider-unsigned casts were wrongly deemed safe.
* Unsigned-to-wider-signed casts were wrongly deemed unsafe.

The root cause was drift caused by the safety condition being repeated in
the concept and again as a static_assert in safeCast. The checking is now
done only in the concept and is expressed in terms of range coverage, and
not via a sizeof/signedness proxy. This change also improves the handling
of bool (which previously compiled despite truncating) and eliminates the
platform-dependent accept/reject behavior for same-size types.

Additional fixes:

* The pointer form of safeDowncast now performs the same static_cast
  in all builds. The dynamic_cast check is now entirely contained in
  the XRPL_ASSERT.
* Extended integer types wider than intmax_t (e.g. __int128) are now
  excluded from SafeToCast instead of silently misevaluating through
  wrapped bounds; they remain expressible via unsafeCast.

Cleanups:

* Remove the single-parameter enum overload of safeCast: it had an
  explicitly-specified template argument binding to Src, where all
  sibling overload bound to Dest, silently yielding the underlying
  type from expressions that read as casts *to* an enum.
* Add checkedCast for conversions guarded by runtime bounds checks
  where safety varies per instantiation.
* Constrain safeDowncast to polymorphic sources and genuine public
  unambiguous downcasts, rejecting upcasts and unrelated types.
* Route enum conversions through std::to_underlying; clarify the
  unsafeCast/checkedCast documentation split.
2026-10-05 19:42:49 -07:00
Nik Bougalis 2ea1f09dcb Modernize beast::Zero for C++20
- Replace the twelve comparison operators with two
  `constexpr`-capable replacements: `==` and `<=>`
  and allow the compiler to synthesize the rest.
- Constrain `==` and `<=>` using concepts and mark
  them as conditionally noexcept).
- Remove the detail::zero_helper indirection which
  did not do what its comment claimed.
2026-10-05 19:30:38 -07:00
Nik Bougalis 68b4b894de 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-05 19:30:38 -07:00
Nik Bougalis defd83e5a6 Make STPathElement::Type a scoped enumeration:
`STPathElement` stored its type as an `unsigned int`, returned it
as a `std::uint32_t`, while the flags themselves were an unscoped
`enum` (i.e. an `int`). The type field has always been serialized
as a single byte.

This commit turns `STPathElement::Type` into a scoped `enum` with
a fixed sized that matches the serialized width, and opts it into
the bitwise operators from `enum_bitops.h`:

- Store the type as `Type`, and use `Type` for the constructor
  parameter and the return type of `getNodeType()`.
- Keep the existing spellings such as `STPathElement::TypeAccount`
  valid through `using enum`.
- 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.
- Make `operator==` a hidden friend.
- Mark the trivial accessors `noexcept`.

No functional changes are intended. The one observable difference
is the rejection error generated for a malformed path element: if
the currency and MPT bits are both set and the input is truncated
within the account field, the invalid bit combination is detected
first; previously the short read would be detected. Such elements
are rejected either way.
2026-10-05 19:30:38 -07:00
Nik Bougalis 452e31a05f 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-05 19:30:37 -07:00
Mayukha Vadari 1798262d7a refactor: Use LedgerHashesEntry everywhere (#8369) 2026-10-05 18:15:02 +00:00
Mayukha Vadari cd633b9ac9 refactor: Use EscrowEntry everywhere (#8354) 2026-10-05 17:51:18 +00:00
Ayaz Salikhov 5c5f7d315a chore: Update nix flake file (#8479) 2026-10-05 16:58:41 +00:00
Mayukha Vadari 0f7493ce50 refactor: Use FeeSettingsEntry everywhere (#8370) 2026-10-05 16:56:41 +00:00
Harshit Gupta 40f61f828a fix: Add string type validation for channel_id and signature (#7582) 2026-10-05 16:55:14 +00:00
Mayukha Vadari 108277f4b7 test: Migrate three beast suites from beast::unit_test to gtest (#7993) 2026-10-05 15:28:05 +00:00
BartandBart 651bb207b4 refactor: Build Throw messages with std::format (#8474)
Co-authored-by: Bart <[email protected]>
2026-10-03 16:45:34 +00:00
Peter Chen c45363fd8b feat: Implement Confidential mpt holder key update (#8266) 2026-10-02 22:54:38 +00:00
Chenna Keshava B S f1744cb76e fix: Do not block MPToken deletion on unrelated confidential balances (#8209) 2026-10-02 22:54:31 +00:00
Gregory Tsipenyuk bcbaa4df07 fix: Enable the large Number mantissa with MPTokensV2 (#8330) 2026-10-02 21:42:14 +00:00
Alex Kremer 3cd357949c chore: Add ignore revs for recent style changes (#8463) 2026-10-02 21:21:09 +00:00
Ayaz Salikhov a9027bb997 ci: Make release always go into stable channel (#8469) 2026-10-02 19:02:46 +00:00
0a6da4de74 test: Restore config test environment variables with a guard (#8333)
Co-authored-by: Bart <[email protected]>
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
2026-10-02 18:24:41 +00:00
163 changed files with 4218 additions and 1695 deletions

No files matched your search

+1
View File
@@ -255,6 +255,7 @@ words:
- queuable
- Raphson
- rcflags
- reencrypted
- replayer
- repodata
- repomd
+12
View File
@@ -5,6 +5,18 @@
# This file is sorted in reverse chronological order, with the most recent commits at the top.
# The commits listed here are ignored by git blame, which is useful for formatting-only commits that would otherwise obscure the history of changes to a file.
# chore: CamelCase for typedef/using in `clang-tidy` (#8177)
97a1824537d20d82d25bf151a37a7a4532ebf085
# chore: Rename CamelCase namespaces to snake_case (#7933)
06488c1318d96f56d0536251bee08ac85fa7fdd3
# style: Unify style for all Doxygen comments (#7776)
73b6852a122854140336e6e6bc30a3a4b41aa5fd
# style: More clang-tidy identifier renaming (#7290)
a830ab10efed8d3e59ef2fc15d66efdf9c6bb0d8
# refactor: Rename static constants (#7120)
5b6e8b6f93b19c1e3f6a3467a25639031d9d9a53
# chore: More fixes for bad renames (#7092)
7afdd71a54d562b32a50b29a5aa00bb997dc9053
# refactor: Enable clang-tidy `readability-identifier-naming` check (#6571)
8995564ed6b9e453e144bb663303072a3c1ba305
# refactor: Enable remaining clang-tidy `cppcoreguidelines` checks (#6538)
+1 -1
View File
@@ -36,4 +36,4 @@ runs:
- name: Determine release channel and package release
id: release_info
uses: XRPLF/actions/release-info@ebcf6cea14eee258697308a51fea55cad777581b
uses: XRPLF/actions/release-info@a9f2eeca6fb3980ba3a84cf68566f1c69ad30674
+12 -3
View File
@@ -269,14 +269,23 @@ jobs:
env:
CONTEXT: image-context
IMAGE: ${{ matrix.target == 'voidstar' && 'xrpld-voidstar' || 'xrplf/xrpld' }}:${{ github.ref_type == 'tag' && github.ref_name || 'develop' }}
# Docker Hub is public, so a private build never reaches it;
# the Antithesis registry is not.
PUSH: ${{ inputs.publish && (matrix.target == 'voidstar' || github.event.repository.visibility == 'public') }}
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Determine release info
id: release_info
uses: ./.github/actions/release-info
# Docker Hub is public, so it only gets builds whose packages are public:
# those of a public codebase, and stable releases.
# The Antithesis registry is private, so it gets every build.
- name: Decide whether to push
env:
PUSH: ${{ inputs.publish && (matrix.target == 'voidstar' || github.event.repository.visibility == 'public' || steps.release_info.outputs.channel == 'stable') }}
run: echo "PUSH=${PUSH}" | tee -a "${GITHUB_ENV}"
- name: Download package artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
+5
View File
@@ -30,6 +30,11 @@ Version 3.5.0 is not yet released.
- `subscribe`, `unsubscribe`: Added an optional `mpt_issuances` request field, an array of MPT issuance IDs (hex strings). Subscribers receive a message with `type` `mptTransaction` for each validated transaction whose metadata affects a subscribed issuance; the message has the same fields as the `transactions` stream. MPT issuance subscriptions count toward the per-connection subscription limit. An empty array, a non-array value, or an invalid ID returns `invalidParams`. ([#5671](https://github.com/XRPLF/rippled/pull/5671))
### Bugfixes in 3.5.0
- `channel_authorize`: The `channel_id` field now returns an `invalidParams` error if the value is not a string. [#7582](https://github.com/XRPLF/rippled/pull/7582)
- `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)
## XRP Ledger server version 3.4.0
Version 3.4.0 is not yet released. These changes are available in the 3.4.0 beta releases.
Generated
+6 -6
View File
@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1781173989,
"narHash": "sha256-fnzKKPvS+oieI/pTzotA5tkoM47EB1NpaBcgk4R97hE=",
"lastModified": 1791130267,
"narHash": "sha256-1sjwcQMcgAbmiip/VivBSiK8p2bTkQGOvCuO7vnwfx4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "8c91a71d13451abc40eb9dae8910f972f979852f",
"rev": "9013764fcc0ea99fa16cf7aa7decf8a5c3889dd2",
"type": "github"
},
"original": {
@@ -47,11 +47,11 @@
]
},
"locked": {
"lastModified": 1784611586,
"narHash": "sha256-OfqgY+0hp/zseZB7uyH0U8kIDPS4scZZCyAurEplvG0=",
"lastModified": 1791189933,
"narHash": "sha256-Y8eN7ki0Urx4Ojf4w1xSpWg8uk/3lJqK+E0Oth77rH0=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "14f58845249f3552a89b07772626b8d3c632fa86",
"rev": "e60029353d0c48d216bc4b065168ccd4079c166f",
"type": "github"
},
"original": {
+146 -55
View File
@@ -1,29 +1,31 @@
#pragma once
#include <xrpl/beast/type_name.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <boost/core/type_name.hpp>
#include <algorithm>
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <iterator>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
namespace xrpl {
/**
* Manages all counted object types.
*
* Counters register themselves on a lock-free intrusive list maintained
* by this object when constructed. Because counters are never destroyed
* or removed, the ABA problem does not apply.
*
* The registry is iterable as a forward range.
*/
class CountedObjects
{
public:
static CountedObjects&
getInstance() noexcept;
using Entry = std::pair<std::string, int>;
using List = std::vector<Entry>;
[[nodiscard]] List
getCounts(int minimumThreshold) const;
public:
/**
* Implementation for @ref CountedObject.
@@ -33,68 +35,147 @@ public:
class Counter
{
public:
Counter(std::string name) noexcept : name_(std::move(name)), count_(0)
{
// Insert ourselves at the front of the lock-free linked list
CountedObjects& instance = CountedObjects::getInstance();
Counter* head = nullptr;
Counter(std::string name) noexcept;
do
{
head = instance.head_.load();
next_ = head;
} while (instance.head_.exchange(this) != head);
// Counters are intrusive list nodes whose addresses are published
// in the registry; they must never be copied or moved. The atomic
// members already force this, but we make it explicit.
Counter(Counter const&) = delete;
Counter&
operator=(Counter const&) = delete;
Counter(Counter&&) = delete;
Counter&
operator=(Counter&&) = delete;
++instance.count_;
}
~Counter() noexcept = default;
int
std::uint32_t
increment() noexcept
{
return ++count_;
auto const newCount = count_.fetch_add(1, std::memory_order::relaxed) + 1;
XRPL_ASSERT(newCount != 0, "xrpl::CountedObjects::Counter::increment : no overflow");
auto maxCount = maxCount_.load(std::memory_order::relaxed);
while (newCount > maxCount &&
!maxCount_.compare_exchange_weak(maxCount, newCount, std::memory_order::relaxed))
{
}
return newCount;
}
int
std::uint32_t
decrement() noexcept
{
return --count_;
auto const prev = count_.fetch_sub(1, std::memory_order::relaxed);
XRPL_ASSERT(prev != 0, "xrpl::CountedObjects::Counter::decrement : no underflow");
return prev - 1;
}
[[nodiscard]] int
getCount() const noexcept
[[nodiscard]] std::uint32_t
count() const noexcept
{
return count_.load();
return count_.load(std::memory_order::relaxed);
}
[[nodiscard]] Counter*
getNext() const noexcept
[[nodiscard]] std::uint32_t
max() const noexcept
{
return next_;
return std::max(
count_.load(std::memory_order::relaxed),
maxCount_.load(std::memory_order::relaxed));
}
[[nodiscard]] std::string const&
getName() const noexcept
name() const noexcept
{
return name_;
}
private:
friend class CountedObjects;
Counter* next_ = nullptr;
std::atomic<std::uint32_t> count_ = 0;
std::atomic<std::uint32_t> maxCount_ = 0;
std::string const name_;
std::atomic<int> count_;
Counter* next_;
};
private:
CountedObjects() noexcept;
~CountedObjects() noexcept = default;
class Iterator
{
public:
using value_type = Counter const;
using reference = value_type&;
using pointer = value_type*;
using difference_type = std::ptrdiff_t;
using iterator_category = std::forward_iterator_tag;
explicit Iterator(Counter* c = nullptr) noexcept : current_(c)
{
}
reference
operator*() const noexcept
{
return *current_;
}
pointer
operator->() const noexcept
{
return current_;
}
Iterator&
operator++() noexcept
{
current_ = current_->next_;
return *this;
}
Iterator
operator++(int) noexcept
{
auto tmp = *this;
++*this;
return tmp;
}
bool
operator==(Iterator const&) const noexcept = default;
private:
Counter* current_;
};
constexpr CountedObjects() noexcept = default;
[[nodiscard]] auto
begin() const noexcept
{
return Iterator{head_.load(std::memory_order::acquire)};
}
[[nodiscard]] auto
end() const noexcept
{
return Iterator{};
}
private:
std::atomic<int> count_;
std::atomic<Counter*> head_;
std::atomic<Counter*> head_ = nullptr;
};
/** The global counted object registry. */
inline constinit CountedObjects gCountedObjects;
inline CountedObjects::Counter::Counter(std::string name) noexcept
: next_(gCountedObjects.head_.load(std::memory_order::relaxed)), name_(std::move(name))
{
while (!gCountedObjects.head_.compare_exchange_weak(
next_, this, std::memory_order::release, std::memory_order::relaxed))
;
}
//------------------------------------------------------------------------------
/**
@@ -103,27 +184,37 @@ private:
* Derived classes have their instances counted automatically. This is used
* for reporting purposes.
*
* The constructors are private and `Object` is befriended so that the
* CRTP parameter must be the deriving class itself: a copy-paste error
* like `class B : public CountedObject<A>` fails to compile instead of
* silently polluting A's count.
*
* @note This class has no move operations by design: a derived class's
* move constructor falls back to the copy constructor for this
* base, so the newly created instance is counted. This keeps the
* invariant that count is the number of outstanding subobjects.
*
* @warning Counted objects constructed during dynamic initialization of
* other translation units may have their increments discarded when
* counter itself is dynamically initialized. Do not create counted
* objects before main() begins.
*
* @ingroup basics
*/
template <class Object>
requires std::is_class_v<Object>
class CountedObject
{
private:
static auto&
getCounter() noexcept
{
static CountedObjects::Counter kC{beast::typeName<Object>()};
return kC;
}
static inline CountedObjects::Counter counter{boost::core::type_name<Object>()};
CountedObject() noexcept
{
getCounter().increment();
counter.increment();
}
CountedObject(CountedObject const&) noexcept
{
getCounter().increment();
counter.increment();
}
CountedObject&
@@ -132,7 +223,7 @@ private:
public:
~CountedObject() noexcept
{
getCounter().decrement();
counter.decrement();
}
friend Object;
+12 -12
View File
@@ -110,10 +110,10 @@ static_assert(
*
* However, it does not have sufficient precision to represent the full integer
* range of int64_t values (-2^63 to 2^63-1), which are needed for XRP and MPT
* values. The implementation of SingleAssetVault, and LendingProtocol need to
* represent those integer values accurately and precisely, both for the
* STNumber field type, and for internal calculations. That necessitated the
* "large" scale.
* values. The implementation of SingleAssetVault, LendingProtocol, and
* MPTokensV2 need to represent those integer values accurately and precisely,
* both for the STNumber field type, and for internal calculations. That
* necessitated the "large" scale.
*
* The "Large" scales are intended to represent all values that can be represented
* by an STAmount - IOUs, XRP, and MPTs. It has a min value of 10^18, and a max
@@ -134,8 +134,8 @@ struct MantissaRange final
// NOLINTBEGIN(readability-enum-initial-value)
// The values don't matter, except for Large
enum class MantissaScale {
// Small can be removed when either featureSingleAssetVault or featureLendingProtocol are
// retired
// Small can be removed when any of featureSingleAssetVault, featureLendingProtocol, or
// featureMPTokensV2 are retired
Small,
// LargeLegacy can be removed when fixCleanup3_2_0 is retired
LargeLegacy,
@@ -311,10 +311,10 @@ concept Integral64 = std::is_same_v<T, std::int64_t> || std::is_same_v<T, std::u
*
* The mantissa range may be changed at runtime via setMantissaScale(). The
* default mantissa range is "large". The range is updated whenever transaction
* processing begins, based on whether SingleAssetVault or LendingProtocol are
* enabled. If either is enabled, the mantissa range is set to "large". If not,
* it is set to "small", preserving backward compatibility and correct
* "amendment-gating".
* processing begins, based on whether SingleAssetVault, LendingProtocol, or
* MPTokensV2 are enabled. If any is enabled, the mantissa range is set to
* "large". If not, it is set to "small", preserving backward compatibility and
* correct "amendment-gating".
*
* It is extremely unlikely that any more calls to setMantissaScale() will be
* needed outside of unit tests.
@@ -344,8 +344,8 @@ concept Integral64 = std::is_same_v<T, std::int64_t> || std::is_same_v<T, std::u
* set/getMantissaScale() functions may be most appropriate. However, if the
* test has anything to do with transaction processing, it should enable or
* disable the amendments that control the mantissa range choice
* (SingleAssetVault and LendingProtocol), and/or check if either of those
* amendments are enabled to determine which result to expect.
* (SingleAssetVault, LendingProtocol, and MPTokensV2), and/or check if any of
* those amendments are enabled to determine which result to expect.
*/
class Number final
{
+3 -2
View File
@@ -3,11 +3,11 @@
#pragma once
#include <xrpl/basics/ByteUtilities.h>
#include <xrpl/beast/type_name.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <boost/align.hpp>
#include <boost/container/static_vector.hpp>
#include <boost/core/type_name.hpp>
#include <boost/predef.h>
#include <algorithm>
@@ -16,6 +16,7 @@
#include <cstring>
#include <mutex>
#include <stdexcept>
#include <typeinfo>
#include <vector>
#if BOOST_OS_LINUX
@@ -335,7 +336,7 @@ public:
}) != cfg.end())
{
throw std::runtime_error(
"SlabAllocatorSet<" + beast::typeName<Type>() + ">: duplicate slab size");
"SlabAllocatorSet<" + boost::core::type_name<Type>() + ">: duplicate slab size");
}
for (auto const& c : cfg)
+3 -2
View File
@@ -1,7 +1,8 @@
#pragma once
#include <xrpl/basics/sanitizers.h>
#include <xrpl/beast/type_name.h>
#include <boost/core/type_name.hpp>
#include <exception>
#include <string>
@@ -55,7 +56,7 @@ Throw(Args&&... args)
std::is_convertible_v<E*, std::exception*>, "Exception must derive from std::exception.");
E e(std::forward<Args>(args)...);
logThrow(std::string("Throwing exception of type " + beast::typeName<E>() + ": ") + e.what());
logThrow("Throwing exception of type " + boost::core::type_name<E>() + ": " + e.what());
throw std::move(e);
}
+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
+152 -47
View File
@@ -2,107 +2,212 @@
#include <xrpl/beast/utility/instrumentation.h> // IWYU pragma: keep
#include <concepts>
#include <cstdint>
#include <limits>
#include <memory>
#include <type_traits>
#include <utility>
namespace xrpl {
// safe_cast adds compile-time checks to a static_cast to ensure that
// the destination can hold all values of the source. This is particularly
// handy when the source or destination is an enumeration type.
/** Every value of @p Src can be represented by @p Dest.
template <class Src, class Dest>
concept SafeToCast = (std::is_integral_v<Src> && std::is_integral_v<Dest>) &&
(std::is_signed_v<Src> || std::is_unsigned_v<Dest>) &&
(std::is_signed_v<Src> != std::is_signed_v<Dest> ? sizeof(Dest) > sizeof(Src)
: sizeof(Dest) >= sizeof(Src));
Given two integral types, the cast is safe when the destination can
hold every possible value of the source.
template <class Dest, class Src>
Comparing the bounds requires care: we use @c std::cmp_less_equal and
@c std::cmp_greater_equal; the plain relational operators would apply
arithmetic conversions, resulting in incorrect results when comparing
across signedness.
Because @c std::cmp_* requires standard signed or unsigned integer
arguments, which excludes character types and bool, we first widen
all type bounds to the maximum-width integer type while preserving
signedness.
@note Extended integer types, like __int128 on gcc, cannot be safely
widened to a standard integer type, so the concept will reject
them.
*/
template <typename Src, typename Dest>
concept SafeToCast = std::is_integral_v<Src> && std::is_integral_v<Dest> && []() consteval {
using WideSrc = std::conditional_t<std::is_signed_v<Src>, std::intmax_t, std::uintmax_t>;
using WideDest = std::conditional_t<std::is_signed_v<Dest>, std::intmax_t, std::uintmax_t>;
// Note: this guard must be an evaluated branch, and not a
// static_assert. The lambda body is outside the immediate
// context, so a substitution-time failure here would be a
// hard error rather than leaving the concept unsatisfied.
if constexpr (sizeof(Src) > sizeof(WideSrc) || sizeof(Dest) > sizeof(WideDest))
{
return false;
}
else
{
return std::cmp_less_equal(
static_cast<WideDest>(std::numeric_limits<Dest>::min()),
static_cast<WideSrc>(std::numeric_limits<Src>::min())) &&
std::cmp_greater_equal(
static_cast<WideDest>(std::numeric_limits<Dest>::max()),
static_cast<WideSrc>(std::numeric_limits<Src>::max()));
}
}();
/** Compile-time-checked static_cast that rejects non-value preserving casts.
@note There is deliberately no enum-to-enum overload, and no overload
returning the underlying type of an enum. For the latter, use
@c std::to_underlying.
*/
/** @{ */
template <typename Dest, typename Src>
requires(std::is_integral_v<Dest> && std::is_integral_v<Src>)
constexpr Dest
safeCast(Src s) noexcept
requires(std::is_integral_v<Dest> && std::is_integral_v<Src>)
{
static_assert(
std::is_signed_v<Dest> || std::is_unsigned_v<Src>, "Cannot cast signed to unsigned");
constexpr unsigned kNotSame = std::is_signed_v<Dest> != std::is_signed_v<Src>;
static_assert(
sizeof(Dest) >= sizeof(Src) + kNotSame,
"Destination is too small to hold all values of source");
SafeToCast<Src, Dest>, "This cast is not value-preserving. Please use unsafeCast instead.");
return static_cast<Dest>(s);
}
template <class Dest, class Src>
template <typename Dest, typename Src>
requires(std::is_enum_v<Dest> && std::is_integral_v<Src>)
constexpr Dest
safeCast(Src s) noexcept
requires(std::is_enum_v<Dest> && std::is_integral_v<Src>)
{
return static_cast<Dest>(safeCast<std::underlying_type_t<Dest>>(s));
}
template <class Dest, class Src>
template <typename Dest, typename Src>
requires(std::is_integral_v<Dest> && std::is_enum_v<Src>)
constexpr Dest
safeCast(Src s) noexcept
requires(std::is_integral_v<Dest> && std::is_enum_v<Src>)
{
return safeCast<Dest>(static_cast<std::underlying_type_t<Src>>(s));
return safeCast<Dest>(std::to_underlying(s));
}
/** @} */
// unsafe_cast explicitly flags a static_cast as not necessarily able to hold
// all values of the source. It includes a compile-time check so that if
// underlying types become safe, it can be converted to a safe_cast.
/** Integral-to-integral cast that is known to be narrowing or sign-erasing.
template <class Dest, class Src>
This explicitly flags a conversion that can lose information for some
values of the source type, where the call site accepts that loss (or
truncation is the intended behavior).
The compile-time check ensures the cast remains "unsafe": if the types
involved later change such that the conversion becomes inherently
value-preserving, the static assertion fires with instructions to
migrate the call site to @ref safeCast.
If the conversion's safety depends on a runtime precondition rather
than on the types, or varies across instantiations of generic code,
use @ref checkedCast instead.
*/
/** @{ */
template <typename Dest, typename Src>
requires(std::is_integral_v<Dest> && std::is_integral_v<Src>)
constexpr Dest
unsafeCast(Src s) noexcept
requires(std::is_integral_v<Dest> && std::is_integral_v<Src>)
{
static_assert(
!SafeToCast<Src, Dest>,
"Only unsafe if casting signed to unsigned or "
"destination is too small");
!SafeToCast<Src, Dest>, "This cast is value-preserving. Please use safeCast instead.");
return static_cast<Dest>(s);
}
template <class Dest, class Src>
template <typename Dest, typename Src>
requires(std::is_enum_v<Dest> && std::is_integral_v<Src>)
constexpr Dest
unsafeCast(Src s) noexcept
requires(std::is_enum_v<Dest> && std::is_integral_v<Src>)
{
return static_cast<Dest>(unsafeCast<std::underlying_type_t<Dest>>(s));
}
template <class Dest, class Src>
template <typename Dest, typename Src>
requires(std::is_integral_v<Dest> && std::is_enum_v<Src>)
constexpr Dest
unsafeCast(Src s) noexcept
requires(std::is_integral_v<Dest> && std::is_enum_v<Src>)
{
return unsafeCast<Dest>(static_cast<std::underlying_type_t<Src>>(s));
return unsafeCast<Dest>(std::to_underlying(s));
}
/** @} */
/** Integral-to-integral cast when the caller has performed a bounds check.
This documents that a runtime precondition or external invariant, which
is not necessarily visible to the compiler, guarantees that the requested
conversion is value-preserving for the values that can actually occur.
This is primarily meant for generic code, where the same expression may
be value-preserving for one instantiation but not for another, making
both @ref safeCast and @ref unsafeCast unusable.
Unlike @ref safeCast and @ref unsafeCast, this imposes no static check
on the type relationship: the caller's claim is about runtime values,
not about types.
*/
/** @{ */
template <typename Dest, typename Src>
requires(std::is_integral_v<Dest> && std::is_integral_v<Src>)
constexpr Dest
checkedCast(Src s) noexcept
{
return static_cast<Dest>(s);
}
template <typename Dest, typename Src>
requires(std::is_enum_v<Dest> && std::is_integral_v<Src>)
constexpr Dest
checkedCast(Src s) noexcept
{
return static_cast<Dest>(checkedCast<std::underlying_type_t<Dest>>(s));
}
template <typename Dest, typename Src>
requires(std::is_integral_v<Dest> && std::is_enum_v<Src>)
constexpr Dest
checkedCast(Src s) noexcept
{
return checkedCast<Dest>(std::to_underlying(s));
}
/** @} */
/** Downcast within a class hierarchy, verified in debug builds.
Performs a static_cast down a hierarchy, but in debug builds verifies
via dynamic_cast that the object's dynamic type actually permits the
downcast. Both build modes execute the same conversion; debug builds
merely add the check.
The pointer form passes null through unchanged, as dynamic_cast does.
@note The check requires @p Src to be polymorphic; in release builds
an invalid downcast is undefined behavior on use, exactly as
with a bare static_cast.
*/
/** @{ */
template <class Dest, class Src>
requires std::is_pointer_v<Dest>
requires(
std::is_pointer_v<Dest> && std::is_polymorphic_v<Src> &&
std::derived_from<std::remove_pointer_t<Dest>, Src> &&
(std::is_const_v<std::remove_pointer_t<Dest>> || !std::is_const_v<Src>) &&
(std::is_volatile_v<std::remove_pointer_t<Dest>> || !std::is_volatile_v<Src>))
inline Dest
safeDowncast(Src* s) noexcept
{
#ifdef NDEBUG
XRPL_ASSERT(
s == nullptr || dynamic_cast<Dest>(s) != nullptr, "xrpl::safeDowncast : valid downcast");
return static_cast<Dest>(s); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)
#else
auto* result = dynamic_cast<Dest>(s);
XRPL_ASSERT(result != nullptr, "xrpl::safeDowncast : pointer downcast is valid");
return result;
#endif
}
template <class Dest, class Src>
requires std::is_lvalue_reference_v<Dest>
requires(
std::is_lvalue_reference_v<Dest> && std::is_polymorphic_v<Src> &&
std::derived_from<std::remove_reference_t<Dest>, Src>)
inline Dest
safeDowncast(Src& s) noexcept
{
#ifndef NDEBUG
XRPL_ASSERT(
dynamic_cast<std::add_pointer_t<std::remove_reference_t<Dest>>>(&s) != nullptr,
"xrpl::safeDowncast : reference downcast is valid");
#endif
return static_cast<Dest>(s); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)
return *safeDowncast<std::add_pointer_t<std::remove_reference_t<Dest>>>(std::addressof(s));
}
/** @} */
} // namespace xrpl
-47
View File
@@ -1,47 +0,0 @@
#pragma once
#include <cstdlib>
#include <string>
#include <type_traits>
#include <typeinfo>
#ifndef _MSC_VER
#include <cxxabi.h>
#endif
namespace beast {
template <typename T>
std::string
typeName()
{
using TR = std::remove_reference_t<T>;
std::string name = typeid(TR).name();
#ifndef _MSC_VER
if (auto s = abi::__cxa_demangle(name.c_str(), nullptr, nullptr, nullptr))
{
name = s;
// NOLINTNEXTLINE(cppcoreguidelines-no-malloc)
std::free(s);
}
#endif
if (std::is_const_v<TR>)
name += " const";
if (std::is_volatile_v<TR>)
name += " volatile";
if (std::is_lvalue_reference_v<T>)
{
name += "&";
}
else if (std::is_rvalue_reference_v<T>)
{
name += "&&";
}
return name;
}
} // namespace beast
+36 -109
View File
@@ -2,26 +2,24 @@
#pragma once
#include <compare>
#include <concepts>
namespace beast {
/**
* Zero allows classes to offer efficient comparisons to zero.
*
* Zero is a struct to allow classes to efficiently compare with zero without
* requiring an rvalue construction.
*
* It's often the case that we have classes which combine a number and a unit.
* In such cases, comparisons like t > 0 or t != 0 make sense, but comparisons
* like t > 1 or t != 1 do not.
* like t > 1 or t != 1 do not. Comparing against kZero expresses exactly that,
* without constructing a T.
*
* The class Zero allows such comparisons to be easily made.
*
* The comparing class T either needs to have a method called signum() which
* returns a positive number, 0, or a negative; or there needs to be a signum
* function which resolves in the namespace which takes an instance of T and
* returns a positive, zero or negative number.
* A type T participates if either `t.signum()` or an unqualified `signum(t)`
* found by argument-dependent lookup returns an integer that is negative,
* zero, or positive according to the sign of t. Both `t == kZero` and
* `kZero == t` work, as do all six relational operators in either order.
*/
struct Zero
{
explicit Zero() = default;
@@ -30,115 +28,44 @@ struct Zero
inline constexpr Zero kZero{};
/**
* Default implementation of signum calls the method on the class.
* Default implementation of signum: call the member function.
*/
template <typename T>
auto
signum(T const& t)
template <class T>
requires requires(T const& t) {
{ t.signum() } -> std::integral;
}
[[nodiscard]] constexpr auto
signum(T const& t) noexcept(noexcept(t.signum()))
{
return t.signum();
}
namespace detail::zero_helper {
namespace detail {
// For argument dependent lookup to function properly, calls to signum must
// be made from a namespace that does not include overloads of the function..
/**
* A type with a usable signum: either the member-based default above, or a
* `signum(t)` overload in T's own namespace, found by ADL. A user overload
* that is a better match than the template wins, as usual.
*/
template <class T>
auto
callSignum(T const& t)
concept HasSignum = requires(T const& t) {
{ signum(t) } -> std::integral;
};
} // namespace detail
template <detail::HasSignum T>
[[nodiscard]] constexpr bool
operator==(T const& t, Zero) noexcept(noexcept(signum(t)))
{
return signum(t);
return signum(t) == 0;
}
} // namespace detail::zero_helper
// Handle operators where T is on the left side using signum.
template <typename T>
bool
operator==(T const& t, Zero)
template <detail::HasSignum T>
[[nodiscard]] constexpr std::strong_ordering
operator<=>(T const& t, Zero) noexcept(noexcept(signum(t)))
{
return detail::zero_helper::callSignum(t) == 0;
}
template <typename T>
bool
operator!=(T const& t, Zero)
{
return detail::zero_helper::callSignum(t) != 0;
}
template <typename T>
bool
operator<(T const& t, Zero)
{
return detail::zero_helper::callSignum(t) < 0;
}
template <typename T>
bool
operator>(T const& t, Zero)
{
return detail::zero_helper::callSignum(t) > 0;
}
template <typename T>
bool
operator>=(T const& t, Zero)
{
return detail::zero_helper::callSignum(t) >= 0;
}
template <typename T>
bool
operator<=(T const& t, Zero)
{
return detail::zero_helper::callSignum(t) <= 0;
}
// Handle operators where T is on the right side by
// reversing the operation, so that T is on the left side.
template <typename T>
bool
operator==(Zero, T const& t)
{
return t == kZero;
}
template <typename T>
bool
operator!=(Zero, T const& t)
{
return t != kZero;
}
template <typename T>
bool
operator<(Zero, T const& t)
{
return t > kZero;
}
template <typename T>
bool
operator>(Zero, T const& t)
{
return t < kZero;
}
template <typename T>
bool
operator>=(Zero, T const& t)
{
return t <= kZero;
}
template <typename T>
bool
operator<=(Zero, T const& t)
{
return t >= kZero;
return signum(t) <=> 0;
}
} // namespace beast
+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;
}
};
//------------------------------------------------------------------------------
@@ -1,11 +1,17 @@
#pragma once
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/SLEBase.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STVector256.h>
#include <cstddef>
#include <optional>
namespace xrpl {
@@ -25,6 +31,31 @@ public:
: Base(keylet::skip(), view, j)
{
}
/**
* Looks up a hash `diff` slots back from the most recent entry in the
* sfHashes vector (diff == 0 is the most recent entry).
*
* Callers decide which skip list entry to read and how to translate a
* target ledger sequence into `diff`; this only does the bounds check
* and vector indexing shared by both the recent (stride 1) and distant
* (stride 256) skip lists.
*
* @param diff how many slots back from the most recent hash to look up;
* 0 is the most recent hash.
* @return the hash at that slot, or std::nullopt if the entry does not
* exist or `diff` is out of range.
*/
[[nodiscard]] std::optional<UInt256>
hashAt(std::size_t diff) const
{
if (!this->exists())
return std::nullopt;
STVector256 const vec = (*this)->getFieldV256(sfHashes);
if (vec.size() > diff)
return vec[vec.size() - diff - 1];
return std::nullopt;
}
};
using LedgerHashesEntryR = LedgerHashesEntry<ReadView>;
-16
View File
@@ -101,22 +101,6 @@ operator<<(std::ostream& os, AccountID const& x)
return os;
}
/**
* Initialize the global cache used to map AccountID to base58 conversions.
*
* The cache is optional and need not be initialized. But because conversion
* is expensive (it requires a SHA-256 operation) in most cases the overhead
* of the cache is worth the benefit.
*
* @param count The number of entries the cache should accommodate. Zero will
* disable the cache, releasing any memory associated with it.
*
* @note The function will only initialize the cache the first time it is
* invoked. Subsequent invocations do nothing.
*/
void
initAccountIdCache(std::size_t count);
} // namespace xrpl
//------------------------------------------------------------------------------
+2 -2
View File
@@ -1,10 +1,10 @@
#pragma once
#include <xrpl/basics/contract.h>
#include <xrpl/beast/type_name.h>
#include <xrpl/protocol/SOTemplate.h>
#include <boost/container/flat_map.hpp>
#include <boost/core/type_name.hpp>
#include <algorithm>
#include <cstddef>
@@ -84,7 +84,7 @@ public:
* Derived classes will load the object with all the known formats.
*/
private:
KnownFormats() : name_(beast::typeName<Derived>())
KnownFormats() : name_(boost::core::type_name<Derived>())
{
}
+56 -51
View File
@@ -2,6 +2,7 @@
#include <xrpl/basics/CountedObject.h>
#include <xrpl/basics/UnorderedContainers.h>
#include <xrpl/basics/enum_bitops.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/AccountID.h>
@@ -15,6 +16,7 @@
#include <cstdint>
#include <memory>
#include <optional>
#include <type_traits>
#include <utility>
#include <vector>
@@ -22,18 +24,8 @@ namespace xrpl {
class STPathElement final : public CountedObject<STPathElement>
{
unsigned int type_;
AccountID accountID_;
PathAsset assetID_;
AccountID issuerID_;
bool isOffer_;
std::size_t hashValue_;
public:
// Bitwise values (typeCurrency | typeMPT)
// NOLINTNEXTLINE(cppcoreguidelines-use-enum-class)
enum Type {
enum class Type : std::uint8_t {
TypeNone = 0x00,
TypeAccount = 0x01, // Rippling through an account (vs taking an offer).
TypeCurrency = 0x10, // Currency follows.
@@ -45,6 +37,8 @@ public:
// Combination of all types.
};
using enum Type;
STPathElement();
STPathElement(STPathElement const&) = default;
STPathElement&
@@ -62,42 +56,42 @@ public:
bool forceAsset = false);
STPathElement(
unsigned int uType,
Type type,
AccountID const& account,
PathAsset const& asset,
AccountID const& issuer);
[[nodiscard]] std::uint32_t
getNodeType() const;
[[nodiscard]] Type
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 +100,35 @@ public:
getMPTID() const;
[[nodiscard]] AccountID const&
getIssuerID() const;
getIssuerID() const noexcept;
[[nodiscard]] bool
isType(Type const& pe) const;
isType(Type pe) const noexcept;
bool
operator==(STPathElement const& t) const;
friend bool
operator==(STPathElement const& lhs, STPathElement const& rhs) noexcept
{
return lhs.isType(TypeAccount) == rhs.isType(TypeAccount) &&
lhs.hashValue_ == rhs.hashValue_ && lhs.accountID_ == rhs.accountID_ &&
lhs.assetID_ == rhs.assetID_ && lhs.issuerID_ == rhs.issuerID_;
}
private:
static std::size_t
getHash(STPathElement const& element);
Type type_;
AccountID accountID_;
PathAsset assetID_;
AccountID issuerID_;
bool isOffer_;
std::size_t hashValue_;
};
template <>
struct enum_bitops::OptIn<STPathElement::Type> : std::true_type
{
};
template <class Hasher>
@@ -124,7 +136,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(STPathElement::TypeAccount));
hash_append(h, e.getAccountID());
hash_append(h, e.getPathAsset());
hash_append(h, e.getIssuerID());
@@ -404,11 +416,11 @@ inline STPathElement::STPathElement(
}
inline STPathElement::STPathElement(
unsigned int uType,
Type type,
AccountID const& account,
PathAsset const& asset,
AccountID const& issuer)
: type_(uType)
: type_(type)
, accountID_(account)
, assetID_(asset)
, issuerID_(issuer)
@@ -423,56 +435,56 @@ inline STPathElement::STPathElement(
hashValue_ = getHash(*this);
}
inline std::uint32_t
STPathElement::getNodeType() const
inline STPathElement::Type
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(Type pe) const noexcept
{
return (type_ & pe) != 0u;
return (type_ & pe) != STPathElement::TypeNone;
}
inline bool
STPathElement::hasIssuer() const
STPathElement::hasIssuer() const noexcept
{
return isType(STPathElement::TypeIssuer);
}
inline bool
STPathElement::hasCurrency() const
STPathElement::hasCurrency() const noexcept
{
return isType(STPathElement::TypeCurrency);
}
inline bool
STPathElement::hasMPT() const
STPathElement::hasMPT() const noexcept
{
return isType(STPathElement::TypeMpt);
}
inline bool
STPathElement::hasAsset() const
STPathElement::hasAsset() const noexcept
{
return isType(STPathElement::TypeAsset);
}
inline bool
STPathElement::isNone() const
STPathElement::isNone() const noexcept
{
return getNodeType() == STPathElement::TypeNone;
}
@@ -480,13 +492,13 @@ STPathElement::isNone() const
// 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 +516,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))
+6
View File
@@ -238,6 +238,12 @@ inline constexpr FlagValue tfUniversalMask = ~tfUniversal;
TF_FLAG(tfSponsorshipEnd, 0x00010000) \
TF_FLAG(tfSponsorshipCreate, 0x00020000) \
TF_FLAG(tfSponsorshipReassign, 0x00040000), \
MASK_ADJ(0)) \
\
TRANSACTION(ConfidentialMPTHolderKeyUpdate, \
TF_FLAG(tfHolderKeyRotation, 0x00010000) \
TF_FLAG(tfHolderKeyRecovery, 0x00020000) \
TF_FLAG(tfCancelRecovery, 0x00040000), \
MASK_ADJ(0))
// clang-format on
@@ -439,6 +439,7 @@ LEDGER_ENTRY(ltMPTOKEN, 0x007f, MPToken, mptoken, ({
{sfIssuerKeyMirrorEpoch, SoeOptional},
{sfAuditorKeyMirrorEpoch, SoeOptional},
{sfHolderEncryptionKey, SoeOptional},
{sfRecoveryKey, SoeOptional},
}))
/** A ledger object which tracks Oracle
@@ -330,6 +330,7 @@ TYPED_SFIELD(sfAuditorEncryptionKey, VL, 44)
TYPED_SFIELD(sfAmountCommitment, VL, 45)
TYPED_SFIELD(sfBalanceCommitment, VL, 46)
TYPED_SFIELD(sfBytecode, VL, 47)
TYPED_SFIELD(sfRecoveryKey, VL, 48)
// account (common)
TYPED_SFIELD(sfAccount, ACCOUNT, 1)
@@ -1147,12 +1147,27 @@ TRANSACTION(ttCONFIDENTIAL_MPT_MIRROR_UPDATE, 92, ConfidentialMPTMirrorUpdate,
{sfZKProof, SoeRequired},
}))
/** This transaction rotates or recovers a confidential MPT holder's ElGamal encryption key,
or cancels a recovery. */
#if TRANSACTION_INCLUDE
# include <xrpl/tx/transactors/token/ConfidentialMPTHolderKeyUpdate.h>
#endif
TRANSACTION(ttCONFIDENTIAL_MPT_HOLDER_KEY_UPDATE, 93, ConfidentialMPTHolderKeyUpdate,
({.amendment = featureConfidentialMPTKeyRotation}),
({
{sfMPTokenIssuanceID, SoeRequired},
{sfHolderEncryptionKey, SoeOptional},
{sfConfidentialBalanceSpending, SoeOptional},
{sfConfidentialBalanceInbox, SoeOptional},
{sfZKProof, SoeOptional},
}))
/** This transaction posts an unsigned transaction on-ledger as a
TransactionProposal, pending multi-signature collection. */
#if TRANSACTION_INCLUDE
# include <xrpl/tx/transactors/proposal/TransactionProposalCreate.h>
#endif
TRANSACTION(ttTRANSACTION_PROPOSAL_CREATE, 93, TransactionProposalCreate,
TRANSACTION(ttTRANSACTION_PROPOSAL_CREATE, 95, TransactionProposalCreate,
({.amendment = featureCosign}),
({
{sfProposedTransaction, SoeRequired},
+1
View File
@@ -392,6 +392,7 @@ JSS(max_ledger); // in/out: LedgerCleaner
JSS(max_queue_size); // out: TxQ
JSS(max_spend_drops); // out: AccountInfo
JSS(max_spend_drops_total); // out: AccountInfo
JSS(maximum);
JSS(mean); // out: get_aggregate_price
JSS(median); // out: get_aggregate_price
JSS(median_fee); // out: TxQ
@@ -339,6 +339,30 @@ public:
{
return this->sle_->isFieldPresent(sfHolderEncryptionKey);
}
/**
* @brief Get sfRecoveryKey (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getRecoveryKey() const
{
if (hasRecoveryKey())
return this->sle_->at(sfRecoveryKey);
return std::nullopt;
}
/**
* @brief Check if sfRecoveryKey is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasRecoveryKey() const
{
return this->sle_->isFieldPresent(sfRecoveryKey);
}
};
/**
@@ -552,6 +576,17 @@ public:
return *this;
}
/**
* @brief Set sfRecoveryKey (SoeOptional)
* @return Reference to this builder for method chaining.
*/
MPTokenBuilder&
setRecoveryKey(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfRecoveryKey] = value;
return *this;
}
/**
* @brief Build and return the completed MPToken wrapper.
* @param index The ledger entry index.
@@ -0,0 +1,279 @@
// This file is auto-generated. Do not edit.
#pragma once
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol_autogen/TransactionBase.h>
#include <xrpl/protocol_autogen/TransactionBuilderBase.h>
#include <xrpl/json/json_value.h>
#include <stdexcept>
#include <optional>
namespace xrpl::transactions {
class ConfidentialMPTHolderKeyUpdateBuilder;
/**
* @brief Transaction: ConfidentialMPTHolderKeyUpdate
*
* Type: ttCONFIDENTIAL_MPT_HOLDER_KEY_UPDATE (93)
* Delegable: Delegation::NotDelegable
* Amendment: featureConfidentialMPTKeyRotation
* Privileges: Privilege::NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use ConfidentialMPTHolderKeyUpdateBuilder to construct new transactions.
*/
class ConfidentialMPTHolderKeyUpdate : public TransactionBase
{
public:
static constexpr xrpl::TxType txType = ttCONFIDENTIAL_MPT_HOLDER_KEY_UPDATE;
/**
* @brief Construct a ConfidentialMPTHolderKeyUpdate transaction wrapper from an existing STTx object.
* @throws std::runtime_error if the transaction type doesn't match.
*/
explicit ConfidentialMPTHolderKeyUpdate(std::shared_ptr<STTx const> tx)
: TransactionBase(std::move(tx))
{
// Verify transaction type
if (tx_->getTxnType() != txType)
{
throw std::runtime_error("Invalid transaction type for ConfidentialMPTHolderKeyUpdate");
}
}
// Transaction-specific field getters
/**
* @brief Get sfMPTokenIssuanceID (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_UINT192::type::value_type
getMPTokenIssuanceID() const
{
return this->tx_->at(sfMPTokenIssuanceID);
}
/**
* @brief Get sfHolderEncryptionKey (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getHolderEncryptionKey() const
{
if (hasHolderEncryptionKey())
{
return this->tx_->at(sfHolderEncryptionKey);
}
return std::nullopt;
}
/**
* @brief Check if sfHolderEncryptionKey is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasHolderEncryptionKey() const
{
return this->tx_->isFieldPresent(sfHolderEncryptionKey);
}
/**
* @brief Get sfConfidentialBalanceSpending (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getConfidentialBalanceSpending() const
{
if (hasConfidentialBalanceSpending())
{
return this->tx_->at(sfConfidentialBalanceSpending);
}
return std::nullopt;
}
/**
* @brief Check if sfConfidentialBalanceSpending is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasConfidentialBalanceSpending() const
{
return this->tx_->isFieldPresent(sfConfidentialBalanceSpending);
}
/**
* @brief Get sfConfidentialBalanceInbox (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getConfidentialBalanceInbox() const
{
if (hasConfidentialBalanceInbox())
{
return this->tx_->at(sfConfidentialBalanceInbox);
}
return std::nullopt;
}
/**
* @brief Check if sfConfidentialBalanceInbox is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasConfidentialBalanceInbox() const
{
return this->tx_->isFieldPresent(sfConfidentialBalanceInbox);
}
/**
* @brief Get sfZKProof (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getZKProof() const
{
if (hasZKProof())
{
return this->tx_->at(sfZKProof);
}
return std::nullopt;
}
/**
* @brief Check if sfZKProof is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasZKProof() const
{
return this->tx_->isFieldPresent(sfZKProof);
}
};
/**
* @brief Builder for ConfidentialMPTHolderKeyUpdate transactions.
*
* Provides a fluent interface for constructing transactions with method chaining.
* Uses STObject internally for flexible transaction construction.
* Inherits common field setters from TransactionBuilderBase.
*/
class ConfidentialMPTHolderKeyUpdateBuilder : public TransactionBuilderBase<ConfidentialMPTHolderKeyUpdateBuilder>
{
public:
/**
* @brief Construct a new ConfidentialMPTHolderKeyUpdateBuilder with required fields.
* @param account The account initiating the transaction.
* @param mPTokenIssuanceID The sfMPTokenIssuanceID field value.
* @param sequence Optional sequence number for the transaction.
* @param fee Optional fee for the transaction.
*/
ConfidentialMPTHolderKeyUpdateBuilder(SF_ACCOUNT::type::value_type account,
std::decay_t<typename SF_UINT192::type::value_type> const& mPTokenIssuanceID, std::optional<SF_UINT32::type::value_type> sequence = std::nullopt,
std::optional<SF_AMOUNT::type::value_type> fee = std::nullopt
)
: TransactionBuilderBase<ConfidentialMPTHolderKeyUpdateBuilder>(ttCONFIDENTIAL_MPT_HOLDER_KEY_UPDATE, account, sequence, fee)
{
setMPTokenIssuanceID(mPTokenIssuanceID);
}
/**
* @brief Construct a ConfidentialMPTHolderKeyUpdateBuilder from an existing STTx object.
* @param tx The existing transaction to copy from.
* @throws std::runtime_error if the transaction type doesn't match.
*/
ConfidentialMPTHolderKeyUpdateBuilder(std::shared_ptr<STTx const> tx)
{
if (tx->getTxnType() != ttCONFIDENTIAL_MPT_HOLDER_KEY_UPDATE)
{
throw std::runtime_error("Invalid transaction type for ConfidentialMPTHolderKeyUpdateBuilder");
}
object_ = *tx;
}
/**
* @brief Transaction-specific field setters
*/
/**
* @brief Set sfMPTokenIssuanceID (SoeRequired)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTHolderKeyUpdateBuilder&
setMPTokenIssuanceID(std::decay_t<typename SF_UINT192::type::value_type> const& value)
{
object_[sfMPTokenIssuanceID] = value;
return *this;
}
/**
* @brief Set sfHolderEncryptionKey (SoeOptional)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTHolderKeyUpdateBuilder&
setHolderEncryptionKey(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfHolderEncryptionKey] = value;
return *this;
}
/**
* @brief Set sfConfidentialBalanceSpending (SoeOptional)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTHolderKeyUpdateBuilder&
setConfidentialBalanceSpending(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfConfidentialBalanceSpending] = value;
return *this;
}
/**
* @brief Set sfConfidentialBalanceInbox (SoeOptional)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTHolderKeyUpdateBuilder&
setConfidentialBalanceInbox(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfConfidentialBalanceInbox] = value;
return *this;
}
/**
* @brief Set sfZKProof (SoeOptional)
* @return Reference to this builder for method chaining.
*/
ConfidentialMPTHolderKeyUpdateBuilder&
setZKProof(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfZKProof] = value;
return *this;
}
/**
* @brief Build and return the ConfidentialMPTHolderKeyUpdate wrapper.
* @param publicKey The public key for signing.
* @param secretKey The secret key for signing.
* @return The constructed transaction wrapper.
*/
ConfidentialMPTHolderKeyUpdate
build(PublicKey const& publicKey, SecretKey const& secretKey)
{
sign(publicKey, secretKey);
return ConfidentialMPTHolderKeyUpdate{std::make_shared<STTx>(std::move(object_))};
}
};
} // namespace xrpl::transactions
@@ -18,7 +18,7 @@ class TransactionProposalCreateBuilder;
/**
* @brief Transaction: TransactionProposalCreate
*
* Type: ttTRANSACTION_PROPOSAL_CREATE (93)
* Type: ttTRANSACTION_PROPOSAL_CREATE (95)
* Delegable: Delegation::NotDelegable
* Amendment: featureCosign
* Privileges: Privilege::NoPriv
+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);
}
/**
+8 -2
View File
@@ -46,6 +46,9 @@ class ValidMPTIssuance
* MPTokens and RippleStates deleted during apply. finalize() checks each
* holder's AccountRoot to detect vault pseudo-account holdings deleted
* outside VaultDelete. All these checks are gated on fixCleanup3_2_0.
*
* Under fixCleanup3_5_0, finalize() also rejects any MPToken erased with
* a non-zero sfMPTAmount.
*/
std::vector<std::shared_ptr<SLE const>> deletedHoldings_;
@@ -138,8 +141,9 @@ public:
* - Convert/ConvertBack symmetry:
* Regular MPToken balance change (±X) == COA (Confidential Outstanding Amount) change (∓X)
* - Cannot delete MPToken with non-zero confidential state:
* Cannot delete if sfIssuerEncryptedBalance exists
* Cannot delete if sfConfidentialBalanceInbox and sfConfidentialBalanceSpending exist
* Cannot delete if any of sfConfidentialBalanceSpending, sfConfidentialBalanceInbox,
* sfIssuerEncryptedBalance or sfAuditorEncryptedBalance is present, and the issuance's
* sfConfidentialOutstandingAmount is non-zero. Mirrors MPTokenAuthorize::preclaim.
* - Privacy flag consistency:
* MPToken confidential balance fields can only be created or changed if
* lsfMPTCanHoldConfidentialBalance is set on the issuance.
@@ -162,6 +166,8 @@ class ValidConfidentialMPToken
std::int64_t outstandingDelta = 0;
SLE::const_pointer issuance;
bool deletedWithEncrypted = false;
// True when an erased MPToken had a non-zero pre-tx public balance.
bool deletedWithBalanceBefore = false;
bool badConsistency = false;
bool badCOA = false;
bool changesConfidentialFields = false;
@@ -0,0 +1,84 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/ApplyContext.h>
#include <xrpl/tx/Transactor.h>
#include <cstdint>
namespace xrpl {
/**
* @brief Allows a confidential MPT holder to rotate or recover their ElGamal
* encryption key, or to cancel a pending recovery.
*
* Submitted by the holder. Exactly one of three modes must be selected via
* the transaction flags:
*
* - Rotation (tfHolderKeyRotation): the holder still has their current
* ElGamal private key. They provide a new public key along with their
* current spending/inbox balances re-encrypted under that new key. The
* holder's encryption key and confidential balances are updated
* immediately.
*
* - Recovery (tfHolderKeyRecovery): the holder has lost their current
* private key. They provide a new public key but cannot provide
* re-encrypted balances. The new key is recorded as a pending
* sfRecoveryKey; the confidential balances are left untouched. Completing
* the recovery (rewriting the balances) is done separately by the issuer
* via ConfidentialMPTRecoverBalance.
*
* - Cancel (tfCancelRecovery): the holder revokes a pending recovery
* authorization created by a prior Recovery-mode transaction. No key,
* balances, or proof are carried; authorization is via the holder's
* ordinary XRPL signature. sfRecoveryKey is removed and everything else
* is left untouched. Fails if no recovery is pending.
*
* @note Zero-knowledge proof verification for Rotation and Recovery is
* deferred to a follow-up once the mpt-crypto constructions are finalized.
*/
class ConfidentialMPTHolderKeyUpdate : public Transactor
{
public:
static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal;
explicit ConfidentialMPTHolderKeyUpdate(ApplyContext& ctx) : Transactor(ctx)
{
}
static bool
checkExtraFeatures(PreflightContext const& ctx);
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
void
visitInvariantEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after) override;
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,
TER result,
XRPAmount fee,
ReadView const& view,
beast::Journal const& j) override;
};
} // namespace xrpl
+73 -73
View File
@@ -2,144 +2,144 @@ Detected OS: macos (Darwin arm64)
Core build tools:
✅ cmake
cmake version 4.1.2
/nix/store/gvabsb4yqb5xsqzqph54rijnn4zpihnp-cmake-4.1.2/bin/cmake
cmake version 4.4.3
/nix/store/q85csxf4s4shx89zif097h1ql4ax1rrz-cmake-4.4.3/bin/cmake
✅ conan
Conan version 2.28.1
/nix/store/9jiyxmkpwmn6dcqs0765s83riw3l5ail-conan-2.28.1/bin/conan
Conan version 2.32.0
/nix/store/921jqsgbilixmr3xchj95si0jl8pi13z-conan-2.32.0/bin/conan
✅ git
git version 2.54.0
/nix/store/a14yxcqvv9x2l9mllgpirzhvz93pgprg-git-2.54.0/bin/git
git version 2.55.0
/nix/store/gw7c7m0dwca5lg4152x9lpmxpj171bzw-git-2.55.0/bin/git
✅ python3
Python 3.13.13
/nix/store/ygxqin6ydzjfawywqpp5pal8wv6sf5bh-python3-3.13.13/bin/python3.13
Python 3.14.7
/nix/store/0kcws5blwbdyxqxjd1w8hjipz61xny3k-python3-3.14.7/bin/python3.14
Development tooling:
✅ ccache
ccache version 4.13.6
/nix/store/57davyvs6p6dkrl3svzwg1ph18wsy4cz-ccache-4.13.6/bin/ccache
/nix/store/ydr5nlzxp1djb5256y0zzqa87vz55gsk-ccache-4.13.6/bin/ccache
✅ clang
clang version 22.1.7
/nix/store/192glrb2cldvziyf3378mzjqbzx3ih4g-clang-wrapper-22.1.7/bin/clang
clang version 22.1.8
/nix/store/dakcxgwz4ixkk6ps50b7vn3dvblyzsnf-clang-wrapper-22.1.8/bin/clang
✅ clang-22
clang version 22.1.7
/nix/store/rbap7zqq7mw00fyqa02p5rj7gqjp4w5i-clang-22/bin/clang-22
clang version 22.1.8
/nix/store/w826sqb98nnym6kzackg829664iqdlxj-clang-22/bin/clang-22
✅ clang++
clang version 22.1.7
/nix/store/192glrb2cldvziyf3378mzjqbzx3ih4g-clang-wrapper-22.1.7/bin/clang++
clang version 22.1.8
/nix/store/dakcxgwz4ixkk6ps50b7vn3dvblyzsnf-clang-wrapper-22.1.8/bin/clang++
✅ clang++-22
clang version 22.1.7
/nix/store/v9haf787f7bcz0mq1sad4bpyx21pj6li-clang++-22/bin/clang++-22
clang version 22.1.8
/nix/store/wnwyw3yfqrhpxij65bl3pa1x65q2zg3m-clang++-22/bin/clang++-22
✅ ClangBuildAnalyzer
ClangBuildAnalyzer 1.6.0
/nix/store/4l50ds9fa2mkvh7wg8qzrlbmjs12sb8l-clangbuildanalyzer-1.6.0/bin/ClangBuildAnalyzer
/nix/store/c7jjnw29ra78jdlzjww2m374izhy05n8-clangbuildanalyzer-1.6.0/bin/ClangBuildAnalyzer
✅ curl
curl 8.20.0 (aarch64-apple-darwin25.3.0) libcurl/8.20.0 OpenSSL/3.6.2 zlib/1.3.2 libssh2/1.11.1 nghttp2/1.69.0 mit-krb5/1.22.1
/nix/store/kclq0czaxvsgh4ym9ld7b6iwy50l1snk-curl-8.20.0-bin/bin/curl
curl 8.22.0 (aarch64-apple-darwin25.6.0) libcurl/8.22.0 OpenSSL/3.5.8 zlib/1.3.2 libssh2/1.11.1 nghttp2/1.70.0 mit-krb5/1.22.2
/nix/store/pq1h17abqy6q5y6k5dn9qj0fwb89flba-curl-8.22.0-bin/bin/curl
✅ file
file-5.47
/nix/store/dax63li7wwcbqxxkkgzc4g2rx7d4w86x-file-5.47/bin/file
file-5.48
/nix/store/y44vpziwbzfm8020dzlqqbybyyy7lb6g-file-5.48/bin/file
✅ less
less 692 (PCRE2 regular expressions)
/nix/store/lvr16y75r1pdxpdv0aph5ak2yd0hkvqm-less-692/bin/less
less 710 (PCRE2 regular expressions)
/nix/store/3yn3dx5fwg5i0inqqzwnxq7f6lwhpjqa-less-710/bin/less
✅ make
GNU Make 4.4.1
/nix/store/8wwiw8pwyhrkzyq28hqzxfl4z84lks81-gnumake-4.4.1/bin/make
/nix/store/8l4wgxmhdxfvc9g18y76bnxl1ghgcp7n-gnumake-4.4.1/bin/make
✅ netstat
present
/nix/store/qsd1kzqb0ahrk433vmyl245gp623j19s-network_cmds-730.80.3/bin/netstat
/nix/store/57fqvgdav8lrmzndkwm7qrgksl01yj23-network_cmds-741.100.2/bin/netstat
✅ ninja
1.13.2
/nix/store/bqykhrblarkj4fl0hz2mf8ngwfv6x6bz-ninja-1.13.2/bin/ninja
/nix/store/infm4vlx71q83bqz0k559w1gn4b0rq4i-ninja-1.13.2/bin/ninja
✅ perl
v5.42.0
/nix/store/js13ri9fvm0ajk1fpd3acigys2a9whdv-perl-5.42.0/bin/perl
v5.42.3
/nix/store/7rvr6qjfzk2ybzn1imjw6c6qq1n1a9cr-perl-5.42.3/bin/perl
✅ pkg-config
0.29.2
/nix/store/lzrwr375jqhhbca116kja96xf1md83l8-pkg-config-wrapper-0.29.2/bin/pkg-config
/nix/store/nhgylvj98klqggy54h7gbc3b2ia4a00y-pkg-config-wrapper-0.29.2/bin/pkg-config
✅ vim
VIM - Vi IMproved 9.2 (2026 Feb 14, compiled Jan 01 1980 00:00:00)
/nix/store/6vbkykg92w603c0sw3mkk7p7mfaawbns-vim-9.2.0389/bin/vim
/nix/store/akaa9kqm3b8rlkv61kvsqr2l2j70rvxb-vim-9.2.1001/bin/vim
✅ zip
Zip 3.0
/nix/store/z6ph729vcakbvz3wh8ln1wk6mi06w487-zip-3.0/bin/zip
/nix/store/dvawr36npnad4xa6dxi2ss0fw44aa82r-zip-3.0/bin/zip
✅ clang-apply-replacements
clang-apply-replacements version 22.1.7
/nix/store/vzyyjf3cm1hbj9wcr2qcb66x6j98zpy7-clang-tools-22.1.7/bin/clang-apply-replacements
clang-apply-replacements version 22.1.8
/nix/store/5bij4zn161lagwyndmrr21vmxjg054nv-clang-tools-22.1.8/bin/clang-apply-replacements
✅ clang-apply-replacements-22
clang-apply-replacements version 22.1.7
/nix/store/m3ii69rca4077lf4wlk7m3jcag1fs577-clang-apply-replacements-22/bin/clang-apply-replacements-22
clang-apply-replacements version 22.1.8
/nix/store/w4bz034k4l0w719rrmbwnqcgyjbswy81-clang-apply-replacements-22/bin/clang-apply-replacements-22
✅ clang-format
clang-format version 22.1.7
/nix/store/vzyyjf3cm1hbj9wcr2qcb66x6j98zpy7-clang-tools-22.1.7/bin/clang-format
clang-format version 22.1.8
/nix/store/5bij4zn161lagwyndmrr21vmxjg054nv-clang-tools-22.1.8/bin/clang-format
✅ clang-format-22
clang-format version 22.1.7
/nix/store/4fawqy6ngqcsqd2ygyyzm93q0xy3f5gs-clang-format-22/bin/clang-format-22
clang-format version 22.1.8
/nix/store/w7z346l0r8y36b6lc5i7jzj3w0581l2y-clang-format-22/bin/clang-format-22
✅ clang-tidy
LLVM version 22.1.7
/nix/store/vzyyjf3cm1hbj9wcr2qcb66x6j98zpy7-clang-tools-22.1.7/bin/clang-tidy
LLVM version 22.1.8
/nix/store/5bij4zn161lagwyndmrr21vmxjg054nv-clang-tools-22.1.8/bin/clang-tidy
✅ clang-tidy-22
LLVM version 22.1.7
/nix/store/jqw4280saixaxxihwdba9ldm2fsm6dr3-clang-tidy-22/bin/clang-tidy-22
LLVM version 22.1.8
/nix/store/0lv25qd3ddgjm3ndn70q61jfr6krlkl7-clang-tidy-22/bin/clang-tidy-22
✅ dot
dot - graphviz version 12.2.1 (0)
/nix/store/ijb4fbnqa6wzlpqnhb6q9knqpf7qqn5z-graphviz-12.2.1/bin/dot
dot - graphviz version 15.1.1 (0)
/nix/store/mc99a6bpbk2waym2cnw6ifmxli4ndb4x-graphviz-15.1.1/bin/dot
✅ doxygen
1.16.1
/nix/store/kbryjdpq9jizjb0ws0nzbf2h2ymbdiwm-doxygen-1.16.1/bin/doxygen
1.17.0
/nix/store/kz43yaj0wg6fx6cbrcmi2442z1ramy0c-doxygen-1.17.0/bin/doxygen
✅ gcovr
gcovr 8.4
/nix/store/wn8jiyh9p0bybs96s4163qp3k8vfmczx-python3.13-gcovr-8.4/bin/gcovr
/nix/store/czj1vxsizrbba280gg22cys40qxkqjgr-python3.14-gcovr-8.4/bin/gcovr
✅ gh
gh version 2.94.0 (nixpkgs)
/nix/store/fhnpw0hs0gjms1ha6ap02jq7rx13gkbp-gh-2.94.0/bin/gh
gh version 2.102.0 (2026-09-30)
/nix/store/s8bahl6zssvry93ihivfmk1l6c58byrr-gh-2.102.0/bin/gh
✅ git-cliff
git-cliff 2.13.1
/nix/store/cy0wwhgxa7yvrz97zydbq6sqmixc90fq-git-cliff-2.13.1/bin/git-cliff
git-cliff 2.14.2
/nix/store/k4m5yk0qb7hfslwsq1zjajz1d94j12bj-git-cliff-2.14.2/bin/git-cliff
✅ git-lfs
git-lfs/3.7.1 (3.7.1; darwin arm64; go 1.26.3)
/nix/store/k9r7zjfjplqa4d5s71cqvf2iv73jd9mc-git-lfs-3.7.1/bin/git-lfs
git-lfs/3.8.0 (3.8.0; darwin arm64; go 1.26.8)
/nix/store/wzixjr3yb40a13m2q654hm3bdjn6sal3-git-lfs-3.8.0/bin/git-lfs
✅ gpg
gpg (GnuPG) 2.4.9
/nix/store/cgh6iwzz5jgx9z5whka4vgj210i6npc6-gnupg-2.4.9/bin/gpg
/nix/store/0ldfwnwx385i5kh7cyvxx4mkvgjhs5rh-gnupg-2.4.9/bin/gpg
✅ pre-commit
pre-commit 4.5.1
/nix/store/z3cca68620w0w10f090szgzdnmh1waf2-pre-commit-4.5.1/bin/pre-commit
pre-commit 4.6.2
/nix/store/ivh54xypg9qvd3ha9lis0i8p4nkf8n7a-pre-commit-4.6.2/bin/pre-commit
✅ run-clang-tidy
usage: run-clang-tidy [-h] [-allow-enabling-alpha-checkers]
/nix/store/4x28x911z2f9y7adqlh3qspp4a16dig7-run-clang-tidy/bin/run-clang-tidy
/nix/store/3xss1mm3mh1hzcmg182na6ki1p5rnxf1-run-clang-tidy/bin/run-clang-tidy
✅ run-clang-tidy-22
usage: run-clang-tidy [-h] [-allow-enabling-alpha-checkers]
/nix/store/x8iymrh76sk5q91ryg5pa7i32s6gfh34-run-clang-tidy-22/bin/run-clang-tidy-22
/nix/store/w68r9z07hcq4fwbfyq3yvc7qxm8aqfbl-run-clang-tidy-22/bin/run-clang-tidy-22
Rust toolchain:
✅ cargo
cargo 1.97.1 (c980f4866 2026-06-30)
/nix/store/bnfk1sl4s9angb0vj1cj9a5y5zvqinwy-rust-minimal-1.97.1/bin/cargo
/nix/store/gklji8yq8yrx1k039c10kwz60vqf2dxj-rust-minimal-1.97.1/bin/cargo
✅ cargo-audit
cargo-audit-audit 0.22.1
/nix/store/snwkga2f5gyf404h7mmp9wriwxb8v65f-cargo-audit-0.22.1/bin/cargo-audit
cargo-audit-audit 0.22.2
/nix/store/7jgrwj557l91940aj0ag4i924ima1cqn-cargo-audit-0.22.2/bin/cargo-audit
✅ cargo-llvm-cov
cargo-llvm-cov 0.8.5
/nix/store/fpiqdh91gwyxalqp409ynm0s0g086w7w-cargo-llvm-cov-0.8.5/bin/cargo-llvm-cov
cargo-llvm-cov 0.9.0
/nix/store/5q1h285z8zzkw2qxig9a0b1avcsk1cf0-cargo-llvm-cov-0.9.0/bin/cargo-llvm-cov
✅ cargo-nextest
cargo-nextest 0.9.137
/nix/store/ylz7m947mhkgsp6i7611id3s3gcd58nq-cargo-nextest-0.9.137/bin/cargo-nextest
cargo-nextest 0.9.146
/nix/store/69v3hx3k597hcxd2zy7yad7fg0aw5yxd-cargo-nextest-0.9.146/bin/cargo-nextest
✅ clippy-driver
clippy 0.1.97 (8bab26f4f6 2026-07-14)
/nix/store/bnfk1sl4s9angb0vj1cj9a5y5zvqinwy-rust-minimal-1.97.1/bin/clippy-driver
/nix/store/gklji8yq8yrx1k039c10kwz60vqf2dxj-rust-minimal-1.97.1/bin/clippy-driver
✅ rust-analyzer
rust-analyzer 1.97.1 (8bab26f4 2026-07-14)
/nix/store/j6apc5pmd0giy15da9p650r8zklslmvi-rust-analyzer-preview-1.97.1-aarch64-apple-darwin/bin/rust-analyzer
/nix/store/30rbyk8gx7v53fxqhs4pp0fvxfjycspr-rust-analyzer-preview-1.97.1-aarch64-apple-darwin/bin/rust-analyzer
✅ rust-nightly
rustc 1.99.0-nightly (87e5904f5 2026-07-20)
/nix/store/fqpjz4l0nsnji8b2pz57mnj0akbp6hcl-rust-nightly/bin/rust-nightly
rustc 1.101.0-nightly (282215592 2026-10-04)
/nix/store/a9552rf3pylcnkijc1z0hf1lcnv50djq-rust-nightly/bin/rust-nightly
✅ rustc
rustc 1.97.1 (8bab26f4f 2026-07-14)
/nix/store/bnfk1sl4s9angb0vj1cj9a5y5zvqinwy-rust-minimal-1.97.1/bin/rustc
/nix/store/gklji8yq8yrx1k039c10kwz60vqf2dxj-rust-minimal-1.97.1/bin/rustc
✅ rustfmt
rustfmt 1.9.0-stable (8bab26f4f6 2026-07-14)
/nix/store/5ymwgr9jqjz7zzbmj0j5vqbwcd3kp0vm-rustfmt-preview-1.97.1-aarch64-apple-darwin/bin/rustfmt
/nix/store/0jmdipyn8266j9kqx2xxm2qkxlrlrjfc-rustfmt-preview-1.97.1-aarch64-apple-darwin/bin/rustfmt
Skipping git-over-HTTPS check (CHECK_TOOLS_SKIP_CLONE is set).
+2 -1
View File
@@ -21,7 +21,8 @@ let
)
);
toolchain = if pkgs.stdenv.isLinux then linux.toolchain else (darwin.toolchain ++ [ darwinEnv ]);
toolchain =
if pkgs.stdenv.hostPlatform.isLinux then linux.toolchain else (darwin.toolchain ++ [ darwinEnv ]);
in
{
default = pkgs.buildEnv {
+7 -6
View File
@@ -20,15 +20,16 @@ let
# Custom-glibc stdenvs, matching the CI environment. darwin has no custom
# glibc, so there they fall back to the plain nixpkgs stdenvs.
customGccStdenv = if pkgs.stdenv.isLinux then linux.gccStdenv else plainGccStdenv;
customClangStdenv = if pkgs.stdenv.isLinux then linux.clangStdenv else plainClangStdenv;
customGccStdenv = if pkgs.stdenv.hostPlatform.isLinux then linux.gccStdenv else plainGccStdenv;
customClangStdenv =
if pkgs.stdenv.hostPlatform.isLinux then linux.clangStdenv else plainClangStdenv;
# gcov matching each gcc shell, so `-Dcoverage=ON` builds work in the shell.
plainGcov = mkGcov {
name = "plain";
cc = gccPackage.cc;
};
customGccGcov = if pkgs.stdenv.isLinux then linux.gcov else plainGcov;
customGccGcov = if pkgs.stdenv.hostPlatform.isLinux then linux.gcov else plainGcov;
# Whole directory: init.sh locates the profiles relative to itself.
conanDir = ../conan;
@@ -51,7 +52,7 @@ let
# Not sdkEnv: a shell's stdenv already sets that up. Prepended so the stub
# beats the nixpkgs libresolv this shell's tooling drags in.
darwinLibresolvHook = pkgs.lib.optionalString pkgs.stdenv.isDarwin (
darwinLibresolvHook = pkgs.lib.optionalString pkgs.stdenv.hostPlatform.isDarwin (
pkgs.lib.concatLines (
pkgs.lib.mapAttrsToList (
name: value: ''export ${name}="${value} ''${${name}:-}"''
@@ -126,7 +127,7 @@ let
in
rec {
# macOS: Nix Clang. Linux: Nix GCC.
default = if pkgs.stdenv.isDarwin then clang else gcc;
default = if pkgs.stdenv.hostPlatform.isDarwin then clang else gcc;
# gcc/clang use the custom-glibc toolchain, matching CI. On darwin there is no
# custom glibc, so they fall back to the plain nixpkgs toolchain.
@@ -171,7 +172,7 @@ rec {
# The *-plain shells (stock nixpkgs toolchain) exist only on Linux: on darwin
# gcc/clang are already plain, so these would be redundant and are omitted, which
# makes `nix develop .#gcc-plain` fail there rather than silently aliasing gcc.
// pkgs.lib.optionalAttrs pkgs.stdenv.isLinux {
// pkgs.lib.optionalAttrs pkgs.stdenv.hostPlatform.isLinux {
gcc-plain = makeShell {
shellName = "gcc-plain";
stdenv = plainGccStdenv;
+6 -4
View File
@@ -217,12 +217,13 @@ The `release-info` action decides the channel from the event, and
| tag | `X.Y.Z-bN` | `beta` | `deb-beta` | `rpm-beta-hosted` |
| tag, any other | `xrpld --version` | `custom` | `deb-custom` | `rpm-custom-hosted` |
| push to `develop` | `0.0.0-dev+<hash>` | `develop` | `deb-develop` | `rpm-develop-hosted` |
| tag, non-public codebase | _any_ | `private` | `deb-private` | `rpm-private-hosted` |
| tag, non-public codebase | _any but `X.Y.Z`_ | `private` | `deb-private` | `rpm-private-hosted` |
A variant is published to the same channel under its own name, so
`xrpld-assert` never overwrites `xrpld`.
Only a tag picks a release channel. Versions sort in row order, so moving to a
Only a tag picks a release channel. A final release, `X.Y.Z`, goes to `stable`
even from a non-public codebase. Versions sort in row order, so moving to a
more mature channel never downgrades. A tag matching none of the release
patterns, such as `X.Y.Z-hotfix1`, publishes to `custom`, which sits outside
that order.
@@ -284,8 +285,9 @@ checks that the server starts in each image. A tag's images are tagged with the
tag name, a develop image as `develop`. With `publish: true`:
- `xrpld` is pushed to `xrplf/xrpld` on Docker Hub using the `DOCKERHUB_TOKEN`
secret, an organization access token for `xrplf`. Private builds are never
pushed there.
secret, an organization access token for `xrplf`. Builds of a non-public
codebase are pushed there only for `stable` releases, whose packages are
public too.
- `voidstar` replaces `/usr/bin/xrpld` with the binary of the `voidstar` build
config, adds `libvoidstar.so` and links the binary into `/symbols`, as
Antithesis expects. It is pushed as `xrpld-voidstar` to the Antithesis
-39
View File
@@ -1,39 +0,0 @@
#include <xrpl/basics/CountedObject.h>
#include <algorithm>
namespace xrpl {
CountedObjects&
CountedObjects::getInstance() noexcept
{
static CountedObjects kInstance;
return kInstance;
}
CountedObjects::CountedObjects() noexcept : count_(0), head_(nullptr)
{
}
CountedObjects::List
CountedObjects::getCounts(int minimumThreshold) const
{
List counts;
// When other operations are concurrent, the count
// might be temporarily less than the actual count.
counts.reserve(count_.load());
for (auto* ctr = head_.load(); ctr != nullptr; ctr = ctr->getNext())
{
if (ctr->getCount() >= minimumThreshold)
counts.emplace_back(ctr->getName(), ctr->getCount());
}
std::ranges::sort(counts);
return counts;
}
} // namespace xrpl
+4 -2
View File
@@ -5,6 +5,7 @@
#include <cerrno>
#include <cstddef>
#include <filesystem>
#include <format>
#include <fstream>
#include <iomanip>
#include <ios>
@@ -99,12 +100,13 @@ uniqueRandomPath(
if (ec)
{
Throw<std::runtime_error>(
"Unable to check path '" + candidate.string() + "': " + ec.message());
std::format("Unable to check path '{}': {}", candidate.string(), ec.message()));
}
if (!exists)
return candidate;
}
Throw<std::runtime_error>("Unable to generate a unique path under '" + base.string() + "'");
Throw<std::runtime_error>(
std::format("Unable to generate a unique path under '{}'", base.string()));
}
TempDir::TempDir() : path_(uniqueRandomPath(std::filesystem::temp_directory_path()))
+2 -1
View File
@@ -11,6 +11,7 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/LedgerTiming.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/FeeSettingsEntry.h>
#include <xrpl/nodestore/NodeObject.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Fees.h>
@@ -560,7 +561,7 @@ Ledger::setup()
try
{
if (auto const sle = read(keylet::feeSettings()))
if (auto const sle = FeeSettingsEntryR(*this))
{
bool oldFees = false;
bool newFees = false;
+2 -1
View File
@@ -16,6 +16,7 @@
#include <boost/container/pmr/monotonic_buffer_resource.hpp>
#include <cstddef>
#include <format>
#include <memory>
#include <optional>
#include <stdexcept>
@@ -264,7 +265,7 @@ OpenView::rawTxInsert(
auto const result = txs_.emplace(
std::piecewise_construct, std::forward_as_tuple(key), std::forward_as_tuple(txn, metaData));
if (!result.second)
Throw<std::logic_error>("rawTxInsert: duplicate TX id: " + to_string(key));
Throw<std::logic_error>(std::format("rawTxInsert: duplicate TX id: {}", to_string(key)));
}
} // namespace xrpl
+8 -10
View File
@@ -9,6 +9,7 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/LedgerHashesEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/CredentialHelpers.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
@@ -361,17 +362,16 @@ hashOfSeq(ReadView const& ledger, LedgerIndex seq, beast::Journal journal)
if (int const diff = ledger.seq() - seq; diff <= 256)
{
// Within 256...
auto const hashIndex = ledger.read(keylet::skip());
LedgerHashesEntryR const hashIndex(ledger, journal);
if (hashIndex)
{
XRPL_ASSERT(
hashIndex->getFieldU32(sfLastLedgerSequence) == (ledger.seq() - 1),
"xrpl::hashOfSeq : matching ledger sequence");
STVector256 vec = hashIndex->getFieldV256(sfHashes);
if (vec.size() >= diff)
return vec[vec.size() - diff];
if (auto const hash = hashIndex.hashAt(diff - 1))
return hash;
JLOG(journal.warn()) << "Ledger " << ledger.seq() << " missing hash for " << seq << " ("
<< vec.size() << "," << diff << ")";
<< hashIndex->getFieldV256(sfHashes).size() << "," << diff << ")";
}
else
{
@@ -387,16 +387,14 @@ hashOfSeq(ReadView const& ledger, LedgerIndex seq, beast::Journal journal)
}
// in skiplist
auto const hashIndex = ledger.read(keylet::skip(seq));
LedgerHashesEntryR const hashIndex(keylet::skip(seq), ledger, journal);
if (hashIndex)
{
auto const lastSeq = hashIndex->getFieldU32(sfLastLedgerSequence);
XRPL_ASSERT(lastSeq >= seq, "xrpl::hashOfSeq : minimum last ledger");
XRPL_ASSERT((lastSeq & 0xff) == 0, "xrpl::hashOfSeq : valid last ledger");
auto const diff = (lastSeq - seq) >> 8;
STVector256 vec = hashIndex->getFieldV256(sfHashes);
if (vec.size() > diff)
return vec[vec.size() - diff - 1];
if (auto const hash = hashIndex.hashAt((lastSeq - seq) >> 8))
return hash;
}
JLOG(journal.warn()) << "Can't get seq " << seq << " from " << ledger.seq() << " error";
return std::nullopt;
+10 -7
View File
@@ -33,6 +33,7 @@
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <format>
#include <functional>
#include <iterator>
#include <memory>
@@ -394,9 +395,11 @@ removeToken(ApplyView& view, AccountID const& owner, UInt256 const& nftokenID, S
if (!page2)
{
Throw<std::runtime_error>(
"page " + to_string(page1->key()) + " has a broken " + field.getName() +
" field pointing to " + to_string(*id));
Throw<std::runtime_error>(std::format(
"page {} has a broken {} field pointing to {}",
to_string(page1->key()),
field.getName(),
to_string(*id)));
}
}
@@ -597,7 +600,7 @@ removeTokenOffersWithLimit(ApplyView& view, Keylet const& directory, std::size_t
else
{
Throw<std::runtime_error>(
"Offer " + to_string(offerIndexes[i]) + " cannot be deleted!");
std::format("Offer {} cannot be deleted!", to_string(offerIndexes[i])));
}
}
@@ -734,9 +737,9 @@ repairNFTokenDirectoryLinks(ApplyView& view, AccountID const& owner)
if (!newPrev)
{
// LCOV_EXCL_START
Throw<std::runtime_error>(
"NFTokenPage directory for " + to_string(owner) +
" cannot be repaired. Unexpected link problem.");
Throw<std::runtime_error>(std::format(
"NFTokenPage directory for {} cannot be repaired. Unexpected link problem.",
to_string(owner)));
// LCOV_EXCL_STOP
}
newPrev->at(sfNextPageMin) = nextPage->key();
@@ -35,6 +35,7 @@
#include <atomic>
#include <cstddef>
#include <filesystem>
#include <format>
#include <functional>
#include <memory>
#include <stdexcept>
@@ -193,7 +194,7 @@ public:
if (!s.ok())
{
Throw<std::runtime_error>(
std::string("Unable to set RocksDB bbt_options: ") + s.ToString());
std::format("Unable to set RocksDB bbt_options: {}", s.ToString()));
}
}
@@ -206,7 +207,7 @@ public:
if (!s.ok())
{
Throw<std::runtime_error>(
std::string("Unable to set RocksDB options: ") + s.ToString());
std::format("Unable to set RocksDB options: {}", s.ToString()));
}
}
@@ -241,7 +242,7 @@ public:
if (!status.ok() || (localDb == nullptr))
{
Throw<std::runtime_error>(
std::string("Unable to open/create RocksDB: ") + status.ToString());
std::format("Unable to open/create RocksDB: {}", status.ToString()));
}
db.reset(localDb);
}
@@ -316,8 +317,8 @@ public:
}
else
{
status = static_cast<Status>(
static_cast<int>(Status::CustomCode) + unsafeCast<int>(getStatus.code()));
status = checkedCast<Status>(
safeCast<int>(Status::CustomCode) + safeCast<int>(getStatus.code()));
JLOG(journal.error()) << getStatus.ToString();
}
@@ -356,7 +357,7 @@ public:
auto ret = db->Write(options, &wb);
if (!ret.ok())
Throw<std::runtime_error>("storeBatch failed: " + ret.ToString());
Throw<std::runtime_error>(std::format("storeBatch failed: {}", ret.ToString()));
}
void
+114 -61
View File
@@ -8,6 +8,8 @@
#include <xrpl/protocol/digest.h>
#include <xrpl/protocol/tokens.h>
#include <algorithm>
#include <array>
#include <atomic>
#include <cstdint>
#include <cstring>
@@ -19,85 +21,136 @@
namespace xrpl {
namespace detail {
namespace {
/**
* Caches the base58 representations of AccountIDs
/** If true, we implement a cache for account ID lookups.
This is generally a good idea and can improve performance, especially
when serving client requests.
The cache may generate warnings under some sanitizers; if you want to
disable caching, set this variable to false.
*/
class AccountIdCache
constexpr bool cacheAccountIDs = true;
/** Represents a cached entry for the given AccountID.
We keep the size and alignment of the structure at 64 bytes to
make sure it fits within a cache line and avoid false sharing.
*/
struct alignas(64) CachedAccountID
{
private:
struct CachedAccountID
{
AccountID id;
char encoding[40] = {0};
};
/** The sequence lock for the entry.
// The actual cache
std::vector<CachedAccountID> cache_;
An even value means that the data is stable, whereas an odd
value indicates the data is being actively modified.
// We use a hash function designed to resist algorithmic complexity attacks
HardenedHash<> hasher_;
We use a 32-bit value to ensure that the counter will never
return to an observed value during a read window.
*/
std::atomic<std::uint32_t> seq{0};
// 64 spinlocks, packed into a single 64-bit value
std::atomic<std::uint64_t> locks_ = 0;
/** The account id. */
AccountID id{};
public:
AccountIdCache(std::size_t count) : cache_(count)
{
// This is non-binding, but we try to avoid wasting memory that
// is caused by overallocation.
cache_.shrink_to_fit();
}
/** The length of the cached encoding.
std::string
toBase58(AccountID const& id)
{
auto const index = hasher_(id) % cache_.size();
This field disambiguates collisions between ACCOUNT_ZERO and
never-written-to slots, since they would otherwise share the
same encoded bits.
*/
std::uint8_t len = 0;
PackedSpinlock sl(locks_, index % 64);
{
std::scoped_lock const lock(sl);
// The check against the first character of the encoding ensures
// that we don't mishandle the case of the all-zero account:
if (cache_[index].encoding[0] != 0 && cache_[index].id == id)
return cache_[index].encoding;
}
auto ret = encodeBase58Token(TokenType::AccountID, id.data(), id.size());
XRPL_ASSERT(ret.size() <= 38, "xrpl::detail::AccountIdCache : maximum result size");
{
std::scoped_lock const lock(sl);
cache_[index].id = id;
std::strcpy(cache_[index].encoding, ret.c_str());
}
return ret;
}
/** The actual string encoding. */
char encoding[39] = {0};
};
} // namespace detail
static_assert(sizeof(CachedAccountID) == 64 && alignof(CachedAccountID) == 64);
static std::unique_ptr<detail::AccountIdCache> gAccountIdCache;
// The actual cache to use: a simple, direct-mapped, best-effort construct.
//
// We do not use a hardened hash for this because an AccountID is already
// just cryptographically generated (i.e. uniformly distributed) bits and
// there is no amplification, since collisions do not carry an additional
// cost.
//
// The size is a "nice" power of 2 because it makes the calculation of an
// index easier.
constinit std::array<CachedAccountID, 65536> gAccountIdCache{};
void
initAccountIdCache(std::size_t count)
{
if (!gAccountIdCache && count != 0)
gAccountIdCache = std::make_unique<detail::AccountIdCache>(count);
}
} // namespace
std::string
toBase58(AccountID const& v)
{
if (gAccountIdCache)
return gAccountIdCache->toBase58(v);
// Since the input bits are generated by a cryptographically secure
// hash construct, we can just pick any subset of them, no muss, no
// fuss.
auto const index = [&v]() -> std::uint16_t {
auto const data = v.data();
return encodeBase58Token(TokenType::AccountID, v.data(), v.size());
return static_cast<std::uint16_t>(data[0]) | (static_cast<std::uint16_t>(data[1]) << 8);
}();
if constexpr (cacheAccountIDs)
{
auto& slot = gAccountIdCache[index];
// This is the optimistic, store-free read path: a collision with a
// concurrent writer will be detected because the seq recheck fails
// and we fall through to the slow path.
//
// These unsynchronized reads of the slot fields are, formally, data
// races and, technically speaking, result in undefined behavior and
// a tool like TSAN might flag them. These should not have an impact
// on the correctness of the returned data: the sequence lock checks
// ensure we discard any data that was in flux.
if (auto const s0 = slot.seq.load(std::memory_order::acquire);
(s0 & 1) == 0 && slot.len != 0 && slot.id == v)
{
char buf[38];
auto const len = std::min<std::size_t>(slot.len, sizeof(buf));
std::memcpy(buf, slot.encoding, len);
// Prevents the loads above from sinking below the recheck; the
// acquire on s0 constrains later operations, not earlier ones.
std::atomic_thread_fence(std::memory_order::acquire);
// Everything read from the slot — the gates above and the bytes in
// buf — precedes this recheck and is validated by it. No slot access
// may follow it.
if (slot.seq.load(std::memory_order::relaxed) == s0)
return std::string(buf, len);
}
}
auto ret = encodeBase58Token(TokenType::AccountID, v.data(), v.size());
XRPL_ASSERT(!ret.empty() && ret.size() <= 38, "xrpl::toBase58 : maximum result size");
if constexpr (cacheAccountIDs)
{
auto& slot = gAccountIdCache[index];
// The CAS to an odd value excludes other writers; if it fails, another
// thread wrote or is writing into this slot. That is not a problem: we
// just don't store our result into the cache this time.
if (auto s0 = slot.seq.load(std::memory_order::relaxed); (s0 & 1) == 0 &&
slot.seq.compare_exchange_strong(s0, s0 + 1, std::memory_order::acquire))
{
slot.id = v;
slot.len = static_cast<std::uint8_t>(ret.size());
std::copy_n(ret.data(), ret.size(), slot.encoding);
// The acquire on the CAS above keeps the writes from becoming visible
// before the seqlock transitions to an odd value, which means readers
// can detect an in-progress write. This release publishes them before
// the seqlock transitions to an even value.
slot.seq.store(s0 + 2, std::memory_order::release);
}
}
return ret;
}
template <>
+3 -2
View File
@@ -14,6 +14,7 @@
#include <algorithm>
#include <cstdint>
#include <format>
#include <functional>
#include <limits>
#include <optional>
@@ -105,8 +106,8 @@ Permission::Permission()
if (type <= UINT16_MAX)
{
// LCOV_EXCL_START
Throw<std::logic_error>(
"Granular permission value must exceed the maximum uint16_t value: " + name);
Throw<std::logic_error>(std::format(
"Granular permission value must exceed the maximum uint16_t value: {}", name));
// LCOV_EXCL_STOP
}
}
+5 -3
View File
@@ -43,8 +43,9 @@ setCurrentTransactionRules(std::optional<Rules> r)
auto const range = [&r]() {
// If any new conditions with new amendments are added to "enableLargeNumbers", those
// amendments must also be added to useRulesGuards.
bool const enableLargeNumbers =
!r || (r->enabled(featureSingleAssetVault) || r->enabled(featureLendingProtocol));
bool const enableLargeNumbers = !r ||
(r->enabled(featureSingleAssetVault) || r->enabled(featureLendingProtocol) ||
r->enabled(featureMPTokensV2));
// If enableLargeNumbers is true, then useRulesGuards must also return true.
// However, the reverse is not true. Other amendments can cause the rules guard to be used,
// even though large numbers are _not_ used.
@@ -84,7 +85,8 @@ useRulesGuards(Rules const& rules)
// with createGuards, and any other callers, and the first set of guards can be created directly
// at the call site, without using optional.
return rules.enabled(featureSingleAssetVault) || rules.enabled(featureLendingProtocol) ||
rules.enabled(fixCleanup3_2_0) || rules.enabled(fixCleanup3_3_0);
rules.enabled(fixCleanup3_2_0) || rules.enabled(fixCleanup3_3_0) ||
rules.enabled(featureMPTokensV2);
}
void
+4 -5
View File
@@ -37,6 +37,7 @@
#include <cstddef>
#include <cstdint>
#include <exception>
#include <format>
#include <iterator>
#include <limits>
#include <memory>
@@ -1193,8 +1194,7 @@ muldiv(std::uint64_t multiplier, std::uint64_t multiplicand, std::uint64_t divis
if (ret > std::numeric_limits<std::uint64_t>::max())
{
Throw<std::overflow_error>(
"overflow: (" + std::to_string(multiplier) + " * " + std::to_string(multiplicand) +
") / " + std::to_string(divisor));
std::format("overflow: ({} * {}) / {}", multiplier, multiplicand, divisor));
}
return static_cast<uint64_t>(ret);
@@ -1215,9 +1215,8 @@ muldivRound(
if (ret > std::numeric_limits<std::uint64_t>::max())
{
Throw<std::overflow_error>(
"overflow: ((" + std::to_string(multiplier) + " * " + std::to_string(multiplicand) +
") + " + std::to_string(rounding) + ") / " + std::to_string(divisor));
Throw<std::overflow_error>(std::format(
"overflow: (({} * {}) + {}) / {}", multiplier, multiplicand, rounding, divisor));
}
return static_cast<uint64_t>(ret);
+2 -3
View File
@@ -35,9 +35,8 @@ STLedgerEntry::STLedgerEntry(Keylet const& k) : STObject(sfLedgerEntry), key_(k.
if (format == nullptr)
{
Throw<std::runtime_error>(
"Attempt to create a SLE of unknown type " +
std::to_string(safeCast<std::uint16_t>(k.type)));
Throw<std::runtime_error>(std::format(
"Attempt to create a SLE of unknown type {}", safeCast<std::uint16_t>(k.type)));
}
set(format->getSOTemplate());
+2 -1
View File
@@ -19,6 +19,7 @@
#include <cstddef>
#include <cstdint>
#include <format>
#include <limits>
#include <ostream>
#include <stdexcept>
@@ -170,7 +171,7 @@ partsFromString(std::string const& number)
boost::smatch match;
if (!boost::regex_match(number, match, kReNumber))
Throw<std::runtime_error>("'" + number + "' is not a number");
Throw<std::runtime_error>(std::format("'{}' is not a number", number));
// Match fields:
// 0 = whole input
+75 -47
View File
@@ -5,6 +5,7 @@
#include <xrpl/basics/UnorderedContainers.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/contract.h>
#include <xrpl/basics/safe_cast.h>
#include <xrpl/beast/hash/uhash.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/json/json_value.h>
@@ -17,9 +18,13 @@
#include <xrpl/protocol/jss.h>
#include <algorithm>
#include <concepts>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <optional>
#include <stdexcept>
#include <type_traits>
#include <utility>
#include <vector>
@@ -36,10 +41,10 @@ STPathElement::getHash(STPathElement const& element)
// 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.
// The note above is only true because the result of this function is only
// used by the STPathElement equality operator as a fast-reject prefilter,
// ahead of the field comparisons that then decide the answer. Do not use
// it to key a container.
for (auto const x : element.getAccountID())
hashAccount += (hashAccount * 257) ^ x;
@@ -106,12 +111,38 @@ STPathSet::operator=(STPathSet const& other)
STPathSet::STPathSet(SerialIter& sit, SField const& name) : STBase(name)
{
std::vector<STPathElement> path;
for (;;)
{
int const iType = sit.get8();
using enum STPathElement::Type;
if (iType == STPathElement::TypeNone || iType == STPathElement::TypeBoundary)
auto parsePathElementType = [](std::underlying_type_t<STPathElement::Type> byte) noexcept
-> std::optional<STPathElement::Type> {
if (byte == std::to_underlying(TypeBoundary))
return TypeBoundary;
constexpr auto knownBits = static_cast<std::underlying_type_t<STPathElement::Type>>(
std::to_underlying(TypeAccount) | std::to_underlying(TypeCurrency) |
std::to_underlying(TypeIssuer) | std::to_underlying(TypeMpt));
if ((byte & ~knownBits) != 0)
return std::nullopt;
return static_cast<STPathElement::Type>(byte);
};
std::vector<STPathElement> path;
std::optional<STPathElement::Type> type;
do
{
type = parsePathElementType(sit.get8());
if (!type)
{
JLOG(debugLog().error()) << "Bad path element in pathset";
Throw<std::runtime_error>("bad path element");
}
if (type == TypeNone || type == TypeBoundary)
{
if (path.empty())
{
@@ -122,28 +153,11 @@ STPathSet::STPathSet(SerialIter& sit, SField const& name) : STBase(name)
// Move rather than converting the vector to an STPath by copy.
value_.emplace_back(std::move(path));
path.clear();
if (iType == STPathElement::TypeNone)
return;
}
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 (hasAccount)
account = sit.get160();
bool const hasCurrency = ((*type & TypeCurrency) == TypeCurrency);
bool const hasMPT = ((*type & TypeMpt) == TypeMpt);
if (hasCurrency && hasMPT)
{
@@ -151,18 +165,26 @@ STPathSet::STPathSet(SerialIter& sit, SField const& name) : STBase(name)
Throw<std::runtime_error>("bad path element: MPT and Currency");
}
AccountID account;
AccountID issuer;
if ((*type & TypeAccount) == TypeAccount)
account = sit.get160();
PathAsset asset;
if (hasCurrency)
asset = Currency::fromRaw(sit.get160());
if (hasMPT)
asset = sit.get192();
if (hasIssuer)
if ((*type & TypeIssuer) == TypeIssuer)
issuer = sit.get160();
path.emplace_back(account, asset, issuer, hasCurrency || hasMPT);
}
}
} while (type != TypeNone);
}
STBase*
@@ -216,24 +238,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(STPathElement::TypeAccount))
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);
@@ -265,34 +285,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<STPathElement::Type> auto t) noexcept {
static_assert(
!requires(Serializer& ser) { ser.add8(t); },
"Serializer::add8 now accepts STPathElement::Type directly; remove toByte.");
return std::to_underlying(t);
};
for (auto const& spPath : value_)
{
if (!first)
s.add8(STPathElement::TypeBoundary);
s.add8(toByte(STPathElement::TypeBoundary));
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(STPathElement::TypeAccount))
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(STPathElement::TypeNone));
}
} // namespace xrpl
+2 -3
View File
@@ -59,9 +59,8 @@ getTxFormat(TxType type)
if (format == nullptr)
{
Throw<std::runtime_error>(
"Invalid transaction type " +
std::to_string(safeCast<std::underlying_type_t<TxType>>(type)));
Throw<std::runtime_error>(std::format(
"Invalid transaction type {}", safeCast<std::underlying_type_t<TxType>>(type)));
}
return format;
+2 -2
View File
@@ -9,8 +9,8 @@
#include <xrpl/protocol/Serializer.h>
#include <cstddef>
#include <format>
#include <stdexcept>
#include <string>
#include <utility>
namespace xrpl {
@@ -22,7 +22,7 @@ STVector256::STVector256(SerialIter& sit, SField const& name) : STBase(name)
if (slice.size() % UInt256::size() != 0)
{
Throw<std::runtime_error>(
"Bad serialization for STVector256: " + std::to_string(slice.size()));
std::format("Bad serialization for STVector256: {}", slice.size()));
}
auto const cnt = slice.size() / UInt256::size();
+2 -1
View File
@@ -70,7 +70,8 @@ STXChainBridge::STXChainBridge(SField const& name, json::Value const& v) : STBas
std::string const name = it.memberName();
if (!kBridgeJson.isMember(name))
{
Throw<std::runtime_error>("STXChainBridge extra field detected: " + name);
Throw<std::runtime_error>(
std::format("STXChainBridge extra field detected: {}", name));
}
}
return true;
+3 -3
View File
@@ -15,8 +15,8 @@
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <format>
#include <stdexcept>
#include <string>
#include <type_traits>
namespace xrpl {
@@ -456,7 +456,7 @@ SerialIter::getFieldID(int& type, int& name)
// uncommon type
type = get8();
if (type < 16)
Throw<std::runtime_error>("gFID: uncommon type out of range " + std::to_string(type));
Throw<std::runtime_error>(std::format("gFID: uncommon type out of range {}", type));
}
if (name == 0)
@@ -464,7 +464,7 @@ SerialIter::getFieldID(int& type, int& name)
// uncommon name
name = get8();
if (name < 16)
Throw<std::runtime_error>("gFID: uncommon name out of range " + std::to_string(name));
Throw<std::runtime_error>(std::format("gFID: uncommon name out of range {}", name));
}
}
+5 -4
View File
@@ -27,6 +27,7 @@
#include <soci/sqlite3/soci-sqlite3.h> // IWYU pragma: keep
#include <format>
#include <memory>
namespace xrpl {
@@ -40,8 +41,8 @@ getSociSqliteInit(std::string const& name, std::string const& dir, std::string c
{
if (name.empty())
{
Throw<std::runtime_error>(
"Sqlite databases must specify a dir and a name. Name: " + name + " Dir: " + dir);
Throw<std::runtime_error>(std::format(
"Sqlite databases must specify a dir and a name. Name: {} Dir: {}", name, dir));
}
std::filesystem::path file(dir);
if (std::filesystem::is_directory(file))
@@ -56,7 +57,7 @@ getSociInit(BasicConfig const& config, std::string const& dbName)
auto const backendName = get(section, Keys::kBackend, "sqlite");
if (backendName != "sqlite")
Throw<std::runtime_error>("Unsupported soci backend: " + backendName);
Throw<std::runtime_error>(std::format("Unsupported soci backend: {}", backendName));
auto const path = config.legacy(Sections::kDatabasePath);
auto const ext = dbName == "validators" || dbName == "peerfinder" ? ".sqlite" : ".db";
@@ -101,7 +102,7 @@ open(soci::session& s, std::string const& beName, std::string const& connectionS
}
else
{
Throw<std::runtime_error>("Unsupported soci backend: " + beName);
Throw<std::runtime_error>(std::format("Unsupported soci backend: {}", beName));
}
}
+2 -1
View File
@@ -26,6 +26,7 @@
#include <cstddef>
#include <cstdint>
#include <exception>
#include <format>
#include <functional>
#include <limits>
#include <mutex>
@@ -48,7 +49,7 @@ to_string(Manifest const& m)
return "Revocation Manifest " + mk;
if (!m.signingKey)
Throw<std::runtime_error>("No SigningKey in manifest " + mk);
Throw<std::runtime_error>(std::format("No SigningKey in manifest {}", mk));
return "Manifest " + mk + " (" + std::to_string(m.sequence) + ": " +
toBase58(TokenType::NodePublic, *m.signingKey) + ")";
+3 -2
View File
@@ -8,6 +8,7 @@
#include <algorithm>
#include <cstddef>
#include <format>
#include <optional>
#include <stdexcept>
#include <string>
@@ -107,10 +108,10 @@ SHAMapNodeID::getChildNodeID(unsigned int branch) const
depth_ <= SHAMap::kLeafDepth, "xrpl::SHAMapNodeID::getChildNodeID : maximum leaf depth");
if (depth_ >= SHAMap::kLeafDepth)
Throw<std::logic_error>("Request for child node ID of " + to_string(*this));
Throw<std::logic_error>(std::format("Request for child node ID of {}", to_string(*this)));
if (!isPrefixOf(id_))
Throw<std::logic_error>("Incorrect mask for " + to_string(*this));
Throw<std::logic_error>(std::format("Incorrect mask for {}", to_string(*this)));
SHAMapNodeID node{depth_ + 1, id_};
node.id_.begin()[depth_ / 2] |= ((depth_ & 1) != 0u) ? branch : (branch << 4);
+21 -24
View File
@@ -18,6 +18,7 @@
#include <xrpl/shamap/SHAMapTxPlusMetaLeafNode.h>
#include <cstdint>
#include <format>
#include <stdexcept>
#include <string>
#include <type_traits>
@@ -30,9 +31,8 @@ SHAMapTreeNode::makeTransaction(Slice data, SHAMapHash const& hash, bool hashVal
{
if (data.size() < kMinShaMapItemBytes)
{
Throw<std::runtime_error>(
"Short TXN node: " + std::to_string(data.size()) + " bytes (minimum " +
std::to_string(kMinShaMapItemBytes) + " required)");
Throw<std::runtime_error>(std::format(
"Short TXN node: {} bytes (minimum {} required)", data.size(), kMinShaMapItemBytes));
}
auto item = makeShamapitem(sha512Half(HashPrefix::TransactionId, data), data);
@@ -52,27 +52,25 @@ SHAMapTreeNode::makeTransactionWithMeta(Slice data, SHAMapHash const& hash, bool
if (s.size() < tag.kBytes)
{
Throw<std::runtime_error>(
"Short TXN+MD node: " + std::to_string(s.size()) + " bytes (minimum " +
std::to_string(tag.kBytes) + " required for tag)");
Throw<std::runtime_error>(std::format(
"Short TXN+MD node: {} bytes (minimum {} required for tag)", s.size(), tag.kBytes));
}
// FIXME: improve this interface so that the above check isn't needed
if (!s.getBitString(tag, s.size() - tag.kBytes))
{
Throw<std::out_of_range>(
"Short TXN+MD node: failed to read tag at offset " +
std::to_string(s.size() - tag.kBytes));
Throw<std::out_of_range>(std::format(
"Short TXN+MD node: failed to read tag at offset {}", s.size() - tag.kBytes));
}
s.chop(tag.kBytes);
if (s.size() < kMinShaMapItemBytes)
{
Throw<std::runtime_error>(
"Short TXN+MD node: " + std::to_string(s.size()) +
" bytes after tag removal (minimum " + std::to_string(kMinShaMapItemBytes) +
" required)");
Throw<std::runtime_error>(std::format(
"Short TXN+MD node: {} bytes after tag removal (minimum {} required)",
s.size(),
kMinShaMapItemBytes));
}
auto item = makeShamapitem(tag, s.slice());
@@ -92,16 +90,15 @@ SHAMapTreeNode::makeAccountState(Slice data, SHAMapHash const& hash, bool hashVa
if (s.size() < tag.kBytes)
{
Throw<std::runtime_error>(
"Short AS node: " + std::to_string(s.size()) + " bytes (minimum " +
std::to_string(tag.kBytes) + " required for tag)");
Throw<std::runtime_error>(std::format(
"Short AS node: {} bytes (minimum {} required for tag)", s.size(), tag.kBytes));
}
// FIXME: improve this interface so that the above check isn't needed
if (!s.getBitString(tag, s.size() - tag.kBytes))
{
Throw<std::out_of_range>(
"Short AS node: failed to read tag at offset " + std::to_string(s.size() - tag.kBytes));
std::format("Short AS node: failed to read tag at offset {}", s.size() - tag.kBytes));
}
s.chop(tag.kBytes);
@@ -111,9 +108,10 @@ SHAMapTreeNode::makeAccountState(Slice data, SHAMapHash const& hash, bool hashVa
if (s.size() < kMinShaMapItemBytes)
{
Throw<std::runtime_error>(
"Short AS node: " + std::to_string(s.size()) + " bytes after tag removal (minimum " +
std::to_string(kMinShaMapItemBytes) + " required)");
Throw<std::runtime_error>(std::format(
"Short AS node: {} bytes after tag removal (minimum {} required)",
s.size(),
kMinShaMapItemBytes));
}
auto item = makeShamapitem(tag, s.slice());
@@ -152,7 +150,7 @@ SHAMapTreeNode::makeFromWire(Slice rawNode)
if (type == kWireTypeTransactionWithMeta)
return makeTransactionWithMeta(rawNode, hash, hashValid);
Throw<std::runtime_error>("wire: Unknown type (" + std::to_string(type) + ")");
Throw<std::runtime_error>(std::format("wire: Unknown type ({})", type));
}
SHAMapTreeNodePtr
@@ -183,9 +181,8 @@ SHAMapTreeNode::makeFromPrefix(Slice rawNode, SHAMapHash const& hash)
if (type == HashPrefix::TxNode)
return makeTransactionWithMeta(rawNode, hash, hashValid);
Throw<std::runtime_error>(
"prefix: unknown type (" +
std::to_string(safeCast<std::underlying_type_t<HashPrefix>>(type)) + ")");
Throw<std::runtime_error>(std::format(
"prefix: unknown type ({})", safeCast<std::underlying_type_t<HashPrefix>>(type)));
}
std::string
+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
@@ -134,7 +134,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;
@@ -951,7 +952,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
@@ -1122,7 +1124,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 =
@@ -1604,7 +1606,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
@@ -1684,7 +1686,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 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");
+4 -2
View File
@@ -346,7 +346,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 +378,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.";
+56 -14
View File
@@ -41,6 +41,7 @@ constexpr auto kConfidentialMptTxTypes = std::to_array<TxType>({
ttCONFIDENTIAL_MPT_MERGE_INBOX,
ttCONFIDENTIAL_MPT_CLAWBACK,
ttCONFIDENTIAL_MPT_MIRROR_UPDATE,
ttCONFIDENTIAL_MPT_HOLDER_KEY_UPDATE,
});
// Clamp to the cap (== INT64_MAX) before the signed conversion. Invariant
@@ -73,7 +74,7 @@ ValidMPTIssuance::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef
// only meaningful post-fixCleanup3_2_0 (the field is never set
// pre-amendment, and the holding-deletion rule does not apply).
// Skip both blocks when the amendment is off so we avoid wasted work
// on the hot path.
// on the hot path, except where noted for fixCleanup3_5_0 below.
bool const fix320Enabled = isFeatureEnabled(fixCleanup3_2_0);
if (after && after->getType() == ltMPTOKEN_ISSUANCE)
@@ -108,7 +109,10 @@ ValidMPTIssuance::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef
if (isDelete)
{
mptokensDeleted_++;
if (fix320Enabled)
// deletedHoldings_ also feeds finalize()'s erase-time public
// balance check, gated on fixCleanup3_5_0 independently of
// fixCleanup3_2_0.
if (fix320Enabled || isFeatureEnabled(fixCleanup3_5_0))
deletedHoldings_.push_back(after);
}
else if (!before)
@@ -195,6 +199,19 @@ ValidMPTIssuance::finalize(
return false;
}
// Deleting an MPToken with a non-zero MPTAmount is rejected.
if (rules.enabled(fixCleanup3_5_0))
{
for (auto const& sleHolding : deletedHoldings_)
{
if (sleHolding->getType() == ltMPTOKEN && sleHolding->getFieldU64(sfMPTAmount) > 0)
{
JLOG(j.fatal()) << "Invariant failed: MPToken deleted with non-zero balance";
return false;
}
}
}
if (isTesSuccess(result) || (mptV2Enabled && result == tecINCOMPLETE))
{
[[maybe_unused]]
@@ -579,16 +596,27 @@ ValidConfidentialMPToken::visitEntry(
change.mptAmountDelta =
subtractMPTAmountDelta(change.mptAmountDelta, before->getFieldU64(sfMPTAmount));
// Cannot delete MPToken with non-zero confidential state or non-zero public amount
// Cannot delete MPToken with non-zero confidential state.
if (isDelete)
{
bool const hasPublicBalance = before->getFieldU64(sfMPTAmount) > 0;
bool const hasEncryptedFields = before->isFieldPresent(sfConfidentialBalanceSpending) ||
// changes_ is keyed by issuance, so sibling holders erased by the
// same transaction share this entry. Only ever set these flags,
// never clear them, or an empty sibling visited later would mask
// a funded MPToken.
// Retired by fixCleanup3_5_0, which moved the public balance
// check to ValidMPTIssuance::finalize. Kept pre-amendment for
// consensus safety: a non-zero public balance used to feed the
// confidential gate below, rejecting the erase whenever the
// issuance's COA was non-zero, and already-validated ledgers
// depend on that.
if (!isFeatureEnabled(fixCleanup3_5_0) && before->getFieldU64(sfMPTAmount) > 0)
changes_[id].deletedWithBalanceBefore = true;
if (before->isFieldPresent(sfConfidentialBalanceSpending) ||
before->isFieldPresent(sfConfidentialBalanceInbox) ||
before->isFieldPresent(sfIssuerEncryptedBalance) ||
before->isFieldPresent(sfAuditorEncryptedBalance);
if (hasPublicBalance || hasEncryptedFields)
before->isFieldPresent(sfAuditorEncryptedBalance))
changes_[id].deletedWithEncrypted = true;
}
}
@@ -694,6 +722,8 @@ ValidConfidentialMPToken::finalize(
if (result != tesSUCCESS)
return true;
bool const fix350Enabled = view.rules().enabled(fixCleanup3_5_0);
for (auto const& [id, checks] : changes_)
{
// Find the MPTokenIssuance
@@ -708,8 +738,20 @@ ValidConfidentialMPToken::finalize(
if (!issuance)
continue;
// Cannot delete MPToken with non-zero confidential state
if (checks.deletedWithEncrypted)
// Cannot delete MPToken with non-zero confidential state.
//
// Before fixCleanup3_5_0 this gate also absorbed the pre-transaction
// public balance, so any drain-then-erase of an MPToken -- an
// AMMWithdraw of the whole pool, a LoanBrokerDelete returning cover --
// was rejected whenever some unrelated holder of the same issuance
// held a confidential balance. The COA gate itself is correct for
// ciphertext and mirrors MPTokenAuthorize::preclaim; only the public
// balance leg was misplaced.
bool const deletedWithEncrypted = fix350Enabled
? checks.deletedWithEncrypted
: (checks.deletedWithEncrypted || checks.deletedWithBalanceBefore);
if (deletedWithEncrypted)
{
if ((*issuance)[~sfConfidentialOutstandingAmount].value_or(0) > 0)
{
@@ -784,10 +826,10 @@ ValidConfidentialMPToken::finalize(
return false;
}
// Among confidential MPT transactions, only ConfidentialMPTSend and
// ConfidentialMPTMergeInbox leave coaDelta unmodified. Therefore, if a confidential MPT
// transaction reaches here, it must be one of these two types, neither of which will
// modify sfOutstandingAmount
// Reaching here means this confidential MPT transaction left coaDelta
// unmodified (e.g. ConfidentialMPTSend, ConfidentialMPTMergeInbox, or
// ConfidentialMPTHolderKeyUpdate/ConfidentialMPTMirrorUpdate, none of which touch
// sfConfidentialOutstandingAmount), so it must not modify sfOutstandingAmount either.
if (checks.outstandingDelta != 0)
{
JLOG(j.fatal()) << "Invariant failed: OutstandingAmount changed "
+1 -1
View File
@@ -985,7 +985,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
+13 -27
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() == STPathElement::TypeAccount && 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() & ~STPathElement::TypeAll) != STPathElement::TypeNone || 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(STPathElement::TypeAccount);
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 hasAsset = [](STPathElement const pe) {
return pe.getNodeType() & STPathElement::TypeAsset;
};
auto hasAsset = [](STPathElement const& pe) { return pe.hasAsset(); };
std::vector<STPathElement> normPath;
// reserve enough for the path, the implied source, destination,
@@ -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};
}
}
@@ -295,7 +295,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
@@ -318,7 +319,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};
}
}
@@ -326,7 +328,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
@@ -342,7 +345,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};
}
}
@@ -3,6 +3,7 @@
#include <xrpl/basics/Log.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/entries/EscrowEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/EscrowHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
@@ -94,13 +95,12 @@ EscrowCancel::preclaim(PreclaimContext const& ctx)
if (ctx.view.rules().enabled(featureTokenEscrow))
{
auto const seqProxy = SeqProxy::rawSequence(ctx.tx[sfOfferSequence]);
auto const k = keylet::escrow(ctx.tx[sfOwner], seqProxy);
auto const slep = ctx.view.read(k);
if (!slep)
EscrowEntryR const escrow(ctx.tx[sfOwner], seqProxy, ctx.view);
if (!escrow)
return tecNO_TARGET;
AccountID const account = (*slep)[sfAccount];
STAmount const amount = (*slep)[sfAmount];
AccountID const account = (*escrow)[sfAccount];
STAmount const amount = (*escrow)[sfAmount];
if (!isXRP(amount))
{
@@ -120,9 +120,8 @@ TER
EscrowCancel::doApply()
{
auto const seqProxy = SeqProxy::rawSequence(ctx_.tx[sfOfferSequence]);
auto const k = keylet::escrow(ctx_.tx[sfOwner], seqProxy);
auto const slep = ctx_.view().peek(k);
if (!slep)
EscrowEntryW escrow(ctx_.tx[sfOwner], seqProxy, ctx_.view(), j_);
if (!escrow)
{
if (ctx_.view().rules().enabled(featureTokenEscrow))
return tecINTERNAL; // LCOV_EXCL_LINE
@@ -133,19 +132,19 @@ EscrowCancel::doApply()
auto const now = ctx_.view().header().parentCloseTime;
// No cancel time specified: can't execute at all.
if (!(*slep)[~sfCancelAfter])
if (!(*escrow)[~sfCancelAfter])
return tecNO_PERMISSION;
// Too soon: can't execute before the cancel time.
if (!after(now, (*slep)[sfCancelAfter]))
if (!after(now, (*escrow)[sfCancelAfter]))
return tecNO_PERMISSION;
AccountID const account = (*slep)[sfAccount];
AccountID const account = (*escrow)[sfAccount];
// Remove escrow from owner directory
{
auto const page = (*slep)[sfOwnerNode];
if (!ctx_.view().dirRemove(keylet::ownerDir(account), page, k.key, true))
auto const page = (*escrow)[sfOwnerNode];
if (!ctx_.view().dirRemove(keylet::ownerDir(account), page, escrow.key(), true))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete Escrow from owner.";
@@ -155,9 +154,10 @@ EscrowCancel::doApply()
}
// Remove escrow from recipient's owner directory, if present.
if (auto const optPage = (*slep)[~sfDestinationNode]; optPage)
if (auto const optPage = (*escrow)[~sfDestinationNode]; optPage)
{
if (!ctx_.view().dirRemove(keylet::ownerDir((*slep)[sfDestination]), *optPage, k.key, true))
if (!ctx_.view().dirRemove(
keylet::ownerDir((*escrow)[sfDestination]), *optPage, escrow.key(), true))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete Escrow from recipient.";
@@ -167,13 +167,13 @@ EscrowCancel::doApply()
}
auto const sle = ctx_.view().peek(keylet::account(account));
STAmount const amount = slep->getFieldAmount(sfAmount);
STAmount const amount = escrow->getFieldAmount(sfAmount);
// 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, slep, 1, ctx_.journal);
decreaseOwnerCountForObject(ctx_.view(), sle, escrow.mutableRawSle(), 1, ctx_.journal);
// Transfer amount back to the owner
if (isXRP(amount))
@@ -192,7 +192,7 @@ EscrowCancel::doApply()
return escrowUnlockApplyHelper<T>(
ctx_.getApplyViewContext(),
kParityRate,
ctx_.view().rules().enabled(fixCleanup3_2_0) ? sle : slep,
ctx_.view().rules().enabled(fixCleanup3_2_0) ? sle : escrow.mutableRawSle(),
preFeeBalance_,
amount,
issuer,
@@ -206,9 +206,9 @@ EscrowCancel::doApply()
return ret; // LCOV_EXCL_LINE
// Remove escrow from issuers owner directory, if present.
if (auto const optPage = (*slep)[~sfIssuerNode]; optPage)
if (auto const optPage = (*escrow)[~sfIssuerNode]; optPage)
{
if (!ctx_.view().dirRemove(keylet::ownerDir(issuer), *optPage, k.key, true))
if (!ctx_.view().dirRemove(keylet::ownerDir(issuer), *optPage, escrow.key(), true))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete Escrow from recipient.";
@@ -219,10 +219,10 @@ EscrowCancel::doApply()
}
if (!recycleReserve)
decreaseOwnerCountForObject(ctx_.view(), sle, slep, 1, ctx_.journal);
decreaseOwnerCountForObject(ctx_.view(), sle, escrow.mutableRawSle(), 1, ctx_.journal);
// Remove escrow from ledger
ctx_.view().erase(slep);
escrow.erase();
return tesSUCCESS;
}
@@ -7,6 +7,7 @@
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/entries/EscrowEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
@@ -481,30 +482,31 @@ EscrowCreate::doApply()
// Create escrow in ledger. Note that we use the value from the
// sequence or ticket. For more explanation see comments in SeqProxy.h.
Keylet const escrowKeylet = keylet::escrow(accountID_, ctx_.tx.getSeqProxy());
auto const slep = std::make_shared<SLE>(escrowKeylet);
(*slep)[sfAmount] = amount;
(*slep)[sfAccount] = accountID_;
(*slep)[~sfCondition] = ctx_.tx[~sfCondition];
(*slep)[~sfSourceTag] = ctx_.tx[~sfSourceTag];
(*slep)[sfDestination] = ctx_.tx[sfDestination];
(*slep)[~sfCancelAfter] = ctx_.tx[~sfCancelAfter];
(*slep)[~sfFinishAfter] = ctx_.tx[~sfFinishAfter];
(*slep)[~sfDestinationTag] = ctx_.tx[~sfDestinationTag];
EscrowEntryW escrow(accountID_, ctx_.tx.getSeqProxy(), ctx_.view(), j_);
Keylet const escrowKeylet = escrow.keylet();
escrow.newSLE();
(*escrow)[sfAmount] = amount;
(*escrow)[sfAccount] = accountID_;
(*escrow)[~sfCondition] = ctx_.tx[~sfCondition];
(*escrow)[~sfSourceTag] = ctx_.tx[~sfSourceTag];
(*escrow)[sfDestination] = ctx_.tx[sfDestination];
(*escrow)[~sfCancelAfter] = ctx_.tx[~sfCancelAfter];
(*escrow)[~sfFinishAfter] = ctx_.tx[~sfFinishAfter];
(*escrow)[~sfDestinationTag] = ctx_.tx[~sfDestinationTag];
if (ctx_.view().rules().enabled(fixIncludeKeyletFields))
{
(*slep)[sfSequence] = ctx_.tx.getSeqProxy().value();
(*escrow)[sfSequence] = ctx_.tx.getSeqProxy().value();
}
if (ctx_.view().rules().enabled(featureTokenEscrow) && !isXRP(amount))
{
auto const xferRate = transferRate(ctx_.view(), amount);
if (xferRate != kParityRate)
(*slep)[sfTransferRate] = xferRate.value;
(*escrow)[sfTransferRate] = xferRate.value;
}
ctx_.view().insert(slep);
escrow.insert();
// Add escrow to sender's owner directory
{
@@ -512,7 +514,7 @@ EscrowCreate::doApply()
keylet::ownerDir(accountID_), escrowKeylet, describeOwnerDir(accountID_));
if (!page)
return tecDIR_FULL; // LCOV_EXCL_LINE
(*slep)[sfOwnerNode] = *page;
(*escrow)[sfOwnerNode] = *page;
}
// If it's not a self-send, add escrow to recipient's owner directory.
@@ -523,7 +525,7 @@ EscrowCreate::doApply()
ctx_.view().dirInsert(keylet::ownerDir(dest), escrowKeylet, describeOwnerDir(dest));
if (!page)
return tecDIR_FULL; // LCOV_EXCL_LINE
(*slep)[sfDestinationNode] = *page;
(*escrow)[sfDestinationNode] = *page;
}
// IOU escrow objects are added to the issuer's owner directory to help
@@ -536,7 +538,7 @@ EscrowCreate::doApply()
ctx_.view().dirInsert(keylet::ownerDir(issuer), escrowKeylet, describeOwnerDir(issuer));
if (!page)
return tecDIR_FULL; // LCOV_EXCL_LINE
(*slep)[sfIssuerNode] = *page;
(*escrow)[sfIssuerNode] = *page;
}
// Deduct owner's balance
@@ -559,7 +561,7 @@ EscrowCreate::doApply()
// increment owner count
increaseOwnerCount(ctx_.getApplyViewContext(), sle, 1, ctx_.journal);
addSponsorToLedgerEntry(ctx_.getApplyViewContext(), slep);
addSponsorToLedgerEntry(ctx_.getApplyViewContext(), escrow.mutableRawSle());
ctx_.view().update(sle);
return tesSUCCESS;
}
@@ -9,6 +9,7 @@
#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>
#include <xrpl/ledger/helpers/CredentialHelpers.h>
#include <xrpl/ledger/helpers/EscrowHelpers.h>
@@ -211,13 +212,12 @@ EscrowFinish::preclaim(PreclaimContext const& ctx)
if (ctx.view.rules().enabled(featureTokenEscrow))
{
auto const seqProxy = SeqProxy::rawSequence(ctx.tx[sfOfferSequence]);
auto const k = keylet::escrow(ctx.tx[sfOwner], seqProxy);
auto const slep = ctx.view.read(k);
if (!slep)
EscrowEntryR const escrow(ctx.tx[sfOwner], seqProxy, ctx.view);
if (!escrow)
return tecNO_TARGET;
AccountID const dest = (*slep)[sfDestination];
STAmount const amount = (*slep)[sfAmount];
AccountID const dest = (*escrow)[sfDestination];
STAmount const amount = (*escrow)[sfAmount];
if (!isXRP(amount))
{
@@ -237,9 +237,8 @@ TER
EscrowFinish::doApply()
{
auto const seqProxy = SeqProxy::rawSequence(ctx_.tx[sfOfferSequence]);
auto const k = keylet::escrow(ctx_.tx[sfOwner], seqProxy);
auto const slep = ctx_.view().peek(k);
if (!slep)
EscrowEntryW escrow(ctx_.tx[sfOwner], seqProxy, ctx_.view(), j_);
if (!escrow)
{
if (ctx_.view().rules().enabled(featureTokenEscrow))
return tecINTERNAL; // LCOV_EXCL_LINE
@@ -252,11 +251,11 @@ EscrowFinish::doApply()
auto const now = ctx_.view().header().parentCloseTime;
// Too soon: can't execute before the finish time
if ((*slep)[~sfFinishAfter] && !after(now, (*slep)[sfFinishAfter]))
if ((*escrow)[~sfFinishAfter] && !after(now, (*escrow)[sfFinishAfter]))
return tecNO_PERMISSION;
// Too late: can't execute after the cancel time
if ((*slep)[~sfCancelAfter] && after(now, (*slep)[sfCancelAfter]))
if ((*escrow)[~sfCancelAfter] && after(now, (*escrow)[sfCancelAfter]))
return tecNO_PERMISSION;
// Check cryptocondition fulfillment
@@ -296,7 +295,7 @@ EscrowFinish::doApply()
return tecCRYPTOCONDITION_ERROR;
// Check against condition in the ledger entry:
auto const cond = (*slep)[~sfCondition];
auto const cond = (*escrow)[~sfCondition];
// If a condition wasn't specified during creation,
// one shouldn't be included now.
@@ -313,7 +312,7 @@ EscrowFinish::doApply()
}
// NOTE: Escrow payments cannot be used to fund accounts.
AccountID const destID = (*slep)[sfDestination];
AccountID const destID = (*escrow)[sfDestination];
auto const sled = ctx_.view().peek(keylet::account(destID));
if (!sled)
return tecNO_DST;
@@ -323,12 +322,12 @@ EscrowFinish::doApply()
!isTesSuccess(err))
return err;
AccountID const account = (*slep)[sfAccount];
AccountID const account = (*escrow)[sfAccount];
// Remove escrow from owner directory
{
auto const page = (*slep)[sfOwnerNode];
if (!ctx_.view().dirRemove(keylet::ownerDir(account), page, k.key, true))
auto const page = (*escrow)[sfOwnerNode];
if (!ctx_.view().dirRemove(keylet::ownerDir(account), page, escrow.key(), true))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete Escrow from owner.";
@@ -338,9 +337,9 @@ EscrowFinish::doApply()
}
// Remove escrow from recipient's owner directory, if present.
if (auto const optPage = (*slep)[~sfDestinationNode])
if (auto const optPage = (*escrow)[~sfDestinationNode])
{
if (!ctx_.view().dirRemove(keylet::ownerDir(destID), *optPage, k.key, true))
if (!ctx_.view().dirRemove(keylet::ownerDir(destID), *optPage, escrow.key(), true))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete Escrow from recipient.";
@@ -355,9 +354,9 @@ EscrowFinish::doApply()
bool const recycleReserve =
ctx_.view().rules().enabled(featureSponsor) || ctx_.view().rules().enabled(fixCleanup3_4_0);
if (recycleReserve)
decreaseOwnerCountForObject(ctx_.view(), account, slep, 1, ctx_.journal);
decreaseOwnerCountForObject(ctx_.view(), account, escrow.mutableRawSle(), 1, ctx_.journal);
STAmount const amount = slep->getFieldAmount(sfAmount);
STAmount const amount = escrow->getFieldAmount(sfAmount);
// Transfer amount to destination
if (isXRP(amount))
{
@@ -368,8 +367,8 @@ EscrowFinish::doApply()
if (!ctx_.view().rules().enabled(featureTokenEscrow))
return temDISABLED; // LCOV_EXCL_LINE
Rate lockedRate = slep->isFieldPresent(sfTransferRate)
? xrpl::Rate(slep->getFieldU32(sfTransferRate))
Rate lockedRate = escrow->isFieldPresent(sfTransferRate)
? xrpl::Rate(escrow->getFieldU32(sfTransferRate))
: kParityRate;
auto const issuer = amount.getIssuer();
bool const createAsset = destID == accountID_;
@@ -392,9 +391,9 @@ EscrowFinish::doApply()
return ret;
// Remove escrow from issuers owner directory, if present.
if (auto const optPage = (*slep)[~sfIssuerNode]; optPage)
if (auto const optPage = (*escrow)[~sfIssuerNode]; optPage)
{
if (!ctx_.view().dirRemove(keylet::ownerDir(issuer), *optPage, k.key, true))
if (!ctx_.view().dirRemove(keylet::ownerDir(issuer), *optPage, escrow.key(), true))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete Escrow from recipient.";
@@ -407,10 +406,10 @@ EscrowFinish::doApply()
ctx_.view().update(sled);
if (!recycleReserve)
decreaseOwnerCountForObject(ctx_.view(), account, slep, 1, ctx_.journal);
decreaseOwnerCountForObject(ctx_.view(), account, escrow.mutableRawSle(), 1, ctx_.journal);
// Remove escrow from ledger
ctx_.view().erase(slep);
escrow.erase();
return tesSUCCESS;
}
@@ -91,7 +91,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;
@@ -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
@@ -359,7 +359,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))
@@ -431,7 +432,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();
+6 -7
View File
@@ -8,6 +8,7 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/AmendmentTable.h>
#include <xrpl/ledger/entries/FeeSettingsEntry.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Protocol.h>
@@ -260,16 +261,14 @@ Change::applyAmendment()
TER
Change::applyFee()
{
auto const k = keylet::feeSettings();
SLE::pointer feeObject = view().peek(k);
FeeSettingsEntryW feeObject(view());
if (!feeObject)
{
feeObject = std::make_shared<SLE>(k);
view().insert(feeObject);
feeObject.newSLE();
feeObject.insert();
}
auto set = [](SLE::pointer& feeObject, STTx const& tx, auto const& field) {
auto set = [](FeeSettingsEntryW& feeObject, STTx const& tx, auto const& field) {
feeObject->at(field) = tx[field];
};
if (view().rules().enabled(featureXRPFees))
@@ -291,7 +290,7 @@ Change::applyFee()
set(feeObject, ctx_.tx, sfReserveIncrement);
}
view().update(feeObject);
feeObject.update();
JLOG(j_.warn()) << "Fees have been changed";
return tesSUCCESS;
@@ -0,0 +1,220 @@
#include <xrpl/tx/transactors/token/ConfidentialMPTHolderKeyUpdate.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/Transactor.h>
#include <bit>
#include <cstdint>
namespace xrpl {
bool
ConfidentialMPTHolderKeyUpdate::checkExtraFeatures(PreflightContext const& ctx)
{
// Holder key update is only meaningful when confidential transfers are enabled.
return ctx.rules.enabled(featureConfidentialTransfer);
}
std::uint32_t
ConfidentialMPTHolderKeyUpdate::getFlagsMask(PreflightContext const& ctx)
{
return tfConfidentialMPTHolderKeyUpdateMask;
}
NotTEC
ConfidentialMPTHolderKeyUpdate::preflight(PreflightContext const& ctx)
{
bool const rotation = ctx.tx.isFlag(tfHolderKeyRotation);
bool const recovery = ctx.tx.isFlag(tfHolderKeyRecovery);
bool const cancel = ctx.tx.isFlag(tfCancelRecovery);
// Exactly one of the three mode flags must be set.
static constexpr auto modeFlags = tfHolderKeyRotation | tfHolderKeyRecovery | tfCancelRecovery;
if (std::popcount(ctx.tx.getFlags() & modeFlags) != 1)
return temINVALID_FLAG;
// The issuer cannot hold confidential balances.
if (ctx.tx[sfAccount] == MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer())
return temMALFORMED;
bool const hasHolderKey = ctx.tx.isFieldPresent(sfHolderEncryptionKey);
bool const hasSpending = ctx.tx.isFieldPresent(sfConfidentialBalanceSpending);
bool const hasInbox = ctx.tx.isFieldPresent(sfConfidentialBalanceInbox);
bool const hasProof = ctx.tx.isFieldPresent(sfZKProof);
if (cancel)
{
// Cancel mode only revokes a pending recovery authorization; it
// carries no key material, balances, or proof.
if (hasHolderKey || hasSpending || hasInbox || hasProof)
return temMALFORMED;
return tesSUCCESS;
}
// Rotation and Recovery both require a holder key and a proof.
if (!hasHolderKey || !hasProof)
return temMALFORMED;
// Rotation mode requires re-encrypted balances; Recovery mode must not
// provide them since the holder cannot decrypt the current ones.
if (rotation && (!hasSpending || !hasInbox))
return temMALFORMED;
if (recovery && (hasSpending || hasInbox))
return temMALFORMED;
if (hasSpending &&
ctx.tx[sfConfidentialBalanceSpending].length() != kEcGamalEncryptedTotalLength)
return temBAD_CIPHERTEXT;
if (hasInbox && ctx.tx[sfConfidentialBalanceInbox].length() != kEcGamalEncryptedTotalLength)
return temBAD_CIPHERTEXT;
if (!isValidCompressedECPoint(ctx.tx[sfHolderEncryptionKey]))
return temMALFORMED;
if (hasSpending && !isValidCiphertext(ctx.tx[sfConfidentialBalanceSpending]))
return temBAD_CIPHERTEXT;
if (hasInbox && !isValidCiphertext(ctx.tx[sfConfidentialBalanceInbox]))
return temBAD_CIPHERTEXT;
return tesSUCCESS;
}
XRPAmount
ConfidentialMPTHolderKeyUpdate::calculateBaseFee(ReadView const& view, STTx const& tx)
{
return Transactor::calculateBaseFee(view, tx, kConfidentialFeeMultiplier);
}
TER
ConfidentialMPTHolderKeyUpdate::preclaim(PreclaimContext const& ctx)
{
auto const account = ctx.tx[sfAccount];
auto const mptIssuanceID = ctx.tx[sfMPTokenIssuanceID];
auto const sleIssuance = ctx.view.read(keylet::mptokenIssuance(mptIssuanceID));
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
if (!sleIssuance->isFlag(lsfMPTCanHoldConfidentialBalance))
return tecNO_PERMISSION;
auto const sleMptoken = ctx.view.read(keylet::mptoken(mptIssuanceID, account));
if (!sleMptoken)
return tecOBJECT_NOT_FOUND;
if (!sleMptoken->isFieldPresent(sfHolderEncryptionKey) ||
!sleMptoken->isFieldPresent(sfConfidentialBalanceSpending) ||
!sleMptoken->isFieldPresent(sfConfidentialBalanceInbox))
{
return tecNO_PERMISSION;
}
if (ctx.tx.isFlag(tfCancelRecovery))
{
// Nothing to cancel if no recovery is pending.
if (!sleMptoken->isFieldPresent(sfRecoveryKey))
return tecNO_PERMISSION;
return tesSUCCESS;
}
if (ctx.tx[sfHolderEncryptionKey] == (*sleMptoken)[sfHolderEncryptionKey])
return tecDUPLICATE;
// Recovery mode: reject if a recovery is already pending
if (ctx.tx.isFlag(tfHolderKeyRecovery) && sleMptoken->isFieldPresent(sfRecoveryKey))
return tecNO_PERMISSION;
return tesSUCCESS;
}
TER
ConfidentialMPTHolderKeyUpdate::doApply()
{
auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, accountID_));
if (!sleMptoken)
{
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::ConfidentialMPTHolderKeyUpdate::doApply : preclaim already validated the "
"MPToken exists");
return tecINTERNAL;
// LCOV_EXCL_STOP
}
if (ctx_.tx.isFlag(tfCancelRecovery))
{
// The holder revokes their pending recovery authorization; the
// current key and balances are left untouched.
sleMptoken->makeFieldAbsent(sfRecoveryKey);
view().update(sleMptoken);
return tesSUCCESS;
}
auto const newPubKey = ctx_.tx[sfHolderEncryptionKey];
if (ctx_.tx.isFlag(tfHolderKeyRotation))
{
// Replace the key and balances in rotation mode. Rotation proves the
// holder still has the old key, so any pending recovery is cancelled.
(*sleMptoken)[sfHolderEncryptionKey] = newPubKey;
(*sleMptoken)[sfConfidentialBalanceSpending] = ctx_.tx[sfConfidentialBalanceSpending];
(*sleMptoken)[sfConfidentialBalanceInbox] = ctx_.tx[sfConfidentialBalanceInbox];
sleMptoken->makeFieldAbsent(sfRecoveryKey);
incrementConfidentialVersion(*sleMptoken);
}
else if (ctx_.tx.isFlag(tfHolderKeyRecovery))
{
// Recovery mode: the holder cannot decrypt their current balances,
// so only the pending recovery key is recorded. The balances are
// rewritten separately by the issuer via ConfidentialMPTRecoverBalance.
(*sleMptoken)[sfRecoveryKey] = newPubKey;
}
else
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::ConfidentialMPTHolderKeyUpdate::doApply : invalid mode");
return tecINTERNAL;
// LCOV_EXCL_STOP
}
view().update(sleMptoken);
return tesSUCCESS;
}
void
ConfidentialMPTHolderKeyUpdate::visitInvariantEntry(bool, SLE::ConstRef, SLE::ConstRef)
{
}
bool
ConfidentialMPTHolderKeyUpdate::finalizeInvariants(
STTx const&,
TER,
XRPAmount,
ReadView const&,
beast::Journal const&)
{
return true;
}
} // namespace xrpl
+15 -10
View File
@@ -31,6 +31,15 @@
namespace xrpl::test {
class AMMClawbackMPT_test : public beast::unit_test::Suite
{
// SingleAssetVault, LendingProtocol and MPTokensV2 enable the large
// Number mantissa.
static bool
largeMantissa(FeatureBitset const& features)
{
return features[featureSingleAssetVault] || features[featureLendingProtocol] ||
features[featureMPTokensV2];
}
void
testInvalidRequest(FeatureBitset features)
{
@@ -1724,7 +1733,7 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
.asString();
auto const lpTokenBalance =
amm.ammRpcInfo()[jss::amm][jss::lp_token][jss::value].asString();
if (features[featureSingleAssetVault] || features[featureLendingProtocol])
if (largeMantissa(features))
{
BEAST_EXPECT(lpToken == "1.414213562374011" && lpTokenBalance == "1.4142135623741");
}
@@ -1741,16 +1750,14 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
env(amm::ammClawback(gw, alice, usd, eur, std::nullopt));
BEAST_EXPECT(!amm.ammExists());
}
else if (
features[fixAMMv1_3] &&
(features[featureSingleAssetVault] || features[featureLendingProtocol]))
else if (features[fixAMMv1_3] && largeMantissa(features))
{
env(amm::ammClawback(gw, alice, usd, eur, std::nullopt));
// Without the Rounding feature and with new Number a dust pool
// amount remains
BEAST_EXPECT(amm.ammExists());
}
else if (!features[featureSingleAssetVault] && !features[featureLendingProtocol])
else if (!largeMantissa(features))
{
env(amm::ammClawback(gw, alice, usd, eur, std::nullopt), Ter(tecINTERNAL));
BEAST_EXPECT(amm.ammExists());
@@ -1794,7 +1801,7 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
.asString();
auto const lpTokenBalance =
amm.ammRpcInfo()[jss::amm][jss::lp_token][jss::value].asString();
if (!features[featureSingleAssetVault] && !features[featureLendingProtocol])
if (!largeMantissa(features))
{
BEAST_EXPECT(lpToken == "1.414213562374011" && lpTokenBalance == "1.414213562374");
}
@@ -1811,16 +1818,14 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
env(amm::ammClawback(gw, alice, usd, eur, std::nullopt));
BEAST_EXPECT(!amm.ammExists());
}
else if (
features[fixAMMv1_3] &&
(features[featureSingleAssetVault] || features[featureLendingProtocol]))
else if (features[fixAMMv1_3] && largeMantissa(features))
{
// Without the Rounding feature and with new Number a dust pool
// amount remains
env(amm::ammClawback(gw, alice, usd, eur, std::nullopt));
BEAST_EXPECT(amm.ammExists());
}
else if (!features[featureSingleAssetVault] && !features[featureLendingProtocol])
else if (!largeMantissa(features))
{
env(amm::ammClawback(gw, alice, usd, eur, std::nullopt), Ter(tecINTERNAL));
BEAST_EXPECT(amm.ammExists());
+2 -2
View File
@@ -2857,8 +2857,8 @@ class AMMClawback_test : public beast::unit_test::Suite
{
// For now, just disable SAV entirely, which locks in the small Number
// mantissas
FeatureBitset const all =
jtx::testableAmendments() - featureSingleAssetVault - featureLendingProtocol;
FeatureBitset const all = jtx::testableAmendments() - featureSingleAssetVault -
featureLendingProtocol - featureMPTokensV2;
testInvalidRequest(all);
testInvalidRequest(all - featureMPTokensV2);
+3 -3
View File
@@ -116,7 +116,7 @@ private:
Txflags(tfPartialPayment));
BEAST_EXPECT(ammCarol.expectBalances(
btc(1'001'000'000'374'816), eth(100'000'000'000'000'000), ammCarol.tokens()));
btc(1'001'000'000'374'814), eth(100'000'000'000'000'000), ammCarol.tokens()));
env.require(Balance(bob_, eth(200'100'000'000'000'000)));
BEAST_EXPECT(isOffer(env, carol_, btc(49'000'000'000'000), XRP(49)));
@@ -1890,7 +1890,7 @@ 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 ipe = [](MPTTester const& iss) {
return STPathElement(
@@ -1928,7 +1928,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 ok = sb.peek(keylet::offer(o)))
+18 -37
View File
@@ -73,7 +73,8 @@ class AMMExtended_test : public jtx::AMMTest
// For now, just disable SAV entirely, which locks in the small Number
// mantissas
FeatureBitset const all_{
testableAmendments() - featureSingleAssetVault - featureLendingProtocol};
testableAmendments() - featureSingleAssetVault - featureLendingProtocol -
featureMPTokensV2};
private:
void
@@ -267,39 +268,20 @@ private:
{features});
// tfPassive -- place the offer without crossing it.
if (features[featureMPTokensV2])
{
Env env{*this, features};
fund(env, gw_, {alice_, carol_}, XRP(30'000'000), {USD(30'000'000)});
AMM const ammAlice(env, alice_, XRP(10'100'000), USD(10'000'000));
// Scale the exact-quality fixture up so the visual relationship
// stays clear: the passive CLOB offer has the same 1:1 quality as
// the generated AMM offer, so it should not cross.
env(offer(carol_, XRP(100'000), USD(100'000), tfPassive));
env.close();
BEAST_EXPECT(
ammAlice.expectBalances(XRP(10'100'000), USD(10'000'000), ammAlice.tokens()));
BEAST_EXPECT(expectOffers(env, carol_, 1, {{{XRP(100'000), USD(100'000)}}}));
}
else
{
testAMM(
[&](AMM& ammAlice, Env& env) {
// Carol creates a passive offer that could cross AMM.
// Carol's offer should stay in the ledger.
env(offer(carol_, XRP(100), USD(100), tfPassive));
env.close();
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'100), STAmount{USD, 10'000}, ammAlice.tokens()));
BEAST_EXPECT(expectOffers(env, carol_, 1, {{{XRP(100), STAmount{USD, 100}}}}));
},
{{XRP(10'100), USD(10'000)}},
0,
std::nullopt,
{features});
}
testAMM(
[&](AMM& ammAlice, Env& env) {
// Carol creates a passive offer that could cross AMM.
// Carol's offer should stay in the ledger.
env(offer(carol_, XRP(100), USD(100), tfPassive));
env.close();
BEAST_EXPECT(
ammAlice.expectBalances(XRP(10'100), STAmount{USD, 10'000}, ammAlice.tokens()));
BEAST_EXPECT(expectOffers(env, carol_, 1, {{{XRP(100), STAmount{USD, 100}}}}));
},
{{XRP(10'100), USD(10'000)}},
0,
std::nullopt,
{features});
// tfPassive -- cross only offers of better quality.
testAMM(
@@ -1450,7 +1432,6 @@ private:
testRmFundedOffer(all_ - fixAMMv1_1 - fixAMMv1_3);
testEnforceNoRipple(all_);
testFillModes(all_);
testFillModes(all_ - featureMPTokensV2);
testOfferCrossWithXRP(all_);
testOfferCrossWithLimitOverride(all_);
testCurrencyConversionEntire(all_);
@@ -2068,7 +2049,7 @@ 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 ipe = [](Issue const& iss) {
return STPathElement(
@@ -2106,7 +2087,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 ok = sb.peek(keylet::offer(o)))
+28 -25
View File
@@ -1451,12 +1451,14 @@ private:
auto carolXRP = env.balance(carol_, XRP);
auto carolMPT = env.balance(carol_, MPT(ammAlice[1]));
// The exact deposit is 201; rounding in the pool's favor at
// the large mantissa takes one more unit.
ammAlice.deposit(carol_, 100'000, MPT(ammAlice[1])(205));
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'000), MPT(ammAlice[1])(10'201), IOUAmount{10'100'000, 0}));
XRP(10'000), MPT(ammAlice[1])(10'202), IOUAmount{10'100'000, 0}));
env.require(Balance(carol_, carolXRP - drops(baseFee)));
env.require(Balance(carol_, carolMPT - MPT(ammAlice[1])(201)));
env.require(Balance(carol_, carolMPT - MPT(ammAlice[1])(202)));
},
{{XRP(10'000), gAmmmpt(10'000)}});
@@ -1467,11 +1469,13 @@ private:
auto carolXRP = env.balance(carol_, XRP);
auto carolMPT = env.balance(carol_, MPT(ammAlice[1]));
// The exact deposit is 201 XRP; rounding in the pool's favor
// at the large mantissa takes one more drop.
ammAlice.deposit(carol_, 100'000, XRP(205));
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'201), MPT(ammAlice[1])(10'000), IOUAmount{10'100'000, 0}));
XRPAmount{10'201'000'001}, MPT(ammAlice[1])(10'000), IOUAmount{10'100'000, 0}));
env.require(Balance(carol_, carolXRP - XRP(201) - drops(baseFee)));
env.require(Balance(carol_, carolXRP - XRPAmount{201'000'001} - drops(baseFee)));
env.require(Balance(carol_, carolMPT));
},
{{XRP(10'000), gAmmmpt(10'000)}});
@@ -1551,10 +1555,10 @@ private:
STAmount{ammAlice.lptIssue(), 2004, -6});
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'000), MPT(ammAlice[1])(10'081), IOUAmount{10'039'920'31840891, -8}));
XRP(10'000), MPT(ammAlice[1])(10'080), IOUAmount{10'039'920'3184089, -7}));
env.require(Balance(carol_, carolXRP - drops(baseFee)));
env.require(Balance(carol_, carolMPT - MPT(ammAlice[1])(81)));
env.require(Balance(carol_, carolMPT - MPT(ammAlice[1])(80)));
},
{{XRP(10'000), gAmmmpt(10'000)}});
@@ -1573,10 +1577,10 @@ private:
STAmount{ammAlice.lptIssue(), 2004, -6});
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'000), MPT(ammAlice[1])(10'081), IOUAmount{10'039'920'31840891, -8}));
XRP(10'000), MPT(ammAlice[1])(10'080), IOUAmount{10'039'920'3184089, -7}));
env.require(Balance(carol_, carolXRP - drops(baseFee)));
env.require(Balance(carol_, carolMPT - MPT(ammAlice[1])(81)));
env.require(Balance(carol_, carolMPT - MPT(ammAlice[1])(80)));
},
{{XRP(10'000), gAmmmpt(10'000)}});
@@ -1797,9 +1801,9 @@ private:
ammAlice.deposit(gw_, MPT(ammAlice[0])(1000));
BEAST_EXPECT(ammAlice.expectBalances(
MPT(ammAlice[0])(1'010'999),
MPT(ammAlice[0])(1'011'000),
MPT(ammAlice[1])(1'010'000),
IOUAmount{1'010'499'376546071, -9}));
IOUAmount{1'010'499'876298854, -9}));
},
{{gAmmmpt(10'000), gAmmmpt(10'000)}});
@@ -2212,7 +2216,7 @@ private:
if (env.enabled(fixAMMv1_3))
{
BEAST_EXPECT(ammAlice.expectBalances(
MPT(ammAlice[0])(1), STAmount{USD, 1, -11}, IOUAmount{1, -8}));
MPT(ammAlice[0])(1), STAmount{USD, 1, -12}, IOUAmount{1, -9}));
}
},
{{gAmmmpt(10'000'000'000), USD(10'000)}},
@@ -2541,11 +2545,12 @@ private:
// XRP amount to withdraw is 0
ammAlice.withdraw(
alice_, IOUAmount{1, -5}, std::nullopt, std::nullopt, Ter(tecAMM_FAILED));
// Calculated tokens to withdraw are 0
// Calculated tokens to withdraw are 0: 1e-16 of 10,000 USD is
// below the large Number mantissa's precision
ammAlice.withdraw(
alice_,
std::nullopt,
STAmount{USD, 1, -11},
STAmount{USD, 1, -16},
std::nullopt,
Ter(tecAMM_INVALID_TOKENS));
ammAlice.deposit(carol_, STAmount{USD, 1, -10});
@@ -2707,7 +2712,7 @@ private:
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount(9000'000001),
MPT(ammAlice[1])(10'000),
IOUAmount{9'486'832'98050514, -8}));
IOUAmount{9'486'832'980505138, -9}));
},
{{XRP(10'000), gAmmmpt(10'000)}});
testAMM(
@@ -2715,7 +2720,7 @@ private:
// single withdraw MPT from XRP/MPT
ammAlice.withdraw(alice_, MPT(ammAlice[1])(1'000));
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10000), MPT(ammAlice[1])(9001), IOUAmount{9'486'832'98050514, -8}));
XRP(10000), MPT(ammAlice[1])(9001), IOUAmount{9'486'832'980505138, -9}));
},
{{XRP(10'000), gAmmmpt(10'000)}});
testAMM(
@@ -2723,9 +2728,7 @@ private:
// single withdraw IOU from IOU/MPT
ammAlice.withdraw(alice_, USD(1'000));
BEAST_EXPECT(ammAlice.expectBalances(
STAmount{USD, UINT64_C(9000'000000000004), -12},
MPT(ammAlice[1])(10'000),
IOUAmount{9486'83298050514, -11}));
USD(9'000), MPT(ammAlice[1])(10'000), IOUAmount{9486'832980505138, -12}));
},
{{USD(10'000), gAmmmpt(10'000)}});
testAMM(
@@ -2733,7 +2736,7 @@ private:
// single withdraw MPT from IOU/MPT
ammAlice.withdraw(alice_, MPT(ammAlice[1])(1'000));
BEAST_EXPECT(ammAlice.expectBalances(
USD(10'000), MPT(ammAlice[1])(9001), IOUAmount{9486'83298050514, -11}));
USD(10'000), MPT(ammAlice[1])(9001), IOUAmount{9486'832980505138, -12}));
},
{{USD(10'000), gAmmmpt(10'000)}});
testAMM(
@@ -2743,7 +2746,7 @@ private:
BEAST_EXPECT(ammAlice.expectBalances(
MPT(ammAlice[0])(9001),
MPT(ammAlice[1])(10'000),
IOUAmount{9486'83298050514, -11}));
IOUAmount{9486'832980505138, -12}));
},
{{gAmmmpt(10'000), gAmmmpt(10'000)}});
@@ -3024,7 +3027,7 @@ private:
ammAlice.deposit(carol_, 1'000);
ammAlice.withdrawAll(carol_, USD(0));
BEAST_EXPECT(ammAlice.expectBalances(
STAmount{USD, UINT64_C(9'090'909090909092), -12},
STAmount{USD, UINT64_C(9'090'909090909091), -12},
MPT(ammAlice[1])(11'000),
IOUAmount{10'000}));
},
@@ -3249,7 +3252,7 @@ private:
BEAST_EXPECT(ammAlice.expectBalances(
MPT(ammAlice[0])(10'000'000'000),
STAmount{USD, 1, -11},
IOUAmount{316227765, -9}));
IOUAmount{316227766, -9}));
},
{{gAmmmpt(10'000'000'000), USD(10'000)}});
// XRP
@@ -4155,7 +4158,7 @@ private:
auto 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 pf = AMMBid::checkExtraFeatures(ctx);
BEAST_EXPECT(pf == false);
env.app().config().features.insert(featureMPTokensV2);
@@ -4167,7 +4170,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 pf = AMMBid::preflight(ctx);
BEAST_EXPECT(pf == temBAD_AMM_TOKENS);
}
@@ -5960,7 +5963,7 @@ private:
if (!features[fixAMMv1_3])
{
BEAST_EXPECT(tokensFee == IOUAmount(98'019'80198019, -8));
BEAST_EXPECT(tokensNoFee == IOUAmount(98'495'13933556, -8));
BEAST_EXPECT(tokensNoFee == IOUAmount(98'495'13933557, -8));
}
else
{
+11 -72
View File
@@ -84,7 +84,8 @@ private:
{
// For now, just disable SAV entirely, which locks in the small Number
// mantissas
return jtx::testableAmendments() - featureSingleAssetVault - featureLendingProtocol;
return jtx::testableAmendments() - featureSingleAssetVault - featureLendingProtocol -
featureMPTokensV2;
}
// Seed from the local testableAmendments() which strips SAV and Lending.
@@ -3262,7 +3263,7 @@ private:
auto 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 pf = Transactor::invokePreflight<AMMBid>(pfCtx);
BEAST_EXPECT(pf == temDISABLED);
env.app().config().features.insert(featureAMM);
@@ -3273,7 +3274,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 pf = Transactor::invokePreflight<AMMBid>(pfCtx);
BEAST_EXPECT(!isTesSuccess(pf));
}
@@ -3284,7 +3285,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 pf = Transactor::invokePreflight<AMMBid>(pfCtx);
BEAST_EXPECT(pf == temBAD_AMM_TOKENS);
}
@@ -3810,7 +3811,7 @@ private:
BEAST_EXPECT(amm.expectBalances(XRP(1'000), USD(500), amm.tokens()));
BEAST_EXPECT(expectOffers(env, carol_, 1, {{Amounts{XRP(100), USD(55)}}}));
}
else if (!features[featureMPTokensV2])
else
{
BEAST_EXPECT(amm.expectBalances(
XRPAmount(909'090'909),
@@ -3825,30 +3826,6 @@ private:
env.balance(carol_, USD) ==
STAmount(USD, UINT64_C(29'949'94999999494), -11));
}
else
{
// Post-amendment the transfer fee is taken into account
// when calculating the limit out based on limitQuality.
// This increases the limitQuality and decreases
// the limit out. Consequently, AMM offer size is decreased,
// and the quality is increased, matching the overall
// quality.
// AMM offer ~50USD/91XRP
BEAST_EXPECT(amm.expectBalances(
XRPAmount(909'090'910),
STAmount{USD, UINT64_C(549'99999945), -8},
amm.tokens()));
// Offer ~91XRP/50USD
BEAST_EXPECT(expectOffers(
env,
carol_,
1,
{{Amounts{XRPAmount{9'090'910}, STAmount{USD, 5'0000005, -7}}}}));
// Carol pays 0.1% fee on ~50USD =~ 0.05USD
BEAST_EXPECT(
env.balance(carol_, USD) ==
STAmount(USD, UINT64_C(29'949'95000060055), -11));
}
},
{{XRP(1'000), USD(500)}},
0,
@@ -6498,7 +6475,7 @@ private:
BEAST_EXPECT(expectOffers(env, bob_, 1, {{Amounts{USD(1), XRPAmount(500)}}}));
BEAST_EXPECT(expectOffers(env, carol_, 1, {{Amounts{XRP(100), USD(55)}}}));
}
else if (!features[featureMPTokensV2])
else
{
BEAST_EXPECT(amm.expectBalances(
XRPAmount(909'090'909),
@@ -6511,19 +6488,6 @@ private:
{{Amounts{XRPAmount{9'090'909}, STAmount{USD, 4'99999995, -8}}}}));
BEAST_EXPECT(expectOffers(env, bob_, 1, {{Amounts{USD(1), XRPAmount(500)}}}));
}
else
{
BEAST_EXPECT(amm.expectBalances(
XRPAmount(909'090'910),
STAmount{USD, UINT64_C(549'99999945), -8},
amm.tokens()));
BEAST_EXPECT(expectOffers(
env,
carol_,
1,
{{Amounts{XRPAmount{9'090'910}, STAmount{USD, 5'0000005, -7}}}}));
BEAST_EXPECT(expectOffers(env, bob_, 1, {{Amounts{USD(1), XRPAmount(500)}}}));
}
}
// There is no blocking offer, the same AMM liquidity is consumed
@@ -6535,30 +6499,10 @@ private:
AMM const amm(env, alice_, XRP(1'000), USD(500));
env(offer(carol_, XRP(100), USD(55)));
env.close();
if (!features[featureMPTokensV2])
{
BEAST_EXPECT(amm.expectBalances(
XRPAmount(909'090'909),
STAmount{USD, UINT64_C(550'000000055), -9},
amm.tokens()));
BEAST_EXPECT(expectOffers(
env,
carol_,
1,
{{Amounts{XRPAmount{9'090'909}, STAmount{USD, 4'99999995, -8}}}}));
}
else
{
BEAST_EXPECT(amm.expectBalances(
XRPAmount(909'090'910),
STAmount{USD, UINT64_C(549'99999945), -8},
amm.tokens()));
BEAST_EXPECT(expectOffers(
env,
carol_,
1,
{{Amounts{XRPAmount{9'090'910}, STAmount{USD, 5'0000005, -7}}}}));
}
BEAST_EXPECT(amm.expectBalances(
XRPAmount(909'090'909), STAmount{USD, UINT64_C(550'000000055), -9}, amm.tokens()));
BEAST_EXPECT(expectOffers(
env, carol_, 1, {{Amounts{XRPAmount{9'090'909}, STAmount{USD, 4'99999995, -8}}}}));
}
}
@@ -7448,8 +7392,6 @@ private:
.err = Ter(tecAMM_BALANCE)});
};
// Bound holds regardless of the deposit-side fix amendment.
test(all - featureMPTokensV2);
test(all);
}
@@ -7481,7 +7423,6 @@ private:
testFlags();
testRippling();
testAMMAndCLOB(all);
testAMMAndCLOB(all - featureMPTokensV2);
testAMMAndCLOB(all - fixAMMv1_1 - fixAMMv1_3);
testTradingFee(all);
testTradingFee(all - fixAMMv1_3);
@@ -7501,10 +7442,8 @@ private:
testOverflowOffer(all - fixAMMv1_1 - fixAMMv1_3);
testSwapRounding();
testFixChangeSpotPriceQuality(all);
testFixChangeSpotPriceQuality(all - featureMPTokensV2);
testFixChangeSpotPriceQuality(all - fixAMMv1_1 - fixAMMv1_3);
testFixAMMOfferBlockedByLOB(all);
testFixAMMOfferBlockedByLOB(all - featureMPTokensV2);
testFixAMMOfferBlockedByLOB(all - fixAMMv1_1 - fixAMMv1_3);
testLPTokenBalance(all);
testLPTokenBalance(all - fixAMMv1_3);
+1 -1
View File
@@ -594,7 +594,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
@@ -436,7 +436,7 @@ class Batch_test : public beast::unit_test::Suite
{
auto 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);
}
@@ -451,7 +451,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);
@@ -462,7 +462,12 @@ class Batch_test : public beast::unit_test::Suite
{
auto 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);
}
@@ -1590,7 +1595,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;
});
@@ -4391,7 +4396,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;
});
@@ -5637,7 +5642,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;
@@ -1,6 +1,7 @@
#include <test/jtx/Account.h>
#include <test/jtx/ConfidentialTransfer.h>
#include <test/jtx/Env.h>
#include <test/jtx/amount.h>
#include <test/jtx/mpt.h>
#include <xrpl/basics/Buffer.h>
@@ -20,6 +21,7 @@
#include <cstdint>
#include <memory>
#include <optional>
#include <utility>
#include <vector>
namespace xrpl {
@@ -2454,6 +2456,703 @@ class ConfidentialMPTKeyRotation_test : public ConfidentialTransferTestBase
});
}
void
testConfidentialMPTHolderKeyUpdatePreflight(FeatureBitset features)
{
testcase("ConfidentialMPTHolderKeyUpdate preflight");
using namespace test::jtx;
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
// Both amendments are required: ConfidentialMPTKeyRotation and ConfidentialTransfer.
if (!features[featureConfidentialMPTKeyRotation] || !features[featureConfidentialTransfer])
{
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create({.ownerCount = 1, .flags = tfMPTCanTransfer});
mptAlice.holderKeyUpdate({
.account = bob,
.holderPubKey = gMakeZeroBuffer(kEcPubKeyLength),
.flags = tfHolderKeyRecovery,
.err = temDISABLED,
});
return;
}
ConfidentialEnv ct{env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
Account const bobNewKey("bobNewKey");
ct.mpt.generateKeyPair(bobNewKey);
// The flag contains a value outside the recognized mode bits. This is
// rejected by the flags-mask check
for (auto const flags : {0x00080000u, 0x00080000u | tfHolderKeyRecovery, 0x00100000u})
{
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
.proof = gMakeZeroBuffer(1),
.flags = flags,
.err = temINVALID_FLAG,
});
}
// Exactly one of Rotation, Recovery, and Cancel must be set.
for (auto const flags :
{0u,
tfHolderKeyRotation | tfHolderKeyRecovery,
tfHolderKeyRotation | tfCancelRecovery,
tfHolderKeyRecovery | tfCancelRecovery,
tfHolderKeyRotation | tfHolderKeyRecovery | tfCancelRecovery})
{
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
.proof = gMakeZeroBuffer(1),
.flags = flags,
.err = temINVALID_FLAG,
});
}
// The issuer cannot rotate or recover a confidential balance it cannot hold.
Account const aliceNewKey("aliceNewKey");
ct.mpt.generateKeyPair(aliceNewKey);
ct.mpt.holderKeyUpdate({
.account = alice,
.holderPubKey = ct.mpt.getPubKey(aliceNewKey),
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRecovery,
.err = temMALFORMED,
});
// HolderEncryptionKey one byte short of the required length.
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = gMakeZeroBuffer(kEcPubKeyLength - 1),
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRecovery,
.err = temMALFORMED,
});
// HolderEncryptionKey the correct length, but not a well-formed
// compressed secp256k1 point.
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = gMakeZeroBuffer(kEcPubKeyLength),
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRecovery,
.err = temMALFORMED,
});
// HolderEncryptionKey is entirely absent (as opposed to present with
// the wrong length or format).
ct.mpt.holderKeyUpdate({
.account = bob,
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRecovery,
.err = temMALFORMED,
});
std::optional<Buffer> const cipher = getTrivialCiphertext();
std::optional<Buffer> const none;
// Rotation mode requires both the spending and inbox ciphertexts;
for (auto const& [spending, inbox] :
{std::pair{none, none}, std::pair{cipher, none}, std::pair{none, cipher}})
{
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
.spendingCiphertext = spending,
.inboxCiphertext = inbox,
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRotation,
.err = temMALFORMED,
});
}
// Recovery and Cancel modes must not include either.
for (auto const& [spending, inbox] :
{std::pair{cipher, none}, std::pair{none, cipher}, std::pair{cipher, cipher}})
{
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
.spendingCiphertext = spending,
.inboxCiphertext = inbox,
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRecovery,
.err = temMALFORMED,
});
}
for (auto const& [spending, inbox] :
{std::pair{cipher, none}, std::pair{none, cipher}, std::pair{cipher, cipher}})
{
ct.mpt.holderKeyUpdate({
.account = bob,
.spendingCiphertext = spending,
.inboxCiphertext = inbox,
.flags = tfCancelRecovery,
.err = temMALFORMED,
});
}
// Spending ciphertext has the wrong length.
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
.spendingCiphertext = gMakeZeroBuffer(kEcGamalEncryptedTotalLength - 1),
.inboxCiphertext = getTrivialCiphertext(),
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRotation,
.err = temBAD_CIPHERTEXT,
});
// Inbox ciphertext has the wrong length.
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
.spendingCiphertext = getTrivialCiphertext(),
.inboxCiphertext = gMakeZeroBuffer(kEcGamalEncryptedTotalLength - 1),
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRotation,
.err = temBAD_CIPHERTEXT,
});
// Spending ciphertext has the correct length, but is not a
// well-formed EC ElGamal ciphertext.
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
.spendingCiphertext = getBadCiphertext(),
.inboxCiphertext = getTrivialCiphertext(),
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRotation,
.err = temBAD_CIPHERTEXT,
});
// Inbox ciphertext has the correct length, but is not a well-formed
// EC ElGamal ciphertext.
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
.spendingCiphertext = getTrivialCiphertext(),
.inboxCiphertext = getBadCiphertext(),
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRotation,
.err = temBAD_CIPHERTEXT,
});
// Rotation/Recovery mode requires a ZKProof.
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
.flags = tfHolderKeyRecovery,
.err = temMALFORMED,
});
// Cancel mode must not include HolderEncryptionKey.
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
.flags = tfCancelRecovery,
.err = temMALFORMED,
});
// Cancel mode must not include a ZKProof.
ct.mpt.holderKeyUpdate({
.account = bob,
.proof = gMakeZeroBuffer(1),
.flags = tfCancelRecovery,
.err = temMALFORMED,
});
}
void
testConfidentialMPTHolderKeyUpdatePreclaim(FeatureBitset features)
{
testcase("ConfidentialMPTHolderKeyUpdate preclaim");
using namespace test::jtx;
// The issuance does not exist.
{
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create({
.ownerCount = 1,
.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
});
// Destroy the issuance to test issuance not found.
mptAlice.destroy();
mptAlice.holderKeyUpdate({
.account = bob,
.flags = tfCancelRecovery,
.err = tecOBJECT_NOT_FOUND,
});
}
// The issuance has not enabled confidential balances. The holder is
// authorized so this reaches the confidential-balance-flag check
// rather than failing earlier on a missing MPToken.
{
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create({.ownerCount = 1, .flags = tfMPTCanTransfer});
mptAlice.authorize({.account = bob});
mptAlice.holderKeyUpdate({
.account = bob,
.flags = tfCancelRecovery,
.err = tecNO_PERMISSION,
});
}
// carol exists as an account but was never authorized to hold this
// issuance, so she has no MPToken for it at all.
{
Env env{*this, features};
Account const alice("alice");
Account const carol("carol");
MPTTester mptAlice(env, alice);
mptAlice.create({
.ownerCount = 1,
.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
});
env.fund(XRP(1000), carol);
env.close();
Account const carolNewKey("carolNewKey");
mptAlice.generateKeyPair(carolNewKey);
mptAlice.holderKeyUpdate({
.account = carol,
.holderPubKey = mptAlice.getPubKey(carolNewKey),
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRecovery,
.err = tecOBJECT_NOT_FOUND,
});
}
// The holder has an MPToken but no confidential state yet - never
// registered a key or converted anything.
{
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create({
.ownerCount = 1,
.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
});
mptAlice.authorize({.account = bob});
mptAlice.pay(alice, bob, 100);
Account const bobNewKey("bobNewKey");
mptAlice.generateKeyPair(bobNewKey);
mptAlice.holderKeyUpdate({
.account = bob,
.holderPubKey = mptAlice.getPubKey(bobNewKey),
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRecovery,
.err = tecNO_PERMISSION,
});
}
// Submitting the holder's own current key as the "new" key is a no-op.
{
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
ConfidentialEnv ct{
env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = ct.mpt.getPubKey(bob),
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRecovery,
.err = tecDUPLICATE,
});
}
// A second Recovery-mode transaction must not silently overwrite an
// already-pending RecoveryKey.
{
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
ConfidentialEnv ct{
env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
Account const bobRecoveryKey("bobRecoveryKey");
ct.mpt.generateKeyPair(bobRecoveryKey);
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = ct.mpt.getPubKey(bobRecoveryKey),
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRecovery,
});
Account const bobRecoveryKey2("bobRecoveryKey2");
ct.mpt.generateKeyPair(bobRecoveryKey2);
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = ct.mpt.getPubKey(bobRecoveryKey2),
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRecovery,
.err = tecNO_PERMISSION,
});
}
// bob never submitted a Recovery-mode transaction, so there is no
// sfRecoveryKey to cancel.
{
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
ConfidentialEnv ct{
env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
ct.mpt.holderKeyUpdate({
.account = bob,
.flags = tfCancelRecovery,
.err = tecNO_PERMISSION,
});
}
// Runs rotation, recovery, and cancel in sequence and expects each to
// succeed.
auto const allModesSucceed = [&](ConfidentialEnv& ct, Account const& bob) {
Account const bobNewKey("bobNewKey");
ct.mpt.generateKeyPair(bobNewKey);
auto const reEnc = reencryptHolderBalances(ct.mpt, bob, bob, bobNewKey);
BEAST_EXPECT(reEnc.has_value());
if (!reEnc)
return;
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
.spendingCiphertext = reEnc->first,
.inboxCiphertext = reEnc->second,
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRotation,
});
Account const bobRecoveryKey("bobRecoveryKey");
ct.mpt.generateKeyPair(bobRecoveryKey);
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = ct.mpt.getPubKey(bobRecoveryKey),
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRecovery,
});
ct.mpt.holderKeyUpdate({
.account = bob,
.flags = tfCancelRecovery,
});
};
// Individual (per-holder) lock.
{
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
ConfidentialEnv ct{
env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
ct.mpt.set({.account = alice, .holder = bob, .flags = tfMPTLock});
allModesSucceed(ct, bob);
}
// Global (issuance-wide) lock.
{
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
ConfidentialEnv ct{
env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
ct.mpt.set({.account = alice, .flags = tfMPTLock});
allModesSucceed(ct, bob);
}
// A stale mirror epoch does not block rotation, recovery, or cancel:
{
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
ConfidentialEnv ct{
env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
// Rotate the issuer's key without refreshing bob's mirror,
// leaving bob's mirror epoch stale relative to the issuance.
Account const aliceNewIssuerKey("aliceNewIssuerKey");
ct.mpt.generateKeyPair(aliceNewIssuerKey);
ct.mpt.set({.account = alice, .issuerPubKey = ct.mpt.getPubKey(aliceNewIssuerKey)});
BEAST_EXPECT(ct.mpt.checkKeyEpochs(1u, std::nullopt));
BEAST_EXPECT(ct.mpt.checkMirrorEpochs(bob, std::nullopt, std::nullopt));
allModesSucceed(ct, bob);
}
}
void
testConfidentialMPTHolderKeyUpdateDoApply(FeatureBitset features)
{
testcase("ConfidentialMPTHolderKeyUpdate doApply");
using namespace test::jtx;
// Rotation mode updates the key and re-encrypted balances, bumps the
// version, and clears any pending recovery.
{
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
ConfidentialEnv ct{
env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
auto const prevVersion = ct.mpt.getMPTokenVersion(bob);
Account const bobNewKey("bobNewKey");
ct.mpt.generateKeyPair(bobNewKey);
auto const reEnc = reencryptHolderBalances(ct.mpt, bob, bob, bobNewKey);
BEAST_EXPECT(reEnc.has_value());
if (!reEnc)
return;
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
.spendingCiphertext = reEnc->first,
.inboxCiphertext = reEnc->second,
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRotation,
});
auto const sleMptoken = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id()));
if (!BEAST_EXPECT(sleMptoken))
return;
auto const newPubKey = ct.mpt.getPubKey(bobNewKey);
BEAST_EXPECT(
newPubKey && strHex((*sleMptoken)[sfHolderEncryptionKey]) == strHex(*newPubKey));
BEAST_EXPECT(!sleMptoken->isFieldPresent(sfRecoveryKey));
BEAST_EXPECT(ct.mpt.getMPTokenVersion(bob) == prevVersion + 1);
// The rotated balances must still decrypt to the same amounts,
// but now only under the NEW private key.
auto const spendingCt =
ct.mpt.getEncryptedBalance(bob, MPTTester::holderEncryptedSpending);
auto const inboxCt = ct.mpt.getEncryptedBalance(bob, MPTTester::holderEncryptedInbox);
BEAST_EXPECT(spendingCt.has_value());
BEAST_EXPECT(inboxCt.has_value());
if (!spendingCt || !inboxCt)
return;
auto const spendingAmt = ct.mpt.decryptAmount(bobNewKey, *spendingCt);
auto const inboxAmt = ct.mpt.decryptAmount(bobNewKey, *inboxCt);
BEAST_EXPECT(spendingAmt && *spendingAmt == 40);
BEAST_EXPECT(inboxAmt && *inboxAmt == 0);
}
// Rotation mode re-encrypts both balances correctly when spending and
// inbox are both non-zero and differ from each other.
{
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
Account const carol("carol");
ConfidentialEnv ct{
env,
alice,
{{.account = bob, .payAmount = 100, .convertAmount = 40},
{.account = carol, .payAmount = 100, .convertAmount = 50}}};
// carol sends 15 into bob's inbox, which is not merged into
// spending, leaving bob with spending=40, inbox=15.
ct.mpt.send({.account = carol, .dest = bob, .amt = 15});
Account const bobNewKey("bobNewKey");
ct.mpt.generateKeyPair(bobNewKey);
auto const reEnc = reencryptHolderBalances(ct.mpt, bob, bob, bobNewKey);
BEAST_EXPECT(reEnc.has_value());
if (!reEnc)
return;
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
.spendingCiphertext = reEnc->first,
.inboxCiphertext = reEnc->second,
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRotation,
});
auto const spendingCt =
ct.mpt.getEncryptedBalance(bob, MPTTester::holderEncryptedSpending);
auto const inboxCt = ct.mpt.getEncryptedBalance(bob, MPTTester::holderEncryptedInbox);
BEAST_EXPECT(spendingCt.has_value());
BEAST_EXPECT(inboxCt.has_value());
if (!spendingCt || !inboxCt)
return;
auto const spendingAmt = ct.mpt.decryptAmount(bobNewKey, *spendingCt);
auto const inboxAmt = ct.mpt.decryptAmount(bobNewKey, *inboxCt);
BEAST_EXPECT(spendingAmt && *spendingAmt == 40);
BEAST_EXPECT(inboxAmt && *inboxAmt == 15);
}
// Recovery mode only records the pending recovery key; the current
// key, balances, and version are untouched.
{
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
ConfidentialEnv ct{
env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
auto const prevVersion = ct.mpt.getMPTokenVersion(bob);
auto const prevSpending =
ct.mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending);
auto const prevInbox = ct.mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedInbox);
auto const prevKey = ct.mpt.getPubKey(bob);
Account const bobRecoveryKey("bobRecoveryKey");
ct.mpt.generateKeyPair(bobRecoveryKey);
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = ct.mpt.getPubKey(bobRecoveryKey),
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRecovery,
});
auto const sleMptoken = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id()));
if (!BEAST_EXPECT(sleMptoken))
return;
auto const recoveryKey = ct.mpt.getPubKey(bobRecoveryKey);
BEAST_EXPECT(
sleMptoken->isFieldPresent(sfRecoveryKey) && recoveryKey &&
strHex((*sleMptoken)[sfRecoveryKey]) == strHex(*recoveryKey));
BEAST_EXPECT(
prevKey && strHex((*sleMptoken)[sfHolderEncryptionKey]) == strHex(*prevKey));
BEAST_EXPECT(ct.mpt.getMPTokenVersion(bob) == prevVersion);
BEAST_EXPECT(
ct.mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) ==
prevSpending);
BEAST_EXPECT(
ct.mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedInbox) == prevInbox);
}
// Cancel mode clears the pending recovery key only; the current key,
// balances, and version are untouched.
{
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
ConfidentialEnv ct{
env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
Account const bobRecoveryKey("bobRecoveryKey");
ct.mpt.generateKeyPair(bobRecoveryKey);
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = ct.mpt.getPubKey(bobRecoveryKey),
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRecovery,
});
auto const prevVersion = ct.mpt.getMPTokenVersion(bob);
auto const prevSpending =
ct.mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending);
auto const prevInbox = ct.mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedInbox);
auto const prevKey = ct.mpt.getPubKey(bob);
ct.mpt.holderKeyUpdate({
.account = bob,
.flags = tfCancelRecovery,
});
auto const sleMptoken = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id()));
if (!BEAST_EXPECT(sleMptoken))
return;
BEAST_EXPECT(!sleMptoken->isFieldPresent(sfRecoveryKey));
BEAST_EXPECT(
prevKey && strHex((*sleMptoken)[sfHolderEncryptionKey]) == strHex(*prevKey));
BEAST_EXPECT(ct.mpt.getMPTokenVersion(bob) == prevVersion);
BEAST_EXPECT(
ct.mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) ==
prevSpending);
BEAST_EXPECT(
ct.mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedInbox) == prevInbox);
}
// Rotation mode clears a pending recovery key.
{
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
ConfidentialEnv ct{
env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
Account const bobRecoveryKey("bobRecoveryKey");
ct.mpt.generateKeyPair(bobRecoveryKey);
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = ct.mpt.getPubKey(bobRecoveryKey),
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRecovery,
});
{
auto const sleMptoken = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id()));
if (!BEAST_EXPECT(sleMptoken))
return;
BEAST_EXPECT(sleMptoken->isFieldPresent(sfRecoveryKey));
}
Account const bobNewKey("bobNewKey");
ct.mpt.generateKeyPair(bobNewKey);
auto const reEnc = reencryptHolderBalances(ct.mpt, bob, bob, bobNewKey);
BEAST_EXPECT(reEnc.has_value());
if (!reEnc)
return;
ct.mpt.holderKeyUpdate({
.account = bob,
.holderPubKey = ct.mpt.getPubKey(bobNewKey),
.spendingCiphertext = reEnc->first,
.inboxCiphertext = reEnc->second,
.proof = gMakeZeroBuffer(1),
.flags = tfHolderKeyRotation,
});
auto const sleMptoken = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id()));
if (!BEAST_EXPECT(sleMptoken))
return;
BEAST_EXPECT(!sleMptoken->isFieldPresent(sfRecoveryKey));
}
}
public:
void
testMPTokenIssuanceSetWithFeats(FeatureBitset features)
@@ -2490,6 +3189,12 @@ public:
testConfidentialMPTMirrorUpdateDoApply(all);
testConfidentialMPTMirrorUpdateMultipleRotationsIssuerMode(all);
testConfidentialMPTMirrorUpdateMultipleRotationsHolderMode(all);
testConfidentialMPTHolderKeyUpdatePreflight(all);
testConfidentialMPTHolderKeyUpdatePreflight(all - featureConfidentialMPTKeyRotation);
testConfidentialMPTHolderKeyUpdatePreflight(all - featureConfidentialTransfer);
testConfidentialMPTHolderKeyUpdatePreclaim(all);
testConfidentialMPTHolderKeyUpdateDoApply(all);
}
};
@@ -30,6 +30,7 @@
#include <chrono>
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
@@ -637,6 +638,104 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase
}
}
// A full AMMWithdraw must succeed even when an unrelated third party
// holds a confidential balance. Pre-fixCleanup3_5_0 the erase of the
// AMM pseudo-account's MPToken was rejected in this case, permanently
// stranding the last LP's position.
void
testAMMWithdrawAllBlockedByUnrelatedCOA(FeatureBitset features)
{
testcase("Full AMMWithdraw with an unrelated holder's COA");
using namespace test::jtx;
Account const alice("alice");
Account const bob("bob");
Account const carol("carol");
for (bool const withFix : {true, false})
{
for (bool const carolConverts : {false, true})
{
Env env{*this, withFix ? features | fixCleanup3_5_0 : features - fixCleanup3_5_0};
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
mptAlice.create({.flags = kMptDexFlags | tfMPTCanHoldConfidentialBalance});
mptAlice.authorize({.account = bob});
mptAlice.authorize({.account = carol});
mptAlice.pay(alice, bob, 1'000);
mptAlice.pay(alice, carol, 100);
mptAlice.generateKeyPair(alice);
mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
// bob is the sole LP, so withdrawing all of his LPTokens zeroes
// LPTokenBalance and deletes the AMM in that same transaction.
AMM amm(env, bob, XRP(100), mptAlice(100));
Account const ammHolder("amm", amm.ammAccount());
BEAST_EXPECT(amm.ammExists());
BEAST_EXPECT(mptAlice.getBalance(ammHolder) == 100);
// The AMM pseudo-account cannot initialize confidential state:
// confidential transactions must be signed by sfAccount and it
// has no signing key.
BEAST_EXPECT(
!mptAlice.getEncryptedBalance(ammHolder, MPTTester::holderEncryptedInbox));
BEAST_EXPECT(
!mptAlice.getEncryptedBalance(ammHolder, MPTTester::holderEncryptedSpending));
BEAST_EXPECT(
!mptAlice.getEncryptedBalance(ammHolder, MPTTester::issuerEncryptedBalance));
BEAST_EXPECT(
!mptAlice.getEncryptedBalance(ammHolder, MPTTester::auditorEncryptedBalance));
if (carolConverts)
{
mptAlice.generateKeyPair(carol);
mptAlice.convert(
{.account = carol, .amt = 1, .holderPubKey = mptAlice.getPubKey(carol)});
}
BEAST_EXPECT(mptAlice.getIssuanceConfidentialBalance() == (carolConverts ? 1 : 0));
if (carolConverts && !withFix)
{
amm.withdrawAll(bob, std::nullopt, Ter(tecINVARIANT_FAILED));
env.close();
// bob cannot close his position: the pool, his LPTokens and
// the AMM object all survive the failed withdrawal.
BEAST_EXPECT(amm.ammExists());
BEAST_EXPECT(mptAlice.getBalance(ammHolder) == 100);
// A partial withdrawal still succeeds, because it never
// erases the MPToken -- bob can drain the pool down to a
// residual amount but can never close his position. Withdraw
// half of his actual LPToken balance; a fixed token count
// would round the MPT side of the pool to zero and be
// rejected as a one-sided withdrawal.
auto const bobLPTokens = amm.getLPTokensBalance(bob.id());
amm.withdraw(
bob, IOUAmount{bobLPTokens.mantissa() / 2, bobLPTokens.exponent()});
env.close();
BEAST_EXPECT(amm.ammExists());
BEAST_EXPECT(mptAlice.getBalance(ammHolder) > 0);
}
else
{
// With the fix, carol's confidential balance is irrelevant to
// an AMM she has no stake in: bob closes his position and the
// AMM and its MPToken are erased.
amm.withdrawAll(bob);
env.close();
BEAST_EXPECT(!amm.ammExists());
BEAST_EXPECT(
env.le(keylet::mptoken(mptAlice.issuanceID(), amm.ammAccount())) ==
nullptr);
}
}
}
}
// Exercises every Confidential Transfer transaction type (MPTokenIssuanceSet,
// Convert, MergeInbox, Send, ConvertBack) using tickets instead of regular account
// sequence numbers.
@@ -2561,6 +2660,7 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase
// AMM/pseudo-account interaction.
testAMMHolderCannotHaveConfidentialStateClawback(features);
testAMMWithdrawAllBlockedByUnrelatedCOA(features);
// Ticket interactions.
testWithTickets(features);
+2 -1
View File
@@ -1745,7 +1745,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 -3
View File
@@ -901,8 +901,7 @@ struct Credentials_test : public beast::unit_test::Suite
{
testcase("deleteSLE fail, bad SLE.");
auto view =
std::make_shared<ApplyViewImpl>(env.current().get(), ApplyFlags::TapNone);
auto view = std::make_shared<ApplyViewImpl>(env.current().get(), ApplyFlags::None);
auto ter = xrpl::credentials::deleteSLE(*view, {}, env.journal);
BEAST_EXPECT(ter == tecNO_ENTRY);
}
@@ -1125,7 +1124,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 sleIssuer = av.peek(keylet::account(issuer.id()));
+7 -7
View File
@@ -573,8 +573,8 @@ struct EscrowToken_test : public beast::unit_test::Suite
env(pay(gw, bob, usd(1)));
env.close();
bool const largeMantissa =
features[featureSingleAssetVault] || features[featureLendingProtocol];
bool const largeMantissa = features[featureSingleAssetVault] ||
features[featureLendingProtocol] || features[featureMPTokensV2];
// alice cannot create escrow for 1/10 iou - precision loss
env(escrow::create(alice, bob, usd(1)),
@@ -2294,8 +2294,8 @@ struct EscrowToken_test : public beast::unit_test::Suite
env(pay(gw, bob, usd(1)));
env.close();
bool const largeMantissa =
features[featureSingleAssetVault] || features[featureLendingProtocol];
bool const largeMantissa = features[featureSingleAssetVault] ||
features[featureLendingProtocol] || features[featureMPTokensV2];
// alice cannot create escrow for 1/10 iou - precision loss
env(escrow::create(alice, bob, usd(1)),
@@ -2795,7 +2795,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 sleNew =
std::make_shared<SLE>(keylet::escrow(alice, SeqProxy::rawSequence(seq1)));
MPTIssue const mpt{MPTIssue{makeMptID(1, AccountID(0x4985601))}};
@@ -3023,7 +3023,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 sleNew =
std::make_shared<SLE>(keylet::escrow(alice, SeqProxy::rawSequence(seq1)));
MPTIssue const mpt{MPTIssue{makeMptID(1, AccountID(0x4985601))}};
@@ -4334,7 +4334,7 @@ public:
using namespace test::jtx;
FeatureBitset const all{testableAmendments()};
for (FeatureBitset const& feats :
{all - featureSingleAssetVault - featureLendingProtocol, all})
{all - featureSingleAssetVault - featureLendingProtocol - featureMPTokensV2, all})
{
testIOUWithFeats(feats);
testIOUWithFeats(feats - fixCleanup3_2_0);
+6 -6
View File
@@ -1302,8 +1302,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));
@@ -1312,8 +1312,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));
@@ -1322,8 +1322,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));
+1 -1
View File
@@ -134,7 +134,7 @@ createInvalidFeeTx(
bool
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 isTesSuccess(res.ter);
}
+2 -2
View File
@@ -333,7 +333,7 @@ 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 ipe = [](Asset const& asset) {
return STPathElement(
@@ -371,7 +371,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 ok = sb.peek(keylet::offer(o)))
+2 -2
View File
@@ -476,7 +476,7 @@ 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 ipe = [](Issue const& iss) {
return STPathElement(
@@ -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 ok = sb.peek(keylet::offer(o)))
+1 -1
View File
@@ -59,7 +59,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 -2
View File
@@ -90,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);
}
@@ -122,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);
}
+1 -1
View File
@@ -4059,7 +4059,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
@@ -776,7 +776,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 lastNFTokenPage = ac.view().peek(keylet::nftokenPageMax(alice));
@@ -808,7 +814,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 lastNFTokenPage = ac.view().peek(keylet::nftokenPageMax(alice));
+24
View File
@@ -1404,6 +1404,30 @@ struct PayChan_test : public beast::unit_test::Suite
rv = env.rpc("channel_authorize", "alice", chan1StrBad, "1000");
BEAST_EXPECT(rv[jss::error] == "channelMalformed");
}
{
// channel_id is not a string.
json::Value args{json::ValueType::Object};
args[jss::amount] = "2000";
args[jss::channel_id] = 2000;
args[jss::key_type] = "secp256k1";
args[jss::passphrase] = "passphrase_can_be_anything";
auto const ra =
env.rpc("json", "channel_authorize", args.toStyledString())[jss::result];
BEAST_EXPECT(ra[jss::error] == "invalidParams");
}
{
// channel_id is not a string.
json::Value args{json::ValueType::Object};
args[jss::public_key] = chan1PkStr;
args[jss::channel_id] = 2000;
args[jss::amount] = "1000";
args[jss::signature] = sig;
auto const rv =
env.rpc("json", "channel_verify", args.toStyledString())[jss::result];
BEAST_EXPECT(rv[jss::error] == "invalidParams");
}
{
// give an ill formed base 58 public key
auto illFormedPk = chan1PkStr.substr(0, chan1PkStr.size() - 1);
+1 -1
View File
@@ -572,7 +572,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,
+3 -2
View File
@@ -768,7 +768,8 @@ struct PayStrand_test : public beast::unit_test::Suite
env,
usd,
std::nullopt,
STPath({STPathElement(0, xrpAccount(), xrpCurrency(), xrpAccount())}),
STPath({STPathElement(
STPathElement::Type::TypeNone, xrpAccount(), xrpCurrency(), xrpAccount())}),
temBAD_PATH);
// The same account can't appear more than once on a path
@@ -1076,7 +1077,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,
Loaded 100 of 163 files, more files were not shown because too many files have changed in this diff. Show more