Compare commits

..

7 Commits

Author SHA1 Message Date
Elliot Lee
beba87129e Set version to 1.12.0-b1 2023-06-26 14:24:00 -07:00
Shawn Xie
b7e902dccc XLS-39 Clawback: (#4553)
Introduces:
* AccountRoot flag: lsfAllowClawback
* New Clawback transaction
* More info on clawback spec: https://github.com/XRPLF/XRPL-Standards/tree/master/XLS-39d-clawback
2023-06-26 14:07:20 -07:00
Howard Hinnant
9eb30d4316 refactor: remove TypedField's move constructor (#4567)
Apply a minor cleanup in `TypedField`:
* Remove a non-working and unused move constructor.
* Constrain the remaining constructor to not be overly generic enough as
  to be used as a copy or move constructor.
2023-06-26 12:32:10 -07:00
John Freeman
8fdad0d7fd ci: use Artifactory remote in nix workflow (#4556)
There is now an Artifactory (thanks @shichengripple001 and team!) to
hold dependency binaries for the builds.

* Rewrite the `nix` workflow to use it and cut the time down to a mere
  21 minutes.
  * This workflow should continue to work (just more slowly) for forks
    that do not have access to the Artifactory.
2023-06-23 14:20:20 -07:00
drlongle
0b812cdece Add RPC/WS ports to server_info (#4427)
Enhance the /crawl endpoint by publishing WebSocket/RPC ports in the
server_info response. The function processing requests to the /crawl
endpoint actually calls server_info internally, so this change enables a
server to advertise its WebSocket/RPC port(s) to peers via the /crawl
endpoint. `grpc` and `peer` ports are included as well.

The new `ports` array contains objects, each containing a `port` for the
listening port (number string), and an array `protocol` listing the
supported protocol(s).

This allows crawlers to build a richer topology without needing to
port-scan nodes. For non-admin users (including peers), the info about
*admin* ports is excluded.

Also increase test coverage for RPC ServerInfo.

Fix #2837.
2023-06-23 10:19:26 -07:00
Scott Schurr
724a301599 fixReducedOffersV1: prevent offers from blocking order books: (#4512)
Curtail the occurrence of order books that are blocked by reduced offers
with the implementation of the fixReducedOffersV1 amendment.

This commit identifies three ways in which offers can be reduced:

1. A new offer can be partially crossed by existing offers, so the new
   offer is reduced when placed in the ledger.

2. An in-ledger offer can be partially crossed by a new offer in a
   transaction. So the in-ledger offer is reduced by the new offer.

3. An in-ledger offer may be under-funded. In this case the in-ledger
   offer is scaled down to match the available funds.

Reduced offers can block order books if the effective quality of the
reduced offer is worse than the quality of the original offer (from the
perspective of the taker). It turns out that, for small values, the
quality of the reduced offer can be significantly affected by the
rounding mode used during scaling computations.

This commit adjusts some rounding modes so that the quality of a reduced
offer is always at least as good (from the taker's perspective) as the
original offer.

The amendment is titled fixReducedOffersV1 because additional ways of
producing reduced offers may come to light. Therefore, there may be a
future need for a V2 amendment.
2023-06-22 22:20:25 -07:00
Ed Hennis
71d7d67fa3 Enable the Beta RPC API (v2) for all unit tests: (#4573)
* Enable api_version 2, which is currently in beta. It is expected to be
  marked stable by the next stable release.
* This does not change any defaults.
* The only existing tests changed were one that set the same flag, which
  was now redundant, and a couple that tested versioning explicitly.
2023-06-21 11:51:37 -07:00
40 changed files with 2475 additions and 53 deletions

View File

@@ -1,6 +1,25 @@
name: nix
on: [push, pull_request]
# This workflow has two job matrixes.
# They can be considered phases because the second matrix ("test")
# depends on the first ("dependencies").
#
# The first phase has a job in the matrix for each combination of
# variables that affects dependency ABI:
# platform, compiler, and configuration.
# It creates a GitHub artifact holding the Conan profile,
# and builds and caches binaries for all the dependencies.
# If an Artifactory remote is configured, they are cached there.
# If not, they are added to the GitHub artifact.
# GitHub's "cache" action has a size limit (10 GB) that is too small
# to hold the binaries if they are built locally.
# We must use the "{upload,download}-artifact" actions instead.
#
# The second phase has a job in the matrix for each test configuration.
# It installs dependency binaries from the cache, whichever was used,
# and builds and tests rippled.
jobs:
dependencies:
@@ -31,6 +50,8 @@ jobs:
env:
build_dir: .build
steps:
- name: checkout
uses: actions/checkout@v3
- name: check environment
run: |
echo ${PATH} | tr ':' '\n'
@@ -38,6 +59,8 @@ jobs:
cmake --version
env
- name: configure Conan
env:
CONAN_URL: http://18.143.149.228:8081/artifactory/api/conan/conan-non-prod
run: |
conan profile new default --detect
conan profile update settings.compiler.cppstd=20 default
@@ -47,19 +70,39 @@ jobs:
conan profile update env.CC=${{ matrix.profile.cc }} default
conan profile update env.CXX=${{ matrix.profile.cxx }} default
conan profile update conf.tools.build:compiler_executables='{"c": "${{ matrix.profile.cc }}", "cpp": "${{ matrix.profile.cxx }}"}' default
- name: checkout
uses: actions/checkout@v3
- name: dependencies
# Do not quote the URL. An empty string will be accepted (with
# a non-fatal warning), but a missing argument will not.
conan remote add ripple ${{ env.CONAN_URL }} --insert 0
- name: try to authenticate to ripple Conan remote
id: remote
run: |
echo outcome=$(conan user --remote ripple ${{ secrets.CONAN_USERNAME }} --password ${{ secrets.CONAN_TOKEN }} && echo success || echo failure) | tee ${GITHUB_OUTPUT}
- name: archive profile
# Create this archive before dependencies are added to the local cache.
run: tar -czf conan.tar -C ~/.conan .
- name: list missing binaries
id: binaries
# Print the list of dependencies that would need to be built locally.
# A non-empty list means we have "failed" to cache binaries remotely.
run: |
echo missing=$(conan info . --build missing --json 2>/dev/null | grep '^\[') | tee ${GITHUB_OUTPUT}
- name: build dependencies
if: (steps.binaries.outputs.missing != '[]')
uses: ./.github/actions/dependencies
with:
configuration: ${{ matrix.configuration }}
- name: archive cache
- name: upload dependencies to remote
if: (steps.binaries.outputs.missing != '[]') && (steps.remote.outputs.outcome == 'success')
run: conan upload --remote ripple '*' --all --parallel --confirm
- name: recreate archive with dependencies
if: (steps.binaries.outputs.missing != '[]') && (steps.remote.outputs.outcome == 'failure')
run: tar -czf conan.tar -C ~/.conan .
- name: upload cache
- name: upload archive
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.platform }}-${{ matrix.compiler }}-${{ matrix.configuration }}
path: conan.tar
if-no-files-found: error
test:

View File

@@ -427,6 +427,7 @@ target_sources (rippled PRIVATE
src/ripple/app/tx/impl/CancelOffer.cpp
src/ripple/app/tx/impl/CashCheck.cpp
src/ripple/app/tx/impl/Change.cpp
src/ripple/app/tx/impl/Clawback.cpp
src/ripple/app/tx/impl/CreateCheck.cpp
src/ripple/app/tx/impl/CreateOffer.cpp
src/ripple/app/tx/impl/CreateTicket.cpp
@@ -692,6 +693,7 @@ if (tests)
src/test/app/AccountTxPaging_test.cpp
src/test/app/AmendmentTable_test.cpp
src/test/app/Check_test.cpp
src/test/app/Clawback_test.cpp
src/test/app/CrossingLimits_test.cpp
src/test/app/DeliverMin_test.cpp
src/test/app/DepositAuth_test.cpp
@@ -721,6 +723,7 @@ if (tests)
src/test/app/PseudoTx_test.cpp
src/test/app/RCLCensorshipDetector_test.cpp
src/test/app/RCLValidations_test.cpp
src/test/app/ReducedOffer_test.cpp
src/test/app/Regression_test.cpp
src/test/app/SHAMapStore_test.cpp
src/test/app/SetAuth_test.cpp

View File

@@ -602,6 +602,13 @@ public:
return *m_networkOPs;
}
virtual ServerHandlerImp&
getServerHandler() override
{
assert(serverHandler_);
return *serverHandler_;
}
boost::asio::io_service&
getIOService() override
{

View File

@@ -89,6 +89,7 @@ class Overlay;
class PathRequests;
class PendingSaves;
class PublicKey;
class ServerHandlerImp;
class SecretKey;
class STLedgerEntry;
class TimeKeeper;
@@ -231,6 +232,8 @@ public:
getOPs() = 0;
virtual OrderBookDB&
getOrderBookDB() = 0;
virtual ServerHandlerImp&
getServerHandler() = 0;
virtual TransactionMaster&
getMasterTransaction() = 0;
virtual perf::PerfLog&

View File

@@ -429,7 +429,7 @@ GRPCServerImpl::GRPCServerImpl(Application& app)
// if present, get endpoint from config
if (app_.config().exists("port_grpc"))
{
Section section = app_.config().section("port_grpc");
const auto& section = app_.config().section("port_grpc");
auto const optIp = section.get("ip");
if (!optIp)

View File

@@ -65,9 +65,11 @@
#include <ripple/rpc/BookChanges.h>
#include <ripple/rpc/DeliveredAmount.h>
#include <ripple/rpc/impl/RPCHelpers.h>
#include <ripple/rpc/impl/ServerHandlerImp.h>
#include <boost/asio/ip/host_name.hpp>
#include <boost/asio/steady_timer.hpp>
#include <algorithm>
#include <mutex>
#include <string>
#include <tuple>
@@ -2661,6 +2663,51 @@ NetworkOPsImp::getServerInfo(bool human, bool admin, bool counters)
info["reporting"] = app_.getReportingETL().getInfo();
}
// This array must be sorted in increasing order.
static constexpr std::array<std::string_view, 7> protocols{
"http", "https", "peer", "ws", "ws2", "wss", "wss2"};
static_assert(std::is_sorted(std::begin(protocols), std::end(protocols)));
{
Json::Value ports{Json::arrayValue};
for (auto const& port : app_.getServerHandler().setup().ports)
{
// Don't publish admin ports for non-admin users
if (!admin &&
!(port.admin_nets_v4.empty() && port.admin_nets_v6.empty() &&
port.admin_user.empty() && port.admin_password.empty()))
continue;
std::vector<std::string> proto;
std::set_intersection(
std::begin(port.protocol),
std::end(port.protocol),
std::begin(protocols),
std::end(protocols),
std::back_inserter(proto));
if (!proto.empty())
{
auto& jv = ports.append(Json::Value(Json::objectValue));
jv[jss::port] = std::to_string(port.port);
jv[jss::protocol] = Json::Value{Json::arrayValue};
for (auto const& p : proto)
jv[jss::protocol].append(p);
}
}
if (app_.config().exists("port_grpc"))
{
auto const& grpcSection = app_.config().section("port_grpc");
auto const optPort = grpcSection.get("port");
if (optPort && grpcSection.get("ip"))
{
auto& jv = ports.append(Json::Value(Json::objectValue));
jv[jss::port] = *optPort;
jv[jss::protocol] = Json::Value{Json::arrayValue};
jv[jss::protocol].append("grpc");
}
}
info[jss::ports] = std::move(ports);
}
return info;
}

View File

@@ -531,14 +531,22 @@ limitStepOut(
TOut& ownerGives,
std::uint32_t transferRateIn,
std::uint32_t transferRateOut,
TOut const& limit)
TOut const& limit,
Rules const& rules)
{
if (limit < stpAmt.out)
{
stpAmt.out = limit;
ownerGives = mulRatio(
stpAmt.out, transferRateOut, QUALITY_ONE, /*roundUp*/ false);
ofrAmt = ofrQ.ceil_out(ofrAmt, stpAmt.out);
if (rules.enabled(fixReducedOffersV1))
// It turns out that the ceil_out implementation has some slop in
// it. ceil_out_strict removes that slop. But removing that slop
// affects transaction outcomes, so the change must be made using
// an amendment.
ofrAmt = ofrQ.ceil_out_strict(ofrAmt, stpAmt.out, /*roundUp*/ true);
else
ofrAmt = ofrQ.ceil_out(ofrAmt, stpAmt.out);
stpAmt.in =
mulRatio(ofrAmt.in, transferRateIn, QUALITY_ONE, /*roundUp*/ true);
}
@@ -577,6 +585,7 @@ BookStep<TIn, TOut, TDerived>::forEachOffer(
sb, afView, book_, sb.parentCloseTime(), counter, j_);
bool const flowCross = afView.rules().enabled(featureFlowCross);
bool const fixReduced = afView.rules().enabled(fixReducedOffersV1);
bool offerAttempted = false;
std::optional<Quality> ofrQ;
while (offers.step())
@@ -654,7 +663,16 @@ BookStep<TIn, TOut, TDerived>::forEachOffer(
ownerGives = funds;
stpAmt.out = mulRatio(
ownerGives, QUALITY_ONE, ofrOutRate, /*roundUp*/ false);
ofrAmt = ofrQ->ceil_out(ofrAmt, stpAmt.out);
// It turns out we can prevent order book blocking by (strictly)
// rounding down the ceil_out() result. This adjustment changes
// transaction outcomes, so it must be made under an amendment.
if (fixReduced)
ofrAmt = ofrQ->ceil_out_strict(
ofrAmt, stpAmt.out, /* roundUp */ false);
else
ofrAmt = ofrQ->ceil_out(ofrAmt, stpAmt.out);
stpAmt.in =
mulRatio(ofrAmt.in, ofrInRate, QUALITY_ONE, /*roundUp*/ true);
}
@@ -770,7 +788,8 @@ BookStep<TIn, TOut, TDerived>::revImp(
ownerGivesAdj,
transferRateIn,
transferRateOut,
remainingOut);
remainingOut,
afView.rules());
remainingOut = beast::zero;
savedIns.insert(stpAdjAmt.in);
savedOuts.insert(remainingOut);
@@ -922,7 +941,8 @@ BookStep<TIn, TOut, TDerived>::fwdImp(
ownerGivesAdjRev,
transferRateIn,
transferRateOut,
remainingOut);
remainingOut,
afView.rules());
if (stpAdjAmtRev.in == remainingIn)
{

View File

@@ -0,0 +1,138 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2023 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <ripple/app/tx/impl/Clawback.h>
#include <ripple/ledger/View.h>
#include <ripple/protocol/Feature.h>
#include <ripple/protocol/Indexes.h>
#include <ripple/protocol/Protocol.h>
#include <ripple/protocol/TxFlags.h>
#include <ripple/protocol/st.h>
namespace ripple {
NotTEC
Clawback::preflight(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureClawback))
return temDISABLED;
if (auto const ret = preflight1(ctx); !isTesSuccess(ret))
return ret;
if (ctx.tx.getFlags() & tfClawbackMask)
return temINVALID_FLAG;
AccountID const issuer = ctx.tx[sfAccount];
STAmount const clawAmount = ctx.tx[sfAmount];
// The issuer field is used for the token holder instead
AccountID const& holder = clawAmount.getIssuer();
if (issuer == holder || isXRP(clawAmount) || clawAmount <= beast::zero)
return temBAD_AMOUNT;
return preflight2(ctx);
}
TER
Clawback::preclaim(PreclaimContext const& ctx)
{
AccountID const issuer = ctx.tx[sfAccount];
STAmount const clawAmount = ctx.tx[sfAmount];
AccountID const& holder = clawAmount.getIssuer();
auto const sleIssuer = ctx.view.read(keylet::account(issuer));
auto const sleHolder = ctx.view.read(keylet::account(holder));
if (!sleIssuer || !sleHolder)
return terNO_ACCOUNT;
std::uint32_t const issuerFlagsIn = sleIssuer->getFieldU32(sfFlags);
// If AllowClawback is not set or NoFreeze is set, return no permission
if (!(issuerFlagsIn & lsfAllowClawback) || (issuerFlagsIn & lsfNoFreeze))
return tecNO_PERMISSION;
auto const sleRippleState =
ctx.view.read(keylet::line(holder, issuer, clawAmount.getCurrency()));
if (!sleRippleState)
return tecNO_LINE;
STAmount const balance = (*sleRippleState)[sfBalance];
// If balance is positive, issuer must have higher address than holder
if (balance > beast::zero && issuer < holder)
return tecNO_PERMISSION;
// If balance is negative, issuer must have lower address than holder
if (balance < beast::zero && issuer > holder)
return tecNO_PERMISSION;
// At this point, we know that issuer and holder accounts
// are correct and a trustline exists between them.
//
// Must now explicitly check the balance to make sure
// available balance is non-zero.
//
// We can't directly check the balance of trustline because
// the available balance of a trustline is prone to new changes (eg.
// XLS-34). So we must use `accountHolds`.
if (accountHolds(
ctx.view,
holder,
clawAmount.getCurrency(),
issuer,
fhIGNORE_FREEZE,
ctx.j) <= beast::zero)
return tecINSUFFICIENT_FUNDS;
return tesSUCCESS;
}
TER
Clawback::doApply()
{
AccountID const& issuer = account_;
STAmount clawAmount = ctx_.tx[sfAmount];
AccountID const holder = clawAmount.getIssuer(); // cannot be reference
// Replace the `issuer` field with issuer's account
clawAmount.setIssuer(issuer);
if (holder == issuer)
return tecINTERNAL;
// Get the spendable balance. Must use `accountHolds`.
STAmount const spendableAmount = accountHolds(
view(),
holder,
clawAmount.getCurrency(),
clawAmount.getIssuer(),
fhIGNORE_FREEZE,
j_);
return rippleCredit(
view(),
holder,
issuer,
std::min(spendableAmount, clawAmount),
true,
j_);
}
} // namespace ripple

View File

@@ -0,0 +1,48 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2023 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_TX_CLAWBACK_H_INCLUDED
#define RIPPLE_TX_CLAWBACK_H_INCLUDED
#include <ripple/app/tx/impl/Transactor.h>
namespace ripple {
class Clawback : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit Clawback(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace ripple
#endif

View File

@@ -824,8 +824,22 @@ CreateOffer::flowCross(
// what is a good threshold to check?
afterCross.in.clear();
afterCross.out = divRound(
afterCross.in, rate, takerAmount.out.issue(), true);
afterCross.out = [&]() {
// Careful analysis showed that rounding up this
// divRound result could lead to placing a reduced
// offer in the ledger that blocks order books. So
// the fixReducedOffersV1 amendment changes the
// behavior to round down instead.
if (psb.rules().enabled(fixReducedOffersV1))
return divRoundStrict(
afterCross.in,
rate,
takerAmount.out.issue(),
false);
return divRound(
afterCross.in, rate, takerAmount.out.issue(), true);
}();
}
else
{

View File

@@ -23,6 +23,7 @@
#include <ripple/basics/FeeUnits.h>
#include <ripple/basics/Log.h>
#include <ripple/ledger/ReadView.h>
#include <ripple/ledger/View.h>
#include <ripple/protocol/Feature.h>
#include <ripple/protocol/STArray.h>
#include <ripple/protocol/SystemParameters.h>
@@ -717,4 +718,62 @@ NFTokenCountTracking::finalize(
return true;
}
//------------------------------------------------------------------------------
void
ValidClawback::visitEntry(
bool,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const&)
{
if (before && before->getType() == ltRIPPLE_STATE)
trustlinesChanged++;
}
bool
ValidClawback::finalize(
STTx const& tx,
TER const result,
XRPAmount const,
ReadView const& view,
beast::Journal const& j)
{
if (tx.getTxnType() != ttCLAWBACK)
return true;
if (result == tesSUCCESS)
{
if (trustlinesChanged > 1)
{
JLOG(j.fatal())
<< "Invariant failed: more than one trustline changed.";
return false;
}
AccountID const issuer = tx.getAccountID(sfAccount);
STAmount const amount = tx.getFieldAmount(sfAmount);
AccountID const& holder = amount.getIssuer();
STAmount const holderBalance = accountHolds(
view, holder, amount.getCurrency(), issuer, fhIGNORE_FREEZE, j);
if (holderBalance.signum() < 0)
{
JLOG(j.fatal())
<< "Invariant failed: trustline balance is negative";
return false;
}
}
else
{
if (trustlinesChanged != 0)
{
JLOG(j.fatal()) << "Invariant failed: some trustlines were changed "
"despite failure of the transaction.";
return false;
}
}
return true;
}
} // namespace ripple

View File

@@ -389,6 +389,34 @@ public:
beast::Journal const&);
};
/**
* @brief Invariant: Token holder's trustline balance cannot be negative after
* Clawback.
*
* We iterate all the trust lines affected by this transaction and ensure
* that no more than one trustline is modified, and also holder's balance is
* non-negative.
*/
class ValidClawback
{
std::uint32_t trustlinesChanged = 0;
public:
void
visitEntry(
bool,
std::shared_ptr<SLE const> const&,
std::shared_ptr<SLE const> const&);
bool
finalize(
STTx const&,
TER const,
XRPAmount const,
ReadView const&,
beast::Journal const&);
};
// additional invariant checks can be declared above and then added to this
// tuple
using InvariantChecks = std::tuple<
@@ -402,7 +430,8 @@ using InvariantChecks = std::tuple<
NoZeroEscrow,
ValidNewAccountRoot,
ValidNFTokenPage,
NFTokenCountTracking>;
NFTokenCountTracking,
ValidClawback>;
/**
* @brief get a tuple of all invariant checks

View File

@@ -182,17 +182,33 @@ TOfferStreamBase<TIn, TOut>::shouldRmSmallIncreasedQOffer() const
}
TTakerGets const ownerFunds = toAmount<TTakerGets>(*ownerFunds_);
bool const fixReduced = view_.rules().enabled(fixReducedOffersV1);
auto const effectiveAmounts = [&] {
if (offer_.owner() != offer_.issueOut().account &&
ownerFunds < ofrAmts.out)
{
// adjust the amounts by owner funds
// adjust the amounts by owner funds.
//
// It turns out we can prevent order book blocking by rounding down
// the ceil_out() result. This adjustment changes transaction
// results, so it must be made under an amendment.
if (fixReduced)
return offer_.quality().ceil_out_strict(
ofrAmts, ownerFunds, /* roundUp */ false);
return offer_.quality().ceil_out(ofrAmts, ownerFunds);
}
return ofrAmts;
}();
// If either the effective in or out are zero then remove the offer.
// This can happen with fixReducedOffersV1 since it rounds down.
if (fixReduced &&
(effectiveAmounts.in.signum() <= 0 ||
effectiveAmounts.out.signum() <= 0))
return true;
if (effectiveAmounts.in > TTakerPays::minPositiveAmount())
return false;

View File

@@ -218,6 +218,37 @@ SetAccount::preclaim(PreclaimContext const& ctx)
}
}
//
// Clawback
//
if (ctx.view.rules().enabled(featureClawback))
{
if (uSetFlag == asfAllowClawback)
{
if (uFlagsIn & lsfNoFreeze)
{
JLOG(ctx.j.trace()) << "Can't set Clawback if NoFreeze is set";
return tecNO_PERMISSION;
}
if (!dirIsEmpty(ctx.view, keylet::ownerDir(id)))
{
JLOG(ctx.j.trace()) << "Owner directory not empty.";
return tecOWNERS;
}
}
else if (uSetFlag == asfNoFreeze)
{
// Cannot set NoFreeze if clawback is enabled
if (uFlagsIn & lsfAllowClawback)
{
JLOG(ctx.j.trace())
<< "Can't set NoFreeze if clawback is enabled";
return tecNO_PERMISSION;
}
}
}
return tesSUCCESS;
}
@@ -562,6 +593,14 @@ SetAccount::doApply()
uFlagsOut &= ~lsfDisallowIncomingTrustline;
}
// Set flag for clawback
if (ctx_.view().rules().enabled(featureClawback) &&
uSetFlag == asfAllowClawback)
{
JLOG(j_.trace()) << "set allow clawback";
uFlagsOut |= lsfAllowClawback;
}
if (uFlagsIn != uFlagsOut)
sle->setFieldU32(sfFlags, uFlagsOut);

View File

@@ -23,6 +23,7 @@
#include <ripple/app/tx/impl/CancelOffer.h>
#include <ripple/app/tx/impl/CashCheck.h>
#include <ripple/app/tx/impl/Change.h>
#include <ripple/app/tx/impl/Clawback.h>
#include <ripple/app/tx/impl/CreateCheck.h>
#include <ripple/app/tx/impl/CreateOffer.h>
#include <ripple/app/tx/impl/CreateTicket.h>
@@ -147,6 +148,8 @@ invoke_preflight(PreflightContext const& ctx)
return invoke_preflight_helper<NFTokenCancelOffer>(ctx);
case ttNFTOKEN_ACCEPT_OFFER:
return invoke_preflight_helper<NFTokenAcceptOffer>(ctx);
case ttCLAWBACK:
return invoke_preflight_helper<Clawback>(ctx);
default:
assert(false);
return {temUNKNOWN, TxConsequences{temUNKNOWN}};
@@ -248,6 +251,8 @@ invoke_preclaim(PreclaimContext const& ctx)
return invoke_preclaim<NFTokenCancelOffer>(ctx);
case ttNFTOKEN_ACCEPT_OFFER:
return invoke_preclaim<NFTokenAcceptOffer>(ctx);
case ttCLAWBACK:
return invoke_preclaim<Clawback>(ctx);
default:
assert(false);
return temUNKNOWN;
@@ -311,6 +316,8 @@ invoke_calculateBaseFee(ReadView const& view, STTx const& tx)
return NFTokenCancelOffer::calculateBaseFee(view, tx);
case ttNFTOKEN_ACCEPT_OFFER:
return NFTokenAcceptOffer::calculateBaseFee(view, tx);
case ttCLAWBACK:
return Clawback::calculateBaseFee(view, tx);
default:
assert(false);
return XRPAmount{0};
@@ -463,6 +470,10 @@ invoke_apply(ApplyContext& ctx)
NFTokenAcceptOffer p(ctx);
return p();
}
case ttCLAWBACK: {
Clawback p(ctx);
return p();
}
default:
assert(false);
return {temUNKNOWN, false};

View File

@@ -74,7 +74,7 @@ namespace detail {
// Feature.cpp. Because it's only used to reserve storage, and determine how
// large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than
// the actual number of amendments. A LogicError on startup will verify this.
static constexpr std::size_t numFeatures = 58;
static constexpr std::size_t numFeatures = 60;
/** Amendments that this server supports and the default voting behavior.
Whether they are enabled depends on the Rules defined in the validated
@@ -345,6 +345,8 @@ extern uint256 const featureXRPFees;
extern uint256 const fixUniversalNumber;
extern uint256 const fixNonFungibleTokensV1_2;
extern uint256 const fixNFTokenRemint;
extern uint256 const fixReducedOffersV1;
extern uint256 const featureClawback;
} // namespace ripple

View File

@@ -243,6 +243,9 @@ enum LedgerSpecificFlags {
0x10000000, // True, reject new paychans
lsfDisallowIncomingTrustline =
0x20000000, // True, reject new trustlines (only if no issued assets)
lsfAMM [[maybe_unused]] = 0x40000000, // True, AMM account
lsfAllowClawback =
0x80000000, // True, enable clawback
// ltOFFER
lsfPassive = 0x00010000,

View File

@@ -223,6 +223,29 @@ public:
toAmount<In>(stRes.in), toAmount<Out>(stRes.out));
}
Amounts
ceil_out_strict(Amounts const& amount, STAmount const& limit, bool roundUp)
const;
template <class In, class Out>
TAmounts<In, Out>
ceil_out_strict(
TAmounts<In, Out> const& amount,
Out const& limit,
bool roundUp) const
{
if (amount.out <= limit)
return amount;
// Use the existing STAmount implementation for now, but consider
// replacing with code specific to IOUAMount and XRPAmount
Amounts stAmt(toSTAmount(amount.in), toSTAmount(amount.out));
STAmount stLim(toSTAmount(limit));
auto const stRes = ceil_out_strict(stAmt, stLim, roundUp);
return TAmounts<In, Out>(
toAmount<In>(stRes.in), toAmount<Out>(stRes.out));
}
/** Returns `true` if lhs is lower quality than `rhs`.
Lower quality means the taker receives a worse deal.
Higher quality is better for the taker.

View File

@@ -273,13 +273,7 @@ struct TypedField : SField
using type = T;
template <class... Args>
explicit TypedField(Args&&... args) : SField(std::forward<Args>(args)...)
{
}
TypedField(TypedField&& u) : SField(std::move(u))
{
}
explicit TypedField(private_access_tag_t pat, Args&&... args);
};
/** Indicate std::optional field semantics. */

View File

@@ -503,7 +503,7 @@ divide(STAmount const& v1, STAmount const& v2, Issue const& issue);
STAmount
multiply(STAmount const& v1, STAmount const& v2, Issue const& issue);
// multiply, or divide rounding result in specified direction
// multiply rounding result in specified direction
STAmount
mulRound(
STAmount const& v1,
@@ -511,6 +511,15 @@ mulRound(
Issue const& issue,
bool roundUp);
// multiply following the rounding directions more precisely.
STAmount
mulRoundStrict(
STAmount const& v1,
STAmount const& v2,
Issue const& issue,
bool roundUp);
// divide rounding result in specified direction
STAmount
divRound(
STAmount const& v1,
@@ -518,6 +527,14 @@ divRound(
Issue const& issue,
bool roundUp);
// divide following the rounding directions more precisely.
STAmount
divRoundStrict(
STAmount const& v1,
STAmount const& v2,
Issue const& issue,
bool roundUp);
// Someone is offering X for Y, what is the rate?
// Rate: smaller is better, the taker wants the most out: in/out
// VFALCO TODO Return a Quality object

View File

@@ -88,6 +88,7 @@ constexpr std::uint32_t asfDisallowIncomingNFTokenOffer = 12;
constexpr std::uint32_t asfDisallowIncomingCheck = 13;
constexpr std::uint32_t asfDisallowIncomingPayChan = 14;
constexpr std::uint32_t asfDisallowIncomingTrustline = 15;
constexpr std::uint32_t asfAllowClawback = 16;
// OfferCreate flags:
constexpr std::uint32_t tfPassive = 0x00010000;
@@ -159,6 +160,9 @@ constexpr std::uint32_t const tfNFTokenCancelOfferMask = ~(tfUniversal);
// NFTokenAcceptOffer flags:
constexpr std::uint32_t const tfNFTokenAcceptOfferMask = ~tfUniversal;
// Clawback flags:
constexpr std::uint32_t const tfClawbackMask = ~tfUniversal;
// clang-format on
} // namespace ripple

View File

@@ -139,6 +139,9 @@ enum TxType : std::uint16_t
/** This transaction accepts an existing offer to buy or sell an existing NFT. */
ttNFTOKEN_ACCEPT_OFFER = 29,
/** This transaction claws back issued tokens. */
ttCLAWBACK = 30,
/** This system-generated transaction type is used to update the status of the various amendments.
For details, see: https://xrpl.org/amendments.html

View File

@@ -33,7 +33,7 @@ namespace BuildInfo {
// and follow the format described at http://semver.org/
//------------------------------------------------------------------------------
// clang-format off
char const* const versionString = "1.11.0"
char const* const versionString = "1.12.0-b1"
// clang-format on
#if defined(DEBUG) || defined(SANITIZER)

View File

@@ -452,6 +452,8 @@ REGISTER_FEATURE(XRPFees, Supported::yes, VoteBehavior::De
REGISTER_FIX (fixUniversalNumber, Supported::yes, VoteBehavior::DefaultNo);
REGISTER_FIX (fixNonFungibleTokensV1_2, Supported::yes, VoteBehavior::DefaultNo);
REGISTER_FIX (fixNFTokenRemint, Supported::yes, VoteBehavior::DefaultNo);
REGISTER_FIX (fixReducedOffersV1, Supported::yes, VoteBehavior::DefaultNo);
REGISTER_FEATURE(Clawback, Supported::yes, VoteBehavior::DefaultNo);
// The following amendments are obsolete, but must remain supported
// because they could potentially get enabled.

View File

@@ -81,12 +81,20 @@ Quality::ceil_in(Amounts const& amount, STAmount const& limit) const
return amount;
}
Amounts
Quality::ceil_out(Amounts const& amount, STAmount const& limit) const
template <STAmount (
*MulRoundFunc)(STAmount const&, STAmount const&, Issue const&, bool)>
static Amounts
ceil_out_impl(
Amounts const& amount,
STAmount const& limit,
bool roundUp,
Quality const& quality)
{
if (amount.out > limit)
{
Amounts result(mulRound(limit, rate(), amount.in.issue(), true), limit);
Amounts result(
MulRoundFunc(limit, quality.rate(), amount.in.issue(), roundUp),
limit);
// Clamp in
if (result.in > amount.in)
result.in = amount.in;
@@ -97,6 +105,21 @@ Quality::ceil_out(Amounts const& amount, STAmount const& limit) const
return amount;
}
Amounts
Quality::ceil_out(Amounts const& amount, STAmount const& limit) const
{
return ceil_out_impl<mulRound>(amount, limit, /* roundUp */ true, *this);
}
Amounts
Quality::ceil_out_strict(
Amounts const& amount,
STAmount const& limit,
bool roundUp) const
{
return ceil_out_impl<mulRoundStrict>(amount, limit, roundUp, *this);
}
Quality
composed_quality(Quality const& lhs, Quality const& rhs)
{

View File

@@ -38,6 +38,13 @@ struct SField::private_access_tag_t
static SField::private_access_tag_t access;
template <class T>
template <class... Args>
TypedField<T>::TypedField(private_access_tag_t pat, Args&&... args)
: SField(pat, std::forward<Args>(args)...)
{
}
// Construct all compile-time SFields, and register them in the knownCodeToField
// database:

View File

@@ -1266,8 +1266,28 @@ multiply(STAmount const& v1, STAmount const& v2, Issue const& issue)
v1.negative() != v2.negative());
}
// This is the legacy version of canonicalizeRound. It's been in use
// for years, so it is deeply embedded in the behavior of cross-currency
// transactions.
//
// However in 2022 it was noticed that the rounding characteristics were
// surprising. When the code converts from IOU-like to XRP-like there may
// be a fraction of the IOU-like representation that is too small to be
// represented in drops. `canonicalizeRound()` currently does some unusual
// rounding.
//
// 1. If the fractional part is greater than or equal to 0.1, then the
// number of drops is rounded up.
//
// 2. However, if the fractional part is less than 0.1 (for example,
// 0.099999), then the number of drops is rounded down.
//
// The XRP Ledger has this rounding behavior baked in. But there are
// situations where this rounding behavior led to undesirable outcomes.
// So an alternative rounding approach was introduced. You'll see that
// alternative below.
static void
canonicalizeRound(bool native, std::uint64_t& value, int& offset)
canonicalizeRound(bool native, std::uint64_t& value, int& offset, bool)
{
if (native)
{
@@ -1301,8 +1321,100 @@ canonicalizeRound(bool native, std::uint64_t& value, int& offset)
}
}
STAmount
mulRound(
// The original canonicalizeRound did not allow the rounding direction to
// be specified. It also ignored some of the bits that could contribute to
// rounding decisions. canonicalizeRoundStrict() tracks all of the bits in
// the value being rounded.
static void
canonicalizeRoundStrict(
bool native,
std::uint64_t& value,
int& offset,
bool roundUp)
{
if (native)
{
if (offset < 0)
{
bool hadRemainder = false;
while (offset < -1)
{
// It would be better to use std::lldiv than to separately
// compute the remainder. But std::lldiv does not support
// unsigned arguments.
std::uint64_t const newValue = value / 10;
hadRemainder |= (value != (newValue * 10));
value = newValue;
++offset;
}
value +=
(hadRemainder && roundUp) ? 10 : 9; // Add before last divide
value /= 10;
++offset;
}
}
else if (value > STAmount::cMaxValue)
{
while (value > (10 * STAmount::cMaxValue))
{
value /= 10;
++offset;
}
value += 9; // add before last divide
value /= 10;
++offset;
}
}
namespace {
// saveNumberRoundMode doesn't do quite enough for us. What we want is a
// Number::RoundModeGuard that sets the new mode and restores the old mode
// when it leaves scope. Since Number doesn't have that facility, we'll
// build it here.
class NumberRoundModeGuard
{
saveNumberRoundMode saved_;
public:
explicit NumberRoundModeGuard(Number::rounding_mode mode) noexcept
: saved_{Number::setround(mode)}
{
}
NumberRoundModeGuard(NumberRoundModeGuard const&) = delete;
NumberRoundModeGuard&
operator=(NumberRoundModeGuard const&) = delete;
};
// We need a class that has an interface similar to NumberRoundModeGuard
// but does nothing.
class DontAffectNumberRoundMode
{
public:
explicit DontAffectNumberRoundMode(Number::rounding_mode mode) noexcept
{
}
DontAffectNumberRoundMode(DontAffectNumberRoundMode const&) = delete;
DontAffectNumberRoundMode&
operator=(DontAffectNumberRoundMode const&) = delete;
};
} // anonymous namespace
// Pass the canonicalizeRound function pointer as a template parameter.
//
// We might need to use NumberRoundModeGuard. Allow the caller
// to pass either that or a replacement as a template parameter.
template <
void (*CanonicalizeFunc)(bool, std::uint64_t&, int&, bool),
typename MightSaveRound>
static STAmount
mulRoundImpl(
STAmount const& v1,
STAmount const& v2,
Issue const& issue,
@@ -1365,8 +1477,15 @@ mulRound(
int offset = offset1 + offset2 + 14;
if (resultNegative != roundUp)
canonicalizeRound(xrp, amount, offset);
STAmount result(issue, amount, offset, resultNegative);
{
CanonicalizeFunc(xrp, amount, offset, roundUp);
}
STAmount result = [&]() {
// If appropriate, tell Number to round down. This gives the desired
// result from STAmount::canonicalize.
MightSaveRound const savedRound(Number::towards_zero);
return STAmount(issue, amount, offset, resultNegative);
}();
if (roundUp && !resultNegative && !result)
{
@@ -1388,7 +1507,32 @@ mulRound(
}
STAmount
divRound(
mulRound(
STAmount const& v1,
STAmount const& v2,
Issue const& issue,
bool roundUp)
{
return mulRoundImpl<canonicalizeRound, DontAffectNumberRoundMode>(
v1, v2, issue, roundUp);
}
STAmount
mulRoundStrict(
STAmount const& v1,
STAmount const& v2,
Issue const& issue,
bool roundUp)
{
return mulRoundImpl<canonicalizeRoundStrict, NumberRoundModeGuard>(
v1, v2, issue, roundUp);
}
// We might need to use NumberRoundModeGuard. Allow the caller
// to pass either that or a replacement as a template parameter.
template <typename MightSaveRound>
static STAmount
divRoundImpl(
STAmount const& num,
STAmount const& den,
Issue const& issue,
@@ -1437,9 +1581,18 @@ divRound(
int offset = numOffset - denOffset - 17;
if (resultNegative != roundUp)
canonicalizeRound(isXRP(issue), amount, offset);
canonicalizeRound(isXRP(issue), amount, offset, roundUp);
STAmount result = [&]() {
// If appropriate, tell Number the rounding mode we are using.
// Note that "roundUp == true" actually means "round away from zero".
// Otherwise round toward zero.
using enum Number::rounding_mode;
MightSaveRound const savedRound(
roundUp ^ resultNegative ? upward : downward);
return STAmount(issue, amount, offset, resultNegative);
}();
STAmount result(issue, amount, offset, resultNegative);
if (roundUp && !resultNegative && !result)
{
if (isXRP(issue))
@@ -1459,4 +1612,24 @@ divRound(
return result;
}
STAmount
divRound(
STAmount const& num,
STAmount const& den,
Issue const& issue,
bool roundUp)
{
return divRoundImpl<DontAffectNumberRoundMode>(num, den, issue, roundUp);
}
STAmount
divRoundStrict(
STAmount const& num,
STAmount const& den,
Issue const& issue,
bool roundUp)
{
return divRoundImpl<NumberRoundModeGuard>(num, den, issue, roundUp);
}
} // namespace ripple

View File

@@ -328,6 +328,14 @@ TxFormats::TxFormats()
{sfTicketSequence, soeOPTIONAL},
},
commonFields);
add(jss::Clawback,
ttCLAWBACK,
{
{sfAmount, soeREQUIRED},
{sfTicketSequence, soeOPTIONAL},
},
commonFields);
}
TxFormats const&

View File

@@ -53,6 +53,7 @@ JSS(Check); // ledger type.
JSS(CheckCancel); // transaction type.
JSS(CheckCash); // transaction type.
JSS(CheckCreate); // transaction type.
JSS(Clawback); // transaction type.
JSS(ClearFlag); // field.
JSS(DeliverMin); // in: TransactionSign
JSS(DepositPreauth); // transaction and ledger type.
@@ -465,13 +466,14 @@ JSS(peers); // out: InboundLedger, handlers/Peers, Overlay
JSS(peer_disconnects); // Severed peer connection counter.
JSS(peer_disconnects_resources); // Severed peer connections because of
// excess resource consumption.
JSS(port); // in: Connect
JSS(port); // in: Connect, out: NetworkOPs
JSS(ports); // out: NetworkOPs
JSS(previous); // out: Reservations
JSS(previous_ledger); // out: LedgerPropose
JSS(proof); // in: BookOffers
JSS(propose_seq); // out: LedgerPropose
JSS(proposers); // out: NetworkOPs, LedgerConsensus
JSS(protocol); // out: PeerImp
JSS(protocol); // out: NetworkOPs, PeerImp
JSS(proxied); // out: RPC ping
JSS(pubkey_node); // out: NetworkOPs
JSS(pubkey_publisher); // out: ValidatorList

View File

@@ -0,0 +1,971 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2023 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <ripple/basics/random.h>
#include <ripple/json/to_string.h>
#include <ripple/ledger/ApplyViewImpl.h>
#include <ripple/protocol/Feature.h>
#include <ripple/protocol/jss.h>
#include <initializer_list>
#include <test/jtx.h>
#include <test/jtx/trust.h>
namespace ripple {
class Clawback_test : public beast::unit_test::suite
{
template <class T>
static std::string
to_string(T const& t)
{
return boost::lexical_cast<std::string>(t);
}
// Helper function that returns the owner count of an account root.
static std::uint32_t
ownerCount(test::jtx::Env const& env, test::jtx::Account const& acct)
{
std::uint32_t ret{0};
if (auto const sleAcct = env.le(acct))
ret = sleAcct->at(sfOwnerCount);
return ret;
}
// Helper function that returns the number of tickets held by an account.
static std::uint32_t
ticketCount(test::jtx::Env const& env, test::jtx::Account const& acct)
{
std::uint32_t ret{0};
if (auto const sleAcct = env.le(acct))
ret = sleAcct->at(~sfTicketCount).value_or(0);
return ret;
}
// Helper function that returns the freeze status of a trustline
static bool
getLineFreezeFlag(
test::jtx::Env const& env,
test::jtx::Account const& src,
test::jtx::Account const& dst,
Currency const& cur)
{
if (auto sle = env.le(keylet::line(src, dst, cur)))
{
auto const useHigh = src.id() > dst.id();
return sle->isFlag(useHigh ? lsfHighFreeze : lsfLowFreeze);
}
Throw<std::runtime_error>("No line in getLineFreezeFlag");
return false; // silence warning
}
void
testAllowClawbackFlag(FeatureBitset features)
{
testcase("Enable AllowClawback flag");
using namespace test::jtx;
// Test that one can successfully set asfAllowClawback flag.
// If successful, asfNoFreeze can no longer be set.
// Also, asfAllowClawback cannot be cleared.
{
Env env(*this, features);
Account alice{"alice"};
env.fund(XRP(1000), alice);
env.close();
// set asfAllowClawback
env(fset(alice, asfAllowClawback));
env.close();
env.require(flags(alice, asfAllowClawback));
// clear asfAllowClawback does nothing
env(fclear(alice, asfAllowClawback));
env.close();
env.require(flags(alice, asfAllowClawback));
// asfNoFreeze cannot be set when asfAllowClawback is set
env.require(nflags(alice, asfNoFreeze));
env(fset(alice, asfNoFreeze), ter(tecNO_PERMISSION));
env.close();
}
// Test that asfAllowClawback cannot be set when
// asfNoFreeze has been set
{
Env env(*this, features);
Account alice{"alice"};
env.fund(XRP(1000), alice);
env.close();
env.require(nflags(alice, asfNoFreeze));
// set asfNoFreeze
env(fset(alice, asfNoFreeze));
env.close();
// NoFreeze is set
env.require(flags(alice, asfNoFreeze));
// asfAllowClawback cannot be set if asfNoFreeze is set
env(fset(alice, asfAllowClawback), ter(tecNO_PERMISSION));
env.close();
env.require(nflags(alice, asfAllowClawback));
}
// Test that asfAllowClawback is not allowed when owner dir is non-empty
{
Env env(*this, features);
Account alice{"alice"};
Account bob{"bob"};
env.fund(XRP(1000), alice, bob);
env.close();
auto const USD = alice["USD"];
env.require(nflags(alice, asfAllowClawback));
// alice issues 10 USD to bob
env.trust(USD(1000), bob);
env(pay(alice, bob, USD(10)));
env.close();
BEAST_EXPECT(ownerCount(env, alice) == 0);
BEAST_EXPECT(ownerCount(env, bob) == 1);
// alice fails to enable clawback because she has trustline with bob
env(fset(alice, asfAllowClawback), ter(tecOWNERS));
env.close();
// bob sets trustline to default limit and pays alice back to delete
// the trustline
env(trust(bob, USD(0), 0));
env(pay(bob, alice, USD(10)));
BEAST_EXPECT(ownerCount(env, alice) == 0);
BEAST_EXPECT(ownerCount(env, bob) == 0);
// alice now is able to set asfAllowClawback
env(fset(alice, asfAllowClawback));
env.close();
env.require(flags(alice, asfAllowClawback));
BEAST_EXPECT(ownerCount(env, alice) == 0);
BEAST_EXPECT(ownerCount(env, bob) == 0);
}
// Test that one cannot enable asfAllowClawback when
// featureClawback amendment is disabled
{
Env env(*this, features - featureClawback);
Account alice{"alice"};
env.fund(XRP(1000), alice);
env.close();
env.require(nflags(alice, asfAllowClawback));
// alice attempts to set asfAllowClawback flag while amendment is
// disabled. no error is returned, but the flag remains to be unset.
env(fset(alice, asfAllowClawback));
env.close();
env.require(nflags(alice, asfAllowClawback));
// now enable clawback amendment
env.enableFeature(featureClawback);
env.close();
// asfAllowClawback can be set
env(fset(alice, asfAllowClawback));
env.close();
env.require(flags(alice, asfAllowClawback));
}
}
void
testValidation(FeatureBitset features)
{
testcase("Validation");
using namespace test::jtx;
// Test that Clawback tx fails for the following:
// 1. when amendment is disabled
// 2. when asfAllowClawback flag has not been set
{
Env env(*this, features - featureClawback);
Account alice{"alice"};
Account bob{"bob"};
env.fund(XRP(1000), alice, bob);
env.close();
env.require(nflags(alice, asfAllowClawback));
auto const USD = alice["USD"];
// alice issues 10 USD to bob
env.trust(USD(1000), bob);
env(pay(alice, bob, USD(10)));
env.close();
env.require(balance(bob, alice["USD"](10)));
env.require(balance(alice, bob["USD"](-10)));
// clawback fails because amendment is disabled
env(claw(alice, bob["USD"](5)), ter(temDISABLED));
env.close();
// now enable clawback amendment
env.enableFeature(featureClawback);
env.close();
// clawback fails because asfAllowClawback has not been set
env(claw(alice, bob["USD"](5)), ter(tecNO_PERMISSION));
env.close();
env.require(balance(bob, alice["USD"](10)));
env.require(balance(alice, bob["USD"](-10)));
}
// Test that Clawback tx fails for the following:
// 1. invalid flag
// 2. negative STAmount
// 3. zero STAmount
// 4. XRP amount
// 5. `account` and `issuer` fields are same account
// 6. trustline has a balance of 0
// 7. trustline does not exist
{
Env env(*this, features);
Account alice{"alice"};
Account bob{"bob"};
env.fund(XRP(1000), alice, bob);
env.close();
// alice sets asfAllowClawback
env(fset(alice, asfAllowClawback));
env.close();
env.require(flags(alice, asfAllowClawback));
auto const USD = alice["USD"];
// alice issues 10 USD to bob
env.trust(USD(1000), bob);
env(pay(alice, bob, USD(10)));
env.close();
env.require(balance(bob, alice["USD"](10)));
env.require(balance(alice, bob["USD"](-10)));
// fails due to invalid flag
env(claw(alice, bob["USD"](5)),
txflags(0x00008000),
ter(temINVALID_FLAG));
env.close();
// fails due to negative amount
env(claw(alice, bob["USD"](-5)), ter(temBAD_AMOUNT));
env.close();
// fails due to zero amount
env(claw(alice, bob["USD"](0)), ter(temBAD_AMOUNT));
env.close();
// fails because amount is in XRP
env(claw(alice, XRP(10)), ter(temBAD_AMOUNT));
env.close();
// fails when `issuer` field in `amount` is not token holder
// NOTE: we are using the `issuer` field for the token holder
env(claw(alice, alice["USD"](5)), ter(temBAD_AMOUNT));
env.close();
// bob pays alice back, trustline has a balance of 0
env(pay(bob, alice, USD(10)));
env.close();
// bob still owns the trustline that has 0 balance
BEAST_EXPECT(ownerCount(env, alice) == 0);
BEAST_EXPECT(ownerCount(env, bob) == 1);
env.require(balance(bob, alice["USD"](0)));
env.require(balance(alice, bob["USD"](0)));
// clawback fails because because balance is 0
env(claw(alice, bob["USD"](5)), ter(tecINSUFFICIENT_FUNDS));
env.close();
// set the limit to default, which should delete the trustline
env(trust(bob, USD(0), 0));
env.close();
// bob no longer owns the trustline
BEAST_EXPECT(ownerCount(env, alice) == 0);
BEAST_EXPECT(ownerCount(env, bob) == 0);
// clawback fails because trustline does not exist
env(claw(alice, bob["USD"](5)), ter(tecNO_LINE));
env.close();
}
}
void
testPermission(FeatureBitset features)
{
// Checks the tx submitter has the permission to clawback.
// Exercises preclaim code
testcase("Permission");
using namespace test::jtx;
// Clawing back from an non-existent account returns error
{
Env env(*this, features);
Account alice{"alice"};
Account bob{"bob"};
// bob's account is not funded and does not exist
env.fund(XRP(1000), alice);
env.close();
// alice sets asfAllowClawback
env(fset(alice, asfAllowClawback));
env.close();
env.require(flags(alice, asfAllowClawback));
// bob, the token holder, does not exist
env(claw(alice, bob["USD"](5)), ter(terNO_ACCOUNT));
env.close();
}
// Test that trustline cannot be clawed by someone who is
// not the issuer of the currency
{
Env env(*this, features);
Account alice{"alice"};
Account bob{"bob"};
Account cindy{"cindy"};
env.fund(XRP(1000), alice, bob, cindy);
env.close();
auto const USD = alice["USD"];
// alice sets asfAllowClawback
env(fset(alice, asfAllowClawback));
env.close();
env.require(flags(alice, asfAllowClawback));
// cindy sets asfAllowClawback
env(fset(cindy, asfAllowClawback));
env.close();
env.require(flags(cindy, asfAllowClawback));
// alice issues 1000 USD to bob
env.trust(USD(1000), bob);
env(pay(alice, bob, USD(1000)));
env.close();
env.require(balance(bob, alice["USD"](1000)));
env.require(balance(alice, bob["USD"](-1000)));
// cindy tries to claw from bob, and fails because trustline does
// not exist
env(claw(cindy, bob["USD"](200)), ter(tecNO_LINE));
env.close();
}
// When a trustline is created between issuer and holder,
// we must make sure the holder is unable to claw back from
// the issuer by impersonating the issuer account.
//
// This must be tested bidirectionally for both accounts because the
// issuer could be either the low or high account in the trustline
// object
{
Env env(*this, features);
Account alice{"alice"};
Account bob{"bob"};
env.fund(XRP(1000), alice, bob);
env.close();
auto const USD = alice["USD"];
auto const CAD = bob["CAD"];
// alice sets asfAllowClawback
env(fset(alice, asfAllowClawback));
env.close();
env.require(flags(alice, asfAllowClawback));
// bob sets asfAllowClawback
env(fset(bob, asfAllowClawback));
env.close();
env.require(flags(bob, asfAllowClawback));
// alice issues 10 USD to bob.
// bob then attempts to submit a clawback tx to claw USD from alice.
// this must FAIL, because bob is not the issuer for this
// trustline!!!
{
// bob creates a trustline with alice, and alice sends 10 USD to
// bob
env.trust(USD(1000), bob);
env(pay(alice, bob, USD(10)));
env.close();
env.require(balance(bob, alice["USD"](10)));
env.require(balance(alice, bob["USD"](-10)));
// bob cannot claw back USD from alice because he's not the
// issuer
env(claw(bob, alice["USD"](5)), ter(tecNO_PERMISSION));
env.close();
}
// bob issues 10 CAD to alice.
// alice then attempts to submit a clawback tx to claw CAD from bob.
// this must FAIL, because alice is not the issuer for this
// trustline!!!
{
// alice creates a trustline with bob, and bob sends 10 CAD to
// alice
env.trust(CAD(1000), alice);
env(pay(bob, alice, CAD(10)));
env.close();
env.require(balance(bob, alice["CAD"](-10)));
env.require(balance(alice, bob["CAD"](10)));
// alice cannot claw back CAD from bob because she's not the
// issuer
env(claw(alice, bob["CAD"](5)), ter(tecNO_PERMISSION));
env.close();
}
}
}
void
testEnabled(FeatureBitset features)
{
testcase("Enable clawback");
using namespace test::jtx;
// Test that alice is able to successfully clawback tokens from bob
Env env(*this, features);
Account alice{"alice"};
Account bob{"bob"};
env.fund(XRP(1000), alice, bob);
env.close();
auto const USD = alice["USD"];
// alice sets asfAllowClawback
env(fset(alice, asfAllowClawback));
env.close();
env.require(flags(alice, asfAllowClawback));
// alice issues 1000 USD to bob
env.trust(USD(1000), bob);
env(pay(alice, bob, USD(1000)));
env.close();
env.require(balance(bob, alice["USD"](1000)));
env.require(balance(alice, bob["USD"](-1000)));
// alice claws back 200 USD from bob
env(claw(alice, bob["USD"](200)));
env.close();
// bob should have 800 USD left
env.require(balance(bob, alice["USD"](800)));
env.require(balance(alice, bob["USD"](-800)));
// alice claws back 800 USD from bob again
env(claw(alice, bob["USD"](800)));
env.close();
// trustline has a balance of 0
env.require(balance(bob, alice["USD"](0)));
env.require(balance(alice, bob["USD"](0)));
}
void
testMultiLine(FeatureBitset features)
{
// Test scenarios where multiple trustlines are involved
testcase("Multi line");
using namespace test::jtx;
// Both alice and bob issues their own "USD" to cindy.
// When alice and bob tries to claw back, they will only
// claw back from their respective trustline.
{
Env env(*this, features);
Account alice{"alice"};
Account bob{"bob"};
Account cindy{"cindy"};
env.fund(XRP(1000), alice, bob, cindy);
env.close();
// alice sets asfAllowClawback
env(fset(alice, asfAllowClawback));
env.close();
env.require(flags(alice, asfAllowClawback));
// bob sets asfAllowClawback
env(fset(bob, asfAllowClawback));
env.close();
env.require(flags(bob, asfAllowClawback));
// alice sends 1000 USD to cindy
env.trust(alice["USD"](1000), cindy);
env(pay(alice, cindy, alice["USD"](1000)));
env.close();
// bob sends 1000 USD to cindy
env.trust(bob["USD"](1000), cindy);
env(pay(bob, cindy, bob["USD"](1000)));
env.close();
// alice claws back 200 USD from cindy
env(claw(alice, cindy["USD"](200)));
env.close();
// cindy has 800 USD left in alice's trustline after clawed by alice
env.require(balance(cindy, alice["USD"](800)));
env.require(balance(alice, cindy["USD"](-800)));
// cindy still has 1000 USD in bob's trustline
env.require(balance(cindy, bob["USD"](1000)));
env.require(balance(bob, cindy["USD"](-1000)));
// bob claws back 600 USD from cindy
env(claw(bob, cindy["USD"](600)));
env.close();
// cindy has 400 USD left in bob's trustline after clawed by bob
env.require(balance(cindy, bob["USD"](400)));
env.require(balance(bob, cindy["USD"](-400)));
// cindy still has 800 USD in alice's trustline
env.require(balance(cindy, alice["USD"](800)));
env.require(balance(alice, cindy["USD"](-800)));
}
// alice issues USD to both bob and cindy.
// when alice claws back from bob, only bob's USD balance is
// affected, and cindy's balance remains unchanged, and vice versa.
{
Env env(*this, features);
Account alice{"alice"};
Account bob{"bob"};
Account cindy{"cindy"};
env.fund(XRP(1000), alice, bob, cindy);
env.close();
auto const USD = alice["USD"];
// alice sets asfAllowClawback
env(fset(alice, asfAllowClawback));
env.close();
env.require(flags(alice, asfAllowClawback));
// alice sends 600 USD to bob
env.trust(USD(1000), bob);
env(pay(alice, bob, USD(600)));
env.close();
env.require(balance(alice, bob["USD"](-600)));
env.require(balance(bob, alice["USD"](600)));
// alice sends 1000 USD to cindy
env.trust(USD(1000), cindy);
env(pay(alice, cindy, USD(1000)));
env.close();
env.require(balance(alice, cindy["USD"](-1000)));
env.require(balance(cindy, alice["USD"](1000)));
// alice claws back 500 USD from bob
env(claw(alice, bob["USD"](500)));
env.close();
// bob's balance is reduced
env.require(balance(alice, bob["USD"](-100)));
env.require(balance(bob, alice["USD"](100)));
// cindy's balance is unchanged
env.require(balance(alice, cindy["USD"](-1000)));
env.require(balance(cindy, alice["USD"](1000)));
// alice claws back 300 USD from cindy
env(claw(alice, cindy["USD"](300)));
env.close();
// bob's balance is unchanged
env.require(balance(alice, bob["USD"](-100)));
env.require(balance(bob, alice["USD"](100)));
// cindy's balance is reduced
env.require(balance(alice, cindy["USD"](-700)));
env.require(balance(cindy, alice["USD"](700)));
}
}
void
testBidirectionalLine(FeatureBitset features)
{
testcase("Bidirectional line");
using namespace test::jtx;
// Test when both alice and bob issues USD to each other.
// This scenario creates only one trustline.
// In this case, both alice and bob can be seen as the "issuer"
// and they can send however many USDs to each other.
// We test that only the person who has a negative balance from their
// perspective is allowed to clawback
Env env(*this, features);
Account alice{"alice"};
Account bob{"bob"};
env.fund(XRP(1000), alice, bob);
env.close();
// alice sets asfAllowClawback
env(fset(alice, asfAllowClawback));
env.close();
env.require(flags(alice, asfAllowClawback));
// bob sets asfAllowClawback
env(fset(bob, asfAllowClawback));
env.close();
env.require(flags(bob, asfAllowClawback));
// alice issues 1000 USD to bob
env.trust(alice["USD"](1000), bob);
env(pay(alice, bob, alice["USD"](1000)));
env.close();
BEAST_EXPECT(ownerCount(env, alice) == 0);
BEAST_EXPECT(ownerCount(env, bob) == 1);
// bob is the holder, and alice is the issuer
env.require(balance(bob, alice["USD"](1000)));
env.require(balance(alice, bob["USD"](-1000)));
// bob issues 1500 USD to alice
env.trust(bob["USD"](1500), alice);
env(pay(bob, alice, bob["USD"](1500)));
env.close();
BEAST_EXPECT(ownerCount(env, alice) == 1);
BEAST_EXPECT(ownerCount(env, bob) == 1);
// bob has negative 500 USD because bob issued 500 USD more than alice
// bob can now been seen as the issuer, while alice is the holder
env.require(balance(bob, alice["USD"](-500)));
env.require(balance(alice, bob["USD"](500)));
// At this point, both alice and bob are the issuers of USD
// and can send USD to each other through one trustline
// alice fails to clawback. Even though she is also an issuer,
// the trustline balance is positive from her perspective
env(claw(alice, bob["USD"](200)), ter(tecNO_PERMISSION));
env.close();
// bob is able to successfully clawback from alice because
// the trustline balance is negative from his perspective
env(claw(bob, alice["USD"](200)));
env.close();
env.require(balance(bob, alice["USD"](-300)));
env.require(balance(alice, bob["USD"](300)));
// alice pays bob 1000 USD
env(pay(alice, bob, alice["USD"](1000)));
env.close();
// bob's balance becomes positive from his perspective because
// alice issued more USD than the balance
env.require(balance(bob, alice["USD"](700)));
env.require(balance(alice, bob["USD"](-700)));
// bob is now the holder and fails to clawback
env(claw(bob, alice["USD"](200)), ter(tecNO_PERMISSION));
env.close();
// alice successfully claws back
env(claw(alice, bob["USD"](200)));
env.close();
env.require(balance(bob, alice["USD"](500)));
env.require(balance(alice, bob["USD"](-500)));
}
void
testDeleteDefaultLine(FeatureBitset features)
{
testcase("Delete default trustline");
using namespace test::jtx;
// If clawback results the trustline to be default,
// trustline should be automatically deleted
Env env(*this, features);
Account alice{"alice"};
Account bob{"bob"};
env.fund(XRP(1000), alice, bob);
env.close();
auto const USD = alice["USD"];
// alice sets asfAllowClawback
env(fset(alice, asfAllowClawback));
env.close();
env.require(flags(alice, asfAllowClawback));
// alice issues 1000 USD to bob
env.trust(USD(1000), bob);
env(pay(alice, bob, USD(1000)));
env.close();
BEAST_EXPECT(ownerCount(env, alice) == 0);
BEAST_EXPECT(ownerCount(env, bob) == 1);
env.require(balance(bob, alice["USD"](1000)));
env.require(balance(alice, bob["USD"](-1000)));
// set limit to default,
env(trust(bob, USD(0), 0));
env.close();
BEAST_EXPECT(ownerCount(env, alice) == 0);
BEAST_EXPECT(ownerCount(env, bob) == 1);
// alice claws back full amount from bob, and should also delete
// trustline
env(claw(alice, bob["USD"](1000)));
env.close();
// bob no longer owns the trustline because it was deleted
BEAST_EXPECT(ownerCount(env, alice) == 0);
BEAST_EXPECT(ownerCount(env, bob) == 0);
}
void
testFrozenLine(FeatureBitset features)
{
testcase("Frozen trustline");
using namespace test::jtx;
// Claws back from frozen trustline
// and the trustline should remain frozen
Env env(*this, features);
Account alice{"alice"};
Account bob{"bob"};
env.fund(XRP(1000), alice, bob);
env.close();
auto const USD = alice["USD"];
// alice sets asfAllowClawback
env(fset(alice, asfAllowClawback));
env.close();
env.require(flags(alice, asfAllowClawback));
// alice issues 1000 USD to bob
env.trust(USD(1000), bob);
env(pay(alice, bob, USD(1000)));
env.close();
env.require(balance(bob, alice["USD"](1000)));
env.require(balance(alice, bob["USD"](-1000)));
// freeze trustline
env(trust(alice, bob["USD"](0), tfSetFreeze));
env.close();
// alice claws back 200 USD from bob
env(claw(alice, bob["USD"](200)));
env.close();
// bob should have 800 USD left
env.require(balance(bob, alice["USD"](800)));
env.require(balance(alice, bob["USD"](-800)));
// trustline remains frozen
BEAST_EXPECT(getLineFreezeFlag(env, alice, bob, USD.currency));
}
void
testAmountExceedsAvailable(FeatureBitset features)
{
testcase("Amount exceeds available");
using namespace test::jtx;
// When alice tries to claw back an amount that is greater
// than what bob holds, only the max available balance is clawed
Env env(*this, features);
Account alice{"alice"};
Account bob{"bob"};
env.fund(XRP(1000), alice, bob);
env.close();
auto const USD = alice["USD"];
// alice sets asfAllowClawback
env(fset(alice, asfAllowClawback));
env.close();
env.require(flags(alice, asfAllowClawback));
// alice issues 1000 USD to bob
env.trust(USD(1000), bob);
env(pay(alice, bob, USD(1000)));
env.close();
env.require(balance(bob, alice["USD"](1000)));
env.require(balance(alice, bob["USD"](-1000)));
// alice tries to claw back 2000 USD
env(claw(alice, bob["USD"](2000)));
env.close();
// check alice and bob's balance.
// alice was only able to claw back 1000 USD at maximum
env.require(balance(bob, alice["USD"](0)));
env.require(balance(alice, bob["USD"](0)));
// bob still owns the trustline because trustline is not in default
// state
BEAST_EXPECT(ownerCount(env, alice) == 0);
BEAST_EXPECT(ownerCount(env, bob) == 1);
// set limit to default,
env(trust(bob, USD(0), 0));
env.close();
// verify that bob's trustline was deleted
BEAST_EXPECT(ownerCount(env, alice) == 0);
BEAST_EXPECT(ownerCount(env, bob) == 0);
}
void
testTickets(FeatureBitset features)
{
testcase("Tickets");
using namespace test::jtx;
// Tests clawback with tickets
Env env(*this, features);
Account alice{"alice"};
Account bob{"bob"};
env.fund(XRP(1000), alice, bob);
env.close();
auto const USD = alice["USD"];
// alice sets asfAllowClawback
env(fset(alice, asfAllowClawback));
env.close();
env.require(flags(alice, asfAllowClawback));
// alice issues 100 USD to bob
env.trust(USD(1000), bob);
env(pay(alice, bob, USD(100)));
env.close();
env.require(balance(bob, alice["USD"](100)));
env.require(balance(alice, bob["USD"](-100)));
// alice creates 10 tickets
std::uint32_t ticketCnt = 10;
std::uint32_t aliceTicketSeq{env.seq(alice) + 1};
env(ticket::create(alice, ticketCnt));
env.close();
std::uint32_t const aliceSeq{env.seq(alice)};
BEAST_EXPECT(ticketCount(env, alice) == ticketCnt);
BEAST_EXPECT(ownerCount(env, alice) == ticketCnt);
while (ticketCnt > 0)
{
// alice claws back 5 USD using a ticket
env(claw(alice, bob["USD"](5)), ticket::use(aliceTicketSeq++));
env.close();
ticketCnt--;
BEAST_EXPECT(ticketCount(env, alice) == ticketCnt);
BEAST_EXPECT(ownerCount(env, alice) == ticketCnt);
}
// alice clawed back 50 USD total, trustline has 50 USD remaining
env.require(balance(bob, alice["USD"](50)));
env.require(balance(alice, bob["USD"](-50)));
// Verify that the account sequence numbers did not advance.
BEAST_EXPECT(env.seq(alice) == aliceSeq);
}
void
testWithFeats(FeatureBitset features)
{
testAllowClawbackFlag(features);
testValidation(features);
testPermission(features);
testEnabled(features);
testMultiLine(features);
testBidirectionalLine(features);
testDeleteDefaultLine(features);
testFrozenLine(features);
testAmountExceedsAvailable(features);
testTickets(features);
}
public:
void
run() override
{
using namespace test::jtx;
FeatureBitset const all{supported_amendments()};
testWithFeats(all);
}
};
BEAST_DEFINE_TESTSUITE(Clawback, app, ripple);
} // namespace ripple

View File

@@ -2126,18 +2126,17 @@ public:
BEAST_EXPECT(
jrr[jss::node][sfBalance.fieldName][jss::value] ==
"49.96666666666667");
jrr = ledgerEntryState(env, bob, gw, "USD");
if (NumberSwitchOver)
Json::Value const bobsUSD =
jrr[jss::node][sfBalance.fieldName][jss::value];
if (!NumberSwitchOver)
{
BEAST_EXPECT(
jrr[jss::node][sfBalance.fieldName][jss::value] ==
"-0.9665000000333333");
BEAST_EXPECT(bobsUSD == "-0.966500000033334");
}
else
{
BEAST_EXPECT(
jrr[jss::node][sfBalance.fieldName][jss::value] ==
"-0.966500000033334");
BEAST_EXPECT(bobsUSD == "-0.9665000000333333");
}
}
}

View File

@@ -0,0 +1,622 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2022 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <ripple/protocol/Feature.h>
#include <ripple/protocol/Quality.h>
#include <ripple/protocol/jss.h>
#include <test/jtx.h>
namespace ripple {
namespace test {
class ReducedOffer_test : public beast::unit_test::suite
{
static auto
ledgerEntryOffer(
jtx::Env& env,
jtx::Account const& acct,
std::uint32_t offer_seq)
{
Json::Value jvParams;
jvParams[jss::offer][jss::account] = acct.human();
jvParams[jss::offer][jss::seq] = offer_seq;
return env.rpc(
"json", "ledger_entry", to_string(jvParams))[jss::result];
}
static bool
offerInLedger(
jtx::Env& env,
jtx::Account const& acct,
std::uint32_t offerSeq)
{
Json::Value ledgerOffer = ledgerEntryOffer(env, acct, offerSeq);
return !(
ledgerOffer.isMember(jss::error) &&
ledgerOffer[jss::error].asString() == "entryNotFound");
}
// Common code to clean up unneeded offers.
static void
cleanupOldOffers(
jtx::Env& env,
jtx::Account const& acct1,
jtx::Account const& acct2,
std::uint32_t acct1OfferSeq,
std::uint32_t acct2OfferSeq)
{
env(offer_cancel(acct1, acct1OfferSeq));
env(offer_cancel(acct2, acct2OfferSeq));
env.close();
}
public:
void
testPartialCrossNewXrpIouQChange()
{
testcase("exercise partial cross new XRP/IOU offer Q change");
using namespace jtx;
auto const gw = Account{"gateway"};
auto const alice = Account{"alice"};
auto const bob = Account{"bob"};
auto const USD = gw["USD"];
// Make one test run without fixReducedOffersV1 and one with.
for (FeatureBitset features :
{supported_amendments() - fixReducedOffersV1,
supported_amendments() | fixReducedOffersV1})
{
Env env{*this, features};
// Make sure none of the offers we generate are under funded.
env.fund(XRP(10'000'000), gw, alice, bob);
env.close();
env(trust(alice, USD(10'000'000)));
env(trust(bob, USD(10'000'000)));
env.close();
env(pay(gw, bob, USD(10'000'000)));
env.close();
// Lambda that:
// 1. Exercises one offer pair,
// 2. Collects the results, and
// 3. Cleans up for the next offer pair.
// Returns 1 if the crossed offer has a bad rate for the book.
auto exerciseOfferPair =
[this, &env, &alice, &bob](
Amounts const& inLedger,
Amounts const& newOffer) -> unsigned int {
// Put inLedger offer in the ledger so newOffer can cross it.
std::uint32_t const aliceOfferSeq = env.seq(alice);
env(offer(alice, inLedger.in, inLedger.out));
env.close();
// Now alice's offer will partially cross bob's offer.
STAmount const initialRate = Quality(newOffer).rate();
std::uint32_t const bobOfferSeq = env.seq(bob);
STAmount const bobInitialBalance = env.balance(bob);
STAmount const bobsFee = drops(10);
env(offer(bob, newOffer.in, newOffer.out, tfSell),
fee(bobsFee));
env.close();
STAmount const bobFinalBalance = env.balance(bob);
// alice's offer should be fully crossed and so gone from
// the ledger.
if (!BEAST_EXPECT(!offerInLedger(env, alice, aliceOfferSeq)))
// If the in-ledger offer was not consumed then further
// results are meaningless.
return 1;
// bob's offer should be in the ledger, but reduced in size.
unsigned int badRate = 1;
{
Json::Value bobOffer =
ledgerEntryOffer(env, bob, bobOfferSeq);
STAmount const reducedTakerGets = amountFromJson(
sfTakerGets, bobOffer[jss::node][sfTakerGets.jsonName]);
STAmount const reducedTakerPays = amountFromJson(
sfTakerPays, bobOffer[jss::node][sfTakerPays.jsonName]);
STAmount const bobGot =
env.balance(bob) + bobsFee - bobInitialBalance;
BEAST_EXPECT(reducedTakerPays < newOffer.in);
BEAST_EXPECT(reducedTakerGets < newOffer.out);
STAmount const inLedgerRate =
Quality(Amounts{reducedTakerPays, reducedTakerGets})
.rate();
badRate = inLedgerRate > initialRate ? 1 : 0;
// If the inLedgerRate is less than initial rate, then
// incrementing the mantissa of the reduced taker pays
// should result in a rate higher than initial. Check
// this to verify that the largest allowable TakerPays
// was computed.
if (badRate == 0)
{
STAmount const tweakedTakerPays =
reducedTakerPays + drops(1);
STAmount const tweakedRate =
Quality(Amounts{tweakedTakerPays, reducedTakerGets})
.rate();
BEAST_EXPECT(tweakedRate > initialRate);
}
#if 0
std::cout << "Placed rate: " << initialRate
<< "; in-ledger rate: " << inLedgerRate
<< "; TakerPays: " << reducedTakerPays
<< "; TakerGets: " << reducedTakerGets
<< "; bob already got: " << bobGot << std::endl;
// #else
std::string_view filler =
inLedgerRate > initialRate ? "**" : " ";
std::cout << "| `" << reducedTakerGets << "` | `"
<< reducedTakerPays << "` | `" << initialRate
<< "` | " << filler << "`" << inLedgerRate << "`"
<< filler << " |`" << std::endl;
#endif
}
// In preparation for the next iteration make sure the two
// offers are gone from the ledger.
cleanupOldOffers(env, alice, bob, aliceOfferSeq, bobOfferSeq);
return badRate;
};
// bob's offer (the new offer) is the same every time:
Amounts const bobsOffer{
STAmount(XRP(1)), STAmount(USD.issue(), 1, 0)};
// alice's offer has a slightly smaller TakerPays with each
// iteration. This should mean that the size of the offer bob
// places in the ledger should increase with each iteration.
unsigned int blockedCount = 0;
for (std::uint64_t mantissaReduce = 1'000'000'000ull;
mantissaReduce <= 5'000'000'000ull;
mantissaReduce += 20'000'000ull)
{
STAmount aliceUSD{
bobsOffer.out.issue(),
bobsOffer.out.mantissa() - mantissaReduce,
bobsOffer.out.exponent()};
STAmount aliceXRP{
bobsOffer.in.issue(), bobsOffer.in.mantissa() - 1};
Amounts alicesOffer{aliceUSD, aliceXRP};
blockedCount += exerciseOfferPair(alicesOffer, bobsOffer);
}
// If fixReducedOffersV1 is enabled, then none of the test cases
// should produce a potentially blocking rate.
//
// Also verify that if fixReducedOffersV1 is not enabled then
// some of the test cases produced a potentially blocking rate.
if (features[fixReducedOffersV1])
{
BEAST_EXPECT(blockedCount == 0);
}
else
{
BEAST_EXPECT(blockedCount >= 170);
}
}
}
void
testPartialCrossOldXrpIouQChange()
{
testcase("exercise partial cross old XRP/IOU offer Q change");
using namespace jtx;
auto const gw = Account{"gateway"};
auto const alice = Account{"alice"};
auto const bob = Account{"bob"};
auto const USD = gw["USD"];
// Make one test run without fixReducedOffersV1 and one with.
for (FeatureBitset features :
{supported_amendments() - fixReducedOffersV1,
supported_amendments() | fixReducedOffersV1})
{
// Make sure none of the offers we generate are under funded.
Env env{*this, features};
env.fund(XRP(10'000'000), gw, alice, bob);
env.close();
env(trust(alice, USD(10'000'000)));
env(trust(bob, USD(10'000'000)));
env.close();
env(pay(gw, alice, USD(10'000'000)));
env.close();
// Lambda that:
// 1. Exercises one offer pair,
// 2. Collects the results, and
// 3. Cleans up for the next offer pair.
auto exerciseOfferPair =
[this, &env, &alice, &bob](
Amounts const& inLedger,
Amounts const& newOffer) -> unsigned int {
// Get the inLedger offer into the ledger so newOffer can cross
// it.
STAmount const initialRate = Quality(inLedger).rate();
std::uint32_t const aliceOfferSeq = env.seq(alice);
env(offer(alice, inLedger.in, inLedger.out));
env.close();
// Now bob's offer will partially cross alice's offer.
std::uint32_t const bobOfferSeq = env.seq(bob);
STAmount const aliceInitialBalance = env.balance(alice);
env(offer(bob, newOffer.in, newOffer.out));
env.close();
STAmount const aliceFinalBalance = env.balance(alice);
// bob's offer should not have made it into the ledger.
if (!BEAST_EXPECT(!offerInLedger(env, bob, bobOfferSeq)))
{
// If the in-ledger offer was not consumed then further
// results are meaningless.
cleanupOldOffers(
env, alice, bob, aliceOfferSeq, bobOfferSeq);
return 1;
}
// alice's offer should still be in the ledger, but reduced in
// size.
unsigned int badRate = 1;
{
Json::Value aliceOffer =
ledgerEntryOffer(env, alice, aliceOfferSeq);
STAmount const reducedTakerGets = amountFromJson(
sfTakerGets,
aliceOffer[jss::node][sfTakerGets.jsonName]);
STAmount const reducedTakerPays = amountFromJson(
sfTakerPays,
aliceOffer[jss::node][sfTakerPays.jsonName]);
STAmount const aliceGot =
env.balance(alice) - aliceInitialBalance;
BEAST_EXPECT(reducedTakerPays < inLedger.in);
BEAST_EXPECT(reducedTakerGets < inLedger.out);
STAmount const inLedgerRate =
Quality(Amounts{reducedTakerPays, reducedTakerGets})
.rate();
badRate = inLedgerRate > initialRate ? 1 : 0;
// If the inLedgerRate is less than initial rate, then
// incrementing the mantissa of the reduced taker pays
// should result in a rate higher than initial. Check
// this to verify that the largest allowable TakerPays
// was computed.
if (badRate == 0)
{
STAmount const tweakedTakerPays =
reducedTakerPays + drops(1);
STAmount const tweakedRate =
Quality(Amounts{tweakedTakerPays, reducedTakerGets})
.rate();
BEAST_EXPECT(tweakedRate > initialRate);
}
#if 0
std::cout << "Placed rate: " << initialRate
<< "; in-ledger rate: " << inLedgerRate
<< "; TakerPays: " << reducedTakerPays
<< "; TakerGets: " << reducedTakerGets
<< "; alice already got: " << aliceGot
<< std::endl;
// #else
std::string_view filler = badRate ? "**" : " ";
std::cout << "| `" << reducedTakerGets << "` | `"
<< reducedTakerPays << "` | `" << initialRate
<< "` | " << filler << "`" << inLedgerRate << "`"
<< filler << " | `" << aliceGot << "` |"
<< std::endl;
#endif
}
// In preparation for the next iteration make sure the two
// offers are gone from the ledger.
cleanupOldOffers(env, alice, bob, aliceOfferSeq, bobOfferSeq);
return badRate;
};
// alice's offer (the old offer) is the same every time:
Amounts const aliceOffer{
STAmount(XRP(1)), STAmount(USD.issue(), 1, 0)};
// bob's offer has a slightly smaller TakerPays with each iteration.
// This should mean that the size of the offer alice leaves in the
// ledger should increase with each iteration.
unsigned int blockedCount = 0;
for (std::uint64_t mantissaReduce = 1'000'000'000ull;
mantissaReduce <= 4'000'000'000ull;
mantissaReduce += 20'000'000ull)
{
STAmount bobUSD{
aliceOffer.out.issue(),
aliceOffer.out.mantissa() - mantissaReduce,
aliceOffer.out.exponent()};
STAmount bobXRP{
aliceOffer.in.issue(), aliceOffer.in.mantissa() - 1};
Amounts bobsOffer{bobUSD, bobXRP};
blockedCount += exerciseOfferPair(aliceOffer, bobsOffer);
}
// If fixReducedOffersV1 is enabled, then none of the test cases
// should produce a potentially blocking rate.
//
// Also verify that if fixReducedOffersV1 is not enabled then
// some of the test cases produced a potentially blocking rate.
if (features[fixReducedOffersV1])
{
BEAST_EXPECT(blockedCount == 0);
}
else
{
BEAST_EXPECT(blockedCount > 10);
}
}
}
void
testUnderFundedXrpIouQChange()
{
testcase("exercise underfunded XRP/IOU offer Q change");
// Bob places an offer that is not fully funded.
//
// This unit test compares the behavior of this situation before and
// after applying the fixReducedOffersV1 amendment.
using namespace jtx;
auto const alice = Account{"alice"};
auto const bob = Account{"bob"};
auto const gw = Account{"gw"};
auto const USD = gw["USD"];
// Make one test run without fixReducedOffersV1 and one with.
for (FeatureBitset features :
{supported_amendments() - fixReducedOffersV1,
supported_amendments() | fixReducedOffersV1})
{
Env env{*this, features};
env.fund(XRP(10000), alice, bob, gw);
env.close();
env.trust(USD(1000), alice, bob);
int blockedOrderBookCount = 0;
for (STAmount initialBobUSD = USD(0.45); initialBobUSD <= USD(1);
initialBobUSD += USD(0.025))
{
// underfund bob's offer
env(pay(gw, bob, initialBobUSD));
env.close();
std::uint32_t const bobOfferSeq = env.seq(bob);
env(offer(bob, drops(2), USD(1)));
env.close();
// alice places an offer that would cross bob's if bob's were
// well funded.
std::uint32_t const aliceOfferSeq = env.seq(alice);
env(offer(alice, USD(1), drops(2)));
env.close();
// We want to detect order book blocking. If:
// 1. bob's offer is still in the ledger and
// 2. alice received no USD
// then we use that as evidence that bob's offer blocked the
// order book.
{
bool const bobsOfferGone =
!offerInLedger(env, bob, bobOfferSeq);
STAmount const aliceBalanceUSD = env.balance(alice, USD);
// Sanity check the ledger if alice got USD.
if (aliceBalanceUSD.signum() > 0)
{
BEAST_EXPECT(aliceBalanceUSD == initialBobUSD);
BEAST_EXPECT(env.balance(bob, USD) == USD(0));
BEAST_EXPECT(bobsOfferGone);
}
// Track occurrences of order book blocking.
if (!bobsOfferGone && aliceBalanceUSD.signum() == 0)
{
++blockedOrderBookCount;
}
// In preparation for the next iteration clean up any
// leftover offers.
cleanupOldOffers(
env, alice, bob, aliceOfferSeq, bobOfferSeq);
// Zero out alice's and bob's USD balances.
if (STAmount const aliceBalance = env.balance(alice, USD);
aliceBalance.signum() > 0)
env(pay(alice, gw, aliceBalance));
if (STAmount const bobBalance = env.balance(bob, USD);
bobBalance.signum() > 0)
env(pay(bob, gw, bobBalance));
env.close();
}
}
// If fixReducedOffersV1 is enabled, then none of the test cases
// should produce a potentially blocking rate.
//
// Also verify that if fixReducedOffersV1 is not enabled then
// some of the test cases produced a potentially blocking rate.
if (features[fixReducedOffersV1])
{
BEAST_EXPECT(blockedOrderBookCount == 0);
}
else
{
BEAST_EXPECT(blockedOrderBookCount > 15);
}
}
}
void
testUnderFundedIouIouQChange()
{
testcase("exercise underfunded IOU/IOU offer Q change");
// Bob places an IOU/IOU offer that is not fully funded.
//
// This unit test compares the behavior of this situation before and
// after applying the fixReducedOffersV1 amendment.
using namespace jtx;
using namespace std::chrono_literals;
auto const alice = Account{"alice"};
auto const bob = Account{"bob"};
auto const gw = Account{"gw"};
auto const USD = gw["USD"];
auto const EUR = gw["EUR"];
STAmount const tinyUSD(USD.issue(), /*mantissa*/ 1, /*exponent*/ -81);
// Make one test run without fixReducedOffersV1 and one with.
for (FeatureBitset features :
{supported_amendments() - fixReducedOffersV1,
supported_amendments() | fixReducedOffersV1})
{
Env env{*this, features};
env.fund(XRP(10000), alice, bob, gw);
env.close();
env.trust(USD(1000), alice, bob);
env.trust(EUR(1000), alice, bob);
STAmount const eurOffer(
EUR.issue(), /*mantissa*/ 2957, /*exponent*/ -76);
STAmount const usdOffer(
USD.issue(), /*mantissa*/ 7109, /*exponent*/ -76);
STAmount const endLoop(
USD.issue(), /*mantissa*/ 50, /*exponent*/ -81);
int blockedOrderBookCount = 0;
for (STAmount initialBobUSD = tinyUSD; initialBobUSD <= endLoop;
initialBobUSD += tinyUSD)
{
// underfund bob's offer
env(pay(gw, bob, initialBobUSD));
env(pay(gw, alice, EUR(100)));
env.close();
// This offer is underfunded
std::uint32_t bobOfferSeq = env.seq(bob);
env(offer(bob, eurOffer, usdOffer));
env.close();
env.require(offers(bob, 1));
// alice places an offer that crosses bob's.
std::uint32_t aliceOfferSeq = env.seq(alice);
env(offer(alice, usdOffer, eurOffer));
env.close();
// Examine the aftermath of alice's offer.
{
bool const bobsOfferGone =
!offerInLedger(env, bob, bobOfferSeq);
STAmount aliceBalanceUSD = env.balance(alice, USD);
#if 0
std::cout
<< "bobs initial: " << initialBobUSD
<< "; alice final: " << aliceBalanceUSD
<< "; bobs offer: " << bobsOfferJson.toStyledString()
<< std::endl;
#endif
// Sanity check the ledger if alice got USD.
if (aliceBalanceUSD.signum() > 0)
{
BEAST_EXPECT(aliceBalanceUSD == initialBobUSD);
BEAST_EXPECT(env.balance(bob, USD) == USD(0));
BEAST_EXPECT(bobsOfferGone);
}
// Track occurrences of order book blocking.
if (!bobsOfferGone && aliceBalanceUSD.signum() == 0)
{
++blockedOrderBookCount;
}
}
// In preparation for the next iteration clean up any
// leftover offers.
cleanupOldOffers(env, alice, bob, aliceOfferSeq, bobOfferSeq);
// Zero out alice's and bob's IOU balances.
auto zeroBalance = [&env, &gw](
Account const& acct, IOU const& iou) {
if (STAmount const balance = env.balance(acct, iou);
balance.signum() > 0)
env(pay(acct, gw, balance));
};
zeroBalance(alice, EUR);
zeroBalance(alice, USD);
zeroBalance(bob, EUR);
zeroBalance(bob, USD);
env.close();
}
// If fixReducedOffersV1 is enabled, then none of the test cases
// should produce a potentially blocking rate.
//
// Also verify that if fixReducedOffersV1 is not enabled then
// some of the test cases produced a potentially blocking rate.
if (features[fixReducedOffersV1])
{
BEAST_EXPECT(blockedOrderBookCount == 0);
}
else
{
BEAST_EXPECT(blockedOrderBookCount > 20);
}
}
}
void
run() override
{
testPartialCrossNewXrpIouQChange();
testPartialCrossOldXrpIouQChange();
testUnderFundedXrpIouQChange();
testUnderFundedIouIouQChange();
}
};
BEAST_DEFINE_TESTSUITE_PRIO(ReducedOffer, tx, ripple, 2);
} // namespace test
} // namespace ripple

View File

@@ -80,6 +80,9 @@ private:
case asfDepositAuth:
mask_ |= lsfDepositAuth;
break;
case asfAllowClawback:
mask_ |= lsfAllowClawback;
break;
default:
Throw<std::runtime_error>("unknown flag");
}

View File

@@ -49,6 +49,9 @@ setupConfigForUnitTests(Config& cfg)
cfg.FEES.account_reserve = XRP(200).value().xrp().drops();
cfg.FEES.owner_reserve = XRP(50).value().xrp().drops();
// The Beta API (currently v2) is always available to tests
cfg.BETA_RPC_API = true;
cfg.overwrite(ConfigSection::nodeDatabase(), "type", "memory");
cfg.overwrite(ConfigSection::nodeDatabase(), "path", "main");
cfg.deprecatedClearSection(ConfigSection::importNodeDatabase());

View File

@@ -59,6 +59,17 @@ trust(
return jv;
}
Json::Value
claw(Account const& account, STAmount const& amount)
{
Json::Value jv;
jv[jss::Account] = account.human();
jv[jss::Amount] = amount.getJson(JsonOptions::none);
jv[jss::TransactionType] = jss::Clawback;
return jv;
}
} // namespace jtx
} // namespace test
} // namespace ripple

View File

@@ -40,6 +40,9 @@ trust(
Account const& peer,
std::uint32_t flags);
Json::Value
claw(Account const& account, STAmount const& amount);
} // namespace jtx
} // namespace test
} // namespace ripple

View File

@@ -206,10 +206,7 @@ public:
testSignerListsApiVersion2()
{
using namespace jtx;
Env env{*this, envconfig([](std::unique_ptr<Config> c) {
c->loadFromString("\n[beta_rpc_api]\n1\n");
return c;
})};
Env env{*this};
Account const alice{"alice"};
env.fund(XRP(1000), alice);

View File

@@ -93,6 +93,12 @@ public:
// and are tested elsewhere
continue;
}
if (flag == asfAllowClawback)
{
// The asfAllowClawback flag can't be cleared. It is tested
// elsewhere.
continue;
}
if (std::find(goodFlags.begin(), goodFlags.end(), flag) !=
goodFlags.end())

View File

@@ -17,6 +17,7 @@
*/
//==============================================================================
#include <ripple/app/misc/NetworkOPs.h>
#include <ripple/beast/unit_test.h>
#include <ripple/protocol/jss.h>
#include <test/jtx.h>
@@ -55,6 +56,16 @@ public:
[validators]
%2%
[port_grpc]
ip = 0.0.0.0
port = 50051
[port_admin]
ip = 0.0.0.0
port = 50052
protocol = wss2
admin = 127.0.0.1
)rippleConfig");
p->loadFromString(boost::str(
@@ -77,8 +88,30 @@ public:
BEAST_EXPECT(result[jss::result][jss::status] == "success");
BEAST_EXPECT(result[jss::result].isMember(jss::info));
}
{
Env env(*this, makeValidatorConfig());
Env env(*this);
// Call NetworkOPs directly and set the admin flag to false.
// Expect that the admin ports are not included in the result.
auto const result =
env.app().getOPs().getServerInfo(true, false, 0);
auto const& ports = result[jss::ports];
BEAST_EXPECT(ports.isArray() && ports.size() == 0);
}
{
auto config = makeValidatorConfig();
auto const rpc_port =
(*config)["port_rpc"].get<unsigned int>("port");
auto const grpc_port =
(*config)["port_grpc"].get<unsigned int>("port");
auto const ws_port = (*config)["port_ws"].get<unsigned int>("port");
BEAST_EXPECT(grpc_port);
BEAST_EXPECT(rpc_port);
BEAST_EXPECT(ws_port);
Env env(*this, std::move(config));
auto const result = env.rpc("server_info");
BEAST_EXPECT(!result[jss::result].isMember(jss::error));
BEAST_EXPECT(result[jss::result][jss::status] == "success");
@@ -86,6 +119,32 @@ public:
BEAST_EXPECT(
result[jss::result][jss::info][jss::pubkey_validator] ==
validator_data::public_key);
auto const& ports = result[jss::result][jss::info][jss::ports];
BEAST_EXPECT(ports.isArray() && ports.size() == 3);
for (auto const& port : ports)
{
auto const& proto = port[jss::protocol];
BEAST_EXPECT(proto.isArray());
auto const p = port[jss::port].asUInt();
BEAST_EXPECT(p == rpc_port || p == ws_port || p == grpc_port);
if (p == grpc_port)
{
BEAST_EXPECT(proto.size() == 1);
BEAST_EXPECT(proto[0u].asString() == "grpc");
}
if (p == rpc_port)
{
BEAST_EXPECT(proto.size() == 2);
BEAST_EXPECT(proto[0u].asString() == "http");
BEAST_EXPECT(proto[1u].asString() == "ws2");
}
if (p == ws_port)
{
BEAST_EXPECT(proto.size() == 1);
BEAST_EXPECT(proto[0u].asString() == "ws");
}
}
}
}

View File

@@ -76,11 +76,16 @@ class Version_test : public beast::unit_test::suite
std::to_string(RPC::apiMinimumSupportedVersion - 1) + "}");
BEAST_EXPECT(badVersion(re));
BEAST_EXPECT(env.app().config().BETA_RPC_API);
re = env.rpc(
"json",
"version",
"{\"api_version\": " +
std::to_string(RPC::apiMaximumSupportedVersion + 1) + "}");
std::to_string(
std::max(
RPC::apiMaximumSupportedVersion, RPC::apiBetaVersion) +
1) +
"}");
BEAST_EXPECT(badVersion(re));
re = env.rpc("json", "version", "{\"api_version\": \"a\"}");
@@ -190,20 +195,25 @@ class Version_test : public beast::unit_test::suite
using namespace test::jtx;
Env env{*this};
BEAST_EXPECT(env.app().config().BETA_RPC_API);
auto const without_api_verion = std::string("{ ") +
"\"jsonrpc\": \"2.0\", "
"\"ripplerpc\": \"2.0\", "
"\"id\": 5, "
"\"method\": \"version\", "
"\"params\": {}}";
auto const with_wrong_api_verion = std::string("{ ") +
auto const with_wrong_api_verion =
std::string("{ ") +
"\"jsonrpc\": \"2.0\", "
"\"ripplerpc\": \"2.0\", "
"\"id\": 6, "
"\"method\": \"version\", "
"\"params\": { "
"\"api_version\": " +
std::to_string(RPC::apiMaximumSupportedVersion + 1) + "}}";
std::to_string(
std::max(RPC::apiMaximumSupportedVersion, RPC::apiBetaVersion) +
1) +
"}}";
auto re = env.rpc(
"json2",
'[' + without_api_verion + ", " + with_wrong_api_verion + ']');