Compare commits

..
Author SHA1 Message Date
Bart 49410ad867 fix: Give every error code an HTTP status
Four rows of the error table name no HTTP status, so the `ErrorInfo`
constructor defaults them to 200. The `ripplerpc: "3.0"` envelope derives
the status from the code, so a reply reporting an error claims success
and anything reading the status, a load balancer above all, reads success
too. Give all four one: `actNotFound` answers 404, matching every
`*NotFound` sibling but `entryNotFound`, and `actMalformed`,
`alreadyMultisig` and `alreadySingleSig` answer 400. Then drop the
constructor that defaulted a status, so no row can omit one again. A gtest
lists by hand the codes that have no row, so an enumerator added without
one fails it, and asserts that every other code names a status other than
200.

No client reads a new status here. `legacyHttpStatus` reports 200 for
exactly those four rows, so the 3.0 envelope answers what it always has.
That list is closed, naming the rows that had no status of their own, so
a row added later reports whatever the table says. The test suite names
the two statuses it compares against most, 200 and 400, as `kOk` and
`kBadRequest`, and every existing assertion on them uses the name.
2026-10-14 10:14:03 +09:00
Bart 3689dca688 style: Realign the error table
The columns of the error table had drifted apart as rows were added, so a reader scanning it follows a ragged edge and a new row has no alignment to copy. Realign all four columns on one set of widths inside the existing `clang-format off` guard, changing no row's content.
2026-10-14 10:14:02 +09:00
Bart 671a00b1ac fix: Mask every credential the server echoes, logs or prints
An error reply echoes the request that caused it, and masking covers four
fields at the top level of an object only. A `secret` nested inside
`params`, where the JSON-RPC transport puts it, comes back in the clear,
as does every other credential field at any depth, and only two sites
mask at all. Collect the names in one list and mask recursively, so
nesting stops mattering and a new field is added once. The list covers
the six names only a reply carries, which is how `wallet_propose` and
`validation_create` wrote a live key to the log; `validation_key` is the
same seed as `validation_seed` in RFC1751 words.

A log is an echo that outlives the reply, so one `loggable()` helper
masks and truncates together and every site that writes a request or a
reply out uses it, the `[rpc_startup]` command and its result included,
and the command line client logs the reply it receives parsed and
masked where it wrote the raw body, a `validation_create` answer among
them, and caps a body it cannot parse at the same length. An
`[rpc_startup]` line that does not parse was echoed raw at fatal, the
one case masking cannot reach, and then run as whatever the parser had
left; it is now reported by position and parser message only, and
skipped. The `HTTP Reply` trace line in libxrpl, which cannot reach the
masker, now carries the status only; the body is logged beside it at
debug, masked when it carries a credential and otherwise as the string
already built for the wire, so a reply with no credential is serialized
once. No site logs an inbound request body uncapped, so the method name,
bounded by the request size limit, is the one thing a client chooses the
length of in the log. The request-duration line used to climb to warn
and error for a slow request with the request in it; the duration alone
still climbs, and the request stays at debug, so the duration line never
lifts text an anonymous client wrote to the default severity.

Three changes are visible to a caller. A credential in an echoed request
reads `<masked>` wherever it appears, nested inside `params` or a batch
entry too. The command line client masks `request_sent`, which carries
the `admin_password` it copies out of the config, so a failing
`./xrpld account_info rBogus` no longer prints a credential the operator
never typed. And a WebSocket frame that does not parse is answered with
its `size` rather than its body.
2026-10-14 10:14:02 +09:00
Bart 9096ec03a7 refactor: Declare the JSON-RPC and ripplerpc version constants
`ripplerpc`, which selects the reply envelope and accepts three values,
and `jsonrpc`, which names the JSON-RPC protocol version, are spelled as
literals at every call site, so neither field has one place stating what
it accepts. Declare `kRippleRpcVersion1/2/3` beside the `api_version`
constants in ApiVersion.h and `kJsonRpcVersion` in a new
protocol/JsonRpc.h, and use them at every production site that reads
a value: the one in RPCCall.cpp that reads the pair, and the three in
ServerHandler.cpp that default, compare and gate on `ripplerpc`. Every
test site spelling a value as a C++ expression follows. A literal inside
a JSON string fixture is left alone, since substituting there means
assembling the JSON by concatenation. The three `ripplerpc` constants
are declared as one set, so the header states every value the field
accepts, and each has a production user in ServerHandler.cpp.

The two fields keep separate constants and separate headers although
both spell "2.0" today. The specification fixes `jsonrpc` at that value
while `ripplerpc` accepts three, and the `ripplerpc` constants go when
support for API versions 1 and 2 goes, where JSON-RPC is a protocol this
server keeps speaking.

ApiVersion.h's header comment named five constants by unprefixed
spellings that no longer exist, and read as a complete map of the file's
version constants, which it stops being here. Two jtx helpers,
`hasEnvelope2` and `setEnvelope2` in TestHelpers.h, replace the
assertion pair and the request pair that every rewritten test site
repeated. No value changes, on the wire or in a test.
2026-10-14 10:14:02 +09:00
Bart 5fec493960 refactor: Let json::Value be compared and constructed from a string view
`json::Value` accepts `char const*`, `std::string` and
`json::StaticString`, so a caller holding a `std::string_view` has to
materialize a `std::string` whose characters are then copied a second
time into the value's own storage. Add the missing constructor, which
the `std::string` one delegates to, and an `operator==` that reads the
value's characters in place.

The comparison is constrained to `std::string_view` exactly rather than
taking a view by plain overload. A string literal converts equally well
to a view and to a Value, so a plain overload makes every
`value == "literal"` ambiguous, and a literal `0`, which converts to a
view through `char const*` as well as to a Value, with it.

No reply changes. The two spellings differ only for a view holding an
embedded NUL, which the Value comparison stops at. The `method: "batch"`
check in `ServerHandler.cpp` is the operator's first production user, so
that comparison stops building a `Value` from the literal on every
request.
2026-10-13 18:45:16 +09:00
Bart a6a4fff2aa refactor: Remove a publish loop nothing can enter
`pubProposedAccountTransaction` declares an `accountHistoryNotify` vector,
never inserts into it, and then tests it and iterates it. The vector is a
local, so the proof is the function itself: between the declaration and the
loop there are exactly two mentions of it, the condition and the loop, and
neither adds an element. Its sibling `pubAccountTransaction` fills its own
copy, which is what makes the empty one here read as live code.

The compiler corroborates it: with the loop gone the message becomes
`MultiApiJson const`, which is possible only because that loop was its sole
mutator. Nothing is lost with the assertion above the loop either. It held
that a `transJson` result carries no member named `account_history_tx_stream`.
No code writes one: outside the two assertions, the token appears in the
`subscribe` and `unsubscribe` request handlers only. The identical assertion
stays where the loop it guards is live.

No subscriber sees a difference. An account-history subscription is
registered in `subAccountHistory_` alone, so it was never in the map this
function reads, and it receives its transactions from
`pubAccountTransaction`. The same function's guard on three subscription maps
also goes: an earlier return leaves `subRTAccount_` non-empty, so the
condition cannot be false.
2026-10-13 18:45:15 +09:00
62 changed files with 2178 additions and 2927 deletions

No files matched your search

-15
View File
@@ -36,18 +36,3 @@ updates:
rust-dependencies:
patterns:
- "*"
- package-ecosystem: uv
directory: /cmake/scripts/codegen
schedule:
interval: weekly
day: monday
time: "04:00"
timezone: Etc/GMT
commit-message:
prefix: "chore: [DEPENDABOT] "
target-branch: develop
groups:
codegen-dependencies:
patterns:
- "*"
@@ -174,6 +174,7 @@ test.server > xrpl.basics
test.server > xrpl.config
test.server > xrpld.app
test.server > xrpld.core
test.server > xrpld.rpc
test.server > xrpl.json
test.server > xrpl.protocol
test.server > xrpl.server
+9
View File
@@ -22,6 +22,15 @@ API version 2 is available in `xrpld` version 2.0.0 and later. See [API-VERSION-
This version is supported by all `xrpld` versions. For WebSocket and HTTP JSON-RPC requests, it is currently the default API version used when no `api_version` is specified.
## Unreleased
### Bugfixes
- A request echoed back in an error reply now has every credential-bearing field masked: `admin_password`, `admin_user`, `passphrase`, `password`, `secret`, `seed`, `seed_hex`, `url_password`, `url_username` and `username`. Nesting no longer matters, so a credential inside `params` is masked too. The same masking is applied to every request and reply written to the log, and it covers six further names that only a reply carries: `master_key`, `master_seed`, `master_seed_hex`, `validation_key`, `validation_private_key` and `validation_seed`, which is how `wallet_propose` and `validation_create` used to write a live private key to the log. A request or reply written to the log is truncated at 10,000 characters. An `[rpc_startup]` line that does not parse is reported by its position in the section and the parser's message, never by its text, and no command runs for it.
- The command line client no longer prints a credential the operator did not type. A failing command echoes the request it built under `request_sent`, which carries the `admin_password` the client copies out of `[port_rpc]` in the config, so `./xrpld account_info rBogus` printed that password to stdout and into any captured output. `request_sent` is now masked. The `rpc` member beside it, which echoes the arguments as they were typed, is unchanged. The command line client also no longer writes an unparsed `json` or `ripple_path_find` argument to its trace log before parsing it, where a `secret` inside that argument could not be masked; it logs the parsed request instead, masked. The reply it receives is logged the same way, parsed and masked, where the raw body was written before, a `validation_create` answer included.
- A WebSocket frame that does not parse, or exceeds the request size limit, is answered `{"type": "error", "error": "jsonInvalid", "size": <bytes>}`. The frame's body is reported by size rather than echoed back in a `value` member, since a body that does not parse has no fields to mask. A client that read `value` gets `size` instead.
- Four error codes that named no HTTP status of their own, and so answered 200 on a reply reporting an error, now name one: `actMalformed`, `alreadyMultisig` and `alreadySingleSig` answer 400, and `actNotFound` answers 404. **No shipped envelope reports these four.** A request sending `ripplerpc: "3.0"` still receives 200 for all four, as it always has, so `account_info` on a malformed account or one the ledger does not hold answers 200 exactly as before.
## XRP Ledger server version 3.5.0
Version 3.5.0 is not yet released.
+1 -1
View File
@@ -10,4 +10,4 @@ pcpp>=1.30
pyparsing>=3.0.0
# Template engine - used to generate C++ code from templates
Mako>=1.4.2
Mako>=1.2.2
+3 -3
View File
@@ -1,8 +1,8 @@
# This file was autogenerated by uv via the following command:
# uv pip compile requirements.in --generate-hashes --output-file requirements.txt
mako==1.4.3 \
--hash=sha256:723296007c870bfd6b3f0c3230dba7198096e5269297ebf5e4eff9e7ffa39d4f \
--hash=sha256:cd6537fe88d5fec315c55c2f8529bc4ce7a9a352ad7db3eeaa6a66e2dd4ec37a
mako==1.3.12 \
--hash=sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9 \
--hash=sha256:9f778e93289bd410bb35daadeb4fc66d95a746f0b75777b942088b7fd7af550a
# via -r requirements.in
markupsafe==3.0.3 \
--hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \
+37
View File
@@ -3,11 +3,13 @@
#include <xrpl/basics/Number.h>
#include <xrpl/json/json_forwards.h>
#include <concepts>
#include <cstring>
#include <iterator>
#include <limits>
#include <map>
#include <string>
#include <string_view>
#include <vector>
/**
@@ -198,6 +200,15 @@ public:
*/
Value(StaticString const& value);
Value(std::string const& value);
/**
* @brief Constructs a value from a string view.
*
* The characters are copied, so the view need not outlive the call and need
* not be NUL-terminated.
*
* @param value The characters to copy.
*/
Value(std::string_view value);
Value(bool value);
Value(Value const& other);
~Value();
@@ -472,6 +483,32 @@ toJson(xrpl::Number const& number)
bool
operator==(Value const&, Value const&);
/**
* Compares a value with a string view, reading the value's characters in place
* rather than building a Value from the view.
*
* Constrained to the exact type: a string literal converts equally well to a
* view and to a Value, so a plain overload makes `value == "literal"`
* ambiguous.
*
* @param x The value to compare.
* @param y The characters to compare it against.
* @return Whether `x` is a string whose characters up to its first NUL are
* exactly the characters of `y`.
*/
template <class T>
requires std::same_as<T, std::string_view>
bool
operator==(Value const& x, T y)
{
if (!x.isString())
return false;
// A string `Value` can hold a null pointer, which names no characters, so it equals no view.
char const* const s = x.asCString();
return s != nullptr && std::string_view{s} == y;
}
bool
operator<(Value const&, Value const&);
+18 -323
View File
@@ -1,12 +1,10 @@
#pragma once
#include <xrpl/basics/Number.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/Rules.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/TER.h>
@@ -19,216 +17,6 @@ namespace xrpl {
class STTx;
/**
* @brief Return the Vault's current assets total.
*
* FixedPrecision Vaults return AssetsAvailable plus AssetsDeployed, with no
* rounding to the asset's 16-digit precision: the sum is exact at Number's
* active 19-digit mantissa width (the large mantissa range, enabled by
* featureSingleAssetVault or featureLendingProtocol), rounds Downward regardless of the
* caller's ambient mode beyond that, and is not rounded to match the cached
* sfAssetsTotal field. Legacy and CashBasis Vaults return the stored
* AssetsTotal.
*
* @param vault The vault SLE.
* @return The current assets total.
*/
[[nodiscard]] Number
getAssetsTotal(SLE::ConstRef vault);
/**
* @brief A change to a FixedPrecision Vault's balances.
*/
struct VaultBalanceChange
{
/**
* Signed change to AssetsAvailable. Must be the exact amount moved on the
* Vault pseudo-account in the same transaction. Absent means no cash
* moved; adjustVaultBalances then uses zero of the Vault's own asset.
*/
std::optional<STAmount> cash = std::nullopt;
/**
* Signed change to AssetsDeployed: open Loan principal, exact.
*/
Number deployed = 0;
/**
* Signed change to scheduled, unpaid interest (YieldUnrealized), exact.
*/
Number yield = 0;
/**
* Signed change to impaired principal (LossUnrealized), exact.
*/
Number loss = 0;
};
/**
* @brief Apply a balance change to a FixedPrecision Vault. The only writer of
* AssetsAvailable, AssetsDeployed, AssetsTotal, YieldUnrealized and
* LossUnrealized on these Vaults after creation.
*
* @param vault The vault SLE. Must be FixedPrecision.
* @param change The balance change to apply.
* @param j Journal for logging the fatal (tefBAD_LEDGER) case.
*
* @return tesSUCCESS; tecLIMIT_EXCEEDED if LossUnrealized would exceed
* AssetsDeployed; tefBAD_LEDGER if AssetsAvailable, AssetsDeployed,
* LossUnrealized or YieldUnrealized would become negative.
*/
[[nodiscard]] TER
adjustVaultBalances(SLE::Ref vault, VaultBalanceChange const& change, beast::Journal j);
/**
* Return the Vault's current live exponent.
*
* Legacy and CashBasis Vaults use the exponent of AssetsTotal. FixedPrecision
* Vaults use max(base exponent, exponent of AssetsTotal): the grid is never
* finer than -Scale and coarsens once AssetsTotal outgrows 16 digits there.
*/
[[nodiscard]] int
getVaultScale(SLE::ConstRef vault);
/**
* Return the Vault's base exponent.
*
* Legacy and CashBasis Vaults use their current live exponent. FixedPrecision
* Vaults use -Scale, or 0 for integral assets.
*/
[[nodiscard]] int
getVaultBaseScale(SLE::ConstRef vault);
/**
* Round an amount at the Vault's posterior live exponent.
*/
[[nodiscard]] STAmount
roundToPosteriorVaultScale(
SLE::ConstRef vault,
STAmount const& amount,
Number::RoundingMode roundingMode);
namespace detail {
/**
* Return the Vault's posterior live exponent after applying an unrounded delta.
*/
[[nodiscard]] int
getPosteriorVaultScale(SLE::ConstRef vault, STAmount const& delta);
/**
* Shared grid-floor core of getVaultScale / getPosteriorVaultScale /
* getPosteriorBrokerCoverScale: baseScale when reference is zero, otherwise
* max(baseScale, scale(reference, asset)), rounded Downward regardless of
* the caller's ambient rounding mode.
*/
[[nodiscard]] int
liveScale(Number const& reference, Asset const& asset, int baseScale);
/**
* Shared core of getPosteriorVaultScale and getPosteriorBrokerCoverScale:
* the exponent reference + delta would have under version's live-scale rule
* (floored at baseScale for FixedPrecision, unbounded for Legacy/CashBasis).
*
* @param version The Vault's LEVersion.
* @param asset The Vault's underlying asset.
* @param baseScale The Vault's base exponent; unused outside the
* FixedPrecision case.
* @param reference The balance the delta is applied to (AssetsAvailable,
* AssetsTotal, or a LoanBroker's CoverAvailable).
* @param delta The unrounded change to reference.
*/
[[nodiscard]] int
posteriorAssetScale(
VaultVersion version,
Asset const& asset,
int baseScale,
Number const& reference,
Number const& delta);
/**
* Shared core of creditToPosteriorAvailableScale and
* creditToPosteriorBrokerCoverScale: floors reference + raw at scale and
* returns the difference from reference, so the delta applied is exactly
* what moves reference onto the floored sum. See
* creditToPosteriorAvailableScale's doc for why the sum, not raw alone,
* must be floored. raw is the exact amount; it is not rounded on its own
* before being added to reference.
*/
[[nodiscard]] STAmount
creditToPosteriorScale(
Asset const& asset,
Number const& reference,
int atScale,
Number const& raw,
Number::RoundingMode roundingMode);
} // namespace detail
/**
* Round a FixedPrecision cash inflow (LoanPay's credit, LoanManage's
* default cover credit) into AssetsAvailable, never AssetsTotal, which is
* a derived cache, by flooring the sum rather than the delta:
* credit equals floor16(AssetsAvailable + raw) minus AssetsAvailable, never
* finer than the Vault's base exponent (-Scale).
*
* Flooring only the delta at its own posterior grid is not enough: the sum
* can still need a 17th digit when AssetsAvailable is off the coarser grid
* right after crossing a power of ten. Example at Scale 6: AssetsAvailable
* equals 9999999999.999999 (16 digits already) and a credit of 0.000011
* floors to 0.00001 on its own (finer) grid, but 9999999999.999999 plus
* 0.00001 equals 10000000000.000009, which still needs 17 digits. Flooring
* the sum instead, floor16(9999999999.999999 + 0.000011) minus
* 9999999999.999999, is exact by construction, since STAmount's own
* 16-digit canonical form of the sum is what gets subtracted from.
*
* raw is taken as an exact Number: rounding it to 16 digits before adding
* it to AssetsAvailable could drop a tail that moves the floored sum.
*/
[[nodiscard]] STAmount
creditToPosteriorAvailableScale(
SLE::ConstRef vault,
Number const& raw,
Number::RoundingMode roundingMode);
/**
* Open-zone capacity ceiling: 9 * 10^(15 + baseScale).
*
* Defined only for FixedPrecision Vaults, where this is 9 * 10^(15 - P).
*/
[[nodiscard]] Number
getVaultOpenLimit(SLE::ConstRef vault);
/**
* Check whether amount is an admissible optional inflow.
*
* Legacy and CashBasis Vaults always succeed. FixedPrecision Vaults must
* remain at their base scale after applying the rounded amount, and the
* posterior capacity (AssetsTotal + YieldUnrealized + rounded amount) must
* stay within the Open zone.
*
* The amount is rounded toward zero at the posterior live exponent.
*/
[[nodiscard]] TER
checkOptionalVaultInflow(SLE::ConstRef vault, STAmount const& amount);
/**
* Open-zone capacity after adding roundedAmount: AssetsTotal + YieldUnrealized
* + roundedAmount, computed TowardsZero regardless of the caller's ambient
* rounding mode. Defined only for FixedPrecision Vaults.
*
* Shared by checkOptionalVaultInflow and LoanSet's loan-origination capacity
* check, which pair this formula with different scale checks (posterior vs.
* current), so only the capacity formula itself -- not the whole check -- is
* shared here.
*
* @param vault The vault SLE. Must be FixedPrecision.
* @param roundedAmount The amount to add, already rounded to the relevant
* posterior or base scale by the caller.
*/
[[nodiscard]] Number
vaultOpenZoneCapacity(SLE::ConstRef vault, Number const& roundedAmount);
/**
* From the perspective of a vault, return the number of shares to give
* depositor when they offer a fixed amount of assets. Note, since shares are
@@ -258,81 +46,31 @@ assetsToSharesDeposit(SLE::ConstRef vault, SLE::ConstRef issuance, STAmount cons
sharesToAssetsDeposit(SLE::ConstRef vault, SLE::ConstRef issuance, STAmount const& shares);
/**
* Adjusts a requested asset change (delta) to match the decimal scale of the
* updated total vault assets. This ensures sfAssetsTotal, sfAssetsAvailable,
* Adjusts a requested asset change (`delta`) to match the decimal scale of the
* updated total vault assets. This ensures `sfAssetsTotal`, `sfAssetsAvailable`,
* and the actual asset transfer change by the exact same representable amount.
*
* Rounding strategy:
* - Legacy/CashBasis debits (withdrawals): rounds down the magnitude of delta
* on the new scale to prevent paying out more than requested.
* - Legacy/CashBasis credits: floors the resulting total asset balance and
* returns the difference from the current total.
* - FixedPrecision credits only (VaultDeposit): rounds the delta toward zero
* at the scale getAssetsTotal (AssetsAvailable + AssetsDeployed, derived,
* not the stored AssetsTotal cache) would have after applying delta.
* FixedPrecision outflows (VaultWithdraw, VaultClawback) never call this;
* they use clampVaultOutflow instead.
* - Debits (withdrawals): Rounds down `|delta|` on the new scale to prevent
* paying out more than requested.
* - Credits (deposits): Floors the resulting total asset balance and returns the
* difference from the current total. This prevents crediting the vault with
* more assets than the user deposited.
*
* Key rules:
* - The returned magnitude never exceeds the magnitude of delta.
* - Returns tecPRECISION_LOSS if the change is smaller than 1 ULP of the target scale
* - The returned magnitude never exceeds `|delta|`.
* - Returns `tecPRECISION_LOSS` if the change is smaller than 1 ULP of the target scale
* (prevents share operations when totals cannot change).
* - For integer assets (XRP, MPT), rounding is a no-op.
*
* @param vault The vault ledger entry.
* @param delta The requested signed change to sfAssetsTotal.
* @return The rounded, positive magnitude, or tecPRECISION_LOSS if the
* @return The rounded, positive magnitude, or `tecPRECISION_LOSS` if the
* change is below representable precision.
*/
[[nodiscard]] std::expected<STAmount, TER>
clampToAssetsTotalScale(SLE::ConstRef vault, STAmount const& delta);
/**
* Outflow clamp dispatcher for VaultWithdraw and VaultClawback: rounds a
* negative delta to the scale appropriate for the Vault's version -- for
* FixedPrecision, at AssetsAvailable's own posterior scale (the balance the
* cash actually leaves); for Legacy/CashBasis, clampToAssetsTotalScale --
* so both call sites share one dispatch point. Same tecPRECISION_LOSS and
* integral-asset and magnitude-never-exceeds-delta rules as
* clampToAssetsTotalScale.
*
* @param vault The vault ledger entry.
* @param delta The requested signed outflow (negative).
* @return The rounded, positive magnitude, or tecPRECISION_LOSS if the
* change is below representable precision.
*/
[[nodiscard]] std::expected<STAmount, TER>
clampVaultOutflow(SLE::ConstRef vault, STAmount const& delta);
/**
* Returns the Vault's base exponent for asset at scale, before a Vault
* object exists to read it from: 0 for an integral asset, -scale otherwise.
* Used by VaultCreate::preclaim, which only has the proposed Scale field,
* not yet a Vault SLE; getVaultBaseScale is the post-creation equivalent.
*
* @param asset The (prospective) Vault's underlying asset.
* @param scale The (prospective) Vault's sfScale value, ignored for
* integral assets.
*/
[[nodiscard]] int
vaultBaseScale(Asset const& asset, std::uint8_t scale);
/**
* Returns true iff value is exactly representable both as an STAmount of
* asset and on the base grid at baseScale (no precision lost rounding
* TowardsZero at baseScale). Returns false, rather than throwing, for a value
* out of range for the asset. Shared representability check for
* VaultCreate::preclaim, VaultSet::preclaim and LoanBrokerSet::preclaim's
* AssetsMaximum / DebtMaximum / cover-rate field checks.
*
* @param asset The vault's underlying asset.
* @param value The value to check.
* @param baseScale The vault's base exponent (see vaultBaseScale /
* getVaultBaseScale).
*/
[[nodiscard]] bool
isOnVaultBaseGrid(Asset const& asset, Number const& value, int baseScale);
/**
* Controls whether to truncate shares instead of rounding.
*/
@@ -360,9 +98,9 @@ enum class WaiveUnrealizedLoss : bool { No = false, Yes = true };
assetsTotalForWithdrawal(SLE::ConstRef vault, WaiveUnrealizedLoss waive);
/**
* Returns true if debiting amount from total (the current value of a
* Returns true if debiting `amount` from `total` (the current value of a
* vault's sfAssetsTotal or sfAssetsAvailable) would canonicalize to the
* same STAmount value. This happens when amount is non-zero but too small
* same STAmount value. This happens when `amount` is non-zero but too small
* to change the stored total at STAmount's precision. Shares would still
* move, so the ValidVault invariant would fail after apply; callers use
* this to reject the transaction upfront instead.
@@ -376,20 +114,6 @@ assetsTotalForWithdrawal(SLE::ConstRef vault, WaiveUnrealizedLoss waive);
[[nodiscard]] bool
debitIsNonZeroDust(Asset const& asset, Number const& total, Number const& amount);
/**
* Returns the reference balance a cash outflow's dust check
* (debitIsNonZeroDust) should compare against: FixedPrecision Vaults use
* AssetsAvailable, the balance the cash actually leaves; Legacy/CashBasis
* Vaults use the given AssetsTotal. Shared by VaultWithdraw and
* VaultClawback.
*
* @param vault The vault SLE.
* @param assetsTotal The vault's current AssetsTotal, as already read by the
* caller.
*/
[[nodiscard]] Number
vaultDebitDustReference(SLE::ConstRef vault, Number const& assetsTotal);
/**
* From the perspective of a vault, return the number of shares to demand from
* the depositor when they ask to withdraw a fixed amount of assets. Since
@@ -434,7 +158,7 @@ sharesToAssetsWithdraw(
WaiveUnrealizedLoss waive = WaiveUnrealizedLoss::No);
/**
* Returns true iff account holds all of the vault's outstanding shares,
* Returns true iff `account` holds all of the vault's outstanding shares —
* i.e. is the sole remaining shareholder. Returns false if the account
* holds no shares or fewer than the total outstanding.
*
@@ -447,13 +171,11 @@ sharesToAssetsWithdraw(
isSoleShareholder(ReadView const& view, AccountID const& account, SLE::ConstRef issuance);
/**
* Resolves a Vault's LEVersion.
*
* LEVersion is the single point every accounting and rounding helper
* should call to decide which protocol a Vault follows. It is written
* at VaultCreate and is not updated afterwards, so a Vault created
* under an older amendment keeps that behaviour after later amendments
* activate. Absent sfLEVersion resolves to VaultVersion::Legacy.
* Resolves a Vault's LEVersion, the single point every accounting touch
* point should call to determine which recognition model (instant interest
* recognition vs. cash-basis) a Vault uses. Vaults created before featureLendingProtocolV1_1
* activated never have sfLEVersion set, which resolves here to
* VaultVersion::Legacy.
*
* @param vault The vault SLE.
*
@@ -463,33 +185,6 @@ isSoleShareholder(ReadView const& view, AccountID const& account, SLE::ConstRef
[[nodiscard]] VaultVersion
getVaultVersion(SLE::ConstRef vault);
/**
* Decodes an already-extracted sfLEVersion value with the same range check as
* getVaultVersion. Usable from contexts, such as an invariant's ledger-entry
* snapshot, that keep the field value but not the owning SLE.
*
* @param leVersion The value of sfLEVersion, or nullopt if absent.
*
* @return The decoded LEVersion, or VaultVersion::Legacy if absent.
*/
[[nodiscard]] VaultVersion
decodeVaultVersion(std::optional<std::uint8_t> leVersion);
/**
* Resolves which LEVersion a newly-created Vault should get under the
* currently active amendments. This is the only place that decides Vault
* version policy; VaultCreate calls it instead of checking amendments
* directly.
*
* @param rules The active ledger rules.
*
* @return VaultVersion::FixedPrecision once featureLendingProtocolV1_2,
* fixCleanup3_2_0 and fixCleanup3_4_0 are all enabled; VaultVersion::CashBasis
* once featureLendingProtocolV1_1 is enabled; VaultVersion::Legacy otherwise.
*/
[[nodiscard]] VaultVersion
getVaultDefaultVersion(Rules const& rules);
/**
* Resolves the VaultKind of a vault SLE. Returns VaultKind::ClosedEnded when
* sfVaultKind is present and equal to that value; anything else (including an
+17 -17
View File
@@ -6,31 +6,21 @@
#include <xrpl/protocol/jss.h>
#include <cstddef>
#include <string_view>
#include <type_traits>
#include <utility>
namespace xrpl {
/**
* API version numbers used in later API versions
* The `api_version` numbers this server serves.
*
* Requests with a version number in the range
* [apiMinimumSupportedVersion, apiMaximumSupportedVersion]
* are supported.
* A request naming a version in [kApiMinimumSupportedVersion,
* kApiMaximumSupportedVersion] is served. With `[beta_rpc_api]` set to `1` in
* the config the range extends to kApiBetaVersion.
*
* If [beta_rpc_api] is enabled in config, the version numbers
* in the range [apiMinimumSupportedVersion, apiBetaVersion]
* are supported.
*
* Network Requests without explicit version numbers use
* apiVersionIfUnspecified. apiVersionIfUnspecified is 1,
* because all the RPC requests with a version >= 2 must
* explicitly specify the version in the requests.
* Note that apiVersionIfUnspecified will be lower than
* apiMinimumSupportedVersion when we stop supporting API
* version 1.
*
* Command line Requests use apiCommandLineVersion.
* A request naming no version is served at kApiVersionIfUnspecified, which is 1
* because a request wanting any later version states it.
*/
namespace rpc {
@@ -57,6 +47,16 @@ static_assert(kApiMaximumSupportedVersion >= kApiMinimumSupportedVersion);
static_assert(kApiBetaVersion >= kApiMaximumSupportedVersion);
static_assert(kApiMaximumValidVersion >= kApiMaximumSupportedVersion);
/**
* Values accepted in the `ripplerpc` request field, which selects the shape of
* the JSON-RPC reply envelope. Distinct from `kJsonRpcVersion` in JsonRpc.h,
* which names the JSON-RPC protocol itself, and from the `api_version`
* constants above, which select the content of the response.
*/
inline constexpr std::string_view kRippleRpcVersion1{"1.0"};
inline constexpr std::string_view kRippleRpcVersion2{"2.0"};
inline constexpr std::string_view kRippleRpcVersion3{"3.0"};
inline void
setVersion(json::Value& parent, unsigned int apiVersion, bool betaEnabled)
{
-5
View File
@@ -180,11 +180,6 @@ struct ErrorInfo
{
}
constexpr ErrorInfo(ErrorCodeI code, char const* token, char const* message)
: code(code), token(token), message(message), httpStatus(200)
{
}
constexpr ErrorInfo(ErrorCodeI code, char const* token, char const* message, int httpStatus)
: code(code), token(token), message(message), httpStatus(httpStatus)
{
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include <string_view>
namespace xrpl::rpc {
/**
* Constants of the JSON-RPC 2.0 protocol itself.
*
* Kept apart from the `api_version` and `ripplerpc` constants in ApiVersion.h,
* which go when support for API versions 1 and 2 goes.
*/
/**
* Value of the `jsonrpc` member of a request and of its reply, fixed at "2.0"
* by the JSON-RPC specification.
*/
inline constexpr std::string_view kJsonRpcVersion{"2.0"};
} // namespace xrpl::rpc
+7 -29
View File
@@ -311,46 +311,24 @@ constexpr std::uint8_t kVaultStrategyFirstComeFirstServe = 1;
* Default IOU scale factor for a Vault
*/
constexpr std::uint8_t kVaultDefaultIouScale = 6;
/**
* Maximum Scale for a Vault created before featureLendingProtocolV1_2.
* Chosen so 1 IOU can always convert to shares:
* 10^19 > maxMPTokenAmount (2^64-1) > 10^18.
* Maximum scale factor for a Vault. The number is chosen to ensure that
* 1 IOU can be always converted to shares.
* 10^19 > maxMPTokenAmount (2^64-1) > 10^18
*/
constexpr std::uint8_t kVaultMaximumIouScale = 18;
/**
* Maximum Scale for a Vault created under featureLendingProtocolV1_2.
*/
constexpr std::uint8_t kVaultMaximumFixedPrecisionIouScale = 10;
/**
* Vault ledger-entry schema versions, persisted as sfLEVersion.
*
* LEVersion records which protocol a Vault was created under so later
* amendments can change the rules for new Vaults without rewriting
* existing ones. VaultCreate writes it from the then-active lending
* amendments; later transactions do not update it. A Vault created
* under an older amendment keeps that amendment's behaviour for its
* lifetime, even after a newer lending amendment activates.
*
* Absent sfLEVersion is implicit Legacy (version 0): instant interest
* recognition and a dynamic AssetsTotal scale. CashBasis (V1.1) uses
* cash-basis recognition on the same dynamic scale. FixedPrecision
* (V1.2) keeps cash-basis recognition and adds the lifetime base grid.
* Vault ledger-entry schema versions. Assigned to newly created
* Vaults once featureLendingProtocolV1_1 is enabled. Vaults created before
* activation are left without LEVersion (implicit legacy version 0,
* instant interest recognition).
*/
enum class VaultVersion : uint8_t {
Legacy = 0,
CashBasis,
FixedPrecision,
};
// Code compares VaultVersion values with < and >= (e.g. "CashBasis or later"),
// so each later version must stay numerically larger than the one before.
static_assert(
VaultVersion::Legacy < VaultVersion::CashBasis &&
VaultVersion::CashBasis < VaultVersion::FixedPrecision);
/**
* Vault kind. Distinguishes closed-ended vaults from the default open-ended
* kind. Persisted as sfVaultKind (UINT8); absent means OpenEnded.
-15
View File
@@ -766,21 +766,6 @@ roundToAsset(
return roundToScale(ret, scale);
}
/**
* Check whether an arbitrary precision Number can be converted to an STAmount
* of a given Asset without exceeding the asset's range.
*
* Use this before `roundToAsset` or `STAmount{asset, value}` on unchecked
* input, since those throw for out-of-range values, e.g. XRP above
* `STAmount::kMaxNativeN`.
*
* @param asset The relevant asset
* @param value The value to be checked
* @return true if the value is in range for the asset; false otherwise.
*/
[[nodiscard]] bool
isRepresentable(Asset const& asset, Number const& value);
//------------------------------------------------------------------------------
inline bool
@@ -17,7 +17,6 @@
XRPL_FEATURE(Cosign, Supported::No, VoteBehavior::DefaultNo)
XRPL_FEATURE(SmartEscrow, Supported::No, VoteBehavior::DefaultNo)
// Requires LendingProtocolV1_1. New vaults take FixedPrecision plus cash-basis.
XRPL_FEATURE(LendingProtocolV1_2, Supported::No, VoteBehavior::DefaultNo)
XRPL_FIX (Cleanup3_5_0, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(ConfidentialMPTKeyRotation, Supported::No, VoteBehavior::DefaultNo)
@@ -514,8 +514,6 @@ LEDGER_ENTRY(ltVAULT, 0x0084, Vault, vault, ({
{sfAssetsAvailable, SoeDefault},
{sfAssetsMaximum, SoeDefault},
{sfLossUnrealized, SoeDefault},
{sfYieldUnrealized, SoeDefault},
{sfAssetsDeployed, SoeDefault},
{sfShareMPTID, SoeRequired},
{sfWithdrawalPolicy, SoeRequired},
{sfScale, SoeDefault},
@@ -240,8 +240,6 @@ TYPED_SFIELD(sfPrincipalRequested, NUMBER, 14)
TYPED_SFIELD(sfTotalValueOutstanding, NUMBER, 15, SField::kSmdNeedsAsset | SField::kSmdDefault)
TYPED_SFIELD(sfPeriodicPayment, NUMBER, 16)
TYPED_SFIELD(sfManagementFeeOutstanding, NUMBER, 17, SField::kSmdNeedsAsset | SField::kSmdDefault)
TYPED_SFIELD(sfYieldUnrealized, NUMBER, 18, SField::kSmdNeedsAsset | SField::kSmdDefault)
TYPED_SFIELD(sfAssetsDeployed, NUMBER, 19, SField::kSmdNeedsAsset | SField::kSmdDefault)
// 32-bit signed (common)
TYPED_SFIELD(sfLoanScale, INT32, 1)
+1 -1
View File
@@ -559,7 +559,7 @@ JSS(signing_key); // out: NetworkOPs
JSS(signing_keys); // out: ValidatorList
JSS(signing_time); // out: NetworkOPs
JSS(signer_lists); // in/out: AccountInfo
JSS(size); // out: get_aggregate_price
JSS(size); // out: get_aggregate_price, ServerHandler
JSS(snapshot); // in: Subscribe
JSS(source_account); // in: PathRequest, RipplePathFind
JSS(source_amount); // in: PathRequest, RipplePathFind
@@ -242,54 +242,6 @@ public:
return this->sle_->isFieldPresent(sfLossUnrealized);
}
/**
* @brief Get sfYieldUnrealized (SoeDefault)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_NUMBER::type::value_type>
getYieldUnrealized() const
{
if (hasYieldUnrealized())
return this->sle_->at(sfYieldUnrealized);
return std::nullopt;
}
/**
* @brief Check if sfYieldUnrealized is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasYieldUnrealized() const
{
return this->sle_->isFieldPresent(sfYieldUnrealized);
}
/**
* @brief Get sfAssetsDeployed (SoeDefault)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_NUMBER::type::value_type>
getAssetsDeployed() const
{
if (hasAssetsDeployed())
return this->sle_->at(sfAssetsDeployed);
return std::nullopt;
}
/**
* @brief Check if sfAssetsDeployed is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasAssetsDeployed() const
{
return this->sle_->isFieldPresent(sfAssetsDeployed);
}
/**
* @brief Get sfShareMPTID (SoeRequired)
* @return The field value.
@@ -619,28 +571,6 @@ public:
return *this;
}
/**
* @brief Set sfYieldUnrealized (SoeDefault)
* @return Reference to this builder for method chaining.
*/
VaultBuilder&
setYieldUnrealized(std::decay_t<typename SF_NUMBER::type::value_type> const& value)
{
object_[sfYieldUnrealized] = value;
return *this;
}
/**
* @brief Set sfAssetsDeployed (SoeDefault)
* @return Reference to this builder for method chaining.
*/
VaultBuilder&
setAssetsDeployed(std::decay_t<typename SF_NUMBER::type::value_type> const& value)
{
object_[sfAssetsDeployed] = value;
return *this;
}
/**
* @brief Set sfShareMPTID (SoeRequired)
* @return Reference to this builder for method chaining.
+12
View File
@@ -7,6 +7,18 @@
namespace xrpl {
/**
* Writes an HTTP reply carrying @p strMsg with status @p nStatus to @p output,
* and logs the status at trace. The body is not logged here: it may carry a
* credential this library cannot mask, so the caller logs it masked.
*
* A 401 with an empty body is answered with the fixed authentication page.
*
* @param nStatus The HTTP status code.
* @param strMsg The body.
* @param output Where the reply bytes are written.
* @param j The journal the status is logged to.
*/
void
httpReply(int nStatus, std::string const& strMsg, json::Output const&, beast::Journal j);
+8 -3
View File
@@ -15,6 +15,7 @@
#include <cstring>
#include <limits>
#include <string>
#include <string_view>
#include <utility>
namespace json {
@@ -241,10 +242,14 @@ Value::Value(xrpl::Number const& value) : type_(ValueType::String), allocated_(t
value_.stringVal = valueAllocator()->duplicateStringValue(tmp.c_str(), tmp.length());
}
Value::Value(std::string const& value) : type_(ValueType::String), allocated_(true)
Value::Value(std::string const& value) : Value(std::string_view{value})
{
value_.stringVal =
valueAllocator()->duplicateStringValue(value.c_str(), (unsigned int)value.length());
}
Value::Value(std::string_view value) : type_(ValueType::String), allocated_(true)
{
value_.stringVal = valueAllocator()->duplicateStringValue(
value.data(), static_cast<unsigned int>(value.length()));
}
Value::Value(StaticString const& value) : type_(ValueType::String)
+47 -505
View File
@@ -1,16 +1,12 @@
#include <xrpl/ledger/helpers/VaultHelpers.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Number.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/CredentialHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h> // IWYU pragma: keep
#include <xrpl/protocol/Protocol.h>
@@ -21,7 +17,6 @@
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <algorithm>
#include <cstdint>
#include <expected>
#include <optional>
@@ -29,405 +24,6 @@
namespace xrpl {
namespace {
[[nodiscard]] int
fixedBaseScale(SLE::ConstRef vault)
{
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::fixedBaseScale : valid Vault sle");
Asset const asset = vault->at(sfAsset);
// sfScale is never written for an integral asset (VaultCreate only sets
// it when scale != 0, and integral assets always get scale 0), so it
// must not be read here; vaultBaseScale ignores the scale argument for
// an integral asset anyway.
if (asset.integral())
return 0;
return vaultBaseScale(asset, vault->at(sfScale));
}
// Thin, vault-specific wrapper over posteriorAssetScale: used by
// getPosteriorVaultScale and creditToPosteriorAvailableScale.
[[nodiscard]] int
posteriorScale(SLE::ConstRef vault, Number const& reference, Number const& delta)
{
return detail::posteriorAssetScale(
getVaultVersion(vault), vault->at(sfAsset), fixedBaseScale(vault), reference, delta);
}
// FixedPrecision-only counterpart to clampToAssetsTotalScale, for cash
// outflows (VaultWithdraw, VaultClawback): rounds the magnitude of delta
// toward zero at AssetsAvailable's own posterior scale, the balance the cash
// actually leaves, instead of the derived AssetsTotal cache. Same
// tecPRECISION_LOSS and integral-asset and magnitude-never-exceeds-delta
// rules as clampToAssetsTotalScale. Not part of the public interface; reached
// only through clampVaultOutflow.
//
// For an outflow it is safe to round the delta directly, unlike the credit
// case: amount on the posterior grid of AssetsAvailable always leaves
// AssetsAvailable minus amount representable, since the posterior grid is
// exactly the grid AssetsAvailable itself will canonicalize to after the
// subtraction.
[[nodiscard]] std::expected<STAmount, TER>
clampToAvailableScale(SLE::ConstRef vault, STAmount const& delta)
{
XRPL_ASSERT(
delta.asset() == vault->at(sfAsset),
"xrpl::clampToAvailableScale : delta and vault asset match");
XRPL_ASSERT(
getVaultVersion(vault) == VaultVersion::FixedPrecision,
"xrpl::clampToAvailableScale : FixedPrecision Vault");
XRPL_ASSERT(delta.negative(), "xrpl::clampToAvailableScale : outflow delta is negative");
Asset const asset = vault->at(sfAsset);
STAmount magnitude = delta.negative() ? -delta : delta;
if (asset.integral())
return magnitude;
STAmount const rounded = roundToScale(
delta,
posteriorScale(vault, vault->at(sfAssetsAvailable), delta),
Number::RoundingMode::TowardsZero);
STAmount actualDelta = rounded.negative() ? -rounded : rounded;
XRPL_ASSERT(
abs(actualDelta) <= abs(delta),
"xrpl::clampToAvailableScale : actual delta smaller or equal to calculated delta");
// Reject changes below scale precision (1 ULP) to prevent share balance changes
// without corresponding asset movements.
if (actualDelta <= beast::kZero)
return std::unexpected(tecPRECISION_LOSS);
return actualDelta;
}
[[nodiscard]] VaultKind
decodeVaultKind(std::optional<std::uint8_t> vaultKind)
{
if (vaultKind && *vaultKind == std::to_underlying(VaultKind::ClosedEnded))
return VaultKind::ClosedEnded;
return VaultKind::OpenEnded;
}
// Write cached AssetsTotal from getAssetsTotal, as the exact total rounded
// Downward to the Vault asset's 16-digit STAmount representation. Below
// coarsening this equals the exact total; above it, the cache is a
// deliberate floor of the exact value. Internal to adjustVaultBalances,
// the only writer of AssetsAvailable, AssetsDeployed, AssetsTotal,
// YieldUnrealized and LossUnrealized on a FixedPrecision Vault after
// creation; not part of the public interface.
void
syncAssetsTotal(SLE::Ref vault)
{
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::syncAssetsTotal : valid Vault sle");
Asset const asset = vault->at(sfAsset);
// adjustVaultBalances writes AssetsAvailable as an already-canonical
// STAmount before calling this, so the associateAsset pass a transactor
// runs afterwards is always a no-op. If this fires, AssetsAvailable was
// written some other way.
XRPL_ASSERT(
(STAmount{asset, vault->at(sfAssetsAvailable)} == vault->at(sfAssetsAvailable)),
"xrpl::syncAssetsTotal : AssetsAvailable is a 16-digit STAmount value");
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
vault->at(sfAssetsTotal) = STAmount{asset, getAssetsTotal(vault)};
}
} // namespace
namespace detail {
[[nodiscard]] int
liveScale(Number const& reference, Asset const& asset, int baseScale)
{
if (reference == beast::kZero)
return baseScale;
// Round Downward, not the ambient mode: the live exponent must match the
// exponent syncAssetsTotal actually stores (also Downward), regardless of
// what rounding mode the caller's own arithmetic is using.
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
return std::max(baseScale, scale(reference, asset));
}
[[nodiscard]] int
posteriorAssetScale(
VaultVersion version,
Asset const& asset,
int baseScale,
Number const& reference,
Number const& delta)
{
// ToNearest for the sum and for scale()'s canonicalization of it, as the
// Legacy/CashBasis clamp always did; liveScale sets its own mode.
NumberRoundModeGuard const rg(Number::RoundingMode::ToNearest);
Number const posterior = reference + delta;
switch (version)
{
case VaultVersion::Legacy:
case VaultVersion::CashBasis:
return scale(posterior, asset);
case VaultVersion::FixedPrecision:
return liveScale(posterior, asset, baseScale);
}
// LCOV_EXCL_START
UNREACHABLE("xrpl::detail::posteriorAssetScale : valid VaultVersion");
return Number::kMinExponent - 1;
// LCOV_EXCL_STOP
}
[[nodiscard]] STAmount
creditToPosteriorScale(
Asset const& asset,
Number const& reference,
int atScale,
Number const& raw,
Number::RoundingMode roundingMode)
{
// Floor the SUM (reference + raw), not just raw, at atScale. See
// creditToPosteriorAvailableScale's doc: a delta floored on its own grid
// can still leave a 17-digit sum once the reference has crossed a power
// of ten.
//
// Both reference + raw and flooredSum - reference can carry more digits
// than Number holds (the sum is on the posterior grid, the reference on
// the finer one). Compute both under roundingMode, not the caller's
// ambient mode, so the credit this returns never exceeds raw.
NumberRoundModeGuard const rg(roundingMode);
Number const flooredSum = roundToAsset(asset, reference + raw, atScale, roundingMode);
return STAmount{asset, flooredSum - reference};
}
[[nodiscard]] int
getPosteriorVaultScale(SLE::ConstRef vault, STAmount const& delta)
{
XRPL_ASSERT(
vault && vault->getType() == ltVAULT,
"xrpl::detail::getPosteriorVaultScale : valid Vault sle");
XRPL_ASSERT(
delta.asset() == vault->at(sfAsset),
"xrpl::detail::getPosteriorVaultScale : delta and Vault asset match");
return posteriorScale(vault, getAssetsTotal(vault), delta);
}
} // namespace detail
[[nodiscard]] Number
getAssetsTotal(SLE::ConstRef vault)
{
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getAssetsTotal : valid Vault sle");
if (getVaultVersion(vault) != VaultVersion::FixedPrecision)
return vault->at(sfAssetsTotal);
// AssetsAvailable is at -Scale (or coarser) and AssetsDeployed is always at
// -Scale, so the sum is exact while it fits 19 digits. Fix the mode so a
// sum that does not fit rounds the same way for every caller.
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
return vault->at(sfAssetsAvailable) + vault->at(sfAssetsDeployed);
}
[[nodiscard]] TER
adjustVaultBalances(SLE::Ref vault, VaultBalanceChange const& change, beast::Journal j)
{
XRPL_ASSERT(
vault && vault->getType() == ltVAULT, "xrpl::adjustVaultBalances : valid Vault sle");
XRPL_ASSERT(
getVaultVersion(vault) == VaultVersion::FixedPrecision,
"xrpl::adjustVaultBalances : FixedPrecision Vault");
Asset const asset = vault->at(sfAsset);
STAmount const cash = change.cash.value_or(STAmount{asset, 0});
XRPL_ASSERT(cash.asset() == asset, "xrpl::adjustVaultBalances : cash and Vault asset match");
// This is the mode the ledger's own transfers (trust line / MPT balance
// updates) normalize in; computing AA' with any other ambient mode would
// not reproduce what the real transfer just did to the backing balance.
XRPL_ASSERT(
Number::getround() == Number::RoundingMode::ToNearest,
"xrpl::adjustVaultBalances : ambient rounding mode is ToNearest");
// AA' is the exact trust-line/MPT arithmetic the ledger uses for the
// transfer itself: a Number sum re-canonicalized to the asset's 16-digit
// STAmount precision. MPT and XRP sums are already integral, so this is
// a no-op rounding for them.
//
// AssetsDeployed, LossUnrealized and YieldUnrealized move no cash, so they stay
// exact Number sums; the asserts below check each fits 16 digits, rather
// than letting an STAmount conversion round it silently.
STAmount const availableAfter{asset, Number(vault->at(sfAssetsAvailable)) + Number(cash)};
Number const assetsDeployedAfter = Number(vault->at(sfAssetsDeployed)) + change.deployed;
Number const lossUnrealizedAfter = Number(vault->at(sfLossUnrealized)) + change.loss;
// LoanSet, LoanPay and default each move YieldUnrealized by the change in
// the Loan's own TotalValueOutstanding - PrincipalOutstanding -
// ManagementFeeOutstanding, so it always equals that sum over the
// Vault's Loans and cannot go negative.
Number const yieldUnrealizedAfter = Number(vault->at(sfYieldUnrealized)) + change.yield;
if (availableAfter < beast::kZero || assetsDeployedAfter < beast::kZero ||
lossUnrealizedAfter < beast::kZero || yieldUnrealizedAfter < beast::kZero)
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "adjustVaultBalances: a balance would become negative."
<< " AssetsAvailable: " << Number(availableAfter)
<< ", AssetsDeployed: " << assetsDeployedAfter
<< ", LossUnrealized: " << lossUnrealizedAfter
<< ", YieldUnrealized: " << yieldUnrealizedAfter;
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
// The impairment rule: a Loan can only be impaired for as much as it
// still owes. Reachable only through LoanManage's impair.
if (lossUnrealizedAfter > assetsDeployedAfter)
return tecLIMIT_EXCEEDED;
// Every caller derives principal from a Loan's own PrincipalOutstanding so the new total must
// stay representable at the asset's own 16-digit precision too.
XRPL_ASSERT(
(STAmount{asset, assetsDeployedAfter} == assetsDeployedAfter),
"xrpl::adjustVaultBalances : AssetsDeployed is a 16-digit STAmount value");
XRPL_ASSERT(
(STAmount{asset, lossUnrealizedAfter} == lossUnrealizedAfter),
"xrpl::adjustVaultBalances : LossUnrealized is a 16-digit STAmount value");
XRPL_ASSERT(
(STAmount{asset, yieldUnrealizedAfter} == yieldUnrealizedAfter),
"xrpl::adjustVaultBalances : YieldUnrealized is a 16-digit STAmount value");
vault->at(sfAssetsAvailable) = availableAfter;
vault->at(sfAssetsDeployed) = assetsDeployedAfter;
vault->at(sfLossUnrealized) = lossUnrealizedAfter;
vault->at(sfYieldUnrealized) = yieldUnrealizedAfter;
syncAssetsTotal(vault);
return tesSUCCESS;
}
[[nodiscard]] int
getVaultScale(SLE::ConstRef vault)
{
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getVaultScale : valid Vault sle");
Number const assetsTotal = getAssetsTotal(vault);
switch (getVaultVersion(vault))
{
case VaultVersion::Legacy:
case VaultVersion::CashBasis:
return scale(assetsTotal, vault->at(sfAsset));
case VaultVersion::FixedPrecision:
return detail::liveScale(assetsTotal, vault->at(sfAsset), fixedBaseScale(vault));
}
// LCOV_EXCL_START
UNREACHABLE("xrpl::getVaultScale : valid VaultVersion");
return Number::kMinExponent - 1;
// LCOV_EXCL_STOP
}
[[nodiscard]] int
getVaultBaseScale(SLE::ConstRef vault)
{
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getVaultBaseScale : valid Vault sle");
switch (getVaultVersion(vault))
{
case VaultVersion::Legacy:
case VaultVersion::CashBasis:
return getVaultScale(vault);
case VaultVersion::FixedPrecision:
return fixedBaseScale(vault);
}
// LCOV_EXCL_START
UNREACHABLE("xrpl::getVaultBaseScale : valid VaultVersion");
return Number::kMinExponent - 1;
// LCOV_EXCL_STOP
}
[[nodiscard]] STAmount
roundToPosteriorVaultScale(
SLE::ConstRef vault,
STAmount const& amount,
Number::RoundingMode roundingMode)
{
XRPL_ASSERT(
vault && vault->getType() == ltVAULT, "xrpl::roundToPosteriorVaultScale : valid Vault sle");
XRPL_ASSERT(
amount.asset() == vault->at(sfAsset),
"xrpl::roundToPosteriorVaultScale : amount and Vault asset match");
if (amount.integral())
return amount;
return roundToScale(amount, detail::getPosteriorVaultScale(vault, amount), roundingMode);
}
[[nodiscard]] STAmount
creditToPosteriorAvailableScale(
SLE::ConstRef vault,
Number const& raw,
Number::RoundingMode roundingMode)
{
XRPL_ASSERT(
vault && vault->getType() == ltVAULT,
"xrpl::creditToPosteriorAvailableScale : valid Vault sle");
Asset const asset = vault->at(sfAsset);
if (asset.integral())
{
NumberRoundModeGuard const rg(roundingMode);
return STAmount{asset, raw};
}
Number const reference = vault->at(sfAssetsAvailable);
int const scale = posteriorScale(vault, reference, raw);
return detail::creditToPosteriorScale(asset, reference, scale, raw, roundingMode);
}
[[nodiscard]] Number
getVaultOpenLimit(SLE::ConstRef vault)
{
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getVaultOpenLimit : valid Vault sle");
XRPL_ASSERT(
getVaultVersion(vault) == VaultVersion::FixedPrecision,
"xrpl::getVaultOpenLimit : FixedPrecision Vault");
return Number{9, 15 + getVaultBaseScale(vault)};
}
[[nodiscard]] TER
checkOptionalVaultInflow(SLE::ConstRef vault, STAmount const& amount)
{
XRPL_ASSERT(
vault && vault->getType() == ltVAULT, "xrpl::checkOptionalVaultInflow : valid Vault sle");
XRPL_ASSERT(
amount.asset() == vault->at(sfAsset),
"xrpl::checkOptionalVaultInflow : amount and Vault asset match");
XRPL_ASSERT(!amount.negative(), "xrpl::checkOptionalVaultInflow : non-negative amount");
if (amount.negative())
return tefINTERNAL; // LCOV_EXCL_LINE
if (getVaultVersion(vault) != VaultVersion::FixedPrecision)
return tesSUCCESS;
STAmount const rounded =
roundToPosteriorVaultScale(vault, amount, Number::RoundingMode::TowardsZero);
int const baseScale = getVaultBaseScale(vault);
// Keep this explicit even though a non-negative YieldUnrealized makes the
// Open-zone capacity ceiling reject every coarsening transition too. The
// protocol defines both conditions independently.
if (detail::getPosteriorVaultScale(vault, rounded) != baseScale)
return tecLIMIT_EXCEEDED;
if (vaultOpenZoneCapacity(vault, rounded) > getVaultOpenLimit(vault))
return tecLIMIT_EXCEEDED;
return tesSUCCESS;
}
[[nodiscard]] Number
vaultOpenZoneCapacity(SLE::ConstRef vault, Number const& roundedAmount)
{
XRPL_ASSERT(
vault && vault->getType() == ltVAULT, "xrpl::vaultOpenZoneCapacity : valid Vault sle");
XRPL_ASSERT(
getVaultVersion(vault) == VaultVersion::FixedPrecision,
"xrpl::vaultOpenZoneCapacity : FixedPrecision Vault");
NumberRoundModeGuard const rg(Number::RoundingMode::TowardsZero);
return getAssetsTotal(vault) + vault->at(sfYieldUnrealized) + roundedAmount;
}
[[nodiscard]] std::optional<STAmount>
assetsToSharesDeposit(SLE::ConstRef vault, SLE::ConstRef issuance, STAmount const& assets)
{
@@ -438,7 +34,7 @@ assetsToSharesDeposit(SLE::ConstRef vault, SLE::ConstRef issuance, STAmount cons
if (assets.negative() || assets.asset() != vault->at(sfAsset))
return std::nullopt; // LCOV_EXCL_LINE
Number const assetTotal = getAssetsTotal(vault);
Number const assetTotal = vault->at(sfAssetsTotal);
STAmount shares{vault->at(sfShareMPTID)};
if (assetTotal == 0)
{
@@ -462,7 +58,7 @@ sharesToAssetsDeposit(SLE::ConstRef vault, SLE::ConstRef issuance, STAmount cons
if (shares.negative() || shares.asset() != vault->at(sfShareMPTID))
return std::nullopt; // LCOV_EXCL_LINE
Number const assetTotal = getAssetsTotal(vault);
Number const assetTotal = vault->at(sfAssetsTotal);
STAmount assets{vault->at(sfAsset)};
if (assetTotal == 0)
{
@@ -489,52 +85,37 @@ clampToAssetsTotalScale(SLE::ConstRef vault, STAmount const& delta)
{
return magnitude;
}
Number const assetsTotal = vault->at(sfAssetsTotal);
// Calculate the scale after applying the delta using ToNearest rounding.
// This aligns the delta with scale checks used by vault invariants.
int const postScale = [&] {
NumberRoundModeGuard const rg(Number::RoundingMode::ToNearest);
return scale(assetsTotal + delta, asset);
}();
STAmount actualDelta;
if (getVaultVersion(vault) == VaultVersion::FixedPrecision)
if (delta.negative())
{
// FixedPrecision only reaches this branch for credits (VaultDeposit);
// outflows go through clampVaultOutflow -> clampToAvailableScale,
// which rounds at AssetsAvailable's own posterior scale instead.
XRPL_ASSERT(
!delta.negative(), "xrpl::clampToAssetsTotalScale : FixedPrecision credit only");
STAmount const rounded =
roundToPosteriorVaultScale(vault, delta, Number::RoundingMode::TowardsZero);
actualDelta = rounded.negative() ? -rounded : rounded;
// For withdrawals (debits), floor the magnitude to the target scale
// to ensure exact grid alignment without paying out extra assets.
actualDelta = roundToScale(magnitude, postScale, Number::RoundingMode::Downward);
}
else
{
Number const assetsTotal = getAssetsTotal(vault);
// Calculate the scale after applying the delta using ToNearest rounding.
// This aligns the delta with scale checks used by vault invariants.
int const postScale = [&] {
NumberRoundModeGuard const rg(Number::RoundingMode::ToNearest);
return scale(assetsTotal + delta, asset);
// For deposits (credits), derive actualDelta from the floored posterior total.
// This prevents grid alignment issues from crediting the vault more than deposited.
//
// Sum using Downward rounding so intermediate precision doesn't round up
// and exceed the original requested amount.
Number const posterior = [&] {
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
return assetsTotal + magnitude;
}();
if (delta.negative())
{
// For withdrawals (debits), floor the magnitude to the target scale
// to ensure exact grid alignment without paying out extra assets.
actualDelta = roundToScale(magnitude, postScale, Number::RoundingMode::Downward);
}
else
{
// For deposits (credits), derive actualDelta from the floored posterior total.
// This prevents grid alignment issues from crediting the vault more than deposited.
//
// Sum using Downward rounding so intermediate precision doesn't round up
// and exceed the original requested amount.
Number const posterior = [&] {
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
return assetsTotal + magnitude;
}();
Number const roundedPosterior =
roundToAsset(asset, posterior, postScale, Number::RoundingMode::Downward);
actualDelta = STAmount{asset, roundedPosterior - assetsTotal};
}
Number const roundedPosterior =
roundToAsset(asset, posterior, postScale, Number::RoundingMode::Downward);
actualDelta = STAmount{asset, roundedPosterior - assetsTotal};
}
XRPL_ASSERT(
@@ -549,36 +130,10 @@ clampToAssetsTotalScale(SLE::ConstRef vault, STAmount const& delta)
return actualDelta;
}
[[nodiscard]] std::expected<STAmount, TER>
clampVaultOutflow(SLE::ConstRef vault, STAmount const& delta)
{
XRPL_ASSERT(delta.negative(), "xrpl::clampVaultOutflow : outflow delta is negative");
return getVaultVersion(vault) == VaultVersion::FixedPrecision
? clampToAvailableScale(vault, delta)
: clampToAssetsTotalScale(vault, delta);
}
[[nodiscard]] int
vaultBaseScale(Asset const& asset, std::uint8_t scale)
{
if (asset.integral())
return 0;
return -static_cast<int>(scale);
}
[[nodiscard]] bool
isOnVaultBaseGrid(Asset const& asset, Number const& value, int baseScale)
{
// isRepresentable first: building an STAmount from an out-of-range value
// throws, and this checks user-supplied values in preclaim.
return isRepresentable(asset, value) && STAmount{asset, value} == value &&
roundToAsset(asset, value, baseScale, Number::RoundingMode::TowardsZero) == value;
}
[[nodiscard]] Number
assetsTotalForWithdrawal(SLE::ConstRef vault, WaiveUnrealizedLoss waive)
{
Number assetTotal = getAssetsTotal(vault);
Number assetTotal = vault->at(sfAssetsTotal);
if (waive == WaiveUnrealizedLoss::No)
assetTotal -= vault->at(sfLossUnrealized);
return assetTotal;
@@ -592,14 +147,6 @@ debitIsNonZeroDust(Asset const& asset, Number const& total, Number const& amount
return STAmount{asset, total - amount} == STAmount{asset, total};
}
[[nodiscard]] Number
vaultDebitDustReference(SLE::ConstRef vault, Number const& assetsTotal)
{
return getVaultVersion(vault) == VaultVersion::FixedPrecision
? Number(vault->at(sfAssetsAvailable))
: assetsTotal;
}
[[nodiscard]] std::optional<STAmount>
assetsToSharesWithdraw(
SLE::ConstRef vault,
@@ -670,41 +217,36 @@ isSoleShareholder(ReadView const& view, AccountID const& account, SLE::ConstRef
return sleToken->getFieldU64(sfMPTAmount) == outstanding;
}
[[nodiscard]] VaultVersion
decodeVaultVersion(std::optional<std::uint8_t> leVersion)
{
if (!leVersion)
return VaultVersion::Legacy;
if (*leVersion > std::to_underlying(VaultVersion::FixedPrecision))
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::decodeVaultVersion : invalid vault version");
return VaultVersion::Legacy;
// LCOV_EXCL_STOP
}
return static_cast<VaultVersion>(*leVersion);
}
[[nodiscard]] VaultVersion
getVaultVersion(SLE::ConstRef vault)
{
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getVaultVersion : valid Vault sle");
return decodeVaultVersion(vault->at(~sfLEVersion));
if (!vault->isFieldPresent(sfLEVersion))
return VaultVersion::Legacy;
auto const version = vault->at(sfLEVersion);
if (version > std::to_underlying(VaultVersion::CashBasis))
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::getVaultVersion : invalid vault version");
return VaultVersion::Legacy;
// LCOV_EXCL_STOP
}
return static_cast<VaultVersion>(version);
}
[[nodiscard]] VaultVersion
getVaultDefaultVersion(Rules const& rules)
namespace {
[[nodiscard]] VaultKind
decodeVaultKind(std::optional<std::uint8_t> vaultKind)
{
// FixedPrecision requires both cleanups, so a FixedPrecision Vault always
// sees them enabled.
if (rules.enabled(featureLendingProtocolV1_2) && rules.enabled(fixCleanup3_2_0) &&
rules.enabled(fixCleanup3_4_0))
return VaultVersion::FixedPrecision;
if (rules.enabled(featureLendingProtocolV1_1))
return VaultVersion::CashBasis;
return VaultVersion::Legacy;
if (vaultKind && *vaultKind == std::to_underlying(VaultKind::ClosedEnded))
return VaultKind::ClosedEnded;
return VaultKind::OpenEnded;
}
} // namespace
[[nodiscard]] VaultKind
getVaultKind(SLE::ConstRef vault)
{
+78 -74
View File
@@ -20,88 +20,92 @@ namespace detail {
// will remain in the object file. But the string literals will remain.
//
// There's a certain amount of tension in determining the correct HTTP
// status to associate with a given RPC error. Initially all RPC errors
// returned 200 (OK). And that's the default behavior if no HTTP status code
// is specified below.
// status to associate with a given RPC error. Every row below names its
// HTTP status, and a row that omits it does not compile, since ErrorInfo has
// no constructor that leaves the status out.
//
// The codes currently selected target the load balancer fail-over use case.
// If a query fails on one node but is likely to have a positive outcome
// on a different node, then the failure should return a 4xx/5xx range
// status code.
// The rows below are aligned by hand into four columns, so that a wrong status or a mistyped token
// is visible by scanning one column. That cannot be had inside the 100-column limit once the
// longest enumerator and the longest message sit in one row. So this region is exempt from the
// limit by design rather than by oversight.
// clang-format off
static constexpr ErrorInfo kUnorderedErrorInfos[]{
{RpcActMalformed, "actMalformed", "Account malformed."},
{RpcActNotFound, "actNotFound", "Account not found."},
{RpcAlreadyMultisig, "alreadyMultisig", "Already multisigned."},
{RpcAlreadySingleSig, "alreadySingleSig", "Already single-signed."},
{RpcAmendmentBlocked, "amendmentBlocked", "Amendment blocked, need upgrade.", 503},
{RpcExpiredValidatorList, "unlBlocked", "Validator list expired.", 503},
{RpcAtxDeprecated, "deprecated", "Use the new API or specify a ledger range.", 400},
{RpcBadKeyType, "badKeyType", "Bad key type.", 400},
{RpcBadFeature, "badFeature", "Feature unknown or invalid.", 500},
{RpcBadIssuer, "badIssuer", "Issuer account malformed.", 400},
{RpcBadMarket, "badMarket", "No such market.", 404},
{RpcBadSecret, "badSecret", "Secret does not match account.", 403},
{RpcBadSeed, "badSeed", "Disallowed seed.", 403},
{RpcBadSyntax, "badSyntax", "Syntax error.", 400},
{RpcChannelMalformed, "channelMalformed", "Payment channel is malformed.", 400},
{RpcChannelAmtMalformed, "channelAmtMalformed", "Payment channel amount is malformed.", 400},
{RpcCommandMissing, "commandMissing", "Missing command entry.", 400},
{RpcDbDeserialization, "dbDeserialization", "Database deserialization error.", 502},
{RpcDstActMalformed, "dstActMalformed", "Destination account is malformed.", 400},
{RpcDstActMissing, "dstActMissing", "Destination account not provided.", 400},
{RpcDstActNotFound, "dstActNotFound", "Destination account not found.", 404},
{RpcDstAmtMalformed, "dstAmtMalformed", "Destination amount/currency/issuer is malformed.", 400},
{RpcDstAmtMissing, "dstAmtMissing", "Destination amount/currency/issuer is missing.", 400},
{RpcDstIsrMalformed, "dstIsrMalformed", "Destination issuer is malformed.", 400},
{RpcExcessiveLgrRange, "excessiveLgrRange", "Ledger range exceeds 1000.", 400},
{RpcForbidden, "forbidden", "Bad credentials.", 403},
{RpcHighFee, "highFee", "Current transaction fee exceeds your limit.", 402},
{RpcInternal, "internal", "Internal error.", 500},
{RpcInvalidLgrRange, "invalidLgrRange", "Ledger range is invalid.", 400},
{RpcInvalidParams, "invalidParams", "Invalid parameters.", 400},
{RpcInvalidHotwallet, "invalidHotWallet", "Invalid hotwallet.", 400},
{RpcIssueMalformed, "issueMalformed", "Issue is malformed.", 400},
{RpcJsonRpc, "json_rpc", "JSON-RPC transport error.", 500},
{RpcLgrIdxsInvalid, "lgrIdxsInvalid", "Ledger indexes invalid.", 400},
{RpcLgrIdxMalformed, "lgrIdxMalformed", "Ledger index malformed.", 400},
{RpcLgrNotFound, "lgrNotFound", "Ledger not found.", 404},
{RpcLgrNotValidated, "lgrNotValidated", "Ledger not validated.", 202},
{RpcMasterDisabled, "masterDisabled", "Master key is disabled.", 403},
{RpcNotEnabled, "notEnabled", "Not enabled in configuration.", 501},
{RpcNotImpl, "notImpl", "Not implemented.", 501},
{RpcNotReady, "notReady", "Not ready to handle this request.", 503},
{RpcNotSupported, "notSupported", "Operation not supported.", 501},
{RpcNoClosed, "noClosed", "Closed ledger is unavailable.", 503},
{RpcNoCurrent, "noCurrent", "Current ledger is unavailable.", 503},
{RpcNotSynced, "notSynced", "Not synced to the network.", 503},
{RpcNoEvents, "noEvents", "Current transport does not support events.", 405},
{RpcNoNetwork, "noNetwork", "Not synced to the network.", 503},
{RpcWrongNetwork, "wrongNetwork", "Wrong network.", 503},
{RpcNoPermission, "noPermission", "You don't have permission for this command.", 401},
{RpcNoPfRequest, "noPathRequest", "No pathfinding request in progress.", 404},
{RpcObjectNotFound, "objectNotFound", "The requested object was not found.", 404},
{RpcPublicMalformed, "publicMalformed", "Public key is malformed.", 400},
{RpcSendmaxMalformed, "sendMaxMalformed", "SendMax amount malformed.", 400},
{RpcSigningMalformed, "signingMalformed", "Signing of transaction is malformed.", 400},
{RpcSlowDown, "slowDown", "You are placing too much load on the server.", 429},
{RpcSrcActMalformed, "srcActMalformed", "Source account is malformed.", 400},
{RpcSrcActMissing, "srcActMissing", "Source account not provided.", 400},
{RpcSrcActNotFound, "srcActNotFound", "Source account not found.", 404},
{RpcDelegateActNotFound, "delegateActNotFound", "Delegate account not found.", 404},
{RpcSrcCurMalformed, "srcCurMalformed", "Source currency is malformed.", 400},
{RpcSrcIsrMalformed, "srcIsrMalformed", "Source issuer is malformed.", 400},
{RpcStreamMalformed, "malformedStream", "Stream malformed.", 400},
{RpcTooBusy, "tooBusy", "The server is too busy to help you now.", 503},
{RpcTxnNotFound, "txnNotFound", "Transaction not found.", 404},
{RpcUnknownCommand, "unknownCmd", "Unknown method.", 405},
{RpcOracleMalformed, "oracleMalformed", "Oracle request is malformed.", 400},
{RpcBadCredentials, "badCredentials", "Credentials do not exist, are not accepted, or have expired.", 400},
{RpcTxSigned, "transactionSigned", "Transaction should not be signed.", 400},
{RpcDomainMalformed, "domainMalformed", "Domain is malformed.", 400},
{RpcEntryNotFound, "entryNotFound", "Entry not found.", 400},
{RpcUnexpectedLedgerType, "unexpectedLedgerType", "Unexpected ledger type.", 400},
{RpcActMalformed, "actMalformed", "Account malformed.", 400},
{RpcActNotFound, "actNotFound", "Account not found.", 404},
{RpcAlreadyMultisig, "alreadyMultisig", "Already multisigned.", 400},
{RpcAlreadySingleSig, "alreadySingleSig", "Already single-signed.", 400},
{RpcAmendmentBlocked, "amendmentBlocked", "Amendment blocked, need upgrade.", 503},
{RpcExpiredValidatorList, "unlBlocked", "Validator list expired.", 503},
{RpcAtxDeprecated, "deprecated", "Use the new API or specify a ledger range.", 400},
{RpcBadKeyType, "badKeyType", "Bad key type.", 400},
{RpcBadFeature, "badFeature", "Feature unknown or invalid.", 500},
{RpcBadIssuer, "badIssuer", "Issuer account malformed.", 400},
{RpcBadMarket, "badMarket", "No such market.", 404},
{RpcBadSecret, "badSecret", "Secret does not match account.", 403},
{RpcBadSeed, "badSeed", "Disallowed seed.", 403},
{RpcBadSyntax, "badSyntax", "Syntax error.", 400},
{RpcChannelMalformed, "channelMalformed", "Payment channel is malformed.", 400},
{RpcChannelAmtMalformed, "channelAmtMalformed", "Payment channel amount is malformed.", 400},
{RpcCommandMissing, "commandMissing", "Missing command entry.", 400},
{RpcDbDeserialization, "dbDeserialization", "Database deserialization error.", 502},
{RpcDstActMalformed, "dstActMalformed", "Destination account is malformed.", 400},
{RpcDstActMissing, "dstActMissing", "Destination account not provided.", 400},
{RpcDstActNotFound, "dstActNotFound", "Destination account not found.", 404},
{RpcDstAmtMalformed, "dstAmtMalformed", "Destination amount/currency/issuer is malformed.", 400},
{RpcDstAmtMissing, "dstAmtMissing", "Destination amount/currency/issuer is missing.", 400},
{RpcDstIsrMalformed, "dstIsrMalformed", "Destination issuer is malformed.", 400},
{RpcExcessiveLgrRange, "excessiveLgrRange", "Ledger range exceeds 1000.", 400},
{RpcForbidden, "forbidden", "Bad credentials.", 403},
{RpcHighFee, "highFee", "Current transaction fee exceeds your limit.", 402},
{RpcInternal, "internal", "Internal error.", 500},
{RpcInvalidLgrRange, "invalidLgrRange", "Ledger range is invalid.", 400},
{RpcInvalidParams, "invalidParams", "Invalid parameters.", 400},
{RpcInvalidHotwallet, "invalidHotWallet", "Invalid hotwallet.", 400},
{RpcIssueMalformed, "issueMalformed", "Issue is malformed.", 400},
{RpcJsonRpc, "json_rpc", "JSON-RPC transport error.", 500},
{RpcLgrIdxsInvalid, "lgrIdxsInvalid", "Ledger indexes invalid.", 400},
{RpcLgrIdxMalformed, "lgrIdxMalformed", "Ledger index malformed.", 400},
{RpcLgrNotFound, "lgrNotFound", "Ledger not found.", 404},
{RpcLgrNotValidated, "lgrNotValidated", "Ledger not validated.", 202},
{RpcMasterDisabled, "masterDisabled", "Master key is disabled.", 403},
{RpcNotEnabled, "notEnabled", "Not enabled in configuration.", 501},
{RpcNotImpl, "notImpl", "Not implemented.", 501},
{RpcNotReady, "notReady", "Not ready to handle this request.", 503},
{RpcNotSupported, "notSupported", "Operation not supported.", 501},
{RpcNoClosed, "noClosed", "Closed ledger is unavailable.", 503},
{RpcNoCurrent, "noCurrent", "Current ledger is unavailable.", 503},
{RpcNotSynced, "notSynced", "Not synced to the network.", 503},
{RpcNoEvents, "noEvents", "Current transport does not support events.", 405},
{RpcNoNetwork, "noNetwork", "Not synced to the network.", 503},
{RpcWrongNetwork, "wrongNetwork", "Wrong network.", 503},
{RpcNoPermission, "noPermission", "You don't have permission for this command.", 401},
{RpcNoPfRequest, "noPathRequest", "No pathfinding request in progress.", 404},
{RpcObjectNotFound, "objectNotFound", "The requested object was not found.", 404},
{RpcPublicMalformed, "publicMalformed", "Public key is malformed.", 400},
{RpcSendmaxMalformed, "sendMaxMalformed", "SendMax amount malformed.", 400},
{RpcSigningMalformed, "signingMalformed", "Signing of transaction is malformed.", 400},
{RpcSlowDown, "slowDown", "You are placing too much load on the server.", 429},
{RpcSrcActMalformed, "srcActMalformed", "Source account is malformed.", 400},
{RpcSrcActMissing, "srcActMissing", "Source account not provided.", 400},
{RpcSrcActNotFound, "srcActNotFound", "Source account not found.", 404},
{RpcDelegateActNotFound, "delegateActNotFound", "Delegate account not found.", 404},
{RpcSrcCurMalformed, "srcCurMalformed", "Source currency is malformed.", 400},
{RpcSrcIsrMalformed, "srcIsrMalformed", "Source issuer is malformed.", 400},
{RpcStreamMalformed, "malformedStream", "Stream malformed.", 400},
{RpcTooBusy, "tooBusy", "The server is too busy to help you now.", 503},
{RpcTxnNotFound, "txnNotFound", "Transaction not found.", 404},
{RpcUnknownCommand, "unknownCmd", "Unknown method.", 405},
{RpcOracleMalformed, "oracleMalformed", "Oracle request is malformed.", 400},
{RpcBadCredentials, "badCredentials", "Credentials do not exist, are not accepted, or have expired.", 400},
{RpcTxSigned, "transactionSigned", "Transaction should not be signed.", 400},
{RpcDomainMalformed, "domainMalformed", "Domain is malformed.", 400},
{RpcEntryNotFound, "entryNotFound", "Entry not found.", 400},
{RpcUnexpectedLedgerType, "unexpectedLedgerType", "Unexpected ledger type.", 400},
};
// clang-format on
-16
View File
@@ -443,22 +443,6 @@ getRate(STAmount const& offerOut, STAmount const& offerIn)
}
}
bool
isRepresentable(Asset const& asset, Number const& value)
{
try
{
// Out-of-range values throw std::overflow_error from Number or
// IOUAmount, or std::runtime_error from STAmount::canonicalize.
[[maybe_unused]] STAmount const amount{asset, value};
return true;
}
catch (std::runtime_error const&)
{
return false;
}
}
/**
* @brief Safely checks if two STAmount values can be added without overflow,
* underflow, or precision loss.
+4 -1
View File
@@ -33,7 +33,10 @@ getHTTPHeaderTimestamp()
void
httpReply(int nStatus, std::string const& content, json::Output const& output, beast::Journal j)
{
JLOG(j.trace()) << "HTTP Reply " << nStatus << " " << content;
// The status only. The body is a reply, which carries a credential when the
// command is a keygen, and this library cannot mask one: the caller logs the
// masked body at debug.
JLOG(j.trace()) << "HTTP Reply " << nStatus;
if (content.empty() && nStatus == 401)
{
@@ -16,7 +16,6 @@
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STNumber.h>
#include <xrpl/protocol/STTakesAsset.h>
@@ -224,15 +223,10 @@ LoanBrokerSet::preclaim(PreclaimContext const& ctx)
}
// Check that relevant values can be represented as the vault asset
// type. This is mostly only relevant for integral (non-IOU) types.
// fixCleanup3_5_0 also rejects out-of-range values, e.g. XRP above
// STAmount::kMaxNativeN, which would otherwise throw.
bool const fix350Enabled = ctx.view.rules().enabled(fixCleanup3_5_0);
// type. This is mostly only relevant for integral (non-IOU) types
for (auto const& field : getValueFields())
{
if (auto const value = tx[field]; value &&
((fix350Enabled && !isRepresentable(asset, *value)) ||
STAmount{asset, *value} != *value))
if (auto const value = tx[field]; value && STAmount{asset, *value} != *value)
{
JLOG(ctx.j.warn()) << field.f->getName() << " (" << *value
<< ") can not be represented as a(n) " << to_string(asset) << ".";
@@ -20,7 +20,6 @@
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STNumber.h>
#include <xrpl/protocol/STObject.h>
@@ -367,14 +366,10 @@ LoanSet::preclaim(PreclaimContext const& ctx)
// Check that relevant values can be represented as the vault asset type.
// This check is almost duplicated in doApply, but that check is done after
// the overall loan scale is known. This is mostly only relevant for
// integral (non-IOU) types. fixCleanup3_5_0 also rejects out-of-range
// values, e.g. XRP above STAmount::kMaxNativeN, which would otherwise throw.
bool const fix350Enabled = ctx.view.rules().enabled(fixCleanup3_5_0);
// integral (non-IOU) types
for (auto const& field : getValueFields())
{
if (auto const value = tx[field]; value &&
((fix350Enabled && !isRepresentable(asset, *value)) ||
STAmount{asset, *value} != *value))
if (auto const value = tx[field]; value && STAmount{asset, *value} != *value)
{
JLOG(ctx.j.warn()) << field.f->getName() << " (" << *value
<< ") can not be represented as a(n) " << to_string(asset) << ".";
@@ -18,7 +18,6 @@
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
#include <xrpl/protocol/STTakesAsset.h>
@@ -88,11 +87,6 @@ VaultCreate::preflight(PreflightContext const& ctx)
{
if (*assetMax < beast::kZero)
return temMALFORMED;
// An AssetsMaximum out of range for the asset, e.g. XRP above
// STAmount::kMaxNativeN, would throw when rounded to the asset.
if (ctx.rules.enabled(fixCleanup3_5_0) && !isRepresentable(ctx.tx[sfAsset], *assetMax))
return temMALFORMED;
}
if (auto const metadata = ctx.tx[~sfMPTokenMetadata])
@@ -7,7 +7,6 @@
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
#include <xrpl/protocol/STTakesAsset.h>
@@ -75,16 +74,6 @@ VaultSet::preclaim(PreclaimContext const& ctx)
return tecNO_PERMISSION;
}
// An AssetsMaximum out of range for the vault asset, e.g. XRP above
// STAmount::kMaxNativeN, would throw when rounded to the asset.
if (auto const assetMax = ctx.tx[~sfAssetsMaximum]; assetMax &&
ctx.view.rules().enabled(fixCleanup3_5_0) &&
!isRepresentable(vault->at(sfAsset), *assetMax))
{
JLOG(ctx.j.debug()) << "VaultSet: max assets out of range for the asset.";
return tecPRECISION_LOSS;
}
auto const mptIssuanceID = (*vault)[sfShareMPTID];
auto const sleIssuance = ctx.view.read(keylet::mptokenIssuance(mptIssuanceID));
if (!sleIssuance)
+1 -2
View File
@@ -2419,8 +2419,7 @@ public:
BEAST_EXPECT(jrr[jss::result][jss::engine_result] == "tesSUCCESS");
if (wsc->version() == 2)
{
BEAST_EXPECT(jrr.isMember(jss::jsonrpc) && jrr[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jrr.isMember(jss::ripplerpc) && jrr[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jrr));
BEAST_EXPECT(jrr.isMember(jss::id) && jrr[jss::id] == 5);
}
+1 -2
View File
@@ -2170,8 +2170,7 @@ public:
BEAST_EXPECT(jrr[jss::result][jss::engine_result] == "tesSUCCESS");
if (wsc->version() == 2)
{
BEAST_EXPECT(jrr.isMember(jss::jsonrpc) && jrr[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jrr.isMember(jss::ripplerpc) && jrr[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jrr));
BEAST_EXPECT(jrr.isMember(jss::id) && jrr[jss::id] == 5);
}
-39
View File
@@ -3369,42 +3369,6 @@ class LoanBroker_test : public beast::unit_test::Suite
}
public:
void
testDebtMaximumOutOfRange(FeatureBitset features)
{
using namespace jtx;
using namespace loan_broker;
bool const withFix = features[fixCleanup3_5_0];
testcase(
std::string("DebtMaximum out of range for the asset (") + (withFix ? "post" : "pre") +
"-fixCleanup3_5_0)");
Env env(*this, features);
Account const alice{"alice"};
env.fund(XRP(100'000), alice);
env.close();
Vault const vault{env};
auto const [tx, keylet] = vault.create({.owner = alice, .asset = xrpIssue()});
env(tx);
env.close();
// Without the fix, the representability check in preclaim throws for
// XRP above STAmount::kMaxNativeN.
auto const ownerCount = env.ownerCount(alice);
Number const maxXrp{static_cast<std::int64_t>(STAmount::kMaxNativeN)};
env(set(alice, keylet.key),
kDebtMaximum(maxXrp + 1),
Ter(withFix ? TER{tecPRECISION_LOSS} : TER{tefEXCEPTION}));
env.close();
BEAST_EXPECT(env.ownerCount(alice) == ownerCount);
env(set(alice, keylet.key), kDebtMaximum(maxXrp));
env.close();
BEAST_EXPECT(env.ownerCount(alice) > ownerCount);
}
void
run() override
{
@@ -3445,9 +3409,6 @@ public:
testLoanBrokerDeleteConfidentialCOA(all_);
testLoanBrokerDeleteConfidentialCOA(all_ - fixCleanup3_5_0);
testDebtMaximumOutOfRange(all_);
testDebtMaximumOutOfRange(all_ - fixCleanup3_5_0);
// featureMPTokensV2 independently makes ValidMPTTransfer enforcing,
// but it's Supported::No (never enabled on real networks); exclude
// it here so fixCleanup3_4_0 alone is the deciding amendment, as it
-36
View File
@@ -22,7 +22,6 @@
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/SeqProxy.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
@@ -933,38 +932,6 @@ private:
run(all_, tesSUCCESS);
}
void
testPrincipalRequestedOutOfRange(FeatureBitset features)
{
using namespace jtx;
using namespace loan;
bool const withFix = features[fixCleanup3_5_0];
testcase(
std::string("LoanSet PrincipalRequested out of range for the asset (") +
(withFix ? "post" : "pre") + "-fixCleanup3_5_0)");
Env env(*this, features);
Account const lender{"lender"};
Account const borrower{"borrower"};
env.fund(XRP(10'000'000), lender, borrower);
env.close();
PrettyAsset const xrp{xrpIssue(), 1'000'000};
auto const broker = createVaultAndBroker(env, xrp, lender);
// Without the fix, the representability check in preclaim throws for
// XRP above STAmount::kMaxNativeN.
auto const ownerCount = env.ownerCount(borrower);
Number const maxXrp{static_cast<std::int64_t>(STAmount::kMaxNativeN)};
env(set(borrower, broker.brokerID, maxXrp + 1),
Sig(sfCounterpartySignature, lender),
Fee(env.current()->fees().base * 2),
Ter(withFix ? TER{tecPRECISION_LOSS} : TER{tefEXCEPTION}));
env.close();
BEAST_EXPECT(env.ownerCount(borrower) == ownerCount);
}
public:
void
run() override
@@ -977,9 +944,6 @@ public:
testLoanSetExistingLineAfterIssuerClearsDefaultRipple();
testLoanSetOriginationFeeTwoMptCreates(all_);
testLoanSetOriginationFeeTwoMptCreates(all_ - fixCleanup3_4_0);
testPrincipalRequestedOutOfRange(all_);
testPrincipalRequestedOutOfRange(all_ - fixCleanup3_5_0);
}
};
+24 -147
View File
@@ -8,7 +8,6 @@
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STIssue.h>
@@ -21,19 +20,17 @@
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <expected>
#include <memory>
#include <optional>
#include <string>
#include <utility>
namespace xrpl {
// True unit test of clampToAssetsTotalScale. The function under test reads
// only fields from the vault SLE and never touches a ledger view or Rules, so
// a bare in-memory ltVAULT SLE is enough; there is no jtx::Env and no
// transaction submitted anywhere in this file.
// True unit test of `clampToAssetsTotalScale`. The function under test only
// reads sfAsset and sfAssetsTotal from the vault SLE and never touches a
// ledger view or Rules, so a bare in-memory ltVAULT SLE is enough; there is
// no jtx::Env and no transaction submitted anywhere in this file.
//
// Number regime: this suite relies on the default thread_local Number
// mantissa range, which src/libxrpl/basics/Number.cpp initializes to
@@ -42,17 +39,17 @@ namespace xrpl {
// thread_local std::reference_wrapper<MantissaRange const> Number::kRange =
// MantissaRange::Access::mantissaRange(MantissaRange::MantissaScale::Large330);
//
// Unlike transaction processing, this test never constructs a ledger Rules
// object, so STAmount::operator=(Number const&) always takes its
// !getCurrentTransactionRules() branch and calls fromNumber, independent
// Unlike transaction processing, this test never constructs a ledger `Rules`
// object, so `STAmount::operator=(Number const&)` always takes its
// `!getCurrentTransactionRules()` branch and calls `fromNumber`, independent
// of amendment state. testProbeLarge330Regime() below asserts directly on a
// value that only round-trips exactly under Large330, pinning the regime
// rather than merely asserting it by comment.
class VaultHelpers_test : public beast::unit_test::Suite
{
private:
// A single row of the clampToAssetsTotalScale table. assetsTotal and
// delta must already be genuine, on-grid STAmount values for asset.
// A single row of the clampToAssetsTotalScale table. `assetsTotal` and
// `delta` must already be genuine, on-grid STAmount values for `asset`.
struct Case
{
char const* name = nullptr;
@@ -61,44 +58,27 @@ private:
std::optional<Number> expected; // nullopt means tecPRECISION_LOSS
};
// Builds a bare ltVAULT SLE. With fixedScale absent, sfLEVersion stays
// absent too, preserving the pre-V1.2 Legacy behavior exercised by the
// existing clamp table. With fixedScale set, the SLE is stamped
// FixedPrecision with that Scale, exercising clampToAssetsTotalScale's
// roundToPosteriorVaultScale branch instead.
// Builds a bare ltVAULT SLE with only sfAsset and sfAssetsTotal set,
// mirroring what a transactor does: set the STNumber field, then call
// associateAsset() so it is quantized to the asset's STAmount grid, the
// same way VaultDeposit::doApply does for a real vault (see
// src/libxrpl/tx/transactors/vault/VaultDeposit.cpp).
static std::shared_ptr<SLE>
makeVault(
Asset const& asset,
Number const& assetsTotal,
std::optional<std::uint8_t> fixedScale = std::nullopt)
makeVault(Asset const& asset, Number const& assetsTotal)
{
auto vault = std::make_shared<SLE>(keylet::vault(UInt256(1)));
vault->setFieldIssue(sfAsset, STIssue{sfAsset, asset});
vault->at(sfAssetsTotal) = assetsTotal;
associateAsset(*vault, asset);
if (fixedScale)
{
vault->at(sfLEVersion) = std::to_underlying(VaultVersion::FixedPrecision);
vault->at(sfScale) = *fixedScale;
// FixedPrecision derives its total from these two fields.
vault->at(sfAssetsAvailable) = assetsTotal;
vault->at(sfAssetsDeployed) = Number{0};
}
return vault;
}
// Runs every case in cases against asset, once per ambient rounding
// Runs every case in `cases` against `asset`, once per ambient rounding
// mode. The function must give the same answer under all four modes,
// and its answer must match the hand-derived expected value.
// fixedScale, when set, builds a FixedPrecision vault at that Scale
// instead of the default Legacy vault.
// and its answer must match the hand-derived `expected` value.
template <std::size_t N>
void
runCases(
Asset const& asset,
std::array<Case, N> const& cases,
std::optional<std::uint8_t> fixedScale = std::nullopt,
bool viaClampVaultOutflow = false)
runCases(Asset const& asset, std::array<Case, N> const& cases)
{
std::array<Number::RoundingMode, 4> const modes{
Number::RoundingMode::ToNearest,
@@ -111,7 +91,7 @@ private:
{
testcase(c.name);
auto const vault = makeVault(asset, c.assetsTotal, fixedScale);
auto const vault = makeVault(asset, c.assetsTotal);
BEAST_EXPECTS(
Number(vault->at(sfAssetsTotal)) == c.assetsTotal,
std::string(c.name) +
@@ -127,8 +107,7 @@ private:
for (auto const mode : modes)
{
NumberRoundModeGuard const rg(mode);
auto const result = viaClampVaultOutflow ? clampVaultOutflow(vault, delta)
: clampToAssetsTotalScale(vault, delta);
auto const result = clampToAssetsTotalScale(vault, delta);
// The function must be insensitive to the caller's ambient
// rounding mode: every mode must agree with the first one
@@ -172,13 +151,7 @@ private:
continue;
}
// c.expected is guaranteed set here: the !c.expected case
// above always continues to the next loop iteration.
STAmount const expected{
asset,
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
*c.expected,
};
STAmount const expected{asset, *c.expected};
if (!BEAST_EXPECTS(
result.has_value(),
std::string(c.name) + ": expected success (" + expected.getText() +
@@ -203,9 +176,9 @@ private:
// representable at that scale.
//
// For debits this holds directly at postScale, because the
// result IS roundToScale(magnitude, postScale, Downward) by
// result IS `roundToScale(magnitude, postScale, Downward)` by
// construction. For credits the result is
// roundedPosterior - assetsTotal, where roundedPosterior
// `roundedPosterior - assetsTotal`, where roundedPosterior
// sits exactly on the postScale grid but assetsTotal sits on
// its own (possibly finer) natural grid; the difference of a
// multiple of 10^postScale and a multiple of 10^assetsScale
@@ -374,7 +347,7 @@ private:
// = 4.999999999999991.
// This mirrors testBugVaultDepositOvercreditsAcrossScaleBoundary
// in VaultBugs_test.cpp (same seed/deposit values), which
// asserts post-fix credited <= paid rather than an exact
// asserts post-fix `credited <= paid` rather than an exact
// number; this row pins the exact value.
.name = "IOU credit: overcredit fix across a scale boundary",
.assetsTotal = Number{9'999'999'999'999'999LL, -15},
@@ -490,100 +463,6 @@ private:
runCases(xrp, xrpCases);
}
// -------------------------------------------------------------------
// FixedPrecision vaults, credits: clampToAssetsTotalScale takes the
// roundToPosteriorVaultScale branch instead of the Legacy/CashBasis
// scale()-of-the-sum branch; it is FixedPrecision's only credit path
// (VaultDeposit). All rows below use a Scale-6 vault (baseScale -6)
// with assetsTotal already sitting on that grid, so TowardsZero-
// truncating delta itself to scale -6 is the whole story: no case
// here forces liveScale to coarsen past baseScale (that scenario needs
// a non-unit share price and is exercised by
// VaultFixedPrecision_test.cpp's testPartialTowardZeroRounding / dust
// tests through real transactions instead).
// -------------------------------------------------------------------
void
testFixedPrecisionCredit(Asset const& iou)
{
std::uint8_t const fixedScale = 6;
Number const onGrid{3'234'567, -6}; // 3.234567, exact at scale -6.
std::array<Case, 3> const cases{
Case{
// delta already exact at the base grid: passes through
// unchanged, same as a Legacy on-grid credit.
.name = "FixedPrecision credit: exact on the base grid",
.assetsTotal = onGrid,
.delta = Number{2, -6},
.expected = Number{2, -6},
},
Case{
// delta = +2.3 base units, truncates to 2 base units.
.name = "FixedPrecision credit: truncated toward zero on the base grid",
.assetsTotal = onGrid,
.delta = Number{23, -7},
.expected = Number{2, -6},
},
Case{
// delta = +0.4 base units: sub-ULP at the fixed grid, so
// TowardsZero truncates it entirely to zero.
.name = "FixedPrecision credit: sub-ULP dust rejected at the base grid",
.assetsTotal = onGrid,
.delta = Number{4, -7},
.expected = std::nullopt,
},
};
runCases(iou, cases, fixedScale);
}
// -------------------------------------------------------------------
// FixedPrecision vaults, debits: outflows (VaultWithdraw, VaultClawback)
// never reach clampToAssetsTotalScale's FixedPrecision branch, which
// only accepts non-negative deltas; they go through the public
// clampVaultOutflow dispatcher instead, which rounds at
// AssetsAvailable's own posterior scale (clampToAvailableScale). These
// rows pin the same truncation behavior through that entry point.
// -------------------------------------------------------------------
void
testFixedPrecisionDebit(Asset const& iou)
{
std::uint8_t const fixedScale = 6;
Number const onGrid{3'234'567, -6}; // 3.234567, exact at scale -6.
std::array<Case, 3> const cases{
Case{
// delta already exact at the base grid: passes through
// unchanged, same as a Legacy on-grid debit.
.name = "FixedPrecision debit: exact on the base grid",
.assetsTotal = onGrid,
.delta = Number{-1, -6},
.expected = Number{1, -6},
},
Case{
// delta = -1.7 base units. TowardsZero truncates the
// magnitude to 1 base unit -- this is the "books the
// truncated amount" behavior VaultFixedPrecision_test's
// testPartialTowardZeroRounding exercises end-to-end via
// VaultWithdraw; this row pins it at the helper level.
.name = "FixedPrecision debit: truncated toward zero on the base grid",
.assetsTotal = onGrid,
.delta = Number{-17, -7},
.expected = Number{1, -6},
},
Case{
// delta = -0.3 base units: sub-ULP at the fixed grid, so
// TowardsZero truncates it entirely to zero.
.name = "FixedPrecision debit: sub-ULP dust rejected at the base grid",
.assetsTotal = onGrid,
.delta = Number{-3, -7},
.expected = std::nullopt,
},
};
runCases(iou, cases, fixedScale, /* viaClampVaultOutflow */ true);
}
public:
void
run() override
@@ -598,8 +477,6 @@ public:
testIouDebits(iou);
testIouCredits(iou);
testIntegralAssets(mpt, xrp);
testFixedPrecisionCredit(iou);
testFixedPrecisionDebit(iou);
}
};
+7 -14
View File
@@ -1052,15 +1052,13 @@ private:
}
void
testAssetsMaximum(FeatureBitset features)
testAssetsMaximum()
{
bool const withFix = features[fixCleanup3_5_0];
testcase(
std::string("Assets Maximum (") + (withFix ? "post" : "pre") + "-fixCleanup3_5_0)");
testcase("Assets Maximum");
using namespace test::jtx;
Env env{*this, features};
Env env{*this, testableAmendments()};
Account const owner{"owner"};
Account const issuer{"issuer"};
@@ -1086,10 +1084,6 @@ private:
auto const initialXRPPlus1 = to_string(kInitialXrp + 1);
BEAST_EXPECT(initialXRPPlus1 == "100000000000000001");
// Without the fix, rounding an out-of-range AssetsMaximum to the vault
// asset throws.
TER const outOfRangeErr = withFix ? TER{temMALFORMED} : TER{tefEXCEPTION};
{
testcase("Assets Maximum: XRP");
@@ -1099,11 +1093,11 @@ private:
tx[sfData] = "4D65746144617461";
tx[sfAssetsMaximum] = maxInt64;
env(tx, Ter(outOfRangeErr));
env(tx, Ter(tefEXCEPTION));
env.close();
tx[sfAssetsMaximum] = initialXRPPlus1;
env(tx, Ter(outOfRangeErr));
env(tx, Ter(tefEXCEPTION));
env.close();
tx[sfAssetsMaximum] = initialXRP;
@@ -1352,7 +1346,7 @@ private:
// What _can't_ IOUs do?
// 1. Exceed maximum exponent / offset
tx[sfAssetsMaximum] = "1000000000000000e81";
env(tx, Ter(outOfRangeErr));
env(tx, Ter(tefEXCEPTION));
env.close();
// 2. Mantissa larger than uint64 max
@@ -1377,8 +1371,7 @@ public:
run() override
{
testScaleIOU();
testAssetsMaximum(all_ - fixCleanup3_5_0);
testAssetsMaximum(all_);
testAssetsMaximum();
}
};
@@ -30,13 +30,11 @@
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
#include <xrpl/protocol/SeqProxy.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h>
#include <cstdint>
#include <functional>
#include <string>
#include <tuple>
@@ -1181,62 +1179,6 @@ private:
}
}
void
testAssetsMaximumOutOfRange(FeatureBitset features)
{
using namespace test::jtx;
bool const withFix = features[fixCleanup3_5_0];
testcase(
std::string("AssetsMaximum out of range for the asset (") + (withFix ? "post" : "pre") +
"-fixCleanup3_5_0)");
Env env{*this, features};
Account const issuer{"issuer"};
Account const owner{"owner"};
env.fund(XRP(1'000'000), issuer, owner);
env.close();
Vault const vault{env};
// Without the fix, rounding an out-of-range AssetsMaximum to the vault
// asset throws.
TER const createErr = withFix ? TER{temMALFORMED} : TER{tefEXCEPTION};
TER const setErr = withFix ? TER{tecPRECISION_LOSS} : TER{tefEXCEPTION};
auto const test = [&](Asset const& asset, Number const& maxValid, Number const& overMax) {
{
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
tx[sfAssetsMaximum] = to_string(overMax);
env(tx, Ter(createErr));
env.close();
BEAST_EXPECT(!env.le(keylet));
}
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
tx[sfAssetsMaximum] = to_string(maxValid);
env(tx);
env.close();
auto setTx = vault.set({.owner = owner, .id = keylet.key});
setTx[sfAssetsMaximum] = to_string(overMax);
env(setTx, Ter(setErr));
env.close();
if (auto const sle = env.le(keylet); BEAST_EXPECT(sle))
BEAST_EXPECT(sle->at(sfAssetsMaximum) == maxValid);
};
Number const maxXrp{static_cast<std::int64_t>(STAmount::kMaxNativeN)};
test(xrpIssue(), maxXrp, maxXrp + 1);
MPT const btc = MPTTester({.env = env, .issuer = issuer, .holders = {owner}});
test(btc.asset(), Number{static_cast<std::int64_t>(kMaxMpTokenAmount)}, Number{1, 19});
Number const maxIou{static_cast<std::int64_t>(STAmount::kMaxValue), STAmount::kMaxOffset};
test(issuer["USD"], maxIou, Number{1, STAmount::kMaxOffset + 16});
}
public:
void
run() override
@@ -1250,9 +1192,6 @@ public:
testVaultWithdrawPseudoAccountDestination(all_ - fixCleanup3_4_0);
testVaultWithdrawPseudoAccountDestination(all_);
testAssetsMaximumOutOfRange(all_ - fixCleanup3_5_0);
testAssetsMaximumOutOfRange(all_);
}
};
+31
View File
@@ -16,10 +16,12 @@
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/ApiVersion.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Book.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/JsonRpc.h>
#include <xrpl/protocol/PathAsset.h>
#include <xrpl/protocol/Quality.h>
#include <xrpl/protocol/SField.h>
@@ -363,6 +365,35 @@ getAccountLines(Env& env, AccountID const& acctId, IOU... ious)
[[nodiscard]] bool
checkArraySize(json::Value const& val, unsigned int size);
/**
* Reports whether @p reply carries the version 2 envelope: `jsonrpc` equal to
* rpc::kJsonRpcVersion and `ripplerpc` equal to rpc::kRippleRpcVersion2, both
* present.
*
* @param reply The reply to inspect.
* @return True when both members are present with those values.
*/
[[nodiscard]]
inline bool
hasEnvelope2(json::Value const& reply)
{
return reply.isMember(jss::jsonrpc) && reply[jss::jsonrpc] == rpc::kJsonRpcVersion &&
reply.isMember(jss::ripplerpc) && reply[jss::ripplerpc] == rpc::kRippleRpcVersion2;
}
/**
* Sets the version 2 envelope on @p request: `jsonrpc` to rpc::kJsonRpcVersion
* and `ripplerpc` to rpc::kRippleRpcVersion2.
*
* @param request The request to stamp.
*/
inline void
setEnvelope2(json::Value& request)
{
request[jss::jsonrpc] = rpc::kJsonRpcVersion;
request[jss::ripplerpc] = rpc::kRippleRpcVersion2;
}
// Helper function that returns the owner count on an account.
std::uint32_t
ownerCount(test::jtx::Env const& env, test::jtx::Account const& account);
+4 -2
View File
@@ -10,6 +10,8 @@
#include <xrpl/json/json_reader.h>
#include <xrpl/json/json_value.h>
#include <xrpl/json/to_string.h>
#include <xrpl/protocol/ApiVersion.h>
#include <xrpl/protocol/JsonRpc.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/server/Port.h>
@@ -163,8 +165,8 @@ public:
jr[jss::method] = cmd;
if (rpcVersion_ == 2)
{
jr[jss::jsonrpc] = "2.0";
jr[jss::ripplerpc] = "2.0";
jr[jss::jsonrpc] = rpc::kJsonRpcVersion;
jr[jss::ripplerpc] = rpc::kRippleRpcVersion2;
jr[jss::id] = 5;
}
if (params)
+4 -2
View File
@@ -9,6 +9,8 @@
#include <xrpl/json/json_reader.h>
#include <xrpl/json/json_value.h>
#include <xrpl/json/to_string.h>
#include <xrpl/protocol/ApiVersion.h>
#include <xrpl/protocol/JsonRpc.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/server/Port.h>
@@ -207,8 +209,8 @@ public:
if (rpcVersion_ == 2)
{
jp[jss::method] = cmd;
jp[jss::jsonrpc] = "2.0";
jp[jss::ripplerpc] = "2.0";
jp[jss::jsonrpc] = rpc::kJsonRpcVersion;
jp[jss::ripplerpc] = rpc::kRippleRpcVersion2;
jp[jss::id] = 5;
}
else
+8 -14
View File
@@ -1,6 +1,7 @@
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/TestHelpers.h>
#include <test/jtx/amount.h>
#include <test/jtx/flags.h>
#include <test/jtx/multisign.h>
@@ -371,8 +372,7 @@ public:
BEAST_EXPECT(
info.isMember(jss::result) && info[jss::result].isMember(jss::account_data));
BEAST_EXPECT(!info[jss::result][jss::account_data].isMember(jss::signer_lists));
BEAST_EXPECT(info.isMember(jss::jsonrpc) && info[jss::jsonrpc] == "2.0");
BEAST_EXPECT(info.isMember(jss::ripplerpc) && info[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(info));
BEAST_EXPECT(info.isMember(jss::id) && info[jss::id] == 5);
}
{
@@ -385,8 +385,7 @@ public:
auto const& signerLists = data[jss::signer_lists];
BEAST_EXPECT(signerLists.isArray());
BEAST_EXPECT(signerLists.size() == 0);
BEAST_EXPECT(info.isMember(jss::jsonrpc) && info[jss::jsonrpc] == "2.0");
BEAST_EXPECT(info.isMember(jss::ripplerpc) && info[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(info));
BEAST_EXPECT(info.isMember(jss::id) && info[jss::id] == 6);
}
{
@@ -396,8 +395,7 @@ public:
info[0u].isMember(jss::result) &&
info[0u][jss::result].isMember(jss::account_data));
BEAST_EXPECT(!info[0u][jss::result][jss::account_data].isMember(jss::signer_lists));
BEAST_EXPECT(info[0u].isMember(jss::jsonrpc) && info[0u][jss::jsonrpc] == "2.0");
BEAST_EXPECT(info[0u].isMember(jss::ripplerpc) && info[0u][jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(info[0u]));
BEAST_EXPECT(info[0u].isMember(jss::id) && info[0u][jss::id] == 5);
BEAST_EXPECT(
@@ -408,8 +406,7 @@ public:
auto const& signerLists = data[jss::signer_lists];
BEAST_EXPECT(signerLists.isArray());
BEAST_EXPECT(signerLists.size() == 0);
BEAST_EXPECT(info[1u].isMember(jss::jsonrpc) && info[1u][jss::jsonrpc] == "2.0");
BEAST_EXPECT(info[1u].isMember(jss::ripplerpc) && info[1u][jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(info[1u]));
BEAST_EXPECT(info[1u].isMember(jss::id) && info[1u][jss::id] == 6);
}
@@ -424,8 +421,7 @@ public:
BEAST_EXPECT(
info.isMember(jss::result) && info[jss::result].isMember(jss::account_data));
BEAST_EXPECT(!info[jss::result][jss::account_data].isMember(jss::signer_lists));
BEAST_EXPECT(info.isMember(jss::jsonrpc) && info[jss::jsonrpc] == "2.0");
BEAST_EXPECT(info.isMember(jss::ripplerpc) && info[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(info));
BEAST_EXPECT(info.isMember(jss::id) && info[jss::id] == 5);
}
{
@@ -445,8 +441,7 @@ public:
BEAST_EXPECT(signerEntries.size() == 1);
auto const& entry0 = signerEntries[0u][sfSignerEntry.jsonName];
BEAST_EXPECT(entry0[sfSignerWeight.jsonName] == 3);
BEAST_EXPECT(info.isMember(jss::jsonrpc) && info[jss::jsonrpc] == "2.0");
BEAST_EXPECT(info.isMember(jss::ripplerpc) && info[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(info));
BEAST_EXPECT(info.isMember(jss::id) && info[jss::id] == 6);
}
@@ -495,8 +490,7 @@ public:
BEAST_EXPECT(entry.isMember(sfAccount.jsonName));
BEAST_EXPECT(entry[sfSignerWeight.jsonName] == 1);
}
BEAST_EXPECT(info.isMember(jss::jsonrpc) && info[jss::jsonrpc] == "2.0");
BEAST_EXPECT(info.isMember(jss::ripplerpc) && info[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(info));
BEAST_EXPECT(info.isMember(jss::id) && info[jss::id] == 6);
}
}
+77 -129
View File
@@ -797,25 +797,21 @@ public:
// account_lines with mal-formed json2 (missing id field).
json::Value request;
request[jss::method] = "account_lines";
request[jss::jsonrpc] = "2.0";
request[jss::ripplerpc] = "2.0";
setEnvelope2(request);
auto const lines = env.rpc("json2", to_string(request));
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(lines));
}
{
// account_lines with no account.
json::Value request;
request[jss::method] = "account_lines";
request[jss::jsonrpc] = "2.0";
request[jss::ripplerpc] = "2.0";
setEnvelope2(request);
request[jss::id] = 5;
auto const lines = env.rpc("json2", to_string(request));
BEAST_EXPECT(
lines[jss::error][jss::message] ==
rpc::missingFieldError(jss::account)[jss::error_message]);
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(lines));
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
}
{
@@ -824,16 +820,14 @@ public:
params[jss::account] = "n9MJkEKHDhy5eTLuHUQeAAjo382frHNbFK4C8hcwN4nwM2SrLdBj";
json::Value request;
request[jss::method] = "account_lines";
request[jss::jsonrpc] = "2.0";
request[jss::ripplerpc] = "2.0";
setEnvelope2(request);
request[jss::id] = 5;
request[jss::params] = params;
auto const lines = env.rpc("json2", to_string(request));
BEAST_EXPECT(
lines[jss::error][jss::message] ==
rpc::makeError(RpcActMalformed)[jss::error_message]);
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(lines));
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
}
Account const alice{"alice"};
@@ -843,16 +837,14 @@ public:
params[jss::account] = alice.human();
json::Value request;
request[jss::method] = "account_lines";
request[jss::jsonrpc] = "2.0";
request[jss::ripplerpc] = "2.0";
setEnvelope2(request);
request[jss::id] = 5;
request[jss::params] = params;
auto const lines = env.rpc("json2", to_string(request));
BEAST_EXPECT(
lines[jss::error][jss::message] ==
rpc::makeError(RpcActNotFound)[jss::error_message]);
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(lines));
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
}
env.fund(XRP(10000), alice);
@@ -869,16 +861,14 @@ public:
json::Value request;
request[jss::method] = "account_lines";
request[jss::jsonrpc] = "2.0";
request[jss::ripplerpc] = "2.0";
setEnvelope2(request);
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::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");
BEAST_EXPECT(hasEnvelope2(lines));
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
};
@@ -895,15 +885,13 @@ public:
params[jss::account] = alice.human();
json::Value request;
request[jss::method] = "account_lines";
request[jss::jsonrpc] = "2.0";
request[jss::ripplerpc] = "2.0";
setEnvelope2(request);
request[jss::id] = 5;
request[jss::params] = params;
auto const lines = env.rpc("json2", to_string(request));
BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
BEAST_EXPECT(lines[jss::result][jss::lines].size() == 0);
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(lines));
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
}
{
@@ -913,16 +901,14 @@ public:
params[jss::ledger_index] = "nonsense";
json::Value request;
request[jss::method] = "account_lines";
request[jss::jsonrpc] = "2.0";
request[jss::ripplerpc] = "2.0";
setEnvelope2(request);
request[jss::id] = 5;
request[jss::params] = params;
auto const lines = env.rpc("json2", to_string(request));
BEAST_EXPECT(
lines[jss::error][jss::message] ==
"Invalid field 'ledger_index', not string or number.");
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(lines));
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
}
{
@@ -932,14 +918,12 @@ public:
params[jss::ledger_index] = 50000;
json::Value request;
request[jss::method] = "account_lines";
request[jss::jsonrpc] = "2.0";
request[jss::ripplerpc] = "2.0";
setEnvelope2(request);
request[jss::id] = 5;
request[jss::params] = params;
auto const lines = env.rpc("json2", to_string(request));
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");
BEAST_EXPECT(hasEnvelope2(lines));
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
}
// Create trust lines to share with alice.
@@ -992,44 +976,38 @@ public:
BEAST_EXPECT(ledger58Info.seq == 58);
// A re-usable test for historic ledgers.
auto const testAccountLinesHistory = [this, &env](
Account const& account,
LedgerHeader const& info,
int count) {
// Get account_lines by ledger index.
json::Value paramsSeq;
paramsSeq[jss::account] = account.human();
paramsSeq[jss::ledger_index] = info.seq;
json::Value requestSeq;
requestSeq[jss::method] = "account_lines";
requestSeq[jss::jsonrpc] = "2.0";
requestSeq[jss::ripplerpc] = "2.0";
requestSeq[jss::id] = 5;
requestSeq[jss::params] = paramsSeq;
auto const linesSeq = env.rpc("json2", to_string(requestSeq));
BEAST_EXPECT(linesSeq[jss::result][jss::lines].isArray());
BEAST_EXPECT(linesSeq[jss::result][jss::lines].size() == count);
BEAST_EXPECT(linesSeq.isMember(jss::jsonrpc) && linesSeq[jss::jsonrpc] == "2.0");
BEAST_EXPECT(linesSeq.isMember(jss::ripplerpc) && linesSeq[jss::ripplerpc] == "2.0");
BEAST_EXPECT(linesSeq.isMember(jss::id) && linesSeq[jss::id] == 5);
auto const testAccountLinesHistory =
[this, &env](Account const& account, LedgerHeader const& info, int count) {
// Get account_lines by ledger index.
json::Value paramsSeq;
paramsSeq[jss::account] = account.human();
paramsSeq[jss::ledger_index] = info.seq;
json::Value requestSeq;
requestSeq[jss::method] = "account_lines";
setEnvelope2(requestSeq);
requestSeq[jss::id] = 5;
requestSeq[jss::params] = paramsSeq;
auto const linesSeq = env.rpc("json2", to_string(requestSeq));
BEAST_EXPECT(linesSeq[jss::result][jss::lines].isArray());
BEAST_EXPECT(linesSeq[jss::result][jss::lines].size() == count);
BEAST_EXPECT(hasEnvelope2(linesSeq));
BEAST_EXPECT(linesSeq.isMember(jss::id) && linesSeq[jss::id] == 5);
// Get account_lines by ledger hash.
json::Value paramsHash;
paramsHash[jss::account] = account.human();
paramsHash[jss::ledger_hash] = to_string(info.hash);
json::Value requestHash;
requestHash[jss::method] = "account_lines";
requestHash[jss::jsonrpc] = "2.0";
requestHash[jss::ripplerpc] = "2.0";
requestHash[jss::id] = 5;
requestHash[jss::params] = paramsHash;
auto const linesHash = env.rpc("json2", to_string(requestHash));
BEAST_EXPECT(linesHash[jss::result][jss::lines].isArray());
BEAST_EXPECT(linesHash[jss::result][jss::lines].size() == count);
BEAST_EXPECT(linesHash.isMember(jss::jsonrpc) && linesHash[jss::jsonrpc] == "2.0");
BEAST_EXPECT(linesHash.isMember(jss::ripplerpc) && linesHash[jss::ripplerpc] == "2.0");
BEAST_EXPECT(linesHash.isMember(jss::id) && linesHash[jss::id] == 5);
};
// Get account_lines by ledger hash.
json::Value paramsHash;
paramsHash[jss::account] = account.human();
paramsHash[jss::ledger_hash] = to_string(info.hash);
json::Value requestHash;
requestHash[jss::method] = "account_lines";
setEnvelope2(requestHash);
requestHash[jss::id] = 5;
requestHash[jss::params] = paramsHash;
auto const linesHash = env.rpc("json2", to_string(requestHash));
BEAST_EXPECT(linesHash[jss::result][jss::lines].isArray());
BEAST_EXPECT(linesHash[jss::result][jss::lines].size() == count);
BEAST_EXPECT(hasEnvelope2(linesHash));
BEAST_EXPECT(linesHash.isMember(jss::id) && linesHash[jss::id] == 5);
};
// Alice should have no trust lines in ledger 3.
testAccountLinesHistory(alice, ledger3Info, 0);
@@ -1049,8 +1027,7 @@ public:
params[jss::ledger_index] = ledger58Info.seq;
json::Value request;
request[jss::method] = "account_lines";
request[jss::jsonrpc] = "2.0";
request[jss::ripplerpc] = "2.0";
setEnvelope2(request);
request[jss::id] = 5;
request[jss::params] = params;
auto const lines = env.rpc("json2", to_string(request));
@@ -1059,8 +1036,7 @@ public:
lines[jss::error][jss::message] ==
"Exactly one of 'ledger_hash' or 'ledger_index' can be "
"specified.");
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(lines));
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
}
{
@@ -1069,15 +1045,13 @@ public:
params[jss::account] = alice.human();
json::Value request;
request[jss::method] = "account_lines";
request[jss::jsonrpc] = "2.0";
request[jss::ripplerpc] = "2.0";
setEnvelope2(request);
request[jss::id] = 5;
request[jss::params] = params;
auto const lines = env.rpc("json2", to_string(request));
BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
BEAST_EXPECT(lines[jss::result][jss::lines].size() == 52);
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(lines));
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
}
{
@@ -1087,15 +1061,13 @@ public:
params[jss::peer] = gw1.human();
json::Value request;
request[jss::method] = "account_lines";
request[jss::jsonrpc] = "2.0";
request[jss::ripplerpc] = "2.0";
setEnvelope2(request);
request[jss::id] = 5;
request[jss::params] = params;
auto const lines = env.rpc("json2", to_string(request));
BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
BEAST_EXPECT(lines[jss::result][jss::lines].size() == 26);
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(lines));
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
}
{
@@ -1105,16 +1077,14 @@ public:
params[jss::peer] = "n9MJkEKHDhy5eTLuHUQeAAjo382frHNbFK4C8hcwN4nwM2SrLdBj";
json::Value request;
request[jss::method] = "account_lines";
request[jss::jsonrpc] = "2.0";
request[jss::ripplerpc] = "2.0";
setEnvelope2(request);
request[jss::id] = 5;
request[jss::params] = params;
auto const lines = env.rpc("json2", to_string(request));
BEAST_EXPECT(
lines[jss::error][jss::message] ==
rpc::makeError(RpcActMalformed)[jss::error_message]);
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(lines));
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
}
{
@@ -1124,16 +1094,14 @@ public:
params[jss::limit] = -1;
json::Value request;
request[jss::method] = "account_lines";
request[jss::jsonrpc] = "2.0";
request[jss::ripplerpc] = "2.0";
setEnvelope2(request);
request[jss::id] = 5;
request[jss::params] = params;
auto const lines = env.rpc("json2", to_string(request));
BEAST_EXPECT(
lines[jss::error][jss::message] ==
rpc::expectedFieldMessage(jss::limit, "unsigned integer"));
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(lines));
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
}
{
@@ -1143,15 +1111,13 @@ public:
paramsA[jss::limit] = 1;
json::Value requestA;
requestA[jss::method] = "account_lines";
requestA[jss::jsonrpc] = "2.0";
requestA[jss::ripplerpc] = "2.0";
setEnvelope2(requestA);
requestA[jss::id] = 5;
requestA[jss::params] = paramsA;
auto const linesA = env.rpc("json2", to_string(requestA));
BEAST_EXPECT(linesA[jss::result][jss::lines].isArray());
BEAST_EXPECT(linesA[jss::result][jss::lines].size() == 1);
BEAST_EXPECT(linesA.isMember(jss::jsonrpc) && linesA[jss::jsonrpc] == "2.0");
BEAST_EXPECT(linesA.isMember(jss::ripplerpc) && linesA[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(linesA));
BEAST_EXPECT(linesA.isMember(jss::id) && linesA[jss::id] == 5);
// Pick up from where the marker left off. We should get 51.
@@ -1161,15 +1127,13 @@ public:
paramsB[jss::marker] = marker;
json::Value requestB;
requestB[jss::method] = "account_lines";
requestB[jss::jsonrpc] = "2.0";
requestB[jss::ripplerpc] = "2.0";
setEnvelope2(requestB);
requestB[jss::id] = 5;
requestB[jss::params] = paramsB;
auto const linesB = env.rpc("json2", to_string(requestB));
BEAST_EXPECT(linesB[jss::result][jss::lines].isArray());
BEAST_EXPECT(linesB[jss::result][jss::lines].size() == 51);
BEAST_EXPECT(linesB.isMember(jss::jsonrpc) && linesB[jss::jsonrpc] == "2.0");
BEAST_EXPECT(linesB.isMember(jss::ripplerpc) && linesB[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(linesB));
BEAST_EXPECT(linesB.isMember(jss::id) && linesB[jss::id] == 5);
// Go again from where the marker left off, but set a limit of 3.
@@ -1179,15 +1143,13 @@ public:
paramsC[jss::marker] = marker;
json::Value requestC;
requestC[jss::method] = "account_lines";
requestC[jss::jsonrpc] = "2.0";
requestC[jss::ripplerpc] = "2.0";
setEnvelope2(requestC);
requestC[jss::id] = 5;
requestC[jss::params] = paramsC;
auto const linesC = env.rpc("json2", to_string(requestC));
BEAST_EXPECT(linesC[jss::result][jss::lines].isArray());
BEAST_EXPECT(linesC[jss::result][jss::lines].size() == 3);
BEAST_EXPECT(linesC.isMember(jss::jsonrpc) && linesC[jss::jsonrpc] == "2.0");
BEAST_EXPECT(linesC.isMember(jss::ripplerpc) && linesC[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(linesC));
BEAST_EXPECT(linesC.isMember(jss::id) && linesC[jss::id] == 5);
// Mess with the marker so it becomes bad and check for the error.
@@ -1197,16 +1159,14 @@ public:
paramsD[jss::marker] = marker;
json::Value requestD;
requestD[jss::method] = "account_lines";
requestD[jss::jsonrpc] = "2.0";
requestD[jss::ripplerpc] = "2.0";
setEnvelope2(requestD);
requestD[jss::id] = 5;
requestD[jss::params] = paramsD;
auto const linesD = env.rpc("json2", to_string(requestD));
BEAST_EXPECT(
linesD[jss::error][jss::message] ==
rpc::makeError(RpcInvalidParams)[jss::error_message]);
BEAST_EXPECT(linesD.isMember(jss::jsonrpc) && linesD[jss::jsonrpc] == "2.0");
BEAST_EXPECT(linesD.isMember(jss::ripplerpc) && linesD[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(linesD));
BEAST_EXPECT(linesD.isMember(jss::id) && linesD[jss::id] == 5);
}
{
@@ -1216,16 +1176,14 @@ public:
params[jss::marker] = true;
json::Value request;
request[jss::method] = "account_lines";
request[jss::jsonrpc] = "2.0";
request[jss::ripplerpc] = "2.0";
setEnvelope2(request);
request[jss::id] = 5;
request[jss::params] = params;
auto const lines = env.rpc("json2", to_string(request));
BEAST_EXPECT(
lines[jss::error][jss::message] ==
rpc::expectedFieldMessage(jss::marker, "string"));
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(lines));
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
}
{
@@ -1236,8 +1194,7 @@ public:
params[jss::peer] = gw2.human();
json::Value request;
request[jss::method] = "account_lines";
request[jss::jsonrpc] = "2.0";
request[jss::ripplerpc] = "2.0";
setEnvelope2(request);
request[jss::id] = 5;
request[jss::params] = params;
auto const lines = env.rpc("json2", to_string(request));
@@ -1246,8 +1203,7 @@ public:
BEAST_EXPECT(line[jss::deep_freeze].asBool() == true);
BEAST_EXPECT(line[jss::no_ripple].asBool() == true);
BEAST_EXPECT(line[jss::peer_authorized].asBool() == true);
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(lines));
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
}
{
@@ -1258,8 +1214,7 @@ public:
paramsA[jss::peer] = alice.human();
json::Value requestA;
requestA[jss::method] = "account_lines";
requestA[jss::jsonrpc] = "2.0";
requestA[jss::ripplerpc] = "2.0";
setEnvelope2(requestA);
requestA[jss::id] = 5;
requestA[jss::params] = paramsA;
auto const linesA = env.rpc("json2", to_string(requestA));
@@ -1268,8 +1223,7 @@ public:
BEAST_EXPECT(lineA[jss::deep_freeze_peer].asBool() == true);
BEAST_EXPECT(lineA[jss::no_ripple_peer].asBool() == true);
BEAST_EXPECT(lineA[jss::authorized].asBool() == true);
BEAST_EXPECT(linesA.isMember(jss::jsonrpc) && linesA[jss::jsonrpc] == "2.0");
BEAST_EXPECT(linesA.isMember(jss::ripplerpc) && linesA[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(linesA));
BEAST_EXPECT(linesA.isMember(jss::id) && linesA[jss::id] == 5);
// Continue from the returned marker to make sure that works.
@@ -1282,16 +1236,14 @@ public:
paramsB[jss::peer] = alice.human();
json::Value requestB;
requestB[jss::method] = "account_lines";
requestB[jss::jsonrpc] = "2.0";
requestB[jss::ripplerpc] = "2.0";
setEnvelope2(requestB);
requestB[jss::id] = 5;
requestB[jss::params] = paramsB;
auto const linesB = env.rpc("json2", to_string(requestB));
BEAST_EXPECT(linesB[jss::result][jss::lines].isArray());
BEAST_EXPECT(linesB[jss::result][jss::lines].size() == 25);
BEAST_EXPECT(!linesB[jss::result].isMember(jss::marker));
BEAST_EXPECT(linesB.isMember(jss::jsonrpc) && linesB[jss::jsonrpc] == "2.0");
BEAST_EXPECT(linesB.isMember(jss::ripplerpc) && linesB[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(linesB));
BEAST_EXPECT(linesB.isMember(jss::id) && linesB[jss::id] == 5);
}
}
@@ -1351,15 +1303,13 @@ public:
linesBegParams[jss::limit] = 2;
json::Value linesBegRequest;
linesBegRequest[jss::method] = "account_lines";
linesBegRequest[jss::jsonrpc] = "2.0";
linesBegRequest[jss::ripplerpc] = "2.0";
setEnvelope2(linesBegRequest);
linesBegRequest[jss::id] = 5;
linesBegRequest[jss::params] = linesBegParams;
auto const linesBeg = env.rpc("json2", to_string(linesBegRequest));
BEAST_EXPECT(linesBeg[jss::result][jss::lines][0u][jss::currency] == "USD");
BEAST_EXPECT(linesBeg[jss::result].isMember(jss::marker));
BEAST_EXPECT(linesBeg.isMember(jss::jsonrpc) && linesBeg[jss::jsonrpc] == "2.0");
BEAST_EXPECT(linesBeg.isMember(jss::ripplerpc) && linesBeg[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(linesBeg));
BEAST_EXPECT(linesBeg.isMember(jss::id) && linesBeg[jss::id] == 5);
// alice pays 100 USD to cheri.
@@ -1373,16 +1323,14 @@ public:
linesEndParams[jss::marker] = linesBeg[jss::result][jss::marker];
json::Value linesEndRequest;
linesEndRequest[jss::method] = "account_lines";
linesEndRequest[jss::jsonrpc] = "2.0";
linesEndRequest[jss::ripplerpc] = "2.0";
setEnvelope2(linesEndRequest);
linesEndRequest[jss::id] = 5;
linesEndRequest[jss::params] = linesEndParams;
auto const linesEnd = env.rpc("json2", to_string(linesEndRequest));
BEAST_EXPECT(
linesEnd[jss::error][jss::message] ==
rpc::makeError(RpcInvalidParams)[jss::error_message]);
BEAST_EXPECT(linesEnd.isMember(jss::jsonrpc) && linesEnd[jss::jsonrpc] == "2.0");
BEAST_EXPECT(linesEnd.isMember(jss::ripplerpc) && linesEnd[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(linesEnd));
BEAST_EXPECT(linesEnd.isMember(jss::id) && linesEnd[jss::id] == 5);
}
+21 -40
View File
@@ -1,4 +1,5 @@
#include <test/jtx/Env.h>
#include <test/jtx/TestHelpers.h>
#include <test/jtx/WSClient.h>
#include <test/jtx/amount.h>
#include <test/jtx/domain.h>
@@ -88,8 +89,7 @@ public:
auto jv = wsc->invoke("subscribe", books);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
if (!BEAST_EXPECT(jv[jss::status] == "success"))
@@ -126,8 +126,7 @@ public:
BEAST_EXPECT(jv[jss::status] == "success");
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
}
@@ -165,8 +164,7 @@ public:
auto jv = wsc->invoke("subscribe", books);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
if (!BEAST_EXPECT(jv[jss::status] == "success"))
@@ -209,8 +207,7 @@ public:
BEAST_EXPECT(jv[jss::status] == "success");
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
}
@@ -242,8 +239,7 @@ public:
auto jv = wsc->invoke("subscribe", books);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
if (!BEAST_EXPECT(jv[jss::status] == "success"))
@@ -288,8 +284,7 @@ public:
BEAST_EXPECT(jv[jss::status] == "success");
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
}
@@ -328,8 +323,7 @@ public:
auto jv = wsc->invoke("subscribe", books);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
if (!BEAST_EXPECT(jv[jss::status] == "success"))
@@ -386,8 +380,7 @@ public:
BEAST_EXPECT(jv[jss::status] == "success");
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
}
@@ -428,8 +421,7 @@ public:
auto jv = wsc->invoke("subscribe", books);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
if (!BEAST_EXPECT(jv[jss::status] == "success"))
@@ -487,8 +479,7 @@ public:
BEAST_EXPECT(jv[jss::status] == "success");
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
}
@@ -542,8 +533,7 @@ public:
auto jv = wsc->invoke("subscribe", books);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
if (!BEAST_EXPECT(jv[jss::status] == "success"))
@@ -613,8 +603,7 @@ public:
BEAST_EXPECT(jv[jss::status] == "success");
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
}
@@ -657,8 +646,7 @@ public:
auto jv = wsc->invoke("subscribe", books);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
if (!BEAST_EXPECT(jv[jss::status] == "success"))
@@ -731,8 +719,7 @@ public:
BEAST_EXPECT(jv[jss::status] == "success");
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
}
@@ -789,8 +776,7 @@ public:
auto jv = wsc->invoke("subscribe", books);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
if (!BEAST_EXPECT(jv[jss::status] == "success"))
@@ -887,8 +873,7 @@ public:
BEAST_EXPECT(jv[jss::status] == "success");
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
}
@@ -921,8 +906,7 @@ public:
auto jv = wsc->invoke("subscribe", books);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
if (!BEAST_EXPECT(jv[jss::status] == "success"))
@@ -952,8 +936,7 @@ public:
auto jv = wsc->invoke("book_offers", jvParams);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
auto jrr = jv[jss::result];
@@ -997,8 +980,7 @@ public:
jv = wsc->invoke("book_offers", jvParams);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
jrr = jv[jss::result];
@@ -1023,8 +1005,7 @@ public:
jv = wsc->invoke("unsubscribe", books);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
+3 -2
View File
@@ -1,5 +1,6 @@
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/TestHelpers.h>
#include <test/jtx/WSClient.h>
#include <test/jtx/amount.h>
#include <test/jtx/balance.h>
@@ -22,6 +23,7 @@
#include <xrpl/protocol/jss.h>
#include <chrono>
#include <string>
#include <utility>
namespace xrpl::test {
@@ -254,8 +256,7 @@ class DeliveredAmount_test : public beast::unit_test::Suite
auto jv = wsc->invoke("subscribe", stream);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::result][jss::ledger_index] == 3);
+2 -2
View File
@@ -1,5 +1,6 @@
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/TestHelpers.h>
#include <test/jtx/WSClient.h>
#include <test/jtx/amount.h>
#include <test/jtx/escrow.h>
@@ -85,8 +86,7 @@ public:
expect(jv[jss::status] == "success");
if (wsc->version() == 2)
{
expect(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
expect(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
expect(hasEnvelope2(jv));
expect(jv.isMember(jss::id) && jv[jss::id] == 5);
}
+397
View File
@@ -0,0 +1,397 @@
#include <test/jtx/Env.h>
#include <xrpld/rpc/detail/MaskSecrets.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/json/json_reader.h>
#include <xrpl/json/json_value.h>
#include <xrpl/json/to_string.h>
#include <xrpl/protocol/jss.h>
#include <algorithm>
#include <array>
#include <string>
#include <string_view>
namespace xrpl::test {
class MaskSecrets_test : public beast::unit_test::Suite
{
static constexpr char const* kMasked = "<masked>";
static constexpr char const* kSensitive = "sensitive";
public:
/**
* Every field in `kCredentialFields` is masked, at the top level.
*/
void
testEveryCredentialField()
{
testcase("Every credential field is masked");
json::Value request(json::ValueType::Object);
for (auto const field : rpc::kCredentialFields)
request[std::string{field}] = kSensitive;
auto const masked = rpc::maskSecrets(request);
for (auto const field : rpc::kCredentialFields)
BEAST_EXPECTS(masked[std::string{field}] == kMasked, std::string{field});
// An empty list would let every assertion above pass while masking nothing.
BEAST_EXPECT(!rpc::kCredentialFields.empty());
}
/**
* A field the list does not name is left as it was.
*/
void
testNonCredentialsSurvive()
{
testcase("Fields that are not credentials are left alone");
json::Value request(json::ValueType::Object);
request[jss::method] = "sign";
request[jss::account] = "rSomeAccount";
request[jss::secret] = kSensitive;
auto const masked = rpc::maskSecrets(request);
BEAST_EXPECT(masked[jss::secret] == kMasked);
BEAST_EXPECT(masked[jss::method] == "sign");
BEAST_EXPECT(masked[jss::account] == "rSomeAccount");
// Masking replaces members rather than adding or removing them.
BEAST_EXPECT(masked.size() == request.size());
}
/**
* A member name is matched whole, not as a substring or case-insensitively.
*/
void
testNamesAreMatchedExactly()
{
testcase("Only an exact field name is masked");
json::Value request(json::ValueType::Object);
for (auto const* name : {"secrets", "Secret", "my_secret", "seedling", "sec", ""})
request[name] = kSensitive;
auto const masked = rpc::maskSecrets(request);
for (auto const* name : {"secrets", "Secret", "my_secret", "seedling", "sec", ""})
BEAST_EXPECTS(masked[name] == kSensitive, name);
}
/**
* A credential is masked wherever it sits, not only at the top level.
*
* The JSON-RPC transport nests a request's fields inside `params`, which is
* where a `secret` arrives.
*/
void
testNesting()
{
testcase("A nested credential is masked at any depth");
// Where the JSON-RPC transport carries a credential: inside `params`.
{
json::Value params(json::ValueType::Object);
params[jss::secret] = kSensitive;
params[jss::account] = "rSomeAccount";
json::Value request(json::ValueType::Object);
request[jss::method] = "sign";
request[jss::params] = json::ValueType::Array;
request[jss::params][0u] = params;
auto const masked = rpc::maskSecrets(request);
BEAST_EXPECT(masked[jss::params][0u][jss::secret] == kMasked);
BEAST_EXPECT(masked[jss::params][0u][jss::account] == "rSomeAccount");
BEAST_EXPECT(masked[jss::method] == "sign");
}
// Where a batch carries one: inside an entry, itself inside `params`.
{
json::Value entry(json::ValueType::Object);
entry[jss::id] = 2;
entry[jss::seed] = kSensitive;
json::Value batch(json::ValueType::Object);
batch[jss::method] = "batch";
batch[jss::params] = json::ValueType::Array;
batch[jss::params][0u] = entry;
auto const masked = rpc::maskSecrets(batch);
BEAST_EXPECT(masked[jss::params][0u][jss::seed] == kMasked);
BEAST_EXPECT(masked[jss::params][0u][jss::id] == 2);
}
// A credential in an array element, which names no member of its own.
{
json::Value inner(json::ValueType::Object);
inner[jss::passphrase] = kSensitive;
json::Value request(json::ValueType::Array);
request[0u] = inner;
auto const masked = rpc::maskSecrets(request);
BEAST_EXPECT(masked[0u][jss::passphrase] == kMasked);
}
// Up to the parser's nesting limit, which is what bounds the recursion.
{
json::Value deep(json::ValueType::Object);
deep[jss::secret] = kSensitive;
for (unsigned i{0}; i < json::Reader::kNestLimit; ++i)
{
json::Value outer(json::ValueType::Object);
outer[jss::params] = deep;
deep = outer;
}
auto const masked = rpc::maskSecrets(deep);
auto const* level = &masked;
for (unsigned i{0}; i < json::Reader::kNestLimit; ++i)
level = &(*level)[jss::params];
BEAST_EXPECT((*level)[jss::secret] == kMasked);
}
}
/**
* A credential's own value is replaced whole, whatever shape it has.
*
* An object or array under `secret` is not walked into: the whole value is
* the credential.
*/
void
testCredentialIsNotDescendedInto()
{
testcase("A credential is replaced whatever it holds");
json::Value nested(json::ValueType::Object);
nested["inner"] = kSensitive;
json::Value request(json::ValueType::Object);
request[jss::secret] = nested;
request[jss::seed] = json::ValueType::Array;
request[jss::seed][0u] = kSensitive;
auto const masked = rpc::maskSecrets(request);
BEAST_EXPECT(masked[jss::secret] == kMasked);
BEAST_EXPECT(masked[jss::seed] == kMasked);
BEAST_EXPECT(!to_string(masked).contains(kSensitive));
}
/**
* A credential that is not a string is masked too, the value being replaced
* whole.
*/
void
testNonStringCredentials()
{
testcase("A credential of any type is masked");
json::Value request(json::ValueType::Object);
request["secret"] = 12345;
request["seed"] = true;
request["passphrase"] = 1.5;
request["password"] = json::ValueType::Null;
auto const masked = rpc::maskSecrets(request);
for (auto const* field : {"secret", "seed", "passphrase", "password"})
BEAST_EXPECTS(masked[field] == kMasked, field);
}
/**
* A value with no members is returned unchanged, masking keying on member
* names.
*/
void
testValuesWithoutMembers()
{
testcase("A value that has no members is returned unchanged");
BEAST_EXPECT(rpc::maskSecrets(json::Value{}).isNull());
BEAST_EXPECT(rpc::maskSecrets(json::Value{kSensitive}) == kSensitive);
BEAST_EXPECT(rpc::maskSecrets(json::Value{42}) == 42);
BEAST_EXPECT(rpc::maskSecrets(json::Value{json::ValueType::Object}).size() == 0);
BEAST_EXPECT(rpc::maskSecrets(json::Value{json::ValueType::Array}).size() == 0);
}
/**
* The request the caller passed in is not modified.
*
* Every call site goes on to use the request it holds, so the mask copies.
*/
void
testRequestIsNotModified()
{
testcase("The request passed in is left unchanged");
json::Value request(json::ValueType::Object);
request[jss::secret] = kSensitive;
request[jss::params] = json::ValueType::Array;
request[jss::params][0u] = json::ValueType::Object;
request[jss::params][0u][jss::seed] = kSensitive;
auto const before = to_string(request);
auto const masked = rpc::maskSecrets(request);
BEAST_EXPECT(to_string(request) == before);
BEAST_EXPECT(request[jss::secret] == kSensitive);
BEAST_EXPECT(request[jss::params][0u][jss::seed] == kSensitive);
BEAST_EXPECT(masked[jss::secret] == kMasked);
}
/**
* `hasSecret` reports a credential at any depth and nothing else.
*
* Every name on the list is reported at the top level, one nested where the
* JSON-RPC transport puts it and one inside an array element are reported
* too, and a value naming none, or having no members at all, is not.
*/
void
testHasSecret()
{
testcase("A credential is detected at any depth");
for (auto const field : rpc::kCredentialFields)
{
json::Value request(json::ValueType::Object);
request[std::string{field}] = kSensitive;
BEAST_EXPECTS(rpc::hasSecret(request), std::string{field});
}
// Inside `params`, where the JSON-RPC transport carries a credential.
{
json::Value request(json::ValueType::Object);
request[jss::method] = "sign";
request[jss::params] = json::ValueType::Array;
request[jss::params][0u] = json::ValueType::Object;
request[jss::params][0u][jss::account] = "rSomeAccount";
request[jss::params][0u][jss::secret] = kSensitive;
BEAST_EXPECT(rpc::hasSecret(request));
}
// Inside an array element, which names no member of its own.
{
json::Value request(json::ValueType::Array);
request[0u] = json::ValueType::Object;
request[0u][jss::passphrase] = kSensitive;
BEAST_EXPECT(rpc::hasSecret(request));
}
// No credential at any depth, and a near miss is not one.
{
json::Value request(json::ValueType::Object);
request[jss::method] = "account_info";
request[jss::params] = json::ValueType::Array;
request[jss::params][0u] = json::ValueType::Object;
request[jss::params][0u][jss::account] = "rSomeAccount";
request[jss::params][0u]["secrets"] = kSensitive;
BEAST_EXPECT(!rpc::hasSecret(request));
}
// A value with no members carries none.
BEAST_EXPECT(!rpc::hasSecret(json::Value{}));
BEAST_EXPECT(!rpc::hasSecret(json::Value{kSensitive}));
BEAST_EXPECT(!rpc::hasSecret(json::Value{42}));
BEAST_EXPECT(!rpc::hasSecret(json::Value{json::ValueType::Object}));
BEAST_EXPECT(!rpc::hasSecret(json::Value{json::ValueType::Array}));
}
/**
* `loggable` masks a value that carries a credential, then truncates at
* `kMaxLoggedChars`; one that carries none is rendered as it is.
*
* The mask has to survive the cap, so the credential is placed where a
* sorted member order puts it first.
*/
void
testLoggable()
{
testcase("A rendering for a log line is masked and capped");
// Sorts before the filler, so the mask falls inside the cap.
json::Value request;
request[jss::secret] = kSensitive;
request["zfiller"] = std::string(2 * rpc::kMaxLoggedChars, 'z');
auto const rendered = rpc::loggable(request);
BEAST_EXPECT(rendered.size() == rpc::kMaxLoggedChars);
BEAST_EXPECT(rendered.contains(kMasked));
BEAST_EXPECT(!rendered.contains(kSensitive));
// A short request is not padded.
json::Value small;
small[jss::seed] = kSensitive;
BEAST_EXPECT(rpc::loggable(small) == to_string(rpc::maskSecrets(small)));
// A value with no credential is the serialized value itself, cut at the cap.
json::Value plain;
plain[jss::account] = "rSomeAccount";
plain["zfiller"] = std::string(2 * rpc::kMaxLoggedChars, 'z');
BEAST_EXPECT(rpc::loggable(plain) == to_string(plain).substr(0, rpc::kMaxLoggedChars));
BEAST_EXPECT(rpc::loggable(small[jss::seed]) == to_string(small[jss::seed]));
}
/**
* A keygen reply is rendered for the log with no key in it.
*
* Every field of each reply is checked, not only the names on the list, so
* a field the list is missing fails here. Only the fields a client may read
* in public are allowed through.
*/
void
testKeygenRepliesAreMasked()
{
testcase("A keygen reply is logged with every key masked");
static constexpr std::array<std::string_view, 6> kPublicFields{
"account_id",
"key_type",
"public_key",
"public_key_hex",
"status",
"validation_public_key",
};
jtx::Env env{*this};
for (auto const command : {"wallet_propose", "validation_create"})
{
auto const reply = env.rpc(command);
auto const& result = reply[jss::result];
BEAST_EXPECT(result[jss::status] == "success");
auto const rendered = rpc::loggable(reply);
for (auto const& name : result.getMemberNames())
{
if (std::ranges::find(kPublicFields, name) != kPublicFields.end())
continue;
auto const& value = result[name];
BEAST_EXPECT(value.isString() && !value.asString().empty());
BEAST_EXPECTS(!rendered.contains(value.asString()), command + (": " + name));
}
}
}
void
run() override
{
testEveryCredentialField();
testNonCredentialsSurvive();
testNamesAreMatchedExactly();
testNesting();
testCredentialIsNotDescendedInto();
testNonStringCredentials();
testValuesWithoutMembers();
testRequestIsNotModified();
testHasSecret();
testLoggable();
testKeygenRepliesAreMasked();
}
};
BEAST_DEFINE_TESTSUITE(MaskSecrets, rpc, xrpl);
} // namespace xrpl::test
+22 -42
View File
@@ -1,4 +1,5 @@
#include <test/jtx/Env.h>
#include <test/jtx/TestHelpers.h>
#include <test/jtx/WSClient.h>
#include <test/jtx/amount.h>
#include <test/jtx/balance.h> // IWYU pragma: keep
@@ -38,8 +39,7 @@ public:
BEAST_EXPECT(jv[jss::status] == "success");
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
}
@@ -53,8 +53,7 @@ public:
auto jv = wsc->invoke("submit", payment);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::result][jss::engine_result] == "tefMAX_LEDGER");
@@ -65,8 +64,7 @@ public:
jv = wsc->invoke("submit", payment);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::result][jss::engine_result] == "tefPAST_SEQ");
@@ -76,8 +74,7 @@ public:
jv = wsc->invoke("submit", payment);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::result][jss::engine_result] == "terPRE_SEQ");
@@ -87,8 +84,7 @@ public:
jv = wsc->invoke("submit", payment);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::result][jss::engine_result] == "tesSUCCESS");
@@ -100,8 +96,7 @@ public:
jv = wsc->invoke("ledger_accept");
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::result].isMember(jss::ledger_current_index));
@@ -128,8 +123,7 @@ public:
jv = wsc->invoke("unsubscribe", jv);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
@@ -164,8 +158,7 @@ public:
jv = wsc->invoke("submit", jv);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::result][jss::engine_result] == "tesSUCCESS");
@@ -187,8 +180,7 @@ public:
jv = wsc->invoke("account_tx", jv);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
@@ -218,8 +210,7 @@ public:
jv = wsc->invoke("submit", jv);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::result][jss::engine_result] == "tesSUCCESS");
@@ -228,8 +219,7 @@ public:
jv = wsc->invoke("ledger_accept");
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::result].isMember(jss::ledger_current_index));
@@ -247,8 +237,7 @@ public:
jv = wsc->invoke("subscribe", jv);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
@@ -260,8 +249,7 @@ public:
auto jv = wsc->invoke("ledger_accept");
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::result].isMember(jss::ledger_current_index));
@@ -281,8 +269,7 @@ public:
jv = wsc->invoke("unsubscribe", jv);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
@@ -300,8 +287,7 @@ public:
jv = wsc->invoke("subscribe", jv);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
@@ -313,8 +299,7 @@ public:
auto jv = wsc->invoke("ledger_accept");
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::result].isMember(jss::ledger_current_index));
@@ -334,8 +319,7 @@ public:
jv = wsc->invoke("unsubscribe", jv);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
@@ -352,8 +336,7 @@ public:
jv = wsc->invoke("account_tx", jv);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
@@ -383,8 +366,7 @@ public:
jv = wsc->invoke("subscribe", jv);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
@@ -399,8 +381,7 @@ public:
jv = wsc->invoke("submit", jv);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::result][jss::engine_result] == "tesSUCCESS");
@@ -421,8 +402,7 @@ public:
jv = wsc->invoke("unsubscribe", jv);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
+15 -28
View File
@@ -1,4 +1,5 @@
#include <test/jtx/Env.h>
#include <test/jtx/TestHelpers.h>
#include <test/jtx/WSClient.h>
#include <test/jtx/amount.h>
#include <test/jtx/batch.h>
@@ -83,8 +84,7 @@ public:
auto jv = wsc->invoke("subscribe", stream);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
@@ -113,8 +113,7 @@ public:
auto jv = wsc->invoke("unsubscribe", stream);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
@@ -149,8 +148,7 @@ public:
auto jv = wsc->invoke("subscribe", stream);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::result][jss::ledger_index] == 2);
@@ -184,8 +182,7 @@ public:
auto jv = wsc->invoke("unsubscribe", stream);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
@@ -208,8 +205,7 @@ public:
auto jv = wsc->invoke("subscribe", stream);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
@@ -266,8 +262,7 @@ public:
auto jv = wsc->invoke("unsubscribe", stream);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
@@ -281,8 +276,7 @@ public:
auto jv = wsc->invoke("subscribe", stream);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
@@ -314,8 +308,7 @@ public:
auto jv = wsc->invoke("unsubscribe", stream);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
@@ -344,8 +337,7 @@ public:
auto jv = wsc->invoke("subscribe", stream);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
@@ -391,8 +383,7 @@ public:
auto jv = wsc->invoke("unsubscribe", stream);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
@@ -414,8 +405,7 @@ public:
auto jv = wsc->invoke("subscribe", stream);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
@@ -425,8 +415,7 @@ public:
auto jv = wsc->invoke("unsubscribe", stream);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
@@ -461,8 +450,7 @@ public:
auto jv = wsc->invoke("subscribe", stream);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
@@ -537,8 +525,7 @@ public:
auto jv = wsc->invoke("unsubscribe", stream);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
+691 -19
View File
@@ -1,18 +1,25 @@
#include <test/jtx/Account.h>
#include <test/jtx/CaptureLogs.h>
#include <test/jtx/Env.h>
#include <test/jtx/JSONRPCClient.h>
#include <test/jtx/WSClient.h>
#include <test/jtx/amount.h>
#include <test/jtx/envconfig.h>
#include <xrpld/app/ledger/LedgerMaster.h>
#include <xrpld/rpc/detail/MaskSecrets.h>
#include <xrpl/basics/base64.h>
#include <xrpl/beast/test/yield_to.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/config/Constants.h>
#include <xrpl/json/json_reader.h>
#include <xrpl/json/json_value.h>
#include <xrpl/json/to_string.h>
#include <xrpl/protocol/ApiVersion.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/Seed.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/server/LoadFeeTrack.h>
#include <xrpl/server/NetworkOPs.h>
@@ -37,11 +44,13 @@
#include <boost/lexical_cast.hpp>
#include <array>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <random>
#include <regex>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
@@ -203,6 +212,43 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
}
}
using Response = boost::beast::http::response<boost::beast::http::string_body>;
static constexpr auto kOk = boost::beast::http::status::ok;
static constexpr auto kBadRequest = boost::beast::http::status::bad_request;
/**
* Posts @p body to the RPC port and returns the reply, parsed.
*
* @param env The environment naming the port.
* @param yield The coroutine the request runs on.
* @param resp Receives the whole HTTP response, for a caller asserting on
* its status.
* @param ec Receives a connection error.
* @param body The request to post.
* @param label Identifies which case failed, for a caller iterating over
* several. An empty one reports what BEAST_EXPECT would.
* @param fields Extra headers to send with it.
* @return The reply, parsed. Empty when the body is not JSON, which this
* asserts against.
*/
json::Value
postAndParse(
test::jtx::Env& env,
boost::asio::yield_context& yield,
Response& resp,
boost::system::error_code& ec,
std::string const& body,
std::string_view label = {},
MyFields const& fields = {})
{
doHTTPRequest(env, yield, false, resp, ec, body, fields);
json::Value reply;
BEAST_EXPECTS(json::Reader{}.parse(resp.body(), reply), label);
return reply;
}
void
doWSRequest(
test::jtx::Env& env,
@@ -217,6 +263,60 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
doRequest(yield, makeWSUpgrade(*ip, *port), *ip, *port, secure, resp, ec);
}
/**
* Sends @p body verbatim over a fresh WebSocket session and returns the
* first reply, parsed, so a message naming no command can be exercised.
*
* @param yield The coroutine the request runs on.
* @param ip The server address.
* @param port The WebSocket port.
* @param body The frame to send.
* @return The reply, parsed. Empty when the connection fails, which this
* asserts against.
*/
json::Value
doWSMessage(
boost::asio::yield_context& yield,
std::string const& ip,
std::uint16_t port,
std::string const& body)
{
using namespace boost::asio;
using namespace boost::beast;
io_context& ios = getIoContext();
websocket::stream<ip::tcp::socket> ws{ios};
boost::system::error_code ec;
ip::tcp::resolver r{ios};
auto const it = r.async_resolve(ip, std::to_string(port), yield[ec]);
if (!BEAST_EXPECTS(!ec, ec.message()))
return {};
async_connect(ws.next_layer(), it, yield[ec]);
if (!BEAST_EXPECTS(!ec, ec.message()))
return {};
ws.async_handshake(ip + ":" + std::to_string(port), "/", yield[ec]);
if (!BEAST_EXPECTS(!ec, ec.message()))
return {};
ws.async_write(buffer(body), yield[ec]);
if (!BEAST_EXPECTS(!ec, ec.message()))
return {};
multi_buffer sb;
ws.async_read(sb, yield[ec]);
if (!BEAST_EXPECTS(!ec, ec.message()))
return {};
std::string text;
text.resize(buffer_size(sb.data()));
buffer_copy(buffer(text.data(), text.size()), sb.data());
json::Value reply;
BEAST_EXPECT(json::Reader{}.parse(text, reply));
return reply;
}
void
doHTTPRequest(
test::jtx::Env& env,
@@ -409,7 +509,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
doHTTPRequest(env, yield, false, resp, ec);
if (!BEAST_EXPECTS(!ec, ec.message()))
return;
BEAST_EXPECT(resp.result() == boost::beast::http::status::ok);
BEAST_EXPECT(resp.result() == kOk);
}
// secure request
@@ -419,7 +519,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
doHTTPRequest(env, yield, true, resp, ec);
if (!BEAST_EXPECTS(!ec, ec.message()))
return;
BEAST_EXPECT(resp.result() == boost::beast::http::status::ok);
BEAST_EXPECT(resp.result() == kOk);
}
}
@@ -545,7 +645,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
// finally if we use the correct user/pass encoded, we should get a 200
auth.set("Authorization", "Basic " + base64Encode(user + ":" + pass));
doHTTPRequest(env, yield, secure, resp, ec, to_string(jr), auth);
BEAST_EXPECT(resp.result() == boost::beast::http::status::ok);
BEAST_EXPECT(resp.result() == kOk);
BEAST_EXPECT(!resp.body().empty());
}
@@ -732,6 +832,16 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
auto resp = sendAndParse("NOT JSON");
BEAST_EXPECT(resp.isMember(jss::error) && resp[jss::error] == "jsonInvalid");
BEAST_EXPECT(!resp.isMember(jss::status));
// The body is reported by size, not echoed.
BEAST_EXPECT(resp.isMember(jss::size) && resp[jss::size] == 8);
BEAST_EXPECT(!resp.isMember(jss::value));
}
{ // an unparsable frame carrying a credential does not echo it
auto resp =
sendAndParse(R"({"command":"submit","secret":"snoPBrXtMeMyMHUVTgbuqAfg1SUTb",})");
BEAST_EXPECT(resp.isMember(jss::error) && resp[jss::error] == "jsonInvalid");
BEAST_EXPECT(!to_string(resp).contains("snoPBrXtMeMyMHUVTgbuqAfg1SUTb"));
}
{ // send incorrect json (method and command fields differ)
@@ -813,7 +923,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
if (!BEAST_EXPECTS(!ec, ec.message()))
return;
BEAST_EXPECT(resp.result() == boost::beast::http::status::ok);
BEAST_EXPECT(resp.result() == kOk);
BEAST_EXPECT(resp.body().contains("connectivity is working."));
// mark the Network as having an Amendment Warning, but won't fail
@@ -858,7 +968,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
if (!BEAST_EXPECTS(!ec, ec.message()))
return;
BEAST_EXPECT(resp.result() == boost::beast::http::status::ok);
BEAST_EXPECT(resp.result() == kOk);
BEAST_EXPECT(resp.body().contains("connectivity is working."));
// with ELB_SUPPORT, status still does not indicate a problem
@@ -878,7 +988,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
if (!BEAST_EXPECTS(!ec, ec.message()))
return;
BEAST_EXPECT(resp.result() == boost::beast::http::status::ok);
BEAST_EXPECT(resp.result() == kOk);
BEAST_EXPECT(resp.body().contains("connectivity is working."));
}
@@ -941,7 +1051,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
if (!BEAST_EXPECTS(!ec, ec.message()))
return;
BEAST_EXPECT(resp.result() == boost::beast::http::status::ok);
BEAST_EXPECT(resp.result() == kOk);
BEAST_EXPECT(resp.body().contains("connectivity is working."));
// mark the Network as Amendment Blocked, but still won't fail until
@@ -989,7 +1099,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
if (!BEAST_EXPECTS(!ec, ec.message()))
return;
BEAST_EXPECT(resp.result() == boost::beast::http::status::ok);
BEAST_EXPECT(resp.result() == kOk);
BEAST_EXPECT(resp.body().contains("connectivity is working."));
env.app().config().elbSupport = true;
@@ -1025,7 +1135,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
{
boost::beast::http::response<boost::beast::http::string_body> resp;
doHTTPRequest(env, yield, false, resp, ec, "{}");
BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request);
BEAST_EXPECT(resp.result() == kBadRequest);
BEAST_EXPECT(resp.body() == "Unable to parse request: \r\n");
}
@@ -1034,7 +1144,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
json::Value jv;
jv["invalid"] = 1;
doHTTPRequest(env, yield, false, resp, ec, to_string(jv));
BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request);
BEAST_EXPECT(resp.result() == kBadRequest);
BEAST_EXPECT(resp.body() == "Null method\r\n");
}
@@ -1043,7 +1153,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
json::Value jv(json::ValueType::Array);
jv.append("invalid");
doHTTPRequest(env, yield, false, resp, ec, to_string(jv));
BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request);
BEAST_EXPECT(resp.result() == kBadRequest);
BEAST_EXPECT(resp.body() == "Unable to parse request: \r\n");
}
@@ -1054,7 +1164,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
j["invalid"] = 1;
jv.append(j);
doHTTPRequest(env, yield, false, resp, ec, to_string(jv));
BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request);
BEAST_EXPECT(resp.result() == kBadRequest);
BEAST_EXPECT(resp.body() == "Unable to parse request: \r\n");
}
@@ -1064,7 +1174,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
jv[jss::method] = "batch";
jv[jss::params] = 2;
doHTTPRequest(env, yield, false, resp, ec, to_string(jv));
BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request);
BEAST_EXPECT(resp.result() == kBadRequest);
BEAST_EXPECT(resp.body() == "Malformed batch request\r\n");
}
@@ -1075,7 +1185,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
jv[jss::params] = json::ValueType::Object;
jv[jss::params]["invalid"] = 3;
doHTTPRequest(env, yield, false, resp, ec, to_string(jv));
BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request);
BEAST_EXPECT(resp.result() == kBadRequest);
BEAST_EXPECT(resp.body() == "Malformed batch request\r\n");
}
@@ -1084,7 +1194,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
boost::beast::http::response<boost::beast::http::string_body> resp;
jv[jss::method] = json::ValueType::Null;
doHTTPRequest(env, yield, false, resp, ec, to_string(jv));
BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request);
BEAST_EXPECT(resp.result() == kBadRequest);
BEAST_EXPECT(resp.body() == "Null method\r\n");
}
@@ -1092,7 +1202,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
boost::beast::http::response<boost::beast::http::string_body> resp;
jv[jss::method] = 1;
doHTTPRequest(env, yield, false, resp, ec, to_string(jv));
BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request);
BEAST_EXPECT(resp.result() == kBadRequest);
BEAST_EXPECT(resp.body() == "method is not string\r\n");
}
@@ -1100,7 +1210,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
boost::beast::http::response<boost::beast::http::string_body> resp;
jv[jss::method] = "";
doHTTPRequest(env, yield, false, resp, ec, to_string(jv));
BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request);
BEAST_EXPECT(resp.result() == kBadRequest);
BEAST_EXPECT(resp.body() == "method is empty\r\n");
}
@@ -1109,7 +1219,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
jv[jss::method] = "some_method";
jv[jss::params] = "params";
doHTTPRequest(env, yield, false, resp, ec, to_string(jv));
BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request);
BEAST_EXPECT(resp.result() == kBadRequest);
BEAST_EXPECT(resp.body() == "params unparsable\r\n");
}
@@ -1118,11 +1228,564 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
jv[jss::params] = json::ValueType::Array;
jv[jss::params][0u] = "not an object";
doHTTPRequest(env, yield, false, resp, ec, to_string(jv));
BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request);
BEAST_EXPECT(resp.result() == kBadRequest);
BEAST_EXPECT(resp.body() == "params unparsable\r\n");
}
}
/**
* The `ripplerpc: "3.0"` envelope reports 200 for the codes below.
*
* `account_info` on an account the ledger does not hold is a routine call,
* and a 4xx there turns a working reply into a failure for anything that
* fails over on one. The status each code names in the table is pinned in
* the `ErrorCodes` gtest.
*
* @param yield The coroutine the requests run on.
*/
void
testGainedStatusesStayOffLegacyEnvelope(boost::asio::yield_context& yield)
{
testcase("A code that gained an HTTP status keeps 200 on the legacy envelope");
using namespace test::jtx;
Env env{*this, envconfig([](std::unique_ptr<Config> cfg) {
cfg->loadFromString(std::string("[") + Sections::kSigningSupport + "]\ntrue");
return cfg;
})};
Account const alice{"alice"};
// Never funded, so the ledger holds no entry for it.
Account const absent{"absent"};
env.fund(XRP(10000), alice);
env.close();
boost::system::error_code ec;
// Accepted by the signing checks, so the request reaches the two conditions below.
// `sign_for` fills nothing in, so the fee and sequence are named here.
auto const accountSet = [&env, &alice] {
json::Value tx;
tx[jss::Account] = alice.human();
tx[jss::TransactionType] = jss::AccountSet;
tx[jss::SigningPubKey] = "";
tx[jss::Fee] = (8 * env.current()->fees().base).jsonClipped();
tx[jss::Sequence] = env.seq(alice);
return tx;
};
auto const account = [](std::string_view ident) {
json::Value params(json::ValueType::Object);
params[jss::account] = ident;
return params;
};
// `sign` single-signs, so a transaction already carrying `Signers` is multisigned.
json::Value multisigned(json::ValueType::Object);
multisigned[jss::secret] = toBase58(generateSeed("alice"));
multisigned[jss::tx_json] = accountSet();
multisigned[jss::tx_json][jss::Signers] = json::ValueType::Array;
// `sign_for` multisigns, so a transaction already carrying `TxnSignature` is single-signed.
json::Value singleSigned(json::ValueType::Object);
singleSigned[jss::account] = alice.human();
singleSigned[jss::secret] = toBase58(generateSeed("alice"));
singleSigned[jss::tx_json] = accountSet();
singleSigned[jss::tx_json][jss::TxnSignature] = "DEADBEEF";
struct Case
{
char const* method;
json::Value params;
ErrorCodeI code;
};
for (auto& [method, params, code] : {
Case{
.method = "account_info",
.params = account("bogus"),
.code = RpcActMalformed,
},
Case{
.method = "account_info",
.params = account(absent.human()),
.code = RpcActNotFound,
},
Case{.method = "sign", .params = multisigned, .code = RpcAlreadyMultisig},
Case{.method = "sign_for", .params = singleSigned, .code = RpcAlreadySingleSig},
})
{
json::Value jv;
jv[jss::method] = method;
jv[jss::params] = json::ValueType::Array;
jv[jss::params][0u] = params;
jv[jss::params][0u][jss::ripplerpc] = rpc::kRippleRpcVersion3;
Response resp;
auto const& info = rpc::getErrorInfo(code);
auto const label = std::string{info.token.cStr()};
auto const reply = postAndParse(env, yield, resp, ec, to_string(jv), label);
BEAST_EXPECTS(reply[jss::error][jss::error] == info.token.cStr(), label);
BEAST_EXPECTS(reply[jss::error][jss::error_code] == code, label);
// The code names a status the envelope declines to report, so the two disagree here
// by design.
BEAST_EXPECTS(info.httpStatus != 200, label);
BEAST_EXPECTS(resp.result() == kOk, label);
}
}
/**
* A credential the server echoes back is masked, on every path that echoes.
*
* Driven from `kCredentialFields` itself, so a field added to the list is
* covered here.
*
* @param yield The coroutine the requests run on.
*/
void
testMaskedCredentials(boost::asio::yield_context& yield)
{
testcase("Echoed requests have their credentials masked");
using namespace test::jtx;
Env env{*this};
boost::system::error_code ec;
// Version 1 echoes the request back on an error, so its credentials must be masked.
{
boost::beast::http::response<boost::beast::http::string_body> resp;
json::Value jv;
jv[jss::method] = "sign";
jv[jss::params] = json::ValueType::Array;
json::Value params(json::ValueType::Object);
params[jss::ripplerpc] = rpc::kRippleRpcVersion1;
for (auto const field : rpc::kCredentialFields)
params[std::string{field}] = "sensitive";
jv[jss::params][0u] = params;
auto const reply = postAndParse(env, yield, resp, ec, to_string(jv));
auto const& echoed = reply[jss::result][jss::request];
for (auto const field : rpc::kCredentialFields)
BEAST_EXPECTS(echoed[std::string{field}] == "<masked>", std::string{field});
}
// A request the session rejects before dispatch is echoed back, so it must be masked.
// Written directly, since the client always names a command.
{
auto const port = env.app().config()[Sections::kPortWs].get<std::uint16_t>(Keys::kPort);
auto const ip = env.app().config()[Sections::kPortWs].get<std::string>(Keys::kIp);
json::Value jv;
for (auto const field : rpc::kCredentialFields)
jv[std::string{field}] = "sensitive";
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
auto const reply = doWSMessage(yield, *ip, *port, to_string(jv));
auto const& echoed = reply[jss::request];
for (auto const field : rpc::kCredentialFields)
BEAST_EXPECTS(echoed[std::string{field}] == "<masked>", std::string{field});
}
// A rejection echoing the whole request must mask the credentials inside
// `params`, where the JSON-RPC transport carries them.
{
json::Value params(json::ValueType::Object);
params[jss::account] = "rSomeAccount";
for (auto const field : rpc::kCredentialFields)
params[std::string{field}] = "sensitive";
json::Value entry;
entry[jss::id] = 2;
entry[jss::params] = json::ValueType::Array;
entry[jss::params][0u] = params;
// A batch: a lone request naming no method is answered with a bare message. The
// `"method": "batch"` form nests entries under `params` on every version.
json::Value batch;
batch[jss::method] = "batch";
batch[jss::params] = json::ValueType::Array;
batch[jss::params][0u] = entry;
Response resp;
auto const reply = postAndParse(env, yield, resp, ec, to_string(batch));
BEAST_EXPECT(reply.isArray() && reply.size() == 1);
auto const& echoed = reply[0u][jss::params][0u];
for (auto const field : rpc::kCredentialFields)
BEAST_EXPECTS(echoed[std::string{field}] == "<masked>", std::string{field});
// The rest of the request survives masking; only the credentials are replaced.
BEAST_EXPECT(echoed[jss::account] == "rSomeAccount");
BEAST_EXPECT(reply[0u][jss::id] == 2);
}
}
/**
* The request written to the log is masked, and capped in length.
*
* Both are asserted on one rendering: without the mask a seed reaches the
* log in the clear, and without the cap a client chooses how much it writes
* there. The marker past the cap is expected nowhere, every site rendering
* through `rpc::loggable`.
*
* `CaptureLogs` assigns its text in the destructor, so the `Env` is scoped
* and the string read after it.
*
* @param yield The coroutine the request runs on.
*/
void
testTheLoggedRequestIsMaskedAndCapped(boost::asio::yield_context& yield)
{
testcase("The logged request is masked and capped");
using namespace test::jtx;
// Sorts after `secret` and before the tail marker, so the cap falls between them.
static constexpr std::size_t kFillerSize = 12000;
static constexpr char const* kSensitiveSeed = "snoPBrXtMeMyMHUVTgbuqAfg1SUTb";
static constexpr char const* kTailMarker = "PastTheCapMarker";
std::string logs;
{
Env env{
*this, envconfig(), std::make_unique<CaptureLogs>(&logs), beast::Severity::Debug};
boost::system::error_code ec;
Response resp;
json::Value params(json::ValueType::Object);
params[jss::secret] = kSensitiveSeed;
params["zfiller"] = std::string(kFillerSize, 'z');
params["zzztail"] = kTailMarker;
json::Value jv;
jv[jss::method] = "ping";
jv[jss::params] = json::ValueType::Array;
jv[jss::params][0u] = params;
auto const reply = postAndParse(env, yield, resp, ec, to_string(jv));
BEAST_EXPECT(reply[jss::result][jss::status] == jss::success);
}
auto const occurrences = [&logs](std::string_view needle) {
std::size_t count = 0;
for (auto at = logs.find(needle); at != std::string::npos;
at = logs.find(needle, at + 1))
{
++count;
}
return count;
};
// The seed reaches no line at all, and the duration line wrote the mask in its place.
BEAST_EXPECT(occurrences(kSensitiveSeed) == 0);
BEAST_EXPECT(logs.contains("RPC request processing duration = "));
BEAST_EXPECT(occurrences("<masked>") >= 2);
// No line reached the tail marker, because every site renders through `rpc::loggable`.
BEAST_EXPECT(occurrences(kTailMarker) == 0);
}
/**
* The reply written to the log is masked only when it carries a credential.
*
* A `wallet_propose` reply carries the keys it generated, and its `Reply:`
* line holds `<masked>` in their place. A `server_info` reply carries none
* and is logged as the string the client received.
*
* @param yield The coroutine the requests run on.
*/
void
testTheLoggedReplyIsMaskedOnlyWhenItCarriesACredential(boost::asio::yield_context& yield)
{
testcase("The logged reply is masked only when it carries a credential");
using namespace test::jtx;
std::string logs;
json::Value wallet;
json::Value info;
{
Env env{
*this, envconfig(), std::make_unique<CaptureLogs>(&logs), beast::Severity::Debug};
boost::system::error_code ec;
Response resp;
json::Value jv;
jv[jss::method] = "wallet_propose";
wallet = postAndParse(env, yield, resp, ec, to_string(jv))[jss::result];
BEAST_EXPECT(wallet[jss::status] == jss::success);
jv[jss::method] = "server_info";
info = postAndParse(env, yield, resp, ec, to_string(jv))[jss::result];
BEAST_EXPECT(info[jss::status] == jss::success);
}
BEAST_EXPECT(logs.contains("Reply: "));
// The keys the client received reach no line, and the mask stands in their place.
for (auto const field : {jss::master_seed, jss::master_key})
{
auto const key = wallet[field].asString();
BEAST_EXPECT(!key.empty());
BEAST_EXPECTS(!logs.contains(key), field.cStr());
}
BEAST_EXPECT(logs.contains("<masked>"));
// A reply with no credential is logged as built: one line carries this member as the
// client read it.
auto const version = info[jss::info][jss::build_version].asString();
BEAST_EXPECT(!version.empty());
BEAST_EXPECT(logs.contains("\"build_version\":\"" + version + "\""));
}
/**
* A handler that throws is reported as `internal`, on both transports.
*
* `RPCHandler` catches the throw and injects `internal`, so the `catch`
* arms in `processSession` and `processRequest` are not what answers this.
* What this pins is the reply: `ledger_entry` is `Role::USER`, so an
* anonymous client reaches it.
*
* @param yield The coroutine the requests run on.
*/
void
testInternalErrorIsReportedOnBothTransports(boost::asio::yield_context& yield)
{
testcase("An internal error is reported on both transports");
using namespace test::jtx;
static constexpr char const* kSensitiveSeed = "snoPBrXtMeMyMHUVTgbuqAfg1SUTb";
std::string logs;
{
Env env{
*this, envconfig(), std::make_unique<CaptureLogs>(&logs), beast::Severity::Info};
env.close();
auto const port = env.app().config()[Sections::kPortWs].get<std::uint16_t>(Keys::kPort);
auto const ip = env.app().config()[Sections::kPortWs].get<std::string>(Keys::kIp);
json::Value params(json::ValueType::Object);
params[jss::ledger_index] = jss::validated;
params[jss::hashes] = -1;
params[jss::secret] = kSensitiveSeed;
json::Value jv;
jv[jss::method] = "ledger_entry";
jv[jss::params] = json::ValueType::Array;
jv[jss::params][0u] = params;
boost::system::error_code ec;
Response resp;
auto const reply = postAndParse(env, yield, resp, ec, to_string(jv));
BEAST_EXPECTS(reply[jss::result][jss::error] == "internal", to_string(reply));
json::Value frame(params);
frame[jss::command] = "ledger_entry";
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
auto const wsReply = doWSMessage(yield, *ip, *port, to_string(frame));
BEAST_EXPECTS(wsReply[jss::error] == "internal", to_string(wsReply));
}
// The capture is not empty, so the absence assertion below is not vacuous.
BEAST_EXPECT(logs.contains("Caught throw: "));
// The line reports what was thrown and never the request.
BEAST_EXPECT(!logs.contains(kSensitiveSeed));
}
/**
* The credentials the command line client adds are masked in what it
* prints.
*
* The client copies `admin_user` and `admin_password` out of `[port_rpc]`
* into the request it builds, and prints to stdout rather than to a log.
*
* The account is asserted unmasked beside them, so that masking everything
* does not satisfy this case.
*
* `request_sent` is a second echo of the same request and is not exercised
* here: `fromCommandLine` writes it only on the transport-error path, which
* no jtx harness drives.
*/
void
testCommandLineCredentialsAreMasked()
{
testcase("The command line client masks the credentials it adds");
using namespace test::jtx;
static constexpr char const* kConfiguredPassword = "correct-horse-battery-staple";
Env env{*this, envconfig([](std::unique_ptr<Config> cfg) {
(*cfg)[Sections::kPortRpc].set(Keys::kAdminUser, "operator");
(*cfg)[Sections::kPortRpc].set(Keys::kAdminPassword, kConfiguredPassword);
return cfg;
})};
// `Env::doRpc` retries an internal error, repeating the request for no gain here.
env.setRetries(0);
// Well formed, so the parser accepts it and the server answers `actNotFound`. A malformed
// account fails in the parser, before there is a sent request to echo.
auto const output = env.rpc("account_info", Account{"never-funded"}.human());
// The call reached the server, so the echo below is the request the client built.
auto const& result = output[jss::result];
BEAST_EXPECTS(result[jss::error] == "actNotFound", to_string(output));
// The credentials from the config are masked; the account the operator typed is not.
BEAST_EXPECT(result[jss::request]["admin_password"] == "<masked>");
BEAST_EXPECT(result[jss::request]["admin_user"] == "<masked>");
BEAST_EXPECT(result[jss::request][jss::account] == Account{"never-funded"}.human());
// The subject of the fix: the configured password reaches no member of the output.
BEAST_EXPECT(!to_string(output).contains(kConfiguredPassword));
}
/**
* A credential in an `[rpc_startup]` command is masked in the startup
* log, and so is the result.
*
* Both lines are written at fatal, which the capture is taken at, and
* only by a server that is not quiet. The passphrase is the marker for the
* command line; a `wallet_propose` for a passphrase is deterministic, so
* the `master_seed` the same command returns through the client is the
* marker for the result line.
*/
void
testStartupCommandsAreMasked()
{
testcase("Startup commands and their results are masked in the log");
using namespace test::jtx;
static constexpr char const* kPassphrase = "startup-passphrase-marker";
static constexpr char const* kMalformedMarker = "startup-malformed-marker";
std::string logs;
std::string seed;
{
Env env{
*this,
envconfig([](std::unique_ptr<Config> cfg) {
// The test config is quiet, and a quiet server writes neither line.
cfg->setupControl(false, false, true);
json::Value command(json::ValueType::Object);
command[jss::command] = "wallet_propose";
command[jss::passphrase] = kPassphrase;
cfg->section(Sections::kRpcStartup).append(to_string(command));
// A second line that does not parse (no closing quote or brace) and
// carries a marker where a credential would be. Nothing parsed, so
// masking cannot reach it; the line must not be echoed at all.
cfg->section(Sections::kRpcStartup)
.append(std::string{R"({"passphrase": ")"} + kMalformedMarker);
return cfg;
}),
std::make_unique<CaptureLogs>(&logs),
beast::Severity::Fatal};
json::Value params(json::ValueType::Object);
params[jss::passphrase] = kPassphrase;
auto const reply = env.rpc("json", "wallet_propose", to_string(params));
seed = reply[jss::result][jss::master_seed].asString();
}
// Both lines were written, so the absence assertions below are not vacuous.
BEAST_EXPECT(logs.contains("Startup RPC: "));
BEAST_EXPECT(logs.contains("Result: "));
BEAST_EXPECT(logs.contains("<masked>"));
BEAST_EXPECT(!logs.contains(kPassphrase));
BEAST_EXPECT(!seed.empty());
BEAST_EXPECT(!logs.contains(seed));
// The malformed line is reported by its position and the parser's message, never by
// its text, and no command runs for it: only the well-formed line was logged as run.
BEAST_EXPECT(logs.contains("Couldn't parse entry 2 in [rpc_startup]"));
BEAST_EXPECT(!logs.contains(kMalformedMarker));
auto const occurrences = [&logs](std::string_view needle) {
std::size_t count = 0;
for (auto at = logs.find(needle); at != std::string::npos;
at = logs.find(needle, at + 1))
{
++count;
}
return count;
};
BEAST_EXPECT(occurrences("Startup RPC: ") == 1);
}
/**
* No credential reaches the log at trace, on any transport.
*
* Several render sites write only at `trace`, and no other test raises
* the threshold that far. A `wallet_propose` reply is the control for the
* `HTTP Reply` line: it carries the keys it generated, and that line
* carries the status only.
*
* @param yield The coroutine the requests run on.
*/
void
testNoCredentialReachesTheLogAtTrace(boost::asio::yield_context& yield)
{
testcase("No credential reaches the log at trace");
using namespace test::jtx;
static constexpr char const* kSensitive = "sensitive-trace-value";
std::string logs;
std::string seed;
{
Env env{
*this, envconfig(), std::make_unique<CaptureLogs>(&logs), beast::Severity::Trace};
auto const port = env.app().config()[Sections::kPortWs].get<std::uint16_t>(Keys::kPort);
auto const ip = env.app().config()[Sections::kPortWs].get<std::string>(Keys::kIp);
json::Value params(json::ValueType::Object);
for (auto const field : rpc::kCredentialFields)
params[std::string{field}] = kSensitive;
json::Value jv;
jv[jss::method] = "ping";
jv[jss::params] = json::ValueType::Array;
jv[jss::params][0u] = params;
boost::system::error_code ec;
Response resp;
auto const reply = postAndParse(env, yield, resp, ec, to_string(jv));
BEAST_EXPECT(reply[jss::result][jss::status] == jss::success);
json::Value frame(params);
frame[jss::command] = "ping";
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
doWSMessage(yield, *ip, *port, to_string(frame));
json::Value wallet;
wallet[jss::method] = "wallet_propose";
auto const proposed = postAndParse(env, yield, resp, ec, to_string(wallet));
seed = proposed[jss::result][jss::master_seed].asString();
}
// Both transports logged, so an empty capture cannot pass this.
BEAST_EXPECT(logs.contains("Websocket received '"));
BEAST_EXPECT(logs.contains("doRpcCommand:"));
BEAST_EXPECT(logs.contains("HTTP Reply "));
BEAST_EXPECT(!logs.contains(kSensitive));
BEAST_EXPECT(logs.contains("<masked>"));
// The seed the client received reaches no line, the status-only reply line included.
BEAST_EXPECT(!seed.empty());
BEAST_EXPECT(!logs.contains(seed));
}
void
testStatusNotOkay(boost::asio::yield_context& yield)
{
@@ -1180,8 +1843,17 @@ public:
testNoRPC(yield);
testWSRequests(yield);
testRPCRequests(yield);
testMaskedCredentials(yield);
testTheLoggedRequestIsMaskedAndCapped(yield);
testTheLoggedReplyIsMaskedOnlyWhenItCarriesACredential(yield);
testInternalErrorIsReportedOnBothTransports(yield);
testNoCredentialReachesTheLogAtTrace(yield);
testGainedStatusesStayOffLegacyEnvelope(yield);
testStatusNotOkay(yield);
});
testCommandLineCredentialsAreMasked();
testStartupCommandsAreMasked();
}
};
+85
View File
@@ -598,6 +598,91 @@ TEST(JsonValue, compare_strings)
#pragma pop_macro("DO_COMPARE")
}
TEST(JsonValue, construct_from_string_view)
{
using namespace std::string_view_literals;
// Characters are copied with an explicit length, so a view need not be NUL-terminated.
EXPECT_EQ(json::Value{"2.0"sv}.asString(), "2.0");
EXPECT_EQ(json::Value{"2.0 and more"sv.substr(0, 3)}.asString(), "2.0");
EXPECT_EQ(json::Value{""sv}.asString(), "");
// A default-constructed view has a null `data()` and yields an empty string value.
std::string_view const empty;
json::Value const fromEmpty{empty};
EXPECT_TRUE(fromEmpty.isString());
EXPECT_EQ(fromEmpty.asString(), "");
EXPECT_TRUE(fromEmpty == ""sv);
// The view need not outlive the call.
json::Value copied;
{
std::string const owner{"borrowed"};
copied = json::Value{std::string_view{owner}};
}
EXPECT_EQ(copied.asString(), "borrowed");
// A reader of a string value stops at the first NUL, so a view carrying one reads back short.
json::Value const embedded{"a\0b"sv};
EXPECT_EQ(embedded.asString(), "a");
EXPECT_TRUE(embedded != "a\0b"sv);
EXPECT_TRUE(embedded == "a"sv);
EXPECT_TRUE(embedded == json::Value{"a\0b"sv});
}
TEST(JsonValue, compare_string_view)
{
// Comparing against a view builds no temporary Value.
using namespace std::string_view_literals;
EXPECT_TRUE(json::Value{"2.0"} == "2.0"sv);
EXPECT_FALSE(json::Value{"2.0"} != "2.0"sv);
EXPECT_TRUE(json::Value{"2.0"} != "2.00"sv);
EXPECT_TRUE(json::Value{"2.0"} != ""sv);
// A view need not be NUL-terminated, and the comparison must not read past its end.
EXPECT_TRUE(json::Value{"2.0"} == "2.0 and more"sv.substr(0, 3));
EXPECT_TRUE(json::Value{""} == ""sv);
// A string value holding a null pointer equals nothing, not even the empty view.
EXPECT_TRUE(json::Value{json::ValueType::String} != ""sv);
EXPECT_TRUE(json::Value{json::ValueType::String} != "2.0"sv);
EXPECT_TRUE(json::Value{json::ValueType::String} != json::Value{""});
// Nothing that is not a string equals one. Null is what an absent member reads as.
EXPECT_TRUE(json::Value{json::ValueType::Null} != ""sv);
EXPECT_TRUE(json::Value{json::ValueType::Null} != "2.0"sv);
EXPECT_TRUE(json::Value{2} != "2"sv);
EXPECT_TRUE(json::Value{true} != "true"sv);
EXPECT_TRUE(json::Value{json::ValueType::Object} != ""sv);
// Only a view reaches this overload; every other spelling compares through Value.
static constexpr json::StaticString kVersion{"2.0"};
EXPECT_TRUE(json::Value{"2.0"} == kVersion);
EXPECT_TRUE(json::Value{"2.1"} != kVersion);
EXPECT_TRUE(json::Value{} != kVersion);
EXPECT_TRUE(json::Value{"2.0"} == "2.0");
EXPECT_TRUE(json::Value{"2.0"} != "2.1");
EXPECT_TRUE(json::Value{} != "2.0");
EXPECT_TRUE(json::Value{"2.0"} == std::string{"2.0"});
EXPECT_TRUE(json::Value{"2.0"} != std::string{"2.1"});
EXPECT_TRUE(json::Value{json::ValueType::Null} != std::string{"2.0"});
// A literal `0` also converts to a view through `char const*`, so a plain overload would make
// this comparison ambiguous. It must compare as a number: RPCCall.cpp rejects a
// `transaction_entry` ledger index this way.
EXPECT_TRUE(json::Value{0} == 0);
EXPECT_TRUE(json::Value{4294967295u} != 0);
EXPECT_TRUE(json::Value{""} != 0);
EXPECT_TRUE(json::Value{2} != 0);
// A null and an Int are different types, so the comparison is false.
EXPECT_TRUE(json::Value{} != 0);
}
TEST(JsonValue, bool)
{
EXPECT_FALSE(json::Value());
+73
View File
@@ -0,0 +1,73 @@
#include <xrpl/protocol/ErrorCodes.h>
#include <gtest/gtest.h>
#include <set>
#include <type_traits>
using namespace xrpl;
// A row cannot be written without a status, so no row can default to 200 again.
static_assert(!std::is_constructible_v<rpc::ErrorInfo, ErrorCodeI, char const*, char const*>);
namespace {
/**
* The numbers between `RpcBadSyntax` and `RpcLast` that have no row in the
* error table: those no enumerator uses, and `RpcReportingUnsupported` (91),
* the one enumerator the table does not list.
*/
std::set<int> const kGaps{
5, 8, 20, 24, 25, 26, 27, 28, 34, 38, 39, 54, 55, 56,
59, 60, 61, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91,
};
} // namespace
TEST(ErrorCodes, table_gaps_report_the_unknown_placeholder)
{
// Codes absent from the table report the placeholder, so a scan by code must skip them.
auto const& gap = rpc::getErrorInfo(static_cast<ErrorCodeI>(5));
EXPECT_EQ(gap.code, RpcUnknown);
EXPECT_STREQ(gap.token.cStr(), "unknown");
auto const& real = rpc::getErrorInfo(RpcInvalidParams);
EXPECT_EQ(real.code, RpcInvalidParams);
EXPECT_STREQ(real.token.cStr(), "invalidParams");
}
TEST(ErrorCodes, exactly_the_listed_codes_have_no_row)
{
// The gap set is written out by hand, so an enumerator added without a row fails here, where a
// test asking the table which codes it knows would take the missing row for a gap.
for (int code = RpcBadSyntax; code <= RpcLast; ++code)
{
bool const isGap = rpc::getErrorInfo(static_cast<ErrorCodeI>(code)).code == RpcUnknown;
EXPECT_EQ(isGap, kGaps.contains(code)) << "code " << code;
}
}
TEST(ErrorCodes, every_code_names_an_http_status)
{
// Written out here rather than read from the row: a test reading the row production reads
// cannot tell whether the row is right.
EXPECT_EQ(rpc::errorCodeHttpStatus(RpcActMalformed), 400);
EXPECT_EQ(rpc::errorCodeHttpStatus(RpcActNotFound), 404);
EXPECT_EQ(rpc::errorCodeHttpStatus(RpcAlreadyMultisig), 400);
EXPECT_EQ(rpc::errorCodeHttpStatus(RpcAlreadySingleSig), 400);
// Every row names a status of its own. A code with no row reports the placeholder's, which the
// default constructor fixes at 200.
for (int code = RpcBadSyntax; code <= RpcLast; ++code)
{
auto const status = rpc::errorCodeHttpStatus(static_cast<ErrorCodeI>(code));
if (kGaps.contains(code))
{
EXPECT_EQ(status, 200) << "code " << code;
}
else
{
EXPECT_NE(status, 200) << "code " << code;
}
}
}
@@ -1,598 +0,0 @@
#include <xrpl/basics/Number.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/helpers/VaultHelpers.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/UintTypes.h>
#include <gtest/gtest.h>
#include <helpers/Account.h>
#include <protocol/VaultTestHelpers.h>
#include <cstdint>
#include <memory>
#include <string_view>
#include <vector>
namespace xrpl {
namespace {
beast::Journal const kNullJournal{beast::Journal::getNullSink()};
// Snapshots every field adjustVaultBalances can touch, for atomicity checks.
struct BalanceSnapshot
{
Number available;
Number assetsDeployed;
Number loss;
Number yield;
Number total;
static BalanceSnapshot
of(SLE::ConstRef vault)
{
return {
.available = vault->at(sfAssetsAvailable),
.assetsDeployed = vault->at(sfAssetsDeployed),
.loss = vault->at(sfLossUnrealized),
.yield = vault->at(sfYieldUnrealized),
.total = vault->at(sfAssetsTotal),
};
}
bool
operator==(BalanceSnapshot const&) const = default;
};
class VaultBalance : public ::testing::Test
{
protected:
test::Account const issuer_{"issuer"};
Issue const iou_{toCurrency("USD"), issuer_.id()};
// FixedPrecision IOU vault; stored is the (derived, hence possibly
// stale) sfAssetsTotal cache.
[[nodiscard]] std::shared_ptr<SLE>
iouVault(
Number const& assetsDeployed,
Number const& available,
Number const& stored = Number{0},
std::uint8_t scale = 6) const
{
return makeVault(
iou_, stored, VaultVersion::FixedPrecision, scale, assetsDeployed, available);
}
// AssetsDeployed 500, available 100.
[[nodiscard]] std::shared_ptr<SLE>
standardVault() const
{
return iouVault(Number{500}, Number{100});
}
[[nodiscard]] STAmount
iouAmount(Number const& n) const
{
return STAmount{iou_, n};
}
// Zero cash change: syncs the cached sfAssetsTotal.
static void
syncTotal(std::shared_ptr<SLE> const& vault)
{
ASSERT_EQ(adjustVaultBalances(vault, {}, kNullJournal), tesSUCCESS);
}
// Above coarsening the cached sfAssetsTotal (Downward, 16 digits) is a
// floor of the exact total: never above it, and within one unit of its
// own live exponent.
void
expectCacheFloorsExactTotal(std::shared_ptr<SLE> const& vault) const
{
Number const exact = getAssetsTotal(vault);
Number const cached = vault->at(sfAssetsTotal);
EXPECT_LE(cached, exact);
int const liveExponent = scale(cached, iou_);
EXPECT_LE(exact - cached, (Number{1, liveExponent}));
}
// Adjusts asset's integral vault by +25 and by an overdraft.
static void
checkIntegralControl(Asset const& asset)
{
auto const vault =
makeVault(asset, Number{0}, VaultVersion::FixedPrecision, 0, Number{500}, Number{100});
ASSERT_EQ(
adjustVaultBalances(vault, {.cash = STAmount{asset, Number{25}}}, kNullJournal),
tesSUCCESS);
EXPECT_EQ(vault->at(sfAssetsAvailable), Number{125});
EXPECT_EQ(vault->at(sfAssetsTotal), getAssetsTotal(vault));
EXPECT_EQ(
adjustVaultBalances(vault, {.cash = STAmount{asset, Number{-1'000}}}, kNullJournal),
tefBAD_LEDGER);
}
};
// ---------------------------------------------------------------------------
// getAssetsTotal
// ---------------------------------------------------------------------------
TEST_F(VaultBalance, get_assets_total_stored_for_non_fixed_precision)
{
struct Row
{
std::string_view name;
VaultVersion version = VaultVersion::Legacy;
std::int64_t stored = 0;
std::int64_t available = 0;
};
std::vector<Row> const rows{
{.name = "Legacy", .version = VaultVersion::Legacy, .stored = 1'234'567, .available = 999},
{
.name = "CashBasis",
.version = VaultVersion::CashBasis,
.stored = 9'876'543,
.available = 100,
},
};
for (auto const& row : rows)
{
SCOPED_TRACE(row.name);
auto const vault =
makeVault(iou_, Number{row.stored}, row.version, 6, Number{0}, Number{row.available});
EXPECT_EQ(getAssetsTotal(vault), Number{row.stored});
EXPECT_FALSE(vault->isFieldPresent(sfAssetsDeployed));
}
}
TEST_F(VaultBalance, get_assets_total_fixed_precision_derived_sum)
{
struct Row
{
std::string_view name;
Number assetsDeployed;
Number available;
// The synced cache equals the exact total (not merely a floor of it).
bool cacheExact = false;
};
std::vector<Row> const rows{
{
.name = "ExactSum",
.assetsDeployed = Number{250'000},
.available = Number{1'000'000},
.cacheExact = true,
},
// available is already 16 significant digits; assetsDeployed pushes the exact
// sum to 17, past what a 16-digit STAmount represents exactly.
{
.name = "ExactPastSixteenDigits",
.assetsDeployed = Number{8, -6},
.available = Number{9'999'999'999'999'999, -6},
},
};
for (auto const& row : rows)
{
SCOPED_TRACE(row.name);
auto const vault = iouVault(row.assetsDeployed, row.available);
Number const exact = row.available + row.assetsDeployed;
// Exact sum: no rounding, no conversion to a 16-digit STAmount.
EXPECT_EQ(getAssetsTotal(vault), exact);
EXPECT_GE(getAssetsTotal(vault), vault->at(sfAssetsAvailable));
syncTotal(vault);
if (row.cacheExact)
{
EXPECT_EQ(vault->at(sfAssetsTotal), getAssetsTotal(vault));
}
expectCacheFloorsExactTotal(vault);
}
}
TEST_F(VaultBalance, get_assets_total_fixed_precision_integral_exact)
{
auto const vault = makeVault(
xrpIssue(), Number{0}, VaultVersion::FixedPrecision, 0, Number{250}, Number{1'000});
EXPECT_EQ(getAssetsTotal(vault), Number{1'250});
}
TEST_F(VaultBalance, get_assets_total_ignores_stale_cache)
{
Number const available{4'000'000};
Number const assetsDeployed{1'000'000};
auto const vault = iouVault(assetsDeployed, available, Number{99});
EXPECT_EQ(getAssetsTotal(vault), available + assetsDeployed);
EXPECT_NE(vault->at(sfAssetsTotal), getAssetsTotal(vault));
}
// The exact total, and the synced cache once Downward-rounded, must never
// fall below AssetsAvailable even when assetsDeployed coarsens the scale.
TEST_F(VaultBalance, get_assets_total_never_below_available)
{
Number const available{9'999'999'999'999'999, -6};
Number const assetsDeployed{5'000'000'000, -6};
auto const vault = iouVault(assetsDeployed, available);
EXPECT_GT(getVaultScale(vault), getVaultBaseScale(vault));
EXPECT_GE(getAssetsTotal(vault), available);
syncTotal(vault);
EXPECT_GE(Number(vault->at(sfAssetsTotal)), available);
}
// available and assetsDeployed are each at most 16 digits, so associateAsset leaves them
// alone. Their sum, 99999999999999995 at exponent -4, has a 17th digit (5)
// that would round up under ToNearest and carry through the run of nines,
// bumping the exponent. Downward truncation drops it without a bump.
TEST_F(VaultBalance, live_scale_downward_keeps_lower_exponent_at_carry_boundary)
{
auto vault = iouVault(Number{35, -4}, Number{9'999'999'999'999'996LL, -3});
int const downwardExponent = [&] {
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
return scale(getAssetsTotal(vault), iou_);
}();
int const toNearestExponent = [&] {
NumberRoundModeGuard const rg(Number::RoundingMode::ToNearest);
return scale(getAssetsTotal(vault), iou_);
}();
EXPECT_LT(downwardExponent, toNearestExponent);
EXPECT_EQ(getVaultScale(vault), downwardExponent);
syncTotal(vault);
EXPECT_EQ(scale(vault->at(sfAssetsTotal), iou_), downwardExponent);
EXPECT_EQ(getVaultScale(vault), scale(vault->at(sfAssetsTotal), iou_));
}
TEST_F(VaultBalance, scale_and_open_include_assets_deployed)
{
// Available alone stays at base scale; Available + AssetsDeployed coarsens.
Number const available{1'000'000'000};
Number const assetsDeployed{9'000'000'000};
auto const vault = iouVault(assetsDeployed, available, available);
EXPECT_EQ(getAssetsTotal(vault), available + assetsDeployed);
EXPECT_GT(getVaultScale(vault), getVaultBaseScale(vault));
// Open capacity uses the derived total, so assetsDeployed counts against the
// ceiling: with assetsDeployed filling most of Open, the inflow is rejected.
auto const nearOpen = iouVault(Number{1, 9}, Number{8, 9});
EXPECT_EQ(checkOptionalVaultInflow(nearOpen, iouAmount(Number{1})), tecLIMIT_EXCEEDED);
}
// ---------------------------------------------------------------------------
// adjustVaultBalances
// ---------------------------------------------------------------------------
TEST_F(VaultBalance, fixed_precision_adjust_balances)
{
Number const assetsDeployed{300};
auto const vault = iouVault(assetsDeployed, Number{100}, Number{99});
ASSERT_EQ(
adjustVaultBalances(vault, {.cash = iouAmount(Number{50})}, kNullJournal), tesSUCCESS);
EXPECT_EQ(vault->at(sfAssetsAvailable), Number{150});
EXPECT_EQ(vault->at(sfAssetsDeployed), assetsDeployed);
EXPECT_EQ(vault->at(sfAssetsTotal), getAssetsTotal(vault));
EXPECT_EQ(getAssetsTotal(vault), Number{450});
ASSERT_EQ(
adjustVaultBalances(vault, {.cash = iouAmount(Number{-20})}, kNullJournal), tesSUCCESS);
EXPECT_EQ(vault->at(sfAssetsAvailable), Number{130});
EXPECT_EQ(vault->at(sfAssetsDeployed), assetsDeployed);
EXPECT_EQ(vault->at(sfAssetsTotal), getAssetsTotal(vault));
EXPECT_EQ(getAssetsTotal(vault), Number{430});
// Clear both rails together, the way a final VaultWithdraw does.
ASSERT_EQ(
adjustVaultBalances(
vault,
{.cash = iouAmount(Number{-130}), .deployed = -Number(assetsDeployed)},
kNullJournal),
tesSUCCESS);
EXPECT_EQ(vault->at(sfAssetsAvailable), Number{0});
EXPECT_EQ(vault->at(sfAssetsDeployed), Number{0});
EXPECT_EQ(vault->at(sfAssetsTotal), Number{0});
EXPECT_EQ(getAssetsTotal(vault), Number{0});
}
TEST_F(VaultBalance, fixed_precision_add_above_coarsening_syncs)
{
Number const assetsDeployed{5, -6};
auto const vault = iouVault(assetsDeployed, Number{9'999'999'999'999'990, -6});
ASSERT_EQ(
adjustVaultBalances(vault, {.cash = iouAmount(Number{10, -6})}, kNullJournal), tesSUCCESS);
EXPECT_EQ(vault->at(sfAssetsDeployed), assetsDeployed);
expectCacheFloorsExactTotal(vault);
}
// For each (balance, delta), AssetsAvailable after adjustVaultBalances must
// equal the plain STAmount sum, the arithmetic the ledger uses for the real
// trust-line/MPT transfer.
TEST_F(VaultBalance, trust_line_arithmetic_mirror)
{
struct Row
{
std::string_view name;
Number balance;
Number delta;
};
std::vector<Row> const rows{
{.name = "Plain", .balance = Number{1'000'000}, .delta = Number{500'000}},
{
.name = "CreditCrossesPowerOfTenUp",
.balance = Number{9'999'999'999'999'999, -9},
.delta = Number{2, -9},
},
{
.name = "DebitCrossesPowerOfTenDown",
.balance = Number{1'000'000'000'000'000, -9},
.delta = Number{-1, -9},
},
{.name = "DebitPartial", .balance = Number{5'000'000}, .delta = Number{-3'000'000}},
{.name = "DebitToZero", .balance = Number{2'000'000}, .delta = Number{-2'000'000}},
{
.name = "CreditFinerThanBalanceGrid",
.balance = Number{1'873'013'129'122'272LL, -9},
.delta = Number{7, -10},
},
};
for (auto const& row : rows)
{
SCOPED_TRACE(row.name);
STAmount const balance{iou_, row.balance};
STAmount const delta{iou_, row.delta};
STAmount const expected{iou_, Number(balance) + Number(delta)};
auto const vault = iouVault(Number{0}, row.balance, Number{0}, 9);
ASSERT_EQ(adjustVaultBalances(vault, {.cash = delta}, kNullJournal), tesSUCCESS);
EXPECT_EQ(Number(vault->at(sfAssetsAvailable)), Number(expected));
}
}
// Each row starts from assetsDeployed 500, available 100.
TEST_F(VaultBalance, fields_move)
{
struct Row
{
std::string_view name;
std::int64_t cash = 0;
std::int64_t deployed = 0;
std::int64_t yield = 0;
std::int64_t loss = 0;
std::int64_t expectedAvailable = 100;
std::int64_t expectedAssetsDeployed = 500;
std::int64_t expectedYield = 0;
std::int64_t expectedLoss = 0;
};
std::vector<Row> const rows{
{.name = "CashAlone", .cash = 25, .expectedAvailable = 125},
{.name = "AssetsDeployedAlone", .deployed = -200, .expectedAssetsDeployed = 300},
{.name = "YieldAlone", .yield = 40, .expectedYield = 40},
{.name = "LossAlone", .loss = 150, .expectedLoss = 150},
{
.name = "AllTogether",
.cash = 40,
.deployed = -100,
.yield = 10,
.loss = 50,
.expectedAvailable = 140,
.expectedAssetsDeployed = 400,
.expectedYield = 10,
.expectedLoss = 50,
},
};
for (auto const& row : rows)
{
SCOPED_TRACE(row.name);
auto const vault = standardVault();
ASSERT_EQ(
adjustVaultBalances(
vault,
{.cash = iouAmount(Number{row.cash}),
.deployed = Number{row.deployed},
.yield = Number{row.yield},
.loss = Number{row.loss}},
kNullJournal),
tesSUCCESS);
EXPECT_EQ(vault->at(sfAssetsAvailable), Number{row.expectedAvailable});
EXPECT_EQ(vault->at(sfAssetsDeployed), Number{row.expectedAssetsDeployed});
EXPECT_EQ(vault->at(sfYieldUnrealized), Number{row.expectedYield});
EXPECT_EQ(vault->at(sfLossUnrealized), Number{row.expectedLoss});
EXPECT_EQ(vault->at(sfAssetsTotal), getAssetsTotal(vault));
}
}
// A failing field rejects the whole change as a unit, including any valid
// cash leg. Each row starts from assetsDeployed 500, available 100.
TEST_F(VaultBalance, invalid_change_fails_atomically)
{
struct Row
{
std::string_view name;
std::int64_t cash = 0;
std::int64_t deployed = 0;
std::int64_t loss = 0;
TER expected;
};
std::vector<Row> const rows{
{.name = "AssetsAvailableNegative", .cash = -200, .expected = tefBAD_LEDGER},
{.name = "AssetsDeployedNegative", .deployed = -600, .expected = tefBAD_LEDGER},
// The +50 credit is valid on its own; the deployed change is not.
{
.name = "AssetsDeployedNegativeWithValidCash",
.cash = 50,
.deployed = -600,
.expected = tefBAD_LEDGER,
},
// The -50 debit is valid on its own (available stays at 50); the
// loss exceeds AssetsDeployed.
{
.name = "LossExceedsAssetsDeployedWithValidCash",
.cash = -50,
.loss = 600,
.expected = tecLIMIT_EXCEEDED,
},
{.name = "LossUnrealizedNegative", .loss = -1, .expected = tefBAD_LEDGER},
{.name = "LossExceedsAssetsDeployed", .loss = 600, .expected = tecLIMIT_EXCEEDED},
};
for (auto const& row : rows)
{
SCOPED_TRACE(row.name);
auto const vault = standardVault();
BalanceSnapshot const before = BalanceSnapshot::of(vault);
EXPECT_EQ(
adjustVaultBalances(
vault,
{.cash = iouAmount(Number{row.cash}),
.deployed = Number{row.deployed},
.loss = Number{row.loss}},
kNullJournal),
row.expected);
EXPECT_EQ(BalanceSnapshot::of(vault), before);
}
}
TEST_F(VaultBalance, loss_at_assets_deployed_boundary_allowed)
{
auto const vault = standardVault();
EXPECT_EQ(adjustVaultBalances(vault, {.loss = Number{500}}, kNullJournal), tesSUCCESS);
EXPECT_EQ(vault->at(sfLossUnrealized), Number{500});
}
TEST_F(VaultBalance, negative_yield_unrealized_rejected)
{
auto const vault = standardVault();
vault->at(sfYieldUnrealized) = Number{5};
auto const before = BalanceSnapshot::of(vault);
EXPECT_EQ(adjustVaultBalances(vault, {.yield = Number{-20}}, kNullJournal), tefBAD_LEDGER);
EXPECT_EQ(BalanceSnapshot::of(vault), before);
}
TEST_F(VaultBalance, assets_total_equals_floor16_of_available_plus_assets_deployed)
{
Number const assetsDeployed{7, -9};
auto vault = iouVault(assetsDeployed, Number{9'999'999'999'999'999, -9}, Number{0}, 9);
syncTotal(vault);
Number const expected = [&] {
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
return Number(STAmount{iou_, Number(vault->at(sfAssetsAvailable)) + assetsDeployed});
}();
EXPECT_EQ(Number(vault->at(sfAssetsTotal)), expected);
}
// ---------------------------------------------------------------------------
// Integral assets
// ---------------------------------------------------------------------------
TEST_F(VaultBalance, xrp_control)
{
checkIntegralControl(xrpIssue());
}
TEST_F(VaultBalance, mpt_control)
{
checkIntegralControl(MPTIssue{makeMptID(1, issuer_.id())});
}
TEST_F(VaultBalance, credit_to_posterior_scale_ignores_ambient_rounding_mode)
{
// reference is on a fine grid (scale -10); raw needs a coarser atScale once
// reference + raw is floored, so flooredSum - reference needs more digits
// than fit in a 16-digit STAmount. creditToPosteriorScale must build that
// difference under its own roundingMode (Downward here), not whatever
// rounding mode happens to be ambient when it is called.
Number const reference{4'218'667'505'995'833LL, -10};
STAmount const raw{iou_, std::int64_t{4'004'426'170'440'612LL}, -8};
int const atScale = -7;
// The exact (pre-STAmount) Downward credit, computed independently, floors
// strictly below raw: part of raw is lost rounding reference + raw onto
// the coarser atScale grid.
Number const exactDownwardCredit = [&] {
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
Number const flooredSum =
roundToAsset(iou_, reference + Number(raw), atScale, Number::RoundingMode::Downward);
return Number(STAmount{iou_, flooredSum - reference});
}();
EXPECT_LT(exactDownwardCredit, Number(raw));
// Calling under an ambient ToNearest mode (the common case: no caller sets
// an explicit guard around this call) must still produce the Downward
// result, not a value rounded under the ambient mode.
NumberRoundModeGuard const ambient(Number::RoundingMode::ToNearest);
STAmount const credit = detail::creditToPosteriorScale(
iou_, reference, atScale, raw, Number::RoundingMode::Downward);
EXPECT_LT(Number(credit), Number(raw));
EXPECT_EQ(Number(credit), exactDownwardCredit);
}
// creditToPosteriorAvailableScale returns
// floor16(AssetsAvailable + raw) - AssetsAvailable, never finer than -Scale.
TEST_F(VaultBalance, credit_to_posterior_available_scale_integral_passes_through)
{
auto const vault = makeVault(xrpIssue(), Number{1'000}, VaultVersion::FixedPrecision);
STAmount const credit =
creditToPosteriorAvailableScale(vault, Number{250}, Number::RoundingMode::Downward);
EXPECT_EQ(credit, STAmount(xrpIssue(), 250));
}
TEST_F(VaultBalance, credit_to_posterior_available_scale_floors_to_base_grid)
{
// Scale 6: the base grid is 1e-6, and AssetsAvailable is small enough
// that the posterior grid is the base grid.
auto const vault = iouVault(Number{0}, Number{100});
STAmount const credit = creditToPosteriorAvailableScale(
vault, Number{12'345'678, -7}, Number::RoundingMode::Downward);
EXPECT_EQ(credit, iouAmount(Number{1'234'567, -6}));
}
TEST_F(VaultBalance, credit_to_posterior_available_scale_floors_the_sum_across_power_of_ten)
{
// 9999999999.999999 + 0.000011 crosses 10^10, where the 16-digit grid is
// 1e-5. Flooring the delta alone would give 0.00001; flooring the sum
// gives 10000000000.00001, so the credit is exactly 0.000011.
auto const vault = iouVault(Number{0}, Number{9'999'999'999'999'999, -6});
STAmount const credit =
creditToPosteriorAvailableScale(vault, Number{11, -6}, Number::RoundingMode::Downward);
EXPECT_EQ(credit, iouAmount(Number{11, -6}));
EXPECT_EQ(
STAmount(iou_, Number{9'999'999'999'999'999, -6} + Number(credit)),
iouAmount(Number{1'000'000'000'000'001, -5}));
}
TEST_F(VaultBalance, credit_to_posterior_available_scale_uses_the_exact_raw_amount)
{
// raw has 17 significant digits. Exact: 0.000009 + 12345678901.234561 =
// 12345678901.23457 on the 1e-5 grid, so the credit is
// 12345678901.234561, stored as 12345678901.23456. Rounding raw to 16
// digits first (12345678901.23456) would floor the sum to
// 12345678901.23456 and give a credit one unit lower, 12345678901.23455.
auto const vault = iouVault(Number{0}, Number{9, -6});
STAmount const credit = creditToPosteriorAvailableScale(
vault, Number{12'345'678'901'234'561, -6}, Number::RoundingMode::Downward);
EXPECT_EQ(credit, iouAmount(Number{1'234'567'890'123'456, -5}));
}
} // namespace
} // namespace xrpl
@@ -1,376 +0,0 @@
#include <xrpl/basics/Number.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/hash/uhash.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/ledger/helpers/VaultHelpers.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/Rules.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
#include <xrpl/protocol/STTakesAsset.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/UintTypes.h>
#include <gtest/gtest.h>
#include <helpers/Account.h>
#include <protocol/VaultTestHelpers.h>
#include <cstdint>
#include <memory>
#include <optional>
#include <unordered_set>
namespace xrpl {
namespace {
TEST(VaultGrid, base_and_live_scale)
{
test::Account const issuer{"issuer"};
Issue const iou{toCurrency("USD"), issuer.id()};
auto const legacy = makeVault(iou, Number{1'000'000}, VaultVersion::Legacy, 6);
EXPECT_EQ(getVaultScale(legacy), -9);
EXPECT_EQ(getVaultBaseScale(legacy), getVaultScale(legacy));
auto const empty = makeVault(iou, Number{0}, VaultVersion::FixedPrecision, 6);
EXPECT_EQ(getVaultScale(empty), -6);
EXPECT_EQ(getVaultBaseScale(empty), -6);
auto const small = makeVault(iou, Number{1'000'000}, VaultVersion::FixedPrecision, 6);
EXPECT_EQ(getVaultScale(small), -6);
EXPECT_EQ(getVaultBaseScale(small), -6);
auto const coarsened = makeVault(iou, Number{10'000'000'000}, VaultVersion::FixedPrecision, 6);
EXPECT_GT(getVaultScale(coarsened), getVaultBaseScale(coarsened));
EXPECT_EQ(getVaultBaseScale(coarsened), -6);
}
TEST(VaultGrid, pre_v12_behavior_is_preserved)
{
test::Account const issuer{"issuer"};
Issue const iou{toCurrency("USD"), issuer.id()};
Number const assetsTotal{1'000'000};
STAmount const onGrid{iou, Number{2, -9}};
STAmount const dust{iou, Number{4, -10}};
STAmount const overOpen{iou, Number{10, 9}};
auto const downward = Number::RoundingMode::Downward;
auto const legacy = makeVault(iou, assetsTotal, VaultVersion::Legacy);
int const expectedLive = getVaultScale(legacy);
int const expectedPosterior = detail::getPosteriorVaultScale(legacy, onGrid);
STAmount const expectedPosteriorRound = roundToPosteriorVaultScale(legacy, dust, downward);
for (auto const version : {
std::optional<VaultVersion>{},
std::optional{VaultVersion::Legacy},
std::optional{VaultVersion::CashBasis},
})
{
auto const vault = makeVault(iou, assetsTotal, version);
EXPECT_EQ(getVaultScale(vault), expectedLive);
EXPECT_EQ(getVaultBaseScale(vault), expectedLive);
EXPECT_EQ(detail::getPosteriorVaultScale(vault, onGrid), expectedPosterior);
EXPECT_EQ(roundToPosteriorVaultScale(vault, dust, downward), expectedPosteriorRound);
EXPECT_EQ(checkOptionalVaultInflow(vault, overOpen), tesSUCCESS);
}
}
TEST(VaultGrid, pre_v12_credit_clamp_floors_posterior_total)
{
test::Account const issuer{"issuer"};
Issue const iou{toCurrency("USD"), issuer.id()};
Number const assetsTotal{9'999'999'999'999'999LL, -15};
STAmount const delta{iou, Number{5}};
STAmount const expected{iou, Number{4'999'999'999'999'991LL, -15}};
for (auto const version : {
std::optional<VaultVersion>{},
std::optional{VaultVersion::Legacy},
std::optional{VaultVersion::CashBasis},
})
{
auto const result = clampToAssetsTotalScale(makeVault(iou, assetsTotal, version), delta);
ASSERT_TRUE(result.has_value());
EXPECT_EQ(*result, expected);
}
}
TEST(VaultGrid, integral_scale_is_zero)
{
auto const vault = makeVault(xrpIssue(), Number{1'000}, VaultVersion::FixedPrecision, 0);
STAmount const delta{xrpIssue(), 7};
EXPECT_EQ(getVaultScale(vault), 0);
EXPECT_EQ(getVaultBaseScale(vault), 0);
EXPECT_EQ(detail::getPosteriorVaultScale(vault, delta), 0);
EXPECT_EQ(roundToPosteriorVaultScale(vault, delta, Number::RoundingMode::TowardsZero), delta);
}
TEST(VaultGrid, posterior_scale_rounds_delta)
{
test::Account const issuer{"issuer"};
Issue const iou{toCurrency("USD"), issuer.id()};
auto const vault =
makeVault(iou, Number{9'999'999'999'999'999, -6}, VaultVersion::FixedPrecision, 6);
STAmount const delta{iou, Number{21, -6}};
EXPECT_EQ(getVaultScale(vault), -6);
EXPECT_EQ(detail::getPosteriorVaultScale(vault, delta), -5);
EXPECT_EQ(
roundToPosteriorVaultScale(vault, delta, Number::RoundingMode::TowardsZero),
STAmount(iou, Number{20, -6}));
}
TEST(VaultGrid, posterior_scale_rejects_dust_at_call_site)
{
test::Account const issuer{"issuer"};
Issue const iou{toCurrency("USD"), issuer.id()};
auto const vault = makeVault(iou, Number{10'000'000'000}, VaultVersion::FixedPrecision, 6);
STAmount const dust{iou, Number{1, -6}};
EXPECT_EQ(
roundToPosteriorVaultScale(vault, dust, Number::RoundingMode::TowardsZero), beast::kZero);
}
TEST(VaultGrid, posterior_outflow_can_refine_scale)
{
test::Account const issuer{"issuer"};
Issue const iou{toCurrency("USD"), issuer.id()};
auto const vault =
makeVault(iou, Number{1'000'000'000'000'001, -5}, VaultVersion::FixedPrecision, 6);
STAmount const delta{iou, -Number{11, -6}};
EXPECT_EQ(getVaultScale(vault), -5);
EXPECT_EQ(detail::getPosteriorVaultScale(vault, delta), -6);
EXPECT_EQ(roundToPosteriorVaultScale(vault, delta, Number::RoundingMode::TowardsZero), delta);
}
TEST(VaultGrid, optional_inflow_capacity_boundaries)
{
test::Account const issuer{"issuer"};
Issue const iou{toCurrency("USD"), issuer.id()};
auto const fixedIou = makeVault(iou, Number{9, 5}, VaultVersion::FixedPrecision, 10);
STAmount const iouDust{iou, Number{1, -10}};
EXPECT_EQ(getVaultOpenLimit(fixedIou), (Number{9, 5}));
EXPECT_EQ(checkOptionalVaultInflow(fixedIou, STAmount{iou}), tesSUCCESS);
EXPECT_EQ(checkOptionalVaultInflow(fixedIou, iouDust), tecLIMIT_EXCEEDED);
auto const fixedXrp = makeVault(xrpIssue(), Number{9, 15}, VaultVersion::FixedPrecision, 0);
STAmount const xrpUnit{xrpIssue(), 1};
EXPECT_EQ(getVaultOpenLimit(fixedXrp), (Number{9, 15}));
EXPECT_EQ(checkOptionalVaultInflow(fixedXrp, STAmount{xrpIssue()}), tesSUCCESS);
EXPECT_EQ(checkOptionalVaultInflow(fixedXrp, xrpUnit), tecLIMIT_EXCEEDED);
auto const legacy = makeVault(iou, Number{10, 5}, VaultVersion::Legacy, 10);
EXPECT_EQ(checkOptionalVaultInflow(legacy, iouDust), tesSUCCESS);
auto const coarsening =
makeVault(iou, Number{9'999'999'999'999'999, -6}, VaultVersion::FixedPrecision, 6);
STAmount const coarseningDelta{iou, Number{21, -6}};
EXPECT_EQ(checkOptionalVaultInflow(coarsening, coarseningDelta), tecLIMIT_EXCEEDED);
}
TEST(VaultGrid, optional_inflow_includes_yield_unrealized)
{
test::Account const issuer{"issuer"};
Issue const iou{toCurrency("USD"), issuer.id()};
auto const vault = makeVault(iou, Number{8'999'999'999}, VaultVersion::FixedPrecision, 6);
STAmount const amount{iou, Number{1}};
EXPECT_EQ(checkOptionalVaultInflow(vault, amount), tesSUCCESS);
vault->at(sfYieldUnrealized) = Number{1};
associateAsset(*vault, iou);
EXPECT_EQ(checkOptionalVaultInflow(vault, amount), tecLIMIT_EXCEEDED);
}
// AssetsAvailable + raw needs 20 digits. Under the ToNearest ambient mode the
// sum would round up to the next 10^-6 before the Downward floor, crediting
// 0.000001 for a raw 0.0000009996.
TEST(VaultGrid, credit_floors_sum_in_requested_mode)
{
test::Account const issuer{"issuer"};
Issue const iou{toCurrency("USD"), issuer.id()};
auto const vault = makeVault(
iou,
Number{0},
VaultVersion::FixedPrecision,
6,
Number{0},
Number{1'234'567'890'123'456LL, -6});
Number const raw{9'996, -10};
NumberRoundModeGuard const ambient(Number::RoundingMode::ToNearest);
STAmount const credit =
creditToPosteriorAvailableScale(vault, raw, Number::RoundingMode::Downward);
EXPECT_EQ(credit, STAmount(iou, 0));
EXPECT_LE(Number(credit), raw);
}
TEST(VaultGrid, is_on_vault_base_grid)
{
test::Account const issuer{"issuer"};
Issue const iou{toCurrency("USD"), issuer.id()};
int const baseScale = vaultBaseScale(iou, 6);
struct Row
{
Number value;
bool expected{};
};
Row const rows[] = {
{.value = Number{0}, .expected = true},
{.value = Number{1}, .expected = true},
{.value = Number{1, -6}, .expected = true},
{.value = Number{1, 20}, .expected = true},
{.value = Number{1'234'567'890'123'456LL, -6}, .expected = true},
// A digit below baseScale.
{.value = Number{1, -7}, .expected = false},
{.value = Number{15, -7}, .expected = false},
// A 17th significant digit, on the base grid but not an STAmount.
{.value = Number{12'345'678'901'234'567LL, 0}, .expected = false},
};
for (auto const& row : rows)
EXPECT_EQ(isOnVaultBaseGrid(iou, row.value, baseScale), row.expected) << row.value;
EXPECT_EQ(vaultBaseScale(xrpIssue(), 6), 0);
EXPECT_TRUE(isOnVaultBaseGrid(xrpIssue(), Number{7}, 0));
// Out of range for the asset: rejected instead of throwing.
EXPECT_FALSE(isOnVaultBaseGrid(
xrpIssue(), Number{static_cast<std::int64_t>(STAmount::kMaxNativeN)} + 1, 0));
EXPECT_FALSE(isOnVaultBaseGrid(iou, Number{1, 100}, baseScale));
}
TEST(VaultGrid, clamp_vault_outflow)
{
test::Account const issuer{"issuer"};
Issue const iou{toCurrency("USD"), issuer.id()};
Asset const mpt{MPTIssue{makeMptID(1, issuer.id())}};
struct Row
{
char const* name = nullptr;
Asset asset;
Number available;
Number delta;
std::optional<Number> expected; // nullopt: tecPRECISION_LOSS
};
Row const rows[] = {
{
.name = "XRP outflow is returned unrounded",
.asset = xrpIssue(),
.available = Number{1'000},
.delta = Number{-7},
.expected = Number{7},
},
{
.name = "MPT outflow is returned unrounded",
.asset = mpt,
.available = Number{1'000},
.delta = Number{-7},
.expected = Number{7},
},
{
.name = "IOU outflow rounds towards zero at the posterior scale",
.asset = iou,
.available = Number{1'000'000},
.delta = Number{-15, -7},
.expected = Number{1, -6},
},
{
.name = "IOU outflow below one unit at the posterior scale is rejected",
.asset = iou,
.available = Number{1'000'000},
.delta = Number{-4, -7},
.expected = std::nullopt,
},
};
for (auto const& row : rows)
{
auto const vault = makeVault(
row.asset,
row.available,
VaultVersion::FixedPrecision,
row.asset.integral() ? 0 : 6,
Number{0},
row.available);
auto const result = clampVaultOutflow(vault, STAmount{row.asset, row.delta});
if (row.expected)
{
ASSERT_TRUE(result.has_value()) << row.name;
EXPECT_EQ(*result, (STAmount{row.asset, *row.expected})) << row.name;
}
else
{
ASSERT_FALSE(result.has_value()) << row.name;
EXPECT_EQ(result.error(), tecPRECISION_LOSS) << row.name;
}
}
}
TEST(VaultGrid, get_vault_default_version)
{
struct Row
{
char const* name = nullptr;
std::unordered_set<UInt256, beast::Uhash<>> enabled;
VaultVersion expected;
};
Row const rows[] = {
{.name = "no lending amendments: Legacy", .enabled = {}, .expected = VaultVersion::Legacy},
{
.name = "LendingProtocolV1_1: CashBasis",
.enabled = {featureLendingProtocolV1_1},
.expected = VaultVersion::CashBasis,
},
{
.name = "V1_1 and V1_2 without the cleanups: CashBasis",
.enabled = {featureLendingProtocolV1_1, featureLendingProtocolV1_2},
.expected = VaultVersion::CashBasis,
},
{
.name = "V1_1, V1_2 and fixCleanup3_2_0 without fixCleanup3_4_0: CashBasis",
.enabled = {featureLendingProtocolV1_1, featureLendingProtocolV1_2, fixCleanup3_2_0},
.expected = VaultVersion::CashBasis,
},
{
.name = "V1_1, V1_2 and fixCleanup3_4_0 without fixCleanup3_2_0: CashBasis",
.enabled = {featureLendingProtocolV1_1, featureLendingProtocolV1_2, fixCleanup3_4_0},
.expected = VaultVersion::CashBasis,
},
{
.name = "V1_2 and both cleanups: FixedPrecision",
.enabled = {featureLendingProtocolV1_2, fixCleanup3_2_0, fixCleanup3_4_0},
.expected = VaultVersion::FixedPrecision,
},
{
.name = "V1_1, V1_2 and both cleanups: FixedPrecision",
.enabled =
{
featureLendingProtocolV1_1,
featureLendingProtocolV1_2,
fixCleanup3_2_0,
fixCleanup3_4_0,
},
.expected = VaultVersion::FixedPrecision,
},
};
for (auto const& row : rows)
{
// Rules keeps a reference to the preset set; row.enabled outlives it.
Rules const rules{row.enabled};
EXPECT_EQ(getVaultDefaultVersion(rules), row.expected) << row.name;
}
}
} // namespace
} // namespace xrpl
@@ -1,65 +0,0 @@
#pragma once
#include <xrpl/basics/Number.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
// Needed for standalone compilation of sfScale/sfLEVersion assignment below;
// full-TU builds already see it transitively, so misc-include-cleaner flags
// it as unused there. Keep it so this header still compiles on its own.
#include <xrpl/protocol/STInteger.h> // NOLINT(misc-include-cleaner)
#include <xrpl/protocol/STIssue.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
#include <xrpl/protocol/STTakesAsset.h>
#include <cstdint>
#include <memory>
#include <optional>
#include <utility>
namespace xrpl {
// Shared by VaultGridTests, VaultBalanceTests and LoanBrokerCoverTests. For
// FixedPrecision, getAssetsTotal derives from AssetsAvailable plus
// AssetsDeployed rather than reading the stored AssetsTotal cache, so a
// FixedPrecision vault must have AssetsAvailable (and, if requested, a
// non-zero AssetsDeployed) seeded separately from assetsTotal. By default
// AssetsAvailable is assetsTotal - assetsDeployed, so the derived total
// equals assetsTotal; pass assetsAvailable explicitly to decouple it (e.g.
// to leave assetsTotal as a deliberately stale cache). Legacy/CashBasis
// vaults have no AssetsDeployed field and normally leave AssetsAvailable
// unset, but some cash-flow tests need it seeded too, so assetsAvailable is
// honored for those versions as well when given.
inline std::shared_ptr<SLE>
makeVault(
Asset const& asset,
Number const& assetsTotal,
std::optional<VaultVersion> version,
std::uint8_t scaleValue = kVaultDefaultIouScale,
Number const& assetsDeployed = Number{0},
std::optional<Number> assetsAvailable = std::nullopt)
{
auto vault = std::make_shared<SLE>(keylet::vault(uint256(1)));
vault->setFieldIssue(sfAsset, STIssue{sfAsset, asset});
vault->at(sfAssetsTotal) = assetsTotal;
if (!asset.integral())
vault->at(sfScale) = scaleValue;
if (version)
vault->at(sfLEVersion) = std::to_underlying(*version);
if (version == VaultVersion::FixedPrecision)
{
vault->at(sfAssetsAvailable) = assetsAvailable.value_or(assetsTotal - assetsDeployed);
vault->at(sfAssetsDeployed) = assetsDeployed;
}
else if (assetsAvailable)
{
vault->at(sfAssetsAvailable) = *assetsAvailable;
}
associateAsset(*vault, asset);
return vault;
}
} // namespace xrpl
@@ -32,8 +32,6 @@ TEST(VaultTests, BuilderSettersRoundTrip)
auto const assetsAvailableValue = canonical_NUMBER();
auto const assetsMaximumValue = canonical_NUMBER();
auto const lossUnrealizedValue = canonical_NUMBER();
auto const yieldUnrealizedValue = canonical_NUMBER();
auto const assetsDeployedValue = canonical_NUMBER();
auto const shareMPTIDValue = canonical_UINT192();
auto const withdrawalPolicyValue = canonical_UINT8();
auto const scaleValue = canonical_UINT8();
@@ -59,8 +57,6 @@ TEST(VaultTests, BuilderSettersRoundTrip)
builder.setAssetsAvailable(assetsAvailableValue);
builder.setAssetsMaximum(assetsMaximumValue);
builder.setLossUnrealized(lossUnrealizedValue);
builder.setYieldUnrealized(yieldUnrealizedValue);
builder.setAssetsDeployed(assetsDeployedValue);
builder.setScale(scaleValue);
builder.setLEVersion(lEVersionValue);
builder.setVaultKind(vaultKindValue);
@@ -170,22 +166,6 @@ TEST(VaultTests, BuilderSettersRoundTrip)
EXPECT_TRUE(entry.hasLossUnrealized());
}
{
auto const& expected = yieldUnrealizedValue;
auto const actualOpt = entry.getYieldUnrealized();
ASSERT_TRUE(actualOpt.has_value());
expectEqualField(expected, *actualOpt, "sfYieldUnrealized");
EXPECT_TRUE(entry.hasYieldUnrealized());
}
{
auto const& expected = assetsDeployedValue;
auto const actualOpt = entry.getAssetsDeployed();
ASSERT_TRUE(actualOpt.has_value());
expectEqualField(expected, *actualOpt, "sfAssetsDeployed");
EXPECT_TRUE(entry.hasAssetsDeployed());
}
{
auto const& expected = scaleValue;
auto const actualOpt = entry.getScale();
@@ -251,8 +231,6 @@ TEST(VaultTests, BuilderFromSleRoundTrip)
auto const assetsAvailableValue = canonical_NUMBER();
auto const assetsMaximumValue = canonical_NUMBER();
auto const lossUnrealizedValue = canonical_NUMBER();
auto const yieldUnrealizedValue = canonical_NUMBER();
auto const assetsDeployedValue = canonical_NUMBER();
auto const shareMPTIDValue = canonical_UINT192();
auto const withdrawalPolicyValue = canonical_UINT8();
auto const scaleValue = canonical_UINT8();
@@ -275,8 +253,6 @@ TEST(VaultTests, BuilderFromSleRoundTrip)
sle->at(sfAssetsAvailable) = assetsAvailableValue;
sle->at(sfAssetsMaximum) = assetsMaximumValue;
sle->at(sfLossUnrealized) = lossUnrealizedValue;
sle->at(sfYieldUnrealized) = yieldUnrealizedValue;
sle->at(sfAssetsDeployed) = assetsDeployedValue;
sle->at(sfShareMPTID) = shareMPTIDValue;
sle->at(sfWithdrawalPolicy) = withdrawalPolicyValue;
sle->at(sfScale) = scaleValue;
@@ -449,32 +425,6 @@ TEST(VaultTests, BuilderFromSleRoundTrip)
expectEqualField(expected, *fromBuilderOpt, "sfLossUnrealized");
}
{
auto const& expected = yieldUnrealizedValue;
auto const fromSleOpt = entryFromSle.getYieldUnrealized();
auto const fromBuilderOpt = entryFromBuilder.getYieldUnrealized();
ASSERT_TRUE(fromSleOpt.has_value());
ASSERT_TRUE(fromBuilderOpt.has_value());
expectEqualField(expected, *fromSleOpt, "sfYieldUnrealized");
expectEqualField(expected, *fromBuilderOpt, "sfYieldUnrealized");
}
{
auto const& expected = assetsDeployedValue;
auto const fromSleOpt = entryFromSle.getAssetsDeployed();
auto const fromBuilderOpt = entryFromBuilder.getAssetsDeployed();
ASSERT_TRUE(fromSleOpt.has_value());
ASSERT_TRUE(fromBuilderOpt.has_value());
expectEqualField(expected, *fromSleOpt, "sfAssetsDeployed");
expectEqualField(expected, *fromBuilderOpt, "sfAssetsDeployed");
}
{
auto const& expected = scaleValue;
@@ -620,10 +570,6 @@ TEST(VaultTests, OptionalFieldsReturnNullopt)
EXPECT_FALSE(entry.getAssetsMaximum().has_value());
EXPECT_FALSE(entry.hasLossUnrealized());
EXPECT_FALSE(entry.getLossUnrealized().has_value());
EXPECT_FALSE(entry.hasYieldUnrealized());
EXPECT_FALSE(entry.getYieldUnrealized().has_value());
EXPECT_FALSE(entry.hasAssetsDeployed());
EXPECT_FALSE(entry.getAssetsDeployed().has_value());
EXPECT_FALSE(entry.hasScale());
EXPECT_FALSE(entry.getScale().has_value());
EXPECT_FALSE(entry.hasLEVersion());
+11 -5
View File
@@ -36,6 +36,7 @@
#include <xrpld/rpc/Role.h>
#include <xrpld/rpc/ServerHandler.h>
#include <xrpld/rpc/detail/Handler.h>
#include <xrpld/rpc/detail/MaskSecrets.h>
#include <xrpld/rpc/detail/PathRequestManager.h>
#include <xrpld/rpc/detail/Pathfinder.h>
#include <xrpld/shamap/NodeFamily.h>
@@ -1460,20 +1461,25 @@ ApplicationImp::setup(boost::program_options::variables_map const& cmdline)
//
// Execute start up rpc commands.
//
for (auto const& cmd : config_->section(Sections::kRpcStartup).lines())
auto const& startupCommands = config_->section(Sections::kRpcStartup).lines();
for (std::size_t i = 0; i < startupCommands.size(); ++i)
{
json::Reader jrReader;
json::Value jvCommand;
if (!jrReader.parse(cmd, jvCommand))
// A line that does not parse has no field to mask, so it is reported by position and
// parser message only, and no command runs for it.
if (!jrReader.parse(startupCommands[i], jvCommand))
{
JLOG(journal_.fatal())
<< "Couldn't parse entry in [" << Sections::kRpcStartup << "]: '" << cmd;
<< "Couldn't parse entry " << (i + 1) << " in [" << Sections::kRpcStartup
<< "]: " << jrReader.getFormattedErrorMessages();
continue;
}
if (!config_->quiet())
{
JLOG(journal_.fatal()) << "Startup RPC: " << jvCommand << std::endl;
JLOG(journal_.fatal()) << "Startup RPC: " << rpc::loggable(jvCommand) << std::endl;
}
resource::Charge loadType = resource::kFeeReferenceRpc;
@@ -1499,7 +1505,7 @@ ApplicationImp::setup(boost::program_options::variables_map const& cmdline)
if (!config_->quiet())
{
JLOG(journal_.fatal()) << "Result: " << jvResult << std::endl;
JLOG(journal_.fatal()) << "Result: " << rpc::loggable(jvResult) << std::endl;
}
}
+17 -37
View File
@@ -3884,37 +3884,32 @@ NetworkOPsImp::pubProposedAccountTransaction(
HashSet<InfoSub::pointer> notify;
int iProposed = 0;
std::vector<SubAccountHistoryInfo> accountHistoryNotify;
{
std::scoped_lock const sl(accountLock_);
if (subRTAccount_.empty())
return;
if (!subAccount_.empty() || !subRTAccount_.empty() || !subAccountHistory_.empty())
for (auto const& affectedAccount : tx->getMentionedAccounts())
{
for (auto const& affectedAccount : tx->getMentionedAccounts())
if (auto const simiIt = subRTAccount_.find(affectedAccount);
simiIt != subRTAccount_.end())
{
if (auto const simiIt = subRTAccount_.find(affectedAccount);
simiIt != subRTAccount_.end())
auto it = simiIt->second.begin();
while (it != simiIt->second.end())
{
auto it = simiIt->second.begin();
InfoSub::pointer const p = it->second.lock();
while (it != simiIt->second.end())
if (p)
{
InfoSub::pointer const p = it->second.lock();
if (p)
{
notify.insert(p);
++it;
++iProposed;
}
else
{
it = simiIt->second.erase(it);
}
notify.insert(p);
++it;
++iProposed;
}
else
{
it = simiIt->second.erase(it);
}
}
}
@@ -3923,10 +3918,10 @@ NetworkOPsImp::pubProposedAccountTransaction(
JLOG(journal_.trace()) << "pubProposedAccountTransaction: " << iProposed;
if (!notify.empty() || !accountHistoryNotify.empty())
if (!notify.empty())
{
// Create two different Json objects, for different API versions
MultiApiJson jvObj = transJson(tx, result, false, ledger, std::nullopt);
MultiApiJson const jvObj = transJson(tx, result, false, ledger, std::nullopt);
for (InfoSub::Ref isrListener : notify)
{
@@ -3934,21 +3929,6 @@ NetworkOPsImp::pubProposedAccountTransaction(
isrListener->getApiVersion(), //
[&](json::Value const& jv) { isrListener->send(jv, true); });
}
XRPL_ASSERT(
jvObj.isMember(jss::account_history_tx_stream) == MultiApiJson::IsMemberResult::None,
"xrpl::NetworkOPs::pubProposedAccountTransaction : "
"account_history_tx_stream not set");
for (auto& info : accountHistoryNotify)
{
auto const& index = info.index;
if (index->forwardTxIndex == 0 && !index->haveHistorical)
jvObj.set(jss::account_history_tx_first, true);
jvObj.set(jss::account_history_tx_index, index->forwardTxIndex++);
jvObj.visit(
info.sink->getApiVersion(), //
[&](json::Value const& jv) { info.sink->send(jv, true); });
}
}
}
+52 -1
View File
@@ -171,6 +171,18 @@ public:
void
onRequest(Session& session);
/**
* Receives one WebSocket frame.
*
* A frame that exceeds the request size limit, does not parse or is not
* an object is answered here with `jsonInvalid` and the frame's `size`;
* its body is never echoed, since an unparsed body has no fields to mask.
* Any other frame is posted to the job queue and answered from
* processSession.
*
* @param session The WebSocket session the frame arrived on.
* @param buffers The frame's bytes.
*/
void
onWSMessage(
std::shared_ptr<WSSession> session,
@@ -183,15 +195,54 @@ public:
onStopped(Server&);
private:
/**
* Serves one parsed WebSocket request.
*
* Closes the connection when its resource balance is past the drop
* threshold. Otherwise checks the API version and the `command` and
* `method` fields, dispatches through rpc::doCommand, charges the
* session's consumer, and shapes the reply as a `response`, echoing the
* masked request on an error.
*
* @param session The session the request arrived on.
* @param coro The coroutine the request runs on.
* @param jv The parsed request.
* @return The reply to send.
*/
json::Value
processSession(
std::shared_ptr<WSSession> const& session,
std::shared_ptr<JobQueue::Coro> const& coro,
json::Value const& jv);
/**
* Serves one HTTP request on a coroutine: hands the body, the client
* address and the forwarding headers to processRequest, then completes
* or closes the session as its keep-alive header asks.
*
* @param session The HTTP session the request arrived on.
* @param coro The coroutine the request runs on.
*/
void
processSession(std::shared_ptr<Session> const&, std::shared_ptr<JobQueue::Coro> coro);
processSession(std::shared_ptr<Session> const& session, std::shared_ptr<JobQueue::Coro> coro);
/**
* Serves one HTTP body: parses it, answers a malformed or unauthorized
* request with a plain-text status, serves a `method: "batch"` body entry
* by entry, dispatches each request through rpc::doCommand, and writes
* the reply with its HTTP status. An error reply echoes the masked
* request.
*
* @param port The port the request arrived on, for its role and limits.
* @param request The raw body.
* @param remoteIPAddress The client address, for resource accounting and
* the role.
* @param output Where the reply bytes are written.
* @param coro The coroutine the request runs on.
* @param forwardedFor The `X-Forwarded-For` header, when the port trusts
* a proxy.
* @param user The `X-User` header.
*/
void
processRequest(
Port const& port,
+88
View File
@@ -0,0 +1,88 @@
#include <xrpld/rpc/detail/MaskSecrets.h>
#include <xrpl/json/json_value.h>
#include <xrpl/json/to_string.h>
#include <algorithm>
#include <string>
#include <string_view>
namespace xrpl::rpc {
namespace {
/**
* Reports whether @p name is one of kCredentialFields.
*
* An array element's memberName is empty, which no credential is spelled as,
* so an element is never a credential itself and is descended into.
*
* @param name The member name to look up.
* @return True if @p name is a credential-bearing field.
*/
bool
isCredential(std::string_view name)
{
return !name.empty() && std::ranges::find(kCredentialFields, name) != kCredentialFields.end();
}
/**
* Replaces every credential-bearing field of @p value in place, at every depth.
*
* @param value The object or array to mask. Any other value is left alone.
*/
void
maskInPlace(json::Value& value)
{
if (!value.isObject() && !value.isArray())
return;
for (auto it = value.begin(); it != value.end(); ++it)
{
// A credential is replaced whatever it holds, so it is not descended into.
if (isCredential(it.memberName()))
{
*it = json::Value{"<masked>"};
}
else
{
maskInPlace(*it);
}
}
}
} // namespace
bool
hasSecret(json::Value const& value)
{
if (!value.isObject() && !value.isArray())
return false;
for (auto it = value.begin(); it != value.end(); ++it)
{
if (isCredential(it.memberName()) || hasSecret(*it))
return true;
}
return false;
}
json::Value
maskSecrets(json::Value const& request)
{
auto masked = request;
maskInPlace(masked);
return masked;
}
std::string
loggable(json::Value const& value)
{
// The copy the mask takes is paid only when there is a credential to replace.
auto text = hasSecret(value) ? to_string(maskSecrets(value)) : to_string(value);
if (text.size() > kMaxLoggedChars)
text.resize(kMaxLoggedChars);
return text;
}
} // namespace xrpl::rpc
+99
View File
@@ -0,0 +1,99 @@
#pragma once
#include <xrpl/json/json_value.h>
#include <array>
#include <cstddef>
#include <string>
#include <string_view>
namespace xrpl::rpc {
/**
* Fields that carry a credential, in a request or in a reply.
*
* Literals rather than jss entries, since not every one has a jss entry.
*
* `username` and `password` are `subscribe`'s deprecated spellings of
* `url_username` and `url_password`, which is why a name identifying rather
* than authenticating is on the list. The `master_` and `validation_` names
* come from `wallet_propose` and `validation_create` replies, and a reply is
* logged like a request. `master_key` is also the public key that
* `validator_info` and the `manifests` and `validations` streams report, and
* it is masked with the rest.
*/
inline constexpr std::array<std::string_view, 16> kCredentialFields{
"admin_password",
"admin_user",
"master_key",
"master_seed",
"master_seed_hex",
"passphrase",
"password",
"secret",
"seed",
"seed_hex",
"url_password",
"url_username",
"username",
"validation_key",
"validation_private_key",
"validation_seed",
};
/**
* How much of a request or a reply a log line carries.
*
* A client chooses the size of what it sends, so nothing it cannot choose
* bounds the length.
*/
inline constexpr std::size_t kMaxLoggedChars = 10000;
/**
* Returns a copy of @p request with every credential-bearing field replaced by
* a placeholder.
*
* Apply to every request echoed back to a caller. Masks at every depth, since
* the JSON-RPC transport nests a credential inside `params` and a batch nests
* one request per entry. Recursion is bounded by json::Reader::kNestLimit.
*
* @param request The request to mask.
* @return The masked copy.
*/
[[nodiscard]] json::Value
maskSecrets(json::Value const& request);
/**
* Reports whether @p value carries a credential-bearing field at any depth.
*
* Walks objects and arrays without copying and stops at the first field named
* in kCredentialFields, so a caller pays for masking only when there is
* something to mask. A value with no members carries none.
*
* @param value The request or reply to inspect.
* @return True if a credential-bearing field is present at any depth.
*/
[[nodiscard]] bool
hasSecret(json::Value const& value);
/**
* Renders @p value for a log line: masked when it carries a credential, then
* truncated to kMaxLoggedChars.
*
* Every site that writes a request or a reply body to the log goes through
* this. A value with no credential is serialized as it is, so the copy the
* mask takes is paid only when hasSecret() reports one.
*
* Call it inside the JLOG argument and never before the macro: that argument is
* evaluated only when the sink is active, so hoisting the call puts the walk
* and the serialization on every request.
*
* Truncation is by bytes, so a multi-byte character can be split.
*
* @param value The request or reply to render.
* @return The masked, truncated text.
*/
[[nodiscard]] std::string
loggable(json::Value const& value);
} // namespace xrpl::rpc
+82 -17
View File
@@ -3,6 +3,7 @@
#include <xrpld/core/Config.h>
#include <xrpld/rpc/MethodNames.h>
#include <xrpld/rpc/ServerHandler.h>
#include <xrpld/rpc/detail/MaskSecrets.h>
#include <xrpl/basics/ByteUtilities.h>
#include <xrpl/basics/Log.h>
@@ -23,6 +24,7 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/ApiVersion.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/JsonRpc.h>
#include <xrpl/protocol/KeyType.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/RPCErr.h>
@@ -614,21 +616,32 @@ private:
return rpcError(RpcInvalidParams);
}
// json <command> <json>
/**
* Parses the `json <command> <json>` form: the second argument is parsed
* as the request and the first becomes its `method`. The request is
* logged masked once parsed; the unparsed text never is.
*
* @param jvParams The command name and the JSON text.
* @return The request, or `invalidParams` when the text does not parse
* to an object.
*/
json::Value
parseJson(json::Value const& jvParams)
{
json::Reader reader;
json::Value jvRequest;
// The command name cannot carry a credential, so it logs whether the JSON parses or not.
JLOG(j_.trace()) << "RPC method: " << jvParams[0u];
JLOG(j_.trace()) << "RPC json: " << jvParams[1u];
if (reader.parse(jvParams[1u].asString(), jvRequest))
{
if (!jvRequest.isObjectOrNull())
return rpcError(RpcInvalidParams);
// Logged only once parsed, so a signing secret among the members can be masked.
JLOG(j_.trace()) << "RPC json: " << rpc::loggable(jvRequest);
jvRequest[jss::method] = jvParams[0u];
return jvRequest;
@@ -637,6 +650,16 @@ private:
return rpcError(RpcInvalidParams);
}
/**
* Reports whether @p jv is a request this client accepts as JSON-RPC 2.0:
* an object whose `jsonrpc` is rpc::kJsonRpcVersion and `ripplerpc` is
* rpc::kRippleRpcVersion2, carrying an `id` and a `method`, with `params`
* absent, null, an array or an object; or a non-empty array of such
* objects.
*
* @param jv The parsed argument.
* @return True when the shape is accepted.
*/
bool
isValidJson2(json::Value const& jv)
{
@@ -651,8 +674,8 @@ private:
}
if (jv.isObject())
{
if (jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0" &&
jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0" &&
if (jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == rpc::kJsonRpcVersion &&
jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == rpc::kRippleRpcVersion2 &&
jv.isMember(jss::id) && jv.isMember(jss::method))
{
return !jv.isMember(jss::params) ||
@@ -1010,7 +1033,14 @@ private:
return jvRequest;
}
// ripple_path_find <json> [<ledger>]
/**
* Parses the `ripple_path_find <json> [<ledger>]` form: the first
* argument is parsed as the request and logged masked once parsed, the
* optional second names the ledger.
*
* @param jvParams The JSON text and, optionally, the ledger.
* @return The request, or `invalidParams` when the text does not parse.
*/
json::Value
parseRipplePathFind(json::Value const& jvParams)
{
@@ -1018,10 +1048,12 @@ private:
json::Value jvRequest{json::ValueType::Object};
bool const bLedger = 2 == jvParams.size();
JLOG(j_.trace()) << "RPC json: " << jvParams[0u];
if (reader.parse(jvParams[0u].asString(), jvRequest))
{
// The JSON is logged only once parsed, so a signing secret among the named members can
// be masked. Unparsed it is a bare string with no members to name.
JLOG(j_.trace()) << "RPC json: " << rpc::loggable(jvRequest);
if (bLedger)
{
jvParseLedger(jvRequest, jvParams[1u].asString());
@@ -1750,11 +1782,7 @@ public:
json::Value
parseCommand(std::string_view strMethod, json::Value const& jvParams, bool allowAnyCommand)
{
if (auto const stream = j_.trace())
{
stream << "Method: '" << strMethod << "'";
stream << "Params: " << jvParams;
}
JLOG(j_.trace()) << "Method: '" << strMethod << "'";
auto const found = std::ranges::lower_bound(kSortedCommands, strMethod, {}, &Command::name);
@@ -1861,15 +1889,26 @@ struct RPCCallImp
"process.");
}
// Parse reply
JLOG(j.debug()) << "RPC reply: " << strData << std::endl;
// Parse reply. A plain text body is a rejection, which carries no field a credential
// could sit in, so it is logged as it is, capped at kMaxLoggedChars.
if (strData.starts_with("Unable to parse request") ||
strData.starts_with(jss::invalid_API_version.cStr()))
{
JLOG(j.debug()) << "RPC reply: "
<< std::string_view{strData}.substr(0, rpc::kMaxLoggedChars);
Throw<RequestNotParsable>(strData);
}
json::Reader reader;
json::Value jvReply;
if (!reader.parse(strData, jvReply))
{
JLOG(j.debug()) << "RPC reply: "
<< std::string_view{strData}.substr(0, rpc::kMaxLoggedChars);
Throw<std::runtime_error>("couldn't parse reply from server");
}
// Logged once parsed, masked: `validation_create` and `wallet_propose` answer with a
// private key, which the fallback scrubber in Log.cpp does not know by every name.
JLOG(j.debug()) << "RPC reply: " << rpc::loggable(jvReply);
if (!jvReply)
Throw<std::runtime_error>("expected reply to have result, error and id properties");
@@ -1911,7 +1950,16 @@ commandLineMethodNames()
return RPCParser::methodNames();
}
// Used internally by rpcClient.
/**
* Translates command line arguments into the request the client sends.
*
* @param args The method name followed by its positional arguments.
* @param retParams Receives the arguments as `method` and `params`, the form
* `rpc` echoes on an error.
* @param apiVersion The `api_version` stamped on each request naming none.
* @param j The journal the built request is logged to, masked.
* @return The request, an array of requests, or the parser's error object.
*/
json::Value
rpcCmdToJson(
std::vector<std::string> const& args,
@@ -1951,12 +1999,27 @@ rpcCmdToJson(
std::for_each(jvRequest.begin(), jvRequest.end(), insertApiVersion);
}
JLOG(j.trace()) << "RPC Request: " << jvRequest << std::endl;
// `sign` and `submit` put the signing secret in the request the command line built.
JLOG(j.trace()) << "RPC Request: " << rpc::loggable(jvRequest) << std::endl;
return jvRequest;
}
//------------------------------------------------------------------------------
/**
* Runs one command line command against the configured server.
*
* Builds the request, adds the admin credentials from the config, sends it
* over HTTP and unwraps the result. On an error the output also carries the
* invocation as `rpc` and the request as `request_sent`, masked.
*
* @param args The method name followed by its arguments.
* @param config The client configuration naming the server and credentials.
* @param logs The log manager.
* @param apiVersion The `api_version` to request.
* @param headers Extra HTTP headers to send.
* @return The exit code and the output to print.
*/
std::pair<int, json::Value>
rpcClient(
std::vector<std::string> const& args,
@@ -2068,7 +2131,9 @@ rpcClient(
if (jvOutput.isMember(jss::error))
{
jvOutput["rpc"] = jvRpc; // How the command was seen as method + params.
jvOutput["request_sent"] = jvRequest; // How the command was translated.
// Carries the config's `admin_password` and prints to stdout, so it is masked.
// `jvRpc` above is positional, so masking by member name does not reach it.
jvOutput["request_sent"] = rpc::maskSecrets(jvRequest);
}
}
+2 -1
View File
@@ -6,6 +6,7 @@
#include <xrpld/rpc/Context.h>
#include <xrpld/rpc/Role.h>
#include <xrpld/rpc/detail/Handler.h>
#include <xrpld/rpc/detail/MaskSecrets.h>
#include <xrpld/rpc/detail/Tuning.h>
#include <xrpl/basics/Log.h>
@@ -136,7 +137,7 @@ fillHandler(JsonContext& context, Handler const*& result)
: context.params[jss::method].asString();
JLOG(context.j.trace()) << "COMMAND:" << strCommand;
JLOG(context.j.trace()) << "REQUEST:" << context.params;
JLOG(context.j.trace()) << "REQUEST:" << loggable(context.params);
auto handler = getHandler(context.apiVersion, context.app.config().betaRpcApi, strCommand);
if (handler == nullptr)
+2 -1
View File
@@ -1,6 +1,7 @@
#include <xrpld/rpc/RPCSub.h>
#include <xrpld/rpc/RPCCall.h>
#include <xrpld/rpc/detail/MaskSecrets.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/StringUtilities.h>
@@ -87,7 +88,7 @@ public:
std::scoped_lock const sl(lock_);
auto const jm = broadcast ? j_.debug() : j_.info();
JLOG(jm) << "rpc_call::fromNetwork push: " << jvObj;
JLOG(jm) << "rpc_call::fromNetwork push: " << rpc::loggable(jvObj);
deque_.emplace_back(seq_++, jvObj);
+86 -72
View File
@@ -4,6 +4,7 @@
#include <xrpld/overlay/Overlay.h>
#include <xrpld/rpc/RPCHandler.h>
#include <xrpld/rpc/Role.h>
#include <xrpld/rpc/detail/MaskSecrets.h>
#include <xrpld/rpc/detail/Tuning.h>
#include <xrpld/rpc/detail/WSInfoSub.h>
@@ -60,7 +61,9 @@
#include <algorithm>
#include <cctype>
#include <chrono>
#include <cstddef>
#include <exception>
#include <limits>
#include <map>
#include <memory>
#include <mutex>
@@ -340,10 +343,13 @@ ServerHandler::onWSMessage(
auto const size = boost::asio::buffer_size(buffers);
if (size > rpc::tuning::kMaxRequestSize || !json::Reader{}.parse(jv, buffers) || !jv.isObject())
{
// An unparsed body cannot be masked field-wise, so its size goes instead of its content.
// Clamped rather than narrowed: json has no integer wider than 32 bits.
json::Value jvResult(json::ValueType::Object);
jvResult[jss::type] = jss::error;
jvResult[jss::error] = "jsonInvalid";
jvResult[jss::value] = buffersToString(buffers);
jvResult[jss::size] =
json::UInt(std::min<std::size_t>(size, std::numeric_limits<json::UInt>::max()));
boost::beast::multi_buffer sb;
json::stream(jvResult, [&sb](auto const p, auto const n) {
sb.commit(boost::asio::buffer_copy(sb.prepare(n), boost::asio::buffer(p, n)));
@@ -354,7 +360,7 @@ ServerHandler::onWSMessage(
return;
}
JLOG(journal_.trace()) << "Websocket received '" << jv << "'";
JLOG(journal_.trace()) << "Websocket received '" << rpc::loggable(jv) << "'";
auto const postResult = jobQueue_.postCoro(
JtClientWebsocket,
@@ -392,22 +398,35 @@ ServerHandler::onStopped(Server&)
//------------------------------------------------------------------------------
/**
* Logs how long a request took.
*
* A slow request is reported at warn from one second and at error from ten,
* with the duration only. The request is logged at debug, since it is client
* text.
*
* @param request The request the duration belongs to.
* @param duration How long processing it took.
* @param journal Where the line is written.
*/
template <class T>
void
logDuration(json::Value const& request, T const& duration, beast::Journal& journal)
{
using namespace std::chrono_literals;
auto const level = [&] {
if (duration >= 10s)
return journal.error();
if (duration >= 1s)
return journal.warn();
return journal.debug();
}();
auto const micros = std::chrono::duration_cast<std::chrono::microseconds>(duration).count();
JLOG(level) << "RPC request processing duration = "
<< std::chrono::duration_cast<std::chrono::microseconds>(duration).count()
<< " microseconds. request = " << request;
if (duration >= 1s)
{
auto const slow = duration >= 10s ? journal.error() : journal.warn();
JLOG(slow) << "RPC request processing duration = " << micros << " microseconds.";
}
if (auto const stream = journal.debug())
{
stream << "RPC request processing duration = " << micros
<< " microseconds. request = " << rpc::loggable(request);
}
}
json::Value
@@ -442,7 +461,7 @@ ServerHandler::processSession(
jr[jss::status] = jss::error;
jr[jss::error] = apiVersion == rpc::kApiInvalidVersion ? jss::invalid_API_version
: jss::missingCommand;
jr[jss::request] = jv;
jr[jss::request] = rpc::maskSecrets(jv);
if (jv.isMember(jss::id))
jr[jss::id] = jv[jss::id];
if (jv.isMember(jss::jsonrpc))
@@ -498,11 +517,9 @@ ServerHandler::processSession(
}
catch (std::exception const& ex)
{
// LCOV_EXCL_START
jr[jss::result] = rpc::makeError(RpcInternal);
JLOG(journal_.error()) << "Exception while processing WS: " << ex.what() << "\n"
<< "Input JSON: " << json::Compact{json::Value{jv}};
// LCOV_EXCL_STOP
<< "Input JSON: " << rpc::loggable(jv);
}
is->getConsumer().charge(loadType);
@@ -519,21 +536,7 @@ ServerHandler::processSession(
jr = jr[jss::result];
jr[jss::status] = jss::error;
auto rq = jv;
if (rq.isObject())
{
if (rq.isMember(jss::passphrase.cStr()))
rq[jss::passphrase.cStr()] = "<masked>";
if (rq.isMember(jss::secret.cStr()))
rq[jss::secret.cStr()] = "<masked>";
if (rq.isMember(jss::seed.cStr()))
rq[jss::seed.cStr()] = "<masked>";
if (rq.isMember(jss::seed_hex.cStr()))
rq[jss::seed_hex.cStr()] = "<masked>";
}
jr[jss::request] = rq;
jr[jss::request] = rpc::maskSecrets(jv);
}
else
{
@@ -599,6 +602,31 @@ constexpr json::Int kServerOverloaded = -32604;
constexpr json::Int kForbidden = -32605;
constexpr json::Int kWrongVersion = -32606;
/**
* The HTTP status the `ripplerpc: "3.0"` envelope reports for @p code.
*
* The codes below answer 200, which is what a client calling `account_info`
* this way already reads. The list is closed: a code belongs on it only if it
* already answered a coded reply at this status.
*
* @param code The error code the reply reports.
* @return The HTTP status the reply is sent with.
*/
static int
legacyHttpStatus(ErrorCodeI code)
{
switch (code)
{
case RpcActMalformed:
case RpcActNotFound:
case RpcAlreadyMultisig:
case RpcAlreadySingleSig:
return 200;
default:
return rpc::errorCodeHttpStatus(code);
}
}
void
ServerHandler::processRequest(
Port const& port,
@@ -628,7 +656,9 @@ ServerHandler::processRequest(
bool batch = false;
unsigned size = 1;
if (jsonOrig.isMember(jss::method) && jsonOrig[jss::method] == "batch")
// Spelled as a view so the name is compared in place. A bare literal would reach the
// Value-to-Value comparison instead, which builds a Value from it, allocating per request.
if (jsonOrig.isMember(jss::method) && jsonOrig[jss::method] == std::string_view{"batch"})
{
batch = true;
if (!jsonOrig.isMember(jss::params) || !jsonOrig[jss::params].isArray())
@@ -648,7 +678,7 @@ ServerHandler::processRequest(
if (!jsonRPC.isObject())
{
json::Value r(json::ValueType::Object);
r[jss::request] = jsonRPC;
r[jss::request] = rpc::maskSecrets(jsonRPC);
r[jss::error] = makeJsonError(kMethodNotFound, "Method not found");
reply.append(r);
continue;
@@ -676,7 +706,7 @@ ServerHandler::processRequest(
return;
}
json::Value r(json::ValueType::Object);
r[jss::request] = jsonRPC;
r[jss::request] = rpc::maskSecrets(jsonRPC);
r[jss::error] = makeJsonError(kWrongVersion, jss::invalid_API_version.cStr());
reply.append(r);
continue;
@@ -718,7 +748,7 @@ ServerHandler::processRequest(
httpReply(503, "Server is overloaded", output, rpcJ);
return;
}
json::Value r = jsonRPC;
json::Value r = rpc::maskSecrets(jsonRPC);
r[jss::error] = makeJsonError(kServerOverloaded, "Server is overloaded");
reply.append(r);
continue;
@@ -733,7 +763,7 @@ ServerHandler::processRequest(
httpReply(403, "Forbidden", output, rpcJ);
return;
}
json::Value r = jsonRPC;
json::Value r = rpc::maskSecrets(jsonRPC);
r[jss::error] = makeJsonError(kForbidden, "Forbidden");
reply.append(r);
continue;
@@ -747,7 +777,7 @@ ServerHandler::processRequest(
httpReply(400, "Null method", output, rpcJ);
return;
}
json::Value r = jsonRPC;
json::Value r = rpc::maskSecrets(jsonRPC);
r[jss::error] = makeJsonError(kMethodNotFound, "Null method");
reply.append(r);
continue;
@@ -762,7 +792,7 @@ ServerHandler::processRequest(
httpReply(400, "method is not string", output, rpcJ);
return;
}
json::Value r = jsonRPC;
json::Value r = rpc::maskSecrets(jsonRPC);
r[jss::error] = makeJsonError(kMethodNotFound, "method is not string");
reply.append(r);
continue;
@@ -777,7 +807,7 @@ ServerHandler::processRequest(
httpReply(400, "method is empty", output, rpcJ);
return;
}
json::Value r = jsonRPC;
json::Value r = rpc::maskSecrets(jsonRPC);
r[jss::error] = makeJsonError(kMethodNotFound, "method is empty");
reply.append(r);
continue;
@@ -819,7 +849,7 @@ ServerHandler::processRequest(
params = jsonRPC;
}
std::string ripplerpc = "1.0";
std::string ripplerpc{rpc::kRippleRpcVersion1};
if (params.isMember(jss::ripplerpc))
{
if (!params[jss::ripplerpc].isString())
@@ -831,7 +861,7 @@ ServerHandler::processRequest(
return;
}
json::Value r = jsonRPC;
json::Value r = rpc::maskSecrets(jsonRPC);
r[jss::error] = makeJsonError(kMethodNotFound, "ripplerpc is not a string");
reply.append(r);
continue;
@@ -849,11 +879,11 @@ ServerHandler::processRequest(
user.remove_suffix(user.size());
}
JLOG(journal_.debug()) << "Query: " << strMethod << params;
JLOG(journal_.debug()) << "Query: " << strMethod << rpc::loggable(params);
// Provide the JSON-RPC method as the field "command" in the request.
params[jss::command] = strMethod;
JLOG(journal_.trace()) << "doRpcCommand:" << strMethod << ":" << params;
JLOG(journal_.trace()) << "doRpcCommand:" << strMethod << ":" << rpc::loggable(params);
resource::Charge loadType = resource::kFeeReferenceRpc;
@@ -883,12 +913,9 @@ ServerHandler::processRequest(
}
catch (std::exception const& ex)
{
// LCOV_EXCL_START
result = rpc::makeError(RpcInternal);
JLOG(journal_.error())
<< "Internal error : " << ex.what()
<< " when processing request: " << json::Compact{json::Value{params}};
// LCOV_EXCL_STOP
JLOG(journal_.error()) << "Internal error : " << ex.what()
<< " when processing request: " << rpc::loggable(params);
}
auto const end = std::chrono::system_clock::now();
@@ -900,7 +927,7 @@ ServerHandler::processRequest(
result[jss::warning] = jss::load;
json::Value r(json::ValueType::Object);
if (ripplerpc >= "2.0")
if (ripplerpc >= rpc::kRippleRpcVersion2)
{
if (result.isMember(jss::error))
{
@@ -924,22 +951,8 @@ ServerHandler::processRequest(
// received.
if (result.isMember(jss::error))
{
auto rq = params;
if (rq.isObject())
{ // But mask potentially sensitive information.
if (rq.isMember(jss::passphrase.cStr()))
rq[jss::passphrase.cStr()] = "<masked>";
if (rq.isMember(jss::secret.cStr()))
rq[jss::secret.cStr()] = "<masked>";
if (rq.isMember(jss::seed.cStr()))
rq[jss::seed.cStr()] = "<masked>";
if (rq.isMember(jss::seed_hex.cStr()))
rq[jss::seed_hex.cStr()] = "<masked>";
}
result[jss::status] = jss::error;
result[jss::request] = rq;
result[jss::request] = rpc::maskSecrets(params);
JLOG(journal_.debug())
<< "rpcError: " << result[jss::error] << ": " << result[jss::error_message];
@@ -982,14 +995,14 @@ ServerHandler::processRequest(
// This feature is enabled with ripplerpc version 3.0 and above.
// Before ripplerpc version 3.0 always return 200.
if (reply.isMember(jss::ripplerpc) && reply[jss::ripplerpc].isString() &&
reply[jss::ripplerpc].asString() >= "3.0")
reply[jss::ripplerpc].asString() >= rpc::kRippleRpcVersion3)
{
// If there's an error_code, use that to determine the HTTP Status.
if (reply.isMember(jss::error) && reply[jss::error].isMember(jss::error_code) &&
reply[jss::error][jss::error_code].isInt())
{
int const errCode = reply[jss::error][jss::error_code].asInt();
return rpc::errorCodeHttpStatus(static_cast<ErrorCodeI>(errCode));
return legacyHttpStatus(static_cast<ErrorCodeI>(errCode));
}
}
// Return OK.
@@ -1004,21 +1017,22 @@ ServerHandler::processRequest(
++rpcRequests_;
rpcSize_.notify(beast::insight::Event::value_type{response.size()});
response += '\n';
// The serialized reply is in hand, so it is logged as built; the masked copy and its second
// serialization are paid for only when a credential has to be replaced.
if (auto const stream = journal_.debug())
{
static int const kMaxSize = 10000;
if (response.size() <= kMaxSize)
if (rpc::hasSecret(reply))
{
stream << "Reply: " << response;
stream << "Reply: " << rpc::loggable(reply);
}
else
{
stream << "Reply: " << response.substr(0, kMaxSize);
stream << "Reply: " << std::string_view{response}.substr(0, rpc::kMaxLoggedChars);
}
}
response += '\n';
httpReply(httpStatus, response, output, rpcJ);
}
+6 -4
View File
@@ -8,6 +8,7 @@
#include <xrpld/rpc/Role.h>
#include <xrpld/rpc/detail/AssetCache.h>
#include <xrpld/rpc/detail/LegacyPathFind.h>
#include <xrpld/rpc/detail/MaskSecrets.h>
#include <xrpld/rpc/detail/Pathfinder.h>
#include <xrpld/rpc/detail/RPCHelpers.h>
#include <xrpld/rpc/detail/Tuning.h>
@@ -1018,7 +1019,8 @@ transactionSign(
// could change the signing prefix of an alternate signature field.
std::shared_ptr<ReadView const> const ledger = app.getOpenLedger().current();
auto const j = app.getJournal("RPCHandler");
JLOG(j.debug()) << "transactionSign: " << jvRequest;
// The next statement reads the signing secret out of this same request.
JLOG(j.debug()) << "transactionSign: " << loggable(jvRequest);
// Add and amend fields based on the transaction type.
SigningForParams signForParams;
@@ -1055,7 +1057,7 @@ transactionSubmit(
auto const& ledger = app.getOpenLedger().current();
auto const j = app.getJournal("RPCHandler");
JLOG(j.debug()) << "transactionSubmit: " << jvRequest;
JLOG(j.debug()) << "transactionSubmit: " << loggable(jvRequest);
// Add and amend fields based on the transaction type.
SigningForParams signForParams;
@@ -1177,7 +1179,7 @@ transactionSignFor(
{
auto const& ledger = app.getOpenLedger().current();
auto const j = app.getJournal("RPCHandler");
JLOG(j.debug()) << "transactionSignFor: " << jvRequest;
JLOG(j.debug()) << "transactionSignFor: " << loggable(jvRequest);
// Verify presence of the signer's account field.
char const accountField[] = "account";
@@ -1301,7 +1303,7 @@ transactionSubmitMultiSigned(
{
auto const& ledger = app.getOpenLedger().current();
auto const j = app.getJournal("RPCHandler");
JLOG(j.debug()) << "transactionSubmitMultiSigned: " << jvRequest;
JLOG(j.debug()) << "transactionSubmitMultiSigned: " << loggable(jvRequest);
// When multi-signing, the "Sequence" and "SigningPubKey" fields must
// be passed in by the caller.