mirror of
https://github.com/XRPLF/rippled.git
synced 2026-10-21 19:18:22 +00:00
Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa50181606 | ||
|
|
74df6d278f | ||
|
|
dc55084ee0 | ||
|
|
3b9e8e19f0 | ||
|
|
079c3b43dd | ||
|
|
962767a9b3 | ||
|
|
4aea1e20ef | ||
|
|
206514a024 | ||
|
|
0633d5cd6b | ||
|
|
6c29093de0 | ||
|
|
16187ab684 | ||
|
|
28f83b5bcf | ||
|
|
67271ddc01 | ||
|
|
2f9e1b1716 | ||
|
|
d7988a2579 | ||
|
|
cf7d48d117 | ||
|
|
495374869a |
No files matched your search
@@ -8,7 +8,6 @@ on:
|
||||
branches:
|
||||
- "develop"
|
||||
- "release/*"
|
||||
- "staging/*"
|
||||
paths:
|
||||
- "crates/**/Cargo.toml"
|
||||
- "crates/Cargo.lock"
|
||||
|
||||
@@ -243,7 +243,7 @@ jobs:
|
||||
- should-run
|
||||
- build-test
|
||||
# Only run when committing to a PR that targets a release branch.
|
||||
if: ${{ github.repository == 'XRPLF/rippled' && needs.should-run.outputs.go == 'true' && github.event_name == 'pull_request' && startsWith(github.event.pull_request.base.ref, 'release/') }}
|
||||
if: ${{ github.repository == 'XRPLF/rippled' && needs.should-run.outputs.go == 'true' && github.event_name == 'pull_request' && startsWith(github.event.pull_request.base.ref, 'release') }}
|
||||
uses: ./.github/workflows/reusable-upload-recipe.yml
|
||||
secrets:
|
||||
remote_username: ${{ secrets.NEXUS_REMOTE_USERNAME }}
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
# This workflow runs all workflows to build and test the code on various Linux
|
||||
# flavors, as well as on MacOS and Windows, on a scheduled basis, on merge into
|
||||
# the 'develop', 'release/*' or 'staging/*' branches, or when requested
|
||||
# manually. Upon pushes to the develop branch, it also uploads the libxrpl
|
||||
# recipe to the Conan remote.
|
||||
# the 'develop' or 'release*' branches, or when requested manually. Upon pushes
|
||||
# to the develop branch it also uploads the libxrpl recipe to the Conan remote.
|
||||
name: Trigger
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "develop"
|
||||
- "release/*"
|
||||
- "staging/*"
|
||||
- "release*"
|
||||
paths:
|
||||
# These paths are unique to `on-trigger.yml`.
|
||||
- ".github/workflows/on-trigger.yml"
|
||||
@@ -64,9 +62,9 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
# When a PR is merged into the develop branch, it will be assigned a unique
|
||||
# When a PR is merged into the develop branch it will be assigned a unique
|
||||
# group identifier, so execution will continue even if another PR is merged
|
||||
# while it is still running. In all other cases, the group identifier is shared
|
||||
# while it is still running. In all other cases the group identifier is shared
|
||||
# per branch, so that any in-progress runs are cancelled when a new commit is
|
||||
# pushed.
|
||||
group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' && github.sha || github.ref }}
|
||||
@@ -100,7 +98,7 @@ jobs:
|
||||
# However, we do not enable ccache for events targeting a release branch,
|
||||
# to protect against the rare case that the output produced by ccache is
|
||||
# not identical to a regular compilation.
|
||||
ccache_enabled: ${{ github.repository_owner == 'XRPLF' && !startsWith(github.ref, 'refs/heads/release/') }}
|
||||
ccache_enabled: ${{ github.repository_owner == 'XRPLF' && !startsWith(github.ref, 'refs/heads/release') }}
|
||||
os: ${{ matrix.os }}
|
||||
secrets:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
@@ -8,8 +8,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- "develop"
|
||||
- "release/*"
|
||||
- "staging/*"
|
||||
- "release*"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -37,7 +37,6 @@ Version 3.5.0 is not yet released.
|
||||
- `channel_verify`: The `channel_id` and `signature` fields now return an `invalidParams` error if the value is not a string. [#7582](https://github.com/XRPLF/rippled/pull/7582)
|
||||
- `account_currencies`, `account_nfts`, `account_offers`: A request with more than one problem may now report a different one of them. An invalid `ledger_hash` or `ledger_index` is reported before an invalid or missing `account`. An invalid `limit` or `marker` is reported before `lgrNotFound` or `actNotFound`, and for `account_offers` also before `actMalformed`. A request with a single problem returns the same error as before. [#8561](https://github.com/XRPLF/rippled/pull/8561)
|
||||
- `deposit_authorized`: A `credentials` entry that is all zeros (for example `"0"` or 64 `0` characters) now returns a `badCredentials` error instead of looking up a credential that can never exist.
|
||||
- `account_info`, `account_lines`, `account_channels`: A request with more than one problem may now report a different one of them. An invalid `ledger_hash` or `ledger_index` is reported before a missing or invalid `account`, or `ident` for `account_info`. For `account_lines`, a `peer` that is not a string, or an invalid `limit` or `marker`, is reported before `lgrNotFound`, `actNotFound` or a malformed `account`, and an invalid `limit` or `marker` also before a malformed `peer`. For `account_channels`, a malformed `account` is reported before `lgrNotFound`, and an invalid `destination_account`, `limit` or `marker` before `lgrNotFound` or `actNotFound`. A request with a single problem returns the same error as before. [#8573](https://github.com/XRPLF/rippled/pull/8573)
|
||||
|
||||
### Bugfixes in 3.5.0
|
||||
|
||||
|
||||
@@ -297,10 +297,7 @@ if(xrpld)
|
||||
VALUE_TYPE "::json::Value"
|
||||
VIEW_HEADER "xrpld/rpc/detail/JsonObjectView.hpp"
|
||||
HANDLERS
|
||||
account_channels
|
||||
account_currencies
|
||||
account_info
|
||||
account_lines
|
||||
account_nfts
|
||||
account_offers
|
||||
book_changes
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
"requires": [
|
||||
"zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1782392402.122708",
|
||||
"xxhash/0.8.3#681d36a0a6111fc56e5e45ea182c19cc%1782392402.420688",
|
||||
"xrpl-rpc-spec/0.1.25#2daa763e2d86ccc4ad0a0fa4d1983e9f%1791907455.579992",
|
||||
"xrpl-rpc-spec/0.1.23#56e8946f5b3fb949a3732e1a51135ca2%1790977482.197086",
|
||||
"sqlite3/3.53.0#324ada52333108388a9a6108bfa96734%1782392403.185447",
|
||||
"soci/4.0.3#e726491a03468795453f7c83fc924a96%1782392402.679521",
|
||||
"snappy/1.1.10#968fef506ff261592ec30c574d4a7809%1782307151.633168",
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ class Xrpl(ConanFile):
|
||||
"nudb/2.0.9",
|
||||
"openssl/3.6.3",
|
||||
"soci/4.0.3",
|
||||
"xrpl-rpc-spec/0.1.25",
|
||||
"xrpl-rpc-spec/0.1.23",
|
||||
"zlib/1.3.2",
|
||||
]
|
||||
|
||||
|
||||
@@ -86,12 +86,15 @@ public:
|
||||
requires std::convertible_to<TT*, T*>
|
||||
SharedIntrusive(SharedIntrusive<TT> const& rhs);
|
||||
|
||||
SharedIntrusive(SharedIntrusive&& rhs);
|
||||
// noexcept so move_if_noexcept moves rather than copies, which NodePathStack's path_ vector
|
||||
// relies on when it relocates. The body is a std::exchange on a raw pointer, which is what
|
||||
// makes the noexcept honest.
|
||||
SharedIntrusive(SharedIntrusive&& rhs) noexcept;
|
||||
|
||||
template <class TT>
|
||||
requires std::convertible_to<TT*, T*>
|
||||
SharedIntrusive(
|
||||
SharedIntrusive<TT>&& rhs); // NOLINT(cppcoreguidelines-rvalue-reference-param-not-moved)
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-rvalue-reference-param-not-moved)
|
||||
SharedIntrusive(SharedIntrusive<TT>&& rhs) noexcept;
|
||||
|
||||
SharedIntrusive&
|
||||
operator=(SharedIntrusive const& rhs);
|
||||
@@ -529,6 +532,22 @@ staticPointerCast(TT const& v)
|
||||
return SharedPtr<T>(StaticCastTagSharedIntrusive{}, v);
|
||||
}
|
||||
|
||||
/**
|
||||
* Statically cast an intrusive pointer, moving out of it.
|
||||
*
|
||||
* Spelling the parameter as `SharedIntrusive<TT>&&` binds rvalues only and
|
||||
* leaves lvalues to the `const&` overload above.
|
||||
*
|
||||
* @param v the pointer to cast, left empty afterwards.
|
||||
* @return a pointer of the requested type to the same object.
|
||||
*/
|
||||
template <class T, class TT>
|
||||
[[nodiscard]] SharedPtr<T>
|
||||
staticPointerCast(SharedIntrusive<TT>&& v)
|
||||
{
|
||||
return SharedPtr<T>(StaticCastTagSharedIntrusive{}, std::move(v));
|
||||
}
|
||||
|
||||
template <class T, class TT>
|
||||
SharedPtr<T>
|
||||
dynamicPointerCast(TT const& v)
|
||||
|
||||
@@ -47,7 +47,7 @@ SharedIntrusive<T>::SharedIntrusive(SharedIntrusive<TT> const& rhs)
|
||||
}
|
||||
|
||||
template <class T>
|
||||
SharedIntrusive<T>::SharedIntrusive(SharedIntrusive&& rhs)
|
||||
SharedIntrusive<T>::SharedIntrusive(SharedIntrusive&& rhs) noexcept
|
||||
: ptr_{std::move(rhs).unsafeExchange(nullptr)}
|
||||
{
|
||||
}
|
||||
@@ -55,7 +55,7 @@ SharedIntrusive<T>::SharedIntrusive(SharedIntrusive&& rhs)
|
||||
template <class T>
|
||||
template <class TT>
|
||||
requires std::convertible_to<TT*, T*>
|
||||
SharedIntrusive<T>::SharedIntrusive(SharedIntrusive<TT>&& rhs)
|
||||
SharedIntrusive<T>::SharedIntrusive(SharedIntrusive<TT>&& rhs) noexcept
|
||||
: ptr_{std::move(rhs).unsafeExchange(nullptr)}
|
||||
{
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/TaggedCache.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
using KeyCache = TaggedCache<UInt256, int, true>;
|
||||
/**
|
||||
* TaggedCache in key-only mode, holding no value per key.
|
||||
*
|
||||
* @tparam Key the key type to remember.
|
||||
*/
|
||||
template <class Key>
|
||||
using KeyCache = TaggedCache<Key, int, true>;
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -352,9 +352,9 @@ class Number final
|
||||
using rep = std::int64_t;
|
||||
using InternalRep = MantissaRange::rep;
|
||||
|
||||
bool negative_{false};
|
||||
InternalRep mantissa_{0};
|
||||
int exponent_{std::numeric_limits<int>::lowest()};
|
||||
bool negative_{false};
|
||||
|
||||
public:
|
||||
// The range for the exponent when normalized
|
||||
@@ -650,12 +650,8 @@ private:
|
||||
shiftExponent(int exponentDelta) const;
|
||||
};
|
||||
|
||||
// The number class currently has 3 tail padding bytes. If you are adding new
|
||||
// members, try to fit them there before expanding the size of the class.
|
||||
static_assert(sizeof(Number) == 16);
|
||||
|
||||
constexpr Number::Number(bool negative, InternalRep mantissa, int exponent, Unchecked) noexcept
|
||||
: mantissa_{mantissa}, exponent_{exponent}, negative_(negative)
|
||||
: negative_(negative), mantissa_{mantissa}, exponent_{exponent}
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -105,6 +105,16 @@ public:
|
||||
std::vector<UInt256> const& amendments,
|
||||
Family& family);
|
||||
|
||||
/**
|
||||
* Create a ledger from a header whose maps are filled in afterwards.
|
||||
*
|
||||
* Both maps start Synching against the hashes the header carries, which are
|
||||
* input rather than derived, so setImmutable() leaves them alone.
|
||||
*
|
||||
* @param info The header to build from.
|
||||
* @param rules The rules in force.
|
||||
* @param family The SHAMap family the maps belong to.
|
||||
*/
|
||||
Ledger(LedgerHeader const& info, Rules rules, Family& family);
|
||||
|
||||
/**
|
||||
@@ -278,23 +288,25 @@ public:
|
||||
/**
|
||||
* Mark this ledger as immutable, so it can no longer be modified.
|
||||
*
|
||||
* A ledger built or loaded locally cannot have an invalid map, since only
|
||||
* a map syncing against hashes from outside can be proven impossible (see
|
||||
* SHAMap::addKnownNode), so a caller on such a path may treat a false
|
||||
* return as a broken internal invariant. A caller assembling a ledger from
|
||||
* peer data may not: for it, false is an outcome a peer can produce.
|
||||
* Only a map syncing against hashes from outside can be found unsound (see
|
||||
* SHAMap::addKnownNode), so a caller whose ledger was built or loaded
|
||||
* locally treats a false return as a broken internal invariant.
|
||||
*
|
||||
* @param rehash Whether to recompute the ledger hash from the header
|
||||
* fields. The transaction and account hashes are recomputed from the
|
||||
* maps too, but only the first time.
|
||||
* maps too, but only the first time and only if the header did not
|
||||
* supply them.
|
||||
* @return false if either map is Invalid, leaving the immutable flag as it
|
||||
* was and the header untouched. A map invalidated partway through can
|
||||
* still leave the other one immutable, so a false return means the
|
||||
* ledger must be discarded rather than retried.
|
||||
* was and the header untouched. The ledger must then be discarded
|
||||
* rather than retried, since one map may already be immutable.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
setImmutable(bool rehash = true);
|
||||
|
||||
/**
|
||||
* @return Whether both maps have been settled, so the ledger can no longer
|
||||
* change.
|
||||
*/
|
||||
bool
|
||||
isImmutable() const
|
||||
{
|
||||
@@ -470,6 +482,15 @@ private:
|
||||
|
||||
bool immutable_;
|
||||
|
||||
/**
|
||||
* Whether the header supplied the transaction and account hashes, so they
|
||||
* must not be derived from the maps.
|
||||
*
|
||||
* True only for a ledger built from a header, which stays verified against
|
||||
* the hash it was asked for. Fixed at construction, unlike immutable_.
|
||||
*/
|
||||
bool const mapHashesFromHeader_ = false;
|
||||
|
||||
// A SHAMap containing the transactions associated with this ledger.
|
||||
SHAMap mutable txMap_;
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Concepts.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Fees.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
@@ -28,8 +27,6 @@
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
@@ -563,54 +560,4 @@ escrowUnlockApplyHelper<MPTIssue>(
|
||||
journal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Smart escrow kill switches, driven by the voted FeeSettings.
|
||||
*
|
||||
* Zeroing `bytecodeSizeLimit` stops new uploads while leaving existing escrows
|
||||
* finishable without forcing holders to wait for `CancelAfter`.
|
||||
* Zeroing `gasLimit` stops both.
|
||||
*/
|
||||
/** @{ */
|
||||
inline bool
|
||||
isBytecodeUploadDisabled(Fees const& fees)
|
||||
{
|
||||
return fees.bytecodeSizeLimit == 0 || fees.gasLimit == 0;
|
||||
}
|
||||
|
||||
inline bool
|
||||
isBytecodeExecutionDisabled(Fees const& fees)
|
||||
{
|
||||
return fees.gasLimit == 0;
|
||||
}
|
||||
/** @} */
|
||||
|
||||
/**
|
||||
* Owner count an escrow costs.
|
||||
*
|
||||
* An escrow costs one increment per started 500 bytes of bytecode, and one
|
||||
* increment if it carries no bytecode.
|
||||
*/
|
||||
template <class T>
|
||||
int32_t
|
||||
calculateAdditionalReserve(T const& finishFunction)
|
||||
{
|
||||
static auto constexpr kBytecodeReserveIncrement = 500;
|
||||
|
||||
if (!finishFunction)
|
||||
return 1;
|
||||
|
||||
auto const size = finishFunction->size();
|
||||
if (size == 0)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
// Empty Bytecode is refused at create, so no escrow carries it. Ceiling
|
||||
// division would answer 0, which would subtract less than the create added.
|
||||
UNREACHABLE("xrpl::calculateAdditionalReserve : empty Bytecode");
|
||||
return 1;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
return static_cast<int32_t>((size + kBytecodeReserveIncrement - 1) / kBytecodeReserveIncrement);
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -398,11 +398,6 @@ constexpr std::uint32_t kMaxInvestmentPeriod = std::chrono::seconds{std::chrono:
|
||||
*/
|
||||
constexpr std::uint8_t kMaxAssetCheckDepth = 5;
|
||||
|
||||
/**
|
||||
* Maximum length of a Data field in Escrow object that can be updated by WASM code.
|
||||
*/
|
||||
constexpr std::size_t kMaxWasmDataLength = 1 * 1024; // 1KB
|
||||
|
||||
/**
|
||||
* A ledger index.
|
||||
*/
|
||||
|
||||
@@ -133,6 +133,7 @@ enum TEMcodes : TERUnderlyingType {
|
||||
temBAD_MPT,
|
||||
temBAD_CIPHERTEXT,
|
||||
temINVALID_BYTECODE,
|
||||
temTEMP_DISABLED,
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -183,8 +184,6 @@ enum TEFcodes : TERUnderlyingType {
|
||||
tefBAD_PATH_COUNT,
|
||||
tefNO_BYTECODE,
|
||||
tefBYTECODE_NOT_INCLUDED,
|
||||
tefTEMP_DISABLED,
|
||||
tefEXCEEDS_LIMIT,
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
#include <xrpl/basics/KeyCache.h>
|
||||
#include <xrpl/basics/TaggedCache.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/partitioned_unordered_map.h>
|
||||
#include <xrpl/beast/hash/hash_append.h>
|
||||
#include <xrpl/beast/insight/Collector.h>
|
||||
#include <xrpl/beast/insight/NullCollector.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/shamap/SHAMapNodeID.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
@@ -15,6 +18,112 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
* Names a subtree whose descendants are all resident, by hash and by position.
|
||||
*
|
||||
* A node's hash covers its children but neither its depth nor its path, and one
|
||||
* cache answers for every map built from a NodeFamily, so the position is what
|
||||
* makes a hit answer for the position asked about and no other. The depth is
|
||||
* kept beside the id because SHAMapNodeID masks its id to its own depth, so an
|
||||
* all-zero id belongs to the root and to every all-zero prefix alike.
|
||||
*
|
||||
* Held as two fields rather than as a SHAMapNodeID, which is a CountedObject:
|
||||
* one per entry would report the cache's population as live node IDs in the
|
||||
* get_counts RPC, and would put an atomic increment on every temporary key a
|
||||
* lookup builds.
|
||||
*/
|
||||
struct FullBelowKey
|
||||
{
|
||||
/**
|
||||
* The hash of the subtree.
|
||||
*/
|
||||
UInt256 hash;
|
||||
|
||||
/**
|
||||
* The masked id of the node the subtree was completed at.
|
||||
*/
|
||||
UInt256 position;
|
||||
|
||||
/**
|
||||
* The depth of that node.
|
||||
*/
|
||||
std::uint32_t depth{0};
|
||||
|
||||
/**
|
||||
* @param subtreeHash The hash of the subtree whose descendants are all
|
||||
* resident.
|
||||
* @param nodeID The node the subtree was completed at.
|
||||
*/
|
||||
FullBelowKey(UInt256 const& subtreeHash, SHAMapNodeID const& nodeID)
|
||||
: hash(subtreeHash)
|
||||
, position(nodeID.getNodeID())
|
||||
, depth(static_cast<std::uint32_t>(nodeID.getDepth()))
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* The same key, named by the node above the position and the branch taken.
|
||||
*
|
||||
* Produces the fields the constructor above would for that child's own node
|
||||
* ID, so the two forms name one entry.
|
||||
*
|
||||
* @param subtreeHash The hash of the subtree that is fully resident.
|
||||
* @param parentID The node above the one the subtree was completed at.
|
||||
* @param branch The branch of that node leading to it.
|
||||
*/
|
||||
FullBelowKey(UInt256 const& subtreeHash, SHAMapNodeID const& parentID, unsigned int branch)
|
||||
: hash(subtreeHash)
|
||||
, position(childNodeID(parentID.getNodeID(), parentID.getDepth(), branch))
|
||||
, depth(static_cast<std::uint32_t>(parentID.getDepth() + 1))
|
||||
{
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
operator==(FullBelowKey const& other) const = default;
|
||||
};
|
||||
|
||||
// The per-entry cost of this cache is computed from these sizes, so pin the layout that arithmetic
|
||||
// assumes: the three fields sit back to back with no padding.
|
||||
static_assert(
|
||||
sizeof(FullBelowKey) == (2 * sizeof(UInt256)) + sizeof(std::uint32_t),
|
||||
"FullBelowKey must stay free of padding");
|
||||
static_assert(
|
||||
alignof(FullBelowKey) == alignof(std::uint32_t),
|
||||
"FullBelowKey's lack of padding rests on its alignment");
|
||||
|
||||
/**
|
||||
* Feed a key to a hasher, field by field.
|
||||
*
|
||||
* Written out rather than hashed in one call over the object's bytes, since
|
||||
* beast::IsUniquelyRepresented is opt-in per type and has no case for a
|
||||
* user-defined struct, whatever its layout.
|
||||
*
|
||||
* @param h The hasher to feed.
|
||||
* @param key The key to hash.
|
||||
*/
|
||||
template <class Hasher>
|
||||
void
|
||||
hash_append(Hasher& h, FullBelowKey const& key) noexcept
|
||||
{
|
||||
using beast::hash_append;
|
||||
hash_append(h, key.hash, key.position, key.depth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Choose which partition of the cache's map a key lives in.
|
||||
*
|
||||
* Taken from the subtree hash, which is already uniformly distributed.
|
||||
*
|
||||
* @param key The key to place.
|
||||
* @return The value PartitionedUnorderedMap reduces by its partition count.
|
||||
*/
|
||||
template <>
|
||||
inline std::size_t
|
||||
extract(FullBelowKey const& key)
|
||||
{
|
||||
return extract(key.hash);
|
||||
}
|
||||
|
||||
namespace detail {
|
||||
|
||||
/**
|
||||
@@ -24,12 +133,12 @@ namespace detail {
|
||||
class BasicFullBelowCache
|
||||
{
|
||||
private:
|
||||
using CacheType = KeyCache;
|
||||
using CacheType = KeyCache<FullBelowKey>;
|
||||
|
||||
public:
|
||||
static constexpr auto kDefaultCacheTargetSize = 0;
|
||||
|
||||
using key_type = UInt256;
|
||||
using key_type = FullBelowKey;
|
||||
using ClockType = CacheType::ClockType;
|
||||
|
||||
/**
|
||||
@@ -86,13 +195,15 @@ public:
|
||||
* Refresh the last access time of an item, if it exists.
|
||||
* Thread safety:
|
||||
* Safe to call from any thread.
|
||||
* @param key The key to refresh.
|
||||
* @return `true` If the key exists.
|
||||
* @param hash The hash of the subtree to ask about.
|
||||
* @param parentID The node above the position to ask about it at.
|
||||
* @param branch The branch of that node leading to that position.
|
||||
* @return `true` If that subtree is recorded as resident at that position.
|
||||
*/
|
||||
bool
|
||||
touchIfExists(key_type const& key)
|
||||
touchIfExists(UInt256 const& hash, SHAMapNodeID const& parentID, unsigned int branch)
|
||||
{
|
||||
return cache_.touchIfExists(key);
|
||||
return cache_.touchIfExists(key_type{hash, parentID, branch});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -101,12 +212,13 @@ public:
|
||||
* be refreshed.
|
||||
* Thread safety:
|
||||
* Safe to call from any thread.
|
||||
* @param key The key to insert.
|
||||
* @param hash The hash of the subtree whose descendants are all resident.
|
||||
* @param nodeID The node that subtree was completed at.
|
||||
*/
|
||||
void
|
||||
insert(key_type const& key)
|
||||
insert(UInt256 const& hash, SHAMapNodeID const& nodeID)
|
||||
{
|
||||
cache_.insert(key);
|
||||
cache_.insert(key_type{hash, nodeID});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -278,11 +278,14 @@ This cache remembers which trie keys have all of their children resident in a
|
||||
when creating the missing nodes list. Missing nodes are those nodes that a
|
||||
`SHAMap` refers to but that are not stored in the local database.
|
||||
|
||||
As a depth-first walk of a `SHAMap` is performed, if an inner node answers true to
|
||||
`isFullBelow()` then it is known that none of this node's children are missing
|
||||
nodes, and thus that subtree does not need to be walked. These nodes are stored
|
||||
in the FullBelowCache. Subsequent walks check the FullBelowCache first when
|
||||
encountering a node, and ignore that subtree if found.
|
||||
As a depth-first walk of a `SHAMap` completes an inner node, meaning none of the
|
||||
nodes below it are missing, the node's hash and position are stored in the
|
||||
FullBelowCache. Subsequent walks check the FullBelowCache when encountering a
|
||||
child, and skip that subtree if found. The entry is keyed by position as well as
|
||||
hash, since a hash commits to a node's contents rather than its place, and the
|
||||
same node object is shared by hash between maps and positions. For the same
|
||||
reason the inner node's own `isFullBelow()` flag is written and read for a
|
||||
walk's root alone, where the position is fixed.
|
||||
|
||||
## `SHAMapTreeNode`
|
||||
|
||||
|
||||
+392
-113
@@ -32,6 +32,7 @@
|
||||
#include <set>
|
||||
#include <stack>
|
||||
#include <tuple>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
@@ -73,20 +74,21 @@ enum class SHAMapState : std::uint8_t {
|
||||
};
|
||||
|
||||
/**
|
||||
* A SHAMap is both a radix tree with a fan-out of 16 and a Merkle tree.
|
||||
* A SHAMap is both a trie with a fan-out of 16 and a Merkle tree.
|
||||
*
|
||||
* A radix tree is a tree with two properties:
|
||||
* A trie holds a key in the position of its nodes: the path from the root down
|
||||
* to a node spells out the prefix every key below it shares (the "prefix
|
||||
* property"). A SHAMap spends one nibble of the 256-bit key per level, so an
|
||||
* inner node has at most 16 children and a leaf sits at depth 64 at the
|
||||
* deepest. A leaf also carries its own full key, so a reader can check that it
|
||||
* was reached through the branches that key names.
|
||||
*
|
||||
* 1. The key for a node is represented by the node's position in the tree
|
||||
* (the "prefix property").
|
||||
* 2. A node with only one child is merged with that child
|
||||
* (the "merge property")
|
||||
* An insert creates an inner node at every nibble two keys share, and a delete
|
||||
* collapses a chain that reduces to one leaf. Every edge spans exactly one
|
||||
* nibble, so a path has one entry per level and 65 entries at the most, and a
|
||||
* path's length names each node's depth. Traversal relies on that.
|
||||
*
|
||||
* These properties result in a significantly smaller memory footprint for
|
||||
* a radix tree.
|
||||
*
|
||||
* A fan-out of 16 means that each node in the tree has at most 16
|
||||
* children. See https://en.wikipedia.org/wiki/Radix_tree
|
||||
* See https://en.wikipedia.org/wiki/Trie
|
||||
*
|
||||
* A Merkle tree is a tree where each non-leaf node is labelled with the hash
|
||||
* of the combined labels of its children nodes.
|
||||
@@ -131,21 +133,18 @@ private:
|
||||
/**
|
||||
* The sequence of the ledger that this map references, if any.
|
||||
*
|
||||
* Written when a map's ledger sequence is established (Ledger::setFull(),
|
||||
* InboundLedger) while a nodestore reader thread reads it. Relaxed either
|
||||
* way: it serves as a lookup hint for a nodestore keyed by hash.
|
||||
* Written by Ledger::setFull() and by InboundLedger, and read on a
|
||||
* nodestore reader thread. Relaxed both ways, since it is only a lookup
|
||||
* hint for a store keyed by hash.
|
||||
*/
|
||||
std::atomic<std::uint32_t> ledgerSeq_ = 0;
|
||||
|
||||
SHAMapTreeNodePtr root_;
|
||||
|
||||
/**
|
||||
* The map's state.
|
||||
*
|
||||
* A getMissingNodes() walk writes it, through setInvalid() and
|
||||
* clearSynching(), while whatever drives the acquisition reads it.
|
||||
* The walk runs with the acquisition's lock released, so this is atomic
|
||||
* rather than guarded.
|
||||
* The map's state. Atomic because a getMissingNodes() walk writes it with
|
||||
* the acquisition's lock released. Mutable because a const descend() can
|
||||
* record the Invalid verdict.
|
||||
*/
|
||||
mutable std::atomic<SHAMapState> state_;
|
||||
SHAMapType const type_;
|
||||
@@ -161,8 +160,7 @@ private:
|
||||
|
||||
public:
|
||||
/**
|
||||
* Number of children each non-leaf node has (the 'radix tree' part of the
|
||||
* map)
|
||||
* Number of children each non-leaf node has, which is the trie's fan-out
|
||||
*/
|
||||
static constexpr unsigned int kBranchFactor = SHAMapInnerNode::kBranchFactor;
|
||||
|
||||
@@ -171,6 +169,18 @@ public:
|
||||
*/
|
||||
static constexpr unsigned int kLeafDepth = 64;
|
||||
|
||||
/**
|
||||
* Whether only a leaf may occupy a position at `depth`.
|
||||
*
|
||||
* @param depth the depth to judge.
|
||||
* @return whether that depth is at or past kLeafDepth.
|
||||
*/
|
||||
[[nodiscard]] static constexpr bool
|
||||
isLeafDepth(unsigned int depth)
|
||||
{
|
||||
return depth >= kLeafDepth;
|
||||
}
|
||||
|
||||
using DeltaItem =
|
||||
std::pair<boost::intrusive_ptr<SHAMapItem const>, boost::intrusive_ptr<SHAMapItem const>>;
|
||||
using Delta = std::map<UInt256, DeltaItem>;
|
||||
@@ -294,9 +304,11 @@ public:
|
||||
/**
|
||||
* Find the first item after the given item.
|
||||
*
|
||||
* @param id the identifier of the item.
|
||||
*
|
||||
* @note The item does not need to exist.
|
||||
* @param id the identifier of the item, which need not exist in the map.
|
||||
* @return an iterator at the first item with a greater key, or end() if the
|
||||
* map holds no greater key.
|
||||
* @throws SHAMapMissingNode if the map cannot be walked. end() is the
|
||||
* separate answer that the map holds no greater key.
|
||||
*/
|
||||
ConstIterator
|
||||
upperBound(UInt256 const& id) const;
|
||||
@@ -304,9 +316,11 @@ public:
|
||||
/**
|
||||
* Find the object with the greatest object id smaller than the input id.
|
||||
*
|
||||
* @param id the identifier of the item.
|
||||
*
|
||||
* @note The item does not need to exist.
|
||||
* @param id the identifier of the item, which need not exist in the map.
|
||||
* @return an iterator at the last item with a smaller key, or end() if the
|
||||
* map holds no smaller key.
|
||||
* @throws SHAMapMissingNode if the map cannot be walked. end() is the
|
||||
* separate answer that the map holds no smaller key.
|
||||
*/
|
||||
ConstIterator
|
||||
lowerBound(UInt256 const& id) const;
|
||||
@@ -419,17 +433,25 @@ public:
|
||||
* deserialized.
|
||||
*
|
||||
* A node that no valid tree can hold makes the map Invalid, which is
|
||||
* terminal: the root hash committed to an impossible shape, so no peer
|
||||
* can satisfy it.
|
||||
* terminal. An acquisition reaching that verdict gives up.
|
||||
*
|
||||
* @param nodeID The position in the tree where this node belongs.
|
||||
* @param treeNode A deserialized tree node to add.
|
||||
* @param filter Optional sync filter to track received nodes.
|
||||
* @return Status indicating whether the node was useful, duplicate, or invalid.
|
||||
*
|
||||
* @note This function expects the treeNode to be a valid, deserialized SHAMapTreeNode. The
|
||||
* caller is responsible for deserialization and basic validation before calling this
|
||||
* function. This also means that the nodeID must be consistent with the node's content.
|
||||
* @note The caller is responsible for deserialization. The position is
|
||||
* checked here, in three steps that share two verdicts. A node
|
||||
* the descent reaches at a position other than the one nodeID
|
||||
* claims is refused with invalid(), and the map stays valid, since
|
||||
* only the label was wrong. A leaf that hash-verified at the
|
||||
* position nodeID claims, but whose own key does not lie under
|
||||
* nodeID, condemns the map and returns mapInvalidated(). A node the
|
||||
* descent itself condemns on the way, resolved from the local store
|
||||
* or the filter rather than supplied by the caller, leaves the map
|
||||
* Invalid and returns invalid(), since that verdict is not the
|
||||
* caller's doing. Any map outside Synching, including one already
|
||||
* condemned, returns duplicate().
|
||||
*/
|
||||
SHAMapAddNode
|
||||
addKnownNode(
|
||||
@@ -524,104 +546,299 @@ public:
|
||||
|
||||
private:
|
||||
/**
|
||||
* A path from the root of the map down to some node, pairing each node with the ID naming its
|
||||
* position.
|
||||
* Whether placing `node` one level below `parentDepth` leaves it no room.
|
||||
*
|
||||
* The two halves of an entry must agree, and the only way to get that wrong is to compute an ID
|
||||
* from the wrong branch. So this type does not accept an ID at all: every push takes the branch
|
||||
* being descended and derives the ID itself, so a node and its ID cannot disagree. Reads are
|
||||
* exposed through the same accessors a std::stack would offer.
|
||||
* Only a leaf may sit at kLeafDepth, and only a parent above kLeafDepth has
|
||||
* a nibble left to record the branch taken. Both bounds are needed, since a
|
||||
* leaf one level above kLeafDepth is allowed. Position is judged separately,
|
||||
* by the walk that keeps a path.
|
||||
*
|
||||
* The conjunct order is load bearing: the depth is tested first, so the
|
||||
* virtual isInner() call runs only at the one level where the bound
|
||||
* applies.
|
||||
*
|
||||
* @param parentDepth the depth of the node being descended from.
|
||||
* @param node the node about to be placed one level below it.
|
||||
* @return whether that placement is past the deepest level this kind of
|
||||
* node may occupy.
|
||||
*/
|
||||
[[nodiscard]] static bool
|
||||
pastLeafDepth(unsigned int parentDepth, SHAMapTreeNode const& node)
|
||||
{
|
||||
return isLeafDepth(parentDepth + 1u) && (node.isInner() || isLeafDepth(parentDepth));
|
||||
}
|
||||
|
||||
/**
|
||||
* A root-down path of nodes through the map.
|
||||
*
|
||||
* Entry `i` is the node at depth `i`, since every edge spans one nibble
|
||||
* (see the class docstring). Only the node is stored. A caller that needs a
|
||||
* position reads a depth and takes the nibbles from its own key.
|
||||
*/
|
||||
class NodePathStack
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @return whether the path holds no node at all.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
empty() const
|
||||
{
|
||||
return stack_.empty();
|
||||
return path_.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return how many nodes the path holds, one more than the last node's
|
||||
* depth.
|
||||
*/
|
||||
[[nodiscard]] std::size_t
|
||||
size() const
|
||||
{
|
||||
return stack_.size();
|
||||
return path_.size();
|
||||
}
|
||||
|
||||
[[nodiscard]] std::pair<SHAMapTreeNodePtr, SHAMapNodeID> const&
|
||||
/**
|
||||
* The node at the end of the path.
|
||||
*
|
||||
* @return a reference into the path, which a later push may invalidate
|
||||
* by reallocating, so a caller that pushes must ask again. An
|
||||
* empty path yields a null node the caller can test.
|
||||
*/
|
||||
[[nodiscard]] SHAMapTreeNodePtr const&
|
||||
top() const
|
||||
{
|
||||
XRPL_ASSERT(!stack_.empty(), "xrpl::SHAMap::NodePathStack::top : non-empty stack");
|
||||
return stack_.top();
|
||||
if (path_.empty())
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::SHAMap::NodePathStack::top : empty stack");
|
||||
static SHAMapTreeNodePtr const kEmpty;
|
||||
return kEmpty;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
return path_.back();
|
||||
}
|
||||
|
||||
/**
|
||||
* The depth of the node at the end of the path.
|
||||
*
|
||||
* @return the depth, which is the entry's own index. Zero on an empty
|
||||
* path.
|
||||
*/
|
||||
[[nodiscard]] unsigned int
|
||||
topDepth() const
|
||||
{
|
||||
if (path_.empty())
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::SHAMap::NodePathStack::topDepth : empty stack");
|
||||
return 0;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
return static_cast<unsigned int>(path_.size() - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shorten the path by one node. An empty path is left as it is.
|
||||
*/
|
||||
void
|
||||
pop()
|
||||
{
|
||||
XRPL_ASSERT(!stack_.empty(), "xrpl::SHAMap::NodePathStack::pop : non-empty stack");
|
||||
stack_.pop();
|
||||
if (path_.empty())
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::SHAMap::NodePathStack::pop : empty stack");
|
||||
return;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
path_.pop_back();
|
||||
}
|
||||
|
||||
/**
|
||||
* Discard the whole path.
|
||||
*/
|
||||
void
|
||||
clear()
|
||||
{
|
||||
stack_ = {};
|
||||
path_.clear();
|
||||
pathKey_ = UInt256{};
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a path at the root of the map, whose ID is the zero-depth ID by definition.
|
||||
*/
|
||||
void
|
||||
pushRoot(SHAMapTreeNodePtr node)
|
||||
{
|
||||
XRPL_ASSERT(stack_.empty(), "xrpl::SHAMap::NodePathStack::pushRoot : empty stack");
|
||||
stack_.emplace(std::move(node), SHAMapNodeID{});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the path to the child of the current node reached by `branch`.
|
||||
* Shorten the path by one node and hand that node to the caller.
|
||||
*
|
||||
* A node keeps the depth it was reached at, never a normalized kLeafDepth. Only a leaf may
|
||||
* sit at kLeafDepth, since an inner node there would have no branch left to select.
|
||||
*/
|
||||
void
|
||||
pushChild(SHAMapTreeNodePtr node, unsigned int branch)
|
||||
{
|
||||
XRPL_ASSERT(node, "xrpl::SHAMap::NodePathStack::pushChild : non-null node input");
|
||||
XRPL_ASSERT(
|
||||
!stack_.empty(), "xrpl::SHAMap::NodePathStack::pushChild : non-empty stack");
|
||||
auto const childID = stack_.top().second.getChildNodeID(branch);
|
||||
XRPL_ASSERT_IF(
|
||||
node->isInner(),
|
||||
childID.getDepth() < kLeafDepth,
|
||||
"xrpl::SHAMap::NodePathStack::pushChild : inner node above leaf depth");
|
||||
XRPL_ASSERT_IF(
|
||||
node->isLeaf(),
|
||||
childID.isPrefixOf(leafKey(*node)),
|
||||
"xrpl::SHAMap::NodePathStack::pushChild : leaf key below branch");
|
||||
stack_.emplace(std::move(node), childID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the path to a node lying on the path to `target`.
|
||||
* Moves the node out, transferring the existing reference.
|
||||
*
|
||||
* For nodes not reached by descending a known branch: the walk tracks only the key it is
|
||||
* heading for, or the node is newly created. Either way `target` selects the branch.
|
||||
* @return the node that was at the end of the path, or an empty
|
||||
* pointer if there was none.
|
||||
*/
|
||||
void
|
||||
pushNode(SHAMapTreeNodePtr node, UInt256 const& target)
|
||||
[[nodiscard]] SHAMapTreeNodePtr
|
||||
releaseNode()
|
||||
{
|
||||
if (stack_.empty())
|
||||
if (path_.empty())
|
||||
{
|
||||
pushRoot(std::move(node));
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::SHAMap::NodePathStack::releaseNode : empty stack");
|
||||
return {};
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
auto node = std::move(path_.back());
|
||||
path_.pop_back();
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shorten the path by one node and hand it over as a `Node`.
|
||||
*
|
||||
* @tparam Node SHAMapInnerNode or SHAMapLeafNode.
|
||||
* @return the node that was at the end of the path, or an empty pointer
|
||||
* if the path was empty or that node is not a `Node`. The path
|
||||
* is shortened either way.
|
||||
*/
|
||||
template <class Node>
|
||||
[[nodiscard]] intr_ptr::SharedPtr<Node>
|
||||
releaseNodeAs()
|
||||
{
|
||||
static_assert(
|
||||
std::is_same_v<Node, SHAMapInnerNode> || std::is_same_v<Node, SHAMapLeafNode>,
|
||||
"releaseNodeAs serves the two concrete node kinds");
|
||||
|
||||
auto node = releaseNode();
|
||||
if (!node)
|
||||
{
|
||||
// The other half of releaseNode()'s empty-path contract, asserted there by the
|
||||
// UNREACHABLE in its own LCOV_EXCL block.
|
||||
return {}; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
if constexpr (std::is_same_v<Node, SHAMapInnerNode>)
|
||||
{
|
||||
// A leaf sits only at the end of a path, which both callers asking for an inner
|
||||
// node have released first. The other half of the contract their own UNREACHABLEs
|
||||
// assert, in SHAMap::dirtyUp and SHAMap::delItem.
|
||||
if (!node->isInner())
|
||||
return {}; // LCOV_EXCL_LINE
|
||||
}
|
||||
else
|
||||
{
|
||||
pushChild(std::move(node), selectBranch(stack_.top().second, target));
|
||||
if (!node->isLeaf())
|
||||
return {};
|
||||
}
|
||||
|
||||
return intr_ptr::staticPointerCast<Node>(std::move(node));
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a path at the root of the map, which sits at depth zero by
|
||||
* definition.
|
||||
*
|
||||
* @return false, leaving the path unchanged, if a path was already
|
||||
* started.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
pushRoot(SHAMapTreeNodePtr node)
|
||||
{
|
||||
if (!path_.empty())
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::SHAMap::NodePathStack::pushRoot : non-empty stack");
|
||||
return false;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
// Reserved here rather than in the constructor, to keep SHAMap::end()
|
||||
// allocation-free. A path holds at most kLeafDepth + 1 entries, so no push on a path
|
||||
// started here reallocates. A copy is not given that room, so top()'s invalidation
|
||||
// rule still holds.
|
||||
path_.reserve(kLeafDepth + 1u);
|
||||
path_.push_back(std::move(node));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the path to the child of the node at its end reached by
|
||||
* `branch`.
|
||||
*
|
||||
* Only a leaf may sit at kLeafDepth, since an inner node there would
|
||||
* have no branch left to select.
|
||||
*
|
||||
* @param node the child to append.
|
||||
* @param branch the branch of the current node that `node` was reached
|
||||
* through, which serves to judge the node offered.
|
||||
* @return false if there is no node to descend from or to push, no
|
||||
* branch of that number, no room left below for the kind of
|
||||
* node offered, or a leaf whose own key does not select
|
||||
* `branch`. The path keeps its nodes.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
pushChild(SHAMapTreeNodePtr node, unsigned int branch)
|
||||
{
|
||||
if (path_.empty() || !node || branch >= kBranchFactor)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::SHAMap::NodePathStack::pushChild : no child to push");
|
||||
return false;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
// A node must have room below it at the depth it is offered for. Reachable, since a
|
||||
// node resolved by hash carries no position of its own.
|
||||
auto const parentDepth = topDepth();
|
||||
bool const tooDeep = pastLeafDepth(parentDepth, *node);
|
||||
SOMETIMES(tooDeep, "xrpl::SHAMap::NodePathStack::pushChild : child past leaf depth");
|
||||
if (tooDeep)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// A leaf's key must agree with every branch recorded above it, which the chain in
|
||||
// `pathKey_` supplies. A path's length gives only the depths.
|
||||
pathKey_ = childNodeID(pathKey_, parentDepth, branch);
|
||||
|
||||
// The test takes the depth and the two keys, so no SHAMapNodeID is built: it
|
||||
// is a CountedObject, and one per push would report as a live node ID.
|
||||
bool const misplaced =
|
||||
node->isLeaf() && !samePositionAtDepth(parentDepth + 1u, pathKey_, leafKey(*node));
|
||||
SOMETIMES(
|
||||
misplaced, "xrpl::SHAMap::NodePathStack::pushChild : leaf key outside branch");
|
||||
if (misplaced)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
path_.push_back(std::move(node));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the path by one node lying on the way to `target`, starting
|
||||
* it if it is empty.
|
||||
*
|
||||
* For nodes not reached by descending a known branch: the walk tracks
|
||||
* only the key it is heading for, or the node is newly created. Either
|
||||
* way `target` names the branch.
|
||||
*
|
||||
* @param node the node to append.
|
||||
* @param target the key the walk is heading for.
|
||||
* @return whatever pushRoot or pushChild returned.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
pushNode(SHAMapTreeNodePtr node, UInt256 const& target)
|
||||
{
|
||||
if (path_.empty())
|
||||
{
|
||||
return pushRoot(std::move(node));
|
||||
}
|
||||
return pushChild(std::move(node), selectBranch(topDepth(), target));
|
||||
}
|
||||
|
||||
private:
|
||||
std::stack<std::pair<SHAMapTreeNodePtr, SHAMapNodeID>> stack_;
|
||||
// path_[i] holds the node at depth i, by construction: pushRoot starts at depth 0 and
|
||||
// pushChild only ever appends one level.
|
||||
std::vector<SHAMapTreeNodePtr> path_;
|
||||
|
||||
// The branches descended, one nibble per level: nibble i is the branch taken from depth i.
|
||||
// Nibbles below the path's end are stale after a pop, so every read masks at the path's
|
||||
// own depth. childNodeID clears a nibble before writing it, so re-descending overwrites.
|
||||
UInt256 pathKey_;
|
||||
};
|
||||
|
||||
using DeltaRef =
|
||||
@@ -649,28 +866,41 @@ private:
|
||||
/**
|
||||
* Record that the map is provably not the one it claims to be.
|
||||
*
|
||||
* Private because only the map itself can prove that, from a node that
|
||||
* contradicts the hashes it is syncing against. Cannot fail, since
|
||||
* Invalid outranks every other state; see trySetState().
|
||||
* Cannot fail, since Invalid outranks every other state. Const because a
|
||||
* const descend() can reach this verdict while resolving a node.
|
||||
*/
|
||||
void
|
||||
setInvalid();
|
||||
setInvalid() const;
|
||||
|
||||
/**
|
||||
* Condemn the map, naming in the log the node whose position proves it.
|
||||
*
|
||||
* Const, because a read path can reach this verdict. See setInvalid().
|
||||
*
|
||||
* @param node the node that cannot sit where it was offered.
|
||||
* @param hash the hash it was resolved under.
|
||||
* @param position the place in the tree it was offered for.
|
||||
*/
|
||||
void
|
||||
condemn(SHAMapTreeNode const& node, SHAMapHash const& hash, SHAMapNodeID const& position) const;
|
||||
|
||||
/**
|
||||
* Move the map to a new state, atomically.
|
||||
*
|
||||
* With clearSynching(), the only writer of state_ past construction, so
|
||||
* the order between the states lives in one place: Invalid outranks all
|
||||
* of them and is always stored, while every other transition is refused
|
||||
* once the map is Invalid, which is what makes that verdict terminal.
|
||||
* With clearSynching(), the only writer of state_ past construction.
|
||||
* Invalid is always stored, and every other transition is refused once
|
||||
* the map is Invalid, which is what makes that verdict terminal.
|
||||
* clearSynching() is narrower and moves only Synching.
|
||||
*
|
||||
* Const to let setInvalid() be const. state_ is mutable, which makes that
|
||||
* legal.
|
||||
*
|
||||
* @param desired The state to move to.
|
||||
* @return false if the map is Invalid and the requested state is not,
|
||||
* leaving it unchanged; true otherwise.
|
||||
* leaving it unchanged. True otherwise.
|
||||
*/
|
||||
bool
|
||||
trySetState(SHAMapState desired);
|
||||
trySetState(SHAMapState desired) const;
|
||||
|
||||
// tree node cache operations
|
||||
SHAMapTreeNodePtr
|
||||
@@ -698,9 +928,13 @@ private:
|
||||
dirtyUp(NodePathStack& stack, UInt256 const& target, SHAMapTreeNodePtr terminal);
|
||||
|
||||
/**
|
||||
* Walk towards the specified id, returning the node. Caller must check
|
||||
* if the return is nullptr, and if not, if the node->peekItem()->key() ==
|
||||
* id
|
||||
* Walk towards the specified id, returning the node.
|
||||
*
|
||||
* @param id the key to walk towards, which need not be in the map.
|
||||
* @param stack records the path walked, or nullptr to skip recording it.
|
||||
* @return the leaf the walk ended on, or nullptr if it ended on an inner
|
||||
* node or was refused. A returned leaf need not hold `id`, so
|
||||
* callers compare its key themselves.
|
||||
*/
|
||||
SHAMapLeafNode*
|
||||
walkTowardsKey(UInt256 const& id, NodePathStack* stack = nullptr) const;
|
||||
@@ -712,10 +946,16 @@ private:
|
||||
|
||||
/**
|
||||
* Unshare the node, allowing it to be modified
|
||||
*
|
||||
* @param node the node to unshare.
|
||||
* @param depth the depth the node sits at, which says whether it is the
|
||||
* root. A clone of the root has to be adopted as the new root. A
|
||||
* clone of any other node is hooked up by the caller.
|
||||
* @return the node, cloned if it was shared.
|
||||
*/
|
||||
template <class Node>
|
||||
intr_ptr::SharedPtr<Node>
|
||||
unshareNode(intr_ptr::SharedPtr<Node>, SHAMapNodeID const& nodeID);
|
||||
[[nodiscard]] intr_ptr::SharedPtr<Node>
|
||||
unshareNode(intr_ptr::SharedPtr<Node> node, unsigned int depth);
|
||||
|
||||
/**
|
||||
* prepare a node to be modified before flushing
|
||||
@@ -736,8 +976,13 @@ private:
|
||||
/**
|
||||
* Returns the first or last item at or below the node already on top of `stack`, extending
|
||||
* `stack` with the path walked to reach it.
|
||||
*
|
||||
* @param stack the path to extend, whose last node the search starts from.
|
||||
* @param direction whether to take the lowest or the highest branch at
|
||||
* each level.
|
||||
* @return the leaf found, or nullptr if no leaf lies below that node.
|
||||
*/
|
||||
SHAMapLeafNode*
|
||||
[[nodiscard]] SHAMapLeafNode*
|
||||
belowHelper(NodePathStack& stack, BelowDirection direction) const;
|
||||
|
||||
/**
|
||||
@@ -751,7 +996,7 @@ private:
|
||||
* @param direction First to search upwards from `id`, Last to search downwards.
|
||||
* @return An iterator to the item found, or end() if no item lies on that side of `id`.
|
||||
*/
|
||||
ConstIterator
|
||||
[[nodiscard]] ConstIterator
|
||||
boundHelper(UInt256 const& id, BelowDirection direction) const;
|
||||
|
||||
// Simple descent
|
||||
@@ -776,6 +1021,22 @@ private:
|
||||
bool& pending,
|
||||
DescendCallback&&) const;
|
||||
|
||||
/**
|
||||
* Resolve the child of `parent` on `branch`, judging where it lands.
|
||||
*
|
||||
* Const, yet it records the Invalid verdict through setInvalid(). A new
|
||||
* caller must therefore be a path that may reach that verdict. Today
|
||||
* addKnownNode() is the only one.
|
||||
*
|
||||
* @param parent the inner node to descend from.
|
||||
* @param parentID the position of `parent`.
|
||||
* @param branch the branch of `parent` to resolve, which the caller has
|
||||
* already bounded.
|
||||
* @param filter an alternate source of nodes, or null for the store alone.
|
||||
* @return the child and the child's position. A null child where the branch
|
||||
* could not be resolved, or where what came back does not belong at
|
||||
* that position, in which case the map is left Invalid.
|
||||
*/
|
||||
std::pair<SHAMapTreeNode*, SHAMapNodeID>
|
||||
descend(
|
||||
SHAMapInnerNode* parent,
|
||||
@@ -876,10 +1137,29 @@ private:
|
||||
};
|
||||
|
||||
// getMissingNodes helper functions
|
||||
|
||||
/**
|
||||
* Examine the remaining branches of one inner node, recording or
|
||||
* requesting what is missing.
|
||||
*
|
||||
* @param mn the walk's shared state, which collects the missing nodes.
|
||||
* @param node the walk's current position, updated to the node to process
|
||||
* next.
|
||||
*/
|
||||
void
|
||||
gmnProcessNodes(MissingNodes&, MissingNodes::StackEntry& node);
|
||||
static void
|
||||
gmnProcessDeferredReads(MissingNodes&);
|
||||
gmnProcessNodes(MissingNodes& mn, MissingNodes::StackEntry& node);
|
||||
|
||||
/**
|
||||
* Wait for every read this pass posted, then hook up or record what each
|
||||
* one resolved.
|
||||
*
|
||||
* Drains all of them even after judging the map, since an outstanding read
|
||||
* holds a pointer to `mn`.
|
||||
*
|
||||
* @param mn the walk's shared state, holding the posted reads.
|
||||
*/
|
||||
void
|
||||
gmnProcessDeferredReads(MissingNodes& mn);
|
||||
|
||||
// fetch from DB helper function
|
||||
SHAMapTreeNodePtr
|
||||
@@ -912,10 +1192,9 @@ SHAMap::state() const
|
||||
}
|
||||
|
||||
inline bool
|
||||
SHAMap::trySetState(SHAMapState desired)
|
||||
SHAMap::trySetState(SHAMapState desired) const
|
||||
{
|
||||
// Invalid is stored outright: a walk reaching that verdict has to win against a thread
|
||||
// settling the map.
|
||||
// Invalid is stored outright: that verdict outranks a concurrent transition.
|
||||
if (desired == SHAMapState::Invalid)
|
||||
{
|
||||
state_.store(SHAMapState::Invalid, std::memory_order_release);
|
||||
@@ -997,7 +1276,7 @@ SHAMap::isValid() const
|
||||
}
|
||||
|
||||
inline void
|
||||
SHAMap::setInvalid()
|
||||
SHAMap::setInvalid() const
|
||||
{
|
||||
// Through trySetState() like every other transition, so state_ has one writer funnel.
|
||||
trySetState(SHAMapState::Invalid);
|
||||
|
||||
@@ -12,6 +12,10 @@ private:
|
||||
int bad_;
|
||||
int duplicate_;
|
||||
|
||||
// Whether a node in the batch proved the map impossible. A fact about the batch rather than
|
||||
// about the map, so a caller can tell this batch's doing from a concurrent walk's verdict.
|
||||
bool invalidatedMap_;
|
||||
|
||||
public:
|
||||
SHAMapAddNode();
|
||||
|
||||
@@ -96,6 +100,26 @@ public:
|
||||
static SHAMapAddNode
|
||||
invalid();
|
||||
|
||||
/**
|
||||
* @return A verdict recording one invalid node that proved the map
|
||||
* impossible.
|
||||
*/
|
||||
static SHAMapAddNode
|
||||
mapInvalidated();
|
||||
|
||||
/**
|
||||
* Whether a node in the batch proved the map impossible.
|
||||
*
|
||||
* Reports what the batch did rather than what the map now holds, so a
|
||||
* verdict another thread's walk reached is not read as this batch's. The
|
||||
* two differ: SHAMap::setInvalid() is written with no lock held, by a
|
||||
* getMissingNodes() walk its caller runs lock-free.
|
||||
*
|
||||
* @return Whether a node in the batch invalidated the map.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
invalidatedMap() const;
|
||||
|
||||
/**
|
||||
* Clear every count back to zero.
|
||||
*/
|
||||
@@ -121,15 +145,15 @@ public:
|
||||
operator+=(SHAMapAddNode const& n);
|
||||
|
||||
private:
|
||||
SHAMapAddNode(int good, int bad, int duplicate);
|
||||
SHAMapAddNode(int good, int bad, int duplicate, bool invalidatedMap = false);
|
||||
};
|
||||
|
||||
inline SHAMapAddNode::SHAMapAddNode() : good_(0), bad_(0), duplicate_(0)
|
||||
inline SHAMapAddNode::SHAMapAddNode() : good_(0), bad_(0), duplicate_(0), invalidatedMap_(false)
|
||||
{
|
||||
}
|
||||
|
||||
inline SHAMapAddNode::SHAMapAddNode(int good, int bad, int duplicate)
|
||||
: good_(good), bad_(bad), duplicate_(duplicate)
|
||||
inline SHAMapAddNode::SHAMapAddNode(int good, int bad, int duplicate, bool invalidatedMap)
|
||||
: good_(good), bad_(bad), duplicate_(duplicate), invalidatedMap_(invalidatedMap)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -187,6 +211,12 @@ SHAMapAddNode::isUseful() const
|
||||
return good_ > 0;
|
||||
}
|
||||
|
||||
inline bool
|
||||
SHAMapAddNode::invalidatedMap() const
|
||||
{
|
||||
return invalidatedMap_;
|
||||
}
|
||||
|
||||
inline SHAMapAddNode
|
||||
SHAMapAddNode::duplicate()
|
||||
{
|
||||
@@ -205,10 +235,17 @@ SHAMapAddNode::invalid()
|
||||
return SHAMapAddNode(0, 1, 0);
|
||||
}
|
||||
|
||||
inline SHAMapAddNode
|
||||
SHAMapAddNode::mapInvalidated()
|
||||
{
|
||||
return SHAMapAddNode(0, 1, 0, true);
|
||||
}
|
||||
|
||||
inline void
|
||||
SHAMapAddNode::reset()
|
||||
{
|
||||
good_ = bad_ = duplicate_ = 0;
|
||||
invalidatedMap_ = false;
|
||||
}
|
||||
|
||||
inline std::string
|
||||
@@ -246,6 +283,9 @@ SHAMapAddNode::operator+=(SHAMapAddNode const& n)
|
||||
bad_ += n.bad_;
|
||||
duplicate_ += n.duplicate_;
|
||||
|
||||
// Or-ed, not summed: one node in the batch having proved the map impossible is the whole fact.
|
||||
invalidatedMap_ = invalidatedMap_ || n.invalidatedMap_;
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,8 @@ class SHAMapInnerNode final : public SHAMapTreeNode, public CountedObject<SHAMap
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* Each inner node has 16 children (the 'radix tree' part of the map)
|
||||
* Children per inner node: one branch per value of the key nibble that a
|
||||
* node's depth selects.
|
||||
*/
|
||||
static constexpr unsigned int kBranchFactor = 16;
|
||||
|
||||
@@ -38,10 +39,16 @@ private:
|
||||
static_assert(alignof(std::atomic<std::uint32_t>) == alignof(std::uint32_t));
|
||||
|
||||
/**
|
||||
* Written from more than one thread, since canonicalization shares a node
|
||||
* between maps and a walk can run with the acquisition lock released (see
|
||||
* SHAMap::state_). Relaxed both ways, since a generation is only compared
|
||||
* for equality and the children it vouches for are published through lock_.
|
||||
* The generation in which a walk found every node below this one resident,
|
||||
* with this node as the walk's root. The node object is shared by hash
|
||||
* between maps and positions, and a hash commits to a node's contents
|
||||
* rather than its place, so the flag is written and read for the root
|
||||
* position only; below the root, the position-keyed FullBelowCache is the
|
||||
* memo. Written from more than one thread, since canonicalization shares a
|
||||
* node between maps and a walk can run with the acquisition lock released
|
||||
* (see SHAMap::state_). Relaxed both ways, since a generation is only
|
||||
* compared for equality and the children it vouches for are published
|
||||
* through lock_.
|
||||
*/
|
||||
std::atomic<std::uint32_t> fullBelowGen_ = 0;
|
||||
std::uint16_t isBranch_ = 0;
|
||||
@@ -160,10 +167,23 @@ public:
|
||||
SHAMapTreeNodePtr
|
||||
canonicalizeChild(unsigned int branch, SHAMapTreeNodePtr node);
|
||||
|
||||
// sync functions
|
||||
/**
|
||||
* Whether a walk rooted at this node found every node below it resident.
|
||||
*
|
||||
* Meaningful only for the node a walk starts at; see fullBelowGen_.
|
||||
*
|
||||
* @param generation The FullBelowCache generation the asking walk runs in.
|
||||
* @return True if a walk rooted here completed in that generation.
|
||||
*/
|
||||
bool
|
||||
isFullBelow(std::uint32_t generation) const;
|
||||
|
||||
/**
|
||||
* Record that a walk rooted at this node found every node below it
|
||||
* resident.
|
||||
*
|
||||
* @param gen The FullBelowCache generation the completing walk ran in.
|
||||
*/
|
||||
void
|
||||
setFullBelowGen(std::uint32_t gen);
|
||||
|
||||
|
||||
@@ -75,4 +75,41 @@ leafKey(SHAMapTreeNode const& node)
|
||||
return safeDowncast<SHAMapLeafNode const&>(node).peekItem()->key();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a node may occupy a position in a SHAMap.
|
||||
*
|
||||
* A leaf's own key names its position. An inner node carries no key, so every
|
||||
* position agrees with it and a caller's own depth rules are what bound it.
|
||||
*
|
||||
* @param nodeID the position the node is claimed to occupy.
|
||||
* @param node the node to judge.
|
||||
* @return whether the node's own key agrees with that position.
|
||||
*/
|
||||
[[nodiscard]] inline bool
|
||||
belongsAt(SHAMapNodeID const& nodeID, SHAMapTreeNode const& node)
|
||||
{
|
||||
return !node.isLeaf() || nodeID.isPrefixOf(leafKey(node));
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a node may occupy the child position a branch leads to.
|
||||
*
|
||||
* The overload above, asked about a child without building the child's id.
|
||||
*
|
||||
* @param parentID the position of the node above, whose depth must be below
|
||||
* SHAMap::kLeafDepth.
|
||||
* @param branch the branch of that node leading to the position judged.
|
||||
* @param node the node to judge.
|
||||
* @return whether the node's own key agrees with that position.
|
||||
*/
|
||||
[[nodiscard]] inline bool
|
||||
belongsAt(SHAMapNodeID const& parentID, unsigned int branch, SHAMapTreeNode const& node)
|
||||
{
|
||||
if (!node.isLeaf())
|
||||
return true;
|
||||
|
||||
auto const& key = leafKey(node);
|
||||
return parentID.isPrefixOf(key) && selectBranch(parentID, key) == branch;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -123,6 +123,38 @@ operator<<(std::ostream& out, SHAMapNodeID const& node)
|
||||
return out << to_string(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the id of the child a parent's branch leads to
|
||||
*
|
||||
* A child's id is its parent's with the nibble at the parent's own depth set to
|
||||
* the branch taken. Every other nibble is copied through, so a masked parent
|
||||
* gives a masked child. The write counterpart of selectBranch.
|
||||
*
|
||||
* @param parentID the id of the parent. The nibble at parentDepth is
|
||||
* overwritten rather than merged, so an id already carrying one
|
||||
* there is accepted.
|
||||
* @param parentDepth the depth of the parent, below SHAMap::kLeafDepth.
|
||||
* @param branch the branch of the parent leading to the child.
|
||||
* @return that child's id, whose depth is parentDepth + 1.
|
||||
*/
|
||||
[[nodiscard]] UInt256
|
||||
childNodeID(UInt256 const& parentID, unsigned int parentDepth, unsigned int branch);
|
||||
|
||||
/**
|
||||
* Whether two keys name the same position at a given depth
|
||||
*
|
||||
* Both sides are masked, so either may carry bits below `depth`.
|
||||
* SHAMapNodeID::isPrefixOf is the stricter form.
|
||||
*
|
||||
* @param depth how many leading nibbles name the position, at most
|
||||
* SHAMap::kLeafDepth. A greater depth is clamped to it.
|
||||
* @param lhs one of the keys.
|
||||
* @param rhs the other key.
|
||||
* @return whether the two agree on those nibbles.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
samePositionAtDepth(unsigned int depth, UInt256 const& lhs, UInt256 const& rhs);
|
||||
|
||||
/**
|
||||
* Return an object representing a serialized SHAMap Node ID
|
||||
*
|
||||
@@ -144,9 +176,26 @@ deserializeSHAMapNodeID(std::string_view s)
|
||||
/** @} */
|
||||
|
||||
/**
|
||||
* Returns the branch that would contain the given hash
|
||||
* Returns the branch at the given depth that would contain the given hash
|
||||
*
|
||||
* @param depth the depth of the node whose branch to select.
|
||||
* @param hash the key whose nibble at that depth names the branch.
|
||||
* @return the branch containing the hash.
|
||||
*/
|
||||
[[nodiscard]] unsigned int
|
||||
selectBranch(SHAMapNodeID const& id, UInt256 const& hash);
|
||||
selectBranch(unsigned int depth, UInt256 const& hash);
|
||||
|
||||
/**
|
||||
* Returns the branch that would contain the given hash
|
||||
*
|
||||
* @param id the node whose depth to read.
|
||||
* @param hash the key whose nibble at that depth names the branch.
|
||||
* @return the branch containing the hash.
|
||||
*/
|
||||
[[nodiscard]] inline unsigned int
|
||||
selectBranch(SHAMapNodeID const& id, UInt256 const& hash)
|
||||
{
|
||||
return selectBranch(id.getDepth(), hash);
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -1,52 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
class ReadView;
|
||||
|
||||
/**
|
||||
* @brief Invariants for TransactionProposal ledger entries.
|
||||
*
|
||||
* - Only transaction types implementing proposal lifecycle operations may
|
||||
* create or modify proposals, and a failed one leaves no proposal effect.
|
||||
* - A proposal is deleted only by TransactionProposalCancel or together with
|
||||
* the Ticket it is keyed to, and a deleted Ticket takes the proposal keyed
|
||||
* to it along.
|
||||
* - Collected signer arrays remain sorted, unique, and bounded.
|
||||
*/
|
||||
class ValidTransactionProposal
|
||||
{
|
||||
// The Ticket each deleted proposal was keyed to; a proposal may go with
|
||||
// its Ticket, or by Cancel, and no other way.
|
||||
std::vector<uint256> ticketsOfDeletedProposals_;
|
||||
// The proposal each deleted Ticket was keyed to, whether or not one
|
||||
// existed; none may survive its Ticket.
|
||||
std::vector<Keylet> proposalsOfDeletedTickets_;
|
||||
std::set<uint256> deletedTickets_;
|
||||
std::uint32_t created_ = 0;
|
||||
std::uint32_t modified_ = 0;
|
||||
std::uint32_t deleted_ = 0;
|
||||
bool invalidSignerArrays_ = false;
|
||||
|
||||
public:
|
||||
void
|
||||
visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after);
|
||||
|
||||
[[nodiscard]] bool
|
||||
finalize(STTx const& tx, TER result, XRPAmount, ReadView const& view, beast::Journal const& j)
|
||||
const;
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -9,7 +9,6 @@
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/invariants/AMMInvariant.h>
|
||||
#include <xrpl/tx/invariants/CosignerInvariant.h>
|
||||
#include <xrpl/tx/invariants/DirectoryInvariant.h>
|
||||
#include <xrpl/tx/invariants/FreezeInvariant.h>
|
||||
#include <xrpl/tx/invariants/LoanBrokerInvariant.h>
|
||||
@@ -484,7 +483,6 @@ using InvariantChecks = std::tuple<
|
||||
ValidAmounts,
|
||||
ValidMPTTransfer,
|
||||
ObjectHasPseudoAccount,
|
||||
ValidTransactionProposal,
|
||||
SponsorshipOwnerCountsMatch,
|
||||
SponsorshipAccountCountMatchesField>;
|
||||
|
||||
|
||||
@@ -20,14 +20,11 @@ public:
|
||||
{
|
||||
}
|
||||
|
||||
static bool
|
||||
checkExtraFeatures(PreflightContext const& ctx);
|
||||
|
||||
static TxConsequences
|
||||
makeTxConsequences(PreflightContext const& ctx);
|
||||
|
||||
static XRPAmount
|
||||
calculateBaseFee(ReadView const& view, STTx const& tx);
|
||||
static bool
|
||||
checkExtraFeatures(PreflightContext const& ctx);
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
@@ -305,7 +305,8 @@ Ledger::Ledger(Ledger const& prevLedger, NetClock::time_point closeTime)
|
||||
}
|
||||
|
||||
Ledger::Ledger(LedgerHeader const& info, Rules rules, Family& family)
|
||||
: immutable_(true)
|
||||
: immutable_(false)
|
||||
, mapHashesFromHeader_(true)
|
||||
, txMap_(SHAMapType::TRANSACTION, info.txHash, family)
|
||||
, stateMap_(SHAMapType::STATE, info.accountHash, family)
|
||||
, rules_(std::move(rules))
|
||||
@@ -337,31 +338,29 @@ Ledger::Ledger(
|
||||
bool
|
||||
Ledger::setImmutable(bool rehash)
|
||||
{
|
||||
// A map found structurally invalid during sync must never be made immutable: isValid() tests
|
||||
// only for Invalid, and an immutable ledger is treated as persistable. Asked before anything is
|
||||
// written, so a refusal leaves the header exactly as it was rather than half relabeled.
|
||||
// An immutable ledger is treated as persistable, so an invalid map must never become
|
||||
// immutable. The validity test runs before anything is written, so a refusal leaves
|
||||
// the header as it was.
|
||||
if (!mapsValid())
|
||||
return false;
|
||||
|
||||
// Read here but written to the header below, once the maps are settled: getHash() can unshare a
|
||||
// dirty tree, so it has to run while the map is still mutable, while a write to the header must
|
||||
// wait until both maps have made it. Skipped once the ledger is immutable, since its maps can
|
||||
// no longer change.
|
||||
bool const deriveMapHashes = !immutable_ && rehash;
|
||||
// Read now but written to the header below: getHash() can unshare a dirty tree, so it must run
|
||||
// while the map is still mutable, while the header may only be written once both maps are
|
||||
// settled. Skipped when the ledger is already immutable or when the header supplied these.
|
||||
bool const deriveMapHashes = !immutable_ && !mapHashesFromHeader_ && rehash;
|
||||
UInt256 const txHash = deriveMapHashes ? txMap_.getHash().asUInt256() : UInt256{};
|
||||
UInt256 const accountHash = deriveMapHashes ? stateMap_.getHash().asUInt256() : UInt256{};
|
||||
|
||||
// Both were valid at the check above, but a concurrent walk can invalidate one in between (see
|
||||
// SHAMap::state_), so the result is checked. setInvalid() outranks Immutable and can land
|
||||
// after both maps have been settled, so this narrows the window it guards.
|
||||
// A concurrent walk can invalidate a map between the check above and here (see
|
||||
// SHAMap::state_), so the result is checked. That narrows the window rather than closing it,
|
||||
// since setInvalid() outranks Immutable.
|
||||
bool const bothImmutable = setMapsImmutable();
|
||||
SOMETIMES(!bothImmutable, "xrpl::Ledger::setImmutable : map invalidated while going immutable");
|
||||
if (!bothImmutable)
|
||||
return false;
|
||||
return false; // LCOV_EXCL_LINE: only the walk named above reaches this, so no test does
|
||||
|
||||
// Written only now, so losing the race above leaves the header describing what the ledger was
|
||||
// built from rather than a map that has since been abandoned. Forced rather than conditional,
|
||||
// since this is the only place the hash transitions to valid.
|
||||
// Written only now, so failing the check above leaves the header describing what the ledger was
|
||||
// built from. Forced, since this is the only place the hash transitions to valid.
|
||||
if (deriveMapHashes)
|
||||
{
|
||||
header_.txHash = txHash;
|
||||
|
||||
@@ -43,9 +43,6 @@ constexpr auto kProposalTxTypes = std::to_array<TxType>({
|
||||
bool
|
||||
isValidProposalTxnType(STObject const& proposedTx)
|
||||
{
|
||||
if (!proposedTx.isFieldPresent(sfTransactionType))
|
||||
return false;
|
||||
|
||||
auto const type = proposedTx.getFieldU16(sfTransactionType);
|
||||
|
||||
if (std::ranges::find(kProposalTxTypes, type) != kProposalTxTypes.end())
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/EscrowHelpers.h>
|
||||
#include <xrpl/ledger/helpers/OracleHelpers.h>
|
||||
#include <xrpl/ledger/helpers/ProposalHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
@@ -320,8 +319,6 @@ getLedgerEntryOwnerCount(SLE const& sle)
|
||||
UNREACHABLE("AccountRoots are not supported by object sponsorship.");
|
||||
return 0;
|
||||
// LCOV_EXCL_STOP
|
||||
case ltESCROW:
|
||||
return calculateAdditionalReserve(sle[~sfBytecode]);
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -137,8 +137,6 @@ transResults()
|
||||
MAKE_ERROR(tefBAD_PATH_COUNT, "Malformed: Too many paths."),
|
||||
MAKE_ERROR(tefNO_BYTECODE, "There is no WASM code to run, but a WASM-specific field was included."),
|
||||
MAKE_ERROR(tefBYTECODE_NOT_INCLUDED, "WASM code requires a field that was not included."),
|
||||
MAKE_ERROR(tefTEMP_DISABLED, "The transaction requires logic that is currently temporarily disabled."),
|
||||
MAKE_ERROR(tefEXCEEDS_LIMIT, "The transaction exceeds a limit set by fee voting."),
|
||||
|
||||
MAKE_ERROR(telLOCAL_ERROR, "Local failure."),
|
||||
MAKE_ERROR(telBAD_DOMAIN, "Domain too long."),
|
||||
@@ -211,6 +209,7 @@ transResults()
|
||||
MAKE_ERROR(temINVALID_INNER_BATCH, "Malformed: Invalid inner batch transaction."),
|
||||
MAKE_ERROR(temBAD_CIPHERTEXT, "Malformed: Invalid ciphertext."),
|
||||
MAKE_ERROR(temINVALID_BYTECODE, "Malformed: Provided byte code is invalid."),
|
||||
MAKE_ERROR(temTEMP_DISABLED, "The transaction requires logic that is currently temporarily disabled."),
|
||||
|
||||
MAKE_ERROR(terRETRY, "Retry transaction."),
|
||||
MAKE_ERROR(terFUNDS_SPENT, "DEPRECATED."),
|
||||
|
||||
+179
-62
@@ -106,6 +106,18 @@ SHAMap::snapShot(bool isMutable) const
|
||||
return std::make_shared<SHAMap>(*this, isMutable);
|
||||
}
|
||||
|
||||
void
|
||||
SHAMap::condemn(SHAMapTreeNode const& node, SHAMapHash const& hash, SHAMapNodeID const& position)
|
||||
const
|
||||
{
|
||||
// journal_ is shared by every map built from one Family, so name which map this is. The root
|
||||
// node's own hash, since getHash() unshares the tree on a zero hash.
|
||||
JLOG(journal_.warn()) << (node.isLeaf() ? "Leaf " : "Inner node ") << hash << " cannot sit at "
|
||||
<< position << ", so the map with root hash " << root_->getHash()
|
||||
<< " is invalid";
|
||||
setInvalid();
|
||||
}
|
||||
|
||||
void
|
||||
SHAMap::dirtyUp(NodePathStack& stack, UInt256 const& target, SHAMapTreeNodePtr child)
|
||||
{
|
||||
@@ -121,14 +133,19 @@ SHAMap::dirtyUp(NodePathStack& stack, UInt256 const& target, SHAMapTreeNodePtr c
|
||||
|
||||
while (!stack.empty())
|
||||
{
|
||||
auto node = intr_ptr::dynamicPointerCast<SHAMapInnerNode>(stack.top().first);
|
||||
SHAMapNodeID const nodeID = stack.top().second;
|
||||
stack.pop();
|
||||
XRPL_ASSERT(node, "xrpl::SHAMap::dirtyUp : non-null node");
|
||||
auto const depth = stack.topDepth();
|
||||
auto node = stack.releaseNodeAs<SHAMapInnerNode>();
|
||||
if (!node)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::SHAMap::dirtyUp : node is not inner");
|
||||
Throw<SHAMapMissingNode>(type_, target);
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
auto const branch = selectBranch(nodeID, target);
|
||||
auto const branch = selectBranch(depth, target);
|
||||
|
||||
node = unshareNode(std::move(node), nodeID);
|
||||
node = unshareNode(std::move(node), depth);
|
||||
node->setChild(branch, std::move(child));
|
||||
|
||||
child = std::move(node);
|
||||
@@ -138,32 +155,62 @@ SHAMap::dirtyUp(NodePathStack& stack, UInt256 const& target, SHAMapTreeNodePtr c
|
||||
SHAMapLeafNode*
|
||||
SHAMap::walkTowardsKey(UInt256 const& id, NodePathStack* stack) const
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
stack == nullptr || stack->empty(), "xrpl::SHAMap::walkTowardsKey : empty stack input");
|
||||
auto inNode = root_;
|
||||
SHAMapNodeID nodeID;
|
||||
if (stack != nullptr && !stack->empty())
|
||||
{
|
||||
// The path must start at the root, so a non-empty one is cleared rather than appended to.
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::SHAMap::walkTowardsKey : non-empty stack input");
|
||||
stack->clear();
|
||||
return nullptr;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
// Every node on this walk lies on the path to `id`, so the stack can derive each ID from the
|
||||
// branch `id` selects at the node above it.
|
||||
auto const pushCurrent = [&] {
|
||||
if (stack != nullptr)
|
||||
stack->pushNode(inNode, id);
|
||||
auto inNode = root_;
|
||||
unsigned int noStackDepth = 0;
|
||||
|
||||
// Returns false when the path could not be extended, which is separate from `id` being absent
|
||||
// (see pushChild). The path is cleared, so a caller reads an empty path as a refusal.
|
||||
auto const pushCurrent = [&] -> bool {
|
||||
if (stack == nullptr || stack->pushNode(inNode, id))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
stack->clear();
|
||||
return false;
|
||||
};
|
||||
|
||||
while (inNode->isInner())
|
||||
{
|
||||
pushCurrent();
|
||||
if (!pushCurrent())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto& inner = safeDowncast<SHAMapInnerNode&>(*inNode);
|
||||
auto const branch = selectBranch(nodeID, id);
|
||||
auto const depth = stack != nullptr ? stack->topDepth() : noStackDepth;
|
||||
auto const branch = selectBranch(depth, id);
|
||||
if (inner.isEmptyBranch(branch))
|
||||
return nullptr;
|
||||
|
||||
inNode = descendThrow(inner, branch);
|
||||
nodeID = nodeID.getChildNodeID(branch);
|
||||
if (stack == nullptr)
|
||||
{
|
||||
// The same depth bound pushChild applies, so both modes refuse at the same depth. A
|
||||
// node resolved from the local store still has its position and type to be judged.
|
||||
bool const tooDeep = pastLeafDepth(depth, *inNode);
|
||||
SOMETIMES(tooDeep, "xrpl::SHAMap::walkTowardsKey : child too deep");
|
||||
if (tooDeep)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
++noStackDepth;
|
||||
}
|
||||
}
|
||||
|
||||
pushCurrent();
|
||||
if (!pushCurrent())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
return safeDowncast<SHAMapLeafNode*>(inNode.get());
|
||||
}
|
||||
|
||||
@@ -374,12 +421,23 @@ SHAMap::descend(
|
||||
!parent->isEmptyBranch(branch), "xrpl::SHAMap::descend : parent branch is non-empty");
|
||||
|
||||
SHAMapTreeNode* child = parent->getChildPointer(branch); // NOLINT(misc-const-correctness)
|
||||
auto const childID = parentID.getChildNodeID(branch);
|
||||
|
||||
if (child == nullptr)
|
||||
{
|
||||
auto const& childHash = parent->getChildHash(branch);
|
||||
SHAMapTreeNodePtr childNode = fetchNodeNT(childHash, filter);
|
||||
|
||||
if (childNode &&
|
||||
(!belongsAt(childID, *childNode) || pastLeafDepth(parentID.getDepth(), *childNode)))
|
||||
{
|
||||
// A hash covers a node's contents, not its position, so position is judged before
|
||||
// canonicalizeChild hooks the node in. The verdict lands on the map, since resolving
|
||||
// the same blob again gives the same node.
|
||||
condemn(*childNode, childHash, childID);
|
||||
return std::make_pair(nullptr, childID);
|
||||
}
|
||||
|
||||
if (childNode)
|
||||
{
|
||||
childNode = parent->canonicalizeChild(branch, std::move(childNode));
|
||||
@@ -387,7 +445,7 @@ SHAMap::descend(
|
||||
}
|
||||
}
|
||||
|
||||
return std::make_pair(child, parentID.getChildNodeID(branch));
|
||||
return std::make_pair(child, childID);
|
||||
}
|
||||
|
||||
SHAMapTreeNode*
|
||||
@@ -434,7 +492,7 @@ SHAMap::descendAsync(
|
||||
|
||||
template <class Node>
|
||||
intr_ptr::SharedPtr<Node>
|
||||
SHAMap::unshareNode(intr_ptr::SharedPtr<Node> node, SHAMapNodeID const& nodeID)
|
||||
SHAMap::unshareNode(intr_ptr::SharedPtr<Node> node, unsigned int depth)
|
||||
{
|
||||
// make sure the node is suitable for the intended operation (copy on write)
|
||||
XRPL_ASSERT(node->cowid() <= cowid_, "xrpl::SHAMap::unshareNode : node valid for cowid");
|
||||
@@ -443,7 +501,7 @@ SHAMap::unshareNode(intr_ptr::SharedPtr<Node> node, SHAMapNodeID const& nodeID)
|
||||
// have a CoW
|
||||
XRPL_ASSERT(state() != SHAMapState::Immutable, "xrpl::SHAMap::unshareNode : not immutable");
|
||||
node = intr_ptr::staticPointerCast<Node>(node->clone(cowid_));
|
||||
if (nodeID.isRoot())
|
||||
if (depth == 0)
|
||||
root_ = node;
|
||||
}
|
||||
return node;
|
||||
@@ -453,14 +511,19 @@ SHAMapLeafNode*
|
||||
SHAMap::belowHelper(NodePathStack& stack, BelowDirection direction) const
|
||||
{
|
||||
XRPL_ASSERT(!stack.empty(), "xrpl::SHAMap::belowHelper : non-empty stack input");
|
||||
if (auto const& top = stack.top().first; top->isLeaf())
|
||||
if (stack.empty())
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
return nullptr;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
if (auto const& top = stack.top(); top->isLeaf())
|
||||
return safeDowncast<SHAMapLeafNode*>(top.get());
|
||||
|
||||
// The stack owns the node/ID pairing, so descending is only ever "push the branch we took".
|
||||
// `scanned` counts how many branches of the current node we have examined; the branch we look
|
||||
// at is derived from it, so no index ever goes out of range. `inner` tracks the node on top of
|
||||
// the stack, which keeps it alive, so it only needs recomputing after a push.
|
||||
auto* inner = safeDowncast<SHAMapInnerNode*>(stack.top().first.get());
|
||||
// `scanned` counts the branches of the current node already examined, and the branch looked at
|
||||
// is derived from it, so the index stays inside the fan-out. `inner` points at the top of the
|
||||
// path, which keeps it alive, so it only needs recomputing after a push.
|
||||
auto* inner = safeDowncast<SHAMapInnerNode*>(stack.top().get());
|
||||
for (auto scanned = 0u; scanned < kBranchFactor;)
|
||||
{
|
||||
auto const childBranch =
|
||||
@@ -472,9 +535,20 @@ SHAMap::belowHelper(NodePathStack& stack, BelowDirection direction) const
|
||||
continue;
|
||||
}
|
||||
|
||||
stack.pushChild(descendThrow(*inner, childBranch), childBranch);
|
||||
auto const parentDepth = stack.topDepth();
|
||||
auto descended = descendThrow(*inner, childBranch);
|
||||
if (!stack.pushChild(std::move(descended), childBranch))
|
||||
{
|
||||
// Throwing rather than returning nullptr keeps nullptr meaning a subtree with no leaf
|
||||
// below it, which begin() and an iterator increment both rely on. Every caller here is
|
||||
// a const read on a shared snapshot, so the verdict is left to the acquisition path
|
||||
// (see SHAMap::descend and gmnProcessNodes).
|
||||
JLOG(journal_.warn()) << "Cannot walk below depth " << parentDepth << " at branch "
|
||||
<< childBranch;
|
||||
Throw<SHAMapMissingNode>(type_, inner->getChildHash(childBranch));
|
||||
}
|
||||
|
||||
auto const& child = stack.top().first;
|
||||
auto const& child = stack.top();
|
||||
if (child->isLeaf())
|
||||
return safeDowncast<SHAMapLeafNode*>(child.get());
|
||||
|
||||
@@ -529,7 +603,12 @@ SHAMapLeafNode const*
|
||||
SHAMap::peekFirstItem(NodePathStack& stack) const
|
||||
{
|
||||
XRPL_ASSERT(stack.empty(), "xrpl::SHAMap::peekFirstItem : empty stack input");
|
||||
stack.pushRoot(root_);
|
||||
if (!stack.pushRoot(root_))
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
return nullptr;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
SHAMapLeafNode const* node = belowHelper(stack, BelowDirection::First);
|
||||
if (node == nullptr)
|
||||
{
|
||||
@@ -543,18 +622,28 @@ SHAMapLeafNode const*
|
||||
SHAMap::peekNextItem(UInt256 const& id, NodePathStack& stack) const
|
||||
{
|
||||
XRPL_ASSERT(!stack.empty(), "xrpl::SHAMap::peekNextItem : non-empty stack input");
|
||||
XRPL_ASSERT(stack.top().first->isLeaf(), "xrpl::SHAMap::peekNextItem : stack starts with leaf");
|
||||
if (stack.empty())
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
return nullptr;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
XRPL_ASSERT(stack.top()->isLeaf(), "xrpl::SHAMap::peekNextItem : stack starts with leaf");
|
||||
stack.pop();
|
||||
while (!stack.empty())
|
||||
{
|
||||
auto const [node, nodeID] = stack.top();
|
||||
auto const& node = stack.top();
|
||||
XRPL_ASSERT(!node->isLeaf(), "xrpl::SHAMap::peekNextItem : another node is not leaf");
|
||||
auto& inner = safeDowncast<SHAMapInnerNode&>(*node);
|
||||
for (auto i = selectBranch(nodeID, id) + 1; i < kBranchFactor; ++i)
|
||||
for (auto i = selectBranch(stack.topDepth(), id) + 1; i < kBranchFactor; ++i)
|
||||
{
|
||||
if (!inner.isEmptyBranch(i))
|
||||
{
|
||||
stack.pushChild(descendThrow(inner, i), i);
|
||||
auto child = descendThrow(inner, i);
|
||||
if (!stack.pushChild(std::move(child), i))
|
||||
{
|
||||
Throw<SHAMapMissingNode>(type_, id);
|
||||
}
|
||||
auto leaf = belowHelper(stack, BelowDirection::First);
|
||||
if (leaf == nullptr)
|
||||
Throw<SHAMapMissingNode>(type_, id);
|
||||
@@ -598,9 +687,15 @@ SHAMap::boundHelper(UInt256 const& id, BelowDirection direction) const
|
||||
|
||||
NodePathStack stack;
|
||||
walkTowardsKey(id, &stack);
|
||||
|
||||
// An empty path means the walk refused a node. An empty map still leaves its root on the path,
|
||||
// and end() would claim no key lies on the requested side of `id`, so a refusal throws.
|
||||
if (stack.empty())
|
||||
Throw<SHAMapMissingNode>(type_, id);
|
||||
|
||||
while (!stack.empty())
|
||||
{
|
||||
auto const [node, nodeID] = stack.top();
|
||||
auto const& node = stack.top();
|
||||
if (node->isLeaf())
|
||||
{
|
||||
auto const& item = safeDowncast<SHAMapLeafNode const&>(*node).peekItem();
|
||||
@@ -610,7 +705,7 @@ SHAMap::boundHelper(UInt256 const& id, BelowDirection direction) const
|
||||
else
|
||||
{
|
||||
auto& inner = safeDowncast<SHAMapInnerNode&>(*node);
|
||||
auto const taken = selectBranch(nodeID, id);
|
||||
auto const taken = selectBranch(stack.topDepth(), id);
|
||||
auto const remaining = searchingForward ? (kBranchFactor - 1u - taken) : taken;
|
||||
|
||||
for (auto scanned = 0u; scanned < remaining; ++scanned)
|
||||
@@ -620,7 +715,11 @@ SHAMap::boundHelper(UInt256 const& id, BelowDirection direction) const
|
||||
if (inner.isEmptyBranch(branch))
|
||||
continue;
|
||||
|
||||
stack.pushChild(descendThrow(inner, branch), branch);
|
||||
auto child = descendThrow(inner, branch);
|
||||
if (!stack.pushChild(std::move(child), branch))
|
||||
{
|
||||
Throw<SHAMapMissingNode>(type_, id);
|
||||
}
|
||||
auto const leaf = belowHelper(stack, direction);
|
||||
if (leaf == nullptr)
|
||||
Throw<SHAMapMissingNode>(type_, id);
|
||||
@@ -662,10 +761,12 @@ SHAMap::delItem(UInt256 const& id)
|
||||
if (stack.empty())
|
||||
Throw<SHAMapMissingNode>(type_, id);
|
||||
|
||||
auto const leaf = intr_ptr::dynamicPointerCast<SHAMapLeafNode>(stack.top().first);
|
||||
stack.pop();
|
||||
// An absent id leaves an inner node on top, which is the "not found" answer.
|
||||
auto const leaf = stack.releaseNodeAs<SHAMapLeafNode>();
|
||||
if (!leaf)
|
||||
return false;
|
||||
|
||||
if (!leaf || (leaf->peekItem()->key() != id))
|
||||
if (leaf->peekItem()->key() != id)
|
||||
return false;
|
||||
|
||||
SHAMapNodeType const type = leaf->getType();
|
||||
@@ -675,19 +776,25 @@ SHAMap::delItem(UInt256 const& id)
|
||||
|
||||
while (!stack.empty())
|
||||
{
|
||||
auto node = intr_ptr::staticPointerCast<SHAMapInnerNode>(stack.top().first);
|
||||
SHAMapNodeID const nodeID = stack.top().second;
|
||||
stack.pop();
|
||||
auto const depth = stack.topDepth();
|
||||
auto node = stack.releaseNodeAs<SHAMapInnerNode>();
|
||||
if (!node)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::SHAMap::delItem : node is not inner");
|
||||
Throw<SHAMapMissingNode>(type_, id);
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
node = unshareNode(std::move(node), nodeID);
|
||||
node = unshareNode(std::move(node), depth);
|
||||
node->setChild(
|
||||
selectBranch(nodeID, id), std::move(prevNode)); // NOLINT(bugprone-use-after-move)
|
||||
selectBranch(depth, id), std::move(prevNode)); // NOLINT(bugprone-use-after-move)
|
||||
|
||||
XRPL_ASSERT(
|
||||
not prevNode, // NOLINT(bugprone-use-after-move)
|
||||
"xrpl::SHAMap::delItem : prevNode should be nullptr after std::move");
|
||||
|
||||
if (!nodeID.isRoot())
|
||||
if (depth != 0)
|
||||
{
|
||||
// we may have made this a node with 1 or 0 children
|
||||
// And, if so, we need to remove this branch
|
||||
@@ -748,8 +855,8 @@ SHAMap::addGiveItem(SHAMapNodeType type, boost::intrusive_ptr<SHAMapItem const>
|
||||
if (stack.empty())
|
||||
Throw<SHAMapMissingNode>(type_, tag);
|
||||
|
||||
auto [node, nodeID] = stack.top();
|
||||
stack.pop();
|
||||
auto depth = stack.topDepth();
|
||||
auto node = stack.releaseNode();
|
||||
|
||||
if (node->isLeaf())
|
||||
{
|
||||
@@ -757,12 +864,12 @@ SHAMap::addGiveItem(SHAMapNodeType type, boost::intrusive_ptr<SHAMapItem const>
|
||||
if (leaf->peekItem()->key() == tag)
|
||||
return false;
|
||||
}
|
||||
node = unshareNode(std::move(node), nodeID);
|
||||
node = unshareNode(std::move(node), depth);
|
||||
if (node->isInner())
|
||||
{
|
||||
// easy case, we end on an inner node
|
||||
auto const inner = intr_ptr::staticPointerCast<SHAMapInnerNode>(node);
|
||||
auto const branch = selectBranch(nodeID, tag);
|
||||
auto const branch = selectBranch(depth, tag);
|
||||
XRPL_ASSERT(
|
||||
inner->isEmptyBranch(branch), "xrpl::SHAMap::addGiveItem : inner branch is empty");
|
||||
inner->setChild(branch, makeTypedLeaf(type, std::move(item), cowid_));
|
||||
@@ -780,13 +887,20 @@ SHAMap::addGiveItem(SHAMapNodeType type, boost::intrusive_ptr<SHAMapItem const>
|
||||
|
||||
auto b1 = 0u, b2 = 0u;
|
||||
|
||||
while ((b1 = selectBranch(nodeID, tag)) == (b2 = selectBranch(nodeID, otherItem->key())))
|
||||
while ((b1 = selectBranch(depth, tag)) == (b2 = selectBranch(depth, otherItem->key())))
|
||||
{
|
||||
stack.pushNode(node, tag);
|
||||
if (!stack.pushNode(node, tag))
|
||||
{
|
||||
// The loop advances only while the two keys agree at the current nibble, and keys
|
||||
// agreeing at every nibble are equal, which returned false above.
|
||||
// LCOV_EXCL_START
|
||||
Throw<SHAMapMissingNode>(type_, tag);
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
// we need a new inner node, since both go on same branch at this
|
||||
// level
|
||||
nodeID = nodeID.getChildNodeID(b1);
|
||||
++depth;
|
||||
node = intr_ptr::makeShared<SHAMapInnerNode>(cowid_);
|
||||
}
|
||||
|
||||
@@ -835,16 +949,19 @@ SHAMap::updateGiveItem(SHAMapNodeType type, boost::intrusive_ptr<SHAMapItem cons
|
||||
if (stack.empty())
|
||||
Throw<SHAMapMissingNode>(type_, tag);
|
||||
|
||||
auto node = intr_ptr::dynamicPointerCast<SHAMapLeafNode>(stack.top().first);
|
||||
auto const nodeID = stack.top().second;
|
||||
stack.pop();
|
||||
auto const depth = stack.topDepth();
|
||||
|
||||
if (!node || (node->peekItem()->key() != tag))
|
||||
// A tag absent from the map leaves an inner node on top, which the API permits.
|
||||
auto node = stack.releaseNodeAs<SHAMapLeafNode>();
|
||||
if (!node)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Or the walk ended on a leaf holding some other key.
|
||||
if (node->peekItem()->key() != tag)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::SHAMap::updateGiveItem : invalid node");
|
||||
return false;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
if (node->getType() != type)
|
||||
@@ -853,7 +970,7 @@ SHAMap::updateGiveItem(SHAMapNodeType type, boost::intrusive_ptr<SHAMapItem cons
|
||||
return false;
|
||||
}
|
||||
|
||||
node = unshareNode(std::move(node), nodeID);
|
||||
node = unshareNode(std::move(node), depth);
|
||||
|
||||
if (node->setItem(item))
|
||||
dirtyUp(stack, tag, node);
|
||||
|
||||
@@ -50,14 +50,31 @@ maskedToDepth(UInt256 const& key, unsigned int depth)
|
||||
return key & depthMask(depth);
|
||||
}
|
||||
|
||||
// Whether `id` at `depth` is what `key` looks like once masked down to that depth, i.e.
|
||||
// whether an ID with this depth and id names a subtree that `key` falls under.
|
||||
// Whether `key` masked to `depth` equals `id`. Asymmetric: `id` is not masked, so this also
|
||||
// refuses an id carrying bits below its own depth.
|
||||
static bool
|
||||
isPrefixOfAtDepth(UInt256 const& id, unsigned int depth, UInt256 const& key)
|
||||
{
|
||||
return maskedToDepth(key, depth) == id;
|
||||
}
|
||||
|
||||
bool
|
||||
samePositionAtDepth(unsigned int depth, UInt256 const& lhs, UInt256 const& rhs)
|
||||
{
|
||||
// The mask is chosen here, so an out-of-range depth would index depthMask's table past its
|
||||
// last entry. kLeafDepth itself is in range, unlike in selectBranch: the position at the leaf
|
||||
// depth is the whole key. A public helper has to hold its own bound.
|
||||
if (depth > SHAMap::kLeafDepth)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::samePositionAtDepth : depth within tree");
|
||||
depth = SHAMap::kLeafDepth;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
return maskedToDepth(lhs, depth) == maskedToDepth(rhs, depth);
|
||||
}
|
||||
|
||||
// canonicalize the hash to a node ID for this depth
|
||||
SHAMapNodeID::SHAMapNodeID(unsigned int depth, UInt256 const& hash) : id_(hash), depth_(depth)
|
||||
{
|
||||
@@ -90,6 +107,31 @@ SHAMapNodeID::getRawString() const
|
||||
return s.getString();
|
||||
}
|
||||
|
||||
[[nodiscard]] UInt256
|
||||
childNodeID(UInt256 const& parentID, unsigned int parentDepth, unsigned int branch)
|
||||
{
|
||||
XRPL_ASSERT(branch < SHAMap::kBranchFactor, "xrpl::childNodeID : valid branch input");
|
||||
XRPL_ASSERT(parentDepth < SHAMap::kLeafDepth, "xrpl::childNodeID : parent above leaf depth");
|
||||
|
||||
// Only a depth below kLeafDepth names a child nibble, so clamp to the last one that does.
|
||||
auto const clamped = std::min(parentDepth, SHAMap::kLeafDepth - 1u);
|
||||
|
||||
// The nibble is cleared before the branch is written, so an id already carrying one there is
|
||||
// overwritten rather than merged. The byte index and both nibble choices read `clamped`, so
|
||||
// they cannot disagree.
|
||||
UInt256 id = parentID;
|
||||
auto& byte = *(id.begin() + (clamped / 2));
|
||||
if ((clamped & 1) != 0u)
|
||||
{
|
||||
byte = static_cast<unsigned char>((byte & 0xF0u) | branch);
|
||||
}
|
||||
else
|
||||
{
|
||||
byte = static_cast<unsigned char>((byte & 0x0Fu) | (branch << 4));
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
SHAMapNodeID
|
||||
SHAMapNodeID::getChildNodeID(unsigned int branch) const
|
||||
{
|
||||
@@ -107,15 +149,13 @@ SHAMapNodeID::getChildNodeID(unsigned int branch) const
|
||||
XRPL_ASSERT(
|
||||
depth_ <= SHAMap::kLeafDepth, "xrpl::SHAMapNodeID::getChildNodeID : maximum leaf depth");
|
||||
|
||||
if (depth_ >= SHAMap::kLeafDepth)
|
||||
if (SHAMap::isLeafDepth(depth_))
|
||||
Throw<std::logic_error>(std::format("Request for child node ID of {}", to_string(*this)));
|
||||
|
||||
if (!isPrefixOf(id_))
|
||||
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);
|
||||
return node;
|
||||
return SHAMapNodeID{depth_ + 1, childNodeID(id_, depth_, branch)};
|
||||
}
|
||||
|
||||
bool
|
||||
@@ -145,16 +185,16 @@ deserializeSHAMapNodeID(void const* data, std::size_t size)
|
||||
}
|
||||
|
||||
[[nodiscard]] unsigned int
|
||||
selectBranch(SHAMapNodeID const& id, UInt256 const& hash)
|
||||
selectBranch(unsigned int depth, UInt256 const& hash)
|
||||
{
|
||||
XRPL_ASSERT(id.getDepth() < SHAMap::kLeafDepth, "xrpl::selectBranch : depth below leaf depth");
|
||||
XRPL_ASSERT(depth < SHAMap::kLeafDepth, "xrpl::selectBranch : depth below leaf depth");
|
||||
|
||||
// A depth-64 ID has no nibble left to select. Callers must not ask, but clamp anyway to keep
|
||||
// the read below the end of the 32-byte key.
|
||||
auto const depth = std::min(id.getDepth(), SHAMap::kLeafDepth - 1u);
|
||||
auto branch = static_cast<unsigned int>(*(hash.begin() + (depth / 2)));
|
||||
// Only a depth below kLeafDepth has a nibble to select, so clamp to the last one that does.
|
||||
auto const clamped = std::min(depth, SHAMap::kLeafDepth - 1u);
|
||||
auto branch = static_cast<unsigned int>(*(hash.begin() + (clamped / 2)));
|
||||
|
||||
if ((depth & 1) != 0u)
|
||||
// The byte index and the nibble choice both read `clamped`, so they cannot disagree.
|
||||
if ((clamped & 1) != 0u)
|
||||
{
|
||||
branch &= 0xf;
|
||||
}
|
||||
|
||||
@@ -31,25 +31,6 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* Whether a depth is at or past the deepest an inner node may occupy.
|
||||
*
|
||||
* Nibbles run out at SHAMap::kLeafDepth, so only a leaf may sit there. True for
|
||||
* every deeper position too, which lies past the end of a key.
|
||||
*
|
||||
* @param depth The depth to judge.
|
||||
* @return Whether an inner node at that depth makes the map impossible.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isLeafDepth(unsigned int depth)
|
||||
{
|
||||
return depth >= SHAMap::kLeafDepth;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void
|
||||
SHAMap::visitLeaves(
|
||||
std::function<void(boost::intrusive_ptr<SHAMapItem const> const& item)> const& leafFunction)
|
||||
@@ -223,12 +204,12 @@ SHAMap::gmnProcessNodes(MissingNodes& mn, MissingNodes::StackEntry& se)
|
||||
// we already know this child node is missing
|
||||
fullBelow = false;
|
||||
}
|
||||
// The depth test precedes the cache lookup for the same reason it does in addKnownNode():
|
||||
// the cache is keyed by node hash and shared across maps, and a hash covers a node's
|
||||
// children but not its depth. Skipping the shortcut forgoes an optimization only.
|
||||
// The cache key carries the child's position beside its hash, so a hit answers for this
|
||||
// child at this position and no other. The depth test runs first, so a child at kLeafDepth
|
||||
// is judged by the arm below rather than answered from the cache: only a leaf sits there.
|
||||
else if (
|
||||
!backed_ || isLeafDepth(nodeID.getDepth() + 1) ||
|
||||
!f_.getFullBelowCache()->touchIfExists(childHash.asUInt256()))
|
||||
!f_.getFullBelowCache()->touchIfExists(childHash.asUInt256(), nodeID, branch))
|
||||
{
|
||||
bool pending = false;
|
||||
auto d = descendAsync(
|
||||
@@ -259,18 +240,28 @@ SHAMap::gmnProcessNodes(MissingNodes& mn, MissingNodes::StackEntry& se)
|
||||
if (--mn.max <= 0)
|
||||
return;
|
||||
}
|
||||
else if (d->isInner() && isLeafDepth(nodeID.getDepth() + 1))
|
||||
// Only a leaf has a position of its own to judge, so the type is tested first. The
|
||||
// depth test bounds the branch this arm names, and the arm below bounds the descent.
|
||||
else if (
|
||||
d->isLeaf() && !isLeafDepth(nodeID.getDepth()) && !belongsAt(nodeID, branch, *d))
|
||||
{
|
||||
// Only a leaf belongs that deep (see isLeafDepth and SHAMap::addKnownNode). A node
|
||||
// resolved locally reaches the walk without passing through addKnownNode(), so the
|
||||
// walk reaches this verdict itself. Ordered ahead of the full-below test below,
|
||||
// which canonicalization shares across maps.
|
||||
JLOG(journal_.warn()) << "Inner node at branch " << branch << " below " << nodeID
|
||||
<< " makes the map invalid";
|
||||
setInvalid();
|
||||
// The judgment SHAMap::descend makes, for the descendAsync path, which hooks what
|
||||
// it resolves before this runs. `fullBelow` is cleared as on the missing path.
|
||||
fullBelow = false;
|
||||
condemn(*d, childHash, nodeID.getChildNodeID(branch));
|
||||
return;
|
||||
}
|
||||
else if (d->isInner() && !safeDowncast<SHAMapInnerNode*>(d)->isFullBelow(mn.generation))
|
||||
else if (pastLeafDepth(nodeID.getDepth(), *d))
|
||||
{
|
||||
// A node resolved locally never passes through addKnownNode(), so the walk judges
|
||||
// it here. The bound also covers nodeID, which the descent below writes.
|
||||
condemn(*d, childHash, nodeID.getChildNodeID(branch));
|
||||
return;
|
||||
}
|
||||
// The node's own full-below flag is not read here. The node object is shared by hash
|
||||
// across maps and positions, so a flag another walk set says nothing about this
|
||||
// position, and the position-keyed cache above has already missed for it.
|
||||
else if (d->isInner())
|
||||
{
|
||||
mn.stack.push(se);
|
||||
|
||||
@@ -289,10 +280,14 @@ SHAMap::gmnProcessNodes(MissingNodes& mn, MissingNodes::StackEntry& se)
|
||||
|
||||
if (fullBelow)
|
||||
{ // No partial node encountered below this node
|
||||
node->setFullBelowGen(mn.generation);
|
||||
// The node flag is written for the root alone, since the root position is the only one a
|
||||
// shared node object can vouch for; see SHAMapInnerNode::fullBelowGen_.
|
||||
if (nodeID.isRoot())
|
||||
node->setFullBelowGen(mn.generation);
|
||||
if (backed_)
|
||||
{
|
||||
f_.getFullBelowCache()->insert(node->getHash().asUInt256());
|
||||
// Keyed by position as well as hash, so the entry answers only for this position.
|
||||
f_.getFullBelowCache()->insert(node->getHash().asUInt256(), nodeID);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,6 +318,25 @@ SHAMap::gmnProcessDeferredReads(MissingNodes& mn)
|
||||
auto nodePtr = std::get<3>(deferredNode);
|
||||
auto const& nodeHash = parent->getChildHash(branch);
|
||||
|
||||
// A deferred entry carries the position the walk held when it posted the read, so a branch
|
||||
// below it is named here only where the tree has room for one.
|
||||
if (nodePtr && nodePtr->isLeaf() && !isLeafDepth(parentID.getDepth()) &&
|
||||
!belongsAt(parentID, branch, *nodePtr))
|
||||
{
|
||||
// The judgment the synchronous paths make, for a node an async read resolved. Skips
|
||||
// rather than returns, since every posted read must be drained while `mn` is alive.
|
||||
condemn(*nodePtr, nodeHash, parentID.getChildNodeID(branch));
|
||||
continue;
|
||||
}
|
||||
|
||||
// The depth bound gmnProcessNodes carries beside its leaf test, for a node an async read
|
||||
// resolved. Skips for the same reason as above.
|
||||
if (nodePtr && pastLeafDepth(parentID.getDepth(), *nodePtr))
|
||||
{
|
||||
condemn(*nodePtr, nodeHash, parentID.getChildNodeID(branch));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (nodePtr)
|
||||
{ // Got the node
|
||||
nodePtr = parent->canonicalizeChild(branch, std::move(nodePtr));
|
||||
@@ -438,12 +452,10 @@ SHAMap::getMissingNodes(int max, SHAMapSyncFilter const* filter)
|
||||
|
||||
if (mn.stack.empty() && !mn.resumes.empty())
|
||||
{
|
||||
// Recheck nodes we could not finish before
|
||||
// Recheck nodes we could not finish before. A node's own full-below flag is not
|
||||
// consulted: it is written for a root only, and a resumed node sits below one.
|
||||
for (auto const& [innerNode, nodeId] : mn.resumes)
|
||||
{
|
||||
if (!innerNode->isFullBelow(mn.generation))
|
||||
mn.stack.emplace(innerNode, nodeId, randInt(255), 0, true);
|
||||
}
|
||||
mn.stack.emplace(innerNode, nodeId, randInt(255), 0, true);
|
||||
|
||||
mn.resumes.clear();
|
||||
}
|
||||
@@ -629,10 +641,6 @@ SHAMap::addKnownNode(
|
||||
{
|
||||
XRPL_ASSERT(!nodeID.isRoot(), "xrpl::SHAMap::addKnownNode : valid node");
|
||||
XRPL_ASSERT(treeNode, "xrpl::SHAMap::addKnownNode : non-null tree node");
|
||||
XRPL_ASSERT_IF(
|
||||
treeNode->isLeaf(),
|
||||
nodeID.isPrefixOf(leafKey(*treeNode)),
|
||||
"xrpl::SHAMap::addKnownNode : leaf position consistent with node ID");
|
||||
|
||||
if (!isSynching())
|
||||
{
|
||||
@@ -640,13 +648,13 @@ SHAMap::addKnownNode(
|
||||
return SHAMapAddNode::duplicate();
|
||||
}
|
||||
|
||||
auto const generation = f_.getFullBelowCache()->getGeneration();
|
||||
SHAMapNodeID currNodeID;
|
||||
auto currNode = root_.get();
|
||||
|
||||
while (currNode->isInner() &&
|
||||
!safeDowncast<SHAMapInnerNode*>(currNode)->isFullBelow(generation) &&
|
||||
(currNodeID.getDepth() < nodeID.getDepth()))
|
||||
// The descent reads no node's own full-below flag: a node object is shared by hash across
|
||||
// maps and positions, so that flag answers for the root position alone. The position-keyed
|
||||
// cache lookup in the loop is the memo that answers for a position below it.
|
||||
while (currNode->isInner() && (currNodeID.getDepth() < nodeID.getDepth()))
|
||||
{
|
||||
auto const branch = selectBranch(currNodeID, nodeID.getNodeID());
|
||||
auto inner = safeDowncast<SHAMapInnerNode*>(currNode);
|
||||
@@ -659,12 +667,11 @@ SHAMap::addKnownNode(
|
||||
|
||||
auto childHash = inner->getChildHash(branch);
|
||||
|
||||
// Depth before the cache: the cache is keyed by node hash and shared across the family, and
|
||||
// a hash covers a node's children but not its depth, so the same subtree can be cached as
|
||||
// complete at one depth and reached at another. Skipping the shortcut only forgoes an
|
||||
// optimization.
|
||||
// The cache key carries the child's position beside its hash, so a hit answers for this
|
||||
// child at this position and no other. The depth test runs first, so a node offered at
|
||||
// kLeafDepth reaches the badDepth test below rather than being answered as a duplicate.
|
||||
if (!isLeafDepth(currNodeID.getDepth() + 1) &&
|
||||
f_.getFullBelowCache()->touchIfExists(childHash.asUInt256()))
|
||||
f_.getFullBelowCache()->touchIfExists(childHash.asUInt256(), currNodeID, branch))
|
||||
{
|
||||
return SHAMapAddNode::duplicate();
|
||||
}
|
||||
@@ -672,6 +679,14 @@ SHAMap::addKnownNode(
|
||||
auto prevNode = inner;
|
||||
std::tie(currNode, currNodeID) = descend(inner, currNodeID, branch, filter);
|
||||
|
||||
if (!isValid())
|
||||
{
|
||||
// descend condemned the map. `childHash` was read before that descent, so the
|
||||
// comparison below would read a stale value.
|
||||
JLOG(journal_.warn()) << "Node " << nodeID << " cannot be hooked into an invalid map";
|
||||
return SHAMapAddNode::invalid();
|
||||
}
|
||||
|
||||
if (currNode != nullptr)
|
||||
continue;
|
||||
|
||||
@@ -682,25 +697,18 @@ SHAMap::addKnownNode(
|
||||
return SHAMapAddNode::invalid();
|
||||
}
|
||||
|
||||
// Only leaves may sit at kLeafDepth (see isLeafDepth), so an inner node there makes the map
|
||||
// impossible. The node is reported as bad data.
|
||||
//
|
||||
// Every node from the root down hash-verified to get here, so it is the requested root hash
|
||||
// itself that commits to a shape no valid tree can have. The verdict belongs to that hash
|
||||
// rather than to our copy of the tree.
|
||||
// Every node from the root down hash-verified to get here, so the requested root hash
|
||||
// itself commits to a shape no tree can have. The verdict belongs to that hash.
|
||||
bool const badDepth = treeNode->isInner() && isLeafDepth(currNodeID.getDepth());
|
||||
SOMETIMES(badDepth, "xrpl::SHAMap::addKnownNode : map is invalid");
|
||||
if (badDepth)
|
||||
{
|
||||
JLOG(journal_.warn()) << "Node " << nodeID << " makes the map invalid at "
|
||||
<< currNodeID;
|
||||
setInvalid();
|
||||
return SHAMapAddNode::invalid();
|
||||
condemn(*treeNode, treeNode->getHash(), currNodeID);
|
||||
return SHAMapAddNode::mapInvalidated();
|
||||
}
|
||||
|
||||
// The data hashes to the child at currNodeID but claims to belong at nodeID, so it is not
|
||||
// the node that was asked for. Only the label is wrong, so the map stays sound and the node
|
||||
// is still obtainable from another sender.
|
||||
// The data hashes to the child at currNodeID but is labeled nodeID, so it is not the node
|
||||
// asked for. Only the label is wrong, so the map stays sound.
|
||||
bool const badPosition = (currNodeID != nodeID);
|
||||
SOMETIMES(badPosition, "xrpl::SHAMap::addKnownNode : node ID does not match its position");
|
||||
if (badPosition)
|
||||
@@ -710,6 +718,14 @@ SHAMap::addKnownNode(
|
||||
return SHAMapAddNode::invalid();
|
||||
}
|
||||
|
||||
// A leaf's own key names its position, and the hash test above ties this leaf to this
|
||||
// parent, so the verdict belongs to the map. Below badPosition, since Invalid is terminal.
|
||||
if (!belongsAt(nodeID, *treeNode))
|
||||
{
|
||||
condemn(*treeNode, treeNode->getHash(), nodeID);
|
||||
return SHAMapAddNode::mapInvalidated();
|
||||
}
|
||||
|
||||
if (backed_)
|
||||
canonicalize(childHash, treeNode);
|
||||
|
||||
@@ -875,7 +891,7 @@ SHAMap::getProofPath(UInt256 const& key) const
|
||||
return {};
|
||||
}
|
||||
|
||||
if (auto const& node = stack.top().first; !node || node->isInner() ||
|
||||
if (auto const& node = stack.top(); !node || node->isInner() ||
|
||||
intr_ptr::staticPointerCast<SHAMapLeafNode>(node)->peekItem()->key() != key)
|
||||
{
|
||||
JLOG(journal_.debug()) << "no path to " << key;
|
||||
@@ -887,7 +903,7 @@ SHAMap::getProofPath(UInt256 const& key) const
|
||||
while (!stack.empty())
|
||||
{
|
||||
Serializer s;
|
||||
stack.top().first->serializeForWire(s);
|
||||
stack.top()->serializeForWire(s);
|
||||
path.emplace_back(std::move(s.modData()));
|
||||
stack.pop();
|
||||
}
|
||||
|
||||
@@ -31,8 +31,6 @@
|
||||
#include <xrpl/protocol/detail/transactions.macro>
|
||||
|
||||
#undef TRANSACTION
|
||||
#undef TRANSACTION_INCLUDE
|
||||
|
||||
#pragma pop_macro("TRANSACTION")
|
||||
|
||||
// DO NOT INCLUDE TRANSACTOR HEADER FILES HERE.
|
||||
|
||||
@@ -1,225 +0,0 @@
|
||||
#include <xrpl/tx/invariants/CosignerInvariant.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STArray.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/SeqProxy.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
namespace {
|
||||
|
||||
bool
|
||||
validSignerArray(STArray const& signers, std::size_t limit, SField const& elementName)
|
||||
{
|
||||
if (signers.size() > limit)
|
||||
return false;
|
||||
|
||||
std::optional<AccountID> previous;
|
||||
for (auto const& signer : signers)
|
||||
{
|
||||
if (signer.getFName() != elementName || !signer.isFieldPresent(sfAccount))
|
||||
return false;
|
||||
|
||||
auto const account = signer.getAccountID(sfAccount);
|
||||
if (previous && account <= *previous)
|
||||
return false;
|
||||
previous = account;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
validNestedSigners(STObject const& signature)
|
||||
{
|
||||
return !signature.isFieldPresent(sfSigners) ||
|
||||
validSignerArray(signature.getFieldArray(sfSigners), STTx::kMaxMultiSigners, sfSigner);
|
||||
}
|
||||
|
||||
bool
|
||||
validSignerArrays(STObject const& proposedTx)
|
||||
{
|
||||
if (!validNestedSigners(proposedTx))
|
||||
return false;
|
||||
|
||||
for (auto const* field :
|
||||
std::array<SField const*, 2>{&sfCounterpartySignature, &sfSponsorSignature})
|
||||
{
|
||||
if (proposedTx.isFieldPresent(*field) &&
|
||||
!validNestedSigners(proposedTx.getFieldObject(*field)))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!proposedTx.isFieldPresent(sfBatchSigners))
|
||||
return true;
|
||||
|
||||
auto const& batchSigners = proposedTx.getFieldArray(sfBatchSigners);
|
||||
if (!validSignerArray(batchSigners, kMaxBatchSigners, sfBatchSigner))
|
||||
return false;
|
||||
|
||||
return std::ranges::all_of(
|
||||
batchSigners, [](auto const& batchSigner) { return validNestedSigners(batchSigner); });
|
||||
}
|
||||
|
||||
// The Ticket a proposal is keyed to: the one its proposed transaction spends.
|
||||
uint256
|
||||
proposalTicketKey(SLE const& proposal)
|
||||
{
|
||||
auto const& proposedTx = proposal.getFieldObject(sfProposedTransaction);
|
||||
return keylet::ticket(
|
||||
proposedTx.getAccountID(sfAccount),
|
||||
SeqProxy::rawTicket(proposedTx.getFieldU32(sfTicketSequence)))
|
||||
.key;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void
|
||||
ValidTransactionProposal::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after)
|
||||
{
|
||||
// `after` is always present (on deletion it holds the final state), so it
|
||||
// alone determines the entry type. LedgerEntryTypesMatch owns malformed
|
||||
// type transitions; skip them rather than read one side with the other's
|
||||
// schema.
|
||||
if (!after || (before && before->getType() != after->getType()))
|
||||
return;
|
||||
|
||||
auto const type = after->getType();
|
||||
|
||||
// Consuming a Ticket deletes the proposal keyed to it (XLS-0103 §4.5).
|
||||
// Remember each Ticket that went, so finalize can tell a proposal deleted
|
||||
// with its Ticket from one that simply vanished, and the proposal each
|
||||
// Ticket was keyed to, so finalize can check that none stayed behind.
|
||||
if (type == ltTICKET)
|
||||
{
|
||||
if (isDelete)
|
||||
{
|
||||
deletedTickets_.insert(after->key());
|
||||
proposalsOfDeletedTickets_.push_back(
|
||||
keylet::txProposal(
|
||||
after->getAccountID(sfAccount), after->getFieldU32(sfTicketSequence)));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (type != ltTRANSACTION_PROPOSAL)
|
||||
return;
|
||||
|
||||
if (!before)
|
||||
{
|
||||
++created_;
|
||||
}
|
||||
else if (isDelete)
|
||||
{
|
||||
++deleted_;
|
||||
ticketsOfDeletedProposals_.push_back(proposalTicketKey(*before));
|
||||
}
|
||||
else
|
||||
{
|
||||
++modified_;
|
||||
}
|
||||
|
||||
// A live proposal's signer lists must stay submittable: within size
|
||||
// limits, sorted by account, and free of duplicates.
|
||||
if (!isDelete && !validSignerArrays(after->getFieldObject(sfProposedTransaction)))
|
||||
invalidSignerArrays_ = true;
|
||||
}
|
||||
|
||||
bool
|
||||
ValidTransactionProposal::finalize(
|
||||
STTx const& tx,
|
||||
TER result,
|
||||
XRPAmount,
|
||||
ReadView const& view,
|
||||
beast::Journal const& j) const
|
||||
{
|
||||
if (!view.rules().enabled(featureCosign))
|
||||
return true;
|
||||
|
||||
if (invalidSignerArrays_)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: TransactionProposal signer arrays are not canonical.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// A proposal leaves the ledger in two ways (XLS-0103 §4.5): a
|
||||
// TransactionProposalCancel deletes the proposal it names, and consuming
|
||||
// the Ticket a proposal is keyed to deletes the proposal with it, whether
|
||||
// the proposed transaction executed (or failed with a claimed-fee tec) or
|
||||
// AccountDelete swept the target's Tickets.
|
||||
std::uint32_t deletedWithTicket = 0;
|
||||
for (auto const& ticketKey : ticketsOfDeletedProposals_)
|
||||
{
|
||||
if (deletedTickets_.contains(ticketKey))
|
||||
++deletedWithTicket;
|
||||
}
|
||||
|
||||
bool const succeeded = isTesSuccess(result);
|
||||
bool effectsMatch = false;
|
||||
if (tx.getTxnType() == ttTRANSACTION_PROPOSAL_CREATE)
|
||||
{
|
||||
effectsMatch = created_ == (succeeded ? 1u : 0u) && modified_ == 0 && deleted_ == 0;
|
||||
}
|
||||
else if (tx.getTxnType() == ttTRANSACTION_PROPOSAL_CANCEL)
|
||||
{
|
||||
// A Cancel deletes one proposal and nothing else. It cannot
|
||||
// spend a reserved Ticket, so no proposal goes with its Ticket here.
|
||||
effectsMatch = created_ == 0 && modified_ == 0 && deletedWithTicket == 0 &&
|
||||
deleted_ == (succeeded ? 1u : 0u);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Any other transaction deletes a proposal only together with its
|
||||
// Ticket, and modifies at most one, only as a successful
|
||||
// SponsorshipTransfer.
|
||||
// TODO: extend this when TransactionProposalSign is implemented.
|
||||
std::uint32_t const modifiable =
|
||||
tx.getTxnType() == ttSPONSORSHIP_TRANSFER && succeeded ? 1 : 0;
|
||||
effectsMatch = created_ == 0 && modified_ <= modifiable && deleted_ == deletedWithTicket;
|
||||
}
|
||||
|
||||
if (!effectsMatch)
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
<< "Invariant failed: TransactionProposal changes do not match transaction result.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// The other direction of the deletion rule: ticketDelete deletes the
|
||||
// proposal keyed to a Ticket along with it (XLS-0103 §4.5), so no
|
||||
// proposal may outlive its Ticket, orphaned on a key that can no longer
|
||||
// be spent.
|
||||
bool const orphaned = std::ranges::any_of(
|
||||
proposalsOfDeletedTickets_, [&view](Keylet const& keylet) { return view.exists(keylet); });
|
||||
if (orphaned)
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
<< "Invariant failed: TransactionProposal outlived the Ticket it is keyed to.";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -935,12 +935,8 @@ ValidClawback::finalize(
|
||||
return !mptV2Enabled;
|
||||
}
|
||||
|
||||
// IOU balances keep 16 significant digits, so a clawback
|
||||
// much smaller than the balance changes it by a rounded
|
||||
// amount, possibly zero. Compare with the same rounded
|
||||
// subtraction the balance update performs.
|
||||
if (*afterBalance > *beforeBalance ||
|
||||
*afterBalance != *beforeBalance - std::min(*beforeBalance, clawAmount))
|
||||
(*beforeBalance - *afterBalance) != std::min(*beforeBalance, clawAmount))
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
<< "Invariant failed: trustline clawback balance change is invalid";
|
||||
|
||||
@@ -254,15 +254,6 @@ private:
|
||||
TAmounts<TIn, TOut> const& stepAmt,
|
||||
TOut const& ownerGives) const;
|
||||
|
||||
template <template <typename, typename> typename Offer>
|
||||
void
|
||||
tryConsumeOffer(
|
||||
PaymentSandbox& sb,
|
||||
Offer<TIn, TOut>& offer,
|
||||
TAmounts<TIn, TOut> const& ofrAmt,
|
||||
TAmounts<TIn, TOut> const& stepAmt,
|
||||
TOut const& ownerGives) const;
|
||||
|
||||
// If clobQuality is available and has a better quality then return nullopt,
|
||||
// otherwise if amm liquidity is available return AMM offer adjusted based
|
||||
// on clobQuality.
|
||||
@@ -819,11 +810,10 @@ BookStep<TIn, TOut, TDerived>::forEachOffer(
|
||||
auto const amtIn = [&](TIn const& in) { return capInMPT && *maxIn_ < in ? *maxIn_ : in; };
|
||||
try
|
||||
{
|
||||
// All arithmetic in this block runs before the offer is consumed.
|
||||
// A crafted MPTokensV2 offer can overflow while transfer rates or
|
||||
// crossing limits are applied, here or in the callback's
|
||||
// limitStepIn()/limitStepOut(); remove that unusable offer instead
|
||||
// of letting it persist as a tecINTERNAL source. Overflows while
|
||||
// consuming the offer are handled by tryConsumeOffer().
|
||||
// crossing limits are applied; remove that unusable offer instead
|
||||
// of letting it persist as a tecINTERNAL source.
|
||||
stpAmt.in = mulRatio(amtIn(ofrAmt.in), ofrInRate, QUALITY_ONE, /*roundUp*/ true);
|
||||
|
||||
// owner pays the transfer fee. An MPT gross-up that doesn't fit in
|
||||
@@ -951,9 +941,16 @@ BookStep<TIn, TOut, TDerived>::forEachOffer(
|
||||
removeOffer("Removing offer with overflowing amount calculation");
|
||||
return true;
|
||||
}
|
||||
// Legacy behavior: the exception escapes flow() and the
|
||||
// transaction fails with tecINTERNAL.
|
||||
// An overflow can only be produced by a crafted MPT offer, and MPT
|
||||
// offers require featureMPTokensV2 (enforced at OfferCreate
|
||||
// preflight). So the amendment is always enabled when we get here
|
||||
// and this legacy re-throw is unreachable in practice.
|
||||
// LCOV_EXCL_START
|
||||
XRPL_ASSERT(
|
||||
sb.rules().enabled(featureMPTokensV2),
|
||||
"xrpl::BookStep::forEachOffer : overflow implies MPTokensV2");
|
||||
throw;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
};
|
||||
|
||||
@@ -994,32 +991,6 @@ BookStep<TIn, TOut, TDerived>::forEachOffer(
|
||||
return {offers.permToRemove(), counter.count()};
|
||||
}
|
||||
|
||||
template <class TIn, class TOut, class TDerived>
|
||||
template <template <typename, typename> typename Offer>
|
||||
void
|
||||
BookStep<TIn, TOut, TDerived>::tryConsumeOffer(
|
||||
PaymentSandbox& sb,
|
||||
Offer<TIn, TOut>& offer,
|
||||
TAmounts<TIn, TOut> const& ofrAmt,
|
||||
TAmounts<TIn, TOut> const& stepAmt,
|
||||
TOut const& ownerGives) const
|
||||
{
|
||||
try
|
||||
{
|
||||
// Under MPTokensV2, an overflow while consuming the offer fails the
|
||||
// strand instead of being handled by forEachOffer().
|
||||
consumeOffer(sb, offer, ofrAmt, stepAmt, ownerGives);
|
||||
}
|
||||
catch (std::overflow_error const&)
|
||||
{
|
||||
if (sb.rules().enabled(featureMPTokensV2))
|
||||
{
|
||||
Throw<FlowException>(tecINTERNAL, "Overflow while consuming offer");
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
template <class TIn, class TOut, class TDerived>
|
||||
template <template <typename, typename> typename Offer>
|
||||
void
|
||||
@@ -1221,7 +1192,7 @@ BookStep<TIn, TOut, TDerived>::revImp(
|
||||
savedOuts.insert(stpAmt.out);
|
||||
result = TAmounts<TIn, TOut>(sum(savedIns), sum(savedOuts));
|
||||
remainingOut = out - result.out;
|
||||
this->tryConsumeOffer(sb, offer, ofrAmt, stpAmt, ownerGives);
|
||||
this->consumeOffer(sb, offer, ofrAmt, stpAmt, ownerGives);
|
||||
// return true b/c even if the payment is satisfied,
|
||||
// we need to consume the offer. An offer limited by what the
|
||||
// issuer can issue is not consumed, though: its remainder is still
|
||||
@@ -1253,7 +1224,7 @@ BookStep<TIn, TOut, TDerived>::revImp(
|
||||
savedOuts.insert(remainingOut);
|
||||
result.in = sum(savedIns);
|
||||
result.out = out;
|
||||
this->tryConsumeOffer(sb, offer, ofrAdjAmt, stpAdjAmt, ownerGivesAdj);
|
||||
this->consumeOffer(sb, offer, ofrAdjAmt, stpAdjAmt, ownerGivesAdj);
|
||||
|
||||
// Explicitly check whether the offer is funded. Given that we have
|
||||
// (stpAmt.out > remainingOut), it's natural to assume the offer
|
||||
@@ -1437,7 +1408,7 @@ BookStep<TIn, TOut, TDerived>::fwdImp(
|
||||
savedOuts = std::move(savedOutsAdj);
|
||||
result = resultAdj;
|
||||
remainingIn = in - result.in;
|
||||
this->tryConsumeOffer(sb, offer, ofrAdjAmt, stpAdjAmt, ownerGivesAdj);
|
||||
this->consumeOffer(sb, offer, ofrAdjAmt, stpAdjAmt, ownerGivesAdj);
|
||||
|
||||
// When the mantissas of two iou amounts differ by less than ten, then
|
||||
// subtracting them leaves a result of zero. This can cause the check
|
||||
@@ -1541,18 +1512,6 @@ BookStep<TIn, TOut, TDerived>::check(StrandContext const& ctx) const
|
||||
return temBAD_PATH;
|
||||
}
|
||||
|
||||
// An issuer paying with its own MPT issues it into the first book. A
|
||||
// holder-sourced strand records the MPT in the first MPTEndpointStep and
|
||||
// so rejects a later book that outputs it again; do the same here. The
|
||||
// step after such a book issues the MPT a second time and the payment
|
||||
// engine cannot keep the two within MaximumAmount once StrandFlow
|
||||
// rebuilds the sandbox on a limiting step.
|
||||
if (ctx.isFirst && book_.in.holds<MPTIssue>() && book_.in.getIssuer() == ctx.strandSrc &&
|
||||
ctx.view.rules().enabled(featureMPTokensV2))
|
||||
{
|
||||
ctx.seenDirectAssets[0].insert(book_.in);
|
||||
}
|
||||
|
||||
// Do not allow two books to output the same issue. This may cause offers on
|
||||
// one step to unfund offers in another step.
|
||||
if (!ctx.seenBookOuts.insert(book_.out).second ||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/PaymentSandbox.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
@@ -875,18 +874,15 @@ MPTEndpointStep<TDerived>::check(StrandContext const& ctx) const
|
||||
|
||||
// pure issue/redeem can't be frozen (issuer/holder)
|
||||
// For the first step: check global freeze of the step's own asset.
|
||||
// For the last step: check only the per-holder lock.
|
||||
// For the last step: check only the per-holder MPToken lock.
|
||||
// Global freeze of the deliver asset is not checked here
|
||||
// because MPT semantics allow issuer<->holder transfers even when globally
|
||||
// locked — only holder-to-holder DEX paths are restricted.
|
||||
// For both steps: a vault share inherits the freeze of the vault's
|
||||
// underlying asset.
|
||||
if (!(ctx.isLast && ctx.isFirst))
|
||||
{
|
||||
auto const& account = ctx.isFirst ? src_ : dst_;
|
||||
bool const frozen = (ctx.isFirst && isGlobalFrozen(ctx.view, mptIssue_)) ||
|
||||
isIndividualFrozen(ctx.view, account, mptIssue_) ||
|
||||
isVaultPseudoAccountFrozen(ctx.view, account, mptIssue_, 0);
|
||||
isIndividualFrozen(ctx.view, account, mptIssue_);
|
||||
if (frozen)
|
||||
return terLOCKED;
|
||||
}
|
||||
|
||||
@@ -110,16 +110,11 @@ EscrowCancel::doApply()
|
||||
auto const sle = ctx_.view().peek(keylet::account(account));
|
||||
STAmount const amount = escrow->getFieldAmount(sfAmount);
|
||||
|
||||
auto const reserveToSubtract = calculateAdditionalReserve((*escrow)[~sfBytecode]);
|
||||
|
||||
// The return can re-create a holding the owner deleted while the escrow
|
||||
// was pending; the removed escrow must not be counted against its reserve.
|
||||
bool const recycleReserve = ctx_.view().rules().enabled(fixCleanup3_4_0);
|
||||
if (recycleReserve)
|
||||
{
|
||||
decreaseOwnerCountForObject(
|
||||
ctx_.view(), sle, escrow.mutableRawSle(), reserveToSubtract, ctx_.journal);
|
||||
}
|
||||
decreaseOwnerCountForObject(ctx_.view(), sle, escrow.mutableRawSle(), 1, ctx_.journal);
|
||||
|
||||
// Transfer amount back to the owner
|
||||
if (isXRP(amount))
|
||||
@@ -165,10 +160,7 @@ EscrowCancel::doApply()
|
||||
}
|
||||
|
||||
if (!recycleReserve)
|
||||
{
|
||||
decreaseOwnerCountForObject(
|
||||
ctx_.view(), sle, escrow.mutableRawSle(), reserveToSubtract, ctx_.journal);
|
||||
}
|
||||
decreaseOwnerCountForObject(ctx_.view(), sle, escrow.mutableRawSle(), 1, ctx_.journal);
|
||||
|
||||
// Remove escrow from ledger
|
||||
escrow.erase();
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/conditions/Condition.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/entries/EscrowEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
@@ -16,7 +16,6 @@
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Concepts.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Fees.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
@@ -131,19 +130,6 @@ escrowCreatePreflightHelper<MPTIssue>(PreflightContext const& ctx)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
XRPAmount
|
||||
EscrowCreate::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
XRPAmount txnFees{Transactor::calculateBaseFee(view, tx)};
|
||||
if (tx.isFieldPresent(sfBytecode))
|
||||
{
|
||||
// Ten base fees in all: `Transactor::calculateBaseFee` charges one,
|
||||
// this adds nine. Plus 5 drops per byte.
|
||||
txnFees += 9 * view.fees().base + 5 * tx[sfBytecode].size();
|
||||
}
|
||||
return txnFees;
|
||||
}
|
||||
|
||||
NotTEC
|
||||
EscrowCreate::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
@@ -175,19 +161,12 @@ EscrowCreate::preflight(PreflightContext const& ctx)
|
||||
ctx.tx[sfCancelAfter] <= ctx.tx[sfFinishAfter])
|
||||
return temBAD_EXPIRATION;
|
||||
|
||||
if (ctx.tx.isFieldPresent(sfBytecode) && !ctx.tx.isFieldPresent(sfCancelAfter))
|
||||
return temBAD_EXPIRATION;
|
||||
|
||||
// In the absence of a FinishAfter, the escrow can be finished
|
||||
// immediately, which can be confusing. When creating an escrow,
|
||||
// we want to ensure that either a FinishAfter time is explicitly
|
||||
// specified or a completion condition is attached.
|
||||
if (!ctx.tx[~sfFinishAfter] && !ctx.tx[~sfCondition] && !ctx.tx[~sfBytecode])
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "Must have at least one of FinishAfter, "
|
||||
"Condition, or Bytecode.";
|
||||
if (!ctx.tx[~sfFinishAfter] && !ctx.tx[~sfCondition])
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
if (auto const cb = ctx.tx[~sfCondition])
|
||||
{
|
||||
@@ -203,33 +182,6 @@ EscrowCreate::preflight(PreflightContext const& ctx)
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.tx.isFieldPresent(sfData))
|
||||
{
|
||||
if (!ctx.tx.isFieldPresent(sfBytecode))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "EscrowCreate with Data requires Bytecode";
|
||||
return temMALFORMED;
|
||||
}
|
||||
auto const data = ctx.tx.getFieldVL(sfData);
|
||||
if (data.size() > kMaxWasmDataLength)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "EscrowCreate.Data bad size " << data.size();
|
||||
return temMALFORMED;
|
||||
}
|
||||
}
|
||||
|
||||
// amendment was checked by checkExtraFeatures
|
||||
if (ctx.tx.isFieldPresent(sfBytecode))
|
||||
{
|
||||
auto const code = ctx.tx.getFieldVL(sfBytecode);
|
||||
// Protocol ceiling only; `preflight` has no view.
|
||||
if (code.empty() || code.size() > kMaxBytecodeSizeLimit)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "EscrowCreate.Bytecode bad size " << code.size();
|
||||
return temMALFORMED;
|
||||
}
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -264,30 +216,6 @@ EscrowCreate::preclaim(PreclaimContext const& ctx)
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
}
|
||||
|
||||
// The limits are voted, so they can change between ledgers. A `tef`, unlike
|
||||
// a `tem`, does not mark the transaction bad for the hold time.
|
||||
if (ctx.tx.isFieldPresent(sfBytecode))
|
||||
{
|
||||
auto const& fees = ctx.view.fees();
|
||||
if (isBytecodeUploadDisabled(fees))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "WASM runtime deactivated by fee voting";
|
||||
return tefTEMP_DISABLED;
|
||||
}
|
||||
|
||||
auto const code = ctx.tx.getFieldVL(sfBytecode);
|
||||
if (code.size() > fees.bytecodeSizeLimit)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "EscrowCreate.Bytecode over voted limit " << code.size();
|
||||
return tefEXCEEDS_LIMIT;
|
||||
}
|
||||
|
||||
// TODO(SmartEscrow): screen the module itself here - that it compiles,
|
||||
// and that it imports and exports what the engine can serve - once the
|
||||
// WASM engine lands. Until then any blob of the right size is taken.
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -308,7 +236,6 @@ EscrowCreate::doApply()
|
||||
|
||||
// Check reserve and funds availability
|
||||
STAmount const amount{ctx_.tx[sfAmount]};
|
||||
auto const reserveToAdd = calculateAdditionalReserve(ctx_.tx[~sfBytecode]);
|
||||
|
||||
auto const balance = sle->getFieldAmount(sfBalance).xrp();
|
||||
// First check: whoever is on the hook for the new owner increment
|
||||
@@ -316,8 +243,8 @@ EscrowCreate::doApply()
|
||||
// validates the sponsor's reserve + remaining credit. When
|
||||
// unsponsored this hits the source branch and validates the
|
||||
// source's pre-lock balance against base + (currentOC+1)*increment.
|
||||
if (auto const ret = checkReserve(
|
||||
ctx_.getApplyViewContext(), sle, balance, {.ownerCountDelta = reserveToAdd}, j_);
|
||||
if (auto const ret =
|
||||
checkReserve(ctx_.getApplyViewContext(), sle, balance, {.ownerCountDelta = 1}, j_);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
@@ -331,15 +258,11 @@ EscrowCreate::doApply()
|
||||
// checkReserve) because that helper diverts to the sponsor's balance
|
||||
// when a sponsor is present and would ignore the source's post-lock
|
||||
// balance entirely. ownerCountDelta differs by case:
|
||||
// - sponsored: 0 — sponsor covers the new owner increment, so
|
||||
// the source only owes reserve for its current owners.
|
||||
// - unsponsored: reserveToAdd — source owes reserve including the new
|
||||
// increment, which Bytecode can make larger than one unit.
|
||||
// - sponsored: 0 — sponsor covers the new owner increment, so the
|
||||
// source only owes reserve for its current owners.
|
||||
// - unsponsored: 1 — source owes reserve including the new increment.
|
||||
auto const sourceReserve = accountReserve(
|
||||
ctx_.view(),
|
||||
sle,
|
||||
j_,
|
||||
{.ownerCountDelta = getTxReserveSponsorID(ctx_.tx) ? 0 : reserveToAdd});
|
||||
ctx_.view(), sle, j_, {.ownerCountDelta = getTxReserveSponsorID(ctx_.tx) ? 0 : 1});
|
||||
if (balance - STAmount(amount).xrp() < sourceReserve)
|
||||
return tecUNFUNDED;
|
||||
}
|
||||
@@ -366,8 +289,6 @@ EscrowCreate::doApply()
|
||||
(*escrow)[~sfCancelAfter] = ctx_.tx[~sfCancelAfter];
|
||||
(*escrow)[~sfFinishAfter] = ctx_.tx[~sfFinishAfter];
|
||||
(*escrow)[~sfDestinationTag] = ctx_.tx[~sfDestinationTag];
|
||||
(*escrow)[~sfBytecode] = ctx_.tx[~sfBytecode];
|
||||
(*escrow)[~sfData] = ctx_.tx[~sfData];
|
||||
|
||||
if (ctx_.view().rules().enabled(fixIncludeKeyletFields))
|
||||
{
|
||||
@@ -435,7 +356,7 @@ EscrowCreate::doApply()
|
||||
}
|
||||
|
||||
// increment owner count
|
||||
increaseOwnerCount(ctx_.getApplyViewContext(), sle, reserveToAdd, ctx_.journal);
|
||||
increaseOwnerCount(ctx_.getApplyViewContext(), sle, 1, ctx_.journal);
|
||||
addSponsorToLedgerEntry(ctx_.getApplyViewContext(), escrow.mutableRawSle());
|
||||
ctx_.view().update(sle);
|
||||
return tesSUCCESS;
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Concepts.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Fees.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Rate.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
@@ -28,8 +27,6 @@
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <system_error>
|
||||
#include <variant>
|
||||
|
||||
@@ -82,24 +79,7 @@ EscrowFinish::preflight(PreflightContext const& ctx)
|
||||
// If you specify a condition, then you must also specify
|
||||
// a fulfillment.
|
||||
if (static_cast<bool>(cb) != static_cast<bool>(fb))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "Condition != Fulfillment";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
if (auto const allowance = ctx.tx[~sfGas]; allowance)
|
||||
{
|
||||
// Protocol ceiling only; `preflight` has no view.
|
||||
if (*allowance == 0)
|
||||
{
|
||||
return temBAD_LIMIT;
|
||||
}
|
||||
if (*allowance > kMaxGasLimit)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "Gas too large: " << *allowance;
|
||||
return temBAD_LIMIT;
|
||||
}
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
@@ -168,15 +148,7 @@ EscrowFinish::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
extraFee += view.fees().base * (32 + (fb->size() / 16));
|
||||
}
|
||||
if (auto const allowance = tx[~sfGas]; allowance)
|
||||
{
|
||||
// The extra fee is the allowance in drops, rounded up to the nearest
|
||||
// whole drop. Widen before multiplying: both factors are 32-bit.
|
||||
std::uint64_t const allowanceFee =
|
||||
((std::uint64_t{*allowance} * view.fees().gasPrice) + microDropsPerDrop - 1) /
|
||||
microDropsPerDrop;
|
||||
extraFee += allowanceFee;
|
||||
}
|
||||
|
||||
return Transactor::calculateBaseFee(view, tx) + extraFee;
|
||||
}
|
||||
|
||||
@@ -190,66 +162,25 @@ EscrowFinish::preclaim(PreclaimContext const& ctx)
|
||||
return err;
|
||||
}
|
||||
|
||||
if (ctx.view.rules().enabled(featureTokenEscrow) ||
|
||||
ctx.view.rules().enabled(featureSmartEscrow))
|
||||
if (ctx.view.rules().enabled(featureTokenEscrow))
|
||||
{
|
||||
// this check is done in doApply before this amendment is enabled
|
||||
auto const seqProxy = SeqProxy::rawSequence(ctx.tx[sfOfferSequence]);
|
||||
EscrowEntryR const escrow(ctx.tx[sfOwner], seqProxy, ctx.view);
|
||||
if (!escrow)
|
||||
return tecNO_TARGET;
|
||||
|
||||
if (ctx.view.rules().enabled(featureSmartEscrow))
|
||||
AccountID const dest = (*escrow)[sfDestination];
|
||||
STAmount const amount = (*escrow)[sfAmount];
|
||||
|
||||
if (!isXRP(amount))
|
||||
{
|
||||
if (escrow->isFieldPresent(sfBytecode))
|
||||
{
|
||||
if (!ctx.tx.isFieldPresent(sfGas))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "Bytecode requires Gas";
|
||||
return tefBYTECODE_NOT_INCLUDED;
|
||||
}
|
||||
|
||||
// The limits are voted, so they can change between ledgers.
|
||||
// A `tef`, unlike a `tem`, does not mark the transaction bad
|
||||
// for the hold time.
|
||||
auto const& fees = ctx.view.fees();
|
||||
if (isBytecodeExecutionDisabled(fees))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "WASM runtime deactivated by fee voting";
|
||||
return tefTEMP_DISABLED;
|
||||
}
|
||||
|
||||
if (ctx.tx[sfGas] > fees.gasLimit)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "Gas over voted limit: " << ctx.tx[sfGas];
|
||||
return tefEXCEEDS_LIMIT;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ctx.tx.isFieldPresent(sfGas))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "Bytecode not present, "
|
||||
"Gas present";
|
||||
return tefNO_BYTECODE;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ctx.view.rules().enabled(featureTokenEscrow))
|
||||
{
|
||||
AccountID const dest = (*escrow)[sfDestination];
|
||||
STAmount const amount = (*escrow)[sfAmount];
|
||||
|
||||
if (!isXRP(amount))
|
||||
{
|
||||
if (auto const ret = std::visit(
|
||||
[&]<typename T>(T const&) {
|
||||
return escrowUnlockPreclaimHelper<T>(ctx.view, dest, amount);
|
||||
},
|
||||
amount.asset().value());
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
}
|
||||
if (auto const ret = std::visit(
|
||||
[&]<typename T>(T const&) {
|
||||
return escrowUnlockPreclaimHelper<T>(ctx.view, dest, amount);
|
||||
},
|
||||
amount.asset().value());
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
return tesSUCCESS;
|
||||
@@ -262,8 +193,7 @@ EscrowFinish::doApply()
|
||||
EscrowEntryW escrow(ctx_.tx[sfOwner], seqProxy, ctx_.view(), j_);
|
||||
if (!escrow)
|
||||
{
|
||||
if (ctx_.view().rules().enabled(featureTokenEscrow) ||
|
||||
ctx_.view().rules().enabled(featureSmartEscrow))
|
||||
if (ctx_.view().rules().enabled(featureTokenEscrow))
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
return tecNO_TARGET;
|
||||
@@ -281,20 +211,6 @@ EscrowFinish::doApply()
|
||||
if ((*escrow)[~sfCancelAfter] && after(now, (*escrow)[sfCancelAfter]))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
AccountID const destID = (*escrow)[sfDestination];
|
||||
auto const sled = ctx_.view().peek(keylet::account(destID));
|
||||
if (ctx_.view().rules().enabled(featureSmartEscrow))
|
||||
{
|
||||
// NOTE: Escrow payments cannot be used to fund accounts.
|
||||
if (!sled)
|
||||
return tecNO_DST;
|
||||
|
||||
if (auto err =
|
||||
verifyDepositPreauth(ctx_.tx, ctx_.view(), accountID_, destID, sled, ctx_.journal);
|
||||
!isTesSuccess(err))
|
||||
return err;
|
||||
}
|
||||
|
||||
// Check cryptocondition fulfillment
|
||||
{
|
||||
auto const id = ctx_.tx.getTransactionID();
|
||||
@@ -348,33 +264,16 @@ EscrowFinish::doApply()
|
||||
return tecCRYPTOCONDITION_ERROR;
|
||||
}
|
||||
|
||||
if (!ctx_.view().rules().enabled(featureSmartEscrow))
|
||||
{
|
||||
// NOTE: Escrow payments cannot be used to fund accounts.
|
||||
if (!sled)
|
||||
return tecNO_DST;
|
||||
// NOTE: Escrow payments cannot be used to fund accounts.
|
||||
AccountID const destID = (*escrow)[sfDestination];
|
||||
auto const sled = ctx_.view().peek(keylet::account(destID));
|
||||
if (!sled)
|
||||
return tecNO_DST;
|
||||
|
||||
if (auto err =
|
||||
verifyDepositPreauth(ctx_.tx, ctx_.view(), accountID_, destID, sled, ctx_.journal);
|
||||
!isTesSuccess(err))
|
||||
return err;
|
||||
}
|
||||
|
||||
// Execute custom release function
|
||||
if ((*escrow)[~sfBytecode])
|
||||
{
|
||||
// TODO(SmartEscrow): run the escrow's contract here once the WASM
|
||||
// engine lands. That change also brings with it the reporting the run
|
||||
// produces - the gas it consumed and the code it returned, in the
|
||||
// transaction metadata - and the tecBYTECODE_REJECTED result that lets
|
||||
// a rejected escrow survive with the data the contract wrote.
|
||||
//
|
||||
// Until then an escrow carrying bytecode can be created, funded and
|
||||
// cancelled, but not finished. Reachable only under featureSmartEscrow,
|
||||
// which is Supported::No.
|
||||
JLOG(j_.debug()) << "EscrowFinish: no WASM engine to run the escrow's bytecode";
|
||||
return tecFAILED_PROCESSING;
|
||||
}
|
||||
if (auto err =
|
||||
verifyDepositPreauth(ctx_.tx, ctx_.view(), accountID_, destID, sled, ctx_.journal);
|
||||
!isTesSuccess(err))
|
||||
return err;
|
||||
|
||||
AccountID const account = (*escrow)[sfAccount];
|
||||
|
||||
@@ -402,18 +301,13 @@ EscrowFinish::doApply()
|
||||
}
|
||||
}
|
||||
|
||||
auto const reserveToSubtract = calculateAdditionalReserve((*escrow)[~sfBytecode]);
|
||||
|
||||
// Delivery can auto-create the destination's holding; the removed escrow
|
||||
// must not be counted against its reserve. The two share a reserve payer
|
||||
// for a self-escrow, or when one sponsor covers both.
|
||||
bool const recycleReserve =
|
||||
ctx_.view().rules().enabled(featureSponsor) || ctx_.view().rules().enabled(fixCleanup3_4_0);
|
||||
if (recycleReserve)
|
||||
{
|
||||
decreaseOwnerCountForObject(
|
||||
ctx_.view(), account, escrow.mutableRawSle(), reserveToSubtract, ctx_.journal);
|
||||
}
|
||||
decreaseOwnerCountForObject(ctx_.view(), account, escrow.mutableRawSle(), 1, ctx_.journal);
|
||||
|
||||
STAmount const amount = escrow->getFieldAmount(sfAmount);
|
||||
// Transfer amount to destination
|
||||
@@ -465,10 +359,7 @@ EscrowFinish::doApply()
|
||||
ctx_.view().update(sled);
|
||||
|
||||
if (!recycleReserve)
|
||||
{
|
||||
decreaseOwnerCountForObject(
|
||||
ctx_.view(), account, escrow.mutableRawSle(), reserveToSubtract, ctx_.journal);
|
||||
}
|
||||
decreaseOwnerCountForObject(ctx_.view(), account, escrow.mutableRawSle(), 1, ctx_.journal);
|
||||
|
||||
// Remove escrow from ledger
|
||||
escrow.erase();
|
||||
|
||||
@@ -37,7 +37,7 @@ TransactionProposalCancel::preclaim(PreclaimContext const& ctx)
|
||||
|
||||
// Never read the zero keylet. Preflight already rejected a zero
|
||||
// ProposalID, and no real proposal can have one (every ProposalID is a
|
||||
// hash), so reaching here with zero is an internal error (and a mathematical rarity).
|
||||
// hash), so reaching here with zero is an internal error. (and a mathematical rarity. )
|
||||
if (proposalID == beast::kZero)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
@@ -895,9 +894,8 @@ public:
|
||||
auto const jNoCred = credentials::ledgerEntry(env, becky, carol, credType);
|
||||
BEAST_EXPECT(
|
||||
jNoCred.isObject() && jNoCred.isMember(jss::result) &&
|
||||
jNoCred[jss::result][jss::error] == "entryNotFound" &&
|
||||
jNoCred[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
jNoCred[jss::result][jss::error_message] == "Entry not found.");
|
||||
jNoCred[jss::result].isMember(jss::error) &&
|
||||
jNoCred[jss::result][jss::error] == "entryNotFound");
|
||||
}
|
||||
|
||||
testcase("Credentials that aren't required");
|
||||
@@ -920,9 +918,8 @@ public:
|
||||
|
||||
BEAST_EXPECT(
|
||||
jNoCred.isObject() && jNoCred.isMember(jss::result) &&
|
||||
jNoCred[jss::result][jss::error] == "entryNotFound" &&
|
||||
jNoCred[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
jNoCred[jss::result][jss::error_message] == "Entry not found.");
|
||||
jNoCred[jss::result].isMember(jss::error) &&
|
||||
jNoCred[jss::result][jss::error] == "entryNotFound");
|
||||
}
|
||||
|
||||
{
|
||||
@@ -960,9 +957,8 @@ public:
|
||||
|
||||
BEAST_EXPECT(
|
||||
jNoCred.isObject() && jNoCred.isMember(jss::result) &&
|
||||
jNoCred[jss::result][jss::error] == "entryNotFound" &&
|
||||
jNoCred[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
jNoCred[jss::result][jss::error_message] == "Entry not found.");
|
||||
jNoCred[jss::result].isMember(jss::error) &&
|
||||
jNoCred[jss::result][jss::error] == "entryNotFound");
|
||||
}
|
||||
|
||||
testcase("Expired credentials");
|
||||
@@ -998,9 +994,8 @@ public:
|
||||
auto jv = credentials::ledgerEntry(env, john, carol, credType);
|
||||
BEAST_EXPECT(
|
||||
jv.isObject() && jv.isMember(jss::result) &&
|
||||
jv[jss::result][jss::error] == "entryNotFound" &&
|
||||
jv[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
jv[jss::result][jss::error_message] == "Entry not found.");
|
||||
jv[jss::result].isMember(jss::error) &&
|
||||
jv[jss::result][jss::error] == "entryNotFound");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1084,9 +1079,8 @@ public:
|
||||
auto const jv = credentials::ledgerEntry(env, becky, carol, credType);
|
||||
BEAST_EXPECT(
|
||||
jv.isObject() && jv.isMember(jss::result) &&
|
||||
jv[jss::result][jss::error] == "entryNotFound" &&
|
||||
jv[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
jv[jss::result][jss::error_message] == "Entry not found.");
|
||||
jv[jss::result].isMember(jss::error) &&
|
||||
jv[jss::result][jss::error] == "entryNotFound");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1127,9 +1121,8 @@ public:
|
||||
auto const jv = credentials::ledgerEntry(env, becky, carol, credType);
|
||||
BEAST_EXPECT(
|
||||
jv.isObject() && jv.isMember(jss::result) &&
|
||||
jv[jss::result][jss::error] == "entryNotFound" &&
|
||||
jv[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
jv[jss::result][jss::error_message] == "Entry not found.");
|
||||
jv[jss::result].isMember(jss::error) &&
|
||||
jv[jss::result][jss::error] == "entryNotFound");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,6 @@
|
||||
#include <xrpl/ledger/OpenView.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Batch.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
@@ -2801,7 +2800,6 @@ class Batch_test : public beast::unit_test::Suite
|
||||
auto const jrr = env.rpc("submit", strHex(slice))[jss::result];
|
||||
expect(
|
||||
jrr[jss::status] == "error" && jrr[jss::error] == "invalidTransaction" &&
|
||||
!jrr.isMember(jss::error_code) && jrr[jss::error_message].isNull() &&
|
||||
jrr[jss::error_exception] ==
|
||||
"fails local checks: Batch inner transactions are never "
|
||||
"considered validly signed.",
|
||||
@@ -5409,10 +5407,7 @@ class Batch_test : public beast::unit_test::Suite
|
||||
|
||||
// The delegated payment never applied.
|
||||
BEAST_EXPECT(env.balance(mallory) == preMallory);
|
||||
auto const txResult = env.rpc("tx", txIDs[1])[jss::result];
|
||||
BEAST_EXPECT(txResult[jss::error] == "txnNotFound");
|
||||
BEAST_EXPECT(txResult[jss::error_code] == RpcTxnNotFound);
|
||||
BEAST_EXPECT(txResult[jss::error_message] == "Transaction not found.");
|
||||
BEAST_EXPECT(env.rpc("tx", txIDs[1])[jss::result][jss::error] == "txnNotFound");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5823,10 +5818,7 @@ class Batch_test : public beast::unit_test::Suite
|
||||
validateClosedLedger(env, testCases);
|
||||
|
||||
// verify jv2 is not present in the closed ledger.
|
||||
auto const txResult = env.rpc("tx", txIDs[1])[jss::result];
|
||||
BEAST_EXPECT(txResult[jss::error] == "txnNotFound");
|
||||
BEAST_EXPECT(txResult[jss::error_code] == RpcTxnNotFound);
|
||||
BEAST_EXPECT(txResult[jss::error_message] == "Transaction not found.");
|
||||
BEAST_EXPECT(env.rpc("tx", txIDs[1])[jss::result][jss::error] == "txnNotFound");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/CaptureLogs.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
#include <test/jtx/amount.h>
|
||||
@@ -15,17 +14,13 @@
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
@@ -808,108 +803,6 @@ class Clawback_test : public beast::unit_test::Suite
|
||||
BEAST_EXPECT(ownerCount(env, bob) == 0);
|
||||
}
|
||||
|
||||
void
|
||||
testSubResolutionAmount(FeatureBitset features)
|
||||
{
|
||||
testcase("Sub-resolution amount");
|
||||
using namespace test::jtx;
|
||||
|
||||
// IOU balances keep 16 significant digits. When alice claws back an
|
||||
// amount much smaller than bob's balance, the balance changes by the
|
||||
// rounded amount, which can be zero, less, or more than requested. The
|
||||
// clawback invariant must accept that instead of failing the
|
||||
// transaction under MPTokensV2 or logging a failure without it.
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
|
||||
struct Case
|
||||
{
|
||||
std::uint64_t balanceMantissa;
|
||||
int balanceExponent;
|
||||
std::uint64_t clawMantissa;
|
||||
int clawExponent;
|
||||
std::uint64_t afterMantissa;
|
||||
int afterExponent;
|
||||
};
|
||||
std::array const cases{
|
||||
// 1e7 - 1e-10 rounds back to 1e7
|
||||
Case{
|
||||
.balanceMantissa = 10'000'000,
|
||||
.balanceExponent = 0,
|
||||
.clawMantissa = 1,
|
||||
.clawExponent = -10,
|
||||
.afterMantissa = 10'000'000,
|
||||
.afterExponent = 0,
|
||||
},
|
||||
// 1234567890.123456 - 0.0000005 rounds back to the balance
|
||||
Case{
|
||||
.balanceMantissa = 1'234'567'890'123'456,
|
||||
.balanceExponent = -6,
|
||||
.clawMantissa = 5,
|
||||
.clawExponent = -7,
|
||||
.afterMantissa = 1'234'567'890'123'456,
|
||||
.afterExponent = -6,
|
||||
},
|
||||
// 0.0000014 removes 0.000001
|
||||
Case{
|
||||
.balanceMantissa = 1'234'567'890'123'456,
|
||||
.balanceExponent = -6,
|
||||
.clawMantissa = 14,
|
||||
.clawExponent = -7,
|
||||
.afterMantissa = 1'234'567'890'123'455,
|
||||
.afterExponent = -6,
|
||||
},
|
||||
// 0.0000016 removes 0.000002
|
||||
Case{
|
||||
.balanceMantissa = 1'234'567'890'123'456,
|
||||
.balanceExponent = -6,
|
||||
.clawMantissa = 16,
|
||||
.clawExponent = -7,
|
||||
.afterMantissa = 1'234'567'890'123'454,
|
||||
.afterExponent = -6,
|
||||
},
|
||||
};
|
||||
|
||||
for (auto const feat : {features, features - featureMPTokensV2})
|
||||
{
|
||||
for (auto const& c : cases)
|
||||
{
|
||||
std::string logs;
|
||||
{
|
||||
Env env(*this, feat, std::make_unique<test::CaptureLogs>(&logs));
|
||||
|
||||
env.fund(XRP(1000), alice, bob);
|
||||
env.close();
|
||||
|
||||
auto const usd = alice["USD"];
|
||||
|
||||
env(fset(alice, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
|
||||
env.trust(usd(10'000'000'000), bob);
|
||||
env(pay(
|
||||
alice, bob, STAmount{usd.issue(), c.balanceMantissa, c.balanceExponent}));
|
||||
env.close();
|
||||
|
||||
env(claw(
|
||||
alice,
|
||||
STAmount{
|
||||
Issue{usd.currency, bob.id()}, c.clawMantissa, c.clawExponent}),
|
||||
Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(
|
||||
env.balance(bob, usd).value() ==
|
||||
STAmount(usd.issue(), c.afterMantissa, c.afterExponent));
|
||||
}
|
||||
// Without MPTokensV2 a failed invariant only logs and the
|
||||
// transaction still succeeds, so check the logs instead.
|
||||
if (!feat[featureMPTokensV2])
|
||||
BEAST_EXPECT(!logs.contains("Invariant failed"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testTickets(FeatureBitset features)
|
||||
{
|
||||
@@ -979,7 +872,6 @@ class Clawback_test : public beast::unit_test::Suite
|
||||
testDeleteDefaultLine(features);
|
||||
testFrozenLine(features);
|
||||
testAmountExceedsAvailable(features);
|
||||
testSubResolutionAmount(features);
|
||||
testTickets(features);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/ConfidentialTransfer.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
@@ -260,9 +259,8 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase
|
||||
|
||||
auto const credentialDeleted = [&](Env& env, Account const& subject) -> bool {
|
||||
auto const credentials = credentials::ledgerEntry(env, subject, dpIssuer, credType);
|
||||
return credentials[jss::result][jss::error] == "entryNotFound" &&
|
||||
credentials[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
credentials[jss::result][jss::error_message] == "Entry not found.";
|
||||
return credentials[jss::result].isMember(jss::error) &&
|
||||
credentials[jss::result][jss::error] == "entryNotFound";
|
||||
};
|
||||
|
||||
// TEST 4: Expired credential with matching depositPreauth entry.
|
||||
|
||||
@@ -7944,8 +7944,6 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
|
||||
// Signature verification fails — rejected before ZKP check
|
||||
auto const jr = env.rpc("submit", strHex(tampered.slice()));
|
||||
BEAST_EXPECT(jr[jss::result][jss::error] == "invalidTransaction");
|
||||
BEAST_EXPECT(!jr[jss::result].isMember(jss::error_code));
|
||||
BEAST_EXPECT(jr[jss::result][jss::error_message].isNull());
|
||||
}
|
||||
|
||||
// Variant B: Re-signed with inflated ciphertext.
|
||||
@@ -8053,8 +8051,6 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
|
||||
|
||||
auto const jr = env.rpc("submit", strHex(tampered.slice()));
|
||||
BEAST_EXPECT(jr[jss::result][jss::error] == "invalidTransaction");
|
||||
BEAST_EXPECT(!jr[jss::result].isMember(jss::error_code));
|
||||
BEAST_EXPECT(jr[jss::result][jss::error_message].isNull());
|
||||
}
|
||||
|
||||
// Variant B: Re-signed with all negated ciphertexts.
|
||||
@@ -8158,8 +8154,6 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
|
||||
|
||||
auto const jr = env.rpc("submit", strHex(tampered.slice()));
|
||||
BEAST_EXPECT(jr[jss::result][jss::error] == "invalidTransaction");
|
||||
BEAST_EXPECT(!jr[jss::result].isMember(jss::error_code));
|
||||
BEAST_EXPECT(jr[jss::result][jss::error_message].isNull());
|
||||
}
|
||||
|
||||
// Variant B: Re-signed with combined ciphertext.
|
||||
@@ -8284,8 +8278,6 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
|
||||
// Signature verification fails
|
||||
auto const jr = env.rpc("submit", strHex(tampered.slice()));
|
||||
BEAST_EXPECT(jr[jss::result][jss::error] == "invalidTransaction");
|
||||
BEAST_EXPECT(!jr[jss::result].isMember(jss::error_code));
|
||||
BEAST_EXPECT(jr[jss::result][jss::error_message].isNull());
|
||||
}
|
||||
|
||||
// Variant B: Re-signed C1 substitution.
|
||||
@@ -8376,8 +8368,6 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
|
||||
// signed — transaction rejected before ZKP verification.
|
||||
auto const jr = env.rpc("submit", strHex(tampered.slice()));
|
||||
BEAST_EXPECT(jr[jss::result][jss::error] == "invalidTransaction");
|
||||
BEAST_EXPECT(!jr[jss::result].isMember(jss::error_code));
|
||||
BEAST_EXPECT(jr[jss::result][jss::error_message].isNull());
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
#include <xrpl/ledger/ApplyViewImpl.h>
|
||||
#include <xrpl/ledger/helpers/CredentialHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
@@ -125,9 +124,8 @@ struct Credentials_test : public beast::unit_test::Suite
|
||||
auto const jle = credentials::ledgerEntry(env, subject, issuer, credType);
|
||||
BEAST_EXPECT(
|
||||
jle.isObject() && jle.isMember(jss::result) &&
|
||||
jle[jss::result][jss::error] == "entryNotFound" &&
|
||||
jle[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
jle[jss::result][jss::error_message] == "Entry not found.");
|
||||
jle[jss::result].isMember(jss::error) &&
|
||||
jle[jss::result][jss::error] == "entryNotFound");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,9 +173,8 @@ struct Credentials_test : public beast::unit_test::Suite
|
||||
auto const jle = credentials::ledgerEntry(env, issuer, issuer, credType);
|
||||
BEAST_EXPECT(
|
||||
jle.isObject() && jle.isMember(jss::result) &&
|
||||
jle[jss::result][jss::error] == "entryNotFound" &&
|
||||
jle[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
jle[jss::result][jss::error_message] == "Entry not found.");
|
||||
jle[jss::result].isMember(jss::error) &&
|
||||
jle[jss::result][jss::error] == "entryNotFound");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -225,9 +222,8 @@ struct Credentials_test : public beast::unit_test::Suite
|
||||
auto const jle = credentials::ledgerEntry(env, subject, issuer, credType);
|
||||
BEAST_EXPECT(
|
||||
jle.isObject() && jle.isMember(jss::result) &&
|
||||
jle[jss::result][jss::error] == "entryNotFound" &&
|
||||
jle[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
jle[jss::result][jss::error_message] == "Entry not found.");
|
||||
jle[jss::result].isMember(jss::error) &&
|
||||
jle[jss::result][jss::error] == "entryNotFound");
|
||||
}
|
||||
|
||||
// resurrection
|
||||
@@ -263,9 +259,8 @@ struct Credentials_test : public beast::unit_test::Suite
|
||||
auto const jle = credentials::ledgerEntry(env, subject, issuer, credType);
|
||||
BEAST_EXPECT(
|
||||
jle.isObject() && jle.isMember(jss::result) &&
|
||||
jle[jss::result][jss::error] == "entryNotFound" &&
|
||||
jle[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
jle[jss::result][jss::error_message] == "Entry not found.");
|
||||
jle[jss::result].isMember(jss::error) &&
|
||||
jle[jss::result][jss::error] == "entryNotFound");
|
||||
}
|
||||
|
||||
// resurrection
|
||||
@@ -299,9 +294,8 @@ struct Credentials_test : public beast::unit_test::Suite
|
||||
auto const jle = credentials::ledgerEntry(env, subject, issuer, credType);
|
||||
BEAST_EXPECT(
|
||||
jle.isObject() && jle.isMember(jss::result) &&
|
||||
jle[jss::result][jss::error] == "entryNotFound" &&
|
||||
jle[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
jle[jss::result][jss::error_message] == "Entry not found.");
|
||||
jle[jss::result].isMember(jss::error) &&
|
||||
jle[jss::result][jss::error] == "entryNotFound");
|
||||
}
|
||||
|
||||
// resurrection
|
||||
@@ -337,9 +331,8 @@ struct Credentials_test : public beast::unit_test::Suite
|
||||
auto const jle = credentials::ledgerEntry(env, subject, issuer, credType);
|
||||
BEAST_EXPECT(
|
||||
jle.isObject() && jle.isMember(jss::result) &&
|
||||
jle[jss::result][jss::error] == "entryNotFound" &&
|
||||
jle[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
jle[jss::result][jss::error_message] == "Entry not found.");
|
||||
jle[jss::result].isMember(jss::error) &&
|
||||
jle[jss::result][jss::error] == "entryNotFound");
|
||||
}
|
||||
|
||||
// resurrection
|
||||
@@ -375,9 +368,8 @@ struct Credentials_test : public beast::unit_test::Suite
|
||||
auto const jle = credentials::ledgerEntry(env, subject, issuer, credType);
|
||||
BEAST_EXPECT(
|
||||
jle.isObject() && jle.isMember(jss::result) &&
|
||||
jle[jss::result][jss::error] == "entryNotFound" &&
|
||||
jle[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
jle[jss::result][jss::error_message] == "Entry not found.");
|
||||
jle[jss::result].isMember(jss::error) &&
|
||||
jle[jss::result][jss::error] == "entryNotFound");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,9 +390,8 @@ struct Credentials_test : public beast::unit_test::Suite
|
||||
auto const jle = credentials::ledgerEntry(env, subject, issuer, credType);
|
||||
BEAST_EXPECT(
|
||||
jle.isObject() && jle.isMember(jss::result) &&
|
||||
jle[jss::result][jss::error] == "entryNotFound" &&
|
||||
jle[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
jle[jss::result][jss::error_message] == "Entry not found.");
|
||||
jle[jss::result].isMember(jss::error) &&
|
||||
jle[jss::result][jss::error] == "entryNotFound");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,9 +410,8 @@ struct Credentials_test : public beast::unit_test::Suite
|
||||
auto const jle = credentials::ledgerEntry(env, subject, issuer, credType);
|
||||
BEAST_EXPECT(
|
||||
jle.isObject() && jle.isMember(jss::result) &&
|
||||
jle[jss::result][jss::error] == "entryNotFound" &&
|
||||
jle[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
jle[jss::result][jss::error_message] == "Entry not found.");
|
||||
jle[jss::result].isMember(jss::error) &&
|
||||
jle[jss::result][jss::error] == "entryNotFound");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -754,9 +744,8 @@ struct Credentials_test : public beast::unit_test::Suite
|
||||
auto const jDelCred = credentials::ledgerEntry(env, subject, issuer, credType2);
|
||||
BEAST_EXPECT(
|
||||
jDelCred.isObject() && jDelCred.isMember(jss::result) &&
|
||||
jDelCred[jss::result][jss::error] == "entryNotFound" &&
|
||||
jDelCred[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
jDelCred[jss::result][jss::error_message] == "Entry not found.");
|
||||
jDelCred[jss::result].isMember(jss::error) &&
|
||||
jDelCred[jss::result][jss::error] == "entryNotFound");
|
||||
|
||||
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
||||
BEAST_EXPECT(ownerCount(env, subject) == 1);
|
||||
@@ -792,9 +781,8 @@ struct Credentials_test : public beast::unit_test::Suite
|
||||
auto const jDelCred = credentials::ledgerEntry(env, subject, issuer, credType);
|
||||
BEAST_EXPECT(
|
||||
jDelCred.isObject() && jDelCred.isMember(jss::result) &&
|
||||
jDelCred[jss::result][jss::error] == "entryNotFound" &&
|
||||
jDelCred[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
jDelCred[jss::result][jss::error_message] == "Entry not found.");
|
||||
jDelCred[jss::result].isMember(jss::error) &&
|
||||
jDelCred[jss::result][jss::error] == "entryNotFound");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,6 @@
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/ledger/Dir.h>
|
||||
#include <xrpl/ledger/helpers/DelegateHelpers.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
@@ -160,8 +159,6 @@ class Delegate_test : public beast::unit_test::Suite
|
||||
env.close();
|
||||
auto const jle = delegate::entry(env, gw, alice);
|
||||
BEAST_EXPECT(jle[jss::result][jss::error] == "entryNotFound");
|
||||
BEAST_EXPECT(jle[jss::result][jss::error_code] == RpcEntryNotFound);
|
||||
BEAST_EXPECT(jle[jss::result][jss::error_message] == "Entry not found.");
|
||||
|
||||
// alice can delegate permissions to gw as well
|
||||
env(delegate::set(alice, gw, permissions));
|
||||
@@ -170,8 +167,6 @@ class Delegate_test : public beast::unit_test::Suite
|
||||
auto const response = delegate::entry(env, gw, alice);
|
||||
// alice has not been granted any permissions by gw
|
||||
BEAST_EXPECT(response[jss::result][jss::error] == "entryNotFound");
|
||||
BEAST_EXPECT(response[jss::result][jss::error_code] == RpcEntryNotFound);
|
||||
BEAST_EXPECT(response[jss::result][jss::error_message] == "Entry not found.");
|
||||
}
|
||||
|
||||
void
|
||||
@@ -2785,11 +2780,9 @@ class Delegate_test : public beast::unit_test::Suite
|
||||
jv[jss::secret] = bob.name();
|
||||
auto jrr = env.rpc("json", "sign_for", to_string(jv))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::status] == "error");
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
jrr[jss::error_message] ==
|
||||
"A Signer may not be the transaction's Account (" + bob.human() + ").");
|
||||
jrr[jss::error_message].asString().contains(
|
||||
"A Signer may not be the transaction's Account"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
@@ -1144,9 +1143,8 @@ struct DepositPreauth_test : public beast::unit_test::Suite
|
||||
ledgerEntryDepositPreauth(env, bob, {{.issuer = issuer, .credType = credType}});
|
||||
BEAST_EXPECT(
|
||||
jDP.isObject() && jDP.isMember(jss::result) &&
|
||||
jDP[jss::result][jss::error] == "entryNotFound" &&
|
||||
jDP[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
jDP[jss::result][jss::error_message] == "Entry not found.");
|
||||
jDP[jss::result].isMember(jss::error) &&
|
||||
jDP[jss::result][jss::error] == "entryNotFound");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1234,9 +1232,8 @@ struct DepositPreauth_test : public beast::unit_test::Suite
|
||||
auto const jDelCred = credentials::ledgerEntry(env, alice, issuer, credType);
|
||||
BEAST_EXPECT(
|
||||
jDelCred.isObject() && jDelCred.isMember(jss::result) &&
|
||||
jDelCred[jss::result][jss::error] == "entryNotFound" &&
|
||||
jDelCred[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
jDelCred[jss::result][jss::error_message] == "Entry not found.");
|
||||
jDelCred[jss::result].isMember(jss::error) &&
|
||||
jDelCred[jss::result][jss::error] == "entryNotFound");
|
||||
}
|
||||
|
||||
{
|
||||
@@ -1286,9 +1283,8 @@ struct DepositPreauth_test : public beast::unit_test::Suite
|
||||
auto const jDelCred = credentials::ledgerEntry(env, gw, issuer, credType);
|
||||
BEAST_EXPECT(
|
||||
jDelCred.isObject() && jDelCred.isMember(jss::result) &&
|
||||
jDelCred[jss::result][jss::error] == "entryNotFound" &&
|
||||
jDelCred[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
jDelCred[jss::result][jss::error_message] == "Entry not found.");
|
||||
jDelCred[jss::result].isMember(jss::error) &&
|
||||
jDelCred[jss::result][jss::error] == "entryNotFound");
|
||||
|
||||
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
||||
BEAST_EXPECT(ownerCount(env, gw) == 0);
|
||||
@@ -1362,9 +1358,8 @@ struct DepositPreauth_test : public beast::unit_test::Suite
|
||||
auto const jDelCred = credentials::ledgerEntry(env, zelda, issuer, credType);
|
||||
BEAST_EXPECT(
|
||||
jDelCred.isObject() && jDelCred.isMember(jss::result) &&
|
||||
jDelCred[jss::result][jss::error] == "entryNotFound" &&
|
||||
jDelCred[jss::result][jss::error_code] == RpcEntryNotFound &&
|
||||
jDelCred[jss::result][jss::error_message] == "Entry not found.");
|
||||
jDelCred[jss::result].isMember(jss::error) &&
|
||||
jDelCred[jss::result][jss::error] == "entryNotFound");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,787 +0,0 @@
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/envconfig.h>
|
||||
#include <test/jtx/escrow.h>
|
||||
#include <test/jtx/fee.h>
|
||||
#include <test/jtx/ter.h>
|
||||
|
||||
#include <xrpld/core/Config.h>
|
||||
|
||||
#include <xrpl/basics/strHex.h>
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/config/Constants.h>
|
||||
#include <xrpl/core/StartUpType.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Fees.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/SeqProxy.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
/**
|
||||
* Smart Escrow's `Bytecode`, `Data` and `Gas` fields.
|
||||
*
|
||||
* The WASM engine that runs an escrow's bytecode is not in this build, so the
|
||||
* `Bytecode` field is opaque here: `EscrowCreate` weighs its size and nothing
|
||||
* reads it, and `EscrowFinish` has nothing to run it with. Everything around
|
||||
* the run is testable, and is tested below - the preflight and preclaim rules,
|
||||
* the fee the bytecode costs, the owner reserve it occupies, and the refund of
|
||||
* that reserve.
|
||||
*
|
||||
* What waits on the engine: a successful finish, tecBYTECODE_REJECTED, the
|
||||
* gas and return code the run reports in the transaction metadata, and the
|
||||
* `Data` a rejected contract leaves behind.
|
||||
*/
|
||||
struct EscrowSmart_test : public beast::unit_test::Suite
|
||||
{
|
||||
// A blob of `bytes` bytes, as hex. Opens with the WASM preamble - "\0asm"
|
||||
// and version 1 - so a reader can see what the field is meant to carry,
|
||||
// but nothing in this build parses it.
|
||||
static std::string
|
||||
bytecodeOfSize(std::size_t bytes)
|
||||
{
|
||||
static std::string const kPreamble = "0061736D01000000";
|
||||
std::string hex = kPreamble;
|
||||
hex.append((bytes * 2) - kPreamble.size(), 'A');
|
||||
return hex;
|
||||
}
|
||||
|
||||
// What EscrowCreate charges for `bytecodeHex`: ten base fees, plus five
|
||||
// drops per byte.
|
||||
static XRPAmount
|
||||
createFeeFor(jtx::Env const& env, std::string const& bytecodeHex)
|
||||
{
|
||||
return env.current()->fees().base * 10 + bytecodeHex.size() / 2 * 5;
|
||||
}
|
||||
|
||||
// A validator's config that votes for `value` on the fee setting `key`.
|
||||
static std::unique_ptr<Config>
|
||||
votingFor(std::string const& key, std::uint32_t value)
|
||||
{
|
||||
auto cfg = jtx::envconfig(jtx::validator, "");
|
||||
cfg->section(Sections::kVoting).set(key, std::to_string(value));
|
||||
return cfg;
|
||||
}
|
||||
|
||||
// Close past the flag ledger, so the vote lands in FeeSettings.
|
||||
static void
|
||||
closePastFlagLedger(jtx::Env& env)
|
||||
{
|
||||
for (auto i = env.current()->seq(); i <= 257; ++i)
|
||||
env.close();
|
||||
}
|
||||
|
||||
// What EscrowFinish charges for `gas`: the gas cost rounded up to the drop.
|
||||
static XRPAmount
|
||||
finishFeeFor(jtx::Env const& env, std::uint64_t gas)
|
||||
{
|
||||
return env.current()->fees().base +
|
||||
((gas * env.current()->fees().gasPrice) + microDropsPerDrop - 1) / microDropsPerDrop;
|
||||
}
|
||||
|
||||
void
|
||||
testCreatePreflight(FeatureBitset features)
|
||||
{
|
||||
testcase("EscrowCreate preflight");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace std::chrono;
|
||||
|
||||
Account const alice{"alice"};
|
||||
Account const carol{"carol"};
|
||||
|
||||
auto const bytecode = bytecodeOfSize(64);
|
||||
|
||||
{
|
||||
// featureSmartEscrow disabled
|
||||
Env env(*this, features - featureSmartEscrow);
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
XRPAmount const txnFees = env.current()->fees().base + 1000;
|
||||
auto const escrowCreate = escrow::create(alice, carol, XRP(1000));
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 100s),
|
||||
Fee(txnFees),
|
||||
Ter(temDISABLED));
|
||||
env.close();
|
||||
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 100s),
|
||||
escrow::Data("00112233"),
|
||||
Fee(txnFees),
|
||||
Ter(temDISABLED));
|
||||
env.close();
|
||||
|
||||
env(escrowCreate,
|
||||
escrow::Data("00112233"),
|
||||
escrow::kFinishTime(env.now() + 100s),
|
||||
Fee(txnFees),
|
||||
Ter(temDISABLED));
|
||||
env.close();
|
||||
}
|
||||
|
||||
{
|
||||
// Bytecode > max length
|
||||
Env env(*this, votingFor(Keys::kBytecodeSizeLimit, 10), features); // 10 bytes
|
||||
closePastFlagLedger(env);
|
||||
XRPAmount const txnFees = env.current()->fees().base + 1000;
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
|
||||
auto const escrowCreate = escrow::create(alice, carol, XRP(500));
|
||||
|
||||
// 11-byte string
|
||||
std::string const longBytecode = "00112233445566778899AA";
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(longBytecode),
|
||||
escrow::kCancelTime(env.now() + 100s),
|
||||
Fee(txnFees),
|
||||
Ter(tefEXCEEDS_LIMIT));
|
||||
env.close();
|
||||
}
|
||||
|
||||
{
|
||||
// gas limit set to 0
|
||||
Env env(*this, votingFor(Keys::kGasLimit, 0), features); // WASM runtime disabled
|
||||
closePastFlagLedger(env);
|
||||
XRPAmount const txnFees = env.current()->fees().base + 1000;
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
|
||||
auto const escrowCreate = escrow::create(alice, carol, XRP(500));
|
||||
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 100s),
|
||||
Fee(txnFees),
|
||||
Ter(tefTEMP_DISABLED));
|
||||
env.close();
|
||||
}
|
||||
|
||||
{
|
||||
// size limit set to 0
|
||||
Env env(
|
||||
*this, votingFor(Keys::kBytecodeSizeLimit, 0), features); // WASM upload disabled
|
||||
closePastFlagLedger(env);
|
||||
XRPAmount const txnFees = env.current()->fees().base + 1000;
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
|
||||
auto const escrowCreate = escrow::create(alice, carol, XRP(500));
|
||||
|
||||
// 1-byte string
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode("AA"),
|
||||
escrow::kCancelTime(env.now() + 100s),
|
||||
Fee(txnFees),
|
||||
Ter(tefTEMP_DISABLED));
|
||||
env.close();
|
||||
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 100s),
|
||||
Fee(txnFees),
|
||||
Ter(tefTEMP_DISABLED));
|
||||
env.close();
|
||||
}
|
||||
|
||||
{
|
||||
// Data without Bytecode
|
||||
Env env(*this, features);
|
||||
XRPAmount const txnFees = env.current()->fees().base + 100000;
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
|
||||
auto const escrowCreate = escrow::create(alice, carol, XRP(500));
|
||||
|
||||
std::string const data(4, 'A');
|
||||
env(escrowCreate,
|
||||
escrow::Data(data),
|
||||
escrow::kFinishTime(env.now() + 100s),
|
||||
Fee(txnFees),
|
||||
Ter(temMALFORMED));
|
||||
env.close();
|
||||
}
|
||||
|
||||
{
|
||||
// Data > max length
|
||||
Env env(*this, features);
|
||||
XRPAmount const txnFees = env.current()->fees().base + 100000;
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
|
||||
auto const escrowCreate = escrow::create(alice, carol, XRP(500));
|
||||
|
||||
// string of length (kMaxWasmDataLength + 1) * 2
|
||||
std::string const longData((kMaxWasmDataLength + 1) * 2, 'B');
|
||||
env(escrowCreate,
|
||||
escrow::Data(longData),
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 100s),
|
||||
Fee(txnFees),
|
||||
Ter(temMALFORMED));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// Bytecode joins FinishAfter and Condition as a way to say how the
|
||||
// escrow completes, but it always needs a CancelAfter: nothing else
|
||||
// can free the funds if the contract never accepts.
|
||||
Env env(
|
||||
*this,
|
||||
envconfig([](std::unique_ptr<Config> cfg) {
|
||||
cfg->startUp = StartUpType::Fresh;
|
||||
return cfg;
|
||||
}),
|
||||
features);
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
|
||||
auto const escrowCreate = escrow::create(alice, carol, XRP(500));
|
||||
XRPAmount const txnFees = createFeeFor(env, bytecode);
|
||||
|
||||
// Success situations
|
||||
{
|
||||
// Bytecode + CancelAfter
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 20s),
|
||||
Fee(txnFees));
|
||||
env.close();
|
||||
}
|
||||
{
|
||||
// Bytecode + Condition + CancelAfter
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 30s),
|
||||
escrow::kCondition(escrow::kCb1),
|
||||
Fee(txnFees));
|
||||
env.close();
|
||||
}
|
||||
{
|
||||
// Bytecode + FinishAfter + CancelAfter
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 40s),
|
||||
escrow::kFinishTime(env.now() + 2s),
|
||||
Fee(txnFees));
|
||||
env.close();
|
||||
}
|
||||
{
|
||||
// Bytecode + Data + CancelAfter
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::Data("00112233"),
|
||||
escrow::kCancelTime(env.now() + 50s),
|
||||
Fee(txnFees));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// Failure situations (i.e. all other combinations)
|
||||
{
|
||||
// only Bytecode
|
||||
env(escrowCreate, escrow::Bytecode(bytecode), Fee(txnFees), Ter(temBAD_EXPIRATION));
|
||||
env.close();
|
||||
}
|
||||
{
|
||||
// Bytecode + FinishAfter
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kFinishTime(env.now() + 2s),
|
||||
Fee(txnFees),
|
||||
Ter(temBAD_EXPIRATION));
|
||||
env.close();
|
||||
}
|
||||
{
|
||||
// Bytecode + Condition
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCondition(escrow::kCb1),
|
||||
Fee(txnFees),
|
||||
Ter(temBAD_EXPIRATION));
|
||||
env.close();
|
||||
}
|
||||
{
|
||||
// Bytecode 0 length
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(""),
|
||||
escrow::kCancelTime(env.now() + 60s),
|
||||
Fee(txnFees),
|
||||
Ter(temMALFORMED));
|
||||
env.close();
|
||||
}
|
||||
{
|
||||
// Not enough fees
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 70s),
|
||||
Fee(txnFees - 1),
|
||||
Ter(telINSUF_FEE_P));
|
||||
env.close();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testCreateFeeAndReserve(FeatureBitset features)
|
||||
{
|
||||
testcase("EscrowCreate fee and reserve");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace std::chrono;
|
||||
|
||||
Account const alice{"alice"};
|
||||
Account const carol{"carol"};
|
||||
|
||||
{
|
||||
// The fee scales with the bytecode: ten base fees plus five drops
|
||||
// per byte, and a drop short of it is refused.
|
||||
Env env(*this, features);
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
env.close();
|
||||
|
||||
auto const bytecode = bytecodeOfSize(1'000);
|
||||
auto const escrowCreate = escrow::create(alice, carol, XRP(500));
|
||||
auto const fee = createFeeFor(env, bytecode);
|
||||
BEAST_EXPECT(fee == env.current()->fees().base * 10 + 5'000);
|
||||
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 100s),
|
||||
Fee(fee - 1),
|
||||
Ter(telINSUF_FEE_P));
|
||||
env.close();
|
||||
|
||||
auto const seq = env.seq(alice);
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::Data("00112233"),
|
||||
escrow::kCancelTime(env.now() + 100s),
|
||||
Fee(fee));
|
||||
env.close();
|
||||
|
||||
// The ledger entry carries both fields.
|
||||
auto const sle = env.le(keylet::escrow(alice, SeqProxy::rawSequence(seq)));
|
||||
if (BEAST_EXPECT(sle))
|
||||
{
|
||||
BEAST_EXPECT(sle->isFieldPresent(sfBytecode));
|
||||
BEAST_EXPECT(strHex(sle->getFieldVL(sfBytecode)) == bytecode);
|
||||
BEAST_EXPECT(strHex(sle->getFieldVL(sfData)) == "00112233");
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// The reserve is one owner increment per started 500 bytes of
|
||||
// bytecode.
|
||||
Env env(*this, features);
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
env.close();
|
||||
|
||||
std::uint32_t expectedOwnerCount = 0;
|
||||
for (auto const size : {
|
||||
std::size_t{64},
|
||||
std::size_t{500},
|
||||
std::size_t{501},
|
||||
std::size_t{1'000},
|
||||
std::size_t{1'001},
|
||||
})
|
||||
{
|
||||
auto const bytecode = bytecodeOfSize(size);
|
||||
env(escrow::create(alice, carol, XRP(100)),
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 1000s),
|
||||
Fee(createFeeFor(env, bytecode)));
|
||||
env.close();
|
||||
|
||||
expectedOwnerCount += (size + 499) / 500;
|
||||
BEAST_EXPECTS(
|
||||
env.ownerCount(alice) == expectedOwnerCount,
|
||||
std::to_string(size) + " bytes: " + std::to_string(env.ownerCount(alice)));
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// A bytecode escrow the owner cannot reserve is refused, where the
|
||||
// same escrow without bytecode would have been accepted.
|
||||
Env env(*this, features);
|
||||
// Base 200 XRP + 50 XRP per owner object, from the unit-test
|
||||
// config. Enough for one owner object, not for two.
|
||||
env.fund(XRP(300), alice, carol);
|
||||
env.close();
|
||||
|
||||
auto const bytecode = bytecodeOfSize(1'000);
|
||||
env(escrow::create(alice, carol, XRP(20)),
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 100s),
|
||||
Fee(createFeeFor(env, bytecode)),
|
||||
Ter(tecINSUFFICIENT_RESERVE));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.ownerCount(alice) == 0);
|
||||
|
||||
// The same escrow without bytecode costs one increment and fits.
|
||||
env(escrow::create(alice, carol, XRP(20)),
|
||||
escrow::kFinishTime(env.now() + 10s),
|
||||
escrow::kCancelTime(env.now() + 100s));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.ownerCount(alice) == 1);
|
||||
}
|
||||
|
||||
{
|
||||
// After the lock, an XRP escrow's owner must still hold the
|
||||
// reserve the bytecode costs, not the single increment a plain
|
||||
// escrow costs.
|
||||
Env env(*this, features);
|
||||
Account const dave{"dave"};
|
||||
Account const erin{"erin"};
|
||||
env.fund(XRP(5000), carol);
|
||||
env.close();
|
||||
|
||||
auto const bytecode = bytecodeOfSize(600); // two increments
|
||||
auto const fee = createFeeFor(env, bytecode);
|
||||
auto const amount = XRP(1'000);
|
||||
auto const fees = env.current()->fees();
|
||||
|
||||
// One increment short after the lock.
|
||||
env.fund(
|
||||
STAmount{fees.accountReserve(1, 1) + amount.value().xrp() + fee}, noripple(dave));
|
||||
// Exactly enough after the lock.
|
||||
env.fund(
|
||||
STAmount{fees.accountReserve(2, 1) + amount.value().xrp() + fee}, noripple(erin));
|
||||
env.close();
|
||||
|
||||
env(escrow::create(dave, carol, amount),
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 100s),
|
||||
Fee(fee),
|
||||
Ter(tecUNFUNDED));
|
||||
env(escrow::create(erin, carol, amount),
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 100s),
|
||||
Fee(fee));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.ownerCount(dave) == 0);
|
||||
BEAST_EXPECT(env.ownerCount(erin) == 2);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testFinishPreflight(FeatureBitset features)
|
||||
{
|
||||
testcase("EscrowFinish preflight");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace std::chrono;
|
||||
|
||||
Account const alice{"alice"};
|
||||
Account const carol{"carol"};
|
||||
|
||||
{
|
||||
// featureSmartEscrow disabled
|
||||
Env env(*this, features - featureSmartEscrow);
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
env(escrow::finish(carol, alice, 1),
|
||||
Fee(env.current()->fees().base + 1000),
|
||||
escrow::Gas(4),
|
||||
Ter(temDISABLED));
|
||||
env.close();
|
||||
}
|
||||
|
||||
{
|
||||
// Gas > gas limit
|
||||
Env env(*this, votingFor(Keys::kGasLimit, 1'000), features); // in gas
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
env.close();
|
||||
|
||||
// Gas above the voted limit is checked against the escrow it
|
||||
// would run, so there has to be one.
|
||||
auto const bytecode = bytecodeOfSize(64);
|
||||
auto const seq = env.seq(alice);
|
||||
env(escrow::create(alice, carol, XRP(500)),
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 100'000s),
|
||||
Fee(createFeeFor(env, bytecode)));
|
||||
closePastFlagLedger(env);
|
||||
|
||||
auto const gas = 1'001;
|
||||
env(escrow::finish(carol, alice, seq),
|
||||
Fee(finishFeeFor(env, gas)),
|
||||
escrow::Gas(gas),
|
||||
Ter(tefEXCEEDS_LIMIT));
|
||||
}
|
||||
|
||||
{
|
||||
// Gas of 0
|
||||
Env env(*this, features);
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
env.close();
|
||||
env(escrow::finish(carol, alice, 1),
|
||||
Fee(env.current()->fees().base + 1000),
|
||||
escrow::Gas(0),
|
||||
Ter(temBAD_LIMIT));
|
||||
}
|
||||
|
||||
{
|
||||
// Gas above the protocol ceiling, refused before any view is read
|
||||
Env env(*this, features);
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
env.close();
|
||||
env(escrow::finish(carol, alice, 1),
|
||||
Fee(env.current()->fees().base + 1000),
|
||||
escrow::Gas(kMaxGasLimit + 1),
|
||||
Ter(temBAD_LIMIT));
|
||||
}
|
||||
|
||||
{
|
||||
// WASM compute disabled after the escrow was created.
|
||||
Env env(*this, votingFor(Keys::kGasLimit, 0), features);
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
env.close();
|
||||
|
||||
auto const bytecode = bytecodeOfSize(64);
|
||||
auto const seq = env.seq(alice);
|
||||
env(escrow::create(alice, carol, XRP(500)),
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 100'000s),
|
||||
Fee(createFeeFor(env, bytecode)));
|
||||
closePastFlagLedger(env);
|
||||
|
||||
auto const gas = 1'000;
|
||||
env(escrow::finish(carol, alice, seq),
|
||||
escrow::Gas(gas),
|
||||
Fee(finishFeeFor(env, gas)),
|
||||
Ter(tefTEMP_DISABLED));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testFinishBytecodePairing(FeatureBitset features)
|
||||
{
|
||||
testcase("EscrowFinish Bytecode and Gas must agree");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace std::chrono;
|
||||
|
||||
Account const alice{"alice"};
|
||||
Account const carol{"carol"};
|
||||
|
||||
Env env(*this, features);
|
||||
|
||||
// Run past the flag ledger so that a Fee change vote occurs and
|
||||
// updates FeeSettings. (It also activates all supported amendments.)
|
||||
for (auto i = env.current()->seq(); i <= 257; ++i)
|
||||
env.close();
|
||||
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
|
||||
auto const bytecode = bytecodeOfSize(64);
|
||||
auto const seq = env.seq(alice);
|
||||
env(escrow::create(alice, carol, XRP(500)),
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 100s),
|
||||
Fee(createFeeFor(env, bytecode)));
|
||||
env.close();
|
||||
|
||||
{
|
||||
// Bytecode on the escrow, no Gas on the finish
|
||||
env(escrow::finish(carol, alice, seq), Ter(tefBYTECODE_NOT_INCLUDED));
|
||||
}
|
||||
|
||||
{
|
||||
// Gas on the finish, no Bytecode on the escrow
|
||||
auto const plainSeq = env.seq(alice);
|
||||
env(escrow::create(alice, carol, XRP(500)),
|
||||
escrow::kFinishTime(env.now() + 10s),
|
||||
escrow::kCancelTime(env.now() + 100s));
|
||||
env.close();
|
||||
|
||||
auto const gas = 100;
|
||||
env(escrow::finish(carol, alice, plainSeq),
|
||||
Fee(finishFeeFor(env, gas)),
|
||||
escrow::Gas(gas),
|
||||
Ter(tefNO_BYTECODE));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testFinishFee(FeatureBitset features)
|
||||
{
|
||||
testcase("EscrowFinish fee");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace std::chrono;
|
||||
|
||||
Account const alice{"alice"};
|
||||
Account const carol{"carol"};
|
||||
|
||||
Env env(
|
||||
*this,
|
||||
envconfig([](std::unique_ptr<Config> cfg) {
|
||||
cfg->fees.gasPrice = 1'000'000; // 1 drop per gas
|
||||
return cfg;
|
||||
}),
|
||||
features);
|
||||
// Run past the flag ledger so that a Fee change vote occurs and
|
||||
// updates FeeSettings. (It also activates all supported amendments.)
|
||||
for (auto i = env.current()->seq(); i <= 257; ++i)
|
||||
env.close();
|
||||
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
|
||||
auto const bytecode = bytecodeOfSize(64);
|
||||
auto const seq = env.seq(alice);
|
||||
env(escrow::create(alice, carol, XRP(1000)),
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 100s),
|
||||
Fee(createFeeFor(env, bytecode)));
|
||||
env.close();
|
||||
|
||||
// A large gas allowance costs more than the allowance itself, and does
|
||||
// not wrap.
|
||||
auto const gas = 996'433;
|
||||
auto const fee = finishFeeFor(env, gas);
|
||||
BEAST_EXPECT(fee.drops() > gas);
|
||||
// At one drop per gas the cost divides evenly, so no drop is added.
|
||||
BEAST_EXPECT(fee == env.current()->fees().base + gas);
|
||||
|
||||
// Intentional low value to test overflow handling
|
||||
env(escrow::finish(carol, alice, seq),
|
||||
Fee(drops(30)),
|
||||
escrow::Gas(gas),
|
||||
Ter(telINSUF_FEE_P));
|
||||
|
||||
env(escrow::finish(carol, alice, seq), Fee(fee - 1), escrow::Gas(gas), Ter(telINSUF_FEE_P));
|
||||
|
||||
// TODO(SmartEscrow): tesSUCCESS once the WASM engine lands. Until then
|
||||
// the fee is charged and the run reports that it could not happen.
|
||||
env(escrow::finish(carol, alice, seq),
|
||||
Fee(fee),
|
||||
escrow::Gas(gas),
|
||||
Ter(tecFAILED_PROCESSING));
|
||||
|
||||
{
|
||||
// A gas cost of a fraction of a drop rounds up to a whole drop:
|
||||
// two gas at one micro-drop each costs one drop, not zero, and not
|
||||
// one drop per gas.
|
||||
Env env(*this, votingFor(Keys::kGasPrice, 1), features);
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
env.close();
|
||||
|
||||
auto const seq = env.seq(alice);
|
||||
env(escrow::create(alice, carol, XRP(1000)),
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 100'000s),
|
||||
Fee(createFeeFor(env, bytecode)));
|
||||
closePastFlagLedger(env);
|
||||
BEAST_EXPECT(env.current()->fees().gasPrice == 1);
|
||||
|
||||
auto const base = env.current()->fees().base;
|
||||
env(escrow::finish(carol, alice, seq), Fee(base), escrow::Gas(2), Ter(telINSUF_FEE_P));
|
||||
env(escrow::finish(carol, alice, seq),
|
||||
Fee(base + 1),
|
||||
escrow::Gas(2),
|
||||
Ter(tecFAILED_PROCESSING));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testFinishWithoutEngine(FeatureBitset features)
|
||||
{
|
||||
testcase("EscrowFinish has no WASM engine");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace std::chrono;
|
||||
|
||||
Account const alice{"alice"};
|
||||
Account const carol{"carol"};
|
||||
|
||||
Env env(*this, features);
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
env.close();
|
||||
|
||||
auto const bytecode = bytecodeOfSize(1'000);
|
||||
auto const seq = env.seq(alice);
|
||||
env(escrow::create(alice, carol, XRP(500)),
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 20s),
|
||||
Fee(createFeeFor(env, bytecode)));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.ownerCount(alice) == 2);
|
||||
|
||||
// TODO(SmartEscrow): the contract decides this once the WASM engine
|
||||
// lands - tesSUCCESS on accept, tecBYTECODE_REJECTED on reject.
|
||||
auto const gas = 1'000;
|
||||
env(escrow::finish(carol, alice, seq),
|
||||
Fee(finishFeeFor(env, gas)),
|
||||
escrow::Gas(gas),
|
||||
Ter(tecFAILED_PROCESSING));
|
||||
env.close();
|
||||
|
||||
// The escrow and its reserve survive.
|
||||
BEAST_EXPECT(env.le(keylet::escrow(alice, SeqProxy::rawSequence(seq))));
|
||||
BEAST_EXPECT(env.ownerCount(alice) == 2);
|
||||
|
||||
// Cancelling returns the whole reserve, not just one increment.
|
||||
env.close(30s);
|
||||
env(escrow::cancel(carol, alice, seq));
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(keylet::escrow(alice, SeqProxy::rawSequence(seq))));
|
||||
BEAST_EXPECT(env.ownerCount(alice) == 0);
|
||||
}
|
||||
|
||||
void
|
||||
testFinishReleasesReserve(FeatureBitset features)
|
||||
{
|
||||
testcase("EscrowFinish releases the reserve");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace std::chrono;
|
||||
|
||||
Account const alice{"alice"};
|
||||
Account const carol{"carol"};
|
||||
|
||||
// Without featureSponsor and fixCleanup3_4_0, the reserve is released
|
||||
// after delivery rather than before.
|
||||
Env env(*this, features - featureSponsor - fixCleanup3_4_0);
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
env.close();
|
||||
|
||||
auto const seq = env.seq(alice);
|
||||
env(escrow::create(alice, carol, XRP(500)), escrow::kFinishTime(env.now() + 1s));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.ownerCount(alice) == 1);
|
||||
|
||||
env(escrow::finish(carol, alice, seq));
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(keylet::escrow(alice, SeqProxy::rawSequence(seq))));
|
||||
BEAST_EXPECT(env.ownerCount(alice) == 0);
|
||||
}
|
||||
|
||||
void
|
||||
testWithFeats(FeatureBitset features)
|
||||
{
|
||||
testCreatePreflight(features);
|
||||
testCreateFeeAndReserve(features);
|
||||
testFinishPreflight(features);
|
||||
testFinishBytecodePairing(features);
|
||||
testFinishFee(features);
|
||||
testFinishWithoutEngine(features);
|
||||
testFinishReleasesReserve(features);
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
using namespace test::jtx;
|
||||
FeatureBitset const all{testableAmendments()};
|
||||
testWithFeats(all);
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(EscrowSmart, app, xrpl);
|
||||
|
||||
} // namespace xrpl::test
|
||||
@@ -1524,7 +1524,7 @@ struct Escrow_test : public beast::unit_test::Suite
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
Account const carol{"carol"};
|
||||
Account const dillon{"dillon"};
|
||||
Account const dillon{"dillon "};
|
||||
Account const zelda{"zelda"};
|
||||
|
||||
char const credType[] = "abcde";
|
||||
@@ -1682,8 +1682,6 @@ public:
|
||||
testWithFeats(all);
|
||||
testWithFeats(all - featureTokenEscrow);
|
||||
testWithFeats(all - fixCleanup3_5_0);
|
||||
testWithFeats(all - featureSmartEscrow);
|
||||
testWithFeats(all - featureTokenEscrow - featureSmartEscrow);
|
||||
testTags(all - fixIncludeKeyletFields);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1215,61 +1215,6 @@ struct FlowMPT_test : public beast::unit_test::Suite
|
||||
BEAST_EXPECT(env.balance(bob, y) == y(0));
|
||||
}
|
||||
|
||||
void
|
||||
testConsumeOfferOverflow(FeatureBitset features)
|
||||
{
|
||||
// An overflow while consuming an offer fails the strand under
|
||||
// MPTokensV2, and the transaction with tecINTERNAL otherwise. Either
|
||||
// way no balances change and the offer stays on the book.
|
||||
testcase("BookStep consumeOffer overflow");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
Account const gw("gateway");
|
||||
Account const eurGW("eur_gateway");
|
||||
Account const maker("maker");
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
|
||||
for (auto const& testFeatures : {features, features - featureMPTokensV2})
|
||||
{
|
||||
Env env(*this, testFeatures);
|
||||
|
||||
auto const withV2 = testFeatures[featureMPTokensV2];
|
||||
|
||||
env.fund(XRP(10'000), gw, eurGW, maker, alice, bob);
|
||||
env.close();
|
||||
|
||||
auto const usd = gw["USD"];
|
||||
auto const eur = eurGW["EUR"];
|
||||
STAmount const usdMax{usd.issue(), STAmount::kMaxValue, STAmount::kMaxOffset};
|
||||
STAmount const usdIn{usd.issue(), 1, 90};
|
||||
|
||||
env.trust(usdMax, maker, alice);
|
||||
env.trust(eur(1'000), maker, bob);
|
||||
env.close();
|
||||
|
||||
env(pay(gw, maker, usdMax));
|
||||
env(pay(gw, alice, usdIn));
|
||||
env(pay(eurGW, maker, eur(100)));
|
||||
env(offer(maker, usdIn, eur(100)));
|
||||
env.close();
|
||||
|
||||
env(pay(alice, bob, eur(100)),
|
||||
Path(~eur),
|
||||
Sendmax(usdIn),
|
||||
Ter(withV2 ? TER{tecPATH_PARTIAL} : TER{tecINTERNAL}));
|
||||
env.close();
|
||||
|
||||
env.require(
|
||||
Balance(bob, eur(0)),
|
||||
Balance(maker, eur(100)),
|
||||
Balance(maker, usdMax),
|
||||
Balance(alice, usdIn));
|
||||
BEAST_EXPECT(isOffer(env, maker, usdIn, eur(100)));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testFalseDry(FeatureBitset features)
|
||||
{
|
||||
@@ -3063,7 +3008,6 @@ struct FlowMPT_test : public beast::unit_test::Suite
|
||||
testBookStepInputAboveMaximumAmount(features);
|
||||
testBookStepInputAboveMaximumAmountWithFee(features);
|
||||
testBookStepMissingIssuance(features);
|
||||
testConsumeOfferOverflow(features);
|
||||
testSelfPayment1(features);
|
||||
testSelfPayment2(features);
|
||||
testSelfFundedXRPEndpoint(false, features);
|
||||
@@ -3072,85 +3016,6 @@ struct FlowMPT_test : public beast::unit_test::Suite
|
||||
testReExecuteDirectStep(features);
|
||||
testSelfPayLowQualityOffer(features);
|
||||
testLockedMidPathHolder(features);
|
||||
testIssuerSourceMPTLoop(features);
|
||||
}
|
||||
|
||||
void
|
||||
testIssuerSourceMPTLoop(FeatureBitset features)
|
||||
{
|
||||
// An issuer paying with its own MPT may not route it back through a
|
||||
// book that outputs the same MPT. It would issue at the first book
|
||||
// and again after the second, and with the issuer's own offer in
|
||||
// the second book the engine could not keep the sum within
|
||||
// MaximumAmount. Holders already get temBAD_PATH_LOOP for this.
|
||||
testcase("Issuer-sourced payment looping through its own MPT");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
Account const gw{"gw"};
|
||||
Account const alice{"alice"};
|
||||
Account const c1{"c1"};
|
||||
Account const e1{"e1"};
|
||||
Account const dan{"dan"};
|
||||
|
||||
Env env(*this, features);
|
||||
env.fund(XRP(1'000), gw, alice, c1, e1, dan);
|
||||
env.close();
|
||||
|
||||
MPT const x =
|
||||
MPTTester({.env = env, .issuer = gw, .holders = {alice, c1, e1, dan}, .maxAmt = 100});
|
||||
auto const eur = gw["EUR"];
|
||||
auto const usd = gw["USD"];
|
||||
env(trust(e1, eur(1'000)));
|
||||
env(trust(e1, usd(1'000)));
|
||||
env(trust(dan, eur(1'000)));
|
||||
env(pay(gw, e1, eur(1'000)));
|
||||
env(pay(gw, e1, usd(1'000)));
|
||||
env(pay(gw, alice, x(10)));
|
||||
env.close();
|
||||
|
||||
// Liquidity for every path below, arranged so nothing crosses at
|
||||
// creation.
|
||||
env(offer(gw, XRP(60), x(30))); // issuer sells x for XRP
|
||||
env(offer(gw, usd(100), x(100))); // issuer sells x for usd
|
||||
env.close();
|
||||
env(offer(c1, x(100), XRP(100))); // buys x for XRP
|
||||
env(offer(e1, XRP(70), usd(70))); // sells usd for XRP
|
||||
env(offer(e1, x(100), eur(100))); // sells eur for x
|
||||
env.close();
|
||||
BEAST_EXPECT(expectOffers(env, gw, 2));
|
||||
BEAST_EXPECT(expectOffers(env, c1, 1));
|
||||
BEAST_EXPECT(expectOffers(env, e1, 2));
|
||||
|
||||
auto const flags = Txflags(tfNoRippleDirect | tfPartialPayment);
|
||||
|
||||
// x -> XRP -> x, the issuer's own offer feeds the endpoint
|
||||
env(pay(gw, dan, x(100)), Sendmax(x(100)), Path(~XRP, ~x), flags, Ter(temBAD_PATH_LOOP));
|
||||
// x -> XRP -> x -> eur, the issuer's own offer feeds a third book
|
||||
env(pay(gw, dan, eur(100)),
|
||||
Sendmax(x(100)),
|
||||
Path(~XRP, ~x, ~eur),
|
||||
flags,
|
||||
Ter(temBAD_PATH_LOOP));
|
||||
// x -> XRP -> usd -> x, a limiting book before the issuer's own offer
|
||||
env(pay(gw, dan, x(100)),
|
||||
Sendmax(x(100)),
|
||||
Path(~XRP, ~usd, ~x),
|
||||
flags,
|
||||
Ter(temBAD_PATH_LOOP));
|
||||
// Same loop from a holder is already rejected
|
||||
env(pay(alice, dan, x(10)), Sendmax(x(10)), Path(~XRP, ~x), flags, Ter(temBAD_PATH_LOOP));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.balance(gw, x) == x(-10));
|
||||
BEAST_EXPECT(env.balance(c1, x) == x(0));
|
||||
BEAST_EXPECT(env.balance(dan, x) == x(0));
|
||||
|
||||
// The issuer paying with another asset may still take its own offer
|
||||
env(pay(gw, dan, x(50)), Sendmax(usd(50)), Path(~x), flags);
|
||||
env.close();
|
||||
BEAST_EXPECT(env.balance(dan, x) == x(50));
|
||||
BEAST_EXPECT(env.balance(gw, x) == x(-60));
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <xrpld/app/ledger/InboundLedgers.h>
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
|
||||
#include <xrpl/basics/Blob.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
@@ -14,7 +15,9 @@
|
||||
#include <xrpl/protocol/LedgerHeader.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/resource/Fees.h>
|
||||
#include <xrpl/shamap/SHAMapNodeID.h>
|
||||
#include <xrpl/shamap/SHAMapTreeNode.h>
|
||||
|
||||
#include <xrpl.pb.h>
|
||||
|
||||
@@ -73,6 +76,50 @@ struct TestableInboundLedger final : InboundLedger
|
||||
trigger(nullptr, TriggerReason::Added);
|
||||
}
|
||||
|
||||
/**
|
||||
* The same, as the timer chain does.
|
||||
*/
|
||||
void
|
||||
triggerTimeout()
|
||||
{
|
||||
trigger(nullptr, TriggerReason::Timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Record how many timeouts have elapsed.
|
||||
*
|
||||
* @param timeouts The count to record.
|
||||
*/
|
||||
void
|
||||
setTimeouts(int timeouts)
|
||||
{
|
||||
ScopedLockType const sl(mtx_);
|
||||
timeouts_ = timeouts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forget any recorded progress.
|
||||
*/
|
||||
void
|
||||
clearProgress()
|
||||
{
|
||||
ScopedLockType const sl(mtx_);
|
||||
progress_ = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a packet has advanced the acquisition since the flag was last
|
||||
* cleared.
|
||||
*
|
||||
* @return Whether progress has been recorded.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
madeProgress() const
|
||||
{
|
||||
ScopedLockType const sl(mtx_);
|
||||
return progress_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record that every part has been fetched.
|
||||
*/
|
||||
@@ -259,6 +306,23 @@ struct InboundLedger_test : public beast::unit_test::Suite
|
||||
return acquire;
|
||||
}
|
||||
|
||||
/**
|
||||
* The chain's state-map nodes as a liAS_NODE reply for the given header.
|
||||
*
|
||||
* @param header The header whose hash and sequence the reply names.
|
||||
* @param chain The chain supplying the nodes.
|
||||
* @param data The nodes to include, each with its claimed position.
|
||||
* @return The reply packet.
|
||||
*/
|
||||
static std::shared_ptr<protocol::TMLedgerData>
|
||||
stateNodePacket(
|
||||
LedgerHeader const& header,
|
||||
DeepChain const& chain,
|
||||
std::vector<std::pair<SHAMapNodeID, SHAMapTreeNodePtr>> const& data)
|
||||
{
|
||||
return packetFor(chain, data, protocol::liAS_NODE, header.hash, header.seq);
|
||||
}
|
||||
|
||||
/**
|
||||
* A ledger whose maps all resolve locally finishes on the spot, and the
|
||||
* finished ledger is immutable and handed on.
|
||||
@@ -360,6 +424,60 @@ struct InboundLedger_test : public beast::unit_test::Suite
|
||||
BEAST_EXPECT(!env.app().getInboundLedgers().isFailure(header.hash));
|
||||
}
|
||||
|
||||
/**
|
||||
* A ledger completed by a walk rather than by tryDB() is settled before it
|
||||
* is reported complete.
|
||||
*
|
||||
* The order is visible only to a reader of isComplete(), which is read
|
||||
* without mtx_, so this case pins the invariant instead: every ledger the
|
||||
* acquisition reports is immutable.
|
||||
*
|
||||
* @param env The environment to run in.
|
||||
*/
|
||||
void
|
||||
testWalkSettlesBeforeReportingComplete(jtx::Env& env)
|
||||
{
|
||||
testcase("A ledger completed by a walk is settled before it is reported");
|
||||
|
||||
auto const chain = DeepChain::toLeaf(2, nextSeed());
|
||||
auto const header = makeHeader(chain);
|
||||
|
||||
// Everything except the leaf, so tryDB() can root the state map but its walk still has
|
||||
// something to ask for, which leaves the completion to trigger().
|
||||
storeHeader(env, header);
|
||||
storeStateNodes(env, header, chain, chain.deepestDepth - 1);
|
||||
|
||||
auto const acquire = std::make_shared<TestableInboundLedger>(
|
||||
env.app(),
|
||||
header.hash,
|
||||
header.seq,
|
||||
InboundLedger::Reason::GENERIC,
|
||||
stopwatch(),
|
||||
std::make_unique<RequestCountingPeerSet>());
|
||||
|
||||
BEAST_EXPECT(!acquire->checkLocal());
|
||||
BEAST_EXPECT(!acquire->isComplete());
|
||||
BEAST_EXPECT(!acquire->isFailed());
|
||||
|
||||
// The walk can finish only now, which places the completion in the walk.
|
||||
storeStateNodes(env, header, chain, chain.deepestDepth);
|
||||
|
||||
acquire->triggerAdded();
|
||||
|
||||
BEAST_EXPECT(acquire->isComplete());
|
||||
BEAST_EXPECT(!acquire->isFailed());
|
||||
|
||||
auto const settled = acquire->getLedger();
|
||||
BEAST_EXPECT(settled != nullptr);
|
||||
if (settled)
|
||||
BEAST_EXPECT(settled->isImmutable());
|
||||
|
||||
// done()'s success arm ran, so the ledger reached LedgerMaster and the hash is absent
|
||||
// from the failure list.
|
||||
BEAST_EXPECT(env.app().getLedgerMaster().getLedgerByHash(header.hash) != nullptr);
|
||||
BEAST_EXPECT(!env.app().getInboundLedgers().isFailure(header.hash));
|
||||
}
|
||||
|
||||
/**
|
||||
* A ledger whose map goes invalid on the way to being settled must be
|
||||
* discarded rather than delivered.
|
||||
@@ -425,6 +543,74 @@ struct InboundLedger_test : public beast::unit_test::Suite
|
||||
BEAST_EXPECT(waitFor([&] { return env.app().getInboundLedgers().isFailure(header.hash); }));
|
||||
}
|
||||
|
||||
/**
|
||||
* A packet for an abandoned map must fail rather than be read as the map
|
||||
* finishing.
|
||||
*
|
||||
* addKnownNode() reports every node a duplicate rather than invalid once
|
||||
* the map is no longer Synching (see SHAMap::addKnownNode), so
|
||||
* receiveNode() must read isValid() rather than isSynching() alone.
|
||||
*
|
||||
* The tx hash never resolves, so haveTransactions_ stays false and
|
||||
* nothing but the check under test can make this packet report success.
|
||||
*
|
||||
* @param env The environment to run in.
|
||||
*/
|
||||
void
|
||||
testInvalidatedMapFailsReceiveNode(jtx::Env& env)
|
||||
{
|
||||
testcase("A packet for an abandoned map fails rather than completes");
|
||||
|
||||
// The fabricated chain, so feeding it to the state map invalidates the map.
|
||||
DeepChain const chain{nextSeed()};
|
||||
|
||||
auto const header = makeHeader(UInt256{777}, chain.rootHash.asUInt256());
|
||||
storeHeader(env, header);
|
||||
|
||||
auto const acquire = std::make_shared<TestableInboundLedger>(
|
||||
env.app(),
|
||||
header.hash,
|
||||
header.seq,
|
||||
InboundLedger::Reason::GENERIC,
|
||||
stopwatch(),
|
||||
std::make_unique<RequestCountingPeerSet>());
|
||||
|
||||
BEAST_EXPECT(!acquire->checkLocal());
|
||||
BEAST_EXPECT(!acquire->isFailed());
|
||||
BEAST_EXPECT(!acquire->getJson(0)[jss::have_state].asBool());
|
||||
|
||||
// Invalidate the state map directly, standing in for a concurrent trigger() walk that
|
||||
// reached the same verdict with mtx_ released.
|
||||
auto const ledger = mutableLedger(*acquire);
|
||||
BEAST_EXPECT(ledger != nullptr);
|
||||
if (!ledger)
|
||||
return;
|
||||
|
||||
auto& stateMap = ledger->stateMap();
|
||||
BEAST_EXPECT(stateMap.addRootNode(chain.rootHash, chain.nodeAt(0), nullptr).isGood());
|
||||
for (auto const& [nodeID, node] : chain.nodesBelowRoot())
|
||||
stateMap.addKnownNode(nodeID, node, nullptr);
|
||||
BEAST_EXPECT(!stateMap.isValid());
|
||||
|
||||
// An ordinary state-node packet, arriving as if it were already in flight when the
|
||||
// invalidation above happened. addKnownNode()'s "while not synching" branch reports it a
|
||||
// duplicate, whatever it contains, since the map is no longer Synching.
|
||||
auto const latePeer = std::make_shared<ChargeRecordingPeer>();
|
||||
BEAST_EXPECT(acquire->gotData(
|
||||
latePeer, stateNodePacket(header, chain, {{chain.idAt(1), chain.nodeAt(1)}})));
|
||||
acquire->runData();
|
||||
|
||||
// The verdict belongs to the concurrent walk, so the sender keeps its fee and the
|
||||
// acquisition fails, leaving the duplicate verdict unread as a finished map.
|
||||
BEAST_EXPECT(latePeer->charges().empty());
|
||||
BEAST_EXPECT(acquire->isFailed());
|
||||
BEAST_EXPECT(!acquire->isComplete());
|
||||
|
||||
// This assertion pins the isValid() check, which is what fails the acquisition while the
|
||||
// map behind it is broken.
|
||||
BEAST_EXPECT(!acquire->getJson(0)[jss::have_state].asBool());
|
||||
}
|
||||
|
||||
/**
|
||||
* An acquisition that fails on local data must still signal.
|
||||
*
|
||||
@@ -502,6 +688,357 @@ struct InboundLedger_test : public beast::unit_test::Suite
|
||||
waitFor([&] { return env.app().getInboundLedgers().isFailure(otherHeader.hash); }));
|
||||
}
|
||||
|
||||
/**
|
||||
* An acquisition whose state root names a shape no valid tree can have must
|
||||
* fail, and must cost the sender the harsher tier.
|
||||
*
|
||||
* @param env The environment to run in.
|
||||
*/
|
||||
void
|
||||
testFabricatedChainFailsAcquire(jtx::Env& env)
|
||||
{
|
||||
testcase("A state-map chain reaching kLeafDepth fails the acquire");
|
||||
|
||||
DeepChain const chain{nextSeed()};
|
||||
auto const header = makeHeader(chain);
|
||||
|
||||
auto const acquire = std::make_shared<TestableInboundLedger>(
|
||||
env.app(),
|
||||
header.hash,
|
||||
header.seq,
|
||||
InboundLedger::Reason::GENERIC,
|
||||
stopwatch(),
|
||||
std::make_unique<RequestCountingPeerSet>());
|
||||
|
||||
// The header is accepted on its own terms, so the acquisition now chases this hash.
|
||||
auto const headerPeer = std::make_shared<ChargeRecordingPeer>();
|
||||
BEAST_EXPECT(acquire->gotData(headerPeer, headerPacket(header)));
|
||||
acquire->runData();
|
||||
BEAST_EXPECT(headerPeer->charges().empty());
|
||||
BEAST_EXPECT(!acquire->isFailed());
|
||||
|
||||
// The root, then the rest of the chain ending in the inner node at kLeafDepth.
|
||||
auto const data = chain.allNodes();
|
||||
|
||||
// The header counted as progress, so clear it to see what the packet below records.
|
||||
acquire->clearProgress();
|
||||
|
||||
auto const chainPeer = std::make_shared<ChargeRecordingPeer>();
|
||||
BEAST_EXPECT(acquire->gotData(chainPeer, stateNodePacket(header, chain, data)));
|
||||
acquire->runData();
|
||||
|
||||
// The acquisition is over, and stays over: no peer can satisfy this hash.
|
||||
BEAST_EXPECT(acquire->isFailed());
|
||||
BEAST_EXPECT(!acquire->isComplete());
|
||||
|
||||
// The nodes ahead of the bad one belong to the same impossible tree, so the packet counts
|
||||
// nothing at all.
|
||||
BEAST_EXPECT(!acquire->madeProgress());
|
||||
|
||||
// A failed acquisition reports no ledger, though it still holds the partial one it built.
|
||||
BEAST_EXPECT(acquire->getLedger() == nullptr);
|
||||
|
||||
BEAST_EXPECT(chainPeer->charges() == std::vector{resource::kFeeMalformedData});
|
||||
|
||||
// getJson() walks the same maps to report what is still needed, and comes back with an
|
||||
// empty list, since the walk stops at the abandoned map.
|
||||
auto const report = acquire->getJson(0);
|
||||
BEAST_EXPECT(report[jss::failed].asBool());
|
||||
BEAST_EXPECT(report[jss::have_header].asBool());
|
||||
BEAST_EXPECT(!report[jss::have_state].asBool());
|
||||
BEAST_EXPECT(report[jss::needed_state_hashes].size() == 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* A merely-wrong state node must cost the recoverable tier and leave the
|
||||
* acquisition alive. The fee split in receiveNode() turns on whether this
|
||||
* node proved the map impossible, so a node that cannot be hooked but
|
||||
* leaves the map sound is charged the lower tier.
|
||||
*
|
||||
* @param env The environment to run in.
|
||||
*/
|
||||
void
|
||||
testWrongStateNodeKeepsAcquireAlive(jtx::Env& env)
|
||||
{
|
||||
testcase("A merely-wrong state node leaves the acquire recoverable");
|
||||
|
||||
DeepChain const chain{nextSeed()};
|
||||
auto const header = makeHeader(chain);
|
||||
|
||||
auto const acquire = std::make_shared<InboundLedger>(
|
||||
env.app(),
|
||||
header.hash,
|
||||
header.seq,
|
||||
InboundLedger::Reason::GENERIC,
|
||||
stopwatch(),
|
||||
std::make_unique<RequestCountingPeerSet>());
|
||||
|
||||
auto const headerPeer = std::make_shared<ChargeRecordingPeer>();
|
||||
BEAST_EXPECT(acquire->gotData(headerPeer, headerPacket(header)));
|
||||
acquire->runData();
|
||||
BEAST_EXPECT(!acquire->isFailed());
|
||||
|
||||
auto const rootPeer = std::make_shared<ChargeRecordingPeer>();
|
||||
BEAST_EXPECT(acquire->gotData(
|
||||
rootPeer, stateNodePacket(header, chain, {{SHAMapNodeID{}, chain.nodeAt(0)}})));
|
||||
acquire->runData();
|
||||
BEAST_EXPECT(rootPeer->charges().empty());
|
||||
|
||||
// nodeAt(1) is the node the root is missing and its hash matches, but it is labeled as
|
||||
// living at depth 2, so it cannot be hooked anywhere.
|
||||
auto const wrongPeer = std::make_shared<ChargeRecordingPeer>();
|
||||
BEAST_EXPECT(acquire->gotData(
|
||||
wrongPeer,
|
||||
stateNodePacket(header, chain, {{SHAMapNodeID{2, UInt256{}}, chain.nodeAt(1)}})));
|
||||
acquire->runData();
|
||||
|
||||
BEAST_EXPECT(wrongPeer->charges() == std::vector{resource::kFeeInvalidData});
|
||||
|
||||
// The map is sound, so the acquisition is still going and still holds its ledger.
|
||||
BEAST_EXPECT(!acquire->isFailed());
|
||||
BEAST_EXPECT(!acquire->isComplete());
|
||||
BEAST_EXPECT(acquire->getLedger() != nullptr);
|
||||
}
|
||||
|
||||
/**
|
||||
* A ledger assembled from local data must be judged even when only one map
|
||||
* is settled.
|
||||
*
|
||||
* A fetch pack is checked against each node's own hash rather than the
|
||||
* shape it implies, so a whole chain can resolve locally without passing
|
||||
* through addKnownNode(). Here the transaction map is the chain, so its walk
|
||||
* abandons it, while the state root is a hash no fetch pack supplies,
|
||||
* leaving that map merely incomplete. tryDB() sets neither flag, so it has
|
||||
* to reach the verdict itself.
|
||||
*
|
||||
* @param env The environment to run in.
|
||||
*/
|
||||
void
|
||||
testLocalChainFailsAcquire(jtx::Env& env)
|
||||
{
|
||||
testcase("A chain found locally fails the acquire");
|
||||
|
||||
DeepChain const chain{nextSeed()};
|
||||
|
||||
// The chain as the transaction root, and an arbitrary hash, seeded nowhere, as the state
|
||||
// root.
|
||||
auto const header = makeHeader(chain.rootHash.asUInt256(), UInt256{99});
|
||||
auto& ledgerMaster = env.app().getLedgerMaster();
|
||||
|
||||
// The header, prefixed the way tryDB() expects to find it in a fetch pack.
|
||||
Serializer hs;
|
||||
hs.add32(HashPrefix::LedgerMaster);
|
||||
addRaw(header, hs);
|
||||
ledgerMaster.addFetchPack(header.hash, std::make_shared<Blob>(hs.modData()));
|
||||
|
||||
// Every node of the chain, keyed by its own hash. TransactionStateSF::getNode() reads
|
||||
// these, so the transaction-map walk resolves the whole chain with no peer involved.
|
||||
for (auto depth = 0u; depth <= SHAMap::kLeafDepth; ++depth)
|
||||
{
|
||||
ledgerMaster.addFetchPack(
|
||||
chain.nodeAt(depth)->getHash().asUInt256(),
|
||||
std::make_shared<Blob>(chain.prefixedNodeAt(depth)));
|
||||
}
|
||||
|
||||
auto const acquire = std::make_shared<InboundLedger>(
|
||||
env.app(),
|
||||
header.hash,
|
||||
header.seq,
|
||||
InboundLedger::Reason::GENERIC,
|
||||
stopwatch(),
|
||||
std::make_unique<RequestCountingPeerSet>());
|
||||
|
||||
// checkLocal() routes into tryDB() with no peer data. It reports true only because the
|
||||
// acquisition ended.
|
||||
BEAST_EXPECT(acquire->checkLocal());
|
||||
|
||||
BEAST_EXPECT(acquire->isFailed());
|
||||
BEAST_EXPECT(!acquire->isComplete());
|
||||
|
||||
// A failed acquisition reports no ledger, though it still holds the partial one it built.
|
||||
BEAST_EXPECT(acquire->getLedger() == nullptr);
|
||||
}
|
||||
|
||||
/**
|
||||
* The aggressive-retry branch of trigger() must judge a map the walk
|
||||
* abandoned.
|
||||
*
|
||||
* That branch reads an empty getNeededHashes() result as a complete map,
|
||||
* and the walk it runs can reach the invalid verdict itself once nodes
|
||||
* resolve from local storage. Only the root is local when tryDB() runs, so
|
||||
* the state map holds a root and the walk stops one level down. The branch
|
||||
* also needs a timeout count above kLedgerBecomeAggressiveThreshold, which
|
||||
* the case records directly rather than waiting out the timer chain.
|
||||
*
|
||||
* @param env The environment to run in.
|
||||
*/
|
||||
void
|
||||
testAggressiveRetryJudgesLocalMap(jtx::Env& env)
|
||||
{
|
||||
testcase("An aggressive retry judges a map the walk abandoned");
|
||||
|
||||
DeepChain const chain{nextSeed()};
|
||||
|
||||
// The chain as the state root, and no transactions, so only the state map is in play.
|
||||
auto const header = makeHeader(chain);
|
||||
auto& ledgerMaster = env.app().getLedgerMaster();
|
||||
|
||||
Serializer hs;
|
||||
hs.add32(HashPrefix::LedgerMaster);
|
||||
addRaw(header, hs);
|
||||
ledgerMaster.addFetchPack(header.hash, std::make_shared<Blob>(hs.modData()));
|
||||
|
||||
// Only the root, so the state map gets a root but the walk stops one level down.
|
||||
ledgerMaster.addFetchPack(
|
||||
chain.nodeAt(0)->getHash().asUInt256(),
|
||||
std::make_shared<Blob>(chain.prefixedNodeAt(0)));
|
||||
|
||||
auto const acquire = std::make_shared<TestableInboundLedger>(
|
||||
env.app(),
|
||||
header.hash,
|
||||
header.seq,
|
||||
InboundLedger::Reason::GENERIC,
|
||||
stopwatch(),
|
||||
std::make_unique<RequestCountingPeerSet>());
|
||||
|
||||
// The acquisition is alive: it has the header and a state root, and still wants the rest.
|
||||
BEAST_EXPECT(!acquire->checkLocal());
|
||||
BEAST_EXPECT(!acquire->isFailed());
|
||||
BEAST_EXPECT(acquire->getJson(0)[jss::have_header].asBool());
|
||||
BEAST_EXPECT(!acquire->getJson(0)[jss::have_state].asBool());
|
||||
|
||||
auto const ledger = mutableLedger(*acquire);
|
||||
BEAST_EXPECT(ledger != nullptr);
|
||||
if (!ledger)
|
||||
return;
|
||||
BEAST_EXPECT(ledger->stateMap().isValid());
|
||||
|
||||
// The rest of the chain becomes resolvable only now, which places the verdict in this walk.
|
||||
for (auto depth = 1u; depth <= SHAMap::kLeafDepth; ++depth)
|
||||
{
|
||||
ledgerMaster.addFetchPack(
|
||||
chain.nodeAt(depth)->getHash().asUInt256(),
|
||||
std::make_shared<Blob>(chain.prefixedNodeAt(depth)));
|
||||
}
|
||||
|
||||
// kLedgerBecomeAggressiveThreshold is 4 and file-local, so name the requirement here.
|
||||
acquire->setTimeouts(5);
|
||||
acquire->clearProgress();
|
||||
acquire->triggerTimeout();
|
||||
|
||||
// The walk resolved the chain locally and abandoned the map, and trigger() recorded that
|
||||
// rather than reading the empty result as a finished acquisition.
|
||||
BEAST_EXPECT(!ledger->stateMap().isValid());
|
||||
BEAST_EXPECT(acquire->isFailed());
|
||||
BEAST_EXPECT(!acquire->isComplete());
|
||||
|
||||
// have_state is the discriminating assertion: only this guard leaves it false, since every
|
||||
// have-flag is set on the way to the setImmutable() backstop in done().
|
||||
BEAST_EXPECT(!acquire->getJson(0)[jss::have_state].asBool());
|
||||
|
||||
// The same branch with no header, which is the other arm of hasInvalidMap(): no map, so
|
||||
// the arm answers false. getNeededHashes() has asked for the header, so the non-empty
|
||||
// branch is the right one and the acquisition stays alive.
|
||||
auto const headerless = std::make_shared<TestableInboundLedger>(
|
||||
env.app(),
|
||||
UInt256{7},
|
||||
0,
|
||||
InboundLedger::Reason::GENERIC,
|
||||
stopwatch(),
|
||||
std::make_unique<RequestCountingPeerSet>());
|
||||
|
||||
headerless->setTimeouts(5);
|
||||
headerless->clearProgress();
|
||||
headerless->triggerTimeout();
|
||||
|
||||
BEAST_EXPECT(mutableLedger(*headerless) == nullptr);
|
||||
BEAST_EXPECT(!headerless->isFailed());
|
||||
BEAST_EXPECT(!headerless->isComplete());
|
||||
}
|
||||
|
||||
/**
|
||||
* The ordinary trigger() path must judge a map its own walk abandoned.
|
||||
*
|
||||
* Covers the state-map walk trigger() runs with mtx_ released, where the
|
||||
* case above covers the empty getNeededHashes() branch. That verdict has to
|
||||
* be read before the flags below it. Staged as that case is, with the
|
||||
* timeout count left at zero to stay off the aggressive branch.
|
||||
*
|
||||
* @param env The environment to run in.
|
||||
*/
|
||||
void
|
||||
testWalkJudgesMapOnOrdinaryTrigger(jtx::Env& env)
|
||||
{
|
||||
testcase("An ordinary trigger judges a map its walk abandoned");
|
||||
|
||||
DeepChain const chain{nextSeed()};
|
||||
|
||||
auto const header = makeHeader(chain);
|
||||
auto& ledgerMaster = env.app().getLedgerMaster();
|
||||
|
||||
Serializer hs;
|
||||
hs.add32(HashPrefix::LedgerMaster);
|
||||
addRaw(header, hs);
|
||||
ledgerMaster.addFetchPack(header.hash, std::make_shared<Blob>(hs.modData()));
|
||||
|
||||
// Only the root, so the state map gets a root but the walk stops one level down.
|
||||
ledgerMaster.addFetchPack(
|
||||
chain.nodeAt(0)->getHash().asUInt256(),
|
||||
std::make_shared<Blob>(chain.prefixedNodeAt(0)));
|
||||
|
||||
auto peerSet = std::make_unique<RequestCountingPeerSet>();
|
||||
auto const* const peerSetPtr = peerSet.get();
|
||||
|
||||
auto const acquire = std::make_shared<TestableInboundLedger>(
|
||||
env.app(),
|
||||
header.hash,
|
||||
header.seq,
|
||||
InboundLedger::Reason::GENERIC,
|
||||
stopwatch(),
|
||||
std::move(peerSet));
|
||||
|
||||
BEAST_EXPECT(!acquire->checkLocal());
|
||||
BEAST_EXPECT(!acquire->isFailed());
|
||||
BEAST_EXPECT(acquire->getJson(0)[jss::have_header].asBool());
|
||||
BEAST_EXPECT(!acquire->getJson(0)[jss::have_state].asBool());
|
||||
|
||||
auto const ledger = mutableLedger(*acquire);
|
||||
BEAST_EXPECT(ledger != nullptr);
|
||||
if (!ledger)
|
||||
return;
|
||||
BEAST_EXPECT(ledger->stateMap().isValid());
|
||||
|
||||
// The rest of the chain becomes resolvable only now, which places the verdict in this walk.
|
||||
for (auto depth = 1u; depth <= SHAMap::kLeafDepth; ++depth)
|
||||
{
|
||||
ledgerMaster.addFetchPack(
|
||||
chain.nodeAt(depth)->getHash().asUInt256(),
|
||||
std::make_shared<Blob>(chain.prefixedNodeAt(depth)));
|
||||
}
|
||||
|
||||
// Sound going in, which is the first half of the tripwire below.
|
||||
BEAST_EXPECT(ledger->stateMap().isValid());
|
||||
|
||||
int const requestsBefore = peerSetPtr->requests();
|
||||
acquire->triggerAdded();
|
||||
|
||||
// The walk resolved the chain locally and abandoned the map, and trigger() recorded that
|
||||
// rather than reading the empty node list as a finished state map.
|
||||
BEAST_EXPECT(!ledger->stateMap().isValid());
|
||||
BEAST_EXPECT(acquire->isFailed());
|
||||
|
||||
// have_state is the discriminating assertion. isComplete() is not: it reads false whether
|
||||
// this guard fired or the setImmutable() backstop in done() did.
|
||||
BEAST_EXPECT(!acquire->getJson(0)[jss::have_state].asBool());
|
||||
|
||||
// A tripwire that keeps this case on the branch it names: the aggressive branch needs both
|
||||
// a TIMEOUT reason and a count above kLedgerBecomeAggressiveThreshold.
|
||||
BEAST_EXPECT(acquire->getJson(0)[jss::timeouts].asInt() == 0);
|
||||
|
||||
// The request count is unchanged after the verdict, so the guard ended the round.
|
||||
BEAST_EXPECT(peerSetPtr->requests() == requestsBefore);
|
||||
}
|
||||
|
||||
/**
|
||||
* A peer-supplied header whose account hash is zero fails the acquisition
|
||||
* and signals, as a local one does. The peer is not charged, and
|
||||
@@ -604,17 +1141,10 @@ struct InboundLedger_test : public beast::unit_test::Suite
|
||||
/**
|
||||
* The retry timer re-asks, then gives up and signals.
|
||||
*
|
||||
* The only case that drives onTimer() rather than trigger() directly,
|
||||
* which is what covers the give-up: past kLedgerTimeoutRetriesMax the
|
||||
* acquisition fails itself and done() records that, so the same
|
||||
* doomed ledger is not asked for again on the next round. It is also
|
||||
* what the retry interval is a constructor parameter for, since the
|
||||
* chain runs past kLedgerTimeoutRetriesMax ticks of three seconds
|
||||
* apiece in production.
|
||||
*
|
||||
* The store is empty for this hash and no reply arrives, so every tick
|
||||
* counts a timeout and the count climbs to the limit. A hash of its own,
|
||||
* so this case owns its entry in the failure list.
|
||||
* The only case that drives onTimer() rather than trigger() directly. Past
|
||||
* kLedgerTimeoutRetriesMax the acquisition fails itself and done() records
|
||||
* that, so the same ledger is not asked for again. The store is empty for
|
||||
* this hash and no reply arrives, so every tick counts a timeout.
|
||||
*
|
||||
* @param env The environment to run in.
|
||||
*/
|
||||
@@ -662,24 +1192,108 @@ struct InboundLedger_test : public beast::unit_test::Suite
|
||||
BEAST_EXPECT(!acquire->isComplete());
|
||||
BEAST_EXPECT(peerSetPtr->requests() > requestsFromInit);
|
||||
|
||||
// done() remembered the hash, which is what stops the next round asking again.
|
||||
// No ledger was ever built here, so both arms of the failed_ gate answer null. The
|
||||
// local-chain case above covers the gate itself.
|
||||
BEAST_EXPECT(acquire->getLedger() == nullptr);
|
||||
BEAST_EXPECT(
|
||||
waitFor([&] { return env.app().getInboundLedgers().isFailure(kUnknownLedger); }));
|
||||
}
|
||||
|
||||
/**
|
||||
* A packet that leaves nothing to fetch must settle the ledger itself,
|
||||
* rather than count on a later walk to notice.
|
||||
*
|
||||
* A map holding one item has that item's leaf for its root, so rooting it
|
||||
* stops it synching on the spot. The packet here offers that root to a map
|
||||
* that already holds it, which counts no good nodes, so runData() takes the
|
||||
* sender for one that returned nothing and asks it nothing further. That
|
||||
* leaves receiveNode() as the only thing that can settle this ledger.
|
||||
*
|
||||
* @param env The environment to run in.
|
||||
*/
|
||||
void
|
||||
testCompletesWhenNothingIsLeftToFetch(jtx::Env& env)
|
||||
{
|
||||
testcase("A packet that leaves nothing to fetch settles the ledger");
|
||||
|
||||
// A leaf at the root, so the state map holds exactly one item. No transactions, which
|
||||
// leaves the state map the only part the acquisition needs.
|
||||
auto const chain = DeepChain::toLeaf(0, nextSeed());
|
||||
auto const header = makeHeader(chain);
|
||||
|
||||
// Only the header is local, so the state map starts with no root of its own.
|
||||
storeHeader(env, header);
|
||||
|
||||
auto peerSet = std::make_unique<RequestCountingPeerSet>();
|
||||
auto const* const peerSetPtr = peerSet.get();
|
||||
|
||||
auto const acquire = std::make_shared<TestableInboundLedger>(
|
||||
env.app(),
|
||||
header.hash,
|
||||
header.seq,
|
||||
InboundLedger::Reason::GENERIC,
|
||||
stopwatch(),
|
||||
std::move(peerSet));
|
||||
|
||||
BEAST_EXPECT(!acquire->checkLocal());
|
||||
BEAST_EXPECT(!acquire->isComplete());
|
||||
BEAST_EXPECT(!acquire->isFailed());
|
||||
BEAST_EXPECT(!acquire->getJson(0)[jss::have_state].asBool());
|
||||
|
||||
// Root the map directly, standing in for a trigger() walk that got that far. The flag is
|
||||
// the acquisition's own, so it stays clear while the map is already whole.
|
||||
auto const ledger = mutableLedger(*acquire);
|
||||
BEAST_EXPECT(ledger != nullptr);
|
||||
if (!ledger)
|
||||
return;
|
||||
|
||||
auto& stateMap = ledger->stateMap();
|
||||
BEAST_EXPECT(stateMap.addRootNode(chain.rootHash, chain.nodeAt(0), nullptr).isUseful());
|
||||
BEAST_EXPECT(!stateMap.isSynching());
|
||||
BEAST_EXPECT(!acquire->getJson(0)[jss::have_state].asBool());
|
||||
|
||||
// The same root again. A leaf carries its own key, so a depth of zero names the root and
|
||||
// the node goes through addRootNode(), which reports a node already held.
|
||||
auto const sender = std::make_shared<ChargeRecordingPeer>();
|
||||
BEAST_EXPECT(acquire->gotData(sender, stateNodePacket(header, chain, chain.allNodes())));
|
||||
acquire->runData();
|
||||
|
||||
// Nothing was asked of the sender afterwards, so no walk ran to reach the same verdict.
|
||||
BEAST_EXPECT(!acquire->madeProgress());
|
||||
BEAST_EXPECT(peerSetPtr->requests() == 0);
|
||||
|
||||
// The sender offered nothing bad, so it keeps its fee, and the map is now reported held.
|
||||
BEAST_EXPECT(sender->charges().empty());
|
||||
BEAST_EXPECT(acquire->getJson(0)[jss::have_state].asBool());
|
||||
|
||||
// Settled, reported complete and handed on, all from this one packet.
|
||||
BEAST_EXPECT(acquire->isComplete());
|
||||
BEAST_EXPECT(!acquire->isFailed());
|
||||
BEAST_EXPECT(ledger->isImmutable());
|
||||
|
||||
BEAST_EXPECT(env.app().getLedgerMaster().getLedgerByHash(header.hash) != nullptr);
|
||||
BEAST_EXPECT(!env.app().getInboundLedgers().isFailure(header.hash));
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
// One Env for the suite, since building one costs far more than any case here. Safe
|
||||
// because every chain is seeded through nextSeed(): the node store, the fetch packs and
|
||||
// the remembered failures are all shared, and all three are keyed by hash.
|
||||
// One Env for the suite, shared safely because every chain is seeded through nextSeed().
|
||||
jtx::Env env{*this};
|
||||
|
||||
testLocalLedgerCompletesAcquire(env);
|
||||
testWalkSettlesBeforeReportingComplete(env);
|
||||
testInvalidatedLedgerFailsInDone(env);
|
||||
testInvalidatedMapFailsReceiveNode(env);
|
||||
testLocalFailureSignalsDone(env);
|
||||
testPeerZeroAccountHashFails(env);
|
||||
testPeerHeaderWithoutTransactionsCompletes(env);
|
||||
testFabricatedChainFailsAcquire(env);
|
||||
testWrongStateNodeKeepsAcquireAlive(env);
|
||||
testLocalChainFailsAcquire(env);
|
||||
testAggressiveRetryJudgesLocalMap(env);
|
||||
testWalkJudgesMapOnOrdinaryTrigger(env);
|
||||
testCompletesWhenNothingIsLeftToFetch(env);
|
||||
|
||||
// Last: the only case that waits out a whole timeout chain.
|
||||
testTimerRetriesThenGivesUp(env);
|
||||
|
||||
@@ -39,7 +39,6 @@
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/IOUAmount.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
@@ -995,9 +994,8 @@ class MPToken_test : public beast::unit_test::Suite
|
||||
test::jtx::MPT const mpT1("MPT", makeMptID(env.seq(alice) + 10, alice));
|
||||
err = !mpTokensV2 ? Ter(temMALFORMED) : Ter(tecOBJECT_NOT_FOUND);
|
||||
env(pay(alice, carol, mpT1(100)), Sendmax(mpt(100)), err);
|
||||
// "paths" is invalid in V1. In V2 any path from the issuer back
|
||||
// to its own MPT is a loop, like it is for a holder.
|
||||
err = !mpTokensV2 ? Ter(temMALFORMED) : Ter(temBAD_PATH_LOOP);
|
||||
// "paths" is invalid in V1
|
||||
err = !mpTokensV2 ? Ter(temMALFORMED) : Ter(tesSUCCESS);
|
||||
env(pay(alice, carol, mpt(100)), Path(~usd), err);
|
||||
}
|
||||
|
||||
@@ -1021,7 +1019,6 @@ class MPToken_test : public beast::unit_test::Suite
|
||||
payment[jss::build_path] = true;
|
||||
auto jrr = env.rpc("json", "submit", to_string(payment));
|
||||
BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::result][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
jrr[jss::result][jss::error_message] ==
|
||||
"Field 'build_path' not allowed in this context.");
|
||||
@@ -1583,8 +1580,6 @@ class MPToken_test : public beast::unit_test::Suite
|
||||
jv[jss::tx_json][jss::Amount][jss::value] = std::to_string(kMaxMpTokenAmount + 1);
|
||||
auto const jrr = env.rpc("json", "submit", to_string(jv));
|
||||
BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::result][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jrr[jss::result][jss::error_message] == "Invalid field 'tx_json.Amount'.");
|
||||
}
|
||||
|
||||
// Pay maximum amount with the transfer fee, SendMax, and
|
||||
@@ -1949,8 +1944,6 @@ class MPToken_test : public beast::unit_test::Suite
|
||||
jtx.stx->add(s);
|
||||
auto jrr = env.rpc("submit", strHex(s.slice()));
|
||||
BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidTransaction");
|
||||
BEAST_EXPECT(!jrr[jss::result].isMember(jss::error_code));
|
||||
BEAST_EXPECT(jrr[jss::result][jss::error_message].isNull());
|
||||
|
||||
// tx is unsigned
|
||||
json::Value jv1;
|
||||
@@ -1958,13 +1951,9 @@ class MPToken_test : public beast::unit_test::Suite
|
||||
jv1[jss::tx_json] = jv;
|
||||
jrr = env.rpc("json", "submit", to_string(jv1));
|
||||
BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::result][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jrr[jss::result][jss::error_message] == "Amount can not be MPT.");
|
||||
|
||||
jrr = env.rpc("json", "sign", to_string(jv1));
|
||||
BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::result][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jrr[jss::result][jss::error_message] == "Amount can not be MPT.");
|
||||
};
|
||||
auto setMPTFields = [&](SField const& field, json::Value& jv, bool withAmount = true) {
|
||||
jv[jss::Asset] = toJson(xrpIssue());
|
||||
@@ -3208,9 +3197,6 @@ class MPToken_test : public beast::unit_test::Suite
|
||||
jv1[jss::tx_json] = jv;
|
||||
auto const jrr = env.rpc("json", "submit", to_string(jv1));
|
||||
BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::result][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
jrr[jss::result][jss::error_message] == "Field 'tx_json.Amount' has invalid data.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
#include <xrpl/config/Constants.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/KeyType.h>
|
||||
@@ -587,7 +586,6 @@ public:
|
||||
auto jrr = env.rpc("json", "sign_for", to_string(jvOne))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::status] == "error");
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
jrr[jss::error_message] ==
|
||||
"When multi-signing 'tx_json.SigningPubKey' must be empty.");
|
||||
@@ -615,7 +613,6 @@ public:
|
||||
jrr = env.rpc("json", "submit_multisigned", to_string(jvSubmit))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::status] == "error");
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
jrr[jss::error_message] == "Invalid Fee field. Fees must be greater than zero.");
|
||||
}
|
||||
@@ -642,7 +639,6 @@ public:
|
||||
jrr = env.rpc("json", "submit_multisigned", to_string(jvSubmit))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::status] == "error");
|
||||
BEAST_EXPECT(jrr[jss::error] == "internal");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInternal);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Internal error.");
|
||||
}
|
||||
|
||||
@@ -655,8 +651,6 @@ public:
|
||||
auto jrr = env.rpc("json", "sign_for", to_string(jv))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::status] == "error");
|
||||
BEAST_EXPECT(jrr[jss::error] == "masterDisabled");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcMasterDisabled);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Master key is disabled.");
|
||||
env.close();
|
||||
BEAST_EXPECT(env.seq(alice) == aliceSeq);
|
||||
}
|
||||
@@ -696,16 +690,12 @@ public:
|
||||
auto jrr = env.rpc("json", "sign_for", to_string(jv))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::status] == "error");
|
||||
BEAST_EXPECT(jrr[jss::error] == "srcActMalformed");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcSrcActMalformed);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'tx_json.Account'.");
|
||||
|
||||
Account const jimmy{"jimmy"};
|
||||
jv[jss::tx_json][jss::Account] = jimmy.human();
|
||||
jrr = env.rpc("json", "sign_for", to_string(jv))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::status] == "error");
|
||||
BEAST_EXPECT(jrr[jss::error] == "srcActNotFound");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcSrcActNotFound);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Source account not found.");
|
||||
}
|
||||
|
||||
{
|
||||
@@ -716,7 +706,6 @@ public:
|
||||
auto jrr = env.rpc("json", "submit_multisigned", to_string(jv))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::status] == "error");
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "tx_json.Signers array may not be empty.");
|
||||
env.close();
|
||||
BEAST_EXPECT(env.seq(alice) == aliceSeq);
|
||||
@@ -1079,9 +1068,6 @@ public:
|
||||
STTx local = *tx.stx;
|
||||
local.setFieldVL(sfSigningPubKey, Blob()); // Empty SigningPubKey
|
||||
auto const info = submitSTTx(local);
|
||||
BEAST_EXPECT(info[jss::result][jss::error] == "invalidTransaction");
|
||||
BEAST_EXPECT(!info[jss::result].isMember(jss::error_code));
|
||||
BEAST_EXPECT(info[jss::result][jss::error_message].isNull());
|
||||
BEAST_EXPECT(
|
||||
info[jss::result][jss::error_exception] ==
|
||||
"fails local checks: Empty SigningPubKey.");
|
||||
@@ -1096,9 +1082,6 @@ public:
|
||||
local.setFieldVL(sfTxnSignature, badSig);
|
||||
// Signature should fail.
|
||||
auto const info = submitSTTx(local);
|
||||
BEAST_EXPECT(info[jss::result][jss::error] == "invalidTransaction");
|
||||
BEAST_EXPECT(!info[jss::result].isMember(jss::error_code));
|
||||
BEAST_EXPECT(info[jss::result][jss::error_message].isNull());
|
||||
BEAST_EXPECT(
|
||||
info[jss::result][jss::error_exception] ==
|
||||
"fails local checks: Invalid signature.");
|
||||
@@ -1112,9 +1095,6 @@ public:
|
||||
local.setFieldU32(sfSequence, seq + 1);
|
||||
// Signature should fail.
|
||||
auto const info = submitSTTx(local);
|
||||
BEAST_EXPECT(info[jss::result][jss::error] == "invalidTransaction");
|
||||
BEAST_EXPECT(!info[jss::result].isMember(jss::error_code));
|
||||
BEAST_EXPECT(info[jss::result][jss::error_message].isNull());
|
||||
BEAST_EXPECT(
|
||||
info[jss::result][jss::error_exception] ==
|
||||
"fails local checks: Invalid signature.");
|
||||
@@ -1125,9 +1105,6 @@ public:
|
||||
STTx local = *tx.stx;
|
||||
local[sfSigningPubKey] = alice.pk(); // Insert sfSigningPubKey
|
||||
auto const info = submitSTTx(local);
|
||||
BEAST_EXPECT(info[jss::result][jss::error] == "invalidTransaction");
|
||||
BEAST_EXPECT(!info[jss::result].isMember(jss::error_code));
|
||||
BEAST_EXPECT(info[jss::result][jss::error_message].isNull());
|
||||
BEAST_EXPECT(
|
||||
info[jss::result][jss::error_exception] ==
|
||||
"fails local checks: Cannot both single- and multi-sign.");
|
||||
@@ -1139,9 +1116,6 @@ public:
|
||||
local.sign(alice.pk(), alice.sk());
|
||||
local.setFieldVL(sfSigningPubKey, Blob()); // Empty SigningPubKey
|
||||
auto const info = submitSTTx(local);
|
||||
BEAST_EXPECT(info[jss::result][jss::error] == "invalidTransaction");
|
||||
BEAST_EXPECT(!info[jss::result].isMember(jss::error_code));
|
||||
BEAST_EXPECT(info[jss::result][jss::error_message].isNull());
|
||||
BEAST_EXPECT(
|
||||
info[jss::result][jss::error_exception] ==
|
||||
"fails local checks: Cannot both single- and multi-sign.");
|
||||
@@ -1157,9 +1131,6 @@ public:
|
||||
signer.setFieldVL(sfTxnSignature, badSig);
|
||||
// Signature should fail.
|
||||
auto const info = submitSTTx(local);
|
||||
BEAST_EXPECT(info[jss::result][jss::error] == "invalidTransaction");
|
||||
BEAST_EXPECT(!info[jss::result].isMember(jss::error_code));
|
||||
BEAST_EXPECT(info[jss::result][jss::error_message].isNull());
|
||||
BEAST_EXPECT(
|
||||
info[jss::result][jss::error_exception].asString().contains(
|
||||
"Invalid signature on account r"));
|
||||
@@ -1170,9 +1141,6 @@ public:
|
||||
STTx local = *tx.stx;
|
||||
local.peekFieldArray(sfSigners).clear(); // Empty Signers array.
|
||||
auto const info = submitSTTx(local);
|
||||
BEAST_EXPECT(info[jss::result][jss::error] == "invalidTransaction");
|
||||
BEAST_EXPECT(!info[jss::result].isMember(jss::error_code));
|
||||
BEAST_EXPECT(info[jss::result][jss::error_message].isNull());
|
||||
BEAST_EXPECT(
|
||||
info[jss::result][jss::error_exception] ==
|
||||
"fails local checks: Invalid Signers array size.");
|
||||
@@ -1218,9 +1186,6 @@ public:
|
||||
bogie_));
|
||||
STTx const local = *tx.stx;
|
||||
auto const info = submitSTTx(local);
|
||||
BEAST_EXPECT(info[jss::result][jss::error] == "invalidTransaction");
|
||||
BEAST_EXPECT(!info[jss::result].isMember(jss::error_code));
|
||||
BEAST_EXPECT(info[jss::result][jss::error_message].isNull());
|
||||
BEAST_EXPECT(
|
||||
info[jss::result][jss::error_exception] ==
|
||||
"fails local checks: Invalid Signers array size.");
|
||||
@@ -1230,9 +1195,6 @@ public:
|
||||
JTx const tx = env.jt(noop(alice), Fee(2 * baseFee), Msig(alice));
|
||||
STTx const local = *tx.stx;
|
||||
auto const info = submitSTTx(local);
|
||||
BEAST_EXPECT(info[jss::result][jss::error] == "invalidTransaction");
|
||||
BEAST_EXPECT(!info[jss::result].isMember(jss::error_code));
|
||||
BEAST_EXPECT(info[jss::result][jss::error_message].isNull());
|
||||
BEAST_EXPECT(
|
||||
info[jss::result][jss::error_exception] ==
|
||||
"fails local checks: Invalid multisigner.");
|
||||
@@ -1242,9 +1204,6 @@ public:
|
||||
JTx const tx = env.jt(noop(alice), Fee(2 * baseFee), Msig(bogie_, bogie_));
|
||||
STTx const local = *tx.stx;
|
||||
auto const info = submitSTTx(local);
|
||||
BEAST_EXPECT(info[jss::result][jss::error] == "invalidTransaction");
|
||||
BEAST_EXPECT(!info[jss::result].isMember(jss::error_code));
|
||||
BEAST_EXPECT(info[jss::result][jss::error_message].isNull());
|
||||
BEAST_EXPECT(
|
||||
info[jss::result][jss::error_exception] ==
|
||||
"fails local checks: Duplicate Signers not allowed.");
|
||||
@@ -1258,9 +1217,6 @@ public:
|
||||
std::ranges::reverse(signers);
|
||||
// Signature should fail.
|
||||
auto const info = submitSTTx(local);
|
||||
BEAST_EXPECT(info[jss::result][jss::error] == "invalidTransaction");
|
||||
BEAST_EXPECT(!info[jss::result].isMember(jss::error_code));
|
||||
BEAST_EXPECT(info[jss::result][jss::error_message].isNull());
|
||||
BEAST_EXPECT(
|
||||
info[jss::result][jss::error_exception] ==
|
||||
"fails local checks: Unsorted Signers array.");
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/ledger/OpenView.h>
|
||||
#include <xrpl/protocol/ApiVersion.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
@@ -4654,22 +4653,18 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
|
||||
if (expect(
|
||||
nftOffers.isMember(jss::result), "expected \"result\"", __FILE__, line))
|
||||
{
|
||||
json::Value const& result = nftOffers[jss::result];
|
||||
expect(
|
||||
result[jss::error].asString() == "objectNotFound",
|
||||
"expected \"objectNotFound\"",
|
||||
__FILE__,
|
||||
line);
|
||||
expect(
|
||||
result[jss::error_code] == RpcObjectNotFound,
|
||||
"expected RpcObjectNotFound",
|
||||
__FILE__,
|
||||
line);
|
||||
expect(
|
||||
result[jss::error_message] == "The requested object was not found.",
|
||||
"expected \"The requested object was not found.\"",
|
||||
__FILE__,
|
||||
line);
|
||||
if (expect(
|
||||
nftOffers[jss::result].isMember(jss::error),
|
||||
"expected \"error\"",
|
||||
__FILE__,
|
||||
line))
|
||||
{
|
||||
expect(
|
||||
nftOffers[jss::result][jss::error].asString() == "objectNotFound",
|
||||
"expected \"objectNotFound\"",
|
||||
__FILE__,
|
||||
line);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -4865,15 +4860,11 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
|
||||
// Sell-side marker passed to nft_buy_offers.
|
||||
json::Value const result = queryWithMarker("nft_buy_offers", sellOfferIndex);
|
||||
BEAST_EXPECT(result[jss::error].asString() == "invalidParams");
|
||||
BEAST_EXPECT(result[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(result[jss::error_message] == "Invalid parameters.");
|
||||
}
|
||||
{
|
||||
// Buy-side marker passed to nft_sell_offers.
|
||||
json::Value const result = queryWithMarker("nft_sell_offers", buyOfferIndex);
|
||||
BEAST_EXPECT(result[jss::error].asString() == "invalidParams");
|
||||
BEAST_EXPECT(result[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(result[jss::error_message] == "Invalid parameters.");
|
||||
}
|
||||
|
||||
// A same-side marker is still accepted. With a single offer on each
|
||||
|
||||
@@ -34,7 +34,6 @@
|
||||
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Book.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
@@ -2756,8 +2755,6 @@ public:
|
||||
|
||||
jro = ledgerEntryOffer(env, bob, bobOfferSeq);
|
||||
BEAST_EXPECT(jro[jss::error] == "entryNotFound");
|
||||
BEAST_EXPECT(jro[jss::error_code] == RpcEntryNotFound);
|
||||
BEAST_EXPECT(jro[jss::error_message] == "Entry not found.");
|
||||
|
||||
env.require(Owners(alice, 1), Owners(bob, 1));
|
||||
}
|
||||
@@ -2791,8 +2788,6 @@ public:
|
||||
|
||||
auto jro = ledgerEntryOffer(env, bob, bobOfferSeq);
|
||||
BEAST_EXPECT(jro[jss::error] == "entryNotFound");
|
||||
BEAST_EXPECT(jro[jss::error_code] == RpcEntryNotFound);
|
||||
BEAST_EXPECT(jro[jss::error_message] == "Entry not found.");
|
||||
};
|
||||
testHelper3TokensMix(test);
|
||||
}
|
||||
@@ -2852,8 +2847,6 @@ public:
|
||||
// Verify the offer was consumed
|
||||
jro = ledgerEntryOffer(env, bob, bobOfferSeq);
|
||||
BEAST_EXPECT(jro[jss::error] == "entryNotFound");
|
||||
BEAST_EXPECT(jro[jss::error_code] == RpcEntryNotFound);
|
||||
BEAST_EXPECT(jro[jss::error_message] == "Entry not found.");
|
||||
|
||||
// verify balances look right after the partial payment
|
||||
// only 300 XRP should have been payed since that's all
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
@@ -1708,8 +1707,6 @@ public:
|
||||
|
||||
jro = ledgerEntryOffer(env, bob, bobOfferSeq);
|
||||
BEAST_EXPECT(jro[jss::error] == "entryNotFound");
|
||||
BEAST_EXPECT(jro[jss::error_code] == RpcEntryNotFound);
|
||||
BEAST_EXPECT(jro[jss::error_message] == "Entry not found.");
|
||||
|
||||
env.require(Owners(alice, 1), Owners(bob, 1));
|
||||
}
|
||||
@@ -1741,8 +1738,6 @@ public:
|
||||
|
||||
auto jro = ledgerEntryOffer(env, bob, bobOfferSeq);
|
||||
BEAST_EXPECT(jro[jss::error] == "entryNotFound");
|
||||
BEAST_EXPECT(jro[jss::error_code] == RpcEntryNotFound);
|
||||
BEAST_EXPECT(jro[jss::error_message] == "Entry not found.");
|
||||
}
|
||||
|
||||
void
|
||||
@@ -1803,8 +1798,6 @@ public:
|
||||
// Verify the offer was consumed
|
||||
jro = ledgerEntryOffer(env, bob, bobOfferSeq);
|
||||
BEAST_EXPECT(jro[jss::error] == "entryNotFound");
|
||||
BEAST_EXPECT(jro[jss::error_code] == RpcEntryNotFound);
|
||||
BEAST_EXPECT(jro[jss::error_message] == "Entry not found.");
|
||||
|
||||
// verify balances look right after the partial payment
|
||||
// only 300 XRP should be have been payed since that's all
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/ApiVersion.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/PathAsset.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
@@ -163,9 +162,7 @@ public:
|
||||
g.signal();
|
||||
});
|
||||
BEAST_EXPECT(g.waitFor(5s));
|
||||
BEAST_EXPECT(result[jss::error] == "srcCurMalformed");
|
||||
BEAST_EXPECT(result[jss::error_code] == RpcSrcCurMalformed);
|
||||
BEAST_EXPECT(result[jss::error_message] == "Source currency is malformed.");
|
||||
BEAST_EXPECT(result.isMember(jss::error));
|
||||
|
||||
// Test rpc::tuning::max_auto_src_cur source currencies.
|
||||
numSrc.clear();
|
||||
@@ -192,9 +189,7 @@ public:
|
||||
g.signal();
|
||||
});
|
||||
BEAST_EXPECT(g.waitFor(5s));
|
||||
BEAST_EXPECT(result[jss::error] == "internal");
|
||||
BEAST_EXPECT(result[jss::error_code] == RpcInternal);
|
||||
BEAST_EXPECT(result[jss::error_message] == "Internal error.");
|
||||
BEAST_EXPECT(result.isMember(jss::error));
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/ApiVersion.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
@@ -319,9 +318,7 @@ public:
|
||||
g.signal();
|
||||
});
|
||||
BEAST_EXPECT(g.waitFor(5s));
|
||||
BEAST_EXPECT(result[jss::error] == "srcCurMalformed");
|
||||
BEAST_EXPECT(result[jss::error_code] == RpcSrcCurMalformed);
|
||||
BEAST_EXPECT(result[jss::error_message] == "Source currency is malformed.");
|
||||
BEAST_EXPECT(result.isMember(jss::error));
|
||||
|
||||
// Test rpc::tuning::max_auto_src_cur source currencies.
|
||||
for (auto i = 0; i < (rpc::tuning::kMaxAutoSrcCur - 1); ++i)
|
||||
@@ -344,9 +341,7 @@ public:
|
||||
g.signal();
|
||||
});
|
||||
BEAST_EXPECT(g.waitFor(5s));
|
||||
BEAST_EXPECT(result[jss::error] == "internal");
|
||||
BEAST_EXPECT(result[jss::error_code] == RpcInternal);
|
||||
BEAST_EXPECT(result[jss::error_message] == "Internal error.");
|
||||
BEAST_EXPECT(result.isMember(jss::error));
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
#include <xrpl/ledger/Dir.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/ApiVersion.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/KeyType.h>
|
||||
@@ -1010,7 +1009,6 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
auto const jrr =
|
||||
env.rpc("json", "account_channels", to_string(params))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'account'.");
|
||||
};
|
||||
|
||||
@@ -1030,7 +1028,6 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
auto const jrr =
|
||||
env.rpc("json", "account_channels", to_string(params))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'destination_account'.");
|
||||
};
|
||||
|
||||
@@ -1041,55 +1038,6 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
testInvalidDestAccountParam(json::Value(json::ValueType::Object));
|
||||
testInvalidDestAccountParam(json::Value(json::ValueType::Array));
|
||||
}
|
||||
{
|
||||
// An empty destination_account is no destination at all.
|
||||
json::Value params;
|
||||
params[jss::account] = alice.human();
|
||||
params[jss::destination_account] = "";
|
||||
auto const jrr = env.rpc("json", "account_channels", to_string(params))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::channels].size() == 1);
|
||||
BEAST_EXPECT(jrr[jss::channels][0u][jss::channel_id] == chan1Str);
|
||||
BEAST_EXPECT(jrr[jss::channels][0u][jss::destination_account] == bob.human());
|
||||
}
|
||||
{
|
||||
// A malformed account is reported without the ledger fields.
|
||||
json::Value params;
|
||||
params[jss::account] = "llIIOO";
|
||||
auto const jrr = env.rpc("json", "account_channels", to_string(params))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "actMalformed");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcActMalformed);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Account malformed.");
|
||||
BEAST_EXPECT(!jrr.isMember(jss::ledger_index));
|
||||
BEAST_EXPECT(!jrr.isMember(jss::ledger_current_index));
|
||||
}
|
||||
{
|
||||
// A bad limit is reported before an account that is not found.
|
||||
json::Value params;
|
||||
params[jss::account] = Account("bogie").human();
|
||||
params[jss::limit] = 0;
|
||||
auto const jrr = env.rpc("json", "account_channels", to_string(params))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'limit'.");
|
||||
}
|
||||
{
|
||||
// A non-string marker names the field; one that does not parse does not.
|
||||
json::Value params;
|
||||
params[jss::account] = alice.human();
|
||||
params[jss::marker] = 1;
|
||||
auto const notString =
|
||||
env.rpc("json", "account_channels", to_string(params))[jss::result];
|
||||
BEAST_EXPECT(notString[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(notString[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(notString[jss::error_message] == "Invalid field 'marker', not string.");
|
||||
|
||||
params[jss::marker] = "NOT_A_MARKER";
|
||||
auto const malformed =
|
||||
env.rpc("json", "account_channels", to_string(params))[jss::result];
|
||||
BEAST_EXPECT(malformed[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(malformed[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(malformed[jss::error_message] == "Invalid parameters.");
|
||||
}
|
||||
{
|
||||
auto const r = env.rpc("account_channels", alice.human(), bob.human());
|
||||
BEAST_EXPECT(r[jss::result][jss::channels].size() == 1);
|
||||
@@ -1233,34 +1181,7 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
{
|
||||
// degenerate case
|
||||
auto const r = testLimit(env, alice, 0);
|
||||
BEAST_EXPECT(r[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(r[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(r[jss::error_message] == "Invalid field 'limit'.");
|
||||
}
|
||||
|
||||
{
|
||||
// The marker's start hint may have a leading '+', and anything after a second
|
||||
// comma is ignored.
|
||||
auto const first = testLimit(env, alice, 1);
|
||||
auto const marker = first[jss::marker].asString();
|
||||
auto const comma = marker.find(',');
|
||||
BEAST_EXPECT(comma != std::string::npos);
|
||||
|
||||
auto const rest = testLimit(
|
||||
env,
|
||||
alice,
|
||||
std::nullopt,
|
||||
marker.substr(0, comma) + ",+" + marker.substr(comma + 1) + ",ignored");
|
||||
BEAST_EXPECT(rest[jss::channels].size() == bobs.size() - 1);
|
||||
|
||||
std::set<std::string> found{
|
||||
first[jss::channels][0u][jss::destination_account].asString(),
|
||||
};
|
||||
|
||||
for (auto const& c : rest[jss::channels])
|
||||
BEAST_EXPECT(found.insert(c[jss::destination_account].asString()).second);
|
||||
|
||||
BEAST_EXPECT(found == bobsB58);
|
||||
BEAST_EXPECT(r.isMember(jss::error_message));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1329,12 +1250,7 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
auto const rs =
|
||||
env.rpc("json", "channel_authorize", args.toStyledString())[jss::result];
|
||||
auto const error = apiVersion < 2u ? "invalidParams" : "badKeyType";
|
||||
auto const errorCode = apiVersion < 2u ? RpcInvalidParams : RpcBadKeyType;
|
||||
auto const errorMessage =
|
||||
apiVersion < 2u ? "Invalid field 'key_type'." : "Bad key type.";
|
||||
BEAST_EXPECT(rs[jss::error] == error);
|
||||
BEAST_EXPECT(rs[jss::error_code] == errorCode);
|
||||
BEAST_EXPECT(rs[jss::error_message] == errorMessage);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1423,40 +1339,22 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
auto const pkAsHex = sliceToHex(pk.slice());
|
||||
auto rv = env.rpc("channel_verify", pkAsHex, chan1Str, "1000x", sig);
|
||||
BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed");
|
||||
BEAST_EXPECT(rv[jss::error_code] == RpcChannelAmtMalformed);
|
||||
BEAST_EXPECT(rv[jss::error_message] == "Payment channel amount is malformed.");
|
||||
rv = env.rpc("channel_verify", pkAsHex, chan1Str, "1000 ", sig);
|
||||
BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed");
|
||||
BEAST_EXPECT(rv[jss::error_code] == RpcChannelAmtMalformed);
|
||||
BEAST_EXPECT(rv[jss::error_message] == "Payment channel amount is malformed.");
|
||||
rv = env.rpc("channel_verify", pkAsHex, chan1Str, "x1000", sig);
|
||||
BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed");
|
||||
BEAST_EXPECT(rv[jss::error_code] == RpcChannelAmtMalformed);
|
||||
BEAST_EXPECT(rv[jss::error_message] == "Payment channel amount is malformed.");
|
||||
rv = env.rpc("channel_verify", pkAsHex, chan1Str, "x", sig);
|
||||
BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed");
|
||||
BEAST_EXPECT(rv[jss::error_code] == RpcChannelAmtMalformed);
|
||||
BEAST_EXPECT(rv[jss::error_message] == "Payment channel amount is malformed.");
|
||||
rv = env.rpc("channel_verify", pkAsHex, chan1Str, " ", sig);
|
||||
BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed");
|
||||
BEAST_EXPECT(rv[jss::error_code] == RpcChannelAmtMalformed);
|
||||
BEAST_EXPECT(rv[jss::error_message] == "Payment channel amount is malformed.");
|
||||
rv = env.rpc("channel_verify", pkAsHex, chan1Str, "1000 1000", sig);
|
||||
BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed");
|
||||
BEAST_EXPECT(rv[jss::error_code] == RpcChannelAmtMalformed);
|
||||
BEAST_EXPECT(rv[jss::error_message] == "Payment channel amount is malformed.");
|
||||
rv = env.rpc("channel_verify", pkAsHex, chan1Str, "1,000", sig);
|
||||
BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed");
|
||||
BEAST_EXPECT(rv[jss::error_code] == RpcChannelAmtMalformed);
|
||||
BEAST_EXPECT(rv[jss::error_message] == "Payment channel amount is malformed.");
|
||||
rv = env.rpc("channel_verify", pkAsHex, chan1Str, " 1000", sig);
|
||||
BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed");
|
||||
BEAST_EXPECT(rv[jss::error_code] == RpcChannelAmtMalformed);
|
||||
BEAST_EXPECT(rv[jss::error_message] == "Payment channel amount is malformed.");
|
||||
rv = env.rpc("channel_verify", pkAsHex, chan1Str, "", sig);
|
||||
BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed");
|
||||
BEAST_EXPECT(rv[jss::error_code] == RpcChannelAmtMalformed);
|
||||
BEAST_EXPECT(rv[jss::error_message] == "Payment channel amount is malformed.");
|
||||
}
|
||||
{
|
||||
// malformed channel
|
||||
@@ -1465,34 +1363,22 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
chan1StrBad.pop_back();
|
||||
auto rv = env.rpc("channel_verify", pkAsHex, chan1StrBad, "1000", sig);
|
||||
BEAST_EXPECT(rv[jss::error] == "channelMalformed");
|
||||
BEAST_EXPECT(rv[jss::error_code] == RpcChannelMalformed);
|
||||
BEAST_EXPECT(rv[jss::error_message] == "Payment channel is malformed.");
|
||||
rv = env.rpc("channel_authorize", "alice", chan1StrBad, "1000");
|
||||
BEAST_EXPECT(rv[jss::error] == "channelMalformed");
|
||||
BEAST_EXPECT(rv[jss::error_code] == RpcChannelMalformed);
|
||||
BEAST_EXPECT(rv[jss::error_message] == "Payment channel is malformed.");
|
||||
|
||||
chan1StrBad = chan1Str;
|
||||
chan1StrBad.push_back('0');
|
||||
rv = env.rpc("channel_verify", pkAsHex, chan1StrBad, "1000", sig);
|
||||
BEAST_EXPECT(rv[jss::error] == "channelMalformed");
|
||||
BEAST_EXPECT(rv[jss::error_code] == RpcChannelMalformed);
|
||||
BEAST_EXPECT(rv[jss::error_message] == "Payment channel is malformed.");
|
||||
rv = env.rpc("channel_authorize", "alice", chan1StrBad, "1000");
|
||||
BEAST_EXPECT(rv[jss::error] == "channelMalformed");
|
||||
BEAST_EXPECT(rv[jss::error_code] == RpcChannelMalformed);
|
||||
BEAST_EXPECT(rv[jss::error_message] == "Payment channel is malformed.");
|
||||
|
||||
chan1StrBad = chan1Str;
|
||||
chan1StrBad.back() = 'x';
|
||||
rv = env.rpc("channel_verify", pkAsHex, chan1StrBad, "1000", sig);
|
||||
BEAST_EXPECT(rv[jss::error] == "channelMalformed");
|
||||
BEAST_EXPECT(rv[jss::error_code] == RpcChannelMalformed);
|
||||
BEAST_EXPECT(rv[jss::error_message] == "Payment channel is malformed.");
|
||||
rv = env.rpc("channel_authorize", "alice", chan1StrBad, "1000");
|
||||
BEAST_EXPECT(rv[jss::error] == "channelMalformed");
|
||||
BEAST_EXPECT(rv[jss::error_code] == RpcChannelMalformed);
|
||||
BEAST_EXPECT(rv[jss::error_message] == "Payment channel is malformed.");
|
||||
}
|
||||
{
|
||||
// channel_id is not a string.
|
||||
@@ -1505,8 +1391,6 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
auto const ra =
|
||||
env.rpc("json", "channel_authorize", args.toStyledString())[jss::result];
|
||||
BEAST_EXPECT(ra[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(ra[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(ra[jss::error_message] == "Invalid parameters.");
|
||||
}
|
||||
{
|
||||
// channel_id is not a string.
|
||||
@@ -1519,8 +1403,6 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
auto const rv =
|
||||
env.rpc("json", "channel_verify", args.toStyledString())[jss::result];
|
||||
BEAST_EXPECT(rv[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(rv[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(rv[jss::error_message] == "Invalid parameters.");
|
||||
}
|
||||
{
|
||||
// give an ill formed base 58 public key
|
||||
@@ -1579,8 +1461,6 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
// Try to authorize using an unknown key type, except an error:
|
||||
auto const rs1 = env.rpc("channel_authorize", "charlie", "nyx", chan, "1000");
|
||||
BEAST_EXPECT(rs1[jss::error] == "badKeyType");
|
||||
BEAST_EXPECT(rs1[jss::error_code] == RpcBadKeyType);
|
||||
BEAST_EXPECT(rs1[jss::error_message] == "Bad key type.");
|
||||
|
||||
// Try to authorize using secp256k1; the authorization _should_
|
||||
// succeed but the verification should fail:
|
||||
@@ -1606,16 +1486,10 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
// send malformed amounts rpc requests
|
||||
auto rs = env.rpc("channel_authorize", "alice", chan1Str, "1000x");
|
||||
BEAST_EXPECT(rs[jss::error] == "channelAmtMalformed");
|
||||
BEAST_EXPECT(rs[jss::error_code] == RpcChannelAmtMalformed);
|
||||
BEAST_EXPECT(rs[jss::error_message] == "Payment channel amount is malformed.");
|
||||
rs = env.rpc("channel_authorize", "alice", chan1Str, "x1000");
|
||||
BEAST_EXPECT(rs[jss::error] == "channelAmtMalformed");
|
||||
BEAST_EXPECT(rs[jss::error_code] == RpcChannelAmtMalformed);
|
||||
BEAST_EXPECT(rs[jss::error_message] == "Payment channel amount is malformed.");
|
||||
rs = env.rpc("channel_authorize", "alice", chan1Str, "x");
|
||||
BEAST_EXPECT(rs[jss::error] == "channelAmtMalformed");
|
||||
BEAST_EXPECT(rs[jss::error_code] == RpcChannelAmtMalformed);
|
||||
BEAST_EXPECT(rs[jss::error_message] == "Payment channel amount is malformed.");
|
||||
{
|
||||
// Missing channel_id
|
||||
json::Value args{json::ValueType::Object};
|
||||
@@ -1624,8 +1498,6 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
args[jss::passphrase] = "passphrase_can_be_anything";
|
||||
rs = env.rpc("json", "channel_authorize", args.toStyledString())[jss::result];
|
||||
BEAST_EXPECT(rs[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(rs[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(rs[jss::error_message] == "Missing field 'channel_id'.");
|
||||
}
|
||||
{
|
||||
// Missing amount
|
||||
@@ -1635,8 +1507,6 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
args[jss::passphrase] = "passphrase_can_be_anything";
|
||||
rs = env.rpc("json", "channel_authorize", args.toStyledString())[jss::result];
|
||||
BEAST_EXPECT(rs[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(rs[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(rs[jss::error_message] == "Missing field 'amount'.");
|
||||
}
|
||||
{
|
||||
// Missing key_type and no secret.
|
||||
@@ -1646,8 +1516,6 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
args[jss::passphrase] = "passphrase_can_be_anything";
|
||||
rs = env.rpc("json", "channel_authorize", args.toStyledString())[jss::result];
|
||||
BEAST_EXPECT(rs[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(rs[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(rs[jss::error_message] == "Missing field 'secret'.");
|
||||
}
|
||||
{
|
||||
// Both passphrase and seed specified.
|
||||
@@ -1659,11 +1527,6 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
args[jss::seed] = "seed can be anything";
|
||||
rs = env.rpc("json", "channel_authorize", args.toStyledString())[jss::result];
|
||||
BEAST_EXPECT(rs[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(rs[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
rs[jss::error_message] ==
|
||||
"Exactly one of the following must be specified: passphrase, secret, seed or "
|
||||
"seed_hex");
|
||||
}
|
||||
{
|
||||
// channel_id is not exact hex.
|
||||
@@ -1674,8 +1537,6 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
args[jss::passphrase] = "passphrase_can_be_anything";
|
||||
rs = env.rpc("json", "channel_authorize", args.toStyledString())[jss::result];
|
||||
BEAST_EXPECT(rs[jss::error] == "channelMalformed");
|
||||
BEAST_EXPECT(rs[jss::error_code] == RpcChannelMalformed);
|
||||
BEAST_EXPECT(rs[jss::error_message] == "Payment channel is malformed.");
|
||||
}
|
||||
{
|
||||
// amount is not a string
|
||||
@@ -1686,8 +1547,6 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
args[jss::passphrase] = "passphrase_can_be_anything";
|
||||
rs = env.rpc("json", "channel_authorize", args.toStyledString())[jss::result];
|
||||
BEAST_EXPECT(rs[jss::error] == "channelAmtMalformed");
|
||||
BEAST_EXPECT(rs[jss::error_code] == RpcChannelAmtMalformed);
|
||||
BEAST_EXPECT(rs[jss::error_message] == "Payment channel amount is malformed.");
|
||||
}
|
||||
{
|
||||
// Amount is not a decimal string.
|
||||
@@ -1698,8 +1557,6 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
args[jss::passphrase] = "passphrase_can_be_anything";
|
||||
rs = env.rpc("json", "channel_authorize", args.toStyledString())[jss::result];
|
||||
BEAST_EXPECT(rs[jss::error] == "channelAmtMalformed");
|
||||
BEAST_EXPECT(rs[jss::error_code] == RpcChannelAmtMalformed);
|
||||
BEAST_EXPECT(rs[jss::error_message] == "Payment channel amount is malformed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1950,8 +1807,6 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
)*";
|
||||
auto const r = env.rpc("json", "channel_verify", txn);
|
||||
BEAST_EXPECT(r["result"]["error"] == "publicMalformed");
|
||||
BEAST_EXPECT(r["result"]["error_code"] == RpcPublicMalformed);
|
||||
BEAST_EXPECT(r["result"]["error_message"] == "Public key is malformed.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Quality.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
@@ -43,9 +42,8 @@ class ReducedOffer_test : public beast::unit_test::Suite
|
||||
{
|
||||
json::Value ledgerOffer = ledgerEntryOffer(env, acct, offerSeq);
|
||||
return !(
|
||||
ledgerOffer[jss::error].asString() == "entryNotFound" &&
|
||||
ledgerOffer[jss::error_code] == RpcEntryNotFound &&
|
||||
ledgerOffer[jss::error_message] == "Entry not found.");
|
||||
ledgerOffer.isMember(jss::error) &&
|
||||
ledgerOffer[jss::error].asString() == "entryNotFound");
|
||||
}
|
||||
|
||||
// Common code to clean up unneeded offers.
|
||||
|
||||
@@ -302,14 +302,10 @@ class SHAMapStore_test : public beast::unit_test::Suite
|
||||
}
|
||||
|
||||
static bool
|
||||
bad(json::Value const& json,
|
||||
std::string const& token,
|
||||
ErrorCodeI error,
|
||||
std::string const& message)
|
||||
bad(json::Value const& json, ErrorCodeI error = RpcLgrNotFound)
|
||||
{
|
||||
return json.isMember(jss::result) && json[jss::result][jss::error] == token &&
|
||||
json[jss::result][jss::error_code] == error &&
|
||||
json[jss::result][jss::error_message] == message;
|
||||
return json.isMember(jss::result) && rpc::containsError(json[jss::result]) &&
|
||||
json[jss::result][jss::error_code] == error;
|
||||
}
|
||||
|
||||
std::string
|
||||
@@ -887,7 +883,7 @@ public:
|
||||
std::map<std::uint32_t, json::Value const> ledgers;
|
||||
|
||||
auto ledgerTmp = env.rpc("ledger", "0");
|
||||
BEAST_EXPECT(bad(ledgerTmp, "lgrNotFound", RpcLgrNotFound, "ledgerNotFound"));
|
||||
BEAST_EXPECT(bad(ledgerTmp));
|
||||
|
||||
ledgers.emplace(1, env.rpc("ledger", "1"));
|
||||
BEAST_EXPECT(goodLedger(env, ledgers[1], "1"));
|
||||
@@ -899,10 +895,10 @@ public:
|
||||
BEAST_EXPECT(goodLedger(env, ledgerTmp, "3"));
|
||||
|
||||
ledgerTmp = env.rpc("ledger", "4");
|
||||
BEAST_EXPECT(bad(ledgerTmp, "lgrNotFound", RpcLgrNotFound, "ledgerNotFound"));
|
||||
BEAST_EXPECT(bad(ledgerTmp));
|
||||
|
||||
ledgerTmp = env.rpc("ledger", "100");
|
||||
BEAST_EXPECT(bad(ledgerTmp, "lgrNotFound", RpcLgrNotFound, "ledgerNotFound"));
|
||||
BEAST_EXPECT(bad(ledgerTmp));
|
||||
|
||||
auto const firstSeq = waitForReady(env);
|
||||
auto lastRotated = firstSeq - 1;
|
||||
@@ -991,7 +987,7 @@ public:
|
||||
// Because advisory_delete is unset,
|
||||
// "can_delete" is disabled.
|
||||
auto const canDelete = env.rpc("can_delete");
|
||||
BEAST_EXPECT(bad(canDelete, "notEnabled", RpcNotEnabled, "Not enabled in configuration."));
|
||||
BEAST_EXPECT(bad(canDelete, RpcNotEnabled));
|
||||
|
||||
// Close ledgers without triggering a rotate
|
||||
for (; ledgerSeq < lastRotated + kDeleteInterval; ++ledgerSeq)
|
||||
|
||||
@@ -59,7 +59,6 @@
|
||||
#include <xrpl/tx/apply.h>
|
||||
#include <xrpl/tx/applySteps.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
@@ -3551,101 +3550,6 @@ public:
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0);
|
||||
}
|
||||
|
||||
{
|
||||
// Bytecode Escrow: its reserve scales with the Bytecode size, so
|
||||
// every leg of a sponsorship has to move the whole weight.
|
||||
auto env = Env{*this, testableAmendments()};
|
||||
|
||||
env.fund(XRP(1000000), alice, bob, sponsor, sponsor2);
|
||||
env.close();
|
||||
|
||||
auto const seq = env.seq(alice);
|
||||
env(escrow::create(alice, bob, XRP(100)),
|
||||
escrow::kCondition(escrow::kCb1),
|
||||
escrow::kCancelTime(env.now() + 100s));
|
||||
env.close();
|
||||
|
||||
auto const escrowKeylet = keylet::escrow(alice, SeqProxy::rawSequence(seq));
|
||||
std::size_t const bytecodeSize = 1200;
|
||||
std::uint32_t const bytecodeWeight = 3; // ceil(1200 / 500)
|
||||
|
||||
if (!cosigning)
|
||||
{
|
||||
env(sponsor::set_reserve(sponsor, 0, bytecodeWeight), sponsor::SponseeAcc(alice));
|
||||
env(sponsor::set_reserve(sponsor2, 0, bytecodeWeight), sponsor::SponseeAcc(alice));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// EscrowCreate's preclaim runs the WASM preflight, so a real
|
||||
// 1200-byte contract cannot be assembled from here; the Bytecode
|
||||
// field is injected directly and the owner's OwnerCount restored
|
||||
// to the weight such a create would have charged.
|
||||
env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal) -> bool {
|
||||
auto const existing = view.read(escrowKeylet);
|
||||
if (!existing)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
auto const escrowSle = std::make_shared<SLE>(*existing);
|
||||
auto const account = std::make_shared<SLE>(*view.read(keylet::account(alice.id())));
|
||||
escrowSle->setFieldVL(sfBytecode, Blob(bytecodeSize, 0x00));
|
||||
account->setFieldU32(sfOwnerCount, bytecodeWeight);
|
||||
view.rawReplace(escrowSle);
|
||||
view.rawReplace(account);
|
||||
return true;
|
||||
});
|
||||
BEAST_EXPECT(ownerCount(env, alice) == bytecodeWeight);
|
||||
|
||||
std::optional<Sig> const sponsorSig =
|
||||
cosigning ? std::optional<Sig>(Sig(sfSponsorSignature, sponsor)) : std::nullopt;
|
||||
std::optional<Sig> const sponsor2Sig =
|
||||
cosigning ? std::optional<Sig>(Sig(sfSponsorSignature, sponsor2)) : std::nullopt;
|
||||
|
||||
// create sponsor: charges the whole bytecode weight, not 1
|
||||
if (sponsorSig)
|
||||
{
|
||||
env(sponsor::transfer(alice, tfSponsorshipCreate, escrowKeylet.key),
|
||||
sponsor::As(sponsor, spfSponsorReserve),
|
||||
*sponsorSig);
|
||||
}
|
||||
else
|
||||
{
|
||||
env(sponsor::transfer(alice, tfSponsorshipCreate, escrowKeylet.key),
|
||||
sponsor::As(sponsor, spfSponsorReserve));
|
||||
}
|
||||
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == bytecodeWeight);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == bytecodeWeight);
|
||||
BEAST_EXPECT(env.le(escrowKeylet)->getAccountID(sfSponsor) == sponsor.id());
|
||||
|
||||
// transfer sponsor: moves the whole weight between the two sponsors
|
||||
if (sponsor2Sig)
|
||||
{
|
||||
env(sponsor::transfer(alice, tfSponsorshipReassign, escrowKeylet.key),
|
||||
sponsor::As(sponsor2, spfSponsorReserve),
|
||||
*sponsor2Sig);
|
||||
}
|
||||
else
|
||||
{
|
||||
env(sponsor::transfer(alice, tfSponsorshipReassign, escrowKeylet.key),
|
||||
sponsor::As(sponsor2, spfSponsorReserve));
|
||||
}
|
||||
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == bytecodeWeight);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == bytecodeWeight);
|
||||
BEAST_EXPECT(env.le(escrowKeylet)->getAccountID(sfSponsor) == sponsor2.id());
|
||||
|
||||
// end sponsor: hands the whole weight back to the owner
|
||||
env(sponsor::transfer(alice, tfSponsorshipEnd, escrowKeylet.key));
|
||||
|
||||
BEAST_EXPECT(ownerCount(env, alice) == bytecodeWeight);
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0);
|
||||
BEAST_EXPECT(!env.le(escrowKeylet)->isFieldPresent(sfSponsor));
|
||||
}
|
||||
|
||||
Account const gw("gw");
|
||||
auto const usd = gw["usd"];
|
||||
{
|
||||
@@ -4501,89 +4405,6 @@ public:
|
||||
BEAST_EXPECT(!sle->isFieldPresent(sfRemainingOwnerCount));
|
||||
}
|
||||
|
||||
// An escrow carrying Bytecode costs one owner count per 500 bytes, so
|
||||
// sponsoring one must move that full weight rather than a single unit.
|
||||
void
|
||||
testBytecodeEscrowReserve()
|
||||
{
|
||||
testcase("Bytecode escrow sponsorship reserve");
|
||||
using namespace test::jtx;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
auto const alice = Account{"alice"};
|
||||
auto const bob = Account{"bob"};
|
||||
auto const sponsor = Account{"sponsor"};
|
||||
|
||||
auto env = Env{*this, testableAmendments()};
|
||||
env.fund(XRP(1000000), alice, bob, sponsor);
|
||||
env.close();
|
||||
|
||||
auto const seq = env.seq(alice);
|
||||
env(escrow::create(alice, bob, XRP(100)),
|
||||
escrow::kCondition(escrow::kCb1),
|
||||
escrow::kCancelTime(env.now() + 100s));
|
||||
env.close();
|
||||
|
||||
auto const escrowKeylet = keylet::escrow(alice, SeqProxy::rawSequence(seq));
|
||||
auto const sponsorKeylet = keylet::sponsorship(sponsor.id(), alice.id());
|
||||
std::size_t const bytecodeSize = 1200;
|
||||
std::uint32_t const bytecodeWeight = 3; // ceil(1200 / 500)
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
||||
|
||||
// Pre-fund exactly the bytecode weight
|
||||
env(sponsor::set_reserve(sponsor, 0, bytecodeWeight), sponsor::SponseeAcc(alice));
|
||||
env.close();
|
||||
if (auto const sle = env.le(sponsorKeylet); BEAST_EXPECT(sle))
|
||||
{
|
||||
BEAST_EXPECT(sle->getFieldU32(sfRemainingOwnerCount) == bytecodeWeight);
|
||||
}
|
||||
|
||||
// Synthesize a bytecode escrow. EscrowCreate's preclaim runs the WASM
|
||||
// preflight, so a real 1200-byte contract cannot be assembled from
|
||||
// here; the Bytecode field is injected directly and the owner's
|
||||
// OwnerCount restored to the weight such a create would have charged.
|
||||
env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal) -> bool {
|
||||
auto const existing = view.read(escrowKeylet);
|
||||
if (!existing)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
auto const escrowSle = std::make_shared<SLE>(*existing);
|
||||
auto const account = std::make_shared<SLE>(*view.read(keylet::account(alice.id())));
|
||||
escrowSle->setFieldVL(sfBytecode, Blob(bytecodeSize, 0x00));
|
||||
account->setFieldU32(sfOwnerCount, bytecodeWeight);
|
||||
view.rawReplace(escrowSle);
|
||||
view.rawReplace(account);
|
||||
return true;
|
||||
});
|
||||
BEAST_EXPECT(ownerCount(env, alice) == bytecodeWeight);
|
||||
|
||||
// Create must charge the full bytecode weight (3), not 1: counting the
|
||||
// escrow as a single unit would leave 2 pre-funded units unspent.
|
||||
env(sponsor::transfer(alice, tfSponsorshipCreate, escrowKeylet.key),
|
||||
sponsor::As(sponsor, spfSponsorReserve));
|
||||
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == bytecodeWeight);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == bytecodeWeight);
|
||||
// All pre-funded units consumed (drained to absent).
|
||||
if (auto const sle = env.le(sponsorKeylet); BEAST_EXPECT(sle))
|
||||
{
|
||||
BEAST_EXPECT(!sle->isFieldPresent(sfRemainingOwnerCount));
|
||||
}
|
||||
|
||||
// End hands the same weight back; create bumped by the same amount, so
|
||||
// the counters return to 0 (counting 1 here would underflow).
|
||||
env(sponsor::transfer(alice, tfSponsorshipEnd, escrowKeylet.key));
|
||||
|
||||
BEAST_EXPECT(ownerCount(env, alice) == bytecodeWeight);
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
||||
if (auto const sle = env.le(escrowKeylet); BEAST_EXPECT(sle))
|
||||
{
|
||||
BEAST_EXPECT(!sle->isFieldPresent(sfSponsor));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testSponsoredTrustLineNoFreeReserve()
|
||||
{
|
||||
@@ -5969,7 +5790,6 @@ protected:
|
||||
testTransferSponsor(jtx::testableAmendments());
|
||||
testTransferSponsor(jtx::testableAmendments() - fixCleanup3_4_0);
|
||||
testLegacySignerListReserve();
|
||||
testBytecodeEscrowReserve();
|
||||
testSponsorFee();
|
||||
testSponsorAccount();
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -20,7 +20,6 @@
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
@@ -216,9 +215,8 @@ struct TransactionProposalCancel_test : public beast::unit_test::Suite
|
||||
}
|
||||
|
||||
// The target account may refuse any proposal made for it, at any point in
|
||||
// its lifecycle, without owning the object: any of its signers, an
|
||||
// authorized delegate, or that delegate's signers can create one and tie
|
||||
// up one of its tickets (XLS-0103 §7.2).
|
||||
// its lifecycle, without owning the object: anyone can create a proposal
|
||||
// against any account and tie up one of its tickets (On-Chain Cosigner spec §7.2).
|
||||
void
|
||||
testTargetCancel(FeatureBitset features)
|
||||
{
|
||||
@@ -685,25 +683,6 @@ struct TransactionProposalCancel_test : public beast::unit_test::Suite
|
||||
BEAST_EXPECT(env.le(keylet::txProposal(proposalID)));
|
||||
BEAST_EXPECT(ownerCount(env, alice) == proposal::kProposalOwnerCount);
|
||||
}
|
||||
|
||||
// Paying with a ticket changes none of that. The failed cancel still
|
||||
// consumes carol's ticket, and nothing else moves: the proposal and
|
||||
// its reserve stay as they were.
|
||||
{
|
||||
uint256 const proposalID = makeProposal(env, alice, target, bob);
|
||||
std::uint32_t const cancelTicket = proposal::createTicket(env, carol);
|
||||
auto const aliceOwnerCount = ownerCount(env, alice);
|
||||
|
||||
env(proposal::cancel(carol, proposalID),
|
||||
ticket::Use(cancelTicket),
|
||||
Ter(tecNO_PERMISSION));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.le(keylet::txProposal(proposalID)));
|
||||
BEAST_EXPECT(!env.le(keylet::ticket(carol.id(), SeqProxy::rawTicket(cancelTicket))));
|
||||
BEAST_EXPECT(ownerCount(env, alice) == aliceOwnerCount);
|
||||
BEAST_EXPECT(ownerCount(env, carol) == 0);
|
||||
}
|
||||
}
|
||||
|
||||
// A Cancel works as an inner Batch transaction, where each inner runs its
|
||||
@@ -1033,25 +1012,15 @@ struct TransactionProposalCancel_test : public beast::unit_test::Suite
|
||||
{
|
||||
json::Value noTicket;
|
||||
noTicket[jss::account] = target.human();
|
||||
auto const noTicketResult = ledgerEntry(noTicket);
|
||||
BEAST_EXPECT(noTicketResult[jss::error] == "malformedRequest");
|
||||
BEAST_EXPECT(noTicketResult[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(noTicketResult[jss::error_message] == "Missing field 'ticket_seq'.");
|
||||
BEAST_EXPECT(ledgerEntry(noTicket)[jss::error] == "malformedRequest");
|
||||
|
||||
json::Value noAccount;
|
||||
noAccount[jss::ticket_seq] = ticketSeq;
|
||||
auto const noAccountResult = ledgerEntry(noAccount);
|
||||
BEAST_EXPECT(noAccountResult[jss::error] == "malformedRequest");
|
||||
BEAST_EXPECT(noAccountResult[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(noAccountResult[jss::error_message] == "Missing field 'account'.");
|
||||
BEAST_EXPECT(ledgerEntry(noAccount)[jss::error] == "malformedRequest");
|
||||
|
||||
json::Value badAccount = byPair;
|
||||
badAccount[jss::account] = "not an account";
|
||||
auto const badAccountResult = ledgerEntry(badAccount);
|
||||
BEAST_EXPECT(badAccountResult[jss::error] == "malformedAddress");
|
||||
BEAST_EXPECT(badAccountResult[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
badAccountResult[jss::error_message] == "Invalid field 'account', not AccountID.");
|
||||
BEAST_EXPECT(ledgerEntry(badAccount)[jss::error] == "malformedAddress");
|
||||
}
|
||||
|
||||
// Listed for the proposer, who owns it, and not for the target.
|
||||
@@ -1068,8 +1037,6 @@ struct TransactionProposalCancel_test : public beast::unit_test::Suite
|
||||
{
|
||||
auto const jrr = ledgerEntry(byPair);
|
||||
BEAST_EXPECT(jrr[jss::error] == "entryNotFound");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcEntryNotFound);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Entry not found.");
|
||||
BEAST_EXPECT(!jrr.isMember(jss::node));
|
||||
BEAST_EXPECT(jrr[jss::index] == to_string(proposalID));
|
||||
}
|
||||
|
||||
@@ -1357,51 +1357,6 @@ struct TransactionProposalCreate_test : public beast::unit_test::Suite
|
||||
env.close();
|
||||
}
|
||||
|
||||
// A proposer may pay for the Create with one of its own Tickets. The
|
||||
// Ticket's reserve goes as the proposal's arrives, so the proposer's owner
|
||||
// count moves by the difference.
|
||||
void
|
||||
testTicketedCreate(FeatureBitset features)
|
||||
{
|
||||
testcase("create paid for with the proposer's own ticket");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
Env env{*this, features};
|
||||
|
||||
Account const alice{"alice"}; // the proposer
|
||||
Account const target{"target"}; // the account the proposal is for
|
||||
Account const bob{"bob"};
|
||||
env.fund(XRP(10000), alice, target, bob);
|
||||
env.close();
|
||||
proposal::authorizeProposer(env, target, alice);
|
||||
|
||||
std::uint32_t const targetTicketSeq = proposal::createTicket(env, target);
|
||||
std::uint32_t const aliceTicketSeq = proposal::createTicket(env, alice);
|
||||
std::uint32_t const aliceSeq = env.seq(alice);
|
||||
std::uint32_t const targetOwnerCount = ownerCount(env, target);
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
||||
|
||||
// The proposal's reserve lands on alice less the Ticket she paid with,
|
||||
// and her Sequence is untouched.
|
||||
env(proposal::create(
|
||||
alice,
|
||||
proposal::unsignedPayload(env, pay(target, bob, XRP(1)), targetTicketSeq),
|
||||
proposal::expiration(env, 100s)),
|
||||
ticket::Use(aliceTicketSeq),
|
||||
proposal::verify::create());
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(proposal::entry(env, target, targetTicketSeq));
|
||||
BEAST_EXPECT(!env.le(keylet::ticket(alice.id(), SeqProxy::rawTicket(aliceTicketSeq))));
|
||||
BEAST_EXPECT(env.seq(alice) == aliceSeq);
|
||||
BEAST_EXPECT(ownerCount(env, alice) == proposal::kProposalOwnerCount);
|
||||
|
||||
// The target's Ticket, the one the proposal is keyed to, stays.
|
||||
BEAST_EXPECT(ownerCount(env, target) == targetOwnerCount);
|
||||
}
|
||||
|
||||
// The proposal's reserve can instead be sponsored: the reserve is charged
|
||||
// to the sponsor's account, and the ledger object records the sponsor, the
|
||||
// same as any other reserve-sponsorable object (TransactionProposalCreate
|
||||
@@ -1773,7 +1728,6 @@ struct TransactionProposalCreate_test : public beast::unit_test::Suite
|
||||
testOtherTransactionTypes(all);
|
||||
testAuxiliaryCoSignatureTypes(all);
|
||||
testReserve(all);
|
||||
testTicketedCreate(all);
|
||||
testSponsoredReserve(all);
|
||||
testSponsorshipTransfer(all);
|
||||
testFeeSponsored(all);
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
#include <xrpl/basics/strHex.h>
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
@@ -67,8 +66,6 @@ class TrustAndBalance_test : public beast::unit_test::Suite
|
||||
// reflects this
|
||||
auto jrr = ledgerEntryState(env, gw, alice, "USD");
|
||||
BEAST_EXPECT(jrr[jss::error] == "entryNotFound");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcEntryNotFound);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Entry not found.");
|
||||
|
||||
// now create a credit limit
|
||||
env(trust(alice, gw["USD"](800)));
|
||||
@@ -103,8 +100,6 @@ class TrustAndBalance_test : public beast::unit_test::Suite
|
||||
// ensure line is deleted
|
||||
jrr = ledgerEntryState(env, gw, alice, "USD");
|
||||
BEAST_EXPECT(jrr[jss::error] == "entryNotFound");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcEntryNotFound);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Entry not found.");
|
||||
|
||||
// TODO Check in both owner books.
|
||||
|
||||
|
||||
@@ -3231,9 +3231,7 @@ public:
|
||||
|
||||
{
|
||||
auto const info = env.rpc("json", "account_info", to_string(prevLedgerWithQueue));
|
||||
BEAST_EXPECT(info[jss::result][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(info[jss::result][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(info[jss::result][jss::error_message] == "Invalid parameters.");
|
||||
BEAST_EXPECT(info.isMember(jss::result) && rpc::containsError(info[jss::result]));
|
||||
}
|
||||
|
||||
env.close();
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/escrow.h>
|
||||
#include <test/jtx/pay.h>
|
||||
#include <test/jtx/token.h>
|
||||
#include <test/jtx/trust.h>
|
||||
@@ -1331,51 +1330,6 @@ class InvariantsMisc_test : public InvariantsBase
|
||||
env(check::create(a1, a2, XRP(1)));
|
||||
return true;
|
||||
});
|
||||
|
||||
// An escrow's owner count scales with its Bytecode size, so
|
||||
// sponsoring one has to move that whole weight. Counting it as a
|
||||
// single unit leaves the sponsored counters short of the object's
|
||||
// real weight, which is what this check exists to catch.
|
||||
UInt256 escrowID;
|
||||
|
||||
doInvariantCheck(
|
||||
{{expectMessage}},
|
||||
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
|
||||
auto const escrow = ac.view().peek(keylet::unchecked(escrowID));
|
||||
if (!escrow)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// ceil(1200 / 500) == 3 owner counts
|
||||
escrow->setFieldVL(sfBytecode, Blob(1200, 0x00));
|
||||
escrow->setAccountID(sfSponsor, a2.id());
|
||||
ac.view().update(escrow);
|
||||
|
||||
// Move the counters by a single unit, as a sponsorship
|
||||
// blind to the Bytecode weight would.
|
||||
auto const sle1 = ac.view().peek(keylet::account(a1.id()));
|
||||
auto const sle2 = ac.view().peek(keylet::account(a2.id()));
|
||||
if (!sle1 || !sle2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
sle1->setFieldU32(sfSponsoredOwnerCount, 1);
|
||||
sle2->setFieldU32(sfSponsoringOwnerCount, 1);
|
||||
ac.view().update(sle1);
|
||||
ac.view().update(sle2);
|
||||
return true;
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttACCOUNT_SET, [](STObject&) {}},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
[&escrowID](Account const& a1, Account const& a2, Env& env) {
|
||||
using namespace std::chrono_literals;
|
||||
escrowID = keylet::escrow(a1.id(), SeqProxy::rawSequence(env.seq(a1))).key;
|
||||
env(escrow::create(a1, a2, XRP(1)),
|
||||
escrow::kCondition(escrow::kCb1),
|
||||
escrow::kCancelTime(env.now() + 100s));
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -1,549 +0,0 @@
|
||||
#include <test/app/invariants/InvariantsBase.h>
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/pay.h>
|
||||
#include <test/jtx/proposal.h>
|
||||
#include <test/jtx/utility.h>
|
||||
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/helpers/ProposalHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STArray.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/SeqProxy.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/ApplyContext.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <initializer_list>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <source_location>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
class InvariantsTransactionProposal_test : public InvariantsBase
|
||||
{
|
||||
void
|
||||
testTransactionProposalInvariants()
|
||||
{
|
||||
using namespace jtx;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
// Submits a real proposal, so the entry is in the closed ledger before
|
||||
// the precheck runs and the precheck can modify it.
|
||||
std::optional<Keylet> liveProposalKeylet;
|
||||
std::optional<std::uint32_t> liveTicketSeq;
|
||||
Preclose const precloseLiveProposal =
|
||||
[&](Account const& owner, Account const& destination, Env& env) {
|
||||
auto const ticketSeq = proposal::createTicket(env, owner);
|
||||
auto const proposedTx =
|
||||
proposal::unsignedPayload(env, pay(owner, destination, XRP(1)), ticketSeq);
|
||||
liveProposalKeylet = keylet::txProposal(owner.id(), ticketSeq);
|
||||
liveTicketSeq = ticketSeq;
|
||||
env(proposal::create(owner, proposedTx, proposal::expiration(env, 100s)));
|
||||
return BEAST_EXPECT(env.le(*liveProposalKeylet));
|
||||
};
|
||||
|
||||
auto peekLiveProposal = [&](ApplyContext& ac) -> SLE::pointer {
|
||||
if (!liveProposalKeylet)
|
||||
return nullptr;
|
||||
return ac.view().peek(*liveProposalKeylet);
|
||||
};
|
||||
|
||||
// Deletes the live proposal as deleteProposal would: out of the
|
||||
// owner's directory, with the owner's reserve released.
|
||||
auto deleteLiveProposal = [&](Account const& owner, ApplyContext& ac) {
|
||||
auto const sle = peekLiveProposal(ac);
|
||||
if (!sle)
|
||||
return false;
|
||||
auto const account = ac.view().peek(keylet::account(owner.id()));
|
||||
if (!account)
|
||||
return false;
|
||||
account->at(sfOwnerCount) -=
|
||||
xrpl::proposal::proposalOwnerCount(sle->getFieldObject(sfProposedTransaction));
|
||||
ac.view().update(account);
|
||||
if (!ac.view().dirRemove(
|
||||
keylet::ownerDir(owner.id()), sle->getFieldU64(sfOwnerNode), sle->key(), false))
|
||||
return false;
|
||||
ac.view().erase(sle);
|
||||
return true;
|
||||
};
|
||||
|
||||
// Consumes one of the owner's Tickets as ticketDelete would.
|
||||
auto deleteTicket = [&](Account const& owner, std::uint32_t ticketSeq, ApplyContext& ac) {
|
||||
auto const ticket =
|
||||
ac.view().peek(keylet::ticket(owner.id(), SeqProxy::rawTicket(ticketSeq)));
|
||||
auto const account = ac.view().peek(keylet::account(owner.id()));
|
||||
if (!ticket || !account)
|
||||
return false;
|
||||
if (!ac.view().dirRemove(
|
||||
keylet::ownerDir(owner.id()),
|
||||
ticket->getFieldU64(sfOwnerNode),
|
||||
ticket->key(),
|
||||
false))
|
||||
return false;
|
||||
if (auto const ticketCount = account->getFieldU32(sfTicketCount); ticketCount > 1)
|
||||
{
|
||||
account->setFieldU32(sfTicketCount, ticketCount - 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
account->makeFieldAbsent(sfTicketCount);
|
||||
}
|
||||
account->at(sfOwnerCount) -= 1;
|
||||
ac.view().update(account);
|
||||
ac.view().erase(ticket);
|
||||
return true;
|
||||
};
|
||||
|
||||
// A transaction outside the whitelist may not modify a proposal at
|
||||
// all; the invariant rejects it on the count, whatever field changed.
|
||||
testcase("TransactionProposal modified outside the whitelist");
|
||||
doInvariantCheck(
|
||||
{{"TransactionProposal changes do not match transaction result"}},
|
||||
[&](Account const&, Account const&, ApplyContext& ac) {
|
||||
auto const sle = peekLiveProposal(ac);
|
||||
if (!sle)
|
||||
return false;
|
||||
auto proposedTx = sle->getFieldObject(sfProposedTransaction);
|
||||
proposedTx.setFieldU32(
|
||||
sfTicketSequence, proposedTx.getFieldU32(sfTicketSequence) + 1);
|
||||
sle->setFieldObject(sfProposedTransaction, proposedTx);
|
||||
ac.view().update(sle);
|
||||
return true;
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttACCOUNT_SET, [](STObject&) {}},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
precloseLiveProposal);
|
||||
|
||||
// Only stages the payload and the key it would live under; nothing
|
||||
// reaches the ledger, so a precheck that inserts the entry by hand
|
||||
// presents the invariant with a creation.
|
||||
std::optional<STObject> stagedPayload;
|
||||
std::optional<Keylet> stagedProposalKeylet;
|
||||
Preclose const precloseStagedPayload =
|
||||
[&](Account const& owner, Account const& destination, Env& env) {
|
||||
auto const ticketSeq = proposal::createTicket(env, owner);
|
||||
auto const proposedJson =
|
||||
proposal::unsignedPayload(env, pay(owner, destination, XRP(1)), ticketSeq);
|
||||
stagedPayload.emplace(parse(proposedJson));
|
||||
stagedProposalKeylet = keylet::txProposal(owner.id(), ticketSeq);
|
||||
return true;
|
||||
};
|
||||
|
||||
// Inserts the staged proposal as doApply would, with the owner's
|
||||
// reserve charged.
|
||||
auto insertStagedProposal = [&](Account const& owner, ApplyContext& ac) {
|
||||
if (!stagedPayload || !stagedProposalKeylet)
|
||||
return false;
|
||||
|
||||
auto const sle = std::make_shared<SLE>(*stagedProposalKeylet);
|
||||
sle->setAccountID(sfOwner, owner.id());
|
||||
sle->setFieldObject(sfProposedTransaction, *stagedPayload);
|
||||
sle->setFieldU32(sfExpiration, 1);
|
||||
sle->setFieldU64(sfOwnerNode, 0);
|
||||
ac.view().insert(sle);
|
||||
|
||||
auto const account = ac.view().peek(keylet::account(owner.id()));
|
||||
if (!account)
|
||||
return false;
|
||||
account->at(sfOwnerCount) += xrpl::proposal::proposalOwnerCount(*stagedPayload);
|
||||
ac.view().update(account);
|
||||
return true;
|
||||
};
|
||||
|
||||
testcase("TransactionProposal effect whitelist");
|
||||
doInvariantCheck(
|
||||
{{"TransactionProposal changes do not match transaction result"}},
|
||||
[&](Account const& owner, Account const&, ApplyContext& ac) {
|
||||
return insertStagedProposal(owner, ac);
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttACCOUNT_SET, [](STObject&) {}},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
precloseStagedPayload);
|
||||
|
||||
// The harness runs no transactor, so a failed result has to be fed in
|
||||
// directly. A failed create must leave no trace.
|
||||
testcase("TransactionProposal failed create still inserts");
|
||||
doInvariantCheck(
|
||||
{{"TransactionProposal changes do not match transaction result"}},
|
||||
[&](Account const& owner, Account const&, ApplyContext& ac) {
|
||||
return insertStagedProposal(owner, ac);
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttTRANSACTION_PROPOSAL_CREATE, [](STObject&) {}},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
precloseStagedPayload,
|
||||
TxAccount::None,
|
||||
std::source_location::current(),
|
||||
tecNO_PERMISSION);
|
||||
|
||||
// Reassigns the live proposal's reserve to a sponsor as a
|
||||
// SponsorshipTransfer would. The sponsored and sponsoring counts move
|
||||
// with it so that SponsorshipOwnerCountsMatch stays quiet.
|
||||
auto const reassignLiveProposal =
|
||||
[&](Account const& owner, Account const& sponsor, ApplyContext& ac) {
|
||||
auto const sle = peekLiveProposal(ac);
|
||||
if (!sle)
|
||||
return false;
|
||||
auto const reserve =
|
||||
xrpl::proposal::proposalOwnerCount(sle->getFieldObject(sfProposedTransaction));
|
||||
sle->setAccountID(sfSponsor, sponsor.id());
|
||||
ac.view().update(sle);
|
||||
|
||||
auto const account = ac.view().peek(keylet::account(owner.id()));
|
||||
auto const sponsorRoot = ac.view().peek(keylet::account(sponsor.id()));
|
||||
if (!account || !sponsorRoot)
|
||||
return false;
|
||||
account->at(sfSponsoredOwnerCount) += reserve;
|
||||
ac.view().update(account);
|
||||
sponsorRoot->at(sfSponsoringOwnerCount) += reserve;
|
||||
ac.view().update(sponsorRoot);
|
||||
return true;
|
||||
};
|
||||
|
||||
// A failed transfer must not have reassigned anything. The counters are
|
||||
// kept consistent so only the result mismatch is left to catch.
|
||||
testcase("TransactionProposal failed sponsorship transfer still reassigns");
|
||||
doInvariantCheck(
|
||||
{{"TransactionProposal changes do not match transaction result"}},
|
||||
reassignLiveProposal,
|
||||
XRPAmount{},
|
||||
STTx{ttSPONSORSHIP_TRANSFER, [](STObject&) {}},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
precloseLiveProposal,
|
||||
TxAccount::None,
|
||||
std::source_location::current(),
|
||||
tecNO_PERMISSION);
|
||||
|
||||
// A proposal leaves the ledger only through TransactionProposalCancel
|
||||
// or together with the Ticket it is keyed to. The harness runs no
|
||||
// transactor, so each deletion below is staged by hand, releasing the
|
||||
// reserve as a real one would, to leave the deletion itself as the
|
||||
// only possible violation.
|
||||
testcase("TransactionProposal deletion outside the whitelist");
|
||||
doInvariantCheck(
|
||||
{{"TransactionProposal changes do not match transaction result"}},
|
||||
[&](Account const& owner, Account const&, ApplyContext& ac) {
|
||||
return deleteLiveProposal(owner, ac);
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttACCOUNT_SET, [](STObject&) {}},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
precloseLiveProposal);
|
||||
|
||||
// The owner holds a second Ticket, one no proposal is keyed to.
|
||||
std::optional<std::uint32_t> spareTicketSeq;
|
||||
Preclose const precloseLiveProposalAndSpareTicket =
|
||||
[&](Account const& owner, Account const& destination, Env& env) {
|
||||
spareTicketSeq = proposal::createTicket(env, owner);
|
||||
return precloseLiveProposal(owner, destination, env);
|
||||
};
|
||||
|
||||
// Consuming some other Ticket of the owner's is no reason for the
|
||||
// proposal to go.
|
||||
testcase("TransactionProposal deleted with an unrelated ticket");
|
||||
doInvariantCheck(
|
||||
{{"TransactionProposal changes do not match transaction result"}},
|
||||
[&](Account const& owner, Account const&, ApplyContext& ac) {
|
||||
return spareTicketSeq && deleteTicket(owner, *spareTicketSeq, ac) &&
|
||||
deleteLiveProposal(owner, ac);
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttACCOUNT_SET, [](STObject&) {}},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
precloseLiveProposalAndSpareTicket);
|
||||
|
||||
// And a deleted Ticket takes the proposal keyed to it along: here the
|
||||
// Ticket went and the proposal stayed behind.
|
||||
testcase("TransactionProposal outlives its ticket");
|
||||
doInvariantCheck(
|
||||
{{"TransactionProposal outlived the Ticket it is keyed to"}},
|
||||
[&](Account const& owner, Account const&, ApplyContext& ac) {
|
||||
return liveTicketSeq && deleteTicket(owner, *liveTicketSeq, ac);
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttACCOUNT_SET, [](STObject&) {}},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
precloseLiveProposal);
|
||||
|
||||
// A Cancel names the live proposal and may pay with the owner's spare
|
||||
// Ticket. Both exist only once the preclose has run, so the Env is
|
||||
// built here rather than by the harness.
|
||||
struct CancelOptions
|
||||
{
|
||||
// The Cancel pays with A1's spare Ticket.
|
||||
bool paySpareTicket = false;
|
||||
TER initialResult = tesSUCCESS;
|
||||
};
|
||||
auto const checkCancel = [&](std::vector<std::string> const& expectLogs,
|
||||
Precheck const& precheck,
|
||||
std::initializer_list<TER> ters,
|
||||
Preclose const& preclose,
|
||||
CancelOptions const& options = CancelOptions(),
|
||||
std::source_location const& loc =
|
||||
std::source_location::current()) {
|
||||
Account const a1{"A1"};
|
||||
Account const a2{"A2"};
|
||||
Env env = makeEnv(testableAmendments());
|
||||
env.fund(XRP(1000), a1, a2);
|
||||
if (!BEAST_EXPECT(preclose(a1, a2, env)) || !BEAST_EXPECT(liveProposalKeylet))
|
||||
return;
|
||||
env.close();
|
||||
|
||||
STTx const tx{ttTRANSACTION_PROPOSAL_CANCEL, [&](STObject& obj) {
|
||||
obj.setAccountID(sfAccount, a1.id());
|
||||
obj.setFieldH256(sfProposalID, liveProposalKeylet->key);
|
||||
if (options.paySpareTicket && spareTicketSeq)
|
||||
{
|
||||
obj.setFieldU32(sfSequence, 0);
|
||||
obj.setFieldU32(sfTicketSequence, *spareTicketSeq);
|
||||
}
|
||||
}};
|
||||
doInvariantCheck(
|
||||
std::move(env),
|
||||
a1,
|
||||
a2,
|
||||
expectLogs,
|
||||
precheck,
|
||||
XRPAmount{},
|
||||
tx,
|
||||
ters,
|
||||
loc,
|
||||
options.initialResult);
|
||||
};
|
||||
|
||||
auto const cancelDeletesLiveProposal =
|
||||
[&](Account const& owner, Account const&, ApplyContext& ac) {
|
||||
return deleteLiveProposal(owner, ac);
|
||||
};
|
||||
|
||||
// A Cancel that did not succeed deleted nothing.
|
||||
testcase("TransactionProposal failed cancel still deletes");
|
||||
{
|
||||
CancelOptions options;
|
||||
options.initialResult = tecNO_PERMISSION;
|
||||
checkCancel(
|
||||
{{"TransactionProposal changes do not match transaction result"}},
|
||||
cancelDeletesLiveProposal,
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
precloseLiveProposal,
|
||||
options);
|
||||
}
|
||||
|
||||
// A Cancel cannot spend a reserved Ticket, so the proposal it deletes
|
||||
// never goes with its Ticket.
|
||||
testcase("TransactionProposal cancel consumes the reserved ticket");
|
||||
checkCancel(
|
||||
{{"TransactionProposal changes do not match transaction result"}},
|
||||
[&](Account const& owner, Account const&, ApplyContext& ac) {
|
||||
return liveTicketSeq && deleteTicket(owner, *liveTicketSeq, ac) &&
|
||||
deleteLiveProposal(owner, ac);
|
||||
},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
precloseLiveProposal);
|
||||
|
||||
// Distinct AccountIDs in ascending order, so an array built from them
|
||||
// violates only the bound on its length.
|
||||
auto ascendingAccounts = [](std::size_t count) {
|
||||
std::vector<AccountID> accounts;
|
||||
accounts.reserve(count);
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
{
|
||||
AccountID account{beast::kZero};
|
||||
account.data()[0] = static_cast<std::uint8_t>(i + 1);
|
||||
accounts.push_back(account);
|
||||
}
|
||||
return accounts;
|
||||
};
|
||||
|
||||
auto signerArray = [](std::vector<AccountID> const& accounts) {
|
||||
STArray signers{sfSigners};
|
||||
for (auto const& account : accounts)
|
||||
{
|
||||
STObject signer{sfSigner};
|
||||
signer.setAccountID(sfAccount, account);
|
||||
signers.pushBack(signer);
|
||||
}
|
||||
return signers;
|
||||
};
|
||||
|
||||
// No transaction type on this amendment branch collects signatures yet,
|
||||
// so every rejection below has to be provoked by editing the stored
|
||||
// payload directly.
|
||||
auto const testBrokenSignerArray = [&](std::string const& name, auto const& breakPayload) {
|
||||
testcase("TransactionProposal canonical signer arrays: " + name);
|
||||
doInvariantCheck(
|
||||
{{"TransactionProposal signer arrays are not canonical"}},
|
||||
[&](Account const&, Account const& signerAccount, ApplyContext& ac) {
|
||||
auto const sle = peekLiveProposal(ac);
|
||||
if (!sle)
|
||||
return false;
|
||||
auto proposedTx = sle->getFieldObject(sfProposedTransaction);
|
||||
breakPayload(proposedTx, signerAccount);
|
||||
sle->setFieldObject(sfProposedTransaction, proposedTx);
|
||||
ac.view().update(sle);
|
||||
return true;
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttACCOUNT_SET, [](STObject&) {}},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
precloseLiveProposal);
|
||||
};
|
||||
|
||||
testBrokenSignerArray("duplicate entries", [&](STObject& tx, Account const& signer) {
|
||||
tx.setFieldArray(sfSigners, signerArray({signer.id(), signer.id()}));
|
||||
});
|
||||
|
||||
testBrokenSignerArray("descending order", [&](STObject& tx, Account const&) {
|
||||
auto accounts = ascendingAccounts(2);
|
||||
std::ranges::reverse(accounts);
|
||||
tx.setFieldArray(sfSigners, signerArray(accounts));
|
||||
});
|
||||
|
||||
testBrokenSignerArray("past the multi-sign limit", [&](STObject& tx, Account const&) {
|
||||
tx.setFieldArray(sfSigners, signerArray(ascendingAccounts(STTx::kMaxMultiSigners + 1)));
|
||||
});
|
||||
|
||||
testBrokenSignerArray("entry without an Account", [](STObject& tx, Account const&) {
|
||||
STArray signers{sfSigners};
|
||||
signers.pushBack(STObject{sfSigner});
|
||||
tx.setFieldArray(sfSigners, signers);
|
||||
});
|
||||
|
||||
for (auto const* field :
|
||||
std::array<SField const*, 2>{&sfCounterpartySignature, &sfSponsorSignature})
|
||||
{
|
||||
testBrokenSignerArray(
|
||||
"duplicates under " + field->getName(), [&](STObject& tx, Account const& signer) {
|
||||
STObject signature{*field};
|
||||
signature.setFieldArray(sfSigners, signerArray({signer.id(), signer.id()}));
|
||||
tx.setFieldObject(*field, signature);
|
||||
});
|
||||
}
|
||||
|
||||
testBrokenSignerArray("past the batch-signer limit", [&](STObject& tx, Account const&) {
|
||||
STArray batchSigners{sfBatchSigners};
|
||||
for (auto const& account : ascendingAccounts(kMaxBatchSigners + 1))
|
||||
{
|
||||
STObject batchSigner{sfBatchSigner};
|
||||
batchSigner.setAccountID(sfAccount, account);
|
||||
batchSigners.pushBack(batchSigner);
|
||||
}
|
||||
tx.setFieldArray(sfBatchSigners, batchSigners);
|
||||
});
|
||||
|
||||
testBrokenSignerArray(
|
||||
"duplicates under a BatchSigner", [&](STObject& tx, Account const& signer) {
|
||||
STObject batchSigner{sfBatchSigner};
|
||||
batchSigner.setAccountID(sfAccount, signer.id());
|
||||
batchSigner.setFieldArray(sfSigners, signerArray({signer.id(), signer.id()}));
|
||||
|
||||
STArray batchSigners{sfBatchSigners};
|
||||
batchSigners.pushBack(batchSigner);
|
||||
tx.setFieldArray(sfBatchSigners, batchSigners);
|
||||
});
|
||||
|
||||
// Negative controls. These share the setup of the failure cases above,
|
||||
// so they confirm each of those failures came from the defect it names
|
||||
// rather than from the hand-built ledger state.
|
||||
std::initializer_list<TER> const passTers = {tesSUCCESS, tesSUCCESS};
|
||||
|
||||
testcase("TransactionProposal well-formed create");
|
||||
doInvariantCheck(
|
||||
{},
|
||||
[&](Account const& owner, Account const&, ApplyContext& ac) {
|
||||
return insertStagedProposal(owner, ac);
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttTRANSACTION_PROPOSAL_CREATE, [](STObject&) {}},
|
||||
passTers,
|
||||
precloseStagedPayload);
|
||||
|
||||
testcase("TransactionProposal left untouched");
|
||||
doInvariantCheck(
|
||||
{},
|
||||
[&](Account const&, Account const&, ApplyContext& ac) {
|
||||
return liveProposalKeylet && ac.view().read(*liveProposalKeylet) != nullptr;
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttACCOUNT_SET, [](STObject&) {}},
|
||||
passTers,
|
||||
precloseLiveProposal);
|
||||
|
||||
// A Cancel deletes the proposal it names.
|
||||
testcase("TransactionProposal cancelled");
|
||||
checkCancel({}, cancelDeletesLiveProposal, passTers, precloseLiveProposal);
|
||||
|
||||
// The Ticket a Cancel pays with is not the one its proposal is keyed
|
||||
// to, so the proposal did not go with its Ticket.
|
||||
testcase("TransactionProposal cancel paid with the canceller's own ticket");
|
||||
{
|
||||
CancelOptions options;
|
||||
options.paySpareTicket = true;
|
||||
checkCancel(
|
||||
{},
|
||||
[&](Account const& owner, Account const&, ApplyContext& ac) {
|
||||
return spareTicketSeq && deleteTicket(owner, *spareTicketSeq, ac) &&
|
||||
deleteLiveProposal(owner, ac);
|
||||
},
|
||||
passTers,
|
||||
precloseLiveProposalAndSpareTicket,
|
||||
options);
|
||||
}
|
||||
|
||||
// A SponsorshipTransfer reassigns the Sponsor.
|
||||
testcase("TransactionProposal sponsor reassigned");
|
||||
doInvariantCheck(
|
||||
{},
|
||||
reassignLiveProposal,
|
||||
XRPAmount{},
|
||||
STTx{ttSPONSORSHIP_TRANSFER, [](STObject&) {}},
|
||||
passTers,
|
||||
precloseLiveProposal);
|
||||
|
||||
// The proposed transaction executing spends the proposal's Ticket and
|
||||
// takes the proposal with it.
|
||||
testcase("TransactionProposal deleted with its ticket");
|
||||
doInvariantCheck(
|
||||
{},
|
||||
[&](Account const& owner, Account const&, ApplyContext& ac) {
|
||||
return liveTicketSeq && deleteTicket(owner, *liveTicketSeq, ac) &&
|
||||
deleteLiveProposal(owner, ac);
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttACCOUNT_SET, [](STObject&) {}},
|
||||
passTers,
|
||||
precloseLiveProposal);
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testTransactionProposalInvariants();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(InvariantsTransactionProposal, app, xrpl);
|
||||
|
||||
} // namespace xrpl::test
|
||||
@@ -1154,8 +1154,6 @@ class LoanBroker_test : public beast::unit_test::Suite
|
||||
auto const jrr = env.rpc("submit", strHex(s.slice()))[jss::result];
|
||||
// fails in doSubmit() on STTx construction
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidTransaction");
|
||||
BEAST_EXPECT(!jrr.isMember(jss::error_code));
|
||||
BEAST_EXPECT(jrr[jss::error_message].isNull());
|
||||
BEAST_EXPECT(jrr[jss::error_exception] == "invalid native account");
|
||||
}
|
||||
|
||||
|
||||
@@ -190,8 +190,6 @@ private:
|
||||
BEAST_EXPECT(jr.isMember(jss::result));
|
||||
auto const jResult = jr[jss::result];
|
||||
BEAST_EXPECT(jResult[jss::error] == "invalidTransaction");
|
||||
BEAST_EXPECT(!jResult.isMember(jss::error_code));
|
||||
BEAST_EXPECT(jResult[jss::error_message].isNull());
|
||||
BEAST_EXPECT(
|
||||
jResult[jss::error_exception] ==
|
||||
"fails local checks: Transaction has bad signature.");
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
#include <xrpl/beast/xor_shift_engine.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/KeyType.h>
|
||||
@@ -127,9 +126,12 @@ private:
|
||||
return params;
|
||||
}();
|
||||
auto const jSignBorrower = env.rpc("json", "sign", to_string(borrowerSignParams));
|
||||
BEAST_EXPECT(jSignBorrower[jss::result][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jSignBorrower[jss::result][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jSignBorrower[jss::result][jss::error_message] == target);
|
||||
BEAST_EXPECT(
|
||||
jSignBorrower.isMember(jss::result) &&
|
||||
jSignBorrower[jss::result].isMember(jss::error) &&
|
||||
jSignBorrower[jss::result][jss::error] == "invalidParams" &&
|
||||
jSignBorrower[jss::result].isMember(jss::error_message) &&
|
||||
jSignBorrower[jss::result][jss::error_message] == target);
|
||||
}
|
||||
}
|
||||
{
|
||||
|
||||
@@ -196,8 +196,7 @@ private:
|
||||
jvResult[jss::tx_blob] = strHex(local.getSerializer().slice());
|
||||
auto res = env.rpc("json", "submit", to_string(jvResult))["result"];
|
||||
BEAST_EXPECT(
|
||||
res[jss::error] == "invalidTransaction" && !res.isMember(jss::error_code) &&
|
||||
res[jss::error_message].isNull() &&
|
||||
res[jss::error] == "invalidTransaction" &&
|
||||
res[jss::error_exception] ==
|
||||
"fails local checks: Counterparty: Invalid signature.");
|
||||
});
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
#include <test/jtx/ter.h>
|
||||
#include <test/jtx/ticket.h>
|
||||
#include <test/jtx/trust.h>
|
||||
#include <test/jtx/txflags.h>
|
||||
#include <test/jtx/vault.h>
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
@@ -1610,263 +1609,26 @@ private:
|
||||
env.close();
|
||||
|
||||
// Direct vault share payment inherits the underlying lock via
|
||||
// sfReferenceHolding. Caught in the strand check, like a global
|
||||
// or individual share lock.
|
||||
// sfReferenceHolding.
|
||||
BEAST_EXPECT(shareBalance(alice) == 499);
|
||||
BEAST_EXPECT(shareBalance(bob) == 501);
|
||||
env(pay(alice, bob, shares(1)), Ter{tecPATH_DRY});
|
||||
env(pay(alice, bob, shares(1)), Ter{tecLOCKED});
|
||||
env.close();
|
||||
BEAST_EXPECT(shareBalance(alice) == 499);
|
||||
BEAST_EXPECT(shareBalance(bob) == 501);
|
||||
|
||||
// The same inherited lock must also block DEX payment paths that
|
||||
// would consume an offer selling vault shares. bob, the
|
||||
// destination, inherits the lock and is caught in the strand
|
||||
// check.
|
||||
// would consume an offer selling vault shares.
|
||||
env(pay(carol, bob, shares(1)),
|
||||
Sendmax(XRP(1)),
|
||||
Path(BookSpec{shares.raw()}),
|
||||
Ter{tecPATH_DRY});
|
||||
Ter{tecPATH_PARTIAL});
|
||||
env.close();
|
||||
BEAST_EXPECT(shareBalance(alice) == 499);
|
||||
BEAST_EXPECT(shareBalance(bob) == 501);
|
||||
BEAST_EXPECT(expectOffers(env, alice, 1));
|
||||
}
|
||||
|
||||
// A holder individually frozen on the underlying can neither sell
|
||||
// nor buy vault shares through an unfrozen holder's offer. The
|
||||
// inherited freeze is caught when the strand is built, before the
|
||||
// offer is consumed.
|
||||
auto const testShareDEXEndpointFrozen =
|
||||
[&](Env& env,
|
||||
PrettyAsset const& asset,
|
||||
Account const& owner,
|
||||
Account const& alice,
|
||||
Account const& bob,
|
||||
Account const& carol,
|
||||
std::function<void(bool)> const& setAliceFrozen) {
|
||||
Vault const vault{env};
|
||||
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = asset(500)}));
|
||||
env(vault.deposit({.depositor = bob, .id = keylet.key, .amount = asset(500)}));
|
||||
env.close();
|
||||
|
||||
PrettyAsset const shares = MPTIssue(env.le(keylet)->at(sfShareMPTID));
|
||||
auto const shareMptID = shares.raw().get<MPTIssue>().getMptID();
|
||||
auto const shareBalance = [&](Account const& account) {
|
||||
auto const sle = env.le(keylet::mptoken(shareMptID, account));
|
||||
return sle ? sle->at(sfMPTAmount) : 0;
|
||||
};
|
||||
|
||||
// bob bids XRP for shares and asks more XRP for shares, so that
|
||||
// his offers don't cross
|
||||
env(offer(bob, shares(1), XRP(1)));
|
||||
env(offer(bob, XRP(2), shares(1)));
|
||||
env.close();
|
||||
|
||||
setAliceFrozen(true);
|
||||
|
||||
auto const aliceShares = shareBalance(alice);
|
||||
auto const bobShares = shareBalance(bob);
|
||||
auto const expectUnchanged = [&] {
|
||||
BEAST_EXPECT(shareBalance(alice) == aliceShares);
|
||||
BEAST_EXPECT(shareBalance(bob) == bobShares);
|
||||
BEAST_EXPECT(expectOffers(env, bob, 2));
|
||||
};
|
||||
|
||||
// alice sells shares: the first step's source is frozen
|
||||
env(pay(alice, carol, XRP(1)),
|
||||
Sendmax(shares(1)),
|
||||
Path(~XRP),
|
||||
Txflags(tfNoRippleDirect),
|
||||
Ter{tecPATH_DRY});
|
||||
env.close();
|
||||
expectUnchanged();
|
||||
|
||||
// carol buys shares for alice: the last step's destination is
|
||||
// frozen
|
||||
env(pay(carol, alice, shares(1)),
|
||||
Sendmax(XRP(2)),
|
||||
Path(BookSpec{shares.raw()}),
|
||||
Txflags(tfNoRippleDirect),
|
||||
Ter{tecPATH_DRY});
|
||||
env.close();
|
||||
expectUnchanged();
|
||||
|
||||
// Both succeed once alice is unfrozen
|
||||
setAliceFrozen(false);
|
||||
|
||||
env(pay(alice, carol, XRP(1)),
|
||||
Sendmax(shares(1)),
|
||||
Path(~XRP),
|
||||
Txflags(tfNoRippleDirect));
|
||||
env.close();
|
||||
BEAST_EXPECT(shareBalance(bob) == bobShares + 1);
|
||||
|
||||
env(pay(carol, alice, shares(1)),
|
||||
Sendmax(XRP(2)),
|
||||
Path(BookSpec{shares.raw()}),
|
||||
Txflags(tfNoRippleDirect));
|
||||
env.close();
|
||||
BEAST_EXPECT(shareBalance(alice) == aliceShares);
|
||||
BEAST_EXPECT(shareBalance(bob) == bobShares);
|
||||
BEAST_EXPECT(expectOffers(env, bob, 0));
|
||||
};
|
||||
|
||||
// Vault shares as the intermediate asset between two books. Only the
|
||||
// offer owners hold the shares in transit. An owner individually
|
||||
// frozen on the underlying can neither give nor receive the shares.
|
||||
auto const testShareDEXMiddleFrozen = [&](Env& env,
|
||||
PrettyAsset const& asset,
|
||||
Account const& owner,
|
||||
Account const& alice,
|
||||
Account const& bob,
|
||||
Account const& carol,
|
||||
std::function<void(bool)> const& setAliceFrozen) {
|
||||
Vault const vault{env};
|
||||
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = asset(500)}));
|
||||
env(vault.deposit({.depositor = bob, .id = keylet.key, .amount = asset(500)}));
|
||||
env.close();
|
||||
|
||||
PrettyAsset const shares = MPTIssue(env.le(keylet)->at(sfShareMPTID));
|
||||
auto const shareMptID = shares.raw().get<MPTIssue>().getMptID();
|
||||
auto const shareBalance = [&](Account const& account) {
|
||||
auto const sle = env.le(keylet::mptoken(shareMptID, account));
|
||||
return sle ? sle->at(sfMPTAmount) : 0;
|
||||
};
|
||||
|
||||
// alice gives the shares: XRP -> shares (alice) -> asset (bob)
|
||||
{
|
||||
env(offer(alice, XRP(1), shares(1)));
|
||||
env(offer(bob, shares(1), asset(1)));
|
||||
env.close();
|
||||
|
||||
setAliceFrozen(true);
|
||||
|
||||
auto const aliceShares = shareBalance(alice);
|
||||
auto const bobShares = shareBalance(bob);
|
||||
env(pay(carol, owner, asset(1)),
|
||||
Sendmax(XRP(1)),
|
||||
Path(BookSpec{shares.raw()}, BookSpec{asset.raw()}),
|
||||
Txflags(tfNoRippleDirect),
|
||||
Ter{tecPATH_PARTIAL});
|
||||
env.close();
|
||||
BEAST_EXPECT(shareBalance(alice) == aliceShares);
|
||||
BEAST_EXPECT(shareBalance(bob) == bobShares);
|
||||
|
||||
setAliceFrozen(false);
|
||||
|
||||
env(pay(carol, owner, asset(1)),
|
||||
Sendmax(XRP(1)),
|
||||
Path(BookSpec{shares.raw()}, BookSpec{asset.raw()}),
|
||||
Txflags(tfNoRippleDirect));
|
||||
env.close();
|
||||
BEAST_EXPECT(shareBalance(alice) == aliceShares - 1);
|
||||
BEAST_EXPECT(shareBalance(bob) == bobShares + 1);
|
||||
BEAST_EXPECT(expectOffers(env, alice, 0));
|
||||
BEAST_EXPECT(expectOffers(env, bob, 0));
|
||||
}
|
||||
|
||||
// alice receives the shares: asset -> shares (bob) -> XRP (alice)
|
||||
{
|
||||
env(offer(bob, asset(1), shares(1)));
|
||||
env(offer(alice, shares(1), XRP(1)));
|
||||
env.close();
|
||||
|
||||
setAliceFrozen(true);
|
||||
|
||||
auto const aliceShares = shareBalance(alice);
|
||||
auto const bobShares = shareBalance(bob);
|
||||
env(pay(owner, carol, XRP(1)),
|
||||
Sendmax(asset(1)),
|
||||
Path(BookSpec{shares.raw()}, ~XRP),
|
||||
Txflags(tfNoRippleDirect),
|
||||
Ter{tecPATH_PARTIAL});
|
||||
env.close();
|
||||
BEAST_EXPECT(shareBalance(alice) == aliceShares);
|
||||
BEAST_EXPECT(shareBalance(bob) == bobShares);
|
||||
|
||||
setAliceFrozen(false);
|
||||
|
||||
env(pay(owner, carol, XRP(1)),
|
||||
Sendmax(asset(1)),
|
||||
Path(BookSpec{shares.raw()}, ~XRP),
|
||||
Txflags(tfNoRippleDirect));
|
||||
env.close();
|
||||
BEAST_EXPECT(shareBalance(alice) == aliceShares + 1);
|
||||
BEAST_EXPECT(shareBalance(bob) == bobShares - 1);
|
||||
BEAST_EXPECT(expectOffers(env, alice, 0));
|
||||
BEAST_EXPECT(expectOffers(env, bob, 0));
|
||||
}
|
||||
};
|
||||
|
||||
{
|
||||
testcase("MPT individually locked: vault share DEX inherits underlying lock");
|
||||
|
||||
Env env{*this, testableAmendments()};
|
||||
Account const issuer{"issuer"};
|
||||
Account const owner{"owner"};
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
Account const carol{"carol"};
|
||||
env.fund(XRP(10'000), issuer, owner, alice, bob, carol);
|
||||
env.close();
|
||||
|
||||
MPTTester asset{{
|
||||
.env = env,
|
||||
.issuer = issuer,
|
||||
.holders = {owner, alice, bob},
|
||||
.flags = tfMPTCanTransfer | tfMPTCanTrade | tfMPTCanLock,
|
||||
}};
|
||||
env(pay(issuer, owner, asset(1'000)));
|
||||
env(pay(issuer, alice, asset(2'000)));
|
||||
env(pay(issuer, bob, asset(2'000)));
|
||||
env.close();
|
||||
|
||||
auto const setAliceFrozen = [&](bool frozen) {
|
||||
asset.set({.holder = alice, .flags = frozen ? tfMPTLock : tfMPTUnlock});
|
||||
env.close();
|
||||
};
|
||||
testShareDEXEndpointFrozen(env, asset, owner, alice, bob, carol, setAliceFrozen);
|
||||
testShareDEXMiddleFrozen(env, asset, owner, alice, bob, carol, setAliceFrozen);
|
||||
}
|
||||
|
||||
{
|
||||
testcase("IOU individually frozen: vault share DEX inherits underlying freeze");
|
||||
|
||||
Env env{*this, testableAmendments()};
|
||||
Account const issuer{"issuer"};
|
||||
Account const owner{"owner"};
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
Account const carol{"carol"};
|
||||
env.fund(XRP(10'000), issuer, owner, alice, bob, carol);
|
||||
env.close();
|
||||
|
||||
// Deliver the IOU from a book to a holder
|
||||
env(fset(issuer, asfDefaultRipple));
|
||||
PrettyAsset const asset = issuer["IOU"];
|
||||
env.trust(asset(10'000), owner, alice, bob);
|
||||
env(pay(issuer, owner, asset(1'000)));
|
||||
env(pay(issuer, alice, asset(2'000)));
|
||||
env(pay(issuer, bob, asset(2'000)));
|
||||
env.close();
|
||||
|
||||
auto const setAliceFrozen = [&](bool frozen) {
|
||||
env(trust(issuer, asset(0), alice, frozen ? tfSetFreeze : tfClearFreeze));
|
||||
env.close();
|
||||
};
|
||||
testShareDEXEndpointFrozen(env, asset, owner, alice, bob, carol, setAliceFrozen);
|
||||
testShareDEXMiddleFrozen(env, asset, owner, alice, bob, carol, setAliceFrozen);
|
||||
}
|
||||
|
||||
{
|
||||
testcase("MPT CanTrade governance: share inherits underlying on DEX and AMM");
|
||||
|
||||
|
||||
@@ -172,7 +172,7 @@ private:
|
||||
jvParams[jss::ledger_index] = jss::validated;
|
||||
jvParams[jss::vault] = to_string(UInt256(42));
|
||||
auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
|
||||
checkError(jvVault[jss::result], "entryNotFound", RpcEntryNotFound, "Entry not found.");
|
||||
BEAST_EXPECT(jvVault[jss::result][jss::error].asString() == "entryNotFound");
|
||||
}
|
||||
|
||||
{
|
||||
@@ -182,7 +182,7 @@ private:
|
||||
jvParams[jss::vault][jss::owner] = issuer.human();
|
||||
jvParams[jss::vault][jss::seq] = 1'000'000;
|
||||
auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
|
||||
checkError(jvVault[jss::result], "entryNotFound", RpcEntryNotFound, "Entry not found.");
|
||||
BEAST_EXPECT(jvVault[jss::result][jss::error].asString() == "entryNotFound");
|
||||
}
|
||||
|
||||
{
|
||||
@@ -191,11 +191,7 @@ private:
|
||||
jvParams[jss::ledger_index] = jss::validated;
|
||||
jvParams[jss::vault] = 42;
|
||||
auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
|
||||
checkError(
|
||||
jvVault[jss::result],
|
||||
"malformedRequest",
|
||||
RpcInvalidParams,
|
||||
"Invalid field 'vault', not hex string or object.");
|
||||
BEAST_EXPECT(jvVault[jss::result][jss::error].asString() == "malformedRequest");
|
||||
}
|
||||
|
||||
{
|
||||
@@ -205,11 +201,7 @@ private:
|
||||
jvParams[jss::vault][jss::owner] = 42;
|
||||
jvParams[jss::vault][jss::seq] = sequence;
|
||||
auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
|
||||
checkError(
|
||||
jvVault[jss::result],
|
||||
"malformedOwner",
|
||||
RpcInvalidParams,
|
||||
"Invalid field 'owner', not AccountID.");
|
||||
BEAST_EXPECT(jvVault[jss::result][jss::error].asString() == "malformedOwner");
|
||||
}
|
||||
|
||||
{
|
||||
@@ -219,11 +211,7 @@ private:
|
||||
jvParams[jss::vault][jss::owner] = issuer.human();
|
||||
jvParams[jss::vault][jss::seq] = "foo";
|
||||
auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
|
||||
checkError(
|
||||
jvVault[jss::result],
|
||||
"malformedRequest",
|
||||
RpcInvalidParams,
|
||||
"Invalid field 'seq', not number.");
|
||||
BEAST_EXPECT(jvVault[jss::result][jss::error].asString() == "malformedRequest");
|
||||
}
|
||||
|
||||
{
|
||||
@@ -233,11 +221,7 @@ private:
|
||||
jvParams[jss::vault][jss::owner] = issuer.human();
|
||||
jvParams[jss::vault][jss::seq] = -1;
|
||||
auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
|
||||
checkError(
|
||||
jvVault[jss::result],
|
||||
"malformedRequest",
|
||||
RpcInvalidParams,
|
||||
"Invalid field 'seq', not number.");
|
||||
BEAST_EXPECT(jvVault[jss::result][jss::error].asString() == "malformedRequest");
|
||||
}
|
||||
|
||||
{
|
||||
@@ -247,11 +231,7 @@ private:
|
||||
jvParams[jss::vault][jss::owner] = issuer.human();
|
||||
jvParams[jss::vault][jss::seq] = 1e20;
|
||||
auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
|
||||
checkError(
|
||||
jvVault[jss::result],
|
||||
"malformedRequest",
|
||||
RpcInvalidParams,
|
||||
"Invalid field 'seq', not number.");
|
||||
BEAST_EXPECT(jvVault[jss::result][jss::error].asString() == "malformedRequest");
|
||||
}
|
||||
|
||||
{
|
||||
@@ -261,11 +241,7 @@ private:
|
||||
jvParams[jss::vault][jss::owner] = issuer.human();
|
||||
jvParams[jss::vault][jss::seq] = true;
|
||||
auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
|
||||
checkError(
|
||||
jvVault[jss::result],
|
||||
"malformedRequest",
|
||||
RpcInvalidParams,
|
||||
"Invalid field 'seq', not number.");
|
||||
BEAST_EXPECT(jvVault[jss::result][jss::error].asString() == "malformedRequest");
|
||||
}
|
||||
|
||||
{
|
||||
@@ -524,8 +500,8 @@ private:
|
||||
|
||||
{
|
||||
testcase("RPC vault_info command line invalid index");
|
||||
json::Value const jv = env.rpc("vault_info", "foobar", "validated");
|
||||
checkError(jv, "invalidParams", RpcInvalidParams, "Invalid parameters.");
|
||||
json::Value jv = env.rpc("vault_info", "foobar", "validated");
|
||||
BEAST_EXPECT(jv[jss::error].asString() == "invalidParams");
|
||||
}
|
||||
|
||||
{
|
||||
@@ -543,7 +519,7 @@ private:
|
||||
{
|
||||
testcase("RPC vault_info command line invalid ledger");
|
||||
json::Value jv = env.rpc("vault_info", strHex(keylet.key), "0");
|
||||
checkError(jv[jss::result], "lgrNotFound", RpcLgrNotFound, "ledgerNotFound");
|
||||
BEAST_EXPECT(jv[jss::result][jss::error].asString() == "lgrNotFound");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,8 @@
|
||||
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/JTx.h>
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/basics/strHex.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Rate.h>
|
||||
@@ -14,10 +11,7 @@
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
/**
|
||||
* Escrow operations.
|
||||
@@ -101,55 +95,4 @@ auto const kCondition = JTxFieldWrapper<BlobField>(sfCondition);
|
||||
|
||||
auto const kFulfillment = JTxFieldWrapper<BlobField>(sfFulfillment);
|
||||
|
||||
/**
|
||||
* Set a hex-encoded blob field on a JTx
|
||||
*/
|
||||
template <SF_VL const& Field>
|
||||
struct HexBlobField
|
||||
{
|
||||
private:
|
||||
std::string value_;
|
||||
|
||||
public:
|
||||
explicit HexBlobField(std::string hex) : value_(std::move(hex))
|
||||
{
|
||||
}
|
||||
|
||||
explicit HexBlobField(Slice const& blob) : value_(strHex(blob))
|
||||
{
|
||||
}
|
||||
|
||||
template <size_t N>
|
||||
explicit HexBlobField(std::array<std::uint8_t, N> const& blob) : HexBlobField(makeSlice(blob))
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
operator()(Env&, JTx& jt) const
|
||||
{
|
||||
// clang-tidy reads a reference template parameter as parenthesized.
|
||||
jt.jv[Field.jsonName] = value_; // NOLINT(readability-redundant-parentheses)
|
||||
}
|
||||
};
|
||||
|
||||
using Bytecode = HexBlobField<sfBytecode>;
|
||||
using Data = HexBlobField<sfData>;
|
||||
|
||||
struct Gas
|
||||
{
|
||||
private:
|
||||
std::uint32_t value_;
|
||||
|
||||
public:
|
||||
explicit Gas(std::uint32_t const value) : value_(value)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
operator()(Env&, JTx& jt) const
|
||||
{
|
||||
jt.jv[sfGas.jsonName] = value_;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace xrpl::test::jtx::escrow
|
||||
@@ -238,13 +238,6 @@ verify::Create::operator()(Env& env, JTx& jt) const
|
||||
// Every entry the transaction could touch, read whole, so what follows can
|
||||
// say that nothing moved rather than that the fields we named did not.
|
||||
auto const& view = *env.current();
|
||||
|
||||
// A proposer paying with one of its own Tickets consumes it whenever the
|
||||
// transaction is applied: on success, and on a claimed-fee tec.
|
||||
std::optional<std::uint32_t> ownTicketSeq;
|
||||
if (jt.jv.isMember(sfTicketSequence.jsonName))
|
||||
ownTicketSeq = jt.jv[sfTicketSequence.jsonName].asUInt();
|
||||
|
||||
auto const proposalKeylet = keylet::txProposal(target, ticketSeq);
|
||||
auto const ownerCountBefore = env.ownerCount(proposer);
|
||||
auto const sponsoredOwnerCountBefore = env.sponsoredOwnerCount(proposer);
|
||||
@@ -261,15 +254,11 @@ verify::Create::operator()(Env& env, JTx& jt) const
|
||||
auto const& view = *applied.current();
|
||||
|
||||
auto const created = isTesSuccess(applied.ter());
|
||||
bool const ticketConsumed = ownTicketSeq && (created || isTecClaim(applied.ter()));
|
||||
std::uint32_t const ticketReturned = ticketConsumed ? 1 : 0;
|
||||
|
||||
// The proposer owns the proposal even when another account covers its
|
||||
// reserve. A proposed Batch costs more owner-count increments. A
|
||||
// Ticket the proposer paid with gives one back.
|
||||
// reserve. A proposed Batch costs more owner-count increments.
|
||||
test.expect(
|
||||
applied.ownerCount(proposer) ==
|
||||
ownerCountBefore + (created ? cost : 0) - ticketReturned,
|
||||
applied.ownerCount(proposer) == ownerCountBefore + (created ? cost : 0),
|
||||
"proposal reserve");
|
||||
test.expect(
|
||||
applied.sponsoredOwnerCount(proposer) ==
|
||||
@@ -302,14 +291,6 @@ verify::Create::operator()(Env& env, JTx& jt) const
|
||||
|
||||
auto const sleProposal = view.read(proposalKeylet);
|
||||
|
||||
// The Ticket the proposer paid with has left its directory.
|
||||
auto expectedDir = proposerDirBefore;
|
||||
if (ticketConsumed)
|
||||
{
|
||||
expectedDir.erase(
|
||||
keylet::ticket(proposer.id(), SeqProxy::rawTicket(*ownTicketSeq)).key);
|
||||
}
|
||||
|
||||
if (!created)
|
||||
{
|
||||
// A create that did not succeed leaves the proposal as it found
|
||||
@@ -319,7 +300,7 @@ verify::Create::operator()(Env& env, JTx& jt) const
|
||||
// Nor did the directory gain a listing for an entry that does not
|
||||
// exist.
|
||||
test.expect(
|
||||
ownerDirKeys(view, proposer.id()) == expectedDir, "proposal owner directory");
|
||||
ownerDirKeys(view, proposer.id()) == proposerDirBefore, "proposal owner directory");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -372,6 +353,7 @@ verify::Create::operator()(Env& env, JTx& jt) const
|
||||
|
||||
// The proposal is listed in the proposer's directory on the page its
|
||||
// OwnerNode names, and nothing else listed moved.
|
||||
auto expectedDir = proposerDirBefore;
|
||||
expectedDir.emplace(sleProposal->key(), sleProposal->getFieldU64(sfOwnerNode));
|
||||
test.expect(ownerDirKeys(view, proposer.id()) == expectedDir, "proposal owner directory");
|
||||
});
|
||||
|
||||
@@ -77,9 +77,7 @@ public:
|
||||
* @brief Check the ledger effects a TransactionProposalCreate must have,
|
||||
* whatever its outcome: on tesSUCCESS a new proposal holding what was
|
||||
* submitted, listed in the proposer's directory and paid for by its reserve;
|
||||
* otherwise nothing moved. Nothing of the target's moves either way. A Ticket
|
||||
* the proposer pays with is expected to be consumed; attach ticket::Use()
|
||||
* before this condition so it sees the TicketSequence.
|
||||
* otherwise nothing moved. Nothing of the target's moves either way.
|
||||
*
|
||||
* @code
|
||||
* env(proposal::create(alice, payload, expiration), proposal::verify::create());
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
@@ -181,7 +180,6 @@ public:
|
||||
auto const result = env.rpc("json", "sign", to_string(req))[jss::result];
|
||||
|
||||
BEAST_EXPECT(result[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(result[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(result[jss::error_message] == "Field 'tx_json.Asset' has invalid data.");
|
||||
}
|
||||
|
||||
@@ -255,7 +253,6 @@ public:
|
||||
auto const result = env.rpc("json", "sign", to_string(req))[jss::result];
|
||||
|
||||
BEAST_EXPECT(result[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(result[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(result[jss::error_message] == "Field 'tx_json.Asset' has invalid data.");
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
@@ -57,16 +56,12 @@ public:
|
||||
Account const gw("gw");
|
||||
auto const usd = gw["USD"];
|
||||
auto const jv = ammAlice.ammRpcInfo({}, {}, usd, usd);
|
||||
BEAST_EXPECT(jv[jss::error] == "actNotFound");
|
||||
BEAST_EXPECT(jv[jss::error_code] == RpcActNotFound);
|
||||
BEAST_EXPECT(jv[jss::error_message] == "Account not found.");
|
||||
});
|
||||
|
||||
// Invalid LP account id
|
||||
testAMM([&](AMM& ammAlice, Env&) {
|
||||
auto const jv = ammAlice.ammRpcInfo(bogie.id());
|
||||
BEAST_EXPECT(jv[jss::error] == "actMalformed");
|
||||
BEAST_EXPECT(jv[jss::error_code] == RpcActMalformed);
|
||||
BEAST_EXPECT(jv[jss::error_message] == "Account malformed.");
|
||||
});
|
||||
|
||||
@@ -74,8 +69,6 @@ public:
|
||||
testAMM([&](AMM& ammAlice, Env&) {
|
||||
auto const jv =
|
||||
ammAlice.ammRpcInfo(json::Value{42}, std::nullopt, XRP, USD, std::nullopt, true, 3);
|
||||
BEAST_EXPECT(jv[jss::error] == "actMalformed");
|
||||
BEAST_EXPECT(jv[jss::error_code] == RpcActMalformed);
|
||||
BEAST_EXPECT(jv[jss::error_message] == "Account malformed.");
|
||||
});
|
||||
|
||||
@@ -89,8 +82,6 @@ public:
|
||||
json::Value{42},
|
||||
false,
|
||||
3);
|
||||
BEAST_EXPECT(jv[jss::error] == "actMalformed");
|
||||
BEAST_EXPECT(jv[jss::error_code] == RpcActMalformed);
|
||||
BEAST_EXPECT(jv[jss::error_message] == "Account malformed.");
|
||||
});
|
||||
|
||||
@@ -115,8 +106,6 @@ public:
|
||||
iss2,
|
||||
accountId(ammAlice, acct),
|
||||
ignoreParams);
|
||||
BEAST_EXPECT(jv[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jv[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jv[jss::error_message] == "Invalid parameters.");
|
||||
}
|
||||
});
|
||||
@@ -132,8 +121,6 @@ public:
|
||||
iss2,
|
||||
accountId(ammAlice, acct),
|
||||
ignoreParams);
|
||||
BEAST_EXPECT(jv[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jv[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jv[jss::error_message] == "Invalid parameters.");
|
||||
}
|
||||
});
|
||||
@@ -150,8 +137,6 @@ public:
|
||||
accountId(ammAlice, acct),
|
||||
ignoreParams,
|
||||
3);
|
||||
BEAST_EXPECT(jv[jss::error] == "actMalformed");
|
||||
BEAST_EXPECT(jv[jss::error_code] == RpcActMalformed);
|
||||
BEAST_EXPECT(jv[jss::error_message] == "Account malformed.");
|
||||
}
|
||||
});
|
||||
@@ -160,8 +145,6 @@ public:
|
||||
testAMM([&](AMM& ammAlice, Env&) {
|
||||
auto const jv = ammAlice.ammRpcInfo(
|
||||
std::nullopt, std::nullopt, std::nullopt, std::nullopt, bogie.id());
|
||||
BEAST_EXPECT(jv[jss::error] == "actMalformed");
|
||||
BEAST_EXPECT(jv[jss::error_code] == RpcActMalformed);
|
||||
BEAST_EXPECT(jv[jss::error_message] == "Account malformed.");
|
||||
});
|
||||
|
||||
@@ -186,8 +169,6 @@ public:
|
||||
iss2,
|
||||
accountId(ammAlice, acct),
|
||||
ignoreParams);
|
||||
BEAST_EXPECT(jv[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jv[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jv[jss::error_message] == "Invalid parameters.");
|
||||
}
|
||||
});
|
||||
@@ -204,13 +185,6 @@ public:
|
||||
accountId(ammAlice, acct),
|
||||
ignoreParams,
|
||||
3);
|
||||
BEAST_EXPECT(
|
||||
jv[jss::error] ==
|
||||
(acct == TestAccount::Bogie ? std::string("actMalformed")
|
||||
: std::string("invalidParams")));
|
||||
BEAST_EXPECT(
|
||||
jv[jss::error_code] ==
|
||||
(acct == TestAccount::Bogie ? RpcActMalformed : RpcInvalidParams));
|
||||
BEAST_EXPECT(
|
||||
jv[jss::error_message] ==
|
||||
(acct == TestAccount::Bogie ? std::string("Account malformed.")
|
||||
@@ -409,9 +383,7 @@ public:
|
||||
testAMM([&](AMM& amm, Env&) {
|
||||
auto const resp = amm.ammRpcInfo(
|
||||
std::nullopt, jss::validated.cStr(), std::nullopt, std::nullopt, gw_);
|
||||
BEAST_EXPECT(resp[jss::error] == "actNotFound");
|
||||
BEAST_EXPECT(resp[jss::error_code] == RpcActNotFound);
|
||||
BEAST_EXPECT(resp[jss::error_message] == "Account not found.");
|
||||
BEAST_EXPECT(resp.isMember("error") && resp["error"] == "actNotFound");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <format>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
@@ -35,8 +34,6 @@ public:
|
||||
{
|
||||
// account_info with no account.
|
||||
auto const info = env.rpc("json", "account_info", "{ }");
|
||||
BEAST_EXPECT(info[jss::result][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(info[jss::result][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(info[jss::result][jss::error_message] == "Missing field 'account'.");
|
||||
}
|
||||
{
|
||||
@@ -46,11 +43,8 @@ public:
|
||||
"account_info",
|
||||
"{\"account\": "
|
||||
"\"n94JNrQYkDrpt62bbSR7nVEhdyAvcJXRAsjEkFYyqRkh9SUTYEqV\"}");
|
||||
BEAST_EXPECT(info[jss::result][jss::error] == "actMalformed");
|
||||
BEAST_EXPECT(info[jss::result][jss::error_code] == RpcActMalformed);
|
||||
BEAST_EXPECT(info[jss::result][jss::error_message] == "Account malformed.");
|
||||
BEAST_EXPECT(info[jss::result].isMember(jss::ledger_current_index));
|
||||
BEAST_EXPECT(info[jss::result][jss::validated] == false);
|
||||
}
|
||||
{
|
||||
// account_info with an account that's not in the ledger.
|
||||
@@ -58,17 +52,12 @@ public:
|
||||
json::Value params;
|
||||
params[jss::account] = bogie.human();
|
||||
auto const info = env.rpc("json", "account_info", to_string(params));
|
||||
BEAST_EXPECT(info[jss::result][jss::error] == "actNotFound");
|
||||
BEAST_EXPECT(info[jss::result][jss::error_code] == RpcActNotFound);
|
||||
BEAST_EXPECT(info[jss::result][jss::error_message] == "Account not found.");
|
||||
BEAST_EXPECT(info[jss::result][jss::account] == bogie.human());
|
||||
BEAST_EXPECT(info[jss::result].isMember(jss::ledger_current_index));
|
||||
BEAST_EXPECT(info[jss::result][jss::validated] == false);
|
||||
}
|
||||
{
|
||||
// Cannot use a seed as account
|
||||
auto const info = env.rpc("json", "account_info", R"JSON({"account": "foo"})JSON");
|
||||
BEAST_EXPECT(info[jss::result][jss::error] == "actMalformed");
|
||||
auto const info = env.rpc("json", "account_info", R"({"account": "foo"})");
|
||||
BEAST_EXPECT(info[jss::result][jss::error_code] == RpcActMalformed);
|
||||
BEAST_EXPECT(info[jss::result][jss::error_message] == "Account malformed.");
|
||||
}
|
||||
@@ -80,7 +69,6 @@ public:
|
||||
params[jss::account] = param;
|
||||
auto jrr = env.rpc("json", "account_info", to_string(params))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'account'.");
|
||||
};
|
||||
|
||||
@@ -99,7 +87,6 @@ public:
|
||||
params[jss::ident] = param;
|
||||
auto jrr = env.rpc("json", "account_info", to_string(params))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'ident'.");
|
||||
};
|
||||
|
||||
@@ -110,41 +97,6 @@ public:
|
||||
testInvalidIdentParam(json::Value(json::ValueType::Object));
|
||||
testInvalidIdentParam(json::Value(json::ValueType::Array));
|
||||
}
|
||||
{
|
||||
// `ident` is ignored when `account` is present
|
||||
json::Value params;
|
||||
params[jss::account] = env.master.human();
|
||||
params[jss::ident] = 1;
|
||||
auto const info = env.rpc("json", "account_info", to_string(params))[jss::result];
|
||||
BEAST_EXPECT(!info.isMember(jss::error));
|
||||
BEAST_EXPECT(info.isMember(jss::account_data));
|
||||
BEAST_EXPECT(info[jss::account_data][sfAccount.jsonName] == env.master.human());
|
||||
}
|
||||
{
|
||||
// an invalid ledger is reported before a missing account
|
||||
json::Value params;
|
||||
params[jss::ledger_hash] = 1;
|
||||
auto const info = env.rpc("json", "account_info", to_string(params))[jss::result];
|
||||
BEAST_EXPECT(info[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(info[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
info[jss::error_message] == "Invalid field 'ledger_hash', not hex string.");
|
||||
}
|
||||
{
|
||||
// `queue` is read as a bool, so any non-empty string requests it, and the queue
|
||||
// can't be requested from a closed ledger
|
||||
env.close();
|
||||
json::Value params;
|
||||
params[jss::account] = env.master.human();
|
||||
params[jss::ledger_index] = "closed";
|
||||
params[jss::queue] = "false";
|
||||
auto const info = env.rpc("json", "account_info", to_string(params))[jss::result];
|
||||
BEAST_EXPECT(info[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(info[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(info[jss::error_message] == "Invalid parameters.");
|
||||
BEAST_EXPECT(info.isMember(jss::ledger_index));
|
||||
BEAST_EXPECT(!info.isMember(jss::account_data));
|
||||
}
|
||||
}
|
||||
|
||||
// Test the "signer_lists" argument in account_info.
|
||||
@@ -282,9 +234,8 @@ public:
|
||||
withSigners[jss::account] = alice.human();
|
||||
withSigners[jss::signer_lists] = true;
|
||||
|
||||
auto const withSignersAsString = std::format(
|
||||
R"JSON({{"api_version": 2, "account": "{}", "signer_lists": "yes"}})JSON",
|
||||
alice.human());
|
||||
auto const withSignersAsString = std::string("{ ") + R"("api_version": 2, "account": ")" +
|
||||
alice.human() + "\", " + "\"signer_lists\": asdfggh }";
|
||||
|
||||
// Alice has no SignerList yet.
|
||||
{
|
||||
@@ -334,15 +285,9 @@ public:
|
||||
}
|
||||
{
|
||||
// account_info with "signer_lists" as not bool should error out
|
||||
auto const info = env.rpc("json", "account_info", withSignersAsString)[jss::result];
|
||||
auto const info = env.rpc("json", "account_info", withSignersAsString);
|
||||
BEAST_EXPECT(info[jss::status] == "error");
|
||||
BEAST_EXPECT(info[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(info[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(info[jss::error_message] == "Invalid parameters.");
|
||||
BEAST_EXPECT(info.isMember(jss::account_data));
|
||||
BEAST_EXPECT(info[jss::account_data][sfAccount.jsonName] == alice.human());
|
||||
BEAST_EXPECT(info.isMember(jss::ledger_current_index));
|
||||
BEAST_EXPECT(!info.isMember(jss::signer_lists));
|
||||
}
|
||||
|
||||
// Give alice a big signer list
|
||||
|
||||
@@ -48,15 +48,9 @@ public:
|
||||
|
||||
using namespace test::jtx;
|
||||
Env env(*this);
|
||||
|
||||
static constexpr std::size_t kLinesPerGateway = 26;
|
||||
static constexpr std::size_t kAllLines = 2 * kLinesPerGateway;
|
||||
|
||||
{
|
||||
// account_lines with no account.
|
||||
auto const lines = env.rpc("json", "account_lines", "{ }");
|
||||
BEAST_EXPECT(lines[jss::result][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(lines[jss::result][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
lines[jss::result][jss::error_message] ==
|
||||
rpc::missingFieldError(jss::account)[jss::error_message]);
|
||||
@@ -66,35 +60,9 @@ public:
|
||||
json::Value params;
|
||||
params[jss::account] = "n9MJkEKHDhy5eTLuHUQeAAjo382frHNbFK4C8hcwN4nwM2SrLdBj";
|
||||
auto const lines = env.rpc("json", "account_lines", to_string(params));
|
||||
BEAST_EXPECT(lines[jss::result][jss::error] == "actMalformed");
|
||||
BEAST_EXPECT(lines[jss::result][jss::error_code] == RpcActMalformed);
|
||||
BEAST_EXPECT(
|
||||
lines[jss::result][jss::error_message] ==
|
||||
rpc::makeError(RpcActMalformed)[jss::error_message]);
|
||||
BEAST_EXPECT(lines[jss::result].isMember(jss::ledger_current_index));
|
||||
BEAST_EXPECT(lines[jss::result][jss::validated] == false);
|
||||
}
|
||||
{
|
||||
// An invalid ledger is reported before a missing account.
|
||||
json::Value params;
|
||||
params[jss::ledger_hash] = 1;
|
||||
auto const lines = env.rpc("json", "account_lines", to_string(params))[jss::result];
|
||||
BEAST_EXPECT(lines[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(lines[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
lines[jss::error_message] == "Invalid field 'ledger_hash', not hex string.");
|
||||
}
|
||||
{
|
||||
// A bad limit is reported before an account that is not found.
|
||||
json::Value params;
|
||||
params[jss::account] = Account{"bogie"}.human();
|
||||
params[jss::limit] = -1;
|
||||
auto const lines = env.rpc("json", "account_lines", to_string(params))[jss::result];
|
||||
BEAST_EXPECT(lines[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(lines[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
lines[jss::error_message] ==
|
||||
rpc::expectedFieldMessage(jss::limit, "unsigned integer"));
|
||||
}
|
||||
{
|
||||
// test account non-string
|
||||
@@ -103,7 +71,6 @@ public:
|
||||
params[jss::account] = param;
|
||||
auto jrr = env.rpc("json", "account_lines", to_string(params))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'account'.");
|
||||
};
|
||||
|
||||
@@ -120,8 +87,6 @@ public:
|
||||
json::Value params;
|
||||
params[jss::account] = alice.human();
|
||||
auto const lines = env.rpc("json", "account_lines", to_string(params));
|
||||
BEAST_EXPECT(lines[jss::result][jss::error] == "actNotFound");
|
||||
BEAST_EXPECT(lines[jss::result][jss::error_code] == RpcActNotFound);
|
||||
BEAST_EXPECT(
|
||||
lines[jss::result][jss::error_message] ==
|
||||
rpc::makeError(RpcActNotFound)[jss::error_message]);
|
||||
@@ -139,7 +104,6 @@ public:
|
||||
params[jss::peer] = param;
|
||||
auto jrr = env.rpc("json", "account_lines", to_string(params))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'peer'.");
|
||||
};
|
||||
|
||||
@@ -164,8 +128,6 @@ public:
|
||||
params[jss::account] = alice.human();
|
||||
params[jss::ledger_index] = "nonsense";
|
||||
auto const lines = env.rpc("json", "account_lines", to_string(params));
|
||||
BEAST_EXPECT(lines[jss::result][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(lines[jss::result][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
lines[jss::result][jss::error_message] ==
|
||||
"Invalid field 'ledger_index', not string or number.");
|
||||
@@ -176,8 +138,6 @@ public:
|
||||
params[jss::account] = alice.human();
|
||||
params[jss::ledger_index] = 50000;
|
||||
auto const lines = env.rpc("json", "account_lines", to_string(params));
|
||||
BEAST_EXPECT(lines[jss::result][jss::error] == "lgrNotFound");
|
||||
BEAST_EXPECT(lines[jss::result][jss::error_code] == RpcLgrNotFound);
|
||||
BEAST_EXPECT(lines[jss::result][jss::error_message] == "ledgerNotFound");
|
||||
}
|
||||
// Create trust lines to share with alice.
|
||||
@@ -231,7 +191,7 @@ public:
|
||||
|
||||
// A re-usable test for historic ledgers.
|
||||
auto const testAccountLinesHistory =
|
||||
[this, &env](Account const& account, LedgerHeader const& info, std::size_t count) {
|
||||
[this, &env](Account const& account, LedgerHeader const& info, int count) {
|
||||
// Get account_lines by ledger index.
|
||||
json::Value paramsSeq;
|
||||
paramsSeq[jss::account] = account.human();
|
||||
@@ -253,10 +213,10 @@ public:
|
||||
testAccountLinesHistory(alice, ledger3Info, 0);
|
||||
|
||||
// Alice should have 26 trust lines in ledger 4.
|
||||
testAccountLinesHistory(alice, ledger4Info, kLinesPerGateway);
|
||||
testAccountLinesHistory(alice, ledger4Info, 26);
|
||||
|
||||
// Alice should have 52 trust lines in ledger 58.
|
||||
testAccountLinesHistory(alice, ledger58Info, kAllLines);
|
||||
testAccountLinesHistory(alice, ledger58Info, 52);
|
||||
|
||||
{
|
||||
// Invalid to specify both index and hash
|
||||
@@ -266,7 +226,6 @@ public:
|
||||
params[jss::ledger_index] = ledger58Info.seq;
|
||||
auto const lines = env.rpc("json", "account_lines", to_string(params))[jss::result];
|
||||
BEAST_EXPECT(lines[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(lines[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
lines[jss::error_message] ==
|
||||
"Exactly one of 'ledger_hash' or 'ledger_index' can be "
|
||||
@@ -279,7 +238,6 @@ public:
|
||||
params[jss::ledger_index] = json::ValueType::Object;
|
||||
auto const lines = env.rpc("json", "account_lines", to_string(params))[jss::result];
|
||||
BEAST_EXPECT(lines[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(lines[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
lines[jss::error_message] == "Invalid field 'ledger_index', not string or number.");
|
||||
}
|
||||
@@ -289,7 +247,7 @@ public:
|
||||
params[jss::account] = alice.human();
|
||||
auto const lines = env.rpc("json", "account_lines", to_string(params));
|
||||
BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
|
||||
BEAST_EXPECT(lines[jss::result][jss::lines].size() == kAllLines);
|
||||
BEAST_EXPECT(lines[jss::result][jss::lines].size() == 52);
|
||||
}
|
||||
{
|
||||
// alice should have 26 trust lines with gw1.
|
||||
@@ -298,7 +256,7 @@ public:
|
||||
params[jss::peer] = gw1.human();
|
||||
auto const lines = env.rpc("json", "account_lines", to_string(params));
|
||||
BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
|
||||
BEAST_EXPECT(lines[jss::result][jss::lines].size() == kLinesPerGateway);
|
||||
BEAST_EXPECT(lines[jss::result][jss::lines].size() == 26);
|
||||
|
||||
// Check no ripple is not set for trustlines between alice and gw1
|
||||
auto const& line = lines[jss::result][jss::lines][0u];
|
||||
@@ -310,39 +268,9 @@ public:
|
||||
params[jss::account] = alice.human();
|
||||
params[jss::peer] = "n9MJkEKHDhy5eTLuHUQeAAjo382frHNbFK4C8hcwN4nwM2SrLdBj";
|
||||
auto const lines = env.rpc("json", "account_lines", to_string(params));
|
||||
BEAST_EXPECT(lines[jss::result][jss::error] == "actMalformed");
|
||||
BEAST_EXPECT(lines[jss::result][jss::error_code] == RpcActMalformed);
|
||||
BEAST_EXPECT(
|
||||
lines[jss::result][jss::error_message] ==
|
||||
rpc::makeError(RpcActMalformed)[jss::error_message]);
|
||||
BEAST_EXPECT(lines[jss::result].isMember(jss::ledger_current_index));
|
||||
BEAST_EXPECT(lines[jss::result][jss::validated] == false);
|
||||
}
|
||||
{
|
||||
// An empty peer is no peer at all.
|
||||
json::Value params;
|
||||
params[jss::account] = alice.human();
|
||||
params[jss::peer] = "";
|
||||
auto const lines = env.rpc("json", "account_lines", to_string(params));
|
||||
params.removeMember(jss::peer);
|
||||
auto const all = env.rpc("json", "account_lines", to_string(params));
|
||||
BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
|
||||
BEAST_EXPECT(lines[jss::result][jss::lines].size() == kAllLines);
|
||||
BEAST_EXPECT(lines[jss::result][jss::lines] == all[jss::result][jss::lines]);
|
||||
}
|
||||
{
|
||||
// ignore_default is read as a bool, so any non-empty string sets it.
|
||||
json::Value params;
|
||||
params[jss::account] = gw1.human();
|
||||
auto const all = env.rpc("json", "account_lines", to_string(params));
|
||||
params[jss::ignore_default] = true;
|
||||
auto const asBool = env.rpc("json", "account_lines", to_string(params));
|
||||
params[jss::ignore_default] = "false";
|
||||
auto const asString = env.rpc("json", "account_lines", to_string(params));
|
||||
BEAST_EXPECT(all[jss::result][jss::lines].size() == kLinesPerGateway);
|
||||
BEAST_EXPECT(asBool[jss::result][jss::lines].size() == 0);
|
||||
BEAST_EXPECT(!asString[jss::result].isMember(jss::error));
|
||||
BEAST_EXPECT(asString[jss::result][jss::lines] == asBool[jss::result][jss::lines]);
|
||||
}
|
||||
{
|
||||
// A negative limit should fail.
|
||||
@@ -350,8 +278,6 @@ public:
|
||||
params[jss::account] = alice.human();
|
||||
params[jss::limit] = -1;
|
||||
auto const lines = env.rpc("json", "account_lines", to_string(params));
|
||||
BEAST_EXPECT(lines[jss::result][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(lines[jss::result][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
lines[jss::result][jss::error_message] ==
|
||||
rpc::expectedFieldMessage(jss::limit, "unsigned integer"));
|
||||
@@ -372,7 +298,7 @@ public:
|
||||
paramsB[jss::marker] = marker;
|
||||
auto const linesB = env.rpc("json", "account_lines", to_string(paramsB));
|
||||
BEAST_EXPECT(linesB[jss::result][jss::lines].isArray());
|
||||
BEAST_EXPECT(linesB[jss::result][jss::lines].size() == kAllLines - 1);
|
||||
BEAST_EXPECT(linesB[jss::result][jss::lines].size() == 51);
|
||||
|
||||
// Go again from where the marker left off, but set a limit of 3.
|
||||
json::Value paramsC;
|
||||
@@ -389,8 +315,6 @@ public:
|
||||
paramsD[jss::account] = alice.human();
|
||||
paramsD[jss::marker] = marker;
|
||||
auto const linesD = env.rpc("json", "account_lines", to_string(paramsD));
|
||||
BEAST_EXPECT(linesD[jss::result][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(linesD[jss::result][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
linesD[jss::result][jss::error_message] ==
|
||||
rpc::makeError(RpcInvalidParams)[jss::error_message]);
|
||||
@@ -401,46 +325,10 @@ public:
|
||||
params[jss::account] = alice.human();
|
||||
params[jss::marker] = true;
|
||||
auto const lines = env.rpc("json", "account_lines", to_string(params));
|
||||
BEAST_EXPECT(lines[jss::result][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(lines[jss::result][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
lines[jss::result][jss::error_message] ==
|
||||
rpc::expectedFieldMessage(jss::marker, "string"));
|
||||
}
|
||||
{
|
||||
// A marker that does not parse is reported without naming the field.
|
||||
json::Value params;
|
||||
params[jss::account] = alice.human();
|
||||
params[jss::marker] = "NOT_A_MARKER";
|
||||
auto const lines = env.rpc("json", "account_lines", to_string(params));
|
||||
BEAST_EXPECT(lines[jss::result][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(lines[jss::result][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
lines[jss::result][jss::error_message] ==
|
||||
rpc::makeError(RpcInvalidParams)[jss::error_message]);
|
||||
}
|
||||
{
|
||||
// The marker's start hint may have a leading '+', and anything after a second
|
||||
// comma is ignored.
|
||||
json::Value params;
|
||||
params[jss::account] = alice.human();
|
||||
params[jss::limit] = 1;
|
||||
auto const first = env.rpc("json", "account_lines", to_string(params));
|
||||
auto const marker = first[jss::result][jss::marker].asString();
|
||||
auto const comma = marker.find(',');
|
||||
BEAST_EXPECT(comma != std::string::npos);
|
||||
|
||||
json::Value next;
|
||||
next[jss::account] = alice.human();
|
||||
next[jss::marker] = marker;
|
||||
auto const plain = env.rpc("json", "account_lines", to_string(next));
|
||||
next[jss::marker] =
|
||||
marker.substr(0, comma) + ",+" + marker.substr(comma + 1) + ",ignored";
|
||||
auto const lines = env.rpc("json", "account_lines", to_string(next));
|
||||
BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
|
||||
BEAST_EXPECT(lines[jss::result][jss::lines].size() == kAllLines - 1);
|
||||
BEAST_EXPECT(lines[jss::result][jss::lines] == plain[jss::result][jss::lines]);
|
||||
}
|
||||
{
|
||||
// Check that the flags we expect from alice to gw2 are present.
|
||||
json::Value params;
|
||||
@@ -558,11 +446,7 @@ public:
|
||||
beckyLinesParams[jss::account] = becky.human();
|
||||
beckyLinesParams[jss::marker] = aliceMarker;
|
||||
auto const beckyLines = env.rpc("json", "account_lines", to_string(beckyLinesParams));
|
||||
BEAST_EXPECT(beckyLines[jss::result][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(beckyLines[jss::result][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
beckyLines[jss::result][jss::error_message] ==
|
||||
rpc::makeError(RpcInvalidParams)[jss::error_message]);
|
||||
BEAST_EXPECT(beckyLines[jss::result].isMember(jss::error_message));
|
||||
}
|
||||
|
||||
void
|
||||
@@ -630,8 +514,6 @@ public:
|
||||
linesEndParams[jss::account] = alice.human();
|
||||
linesEndParams[jss::marker] = linesBeg[jss::result][jss::marker];
|
||||
auto const linesEnd = env.rpc("json", "account_lines", to_string(linesEndParams));
|
||||
BEAST_EXPECT(linesEnd[jss::result][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(linesEnd[jss::result][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
linesEnd[jss::result][jss::error_message] ==
|
||||
rpc::makeError(RpcInvalidParams)[jss::error_message]);
|
||||
@@ -929,8 +811,6 @@ public:
|
||||
request[jss::ripplerpc] = "2.0";
|
||||
request[jss::id] = 5;
|
||||
auto const lines = env.rpc("json2", to_string(request));
|
||||
BEAST_EXPECT(lines[jss::error][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(lines[jss::error][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
lines[jss::error][jss::message] ==
|
||||
rpc::missingFieldError(jss::account)[jss::error_message]);
|
||||
@@ -949,8 +829,6 @@ public:
|
||||
request[jss::id] = 5;
|
||||
request[jss::params] = params;
|
||||
auto const lines = env.rpc("json2", to_string(request));
|
||||
BEAST_EXPECT(lines[jss::error][jss::error] == "actMalformed");
|
||||
BEAST_EXPECT(lines[jss::error][jss::error_code] == RpcActMalformed);
|
||||
BEAST_EXPECT(
|
||||
lines[jss::error][jss::message] ==
|
||||
rpc::makeError(RpcActMalformed)[jss::error_message]);
|
||||
@@ -970,8 +848,6 @@ public:
|
||||
request[jss::id] = 5;
|
||||
request[jss::params] = params;
|
||||
auto const lines = env.rpc("json2", to_string(request));
|
||||
BEAST_EXPECT(lines[jss::error][jss::error] == "actNotFound");
|
||||
BEAST_EXPECT(lines[jss::error][jss::error_code] == RpcActNotFound);
|
||||
BEAST_EXPECT(
|
||||
lines[jss::error][jss::message] ==
|
||||
rpc::makeError(RpcActNotFound)[jss::error_message]);
|
||||
@@ -1000,7 +876,6 @@ public:
|
||||
|
||||
auto const lines = env.rpc("json2", to_string(request));
|
||||
BEAST_EXPECT(lines[jss::error][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(lines[jss::error][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(lines[jss::error][jss::message] == "Invalid field 'peer'.");
|
||||
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
|
||||
@@ -1043,8 +918,6 @@ public:
|
||||
request[jss::id] = 5;
|
||||
request[jss::params] = params;
|
||||
auto const lines = env.rpc("json2", to_string(request));
|
||||
BEAST_EXPECT(lines[jss::error][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(lines[jss::error][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
lines[jss::error][jss::message] ==
|
||||
"Invalid field 'ledger_index', not string or number.");
|
||||
@@ -1064,8 +937,6 @@ public:
|
||||
request[jss::id] = 5;
|
||||
request[jss::params] = params;
|
||||
auto const lines = env.rpc("json2", to_string(request));
|
||||
BEAST_EXPECT(lines[jss::error][jss::error] == "lgrNotFound");
|
||||
BEAST_EXPECT(lines[jss::error][jss::error_code] == RpcLgrNotFound);
|
||||
BEAST_EXPECT(lines[jss::error][jss::message] == "ledgerNotFound");
|
||||
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
|
||||
@@ -1184,7 +1055,6 @@ public:
|
||||
request[jss::params] = params;
|
||||
auto const lines = env.rpc("json2", to_string(request));
|
||||
BEAST_EXPECT(lines[jss::error][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(lines[jss::error][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
lines[jss::error][jss::message] ==
|
||||
"Exactly one of 'ledger_hash' or 'ledger_index' can be "
|
||||
@@ -1240,8 +1110,6 @@ public:
|
||||
request[jss::id] = 5;
|
||||
request[jss::params] = params;
|
||||
auto const lines = env.rpc("json2", to_string(request));
|
||||
BEAST_EXPECT(lines[jss::error][jss::error] == "actMalformed");
|
||||
BEAST_EXPECT(lines[jss::error][jss::error_code] == RpcActMalformed);
|
||||
BEAST_EXPECT(
|
||||
lines[jss::error][jss::message] ==
|
||||
rpc::makeError(RpcActMalformed)[jss::error_message]);
|
||||
@@ -1261,8 +1129,6 @@ public:
|
||||
request[jss::id] = 5;
|
||||
request[jss::params] = params;
|
||||
auto const lines = env.rpc("json2", to_string(request));
|
||||
BEAST_EXPECT(lines[jss::error][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(lines[jss::error][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
lines[jss::error][jss::message] ==
|
||||
rpc::expectedFieldMessage(jss::limit, "unsigned integer"));
|
||||
@@ -1336,8 +1202,6 @@ public:
|
||||
requestD[jss::id] = 5;
|
||||
requestD[jss::params] = paramsD;
|
||||
auto const linesD = env.rpc("json2", to_string(requestD));
|
||||
BEAST_EXPECT(linesD[jss::error][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(linesD[jss::error][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
linesD[jss::error][jss::message] ==
|
||||
rpc::makeError(RpcInvalidParams)[jss::error_message]);
|
||||
@@ -1357,8 +1221,6 @@ public:
|
||||
request[jss::id] = 5;
|
||||
request[jss::params] = params;
|
||||
auto const lines = env.rpc("json2", to_string(request));
|
||||
BEAST_EXPECT(lines[jss::error][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(lines[jss::error][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
lines[jss::error][jss::message] ==
|
||||
rpc::expectedFieldMessage(jss::marker, "string"));
|
||||
@@ -1516,8 +1378,6 @@ public:
|
||||
linesEndRequest[jss::id] = 5;
|
||||
linesEndRequest[jss::params] = linesEndParams;
|
||||
auto const linesEnd = env.rpc("json2", to_string(linesEndRequest));
|
||||
BEAST_EXPECT(linesEnd[jss::error][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(linesEnd[jss::error][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
linesEnd[jss::error][jss::message] ==
|
||||
rpc::makeError(RpcInvalidParams)[jss::error_message]);
|
||||
|
||||
@@ -140,8 +140,6 @@ public:
|
||||
{
|
||||
json::Value const params;
|
||||
auto resp = env.rpc("json", "account_objects", to_string(params));
|
||||
BEAST_EXPECT(resp[jss::result][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(resp[jss::result][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(resp[jss::result][jss::error_message] == "Missing field 'account'.");
|
||||
}
|
||||
// test account non-string
|
||||
@@ -151,7 +149,6 @@ public:
|
||||
params[jss::account] = param;
|
||||
auto jrr = env.rpc("json", "account_objects", to_string(params))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'account'.");
|
||||
};
|
||||
|
||||
@@ -167,8 +164,6 @@ public:
|
||||
json::Value params;
|
||||
params[jss::account] = "n94JNrQYkDrpt62bbSR7nVEhdyAvcJXRAsjEkFYyqRkh9SUTYEqV";
|
||||
auto resp = env.rpc("json", "account_objects", to_string(params));
|
||||
BEAST_EXPECT(resp[jss::result][jss::error] == "actMalformed");
|
||||
BEAST_EXPECT(resp[jss::result][jss::error_code] == RpcActMalformed);
|
||||
BEAST_EXPECT(resp[jss::result][jss::error_message] == "Account malformed.");
|
||||
}
|
||||
// test error on account that's not in the ledger.
|
||||
@@ -176,8 +171,6 @@ public:
|
||||
json::Value params;
|
||||
params[jss::account] = Account{"bogie"}.human();
|
||||
auto resp = env.rpc("json", "account_objects", to_string(params));
|
||||
BEAST_EXPECT(resp[jss::result][jss::error] == "actNotFound");
|
||||
BEAST_EXPECT(resp[jss::result][jss::error_code] == RpcActNotFound);
|
||||
BEAST_EXPECT(resp[jss::result][jss::error_message] == "Account not found.");
|
||||
}
|
||||
Account const bob{"bob"};
|
||||
@@ -187,8 +180,6 @@ public:
|
||||
params[jss::account] = bob.human();
|
||||
params[jss::ledger_index] = 10;
|
||||
auto resp = env.rpc("json", "account_objects", to_string(params));
|
||||
BEAST_EXPECT(resp[jss::result][jss::error] == "lgrNotFound");
|
||||
BEAST_EXPECT(resp[jss::result][jss::error_code] == RpcLgrNotFound);
|
||||
BEAST_EXPECT(resp[jss::result][jss::error_message] == "ledgerNotFound");
|
||||
}
|
||||
|
||||
@@ -199,8 +190,6 @@ public:
|
||||
params[jss::account] = bob.human();
|
||||
params[jss::type] = 10;
|
||||
auto resp = env.rpc("json", "account_objects", to_string(params));
|
||||
BEAST_EXPECT(resp[jss::result][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(resp[jss::result][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
resp[jss::result][jss::error_message] == "Invalid field 'type', not string.");
|
||||
}
|
||||
@@ -210,8 +199,6 @@ public:
|
||||
params[jss::account] = bob.human();
|
||||
params[jss::type] = "expedited";
|
||||
auto resp = env.rpc("json", "account_objects", to_string(params));
|
||||
BEAST_EXPECT(resp[jss::result][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(resp[jss::result][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(resp[jss::result][jss::error_message] == "Invalid field 'type'.");
|
||||
}
|
||||
// test error on limit -ve
|
||||
@@ -220,8 +207,6 @@ public:
|
||||
params[jss::account] = bob.human();
|
||||
params[jss::limit] = -1;
|
||||
auto resp = env.rpc("json", "account_objects", to_string(params));
|
||||
BEAST_EXPECT(resp[jss::result][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(resp[jss::result][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
resp[jss::result][jss::error_message] ==
|
||||
"Invalid field 'limit', not unsigned integer.");
|
||||
@@ -232,8 +217,6 @@ public:
|
||||
params[jss::account] = bob.human();
|
||||
params[jss::sponsored] = "true";
|
||||
auto resp = env.rpc("json", "account_objects", to_string(params));
|
||||
BEAST_EXPECT(resp[jss::result][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(resp[jss::result][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
resp[jss::result][jss::error_message] == "Invalid field 'sponsored', not boolean.");
|
||||
}
|
||||
@@ -255,39 +238,27 @@ public:
|
||||
std::string mark = to_string(resumeMarker);
|
||||
params[jss::marker] = 10;
|
||||
resp = env.rpc("json", "account_objects", to_string(params));
|
||||
BEAST_EXPECT(resp[jss::result][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(resp[jss::result][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
resp[jss::result][jss::error_message] == "Invalid field 'marker', not string.");
|
||||
|
||||
params[jss::marker] = "This is a string with no comma";
|
||||
resp = env.rpc("json", "account_objects", to_string(params));
|
||||
BEAST_EXPECT(resp[jss::result][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(resp[jss::result][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(resp[jss::result][jss::error_message] == "Invalid field 'marker'.");
|
||||
|
||||
params[jss::marker] = "This string has a comma, but is not hex";
|
||||
resp = env.rpc("json", "account_objects", to_string(params));
|
||||
BEAST_EXPECT(resp[jss::result][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(resp[jss::result][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(resp[jss::result][jss::error_message] == "Invalid field 'marker'.");
|
||||
|
||||
params[jss::marker] = std::string(&mark[1U], 64);
|
||||
resp = env.rpc("json", "account_objects", to_string(params));
|
||||
BEAST_EXPECT(resp[jss::result][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(resp[jss::result][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(resp[jss::result][jss::error_message] == "Invalid field 'marker'.");
|
||||
|
||||
params[jss::marker] = std::string(&mark[1U], 65);
|
||||
resp = env.rpc("json", "account_objects", to_string(params));
|
||||
BEAST_EXPECT(resp[jss::result][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(resp[jss::result][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(resp[jss::result][jss::error_message] == "Invalid field 'marker'.");
|
||||
|
||||
params[jss::marker] = std::string(&mark[1U], 65) + "not hex";
|
||||
resp = env.rpc("json", "account_objects", to_string(params));
|
||||
BEAST_EXPECT(resp[jss::result][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(resp[jss::result][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(resp[jss::result][jss::error_message] == "Invalid field 'marker'.");
|
||||
|
||||
// Should this be an error?
|
||||
@@ -632,8 +603,7 @@ public:
|
||||
|
||||
// Make a lambda that checks if the response has error for invalid type
|
||||
auto const acctObjsTypeIsInvalid = [](json::Value const& resp) {
|
||||
return resp[jss::result][jss::error] == "invalidParams" &&
|
||||
resp[jss::result][jss::error_code] == RpcInvalidParams &&
|
||||
return resp[jss::result].isMember(jss::error) &&
|
||||
resp[jss::result][jss::error_message] == "Invalid field \'type\'.";
|
||||
};
|
||||
|
||||
@@ -1204,9 +1174,7 @@ public:
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[jss::marker] = marker;
|
||||
json::Value const resp = env.rpc("json", "account_nfts", to_string(params));
|
||||
return resp[jss::result][jss::error] == "invalidParams" &&
|
||||
resp[jss::result][jss::error_code] == RpcInvalidParams &&
|
||||
resp[jss::result][jss::error_message] == errorMessage;
|
||||
return resp[jss::result][jss::error_message] == errorMessage;
|
||||
};
|
||||
|
||||
// test an invalid marker that is not a string
|
||||
@@ -1459,9 +1427,7 @@ public:
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[jss::marker] = marker;
|
||||
json::Value const resp = env.rpc("json", "account_objects", to_string(params));
|
||||
return resp[jss::result][jss::error] == "invalidParams" &&
|
||||
resp[jss::result][jss::error_code] == RpcInvalidParams &&
|
||||
resp[jss::result][jss::error_message] == "Invalid field \'marker\'.";
|
||||
return resp[jss::result][jss::error_message] == "Invalid field \'marker\'.";
|
||||
};
|
||||
|
||||
auto const markerStr = marker.asString();
|
||||
|
||||
@@ -173,9 +173,7 @@ public:
|
||||
jvParams[jss::limit] = 0u;
|
||||
auto const jrr =
|
||||
env.rpc("json", "account_offers", jvParams.toStyledString())[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'limit'.");
|
||||
BEAST_EXPECT(jrr.isMember(jss::error_message));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,9 +195,9 @@ public:
|
||||
{
|
||||
// no account field
|
||||
auto const jrr = env.rpc("account_offers");
|
||||
BEAST_EXPECT(jrr[jss::status] == "error");
|
||||
BEAST_EXPECT(jrr[jss::error] == "badSyntax");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcBadSyntax);
|
||||
BEAST_EXPECT(jrr[jss::status] == "error");
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Syntax error.");
|
||||
}
|
||||
|
||||
@@ -228,9 +226,9 @@ public:
|
||||
jvParams[jss::account] = "";
|
||||
auto const jrr =
|
||||
env.rpc("json", "account_offers", jvParams.toStyledString())[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::status] == "error");
|
||||
BEAST_EXPECT(jrr[jss::error] == "actMalformed");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcActMalformed);
|
||||
BEAST_EXPECT(jrr[jss::status] == "error");
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Account malformed.");
|
||||
BEAST_EXPECT(jrr.isMember(jss::ledger_current_index));
|
||||
BEAST_EXPECT(jrr[jss::validated] == false);
|
||||
@@ -239,9 +237,9 @@ public:
|
||||
{
|
||||
// bogus account value
|
||||
auto const jrr = env.rpc("account_offers", Account("bogus").human())[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::status] == "error");
|
||||
BEAST_EXPECT(jrr[jss::error] == "actNotFound");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcActNotFound);
|
||||
BEAST_EXPECT(jrr[jss::status] == "error");
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Account not found.");
|
||||
}
|
||||
|
||||
@@ -252,9 +250,9 @@ public:
|
||||
jvParams[jss::limit] = "0"; // NOT an integer
|
||||
auto const jrr =
|
||||
env.rpc("json", "account_offers", jvParams.toStyledString())[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::status] == "error");
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jrr[jss::status] == "error");
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'limit', not unsigned integer.");
|
||||
}
|
||||
|
||||
@@ -265,9 +263,9 @@ public:
|
||||
jvParams[jss::marker] = "NOT_A_MARKER";
|
||||
auto const jrr =
|
||||
env.rpc("json", "account_offers", jvParams.toStyledString())[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::status] == "error");
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jrr[jss::status] == "error");
|
||||
BEAST_EXPECTS(
|
||||
jrr[jss::error_message] == "Invalid field 'marker'.", jrr.toStyledString());
|
||||
}
|
||||
@@ -279,9 +277,9 @@ public:
|
||||
jvParams[jss::marker] = 1;
|
||||
auto const jrr =
|
||||
env.rpc("json", "account_offers", jvParams.toStyledString())[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::status] == "error");
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jrr[jss::status] == "error");
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'marker', not string.");
|
||||
}
|
||||
|
||||
@@ -292,9 +290,9 @@ public:
|
||||
jvParams[jss::ledger_index] = 10u;
|
||||
auto const jrr =
|
||||
env.rpc("json", "account_offers", jvParams.toStyledString())[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::status] == "error");
|
||||
BEAST_EXPECT(jrr[jss::error] == "lgrNotFound");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcLgrNotFound);
|
||||
BEAST_EXPECT(jrr[jss::status] == "error");
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "ledgerNotFound");
|
||||
}
|
||||
|
||||
|
||||
+62
-129
@@ -195,27 +195,19 @@ class AccountTx_test : public beast::unit_test::Suite
|
||||
(j[jss::result][jss::transactions].size() == 0);
|
||||
};
|
||||
|
||||
auto const isErr = [](json::Value const& j, ErrorCodeI code, std::string const& message) {
|
||||
return j.isMember(jss::result) &&
|
||||
j[jss::result][jss::error] == rpc::getErrorInfo(code).token &&
|
||||
j[jss::result][jss::error_code] == code &&
|
||||
j[jss::result][jss::error_message] == message;
|
||||
auto const isErr = [](json::Value const& j, ErrorCodeI code) {
|
||||
return j.isMember(jss::result) && j[jss::result].isMember(jss::error) &&
|
||||
j[jss::result][jss::error] == rpc::getErrorInfo(code).token;
|
||||
};
|
||||
|
||||
json::Value jParams;
|
||||
jParams[jss::api_version] = apiVersion;
|
||||
|
||||
BEAST_EXPECT(isErr(
|
||||
env.rpc("json", "account_tx", to_string(jParams)),
|
||||
RpcInvalidParams,
|
||||
"Missing field 'account'."));
|
||||
BEAST_EXPECT(isErr(env.rpc("json", "account_tx", to_string(jParams)), RpcInvalidParams));
|
||||
|
||||
jParams[jss::account] = "0xDEADBEEF";
|
||||
|
||||
BEAST_EXPECT(isErr(
|
||||
env.rpc("json", "account_tx", to_string(jParams)),
|
||||
RpcActMalformed,
|
||||
"Account malformed."));
|
||||
BEAST_EXPECT(isErr(env.rpc("json", "account_tx", to_string(jParams)), RpcActMalformed));
|
||||
|
||||
jParams[jss::account] = a1.human();
|
||||
BEAST_EXPECT(hasTxs(env.rpc(apiVersion, "json", "account_tx", to_string(jParams))));
|
||||
@@ -235,10 +227,8 @@ class AccountTx_test : public beast::unit_test::Suite
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(isErr(
|
||||
env.rpc("json", "account_tx", to_string(p)),
|
||||
RpcLgrIdxMalformed,
|
||||
"Ledger index malformed."));
|
||||
BEAST_EXPECT(
|
||||
isErr(env.rpc("json", "account_tx", to_string(p)), RpcLgrIdxMalformed));
|
||||
}
|
||||
|
||||
p[jss::ledger_index_min] = 1;
|
||||
@@ -249,18 +239,15 @@ class AccountTx_test : public beast::unit_test::Suite
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(isErr(
|
||||
env.rpc("json", "account_tx", to_string(p)),
|
||||
RpcLgrIdxMalformed,
|
||||
"Ledger index malformed."));
|
||||
BEAST_EXPECT(
|
||||
isErr(env.rpc("json", "account_tx", to_string(p)), RpcLgrIdxMalformed));
|
||||
}
|
||||
|
||||
p[jss::ledger_index_min] = 2;
|
||||
p[jss::ledger_index_max] = 1;
|
||||
BEAST_EXPECT(isErr(
|
||||
env.rpc("json", "account_tx", to_string(p)),
|
||||
(apiVersion == 1 ? RpcLgrIdxsInvalid : RpcInvalidLgrRange),
|
||||
(apiVersion == 1 ? "Ledger indexes invalid." : "Ledger range is invalid.")));
|
||||
(apiVersion == 1 ? RpcLgrIdxsInvalid : RpcInvalidLgrRange)));
|
||||
}
|
||||
// Ledger index min only
|
||||
{
|
||||
@@ -275,17 +262,14 @@ class AccountTx_test : public beast::unit_test::Suite
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(isErr(
|
||||
env.rpc("json", "account_tx", to_string(p)),
|
||||
RpcLgrIdxMalformed,
|
||||
"Ledger index malformed."));
|
||||
BEAST_EXPECT(
|
||||
isErr(env.rpc("json", "account_tx", to_string(p)), RpcLgrIdxMalformed));
|
||||
}
|
||||
|
||||
p[jss::ledger_index_min] = env.current()->header().seq;
|
||||
BEAST_EXPECT(isErr(
|
||||
env.rpc("json", "account_tx", to_string(p)),
|
||||
(apiVersion == 1 ? RpcLgrIdxsInvalid : RpcInvalidLgrRange),
|
||||
(apiVersion == 1 ? "Ledger indexes invalid." : "Ledger range is invalid.")));
|
||||
(apiVersion == 1 ? RpcLgrIdxsInvalid : RpcInvalidLgrRange)));
|
||||
}
|
||||
|
||||
// Ledger index max only
|
||||
@@ -301,10 +285,8 @@ class AccountTx_test : public beast::unit_test::Suite
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(isErr(
|
||||
env.rpc("json", "account_tx", to_string(p)),
|
||||
RpcLgrIdxMalformed,
|
||||
"Ledger index malformed."));
|
||||
BEAST_EXPECT(
|
||||
isErr(env.rpc("json", "account_tx", to_string(p)), RpcLgrIdxMalformed));
|
||||
}
|
||||
|
||||
p[jss::ledger_index_max] = 3;
|
||||
@@ -328,14 +310,10 @@ class AccountTx_test : public beast::unit_test::Suite
|
||||
BEAST_EXPECT(noTxs(env.rpc("json", "account_tx", to_string(p))));
|
||||
|
||||
p[jss::ledger_index] = env.current()->header().seq;
|
||||
BEAST_EXPECT(isErr(
|
||||
env.rpc("json", "account_tx", to_string(p)),
|
||||
RpcLgrNotValidated,
|
||||
"Ledger not validated."));
|
||||
BEAST_EXPECT(isErr(env.rpc("json", "account_tx", to_string(p)), RpcLgrNotValidated));
|
||||
|
||||
p[jss::ledger_index] = env.current()->header().seq + 1;
|
||||
BEAST_EXPECT(isErr(
|
||||
env.rpc("json", "account_tx", to_string(p)), RpcLgrNotFound, "ledgerNotFound"));
|
||||
BEAST_EXPECT(isErr(env.rpc("json", "account_tx", to_string(p)), RpcLgrNotFound));
|
||||
}
|
||||
|
||||
{
|
||||
@@ -348,16 +326,10 @@ class AccountTx_test : public beast::unit_test::Suite
|
||||
BEAST_EXPECT(hasTxs(env.rpc(apiVersion, "json", "account_tx", to_string(p))));
|
||||
|
||||
p[jss::ledger_index] = "current";
|
||||
BEAST_EXPECT(isErr(
|
||||
env.rpc("json", "account_tx", to_string(p)),
|
||||
RpcLgrNotValidated,
|
||||
"Ledger not validated."));
|
||||
BEAST_EXPECT(isErr(env.rpc("json", "account_tx", to_string(p)), RpcLgrNotValidated));
|
||||
|
||||
p[jss::ledger_index] = "";
|
||||
BEAST_EXPECT(isErr(
|
||||
env.rpc("json", "account_tx", to_string(p)),
|
||||
RpcLgrNotValidated,
|
||||
"Ledger not validated."));
|
||||
BEAST_EXPECT(isErr(env.rpc("json", "account_tx", to_string(p)), RpcLgrNotValidated));
|
||||
}
|
||||
|
||||
// Ledger Hash
|
||||
@@ -388,10 +360,7 @@ class AccountTx_test : public beast::unit_test::Suite
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(isErr(
|
||||
env.rpc("json", "account_tx", to_string(p)),
|
||||
RpcInvalidParams,
|
||||
"invalidParams"));
|
||||
BEAST_EXPECT(isErr(env.rpc("json", "account_tx", to_string(p)), RpcInvalidParams));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,10 +374,8 @@ class AccountTx_test : public beast::unit_test::Suite
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(isErr(
|
||||
env.rpc("json", "account_tx", to_string(p)),
|
||||
RpcLgrIdxMalformed,
|
||||
"Ledger index malformed."));
|
||||
BEAST_EXPECT(
|
||||
isErr(env.rpc("json", "account_tx", to_string(p)), RpcLgrIdxMalformed));
|
||||
}
|
||||
}
|
||||
// test account non-string
|
||||
@@ -418,7 +385,6 @@ class AccountTx_test : public beast::unit_test::Suite
|
||||
params[jss::account] = param;
|
||||
auto jrr = env.rpc("json", "account_tx", to_string(params))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'account'.");
|
||||
};
|
||||
|
||||
@@ -440,10 +406,7 @@ class AccountTx_test : public beast::unit_test::Suite
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(isErr(
|
||||
env.rpc("json", "account_tx", to_string(p)),
|
||||
RpcInvalidParams,
|
||||
"Invalid field 'binary'."));
|
||||
BEAST_EXPECT(isErr(env.rpc("json", "account_tx", to_string(p)), RpcInvalidParams));
|
||||
}
|
||||
|
||||
p[jss::binary] = true;
|
||||
@@ -457,10 +420,7 @@ class AccountTx_test : public beast::unit_test::Suite
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(isErr(
|
||||
env.rpc("json", "account_tx", to_string(p)),
|
||||
RpcInvalidParams,
|
||||
"Invalid field 'forward'."));
|
||||
BEAST_EXPECT(isErr(env.rpc("json", "account_tx", to_string(p)), RpcInvalidParams));
|
||||
}
|
||||
|
||||
p[jss::forward] = false;
|
||||
@@ -473,83 +433,70 @@ class AccountTx_test : public beast::unit_test::Suite
|
||||
|
||||
// Test case: limit = 0 should fail (below minimum)
|
||||
p[jss::limit] = 0;
|
||||
BEAST_EXPECT(isErr(
|
||||
env.rpc("json", "account_tx", to_string(p)),
|
||||
RpcInvalidParams,
|
||||
"Invalid field 'limit'."));
|
||||
BEAST_EXPECT(isErr(env.rpc("json", "account_tx", to_string(p)), RpcInvalidParams));
|
||||
|
||||
// Test case: limit = 1.2 should fail (not an integer)
|
||||
p[jss::limit] = 1.2;
|
||||
BEAST_EXPECT(isErr(
|
||||
env.rpc("json", "account_tx", to_string(p)),
|
||||
RpcInvalidParams,
|
||||
rpc::expectedFieldMessage(jss::limit, "unsigned integer")));
|
||||
BEAST_EXPECT(
|
||||
env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] ==
|
||||
rpc::expectedFieldMessage(jss::limit, "unsigned integer"));
|
||||
|
||||
// Test case: limit = "10" should fail (string instead of integer)
|
||||
p[jss::limit] = "10";
|
||||
BEAST_EXPECT(isErr(
|
||||
env.rpc("json", "account_tx", to_string(p)),
|
||||
RpcInvalidParams,
|
||||
rpc::expectedFieldMessage(jss::limit, "unsigned integer")));
|
||||
BEAST_EXPECT(
|
||||
env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] ==
|
||||
rpc::expectedFieldMessage(jss::limit, "unsigned integer"));
|
||||
|
||||
// Test case: limit = true should fail (boolean instead of integer)
|
||||
p[jss::limit] = true;
|
||||
BEAST_EXPECT(isErr(
|
||||
env.rpc("json", "account_tx", to_string(p)),
|
||||
RpcInvalidParams,
|
||||
rpc::expectedFieldMessage(jss::limit, "unsigned integer")));
|
||||
BEAST_EXPECT(
|
||||
env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] ==
|
||||
rpc::expectedFieldMessage(jss::limit, "unsigned integer"));
|
||||
|
||||
// Test case: limit = false should fail (boolean instead of integer)
|
||||
p[jss::limit] = false;
|
||||
BEAST_EXPECT(isErr(
|
||||
env.rpc("json", "account_tx", to_string(p)),
|
||||
RpcInvalidParams,
|
||||
rpc::expectedFieldMessage(jss::limit, "unsigned integer")));
|
||||
BEAST_EXPECT(
|
||||
env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] ==
|
||||
rpc::expectedFieldMessage(jss::limit, "unsigned integer"));
|
||||
|
||||
// Test case: limit = -1 should fail (negative number)
|
||||
p[jss::limit] = -1;
|
||||
BEAST_EXPECT(isErr(
|
||||
env.rpc("json", "account_tx", to_string(p)),
|
||||
RpcInvalidParams,
|
||||
rpc::expectedFieldMessage(jss::limit, "unsigned integer")));
|
||||
BEAST_EXPECT(
|
||||
env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] ==
|
||||
rpc::expectedFieldMessage(jss::limit, "unsigned integer"));
|
||||
|
||||
// Test case: limit = [] should fail (array instead of integer)
|
||||
p[jss::limit] = json::Value(json::ValueType::Array);
|
||||
BEAST_EXPECT(isErr(
|
||||
env.rpc("json", "account_tx", to_string(p)),
|
||||
RpcInvalidParams,
|
||||
rpc::expectedFieldMessage(jss::limit, "unsigned integer")));
|
||||
BEAST_EXPECT(
|
||||
env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] ==
|
||||
rpc::expectedFieldMessage(jss::limit, "unsigned integer"));
|
||||
|
||||
// Test case: limit = {} should fail (object instead of integer)
|
||||
p[jss::limit] = json::Value(json::ValueType::Object);
|
||||
BEAST_EXPECT(isErr(
|
||||
env.rpc("json", "account_tx", to_string(p)),
|
||||
RpcInvalidParams,
|
||||
rpc::expectedFieldMessage(jss::limit, "unsigned integer")));
|
||||
BEAST_EXPECT(
|
||||
env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] ==
|
||||
rpc::expectedFieldMessage(jss::limit, "unsigned integer"));
|
||||
|
||||
// Test case: limit = "malformed" should fail (malformed string)
|
||||
p[jss::limit] = "malformed";
|
||||
BEAST_EXPECT(isErr(
|
||||
env.rpc("json", "account_tx", to_string(p)),
|
||||
RpcInvalidParams,
|
||||
rpc::expectedFieldMessage(jss::limit, "unsigned integer")));
|
||||
BEAST_EXPECT(
|
||||
env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] ==
|
||||
rpc::expectedFieldMessage(jss::limit, "unsigned integer"));
|
||||
|
||||
// Test case: limit = ["limit"] should fail (array with string)
|
||||
p[jss::limit] = json::Value(json::ValueType::Array);
|
||||
p[jss::limit].append("limit");
|
||||
BEAST_EXPECT(isErr(
|
||||
env.rpc("json", "account_tx", to_string(p)),
|
||||
RpcInvalidParams,
|
||||
rpc::expectedFieldMessage(jss::limit, "unsigned integer")));
|
||||
BEAST_EXPECT(
|
||||
env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] ==
|
||||
rpc::expectedFieldMessage(jss::limit, "unsigned integer"));
|
||||
|
||||
// Test case: limit = {"limit": 10} should fail (object with
|
||||
// property)
|
||||
p[jss::limit] = json::Value(json::ValueType::Object);
|
||||
p[jss::limit][jss::limit] = 10;
|
||||
BEAST_EXPECT(isErr(
|
||||
env.rpc("json", "account_tx", to_string(p)),
|
||||
RpcInvalidParams,
|
||||
rpc::expectedFieldMessage(jss::limit, "unsigned integer")));
|
||||
BEAST_EXPECT(
|
||||
env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] ==
|
||||
rpc::expectedFieldMessage(jss::limit, "unsigned integer"));
|
||||
|
||||
// Test case: limit = 10 should succeed (valid integer)
|
||||
p[jss::limit] = 10;
|
||||
@@ -1038,16 +985,12 @@ class AccountTx_test : public beast::unit_test::Suite
|
||||
};
|
||||
|
||||
auto const checkError = [&](json::Value const& delegateParams,
|
||||
std::string const& errToken,
|
||||
ErrorCodeI code,
|
||||
std::string const& errMessage) {
|
||||
std::string const& errToken) {
|
||||
json::Value params;
|
||||
params[jss::account] = alice.human();
|
||||
params[jss::delegate] = delegateParams;
|
||||
auto res = env.rpc("json", "account_tx", to_string(params));
|
||||
BEAST_EXPECT(res[jss::result][jss::error] == errToken);
|
||||
BEAST_EXPECT(res[jss::result][jss::error_code] == code);
|
||||
BEAST_EXPECT(res[jss::result][jss::error_message] == errMessage);
|
||||
};
|
||||
|
||||
// Filter: Delegatee. Expects TX #2 (Signed by Bob)
|
||||
@@ -1118,27 +1061,27 @@ class AccountTx_test : public beast::unit_test::Suite
|
||||
// "delegate" is not an object (e.g., string)
|
||||
{
|
||||
json::Value const p = "not_an_object";
|
||||
checkError(p, "invalidParams", RpcInvalidParams, "Invalid field 'delegate'.");
|
||||
checkError(p, "invalidParams");
|
||||
}
|
||||
|
||||
// Missing "delegate_filter" inside object
|
||||
{
|
||||
json::Value const p(json::ValueType::Object);
|
||||
checkError(p, "invalidParams", RpcInvalidParams, "Invalid field 'delegate_filter'.");
|
||||
checkError(p, "invalidParams");
|
||||
}
|
||||
|
||||
// "delegate_filter" is not a string (e.g., int)
|
||||
{
|
||||
json::Value p;
|
||||
p[jss::delegate_filter] = 123;
|
||||
checkError(p, "invalidParams", RpcInvalidParams, "Invalid field 'delegate_filter'.");
|
||||
checkError(p, "invalidParams");
|
||||
}
|
||||
|
||||
// "delegate_filter" has invalid value
|
||||
{
|
||||
json::Value p;
|
||||
p[jss::delegate_filter] = "random_string";
|
||||
checkError(p, "invalidParams", RpcInvalidParams, "Invalid field 'delegate_filter'.");
|
||||
checkError(p, "invalidParams");
|
||||
}
|
||||
|
||||
// "counterparty" is not a string
|
||||
@@ -1146,7 +1089,7 @@ class AccountTx_test : public beast::unit_test::Suite
|
||||
json::Value p;
|
||||
p[jss::delegate_filter] = "actor";
|
||||
p[jss::counter_party] = 123;
|
||||
checkError(p, "invalidParams", RpcInvalidParams, "Invalid field 'counter_party'.");
|
||||
checkError(p, "invalidParams");
|
||||
}
|
||||
|
||||
// "counterparty" is malformed base58
|
||||
@@ -1154,7 +1097,7 @@ class AccountTx_test : public beast::unit_test::Suite
|
||||
json::Value p;
|
||||
p[jss::delegate_filter] = "actor";
|
||||
p[jss::counter_party] = "not_an_account";
|
||||
checkError(p, "actMalformed", RpcActMalformed, "Account malformed.");
|
||||
checkError(p, "actMalformed");
|
||||
}
|
||||
|
||||
// Multi-signed non-delegated TX: Alice pays Carol via multi-sig.
|
||||
@@ -1238,11 +1181,6 @@ class AccountTx_test : public beast::unit_test::Suite
|
||||
p[jss::marker] = delegateMarker;
|
||||
auto const r = env.rpc("json", "account_tx", to_string(p));
|
||||
BEAST_EXPECT(r[jss::result][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(r[jss::result][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
r[jss::result][jss::error_message] ==
|
||||
"Do not mix delegate and non-delegate pagination markers in account_tx; "
|
||||
"repeat the same `delegate` object when using a delegate marker.");
|
||||
}
|
||||
|
||||
// Obtain a non-delegate marker; it must not carry the flag.
|
||||
@@ -1267,11 +1205,6 @@ class AccountTx_test : public beast::unit_test::Suite
|
||||
p[jss::marker] = normalMarker;
|
||||
auto const r = env.rpc("json", "account_tx", to_string(p));
|
||||
BEAST_EXPECT(r[jss::result][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(r[jss::result][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
r[jss::result][jss::error_message] ==
|
||||
"Do not mix delegate and non-delegate pagination markers in account_tx; "
|
||||
"repeat the same `delegate` object when using a delegate marker.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/config/Constants.h>
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/KeyType.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
@@ -187,42 +186,32 @@ class AmendmentBlocked_test : public beast::unit_test::Suite
|
||||
|
||||
// ledger_accept
|
||||
jr = env.rpc("ledger_accept")[jss::result];
|
||||
BEAST_EXPECT(jr.isMember(jss::error) && jr[jss::error] == "amendmentBlocked");
|
||||
BEAST_EXPECT(jr[jss::status] == "error");
|
||||
BEAST_EXPECT(jr[jss::error] == "amendmentBlocked");
|
||||
BEAST_EXPECT(jr[jss::error_code] == RpcAmendmentBlocked);
|
||||
BEAST_EXPECT(jr[jss::error_message] == "Amendment blocked, need upgrade.");
|
||||
BEAST_EXPECT(!jr.isMember(jss::warnings));
|
||||
|
||||
// ledger_current
|
||||
jr = env.rpc("ledger_current")[jss::result];
|
||||
BEAST_EXPECT(jr.isMember(jss::error) && jr[jss::error] == "amendmentBlocked");
|
||||
BEAST_EXPECT(jr[jss::status] == "error");
|
||||
BEAST_EXPECT(jr[jss::error] == "amendmentBlocked");
|
||||
BEAST_EXPECT(jr[jss::error_code] == RpcAmendmentBlocked);
|
||||
BEAST_EXPECT(jr[jss::error_message] == "Amendment blocked, need upgrade.");
|
||||
BEAST_EXPECT(!jr.isMember(jss::warnings));
|
||||
|
||||
// owner_info
|
||||
jr = env.rpc("owner_info", alice.human())[jss::result];
|
||||
BEAST_EXPECT(jr.isMember(jss::error) && jr[jss::error] == "amendmentBlocked");
|
||||
BEAST_EXPECT(jr[jss::status] == "error");
|
||||
BEAST_EXPECT(jr[jss::error] == "amendmentBlocked");
|
||||
BEAST_EXPECT(jr[jss::error_code] == RpcAmendmentBlocked);
|
||||
BEAST_EXPECT(jr[jss::error_message] == "Amendment blocked, need upgrade.");
|
||||
BEAST_EXPECT(!jr.isMember(jss::warnings));
|
||||
|
||||
// path_find
|
||||
jr = wsc->invoke("path_find", pfReq)[jss::result];
|
||||
BEAST_EXPECT(jr.isMember(jss::error) && jr[jss::error] == "amendmentBlocked");
|
||||
BEAST_EXPECT(jr[jss::status] == "error");
|
||||
BEAST_EXPECT(jr[jss::error] == "amendmentBlocked");
|
||||
BEAST_EXPECT(jr[jss::error_code] == RpcAmendmentBlocked);
|
||||
BEAST_EXPECT(jr[jss::error_message] == "Amendment blocked, need upgrade.");
|
||||
BEAST_EXPECT(!jr.isMember(jss::warnings));
|
||||
|
||||
// submit
|
||||
jr = env.rpc("submit", strHex(s.slice()))[jss::result];
|
||||
BEAST_EXPECT(jr.isMember(jss::error) && jr[jss::error] == "amendmentBlocked");
|
||||
BEAST_EXPECT(jr[jss::status] == "error");
|
||||
BEAST_EXPECT(jr[jss::error] == "amendmentBlocked");
|
||||
BEAST_EXPECT(jr[jss::error_code] == RpcAmendmentBlocked);
|
||||
BEAST_EXPECT(jr[jss::error_message] == "Amendment blocked, need upgrade.");
|
||||
BEAST_EXPECT(!jr.isMember(jss::warnings));
|
||||
|
||||
// submit_multisigned
|
||||
@@ -232,9 +221,7 @@ class AmendmentBlocked_test : public beast::unit_test::Suite
|
||||
BEAST_EXPECT(jr[jss::status] == "success");
|
||||
msReq[jss::tx_json] = jr[jss::tx_json];
|
||||
jr = env.rpc("json", "submit_multisigned", to_string(msReq))[jss::result];
|
||||
BEAST_EXPECT(jr[jss::error] == "amendmentBlocked");
|
||||
BEAST_EXPECT(jr[jss::error_code] == RpcAmendmentBlocked);
|
||||
BEAST_EXPECT(jr[jss::error_message] == "Amendment blocked, need upgrade.");
|
||||
BEAST_EXPECT(jr.isMember(jss::error) && jr[jss::error] == "amendmentBlocked");
|
||||
BEAST_EXPECT(!jr.isMember(jss::warnings));
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/ledger/Ledger.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
@@ -78,12 +77,8 @@ public:
|
||||
// non-conventional ledger input should throw an error
|
||||
params["ledger"] = "non_conventional_ledger_input";
|
||||
resp = env.rpc("json", "book_changes", to_string(params));
|
||||
BEAST_EXPECT(resp[jss::result][jss::status] == "error");
|
||||
BEAST_EXPECT(resp[jss::result][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(resp[jss::result][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
resp[jss::result][jss::error_message] ==
|
||||
"Invalid field 'ledger', not string or number.");
|
||||
BEAST_EXPECT(resp[jss::result].isMember(jss::error));
|
||||
BEAST_EXPECT(resp[jss::result][jss::status] != "success");
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
@@ -1226,7 +1225,6 @@ public:
|
||||
jvParams[jss::ledger_index] = 10u;
|
||||
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "lgrNotFound");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcLgrNotFound);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "ledgerNotFound");
|
||||
}
|
||||
|
||||
@@ -1235,7 +1233,6 @@ public:
|
||||
jvParams[jss::ledger_index] = "validated";
|
||||
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Missing field 'taker_pays'.");
|
||||
}
|
||||
|
||||
@@ -1245,7 +1242,6 @@ public:
|
||||
jvParams[jss::taker_pays] = json::ValueType::Object;
|
||||
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Missing field 'taker_gets'.");
|
||||
}
|
||||
|
||||
@@ -1256,7 +1252,6 @@ public:
|
||||
jvParams[jss::taker_gets] = json::ValueType::Object;
|
||||
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'taker_pays', not object.");
|
||||
}
|
||||
|
||||
@@ -1267,7 +1262,6 @@ public:
|
||||
jvParams[jss::taker_gets] = "not an object";
|
||||
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'taker_gets', not object.");
|
||||
}
|
||||
|
||||
@@ -1278,7 +1272,6 @@ public:
|
||||
jvParams[jss::taker_gets] = json::ValueType::Object;
|
||||
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Missing field 'taker_pays.currency'.");
|
||||
}
|
||||
|
||||
@@ -1289,7 +1282,6 @@ public:
|
||||
jvParams[jss::taker_gets] = json::ValueType::Object;
|
||||
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
jrr[jss::error_message] == "Invalid field 'taker_pays.currency', not string.");
|
||||
}
|
||||
@@ -1301,7 +1293,6 @@ public:
|
||||
jvParams[jss::taker_gets] = json::ValueType::Object;
|
||||
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Missing field 'taker_gets.currency'.");
|
||||
}
|
||||
|
||||
@@ -1312,7 +1303,6 @@ public:
|
||||
jvParams[jss::taker_gets][jss::currency] = 1;
|
||||
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
jrr[jss::error_message] == "Invalid field 'taker_gets.currency', not string.");
|
||||
}
|
||||
@@ -1324,7 +1314,6 @@ public:
|
||||
jvParams[jss::taker_gets][jss::currency] = "XRP";
|
||||
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "srcCurMalformed");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcSrcCurMalformed);
|
||||
BEAST_EXPECT(
|
||||
jrr[jss::error_message] == "Invalid field 'taker_pays.currency', bad currency.");
|
||||
}
|
||||
@@ -1336,7 +1325,6 @@ public:
|
||||
jvParams[jss::taker_gets][jss::currency] = "NOT_VALID";
|
||||
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "dstAmtMalformed");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcDstAmtMalformed);
|
||||
BEAST_EXPECT(
|
||||
jrr[jss::error_message] == "Invalid field 'taker_gets.currency', bad currency.");
|
||||
}
|
||||
@@ -1349,7 +1337,6 @@ public:
|
||||
jvParams[jss::taker_gets][jss::issuer] = 1;
|
||||
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
jrr[jss::error_message] == "Invalid field 'taker_gets.issuer', not string.");
|
||||
}
|
||||
@@ -1362,7 +1349,6 @@ public:
|
||||
jvParams[jss::taker_gets][jss::currency] = "USD";
|
||||
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
jrr[jss::error_message] == "Invalid field 'taker_pays.issuer', not string.");
|
||||
}
|
||||
@@ -1375,7 +1361,6 @@ public:
|
||||
jvParams[jss::taker_gets][jss::currency] = "USD";
|
||||
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "srcIsrMalformed");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcSrcIsrMalformed);
|
||||
BEAST_EXPECT(
|
||||
jrr[jss::error_message] == "Invalid field 'taker_pays.issuer', bad issuer.");
|
||||
}
|
||||
@@ -1388,7 +1373,6 @@ public:
|
||||
jvParams[jss::taker_gets][jss::currency] = "USD";
|
||||
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "srcIsrMalformed");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcSrcIsrMalformed);
|
||||
BEAST_EXPECT(
|
||||
jrr[jss::error_message] ==
|
||||
"Invalid field 'taker_pays.issuer', bad issuer account one.");
|
||||
@@ -1402,7 +1386,6 @@ public:
|
||||
jvParams[jss::taker_gets][jss::issuer] = gw.human() + "DEAD";
|
||||
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "dstIsrMalformed");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcDstIsrMalformed);
|
||||
BEAST_EXPECT(
|
||||
jrr[jss::error_message] == "Invalid field 'taker_gets.issuer', bad issuer.");
|
||||
}
|
||||
@@ -1415,7 +1398,6 @@ public:
|
||||
jvParams[jss::taker_gets][jss::issuer] = toBase58(noAccount());
|
||||
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "dstIsrMalformed");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcDstIsrMalformed);
|
||||
BEAST_EXPECT(
|
||||
jrr[jss::error_message] ==
|
||||
"Invalid field 'taker_gets.issuer', bad issuer account one.");
|
||||
@@ -1430,7 +1412,6 @@ public:
|
||||
jvParams[jss::taker_gets][jss::issuer] = gw.human();
|
||||
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "srcIsrMalformed");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcSrcIsrMalformed);
|
||||
BEAST_EXPECT(
|
||||
jrr[jss::error_message] ==
|
||||
"Unneeded field 'taker_pays.issuer' "
|
||||
@@ -1446,7 +1427,6 @@ public:
|
||||
jvParams[jss::taker_gets][jss::issuer] = gw.human();
|
||||
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "srcIsrMalformed");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcSrcIsrMalformed);
|
||||
BEAST_EXPECT(
|
||||
jrr[jss::error_message] ==
|
||||
"Invalid field 'taker_pays.issuer', expected non-XRP issuer.");
|
||||
@@ -1461,7 +1441,6 @@ public:
|
||||
jvParams[jss::taker_gets][jss::issuer] = gw.human();
|
||||
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'taker', not string.");
|
||||
}
|
||||
|
||||
@@ -1474,7 +1453,6 @@ public:
|
||||
jvParams[jss::taker_gets][jss::issuer] = gw.human();
|
||||
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'taker'.");
|
||||
}
|
||||
|
||||
@@ -1488,7 +1466,6 @@ public:
|
||||
jvParams[jss::taker_gets][jss::issuer] = gw.human();
|
||||
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "badMarket");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcBadMarket);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "No such market.");
|
||||
}
|
||||
|
||||
@@ -1502,7 +1479,6 @@ public:
|
||||
jvParams[jss::taker_gets][jss::issuer] = gw.human();
|
||||
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'limit', not unsigned integer.");
|
||||
}
|
||||
|
||||
@@ -1516,7 +1492,6 @@ public:
|
||||
jvParams[jss::taker_gets][jss::issuer] = gw.human();
|
||||
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'limit'.");
|
||||
}
|
||||
|
||||
@@ -1528,7 +1503,6 @@ public:
|
||||
jvParams[jss::taker_gets][jss::currency] = "USD";
|
||||
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "dstIsrMalformed");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcDstIsrMalformed);
|
||||
BEAST_EXPECT(
|
||||
jrr[jss::error_message] ==
|
||||
"Invalid field 'taker_gets.issuer', "
|
||||
@@ -1544,7 +1518,6 @@ public:
|
||||
jvParams[jss::taker_gets][jss::issuer] = gw.human();
|
||||
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "dstIsrMalformed");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcDstIsrMalformed);
|
||||
BEAST_EXPECT(
|
||||
jrr[jss::error_message] ==
|
||||
"Unneeded field 'taker_gets.issuer' "
|
||||
@@ -1560,7 +1533,6 @@ public:
|
||||
jvParams[jss::domain] = "badString";
|
||||
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "domainMalformed");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcDomainMalformed);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Unable to parse domain.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
#include <test/jtx/Env.h>
|
||||
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
namespace xrpl {
|
||||
@@ -23,8 +22,8 @@ class Connect_test : public beast::unit_test::Suite
|
||||
|
||||
auto const result = env.rpc("json", "connect", "{}");
|
||||
BEAST_EXPECT(result[jss::result][jss::status] == "error");
|
||||
BEAST_EXPECT(result[jss::result].isMember(jss::error));
|
||||
BEAST_EXPECT(result[jss::result][jss::error] == "notSynced");
|
||||
BEAST_EXPECT(result[jss::result][jss::error_code] == RpcNotSynced);
|
||||
BEAST_EXPECT(result[jss::result][jss::error_message] == "Not synced to the network.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Protocol.h> // IWYU pragma: keep
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
@@ -155,16 +154,12 @@ public:
|
||||
Account const becky{"becky"};
|
||||
|
||||
// Lambda that checks the (error) result of deposit_authorized.
|
||||
auto const verifyErr = [this](
|
||||
json::Value const& result,
|
||||
char const* error,
|
||||
ErrorCodeI code,
|
||||
char const* errorMsg) {
|
||||
BEAST_EXPECT(result[jss::result][jss::status] == jss::error);
|
||||
BEAST_EXPECT(result[jss::result][jss::error] == error);
|
||||
BEAST_EXPECT(result[jss::result][jss::error_code] == code);
|
||||
BEAST_EXPECT(result[jss::result][jss::error_message] == errorMsg);
|
||||
};
|
||||
auto const verifyErr =
|
||||
[this](json::Value const& result, char const* error, char const* errorMsg) {
|
||||
BEAST_EXPECT(result[jss::result][jss::status] == jss::error);
|
||||
BEAST_EXPECT(result[jss::result][jss::error] == error);
|
||||
BEAST_EXPECT(result[jss::result][jss::error_message] == errorMsg);
|
||||
};
|
||||
|
||||
Env env(*this);
|
||||
{
|
||||
@@ -172,33 +167,28 @@ public:
|
||||
json::Value args{depositAuthArgs(alice, becky)};
|
||||
args.removeMember(jss::source_account);
|
||||
json::Value const result{env.rpc("json", "deposit_authorized", args.toStyledString())};
|
||||
verifyErr(result, "invalidParams", RpcInvalidParams, "Missing field 'source_account'.");
|
||||
verifyErr(result, "invalidParams", "Missing field 'source_account'.");
|
||||
}
|
||||
{
|
||||
// Non-string source_account field.
|
||||
json::Value args{depositAuthArgs(alice, becky)};
|
||||
args[jss::source_account] = 7.3;
|
||||
json::Value const result{env.rpc("json", "deposit_authorized", args.toStyledString())};
|
||||
verifyErr(
|
||||
result,
|
||||
"invalidParams",
|
||||
RpcInvalidParams,
|
||||
"Invalid field 'source_account', not a string.");
|
||||
verifyErr(result, "invalidParams", "Invalid field 'source_account', not a string.");
|
||||
}
|
||||
{
|
||||
// Corrupt source_account field.
|
||||
json::Value args{depositAuthArgs(alice, becky)};
|
||||
args[jss::source_account] = "rG1QQv2nh2gr7RCZ!P8YYcBUKCCN633jCn";
|
||||
json::Value const result{env.rpc("json", "deposit_authorized", args.toStyledString())};
|
||||
verifyErr(result, "actMalformed", RpcActMalformed, "Account malformed.");
|
||||
verifyErr(result, "actMalformed", "Account malformed.");
|
||||
}
|
||||
{
|
||||
// Missing destination_account field.
|
||||
json::Value args{depositAuthArgs(alice, becky)};
|
||||
args.removeMember(jss::destination_account);
|
||||
json::Value const result{env.rpc("json", "deposit_authorized", args.toStyledString())};
|
||||
verifyErr(
|
||||
result, "invalidParams", RpcInvalidParams, "Missing field 'destination_account'.");
|
||||
verifyErr(result, "invalidParams", "Missing field 'destination_account'.");
|
||||
}
|
||||
{
|
||||
// Non-string destination_account field.
|
||||
@@ -206,46 +196,40 @@ public:
|
||||
args[jss::destination_account] = 7.3;
|
||||
json::Value const result{env.rpc("json", "deposit_authorized", args.toStyledString())};
|
||||
verifyErr(
|
||||
result,
|
||||
"invalidParams",
|
||||
RpcInvalidParams,
|
||||
"Invalid field 'destination_account', not a string.");
|
||||
result, "invalidParams", "Invalid field 'destination_account', not a string.");
|
||||
}
|
||||
{
|
||||
// Corrupt destination_account field.
|
||||
json::Value args{depositAuthArgs(alice, becky)};
|
||||
args[jss::destination_account] = "rP6P9ypfAmc!pw8SZHNwM4nvZHFXDraQas";
|
||||
json::Value const result{env.rpc("json", "deposit_authorized", args.toStyledString())};
|
||||
verifyErr(result, "actMalformed", RpcActMalformed, "Account malformed.");
|
||||
verifyErr(result, "actMalformed", "Account malformed.");
|
||||
}
|
||||
{
|
||||
// Request an invalid ledger.
|
||||
json::Value const args{depositAuthArgs(alice, becky, "-1")};
|
||||
json::Value const result{env.rpc("json", "deposit_authorized", args.toStyledString())};
|
||||
verifyErr(
|
||||
result,
|
||||
"invalidParams",
|
||||
RpcInvalidParams,
|
||||
"Invalid field 'ledger_index', not string or number.");
|
||||
result, "invalidParams", "Invalid field 'ledger_index', not string or number.");
|
||||
}
|
||||
{
|
||||
// Request a ledger that doesn't exist yet as a string.
|
||||
json::Value const args{depositAuthArgs(alice, becky, "17")};
|
||||
json::Value const result{env.rpc("json", "deposit_authorized", args.toStyledString())};
|
||||
verifyErr(result, "lgrNotFound", RpcLgrNotFound, "ledgerNotFound");
|
||||
verifyErr(result, "lgrNotFound", "ledgerNotFound");
|
||||
}
|
||||
{
|
||||
// Request a ledger that doesn't exist yet.
|
||||
json::Value args{depositAuthArgs(alice, becky)};
|
||||
args[jss::ledger_index] = 17;
|
||||
json::Value const result{env.rpc("json", "deposit_authorized", args.toStyledString())};
|
||||
verifyErr(result, "lgrNotFound", RpcLgrNotFound, "ledgerNotFound");
|
||||
verifyErr(result, "lgrNotFound", "ledgerNotFound");
|
||||
}
|
||||
{
|
||||
// alice is not yet funded.
|
||||
json::Value const args{depositAuthArgs(alice, becky)};
|
||||
json::Value const result{env.rpc("json", "deposit_authorized", args.toStyledString())};
|
||||
verifyErr(result, "srcActNotFound", RpcSrcActNotFound, "Source account not found.");
|
||||
verifyErr(result, "srcActNotFound", "Source account not found.");
|
||||
}
|
||||
env.fund(XRP(1000), alice);
|
||||
env.close();
|
||||
@@ -253,8 +237,7 @@ public:
|
||||
// becky is not yet funded.
|
||||
json::Value const args{depositAuthArgs(alice, becky)};
|
||||
json::Value const result{env.rpc("json", "deposit_authorized", args.toStyledString())};
|
||||
verifyErr(
|
||||
result, "dstActNotFound", RpcDstActNotFound, "Destination account not found.");
|
||||
verifyErr(result, "dstActNotFound", "Destination account not found.");
|
||||
}
|
||||
env.fund(XRP(1000), becky);
|
||||
env.close();
|
||||
@@ -273,11 +256,9 @@ public:
|
||||
jtx::Account const& dst,
|
||||
bool authorized,
|
||||
std::vector<std::string> credentialIDs = {},
|
||||
std::string_view error = "",
|
||||
ErrorCodeI code = RpcSuccess,
|
||||
std::string_view errorMessage = "")
|
||||
std::string_view error = "")
|
||||
{
|
||||
BEAST_EXPECT(result[jss::status] == (authorized ? jss::success : jss::error));
|
||||
BEAST_EXPECT(result[jss::status] == authorized ? jss::success : jss::error);
|
||||
if (result.isMember(jss::deposit_authorized))
|
||||
BEAST_EXPECT(result[jss::deposit_authorized] == authorized);
|
||||
if (authorized)
|
||||
@@ -289,11 +270,7 @@ public:
|
||||
|
||||
BEAST_EXPECT(result.isMember(jss::error) == !error.empty());
|
||||
if (!error.empty())
|
||||
{
|
||||
BEAST_EXPECT(result[jss::error].asString() == error);
|
||||
BEAST_EXPECT(result[jss::error_code] == code);
|
||||
BEAST_EXPECT(result[jss::error_message].asString() == errorMessage);
|
||||
}
|
||||
|
||||
if (authorized)
|
||||
{
|
||||
@@ -357,15 +334,7 @@ public:
|
||||
args[jss::credentials] = json::ValueType::Array;
|
||||
|
||||
auto const jv = env.rpc("json", "deposit_authorized", args.toStyledString());
|
||||
checkCredentialsResponse(
|
||||
jv[jss::result],
|
||||
alice,
|
||||
becky,
|
||||
false,
|
||||
{},
|
||||
"invalidParams",
|
||||
RpcInvalidParams,
|
||||
"Invalid field 'credentials', not is non-empty array of CredentialID(hash256).");
|
||||
checkCredentialsResponse(jv[jss::result], alice, becky, false, {}, "invalidParams");
|
||||
}
|
||||
|
||||
{
|
||||
@@ -379,15 +348,7 @@ public:
|
||||
args[jss::credentials].append(3);
|
||||
|
||||
auto const jv = env.rpc("json", "deposit_authorized", args.toStyledString());
|
||||
checkCredentialsResponse(
|
||||
jv[jss::result],
|
||||
alice,
|
||||
becky,
|
||||
false,
|
||||
{},
|
||||
"invalidParams",
|
||||
RpcInvalidParams,
|
||||
"Invalid field 'credentials', not an array of CredentialID(hash256).");
|
||||
checkCredentialsResponse(jv[jss::result], alice, becky, false, {}, "invalidParams");
|
||||
}
|
||||
|
||||
{
|
||||
@@ -401,14 +362,7 @@ public:
|
||||
|
||||
auto const jv = env.rpc("json", "deposit_authorized", args.toStyledString());
|
||||
checkCredentialsResponse(
|
||||
jv[jss::result],
|
||||
alice,
|
||||
becky,
|
||||
false,
|
||||
{"hello world"},
|
||||
"invalidParams",
|
||||
RpcInvalidParams,
|
||||
"Invalid field 'credentials', not an array of CredentialID(hash256).");
|
||||
jv[jss::result], alice, becky, false, {"hello world"}, "invalidParams");
|
||||
}
|
||||
|
||||
{
|
||||
@@ -435,9 +389,7 @@ public:
|
||||
"0127AB8B4B29CCDBB61AA51C0799A8A6BB80B86A9899807C11ED576AF8516"
|
||||
"473",
|
||||
},
|
||||
"badCredentials",
|
||||
RpcBadCredentials,
|
||||
"credentials don't exist");
|
||||
"badCredentials");
|
||||
}
|
||||
|
||||
{
|
||||
@@ -451,14 +403,7 @@ public:
|
||||
|
||||
auto const jv = env.rpc("json", "deposit_authorized", args.toStyledString());
|
||||
checkCredentialsResponse(
|
||||
jv[jss::result],
|
||||
alice,
|
||||
becky,
|
||||
false,
|
||||
{zero},
|
||||
"badCredentials",
|
||||
RpcBadCredentials,
|
||||
"credentials don't exist");
|
||||
jv[jss::result], alice, becky, false, {zero}, "badCredentials");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -471,14 +416,7 @@ public:
|
||||
"deposit_authorized",
|
||||
depositAuthArgs(alice, becky, "validated", {credIdx}).toStyledString());
|
||||
checkCredentialsResponse(
|
||||
jv[jss::result],
|
||||
alice,
|
||||
becky,
|
||||
false,
|
||||
{credIdx},
|
||||
"badCredentials",
|
||||
RpcBadCredentials,
|
||||
"credentials aren't accepted");
|
||||
jv[jss::result], alice, becky, false, {credIdx}, "badCredentials");
|
||||
}
|
||||
|
||||
// alice accept credentials
|
||||
@@ -492,14 +430,7 @@ public:
|
||||
"deposit_authorized",
|
||||
depositAuthArgs(alice, becky, "validated", {credIdx, credIdx}).toStyledString());
|
||||
checkCredentialsResponse(
|
||||
jv[jss::result],
|
||||
alice,
|
||||
becky,
|
||||
false,
|
||||
{credIdx, credIdx},
|
||||
"badCredentials",
|
||||
RpcBadCredentials,
|
||||
"duplicates in credentials");
|
||||
jv[jss::result], alice, becky, false, {credIdx, credIdx}, "badCredentials");
|
||||
}
|
||||
|
||||
{
|
||||
@@ -522,14 +453,7 @@ public:
|
||||
"deposit_authorized",
|
||||
depositAuthArgs(alice, becky, "validated", kCredIds).toStyledString());
|
||||
checkCredentialsResponse(
|
||||
jv[jss::result],
|
||||
alice,
|
||||
becky,
|
||||
false,
|
||||
kCredIds,
|
||||
"invalidParams",
|
||||
RpcInvalidParams,
|
||||
"Invalid field 'credentials', not array too long.");
|
||||
jv[jss::result], alice, becky, false, kCredIds, "invalidParams");
|
||||
}
|
||||
|
||||
{
|
||||
@@ -576,14 +500,7 @@ public:
|
||||
"deposit_authorized",
|
||||
depositAuthArgs(becky, alice, "validated", {credDiana}).toStyledString());
|
||||
checkCredentialsResponse(
|
||||
jv[jss::result],
|
||||
becky,
|
||||
alice,
|
||||
false,
|
||||
{credDiana},
|
||||
"badCredentials",
|
||||
RpcBadCredentials,
|
||||
"credentials doesn't belong to the root account");
|
||||
jv[jss::result], becky, alice, false, {credDiana}, "badCredentials");
|
||||
}
|
||||
|
||||
{
|
||||
@@ -632,14 +549,7 @@ public:
|
||||
depositAuthArgs(alice, becky, "validated", {credIdx2}).toStyledString());
|
||||
|
||||
checkCredentialsResponse(
|
||||
jv[jss::result],
|
||||
alice,
|
||||
becky,
|
||||
false,
|
||||
{credIdx2},
|
||||
"badCredentials",
|
||||
RpcBadCredentials,
|
||||
"credentials are expired");
|
||||
jv[jss::result], alice, becky, false, {credIdx2}, "badCredentials");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/ledger/AmendmentTable.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/digest.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
@@ -207,7 +206,6 @@ class Feature_test : public beast::unit_test::Suite
|
||||
// feature names are case-sensitive - expect error here
|
||||
jrr = env.rpc("feature", "fMM")[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "badFeature");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcBadFeature);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Feature unknown or invalid.");
|
||||
|
||||
// Test feature name size checks
|
||||
@@ -255,7 +253,6 @@ class Feature_test : public beast::unit_test::Suite
|
||||
params[jss::feature] = param;
|
||||
auto jrr = env.rpc("json", "feature", to_string(params))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Invalid parameters.");
|
||||
};
|
||||
|
||||
@@ -269,7 +266,6 @@ class Feature_test : public beast::unit_test::Suite
|
||||
{
|
||||
auto jrr = env.rpc("feature", "AllTheThings")[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "badFeature");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcBadFeature);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Feature unknown or invalid.");
|
||||
}
|
||||
}
|
||||
@@ -324,7 +320,6 @@ class Feature_test : public beast::unit_test::Suite
|
||||
"EF";
|
||||
auto const result = env.rpc("json", "feature", to_string(params))[jss::result];
|
||||
BEAST_EXPECTS(result[jss::error] == "badFeature", result.toStyledString());
|
||||
BEAST_EXPECT(result[jss::error_code] == RpcBadFeature);
|
||||
BEAST_EXPECT(result[jss::error_message] == "Feature unknown or invalid.");
|
||||
}
|
||||
|
||||
@@ -337,7 +332,6 @@ class Feature_test : public beast::unit_test::Suite
|
||||
params[jss::vetoed] = true;
|
||||
auto const result = env.rpc("json", "feature", to_string(params))[jss::result];
|
||||
BEAST_EXPECTS(result[jss::error] == "noPermission", result[jss::error].asString());
|
||||
BEAST_EXPECT(result[jss::error_code] == RpcNoPermission);
|
||||
BEAST_EXPECT(
|
||||
result[jss::error_message] == "You don't have permission for this command.");
|
||||
}
|
||||
@@ -519,7 +513,6 @@ class Feature_test : public beast::unit_test::Suite
|
||||
|
||||
auto const result = env.rpc("json", "feature", to_string(params))[jss::result];
|
||||
BEAST_EXPECT(result[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(result[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(result[jss::error_message] == "Invalid parameters.");
|
||||
};
|
||||
|
||||
@@ -528,7 +521,6 @@ class Feature_test : public beast::unit_test::Suite
|
||||
// anything other than accept or reject is an error
|
||||
jrr = env.rpc("feature", kFeatureName, "maybe");
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Invalid parameters.");
|
||||
}
|
||||
|
||||
@@ -592,7 +584,6 @@ class Feature_test : public beast::unit_test::Suite
|
||||
// anything other than accept or reject is an error
|
||||
jrr = env.rpc("feature", featureName, "maybe");
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Invalid parameters.");
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/ApiVersion.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
@@ -173,12 +172,7 @@ public:
|
||||
|
||||
auto response = jv[jss::result];
|
||||
auto const error = apiVersion < 2u ? "invalidHotWallet" : "invalidParams";
|
||||
auto const errorCode = apiVersion < 2u ? RpcInvalidHotwallet : RpcInvalidParams;
|
||||
auto const errorMessage =
|
||||
apiVersion < 2u ? "Invalid hotwallet." : "Invalid parameters.";
|
||||
BEAST_EXPECT(response[jss::error] == error);
|
||||
BEAST_EXPECT(response[jss::error_code] == errorCode);
|
||||
BEAST_EXPECT(response[jss::error_message] == errorMessage);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -207,8 +201,6 @@ public:
|
||||
auto jv = wsc->invoke("gateway_balances", qry);
|
||||
expect(jv[jss::status] == "error");
|
||||
BEAST_EXPECT(jv[jss::result][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jv[jss::result][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jv[jss::result][jss::error_message] == "Invalid field 'account'.");
|
||||
});
|
||||
|
||||
// The same applies to a non-string "ident".
|
||||
@@ -220,8 +212,6 @@ public:
|
||||
auto jv = wsc->invoke("gateway_balances", qry2);
|
||||
expect(jv[jss::status] == "error");
|
||||
BEAST_EXPECT(jv[jss::result][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jv[jss::result][jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jv[jss::result][jss::error_message] == "Invalid field 'ident'.");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/OpenView.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
@@ -43,14 +42,10 @@ public:
|
||||
auto const baseFee = env.current()->fees().base;
|
||||
// missing base_asset
|
||||
auto ret = Oracle::aggregatePrice(env, std::nullopt, "USD", kOracles);
|
||||
BEAST_EXPECT(ret[jss::error].asString() == "invalidParams");
|
||||
BEAST_EXPECT(ret[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(ret[jss::error_message].asString() == "Missing field 'base_asset'.");
|
||||
|
||||
// missing quote_asset
|
||||
ret = Oracle::aggregatePrice(env, "XRP", std::nullopt, kOracles);
|
||||
BEAST_EXPECT(ret[jss::error].asString() == "invalidParams");
|
||||
BEAST_EXPECT(ret[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(ret[jss::error_message].asString() == "Missing field 'quote_asset'.");
|
||||
|
||||
// invalid base_asset, quote_asset
|
||||
@@ -74,44 +69,28 @@ public:
|
||||
{
|
||||
ret = Oracle::aggregatePrice(env, "USD", v, kOracles);
|
||||
BEAST_EXPECT(ret[jss::error].asString() == "invalidParams");
|
||||
BEAST_EXPECT(ret[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(ret[jss::error_message].asString() == "Invalid parameters.");
|
||||
ret = Oracle::aggregatePrice(env, v, "USD", kOracles);
|
||||
BEAST_EXPECT(ret[jss::error].asString() == "invalidParams");
|
||||
BEAST_EXPECT(ret[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(ret[jss::error_message].asString() == "Invalid parameters.");
|
||||
ret = Oracle::aggregatePrice(env, v, v, kOracles);
|
||||
BEAST_EXPECT(ret[jss::error].asString() == "invalidParams");
|
||||
BEAST_EXPECT(ret[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(ret[jss::error_message].asString() == "Invalid parameters.");
|
||||
}
|
||||
|
||||
// missing oracles array
|
||||
ret = Oracle::aggregatePrice(env, "XRP", "USD");
|
||||
BEAST_EXPECT(ret[jss::error].asString() == "invalidParams");
|
||||
BEAST_EXPECT(ret[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(ret[jss::error_message].asString() == "Missing field 'oracles'.");
|
||||
|
||||
// empty oracles array
|
||||
ret = Oracle::aggregatePrice(env, "XRP", "USD", OraclesData{});
|
||||
BEAST_EXPECT(ret[jss::error].asString() == "oracleMalformed");
|
||||
BEAST_EXPECT(ret[jss::error_code] == RpcOracleMalformed);
|
||||
BEAST_EXPECT(ret[jss::error_message].asString() == "Oracle request is malformed.");
|
||||
|
||||
// no token pairs found
|
||||
ret = Oracle::aggregatePrice(env, "YAN", "USD", kOracles);
|
||||
BEAST_EXPECT(ret[jss::error].asString() == "objectNotFound");
|
||||
BEAST_EXPECT(ret[jss::error_code] == RpcObjectNotFound);
|
||||
BEAST_EXPECT(
|
||||
ret[jss::error_message].asString() == "The requested object was not found.");
|
||||
|
||||
// invalid oracle document id
|
||||
// id doesn't exist
|
||||
ret = Oracle::aggregatePrice(env, "XRP", "USD", {{{owner, 2}}});
|
||||
BEAST_EXPECT(ret[jss::error].asString() == "objectNotFound");
|
||||
BEAST_EXPECT(ret[jss::error_code] == RpcObjectNotFound);
|
||||
BEAST_EXPECT(
|
||||
ret[jss::error_message].asString() == "The requested object was not found.");
|
||||
// invalid values
|
||||
std::vector<AnyValue> const invalidDocument = {kNoneTag, 1.2, -1, "", "none", "1.2"};
|
||||
for (auto const& v : invalidDocument)
|
||||
@@ -120,26 +99,17 @@ public:
|
||||
json::Value jv;
|
||||
toJson(jv, v);
|
||||
BEAST_EXPECT(ret[jss::error].asString() == "invalidParams");
|
||||
BEAST_EXPECT(ret[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(ret[jss::error_message].asString() == "Invalid parameters.");
|
||||
}
|
||||
// missing document id
|
||||
ret = Oracle::aggregatePrice(env, "XRP", "USD", {{{owner, std::nullopt}}});
|
||||
BEAST_EXPECT(ret[jss::error].asString() == "oracleMalformed");
|
||||
BEAST_EXPECT(ret[jss::error_code] == RpcOracleMalformed);
|
||||
BEAST_EXPECT(ret[jss::error_message].asString() == "Oracle request is malformed.");
|
||||
|
||||
// invalid owner
|
||||
ret = Oracle::aggregatePrice(env, "XRP", "USD", {{{some, 1}}});
|
||||
BEAST_EXPECT(ret[jss::error].asString() == "objectNotFound");
|
||||
BEAST_EXPECT(ret[jss::error_code] == RpcObjectNotFound);
|
||||
BEAST_EXPECT(
|
||||
ret[jss::error_message].asString() == "The requested object was not found.");
|
||||
// missing account
|
||||
ret = Oracle::aggregatePrice(env, "XRP", "USD", {{{std::nullopt, 1}}});
|
||||
BEAST_EXPECT(ret[jss::error].asString() == "oracleMalformed");
|
||||
BEAST_EXPECT(ret[jss::error_code] == RpcOracleMalformed);
|
||||
BEAST_EXPECT(ret[jss::error_message].asString() == "Oracle request is malformed.");
|
||||
|
||||
// oracles have wrong asset pair
|
||||
env.fund(XRP(1'000), owner);
|
||||
@@ -152,9 +122,6 @@ public:
|
||||
});
|
||||
ret = Oracle::aggregatePrice(env, "XRP", "USD", {{{owner, oracle.documentID()}}});
|
||||
BEAST_EXPECT(ret[jss::error].asString() == "objectNotFound");
|
||||
BEAST_EXPECT(ret[jss::error_code] == RpcObjectNotFound);
|
||||
BEAST_EXPECT(
|
||||
ret[jss::error_message].asString() == "The requested object was not found.");
|
||||
|
||||
// invalid trim value
|
||||
std::vector<AnyValue> const invalidTrim = {kNoneTag, 0, 26, -1, 1.2, "", "none", "1.2"};
|
||||
@@ -163,8 +130,6 @@ public:
|
||||
ret =
|
||||
Oracle::aggregatePrice(env, "XRP", "USD", {{{owner, oracle.documentID()}}}, v);
|
||||
BEAST_EXPECT(ret[jss::error].asString() == "invalidParams");
|
||||
BEAST_EXPECT(ret[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(ret[jss::error_message].asString() == "Invalid parameters.");
|
||||
}
|
||||
|
||||
// invalid time threshold value
|
||||
@@ -174,8 +139,6 @@ public:
|
||||
ret = Oracle::aggregatePrice(
|
||||
env, "XRP", "USD", {{{owner, oracle.documentID()}}}, std::nullopt, v);
|
||||
BEAST_EXPECT(ret[jss::error].asString() == "invalidParams");
|
||||
BEAST_EXPECT(ret[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(ret[jss::error_message].asString() == "Invalid parameters.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,8 +157,6 @@ public:
|
||||
}
|
||||
auto const ret = Oracle::aggregatePrice(env, "XRP", "USD", oracles);
|
||||
BEAST_EXPECT(ret[jss::error].asString() == "oracleMalformed");
|
||||
BEAST_EXPECT(ret[jss::error_code] == RpcOracleMalformed);
|
||||
BEAST_EXPECT(ret[jss::error_message].asString() == "Oracle request is malformed.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -496,8 +457,6 @@ public:
|
||||
// a nullptr dereference.
|
||||
auto const ret = Oracle::aggregatePrice(env, "XRP", "USD", oracles);
|
||||
BEAST_EXPECT(ret[jss::error].asString() == "objectNotFound");
|
||||
BEAST_EXPECT(ret[jss::error_code] == RpcObjectNotFound);
|
||||
BEAST_EXPECT(ret[jss::error_message].asString() == "The requested object was not found.");
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
+343
-638
File diff suppressed because it is too large.
Load diff
@@ -209,12 +209,7 @@ public:
|
||||
params[jss::seed_hex] = strings.masterSeedHex;
|
||||
|
||||
// Secret fields are mutually exclusive.
|
||||
auto const result = walletPropose(params);
|
||||
BEAST_EXPECT(result[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(result[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
result[jss::error_message] ==
|
||||
"Exactly one of the following must be specified: passphrase, seed or seed_hex");
|
||||
BEAST_EXPECT(containsError(walletPropose(params)));
|
||||
}
|
||||
|
||||
void
|
||||
@@ -228,8 +223,7 @@ public:
|
||||
params[jss::key_type] = "secp256k1";
|
||||
params[jss::passphrase] = 20160506;
|
||||
auto result = walletPropose(params);
|
||||
BEAST_EXPECT(result[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(result[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(containsError(result));
|
||||
BEAST_EXPECT(result[jss::error_message] == "Invalid field 'passphrase', not string.");
|
||||
}
|
||||
|
||||
@@ -238,8 +232,7 @@ public:
|
||||
params[jss::key_type] = "secp256k1";
|
||||
params[jss::seed] = json::ValueType::Object;
|
||||
auto result = walletPropose(params);
|
||||
BEAST_EXPECT(result[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(result[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(containsError(result));
|
||||
BEAST_EXPECT(result[jss::error_message] == "Invalid field 'seed', not string.");
|
||||
}
|
||||
|
||||
@@ -248,8 +241,7 @@ public:
|
||||
params[jss::key_type] = "ed25519";
|
||||
params[jss::seed_hex] = json::ValueType::Array;
|
||||
auto result = walletPropose(params);
|
||||
BEAST_EXPECT(result[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(result[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(containsError(result));
|
||||
BEAST_EXPECT(result[jss::error_message] == "Invalid field 'seed_hex', not string.");
|
||||
}
|
||||
|
||||
@@ -261,8 +253,7 @@ public:
|
||||
params[jss::seed_hex] = common::gMasterSeedHex;
|
||||
params[jss::seed] = common::gMasterSeed;
|
||||
auto result = walletPropose(params);
|
||||
BEAST_EXPECT(result[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(result[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(containsError(result));
|
||||
BEAST_EXPECT(
|
||||
result[jss::error_message] ==
|
||||
"Exactly one of the following must be specified: passphrase, "
|
||||
@@ -275,8 +266,7 @@ public:
|
||||
params[jss::key_type] = "prime256v1";
|
||||
params[jss::passphrase] = common::gMasterKey;
|
||||
auto result = walletPropose(params);
|
||||
BEAST_EXPECT(result[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(result[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(containsError(result));
|
||||
BEAST_EXPECT(result[jss::error_message] == "Invalid parameters.");
|
||||
}
|
||||
|
||||
@@ -285,8 +275,7 @@ public:
|
||||
params[jss::key_type] = json::ValueType::Object;
|
||||
params[jss::seed_hex] = common::gMasterSeedHex;
|
||||
auto result = walletPropose(params);
|
||||
BEAST_EXPECT(result[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(result[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(containsError(result));
|
||||
BEAST_EXPECT(result[jss::error_message] == "Invalid field 'key_type', not string.");
|
||||
}
|
||||
|
||||
@@ -295,8 +284,7 @@ public:
|
||||
params[jss::key_type] = json::ValueType::Array;
|
||||
params[jss::seed] = common::gMasterSeed;
|
||||
auto result = walletPropose(params);
|
||||
BEAST_EXPECT(result[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(result[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(containsError(result));
|
||||
BEAST_EXPECT(result[jss::error_message] == "Invalid field 'key_type', not string.");
|
||||
}
|
||||
}
|
||||
@@ -414,9 +402,8 @@ public:
|
||||
json::Value error;
|
||||
params[jss::secret] = 314159265;
|
||||
auto const ret = keypairForSignature(params, error);
|
||||
BEAST_EXPECT(containsError(error));
|
||||
BEAST_EXPECT(!ret);
|
||||
BEAST_EXPECT(error[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(error[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(error[jss::error_message] == "Invalid field 'secret', not string.");
|
||||
}
|
||||
|
||||
@@ -427,9 +414,8 @@ public:
|
||||
params[jss::secret].append("array:0");
|
||||
|
||||
auto const ret = keypairForSignature(params, error);
|
||||
BEAST_EXPECT(containsError(error));
|
||||
BEAST_EXPECT(!ret);
|
||||
BEAST_EXPECT(error[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(error[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(error[jss::error_message] == "Invalid field 'secret', not string.");
|
||||
}
|
||||
|
||||
@@ -441,9 +427,8 @@ public:
|
||||
params[jss::secret]["number"] = 702;
|
||||
|
||||
auto const ret = keypairForSignature(params, error);
|
||||
BEAST_EXPECT(containsError(error));
|
||||
BEAST_EXPECT(!ret);
|
||||
BEAST_EXPECT(error[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(error[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(error[jss::error_message] == "Invalid field 'secret', not string.");
|
||||
}
|
||||
|
||||
@@ -455,9 +440,8 @@ public:
|
||||
params[jss::secret] = common::gMasterSeed;
|
||||
|
||||
auto const ret = keypairForSignature(params, error);
|
||||
BEAST_EXPECT(containsError(error));
|
||||
BEAST_EXPECT(!ret);
|
||||
BEAST_EXPECT(error[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(error[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(
|
||||
error[jss::error_message] ==
|
||||
"The secret field is not allowed if key_type is used.");
|
||||
@@ -471,9 +455,8 @@ public:
|
||||
params[jss::passphrase] = common::gMasterKey;
|
||||
|
||||
auto const ret = keypairForSignature(params, error);
|
||||
BEAST_EXPECT(containsError(error));
|
||||
BEAST_EXPECT(!ret);
|
||||
BEAST_EXPECT(error[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(error[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(error[jss::error_message] == "Invalid field 'key_type'.");
|
||||
}
|
||||
|
||||
@@ -484,9 +467,8 @@ public:
|
||||
params[jss::seed_hex] = common::gMasterSeedHex;
|
||||
|
||||
auto const ret = keypairForSignature(params, error);
|
||||
BEAST_EXPECT(containsError(error));
|
||||
BEAST_EXPECT(!ret);
|
||||
BEAST_EXPECT(error[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(error[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(error[jss::error_message] == "Invalid field 'key_type', not string.");
|
||||
}
|
||||
|
||||
@@ -497,9 +479,8 @@ public:
|
||||
params[jss::seed] = common::gMasterSeed;
|
||||
|
||||
auto const ret = keypairForSignature(params, error);
|
||||
BEAST_EXPECT(containsError(error));
|
||||
BEAST_EXPECT(!ret);
|
||||
BEAST_EXPECT(error[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(error[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(error[jss::error_message] == "Invalid field 'key_type', not string.");
|
||||
}
|
||||
|
||||
@@ -511,9 +492,8 @@ public:
|
||||
params[jss::passphrase] = 1234567890;
|
||||
|
||||
auto const ret = keypairForSignature(params, error);
|
||||
BEAST_EXPECT(containsError(error));
|
||||
BEAST_EXPECT(!ret);
|
||||
BEAST_EXPECT(error[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(error[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(error[jss::error_message] == "Invalid field 'passphrase', not string.");
|
||||
}
|
||||
|
||||
@@ -524,9 +504,8 @@ public:
|
||||
params[jss::passphrase] = json::ValueType::Object;
|
||||
|
||||
auto const ret = keypairForSignature(params, error);
|
||||
BEAST_EXPECT(containsError(error));
|
||||
BEAST_EXPECT(!ret);
|
||||
BEAST_EXPECT(error[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(error[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(error[jss::error_message] == "Invalid field 'passphrase', not string.");
|
||||
}
|
||||
|
||||
@@ -537,9 +516,8 @@ public:
|
||||
params[jss::passphrase] = json::ValueType::Array;
|
||||
|
||||
auto const ret = keypairForSignature(params, error);
|
||||
BEAST_EXPECT(containsError(error));
|
||||
BEAST_EXPECT(!ret);
|
||||
BEAST_EXPECT(error[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(error[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(error[jss::error_message] == "Invalid field 'passphrase', not string.");
|
||||
}
|
||||
|
||||
@@ -550,9 +528,8 @@ public:
|
||||
params[jss::passphrase] = "";
|
||||
|
||||
auto const ret = keypairForSignature(params, error);
|
||||
BEAST_EXPECT(containsError(error));
|
||||
BEAST_EXPECT(!ret);
|
||||
BEAST_EXPECT(error[jss::error] == "badSeed");
|
||||
BEAST_EXPECT(error[jss::error_code] == RpcBadSeed);
|
||||
BEAST_EXPECT(error[jss::error_message] == "Disallowed seed.");
|
||||
}
|
||||
|
||||
@@ -564,9 +541,8 @@ public:
|
||||
params[jss::seed] = 443556;
|
||||
|
||||
auto const ret = keypairForSignature(params, error);
|
||||
BEAST_EXPECT(containsError(error));
|
||||
BEAST_EXPECT(!ret);
|
||||
BEAST_EXPECT(error[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(error[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(error[jss::error_message] == "Invalid field 'seed', not string.");
|
||||
}
|
||||
|
||||
@@ -577,9 +553,8 @@ public:
|
||||
params[jss::seed] = json::ValueType::Object;
|
||||
|
||||
auto const ret = keypairForSignature(params, error);
|
||||
BEAST_EXPECT(containsError(error));
|
||||
BEAST_EXPECT(!ret);
|
||||
BEAST_EXPECT(error[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(error[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(error[jss::error_message] == "Invalid field 'seed', not string.");
|
||||
}
|
||||
|
||||
@@ -590,9 +565,8 @@ public:
|
||||
params[jss::seed] = json::ValueType::Array;
|
||||
|
||||
auto const ret = keypairForSignature(params, error);
|
||||
BEAST_EXPECT(containsError(error));
|
||||
BEAST_EXPECT(!ret);
|
||||
BEAST_EXPECT(error[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(error[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(error[jss::error_message] == "Invalid field 'seed', not string.");
|
||||
}
|
||||
|
||||
@@ -603,9 +577,8 @@ public:
|
||||
params[jss::seed] = "";
|
||||
|
||||
auto const ret = keypairForSignature(params, error);
|
||||
BEAST_EXPECT(containsError(error));
|
||||
BEAST_EXPECT(!ret);
|
||||
BEAST_EXPECT(error[jss::error] == "badSeed");
|
||||
BEAST_EXPECT(error[jss::error_code] == RpcBadSeed);
|
||||
BEAST_EXPECT(error[jss::error_message] == "Disallowed seed.");
|
||||
}
|
||||
|
||||
@@ -616,9 +589,8 @@ public:
|
||||
params[jss::seed] = "s M V s h z D F p t Z E m h s";
|
||||
|
||||
auto const ret = keypairForSignature(params, error);
|
||||
BEAST_EXPECT(containsError(error));
|
||||
BEAST_EXPECT(!ret);
|
||||
BEAST_EXPECT(error[jss::error] == "badSeed");
|
||||
BEAST_EXPECT(error[jss::error_code] == RpcBadSeed);
|
||||
BEAST_EXPECT(error[jss::error_message] == "Disallowed seed.");
|
||||
}
|
||||
|
||||
@@ -629,9 +601,8 @@ public:
|
||||
params[jss::seed] = "pnnjkbnobnml43679nbvjdsklnbjs";
|
||||
|
||||
auto const ret = keypairForSignature(params, error);
|
||||
BEAST_EXPECT(containsError(error));
|
||||
BEAST_EXPECT(!ret);
|
||||
BEAST_EXPECT(error[jss::error] == "badSeed");
|
||||
BEAST_EXPECT(error[jss::error_code] == RpcBadSeed);
|
||||
BEAST_EXPECT(error[jss::error_message] == "Disallowed seed.");
|
||||
}
|
||||
|
||||
@@ -643,9 +614,8 @@ public:
|
||||
params[jss::seed_hex] = 443556;
|
||||
|
||||
auto const ret = keypairForSignature(params, error);
|
||||
BEAST_EXPECT(containsError(error));
|
||||
BEAST_EXPECT(!ret);
|
||||
BEAST_EXPECT(error[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(error[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(error[jss::error_message] == "Invalid field 'seed_hex', not string.");
|
||||
}
|
||||
|
||||
@@ -656,9 +626,8 @@ public:
|
||||
params[jss::seed_hex] = json::ValueType::Object;
|
||||
|
||||
auto const ret = keypairForSignature(params, error);
|
||||
BEAST_EXPECT(containsError(error));
|
||||
BEAST_EXPECT(!ret);
|
||||
BEAST_EXPECT(error[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(error[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(error[jss::error_message] == "Invalid field 'seed_hex', not string.");
|
||||
}
|
||||
|
||||
@@ -669,9 +638,8 @@ public:
|
||||
params[jss::seed_hex] = json::ValueType::Array;
|
||||
|
||||
auto const ret = keypairForSignature(params, error);
|
||||
BEAST_EXPECT(containsError(error));
|
||||
BEAST_EXPECT(!ret);
|
||||
BEAST_EXPECT(error[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(error[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(error[jss::error_message] == "Invalid field 'seed_hex', not string.");
|
||||
}
|
||||
|
||||
@@ -682,9 +650,8 @@ public:
|
||||
params[jss::seed_hex] = "";
|
||||
|
||||
auto const ret = keypairForSignature(params, error);
|
||||
BEAST_EXPECT(containsError(error));
|
||||
BEAST_EXPECT(!ret);
|
||||
BEAST_EXPECT(error[jss::error] == "badSeed");
|
||||
BEAST_EXPECT(error[jss::error_code] == RpcBadSeed);
|
||||
BEAST_EXPECT(error[jss::error_message] == "Disallowed seed.");
|
||||
}
|
||||
|
||||
@@ -695,9 +662,8 @@ public:
|
||||
params[jss::seed_hex] = "A670A19B";
|
||||
|
||||
auto const ret = keypairForSignature(params, error);
|
||||
BEAST_EXPECT(containsError(error));
|
||||
BEAST_EXPECT(!ret);
|
||||
BEAST_EXPECT(error[jss::error] == "badSeed");
|
||||
BEAST_EXPECT(error[jss::error_code] == RpcBadSeed);
|
||||
BEAST_EXPECT(error[jss::error_message] == "Disallowed seed.");
|
||||
}
|
||||
|
||||
@@ -708,9 +674,8 @@ public:
|
||||
params[jss::seed_hex] = common::gPassphrase;
|
||||
|
||||
auto const ret = keypairForSignature(params, error);
|
||||
BEAST_EXPECT(containsError(error));
|
||||
BEAST_EXPECT(!ret);
|
||||
BEAST_EXPECT(error[jss::error] == "badSeed");
|
||||
BEAST_EXPECT(error[jss::error_code] == RpcBadSeed);
|
||||
BEAST_EXPECT(error[jss::error_message] == "Disallowed seed.");
|
||||
}
|
||||
|
||||
@@ -721,9 +686,8 @@ public:
|
||||
params[jss::seed_hex] = "BE6A670A19B209E112146D0A7ED2AAD72567D0FC913";
|
||||
|
||||
auto const ret = keypairForSignature(params, error);
|
||||
BEAST_EXPECT(containsError(error));
|
||||
BEAST_EXPECT(!ret);
|
||||
BEAST_EXPECT(error[jss::error] == "badSeed");
|
||||
BEAST_EXPECT(error[jss::error_code] == RpcBadSeed);
|
||||
BEAST_EXPECT(error[jss::error_message] == "Disallowed seed.");
|
||||
}
|
||||
}
|
||||
@@ -759,8 +723,7 @@ public:
|
||||
|
||||
auto const ret = keypairForSignature(params, error);
|
||||
|
||||
BEAST_EXPECT(error[jss::error] == "badSeed");
|
||||
BEAST_EXPECT(error[jss::error_code] == RpcBadSeed);
|
||||
BEAST_EXPECT(containsError(error));
|
||||
BEAST_EXPECT(
|
||||
error[jss::error_message] == "Specified seed is for an Ed25519 wallet.");
|
||||
}
|
||||
@@ -791,8 +754,7 @@ public:
|
||||
|
||||
auto const ret = keypairForSignature(params, error);
|
||||
|
||||
BEAST_EXPECT(error[jss::error] == "badSeed");
|
||||
BEAST_EXPECT(error[jss::error_code] == RpcBadSeed);
|
||||
BEAST_EXPECT(containsError(error));
|
||||
BEAST_EXPECT(
|
||||
error[jss::error_message] == "Specified seed is for an Ed25519 wallet.");
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
@@ -132,9 +131,8 @@ public:
|
||||
json::Value jvParams;
|
||||
jvParams[jss::limit] = "0"; // NOT an integer
|
||||
auto const jrr = env.rpc("json", "ledger_data", to_string(jvParams))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::status] == "error");
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jrr[jss::status] == "error");
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'limit', not integer.");
|
||||
}
|
||||
|
||||
@@ -143,9 +141,8 @@ public:
|
||||
json::Value jvParams;
|
||||
jvParams[jss::marker] = "NOT_A_MARKER";
|
||||
auto const jrr = env.rpc("json", "ledger_data", to_string(jvParams))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::status] == "error");
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jrr[jss::status] == "error");
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'marker', not valid.");
|
||||
}
|
||||
|
||||
@@ -154,9 +151,8 @@ public:
|
||||
json::Value jvParams;
|
||||
jvParams[jss::marker] = 1;
|
||||
auto const jrr = env.rpc("json", "ledger_data", to_string(jvParams))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::status] == "error");
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jrr[jss::status] == "error");
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'marker', not valid.");
|
||||
}
|
||||
|
||||
@@ -165,9 +161,8 @@ public:
|
||||
json::Value jvParams;
|
||||
jvParams[jss::ledger_index] = 10u;
|
||||
auto const jrr = env.rpc("json", "ledger_data", to_string(jvParams))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::status] == "error");
|
||||
BEAST_EXPECT(jrr[jss::error] == "lgrNotFound");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcLgrNotFound);
|
||||
BEAST_EXPECT(jrr[jss::status] == "error");
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "ledgerNotFound");
|
||||
}
|
||||
|
||||
@@ -176,9 +171,8 @@ public:
|
||||
json::Value jvParams;
|
||||
jvParams[jss::binary] = "true";
|
||||
auto const jrr = env.rpc("json", "ledger_data", to_string(jvParams))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::status] == "error");
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_code] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jrr[jss::status] == "error");
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'binary', not boolean.");
|
||||
}
|
||||
}
|
||||
@@ -465,8 +459,8 @@ public:
|
||||
jvParams[jss::ledger_index] = "current";
|
||||
jvParams[jss::type] = "misspelling";
|
||||
auto const jrr = env.rpc("json", "ledger_data", to_string(jvParams))[jss::result];
|
||||
BEAST_EXPECT(jrr.isMember("error"));
|
||||
BEAST_EXPECT(jrr["error"] == "invalidParams");
|
||||
BEAST_EXPECT(jrr["error_code"] == RpcInvalidParams);
|
||||
BEAST_EXPECT(jrr["error_message"] == "Invalid field 'type'.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +153,6 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
checkErrorValue(
|
||||
json::Value const& jv,
|
||||
std::string const& err,
|
||||
ErrorCodeI code,
|
||||
std::string const& msg,
|
||||
std::source_location const location = std::source_location::current())
|
||||
{
|
||||
@@ -166,14 +165,6 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
"Expected error " + err + ", received " + jv[jss::error].asString() + ", at line " +
|
||||
std::to_string(location.line()) + ", " + jv.toStyledString());
|
||||
}
|
||||
if (BEAST_EXPECT(jv.isMember(jss::error_code)))
|
||||
{
|
||||
BEAST_EXPECTS(
|
||||
jv[jss::error_code] == code,
|
||||
"Expected error code " + std::to_string(code) + ", received " +
|
||||
jv[jss::error_code].asString() + ", at line " +
|
||||
std::to_string(location.line()) + ", " + jv.toStyledString());
|
||||
}
|
||||
if (msg.empty())
|
||||
{
|
||||
BEAST_EXPECTS(
|
||||
@@ -349,21 +340,12 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
apiVersion, "json", "ledger_entry", to_string(correctRequest))[jss::result];
|
||||
if (apiVersion < 2u)
|
||||
{
|
||||
BEAST_EXPECTS(jrr[jss::status] == "error", std::to_string(location.line()));
|
||||
BEAST_EXPECTS(
|
||||
jrr[jss::error] == "unknownOption", std::to_string(location.line()));
|
||||
BEAST_EXPECTS(!jrr.isMember(jss::error_code), std::to_string(location.line()));
|
||||
BEAST_EXPECTS(
|
||||
jrr[jss::error_message].isNull(), std::to_string(location.line()));
|
||||
checkErrorValue(jrr, "unknownOption", "", location);
|
||||
}
|
||||
else
|
||||
{
|
||||
checkErrorValue(
|
||||
jrr,
|
||||
"invalidParams",
|
||||
RpcInvalidParams,
|
||||
"No ledger_entry params provided.",
|
||||
location);
|
||||
jrr, "invalidParams", "No ledger_entry params provided.", location);
|
||||
}
|
||||
}
|
||||
auto const tryField = [&](json::Value fieldValue) -> void {
|
||||
@@ -372,7 +354,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
apiVersion, "json", "ledger_entry", to_string(correctRequest))[jss::result];
|
||||
auto const expectedErrMsg = rpc::expectedFieldMessage(
|
||||
fieldName, typeNameOverride.value_or(getTypeName(typeID)));
|
||||
checkErrorValue(jrr, expectedError, RpcInvalidParams, expectedErrMsg, location);
|
||||
checkErrorValue(jrr, expectedError, expectedErrMsg, location);
|
||||
};
|
||||
|
||||
auto const& badValues = getBadValues(typeID);
|
||||
@@ -405,21 +387,13 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
json::Value const jrr = env.rpc(
|
||||
apiVersion, "json", "ledger_entry", to_string(correctRequest))[jss::result];
|
||||
checkErrorValue(
|
||||
jrr,
|
||||
"malformedRequest",
|
||||
RpcInvalidParams,
|
||||
rpc::missingFieldMessage(fieldName.cStr()),
|
||||
location);
|
||||
jrr, "malformedRequest", rpc::missingFieldMessage(fieldName.cStr()), location);
|
||||
|
||||
correctRequest[parentFieldName][fieldName] = json::ValueType::Null;
|
||||
json::Value const jrr2 = env.rpc(
|
||||
apiVersion, "json", "ledger_entry", to_string(correctRequest))[jss::result];
|
||||
checkErrorValue(
|
||||
jrr2,
|
||||
"malformedRequest",
|
||||
RpcInvalidParams,
|
||||
rpc::missingFieldMessage(fieldName.cStr()),
|
||||
location);
|
||||
jrr2, "malformedRequest", rpc::missingFieldMessage(fieldName.cStr()), location);
|
||||
}
|
||||
auto const tryField = [&](json::Value fieldValue) -> void {
|
||||
correctRequest[parentFieldName][fieldName] = fieldValue;
|
||||
@@ -429,7 +403,6 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
checkErrorValue(
|
||||
jrr,
|
||||
expectedError,
|
||||
RpcInvalidParams,
|
||||
rpc::expectedFieldMessage(fieldName, getTypeName(typeID)),
|
||||
location);
|
||||
};
|
||||
@@ -524,7 +497,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
"AA";
|
||||
auto const jrr = env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(jrr, "lgrNotFound", RpcLgrNotFound, "ledgerNotFound");
|
||||
checkErrorValue(jrr, "lgrNotFound", "ledgerNotFound");
|
||||
}
|
||||
{
|
||||
// Missing ledger_entry ledger_hash
|
||||
@@ -538,10 +511,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
json::Value const jrr = env.rpc(
|
||||
apiVersion, "json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(
|
||||
jrr,
|
||||
"invalidParams",
|
||||
RpcInvalidParams,
|
||||
"Invalid field 'ledger_hash', not hex string.");
|
||||
jrr, "invalidParams", "Invalid field 'ledger_hash', not hex string.");
|
||||
};
|
||||
|
||||
auto const& badValues = getBadValues(typeId);
|
||||
@@ -560,7 +530,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
"00000000000000000000000000000000000000000000000000000000000000"
|
||||
"00";
|
||||
auto const jrr = env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(jrr, "entryNotFound", RpcEntryNotFound, "Entry not found.");
|
||||
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
|
||||
}
|
||||
|
||||
forAllApiVersions([&, this](unsigned apiVersion) {
|
||||
@@ -576,15 +546,11 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
|
||||
if (apiVersion < 2u)
|
||||
{
|
||||
BEAST_EXPECT(jrr[jss::status] == "error");
|
||||
BEAST_EXPECT(jrr[jss::error] == "unknownOption");
|
||||
BEAST_EXPECT(!jrr.isMember(jss::error_code));
|
||||
BEAST_EXPECT(jrr[jss::error_message].isNull());
|
||||
checkErrorValue(jrr, "unknownOption", "");
|
||||
}
|
||||
else
|
||||
{
|
||||
checkErrorValue(
|
||||
jrr, "invalidParams", RpcInvalidParams, "No ledger_entry params provided.");
|
||||
checkErrorValue(jrr, "invalidParams", "No ledger_entry params provided.");
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -689,7 +655,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(jrr, "entryNotFound", RpcEntryNotFound, "Entry not found.");
|
||||
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -888,7 +854,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(jrr, "entryNotFound", RpcEntryNotFound, "Entry not found.");
|
||||
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
|
||||
}
|
||||
{
|
||||
// Request an index that is not a check. We'll use alice's
|
||||
@@ -906,8 +872,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(
|
||||
jrr, "unexpectedLedgerType", RpcUnexpectedLedgerType, "Unexpected ledger type.");
|
||||
checkErrorValue(jrr, "unexpectedLedgerType", "Unexpected ledger type.");
|
||||
}
|
||||
{
|
||||
// Check malformed cases
|
||||
@@ -966,7 +931,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
env,
|
||||
"48004829F915654A81B11C4AB8218D96FED67F209B58328A72314FB6EA288B"
|
||||
"E4");
|
||||
checkErrorValue(jv[jss::result], "entryNotFound", RpcEntryNotFound, "Entry not found.");
|
||||
checkErrorValue(jv[jss::result], "entryNotFound", "Entry not found.");
|
||||
}
|
||||
|
||||
{
|
||||
@@ -1161,8 +1126,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
auto const expectedErrMsg = fieldValue.isNull()
|
||||
? rpc::missingFieldMessage(jss::issuer.cStr())
|
||||
: rpc::expectedFieldMessage(jss::issuer, "AccountID");
|
||||
checkErrorValue(
|
||||
jrr, "malformedAuthorizedCredentials", RpcInvalidParams, expectedErrMsg);
|
||||
checkErrorValue(jrr, "malformedAuthorizedCredentials", expectedErrMsg);
|
||||
};
|
||||
|
||||
auto const& badValues = getBadValues(FieldType::AccountField);
|
||||
@@ -1191,7 +1155,6 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
checkErrorValue(
|
||||
jrr[jss::result],
|
||||
"malformedAuthorizedCredentials",
|
||||
RpcInvalidParams,
|
||||
rpc::expectedFieldMessage(jss::authorized_credentials, "array"));
|
||||
}
|
||||
|
||||
@@ -1214,8 +1177,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
auto const expectedErrMsg = fieldValue.isNull()
|
||||
? rpc::missingFieldMessage(jss::credential_type.cStr())
|
||||
: rpc::expectedFieldMessage(jss::credential_type, "hex string");
|
||||
checkErrorValue(
|
||||
jrr, "malformedAuthorizedCredentials", RpcInvalidParams, expectedErrMsg);
|
||||
checkErrorValue(jrr, "malformedAuthorizedCredentials", expectedErrMsg);
|
||||
};
|
||||
|
||||
auto const& badValues = getBadValues(FieldType::BlobField);
|
||||
@@ -1245,7 +1207,6 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
checkErrorValue(
|
||||
jrr[jss::result],
|
||||
"malformedRequest",
|
||||
RpcInvalidParams,
|
||||
"Must have exactly one of `authorized` and "
|
||||
"`authorized_credentials`.");
|
||||
}
|
||||
@@ -1278,7 +1239,6 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
checkErrorValue(
|
||||
jrr[jss::result],
|
||||
"malformedAuthorizedCredentials",
|
||||
RpcInvalidParams,
|
||||
"Invalid field 'authorized_credentials', not array of objects.");
|
||||
}
|
||||
|
||||
@@ -1297,7 +1257,6 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
checkErrorValue(
|
||||
jrr[jss::result],
|
||||
"malformedAuthorizedCredentials",
|
||||
RpcInvalidParams,
|
||||
"Invalid field 'authorized_credentials', not array of objects.");
|
||||
}
|
||||
|
||||
@@ -1312,7 +1271,6 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
checkErrorValue(
|
||||
jrr[jss::result],
|
||||
"malformedAuthorizedCredentials",
|
||||
RpcInvalidParams,
|
||||
"Invalid field 'authorized_credentials', array empty.");
|
||||
}
|
||||
|
||||
@@ -1350,7 +1308,6 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
checkErrorValue(
|
||||
jrr[jss::result],
|
||||
"malformedAuthorizedCredentials",
|
||||
RpcInvalidParams,
|
||||
"Invalid field 'authorized_credentials', array too long.");
|
||||
}
|
||||
}
|
||||
@@ -1486,10 +1443,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(
|
||||
jrr,
|
||||
"malformedRequest",
|
||||
RpcInvalidParams,
|
||||
"Must have exactly one of `owner` and `dir_root` fields.");
|
||||
jrr, "malformedRequest", "Must have exactly one of `owner` and `dir_root` fields.");
|
||||
}
|
||||
{
|
||||
// Incomplete directory object. Missing both dir_root and owner.
|
||||
@@ -1500,10 +1454,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(
|
||||
jrr,
|
||||
"malformedRequest",
|
||||
RpcInvalidParams,
|
||||
"Must have exactly one of `owner` and `dir_root` fields.");
|
||||
jrr, "malformedRequest", "Must have exactly one of `owner` and `dir_root` fields.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1666,7 +1617,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
jvParams[jss::nft_offer][jss::seq] = offerSeq + 1000;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(jrr, "entryNotFound", RpcEntryNotFound, "Entry not found.");
|
||||
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
|
||||
}
|
||||
|
||||
// negative tests
|
||||
@@ -1884,7 +1835,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(jrr, "entryNotFound", RpcEntryNotFound, "Entry not found.");
|
||||
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
|
||||
}
|
||||
{
|
||||
// Request an index that is not a payment channel.
|
||||
@@ -1893,7 +1844,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(jrr, "entryNotFound", RpcEntryNotFound, "Entry not found.");
|
||||
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
|
||||
}
|
||||
|
||||
{
|
||||
@@ -1968,7 +1919,6 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
checkErrorValue(
|
||||
jrr,
|
||||
"malformedRequest",
|
||||
RpcInvalidParams,
|
||||
"Invalid field 'accounts', not length-2 array of "
|
||||
"Accounts.");
|
||||
}
|
||||
@@ -1987,7 +1937,6 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
checkErrorValue(
|
||||
jrr,
|
||||
"malformedRequest",
|
||||
RpcInvalidParams,
|
||||
"Invalid field 'accounts', not length-2 array of "
|
||||
"Accounts.");
|
||||
}
|
||||
@@ -2008,7 +1957,6 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
checkErrorValue(
|
||||
jrr,
|
||||
"malformedAddress",
|
||||
RpcInvalidParams,
|
||||
rpc::expectedFieldMessage(jss::accounts, "array of Accounts"));
|
||||
}
|
||||
|
||||
@@ -2024,7 +1972,6 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
checkErrorValue(
|
||||
jrr,
|
||||
"malformedAddress",
|
||||
RpcInvalidParams,
|
||||
rpc::expectedFieldMessage(jss::accounts, "array of Accounts"));
|
||||
}
|
||||
};
|
||||
@@ -2047,8 +1994,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(
|
||||
jrr, "malformedRequest", RpcInvalidParams, "Cannot have a trustline to self.");
|
||||
checkErrorValue(jrr, "malformedRequest", "Cannot have a trustline to self.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2102,7 +2048,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(jrr, "entryNotFound", RpcEntryNotFound, "Entry not found.");
|
||||
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
|
||||
}
|
||||
{
|
||||
// Request an index that is not a signer list.
|
||||
@@ -2111,7 +2057,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(jrr, "entryNotFound", RpcEntryNotFound, "Entry not found.");
|
||||
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
|
||||
}
|
||||
|
||||
{
|
||||
@@ -2202,7 +2148,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(jrr, "entryNotFound", RpcEntryNotFound, "Entry not found.");
|
||||
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
|
||||
}
|
||||
{
|
||||
// First real ticket requested by index.
|
||||
@@ -2238,7 +2184,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(jrr, "entryNotFound", RpcEntryNotFound, "Entry not found.");
|
||||
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
|
||||
}
|
||||
{
|
||||
// Request a ticket using an account root entry.
|
||||
@@ -2247,8 +2193,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(
|
||||
jrr, "unexpectedLedgerType", RpcUnexpectedLedgerType, "Unexpected ledger type.");
|
||||
checkErrorValue(jrr, "unexpectedLedgerType", "Unexpected ledger type.");
|
||||
}
|
||||
|
||||
{
|
||||
@@ -2316,7 +2261,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
jvParams[jss::transaction_proposal][jss::ticket_seq] = ticketSeq + 1;
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
auto const jrr = env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(jrr, "entryNotFound", RpcEntryNotFound, "Entry not found.");
|
||||
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
|
||||
}
|
||||
{
|
||||
// Lookup by an index of the wrong entry type.
|
||||
@@ -2324,8 +2269,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
jvParams[jss::transaction_proposal] = to_string(keylet::account(target).key);
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
auto const jrr = env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(
|
||||
jrr, "unexpectedLedgerType", RpcUnexpectedLedgerType, "Unexpected ledger type.");
|
||||
checkErrorValue(jrr, "unexpectedLedgerType", "Unexpected ledger type.");
|
||||
}
|
||||
|
||||
{
|
||||
@@ -2389,7 +2333,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(jrr, "entryNotFound", RpcEntryNotFound, "Entry not found.");
|
||||
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
|
||||
}
|
||||
{
|
||||
// Malformed DID index
|
||||
@@ -2511,7 +2455,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(jrr, "entryNotFound", RpcEntryNotFound, "Entry not found.");
|
||||
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
|
||||
}
|
||||
{
|
||||
// Request the MPToken using its owner + mptIssuanceID.
|
||||
@@ -2534,7 +2478,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(jrr, "entryNotFound", RpcEntryNotFound, "Entry not found.");
|
||||
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
|
||||
}
|
||||
{
|
||||
// Malformed MPTIssuance index
|
||||
@@ -2602,8 +2546,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
"12F1F1F1F180D67377B2FAB292A31C922470326268D2B9B74CD1E582645B9A"
|
||||
"DE";
|
||||
auto const jrr = env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkErrorValue(
|
||||
jrr[jss::result], "entryNotFound", RpcEntryNotFound, "Entry not found.");
|
||||
checkErrorValue(jrr[jss::result], "entryNotFound", "Entry not found.");
|
||||
}
|
||||
{
|
||||
// test basic malformed scenarios
|
||||
@@ -2649,9 +2592,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
auto checkResult = [&](bool good,
|
||||
json::Value const& jv,
|
||||
json::StaticString const& expectedType,
|
||||
std::optional<std::string> const& expectedError = {},
|
||||
ErrorCodeI expectedCode = RpcEntryNotFound,
|
||||
std::string const& expectedMessage = "Entry not found.") {
|
||||
std::optional<std::string> const& expectedError = {}) {
|
||||
if (good)
|
||||
{
|
||||
BEAST_EXPECTS(
|
||||
@@ -2666,10 +2607,9 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
{
|
||||
BEAST_EXPECTS(
|
||||
jv.isObject() && jv.isMember(jss::result) &&
|
||||
jv[jss::result].isMember(jss::error) &&
|
||||
!jv[jss::result].isMember(jss::node) &&
|
||||
jv[jss::result][jss::error] == expectedError.value_or("entryNotFound") &&
|
||||
jv[jss::result][jss::error_code] == expectedCode &&
|
||||
jv[jss::result][jss::error_message] == expectedMessage,
|
||||
jv[jss::result][jss::error] == expectedError.value_or("entryNotFound"),
|
||||
to_string(jv));
|
||||
}
|
||||
};
|
||||
@@ -2700,13 +2640,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[field] = json::ValueType::Null;
|
||||
auto const jv = env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(
|
||||
false,
|
||||
jv,
|
||||
expectedType,
|
||||
"malformedRequest",
|
||||
RpcInvalidParams,
|
||||
rpc::expectedFieldMessage(field, "hex string"));
|
||||
checkResult(false, jv, expectedType, "malformedRequest");
|
||||
BEAST_EXPECT(!jv[jss::result].isMember(jss::index));
|
||||
}
|
||||
|
||||
@@ -2716,13 +2650,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[field] = "arbitrary string";
|
||||
auto const jv = env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(
|
||||
false,
|
||||
jv,
|
||||
expectedType,
|
||||
"malformedRequest",
|
||||
RpcInvalidParams,
|
||||
rpc::expectedFieldMessage(field, "hex string"));
|
||||
checkResult(false, jv, expectedType, "malformedRequest");
|
||||
BEAST_EXPECT(!jv[jss::result].isMember(jss::index));
|
||||
}
|
||||
|
||||
@@ -2732,13 +2660,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[field] = false;
|
||||
auto const jv = env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(
|
||||
false,
|
||||
jv,
|
||||
expectedType,
|
||||
"invalidParams",
|
||||
RpcInvalidParams,
|
||||
rpc::expectedFieldMessage(field, "true"));
|
||||
checkResult(false, jv, expectedType, "invalidParams");
|
||||
BEAST_EXPECT(!jv[jss::result].isMember(jss::index));
|
||||
}
|
||||
|
||||
@@ -2750,8 +2672,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[field] = badKey;
|
||||
auto const jv = env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(
|
||||
false, jv, expectedType, "entryNotFound", RpcEntryNotFound, "Entry not found.");
|
||||
checkResult(false, jv, expectedType, "entryNotFound");
|
||||
BEAST_EXPECTS(jv[jss::result][jss::index] == badKey, to_string(jv));
|
||||
}
|
||||
|
||||
@@ -2762,13 +2683,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
params[jss::index] = field;
|
||||
params[jss::api_version] = 2;
|
||||
auto const jv = env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(
|
||||
false,
|
||||
jv,
|
||||
expectedType,
|
||||
"malformedRequest",
|
||||
RpcInvalidParams,
|
||||
rpc::expectedFieldMessage(jss::index, "hex string"));
|
||||
checkResult(false, jv, expectedType, "malformedRequest");
|
||||
BEAST_EXPECT(!jv[jss::result].isMember(jss::index));
|
||||
}
|
||||
|
||||
@@ -2807,13 +2722,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
params[jss::index] = field;
|
||||
params[jss::api_version] = 2;
|
||||
auto const jv = env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(
|
||||
false,
|
||||
jv,
|
||||
expectedType,
|
||||
"malformedRequest",
|
||||
RpcInvalidParams,
|
||||
rpc::expectedFieldMessage(jss::index, "hex string"));
|
||||
checkResult(false, jv, expectedType, "malformedRequest");
|
||||
BEAST_EXPECT(!jv[jss::result].isMember(jss::index));
|
||||
}
|
||||
|
||||
@@ -2878,9 +2787,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
auto checkResult = [&](bool good,
|
||||
json::Value const& jv,
|
||||
int expectedCount,
|
||||
std::optional<std::string> const& expectedError = {},
|
||||
ErrorCodeI expectedCode = RpcEntryNotFound,
|
||||
std::string const& expectedMessage = "Entry not found.") {
|
||||
std::optional<std::string> const& expectedError = {}) {
|
||||
if (good)
|
||||
{
|
||||
BEAST_EXPECTS(
|
||||
@@ -2900,10 +2807,9 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
{
|
||||
BEAST_EXPECTS(
|
||||
jv.isObject() && jv.isMember(jss::result) &&
|
||||
jv[jss::result].isMember(jss::error) &&
|
||||
!jv[jss::result].isMember(jss::node) &&
|
||||
jv[jss::result][jss::error] == expectedError.value_or("entryNotFound") &&
|
||||
jv[jss::result][jss::error_code] == expectedCode &&
|
||||
jv[jss::result][jss::error_message] == expectedMessage,
|
||||
jv[jss::result][jss::error] == expectedError.value_or("entryNotFound"),
|
||||
to_string(jv));
|
||||
}
|
||||
};
|
||||
@@ -2933,13 +2839,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[jss::hashes] = json::ValueType::Null;
|
||||
auto jv = env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(
|
||||
false,
|
||||
jv,
|
||||
0,
|
||||
"malformedRequest",
|
||||
RpcInvalidParams,
|
||||
rpc::expectedFieldMessage(jss::hashes, "hex string"));
|
||||
checkResult(false, jv, 0, "malformedRequest");
|
||||
BEAST_EXPECT(!jv[jss::result].isMember(jss::index));
|
||||
}
|
||||
|
||||
@@ -2949,13 +2849,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[jss::hashes] = "arbitrary string";
|
||||
auto const jv = env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(
|
||||
false,
|
||||
jv,
|
||||
0,
|
||||
"malformedRequest",
|
||||
RpcInvalidParams,
|
||||
rpc::expectedFieldMessage(jss::hashes, "hex string"));
|
||||
checkResult(false, jv, 0, "malformedRequest");
|
||||
BEAST_EXPECT(!jv[jss::result].isMember(jss::index));
|
||||
}
|
||||
|
||||
@@ -2965,13 +2859,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[jss::hashes] = "10";
|
||||
auto const jv = env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(
|
||||
false,
|
||||
jv,
|
||||
0,
|
||||
"malformedRequest",
|
||||
RpcInvalidParams,
|
||||
rpc::expectedFieldMessage(jss::hashes, "hex string"));
|
||||
checkResult(false, jv, 0, "malformedRequest");
|
||||
BEAST_EXPECT(!jv[jss::result].isMember(jss::index));
|
||||
}
|
||||
|
||||
@@ -2981,13 +2869,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[jss::hashes] = false;
|
||||
auto const jv = env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(
|
||||
false,
|
||||
jv,
|
||||
0,
|
||||
"invalidParams",
|
||||
RpcInvalidParams,
|
||||
rpc::expectedFieldMessage(jss::hashes, "true"));
|
||||
checkResult(false, jv, 0, "invalidParams");
|
||||
BEAST_EXPECT(!jv[jss::result].isMember(jss::index));
|
||||
}
|
||||
|
||||
@@ -2997,7 +2879,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[jss::hashes] = -1;
|
||||
auto const jv = env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(false, jv, 0, "internal", RpcInternal, "Internal error.");
|
||||
checkResult(false, jv, 0, "internal");
|
||||
BEAST_EXPECT(!jv[jss::result].isMember(jss::index));
|
||||
}
|
||||
|
||||
@@ -3008,8 +2890,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[jss::hashes] = badKey;
|
||||
auto const jv = env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(
|
||||
false, jv, 0, "entryNotFound", RpcEntryNotFound, "Entry not found.");
|
||||
checkResult(false, jv, 0, "entryNotFound");
|
||||
BEAST_EXPECT(jv[jss::result][jss::index] == badKey);
|
||||
}
|
||||
|
||||
@@ -3147,18 +3028,12 @@ class LedgerEntry_XChain_test : public beast::unit_test::Suite,
|
||||
public test::jtx::XChainBridgeObjects
|
||||
{
|
||||
void
|
||||
checkErrorValue(
|
||||
json::Value const& jv,
|
||||
std::string const& err,
|
||||
ErrorCodeI code,
|
||||
std::string const& msg)
|
||||
checkErrorValue(json::Value const& jv, std::string const& err, std::string const& msg)
|
||||
{
|
||||
if (BEAST_EXPECT(jv.isMember(jss::status)))
|
||||
BEAST_EXPECT(jv[jss::status] == "error");
|
||||
if (BEAST_EXPECT(jv.isMember(jss::error)))
|
||||
BEAST_EXPECT(jv[jss::error] == err);
|
||||
if (BEAST_EXPECT(jv.isMember(jss::error_code)))
|
||||
BEAST_EXPECT(jv[jss::error_code] == code);
|
||||
if (msg.empty())
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
@@ -3235,7 +3110,7 @@ class LedgerEntry_XChain_test : public beast::unit_test::Suite,
|
||||
json::Value const jrr =
|
||||
mcEnv.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
|
||||
checkErrorValue(jrr, "entryNotFound", RpcEntryNotFound, "Entry not found.");
|
||||
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
|
||||
}
|
||||
{
|
||||
// create two claim ids and verify that the bridge counter was
|
||||
@@ -3422,7 +3297,7 @@ class LedgerEntry_XChain_test : public beast::unit_test::Suite,
|
||||
[jss::xchain_owned_create_account_claim_id] = 1;
|
||||
json::Value const jrr =
|
||||
scEnv.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(jrr, "entryNotFound", RpcEntryNotFound, "Entry not found.");
|
||||
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loaded 100 of 136 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user