Compare commits

..
Author SHA1 Message Date
Bart fa50181606 test: Cover visitDifferences node skipping
visitDifferences() is reached in production only through a file-static
populateFetchPack() in LedgerMaster.cpp, so the private hasLeafNode()
and hasInnerNode() it consults were untested. Four cases pin both
answers: only the nodes on a missing item's path, nothing against an
identical map, every node against a null map, and a callback stop.

The first case uses 64 shared items, so the shared leaves build inner
nodes of their own and whole matching subtrees exist to skip. A
finalize() helper seals each map first, since visitDifferences() returns
early while the root hash is zero, and getHash() is what seals it, by
unsharing such a map. Each call is wrapped in ASSERT_NO_FATAL_FAILURE,
which carries a gtest ASSERT_ failure out of a helper to its caller.
2026-10-16 06:37:14 +00:00
Bart 74df6d278f refactor: Derive a node's position from its path, not a stored ID
NodePathStack stored a SHAMapNodeID beside every node, so the path
carried two answers to where a node sits that could disagree. The path
answers it alone: a SHAMap has no path compression, so entry i sits at
depth i. No consumer wanted the ID, only a depth and the nibbles of a
key it already held, so selectBranch() gains a depth overload,
unshareNode() takes a depth, and SHAMap::isLeafDepth() replaces a
file-local copy so one predicate serves every site that judges a depth.

What a path cannot derive is which branches the caller descended, so
pathKey_ records those and a leaf is judged against every branch above
it through samePositionAtDepth(). SharedIntrusive's move constructors
are noexcept, since the path is a vector of them and move_if_noexcept
would otherwise copy each element.
2026-10-16 06:37:13 +00:00
Bart dc55084ee0 refactor: Move entries off NodePathStack instead of copying them
NodePathStack::releaseNode() moves the top node out and pops it, where
five sites copied it out and then popped, each copy costing an atomic
increment and the original's release. dirtyUp() and delItem() walk up to
kLeafDepth levels per insert or delete on the ledger write path. The two
sites that read without popping bind a reference into the container,
sound only because it is a std::stack over a std::deque.

staticPointerCast() gains an rvalue overload, spelled with
SharedIntrusive<TT>&& so an lvalue still picks the const-ref one. Three
dynamic casts become static with a live type test, and a fourth static
cast that had none gains one, reporting UNREACHABLE in dirtyUp() and
delItem()'s loop and answering false in updateGiveItem() and delItem()'s
leaf cast, since the public API permits a call whose tag is absent.
2026-10-16 06:37:13 +00:00
Bart 3b9e8e19f0 fix: Judge a node's position on both getMissingNodes paths
Both getMissingNodes paths judge a leaf's position: the arm that handles
a node descendAsync has already resolved, and the deferred-read hook for
one an asynchronous read resolves later. The deferred-read hook also
refuses an inner node at kLeafDepth, which only the synchronous arm
judged before. gmnProcessDeferredReads() is no longer static so it can
record the verdict, and it keeps draining every read this pass posted
after reaching it, since each holds a pointer to the walk's state. Both
of its tests skip the node rather than return.

A second belongsAt() overload answers about a child from its parent's
position and the branch taken, so neither site builds the child's ID,
and the type is tested first, which keeps the position test off every
inner child. It is reached only for a child the full-below lookup did
not answer for: that lookup decides which children the walk visits, and
this test decides whether a visited leaf belongs where it was put.

With the node flag no longer trusted below the root, this is also what
closes the shared-subtree case: a root naming a completed subtree one
branch over, or using it as a root, now descends it and condemns the
map at the misplaced leaf. The full-below case asserts that outcome.
2026-10-16 06:37:12 +00:00
Bart 079c3b43dd fix: Judge a node's position at its hook and in both full-below memos
descend()'s filter path and addKnownNode()'s leaf hook judge a node's
position before hooking it. setInvalid() and the trySetState() it routes
through are const, so a read-only descend() can record that verdict.

A full-below entry was keyed on the subtree hash alone, so a hit
answered for a hash without answering for a position. FullBelowKey
carries the masked node ID, from the new childNodeID(), and its depth
beside the hash, so a hit answers for one position. The depth is kept
because SHAMapNodeID masks its id to its own depth, leaving the root and
every all-zero prefix sharing one id, and the position is two plain
fields rather than a SHAMapNodeID, which is a CountedObject a cache
entry has no business entering. The key grows from 32 to 68 bytes, so
about 18 MiB more at the kFullBelowTargetSize of 524288 entries.

SHAMapInnerNode::fullBelowGen_ is the second memo with the same flaw.
The TreeNodeCache shares one node object by hash across maps and
positions, and the walk read the flag in its loop condition ahead of the
cache lookup, so a subtree completed at one position was skipped at
another. The flag is now written and read for a walk's root alone, where
the position is fixed, and the position-keyed cache is the only memo
below it. A backed map already consults the cache first, so a warm cache
costs nothing more; an unbacked map re-walks its resident nodes on each
pass. The new case completes a subtree in one map, then checks that a
root naming it one branch over descends it and that the subtree used as
a root is walked rather than trusted.
2026-10-16 06:37:12 +00:00
Bart 962767a9b3 fix: Refuse a node that cannot sit where a path puts it
NodePathStack::pushChild() refuses a node with no room below it at the
depth offered, and a leaf whose key does not lie under the branch
reached. belongsAt() and pastLeafDepth() are the predicates, and
walkTowardsKey()'s no-path mode shares pastLeafDepth(), so both its
modes refuse at the same depth; the no-path mode reports the key
absent, since its callers ask about a key rather than a position. Each reports SOMETIMES, not UNREACHABLE,
since the two-argument descend() judges neither position nor type.
boundHelper() throws where it answered end(), since an empty map still
leaves its root on the path, so an empty path means a node was refused
while end() claims no key lies on the requested side.

A refusal also reaches the empty-path throws in delItem(), addGiveItem()
and updateGiveItem(). OpenLedger::apply() gains in its retry pass the
guard the first pass already had, counting a transaction it cannot apply
as a failure.
2026-10-16 06:37:11 +00:00
Bart 4aea1e20ef fix: Bound the duplicate credit a TX-set acquire grants
A duplicate-only reply postpones at most kMaxDuplicateCredits
consecutive timer intervals with no useful node in between, so the
timeout count always resumes. A batch that advances the set earns the
budget back, and stillNeed() clears it on revival. The credit is spent
only while nothing else has answered the interval, so a wide fan-out
costs one credit rather than one per loser.

Two bounds join it: the sender must be a peer already asked, and the
acquisition must still be running. Both are read in takeNodes() before
takeNodesLocked(), which settles the set on some paths and enrolls the
sender in requestedPeers_ on others. takeNodes() also absorbs the
settled-set branch, so both bounds and the late-reply allowance are
judged ahead of takeNodesLocked().
2026-10-16 06:37:11 +00:00
Bart 206514a024 fix: Keep an acquisition registered when it supplies its own set
giveSet()'s third parameter is now fromAcquire, and the entry's acquire
pointer is reset only when something other than the acquisition supplied
the set. Dropping it cancels an acquisition in flight, which a set
arriving some other way should do and an acquisition completing on its
own should not, so the entry stays registered until newRound() sweeps it
and a late reply still reaches the per-peer allowance. Keying the entry
by hash alone holds because addRootNode() refuses a root that does not
hash to the hash asked for.

wantsReplyFrom() decides ahead of the parse, so a reply turned away is
never built into nodes. It shares chargeLateReply() with
takeNodesLocked(), and only one of the two sees a reply, so one charge.
2026-10-16 06:37:10 +00:00
Bart 0633d5cd6b fix: Fail a receiveNode() packet whose map has been abandoned
receiveNode() asks map.isValid() right after the locked receive and
fails the acquisition without charging the sender, since the verdict
belongs to the walk that reached it rather than to this packet. The test
sits ahead of isSynching(), which reads an abandoned map the same way as
one this packet just finished, because addKnownNode() answers every node
offered to such a map as a duplicate.

A second case pins the other outcome at that site: a packet leaving
nothing to fetch settles the ledger and reports it complete from
receiveNode() itself. DeepChain::allNodes() names the node list, root
included, that two suites assembled by hand.
2026-10-16 06:37:10 +00:00
Bart 6c29093de0 fix: Fail an acquisition whose map cannot exist
A node that leaves a map invalid proves the hash being chased belongs to
no valid tree, so TransactionAcquire::takeNodesLocked() and
InboundLedger::receiveNode() fail the acquisition there rather than
leaving the failure to the next trigger(). The whole batch is discarded,
since the nodes hooked in ahead of that one belong to the same tree.
Such a node costs kFeeMalformedData rather than kFeeInvalidData.

stillNeed() reports whether the set is still worth keeping and refuses
to revive one whose map went invalid. InboundTransactions::getSet()
refreshes an entry's retention window only while that answer is yes, so
such an entry is swept rather than kept alive by the asking.
recordPacket() and haveEverything() name the packet tally and the
nothing-left-to-fetch test that several sites repeated.
2026-10-16 06:37:09 +00:00
Bart 16187ab684 fix: Charge declined TX-set data where it was declined
takeNodesLocked() issues the charge for declined data, under the lock
that reached the verdict: an empty node list, a root that does not hash
to the set asked for, a node that cannot be hooked, and node data that
will not deserialize each cost kFeeInvalidData at the site rejecting it.
A batch whose parse throws is tallied as invalid but not charged, since
what throws there is this code rather than the data.

A reply arriving after the acquisition has failed is free once per peer:
each peer holds one unspent pass, renewed by each request the acquisition
sends it, so overlapping requests to one peer share one pass, and a reply
past that pass costs kFeeUselessData. requestedPeers_ records every peer
a request went to, since trigger() can also send a targeted request to an
unsolicited sender, and it records one where it builds the request, so
selection alone does not enroll a peer. lateReplyGranted_ keys the
allowance by peer identity rather than counting it, since a count cannot
say whose pass a reply spends. recordAsked() renews a peer's pass with
every request that reaches it, targeted or broadcast; a revival on its
own renews nothing. A reply to a completed set still costs
kFeeUselessData, since giveSet() drops the acquisition.

That settled reply reports a duplicate, so takeNodes() reads whether the
set was settled when the call arrived and grants the timer credit only
to a reply the running acquisition earned.
2026-10-16 06:37:08 +00:00
Bart 28f83b5bcf fix: Count partial batch progress in a TX-set reply
TransactionAcquire::takeNodes() accumulates one SHAMapAddNode across the
whole batch, so a packet that ends on a rejected node still counts the
nodes hooked in ahead of it, as InboundLedger::receiveNode() does. The
body moves to takeNodesLocked() and takeNodes() records progress on the
single exit, since several inner returns stop the batch early.

Progress is recorded for a batch that hooked a node in or answered with
one already held. A batch of nodes already held is what an honest second
responder to trigger()'s fan-out sends, so gotData() reads isGood()
rather than isUseful() when it decides whether to charge for useless
data.
2026-10-16 06:37:08 +00:00
Bart 67271ddc01 fix: Restart the timer when reviving a timed-out TX-set acquire
TransactionAcquire::stillNeed() restarts the retry timer whenever it
revives a failed acquisition, so a revived object resumes asking rather
than waiting for a peer to send data unprompted. expires_after() cancels
whatever wait was outstanding, so the timer holds at most one wait at a
time. A job already handed to the JobQueue is not canceled and re-arms
this same timer, which folds back into the one chain. A running
acquisition returns early and keeps the wait it has, so a consensus
round that asks for the set again does not restart it. The timeout count
is clamped either way.
2026-10-16 06:37:07 +00:00
Bart 2f9e1b1716 fix: Start a ledger built from a header mutable
Ledger(LedgerHeader const&, Rules, Family&) starts at immutable_ false.
Its maps are constructed Synching and filled in afterwards by an
acquisition or a replay, so setImmutable() is what settles the ledger,
and only once both maps are sound. mapHashesFromHeader_ records that
this constructor's transaction and account hashes are input rather than
derived, so setImmutable() leaves them alone: deriving them from the
maps would relabel a map that fell short of its target. The flag is
const, unlike immutable_.

A ledger this constructor built is therefore unsettled until
setImmutable() says otherwise, and LedgerMaster::switchLCL() and
LedgerHistory::insert() each require a settled one.
InboundLedger::getLedger() already reports nothing once the acquisition
has failed, so done()'s posted job reads the accessor: failure and a
null ledger_ are refused by one check, and checkAccept() requires a
non-null one. isFailed()'s comment said the opposite of what it returns.
2026-10-16 06:37:07 +00:00
Bart d7988a2579 fix: Settle an acquired ledger before reporting it complete
done() owns the publication of complete_ and sets it only once the
ledger is settled, so a caller reading isComplete() without mtx_ may use
the ledger directly. LedgerHistory::insert(), LedgerHolder::set() and
LedgerMaster::switchLCL() each require an immutable ledger, and that
ordering is what their requirement rests on. trigger() and receiveNode()
set the have-flags and leave the verdict to done(), while tryDB()
settles its own result and sets complete_ itself, so that path arrives
with only the reporting left.
2026-10-16 06:37:06 +00:00
Bart cf7d48d117 fix: Publish an acquisition's outcome flags across threads
complete_ and failed_ are std::atomic<bool> at the default seq_cst
order, so a reader that sees either flag set also sees the work the
writer did before setting it. isComplete() and isFailed() return them
without taking mtx_, and InboundLedgers::acquire() calls both outside
every lock it holds. A static_assert pins both to a lock-free
representation, so neither accessor can block. No access site changes: a
plain assignment and a plain test compile unchanged, and nothing takes a
reference or an address of either field.

Scoped to those two, which are the only fields here reached through an
accessor that takes no lock.
2026-10-16 06:37:06 +00:00
Bart 495374869a fix: Tell a satisfied InboundLedger map from an abandoned one
hasInvalidMap() reports whether either map of the ledger being acquired
has been found invalid, and the three places that read an empty walk
result as nothing left to fetch ask it first: tryDB(), whose two walks
set haveState_ and haveTransactions_ independently, trigger()'s
aggressive-retry branch, whose getNeededHashes() walk can reach the
verdict itself, and trigger()'s state-map walk, the one walk that runs
with mtx_ released.

The state-map walk asks outside the guard that re-reads the flags after
re-locking, since the verdict is about the map rather than the round,
and it withdraws the completeness claim beside the failure.
2026-10-16 06:37:04 +00:00
136 changed files with 6370 additions and 6473 deletions

No files matched your search

-1
View File
@@ -8,7 +8,6 @@ on:
branches:
- "develop"
- "release/*"
- "staging/*"
paths:
- "crates/**/Cargo.toml"
- "crates/Cargo.lock"
+1 -1
View File
@@ -243,7 +243,7 @@ jobs:
- should-run
- build-test
# Only run when committing to a PR that targets a release branch.
if: ${{ github.repository == 'XRPLF/rippled' && needs.should-run.outputs.go == 'true' && github.event_name == 'pull_request' && startsWith(github.event.pull_request.base.ref, 'release/') }}
if: ${{ github.repository == 'XRPLF/rippled' && needs.should-run.outputs.go == 'true' && github.event_name == 'pull_request' && startsWith(github.event.pull_request.base.ref, 'release') }}
uses: ./.github/workflows/reusable-upload-recipe.yml
secrets:
remote_username: ${{ secrets.NEXUS_REMOTE_USERNAME }}
+6 -8
View File
@@ -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 }}
+1 -2
View File
@@ -8,8 +8,7 @@ on:
push:
branches:
- "develop"
- "release/*"
- "staging/*"
- "release*"
workflow_dispatch:
jobs:
-1
View File
@@ -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
-3
View File
@@ -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
View File
@@ -3,7 +3,7 @@
"requires": [
"zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1782392402.122708",
"xxhash/0.8.3#681d36a0a6111fc56e5e45ea182c19cc%1782392402.420688",
"xrpl-rpc-spec/0.1.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
View File
@@ -40,7 +40,7 @@ class Xrpl(ConanFile):
"nudb/2.0.9",
"openssl/3.6.3",
"soci/4.0.3",
"xrpl-rpc-spec/0.1.25",
"xrpl-rpc-spec/0.1.23",
"zlib/1.3.2",
]
+22 -3
View File
@@ -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)
+2 -2
View File
@@ -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)}
{
}
+7 -2
View File
@@ -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
+2 -6
View File
@@ -352,9 +352,9 @@ class Number final
using rep = std::int64_t;
using InternalRep = MantissaRange::rep;
bool negative_{false};
InternalRep mantissa_{0};
int exponent_{std::numeric_limits<int>::lowest()};
bool negative_{false};
public:
// The range for the exponent when normalized
@@ -650,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}
{
}
+30 -9
View File
@@ -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
-5
View File
@@ -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.
*/
+1 -2
View File
@@ -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,
};
//------------------------------------------------------------------------------
+121 -9
View File
@@ -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});
}
/**
+8 -5
View File
@@ -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
View File
@@ -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);
+44 -4
View File
@@ -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;
}
+26 -6
View File
@@ -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);
+37
View File
@@ -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
+51 -2
View File
@@ -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);
+15 -16
View File
@@ -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;
}
+1 -2
View File
@@ -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
View File
@@ -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);
+53 -13
View File
@@ -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;
}
+83 -67
View File
@@ -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();
}
-2
View File
@@ -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
+1 -5
View File
@@ -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";
+15 -56
View File
@@ -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 ||
+2 -6
View File
@@ -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
+12 -19
View File
@@ -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");
}
}
}
+2 -10
View File
@@ -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");
}
}
-108
View File
@@ -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());
}
// -----------------------------------------------------------------
+22 -34
View File
@@ -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");
}
}
}
+2 -9
View File
@@ -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"));
}
}
+8 -13
View File
@@ -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");
}
}
-787
View File
@@ -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
+1 -3
View File
@@ -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);
}
};
-135
View File
@@ -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
+629 -15
View File
@@ -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);
+2 -16
View File
@@ -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.");
}
}
-44
View File
@@ -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.");
+12 -21
View File
@@ -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
-7
View File
@@ -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
-7
View File
@@ -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
+2 -7
View File
@@ -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
+2 -7
View File
@@ -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
+1 -146
View File
@@ -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.");
}
}
+2 -4
View File
@@ -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.
+7 -11
View File
@@ -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)
-180
View File
@@ -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);
-5
View File
@@ -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.
+1 -3
View File
@@ -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
-2
View File
@@ -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.");
+6 -4
View File
@@ -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);
}
}
{
+1 -2
View File
@@ -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.");
});
+4 -242
View File
@@ -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");
+11 -35
View File
@@ -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");
}
}
-57
View File
@@ -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
+4 -22
View File
@@ -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");
});
+1 -3
View File
@@ -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());
-3
View File
@@ -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.");
}
+1 -29
View File
@@ -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");
});
}
+4 -59
View File
@@ -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
+7 -147
View File
@@ -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]);
+3 -37
View File
@@ -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();
+8 -10
View File
@@ -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
View File
@@ -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.");
}
}
}
+6 -19
View File
@@ -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));
}
+2 -7
View File
@@ -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
-28
View File
@@ -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.");
}
}
+1 -2
View File
@@ -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.");
}
}
+29 -119
View File
@@ -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");
}
}
}
-9
View File
@@ -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
View File
@@ -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'.");
});
}
-41
View File
@@ -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
File diff suppressed because it is too large. Load diff
+34 -72
View File
@@ -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.");
}
+6 -12
View File
@@ -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'.");
}
}
+54 -179
View File
@@ -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