mirror of
https://github.com/XRPLF/rippled.git
synced 2026-09-27 15:28:03 +00:00
feat: Handle Smart Escrow's Bytecode, Data and Gas fields
Add the transaction handling for `EscrowCreate.Bytecode`/`Data` and `EscrowFinish.Gas`, gated on `featureSmartEscrow`. The field and result-code plumbing landed in #8157. EscrowCreate: * `Bytecode` joins `FinishAfter` and `Condition` as a way to say how an escrow completes, but always needs a `CancelAfter`: nothing else can free the funds if the contract never accepts. * `Data` requires `Bytecode` and is capped at `kMaxWasmDataLength`. * Bytecode is capped at the voted `BytecodeSizeLimit`, and refused outright when fee voting has zeroed `GasLimit` or `BytecodeSizeLimit`. * The fee is ten base fees plus five drops per byte. * The owner reserve is one increment per 500 bytes beyond the first 500, on top of the increment every escrow costs. EscrowCancel and EscrowFinish refund what was taken. EscrowFinish: * `Gas` is bounded by the voted `GasLimit` and costs `GasPrice` micro-drops each. * `Gas` and the escrow's `Bytecode` must both be present or both absent (tefBYTECODE_NOT_INCLUDED / tefNO_BYTECODE). * The destination and deposit-preauth checks move ahead of the condition check, so a contract never runs against a destination that cannot receive. The WASM engine is not in this build, so nothing runs the bytecode: creating, funding and cancelling a Smart Escrow works, and finishing one reports tecFAILED_PROCESSING at the TODO where the contract would run. The gas and return-code metadata, tecBYTECODE_REJECTED, and the `Data` a rejected contract leaves behind all arrive with the engine. Reachable only under `featureSmartEscrow`, which is `Supported::No`.
This commit is contained in:
@@ -25,6 +25,8 @@
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
template <ValidIssueType T>
|
||||
@@ -272,4 +274,19 @@ escrowUnlockApplyHelper<MPTIssue>(
|
||||
journal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Owner count an escrow costs.
|
||||
*
|
||||
* An escrow carrying bytecode costs one increment per 500 bytes beyond the
|
||||
* first 500, on top of the single increment every escrow costs.
|
||||
*/
|
||||
template <class T>
|
||||
int32_t
|
||||
calculateAdditionalReserve(T const& finishFunction)
|
||||
{
|
||||
if (!finishFunction)
|
||||
return 1;
|
||||
return 1 + (finishFunction->size() / 500);
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -376,6 +376,11 @@ constexpr std::uint32_t kMaxInvestmentPeriod = std::chrono::seconds{std::chrono:
|
||||
*/
|
||||
constexpr std::uint8_t kMaxAssetCheckDepth = 5;
|
||||
|
||||
/**
|
||||
* Maximum length of a Data field in Escrow object that can be updated by WASM code.
|
||||
*/
|
||||
constexpr std::size_t kMaxWasmDataLength = 1 * 1024; // 1KB
|
||||
|
||||
/**
|
||||
* A ledger index.
|
||||
*/
|
||||
|
||||
@@ -20,11 +20,14 @@ public:
|
||||
{
|
||||
}
|
||||
|
||||
static bool
|
||||
checkExtraFeatures(PreflightContext const& ctx);
|
||||
|
||||
static TxConsequences
|
||||
makeTxConsequences(PreflightContext const& ctx);
|
||||
|
||||
static bool
|
||||
checkExtraFeatures(PreflightContext const& ctx);
|
||||
static XRPAmount
|
||||
calculateBaseFee(ReadView const& view, STTx const& tx);
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
@@ -169,11 +169,13 @@ EscrowCancel::doApply()
|
||||
auto const sle = ctx_.view().peek(keylet::account(account));
|
||||
STAmount const amount = slep->getFieldAmount(sfAmount);
|
||||
|
||||
auto const reserveToSubtract = calculateAdditionalReserve((*slep)[~sfBytecode]);
|
||||
|
||||
// The return can re-create a holding the owner deleted while the escrow
|
||||
// was pending; the removed escrow must not be counted against its reserve.
|
||||
bool const recycleReserve = ctx_.view().rules().enabled(fixCleanup3_4_0);
|
||||
if (recycleReserve)
|
||||
decreaseOwnerCountForObject(ctx_.view(), sle, slep, 1, ctx_.journal);
|
||||
decreaseOwnerCountForObject(ctx_.view(), sle, slep, reserveToSubtract, ctx_.journal);
|
||||
|
||||
// Transfer amount back to the owner
|
||||
if (isXRP(amount))
|
||||
@@ -219,7 +221,7 @@ EscrowCancel::doApply()
|
||||
}
|
||||
|
||||
if (!recycleReserve)
|
||||
decreaseOwnerCountForObject(ctx_.view(), sle, slep, 1, ctx_.journal);
|
||||
decreaseOwnerCountForObject(ctx_.view(), sle, slep, reserveToSubtract, ctx_.journal);
|
||||
|
||||
// Remove escrow from ledger
|
||||
ctx_.view().erase(slep);
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
#include <xrpl/conditions/Condition.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
|
||||
#include <xrpl/ledger/helpers/EscrowHelpers.h>
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
|
||||
#include <xrpl/ledger/helpers/SponsorHelpers.h>
|
||||
@@ -129,6 +131,19 @@ escrowCreatePreflightHelper<MPTIssue>(PreflightContext const& ctx)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
XRPAmount
|
||||
EscrowCreate::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
XRPAmount txnFees{Transactor::calculateBaseFee(view, tx)};
|
||||
if (tx.isFieldPresent(sfBytecode))
|
||||
{
|
||||
// 10 base fees for the transaction (1 is in
|
||||
// `Transactor::calculateBaseFee`), plus 5 drops per byte
|
||||
txnFees += 9 * view.fees().base + 5 * tx[sfBytecode].size();
|
||||
}
|
||||
return txnFees;
|
||||
}
|
||||
|
||||
NotTEC
|
||||
EscrowCreate::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
@@ -160,12 +175,19 @@ EscrowCreate::preflight(PreflightContext const& ctx)
|
||||
ctx.tx[sfCancelAfter] <= ctx.tx[sfFinishAfter])
|
||||
return temBAD_EXPIRATION;
|
||||
|
||||
if (ctx.tx.isFieldPresent(sfBytecode) && !ctx.tx.isFieldPresent(sfCancelAfter))
|
||||
return temBAD_EXPIRATION;
|
||||
|
||||
// In the absence of a FinishAfter, the escrow can be finished
|
||||
// immediately, which can be confusing. When creating an escrow,
|
||||
// we want to ensure that either a FinishAfter time is explicitly
|
||||
// specified or a completion condition is attached.
|
||||
if (!ctx.tx[~sfFinishAfter] && !ctx.tx[~sfCondition])
|
||||
if (!ctx.tx[~sfFinishAfter] && !ctx.tx[~sfCondition] && !ctx.tx[~sfBytecode])
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "Must have at least one of FinishAfter, "
|
||||
"Condition, or Bytecode.";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
if (auto const cb = ctx.tx[~sfCondition])
|
||||
{
|
||||
@@ -181,6 +203,41 @@ EscrowCreate::preflight(PreflightContext const& ctx)
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.tx.isFieldPresent(sfData))
|
||||
{
|
||||
if (!ctx.tx.isFieldPresent(sfBytecode))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "EscrowCreate with Data requires Bytecode";
|
||||
return temMALFORMED;
|
||||
}
|
||||
auto const data = ctx.tx.getFieldVL(sfData);
|
||||
if (data.size() > kMaxWasmDataLength)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "EscrowCreate.Data bad size " << data.size();
|
||||
return temMALFORMED;
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.tx.isFieldPresent(sfBytecode))
|
||||
{
|
||||
auto const fees(ctx.registry.get().getFees());
|
||||
if (fees.bytecodeSizeLimit == 0 || fees.gasLimit == 0)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "WASM runtime deactivated by fee voting";
|
||||
return temTEMP_DISABLED;
|
||||
}
|
||||
|
||||
auto const code = ctx.tx.getFieldVL(sfBytecode);
|
||||
if (code.empty() || code.size() > fees.bytecodeSizeLimit)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "EscrowCreate.Bytecode bad size " << code.size();
|
||||
return temMALFORMED;
|
||||
}
|
||||
// TODO(SmartEscrow): screen the module itself here - that it compiles,
|
||||
// and that it imports and exports what the engine can serve - once the
|
||||
// WASM engine lands. Until then any blob of the right size is taken.
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -439,6 +496,7 @@ EscrowCreate::doApply()
|
||||
|
||||
// Check reserve and funds availability
|
||||
STAmount const amount{ctx_.tx[sfAmount]};
|
||||
auto const reserveToAdd = calculateAdditionalReserve(ctx_.tx[~sfBytecode]);
|
||||
|
||||
auto const balance = sle->getFieldAmount(sfBalance).xrp();
|
||||
// First check: whoever is on the hook for the new owner increment
|
||||
@@ -446,8 +504,8 @@ EscrowCreate::doApply()
|
||||
// validates the sponsor's reserve + remaining credit. When
|
||||
// unsponsored this hits the source branch and validates the
|
||||
// source's pre-lock balance against base + (currentOC+1)*increment.
|
||||
if (auto const ret =
|
||||
checkReserve(ctx_.getApplyViewContext(), sle, balance, {.ownerCountDelta = 1}, j_);
|
||||
if (auto const ret = checkReserve(
|
||||
ctx_.getApplyViewContext(), sle, balance, {.ownerCountDelta = reserveToAdd}, j_);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
@@ -491,6 +549,8 @@ EscrowCreate::doApply()
|
||||
(*slep)[~sfCancelAfter] = ctx_.tx[~sfCancelAfter];
|
||||
(*slep)[~sfFinishAfter] = ctx_.tx[~sfFinishAfter];
|
||||
(*slep)[~sfDestinationTag] = ctx_.tx[~sfDestinationTag];
|
||||
(*slep)[~sfBytecode] = ctx_.tx[~sfBytecode];
|
||||
(*slep)[~sfData] = ctx_.tx[~sfData];
|
||||
|
||||
if (ctx_.view().rules().enabled(fixIncludeKeyletFields))
|
||||
{
|
||||
@@ -558,7 +618,7 @@ EscrowCreate::doApply()
|
||||
}
|
||||
|
||||
// increment owner count
|
||||
increaseOwnerCount(ctx_.getApplyViewContext(), sle, 1, ctx_.journal);
|
||||
increaseOwnerCount(ctx_.getApplyViewContext(), sle, reserveToAdd, ctx_.journal);
|
||||
addSponsorToLedgerEntry(ctx_.getApplyViewContext(), slep);
|
||||
ctx_.view().update(sle);
|
||||
return tesSUCCESS;
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Concepts.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Fees.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
@@ -31,6 +32,8 @@
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <system_error>
|
||||
#include <variant>
|
||||
|
||||
@@ -83,7 +86,29 @@ EscrowFinish::preflight(PreflightContext const& ctx)
|
||||
// If you specify a condition, then you must also specify
|
||||
// a fulfillment.
|
||||
if (static_cast<bool>(cb) != static_cast<bool>(fb))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "Condition != Fulfillment";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
if (auto const allowance = ctx.tx[~sfGas]; allowance)
|
||||
{
|
||||
auto const fees(ctx.registry.get().getFees());
|
||||
if (fees.gasLimit == 0)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "WASM runtime deactivated by fee voting";
|
||||
return temTEMP_DISABLED;
|
||||
}
|
||||
if (*allowance == 0)
|
||||
{
|
||||
return temBAD_LIMIT;
|
||||
}
|
||||
if (*allowance > fees.gasLimit)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "Gas too large: " << *allowance;
|
||||
return temBAD_LIMIT;
|
||||
}
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
@@ -132,7 +157,15 @@ EscrowFinish::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
extraFee += view.fees().base * (32 + (fb->size() / 16));
|
||||
}
|
||||
|
||||
if (std::optional<uint64_t> const allowance = tx[~sfGas]; allowance)
|
||||
{
|
||||
// The extra fee is the allowance in drops, rounded up to the nearest
|
||||
// whole drop.
|
||||
// Integer math rounds down by default, so we add 1 to round up.
|
||||
uint64_t const allowanceFee =
|
||||
(((*allowance) * view.fees().gasPrice) / microDropsPerDrop) + 1;
|
||||
extraFee += allowanceFee;
|
||||
}
|
||||
return Transactor::calculateBaseFee(view, tx) + extraFee;
|
||||
}
|
||||
|
||||
@@ -208,26 +241,51 @@ EscrowFinish::preclaim(PreclaimContext const& ctx)
|
||||
return err;
|
||||
}
|
||||
|
||||
if (ctx.view.rules().enabled(featureTokenEscrow))
|
||||
if (ctx.view.rules().enabled(featureTokenEscrow) ||
|
||||
ctx.view.rules().enabled(featureSmartEscrow))
|
||||
{
|
||||
// this check is done in doApply before this amendment is enabled
|
||||
auto const seqProxy = SeqProxy::rawSequence(ctx.tx[sfOfferSequence]);
|
||||
auto const k = keylet::escrow(ctx.tx[sfOwner], seqProxy);
|
||||
auto const slep = ctx.view.read(k);
|
||||
if (!slep)
|
||||
return tecNO_TARGET;
|
||||
|
||||
AccountID const dest = (*slep)[sfDestination];
|
||||
STAmount const amount = (*slep)[sfAmount];
|
||||
|
||||
if (!isXRP(amount))
|
||||
if (ctx.view.rules().enabled(featureSmartEscrow))
|
||||
{
|
||||
if (auto const ret = std::visit(
|
||||
[&]<typename T>(T const&) {
|
||||
return escrowFinishPreclaimHelper<T>(ctx, dest, amount);
|
||||
},
|
||||
amount.asset().value());
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
if (slep->isFieldPresent(sfBytecode))
|
||||
{
|
||||
if (!ctx.tx.isFieldPresent(sfGas))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "Bytecode requires Gas";
|
||||
return tefBYTECODE_NOT_INCLUDED;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ctx.tx.isFieldPresent(sfGas))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "Bytecode not present, "
|
||||
"Gas present";
|
||||
return tefNO_BYTECODE;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ctx.view.rules().enabled(featureTokenEscrow))
|
||||
{
|
||||
AccountID const dest = (*slep)[sfDestination];
|
||||
STAmount const amount = (*slep)[sfAmount];
|
||||
|
||||
if (!isXRP(amount))
|
||||
{
|
||||
if (auto const ret = std::visit(
|
||||
[&]<typename T>(T const&) {
|
||||
return escrowFinishPreclaimHelper<T>(ctx, dest, amount);
|
||||
},
|
||||
amount.asset().value());
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
return tesSUCCESS;
|
||||
@@ -241,7 +299,8 @@ EscrowFinish::doApply()
|
||||
auto const slep = ctx_.view().peek(k);
|
||||
if (!slep)
|
||||
{
|
||||
if (ctx_.view().rules().enabled(featureTokenEscrow))
|
||||
if (ctx_.view().rules().enabled(featureTokenEscrow) ||
|
||||
ctx_.view().rules().enabled(featureSmartEscrow))
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
return tecNO_TARGET;
|
||||
@@ -259,6 +318,20 @@ EscrowFinish::doApply()
|
||||
if ((*slep)[~sfCancelAfter] && after(now, (*slep)[sfCancelAfter]))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
AccountID const destID = (*slep)[sfDestination];
|
||||
auto const sled = ctx_.view().peek(keylet::account(destID));
|
||||
if (ctx_.view().rules().enabled(featureSmartEscrow))
|
||||
{
|
||||
// NOTE: Escrow payments cannot be used to fund accounts.
|
||||
if (!sled)
|
||||
return tecNO_DST;
|
||||
|
||||
if (auto err =
|
||||
verifyDepositPreauth(ctx_.tx, ctx_.view(), accountID_, destID, sled, ctx_.journal);
|
||||
!isTesSuccess(err))
|
||||
return err;
|
||||
}
|
||||
|
||||
// Check cryptocondition fulfillment
|
||||
{
|
||||
auto const id = ctx_.tx.getTransactionID();
|
||||
@@ -312,16 +385,33 @@ EscrowFinish::doApply()
|
||||
return tecCRYPTOCONDITION_ERROR;
|
||||
}
|
||||
|
||||
// NOTE: Escrow payments cannot be used to fund accounts.
|
||||
AccountID const destID = (*slep)[sfDestination];
|
||||
auto const sled = ctx_.view().peek(keylet::account(destID));
|
||||
if (!sled)
|
||||
return tecNO_DST;
|
||||
if (!ctx_.view().rules().enabled(featureSmartEscrow))
|
||||
{
|
||||
// NOTE: Escrow payments cannot be used to fund accounts.
|
||||
if (!sled)
|
||||
return tecNO_DST;
|
||||
|
||||
if (auto err =
|
||||
verifyDepositPreauth(ctx_.tx, ctx_.view(), accountID_, destID, sled, ctx_.journal);
|
||||
!isTesSuccess(err))
|
||||
return err;
|
||||
if (auto err =
|
||||
verifyDepositPreauth(ctx_.tx, ctx_.view(), accountID_, destID, sled, ctx_.journal);
|
||||
!isTesSuccess(err))
|
||||
return err;
|
||||
}
|
||||
|
||||
// Execute custom release function
|
||||
if ((*slep)[~sfBytecode])
|
||||
{
|
||||
// TODO(SmartEscrow): run the escrow's contract here once the WASM
|
||||
// engine lands. That change also brings with it the reporting the run
|
||||
// produces - the gas it consumed and the code it returned, in the
|
||||
// transaction metadata - and the tecBYTECODE_REJECTED result that lets
|
||||
// a rejected escrow survive with the data the contract wrote.
|
||||
//
|
||||
// Until then an escrow carrying bytecode can be created, funded and
|
||||
// cancelled, but not finished. Reachable only under featureSmartEscrow,
|
||||
// which is Supported::No.
|
||||
JLOG(j_.debug()) << "EscrowFinish: no WASM engine to run the escrow's bytecode";
|
||||
return tecFAILED_PROCESSING;
|
||||
}
|
||||
|
||||
AccountID const account = (*slep)[sfAccount];
|
||||
|
||||
@@ -349,13 +439,15 @@ EscrowFinish::doApply()
|
||||
}
|
||||
}
|
||||
|
||||
auto const reserveToSubtract = calculateAdditionalReserve((*slep)[~sfBytecode]);
|
||||
|
||||
// Delivery can auto-create the destination's holding; the removed escrow
|
||||
// must not be counted against its reserve. The two share a reserve payer
|
||||
// for a self-escrow, or when one sponsor covers both.
|
||||
bool const recycleReserve =
|
||||
ctx_.view().rules().enabled(featureSponsor) || ctx_.view().rules().enabled(fixCleanup3_4_0);
|
||||
if (recycleReserve)
|
||||
decreaseOwnerCountForObject(ctx_.view(), account, slep, 1, ctx_.journal);
|
||||
decreaseOwnerCountForObject(ctx_.view(), account, slep, reserveToSubtract, ctx_.journal);
|
||||
|
||||
STAmount const amount = slep->getFieldAmount(sfAmount);
|
||||
// Transfer amount to destination
|
||||
@@ -407,7 +499,7 @@ EscrowFinish::doApply()
|
||||
ctx_.view().update(sled);
|
||||
|
||||
if (!recycleReserve)
|
||||
decreaseOwnerCountForObject(ctx_.view(), account, slep, 1, ctx_.journal);
|
||||
decreaseOwnerCountForObject(ctx_.view(), account, slep, reserveToSubtract, ctx_.journal);
|
||||
|
||||
// Remove escrow from ledger
|
||||
ctx_.view().erase(slep);
|
||||
|
||||
704
src/test/app/EscrowSmart_test.cpp
Normal file
704
src/test/app/EscrowSmart_test.cpp
Normal file
@@ -0,0 +1,704 @@
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/balance.h>
|
||||
#include <test/jtx/envconfig.h>
|
||||
#include <test/jtx/escrow.h>
|
||||
#include <test/jtx/fee.h>
|
||||
#include <test/jtx/noop.h>
|
||||
#include <test/jtx/ter.h>
|
||||
|
||||
#include <xrpld/core/Config.h>
|
||||
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/core/StartUpType.h>
|
||||
#include <xrpl/ledger/OpenView.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Fees.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/SeqProxy.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
/**
|
||||
* Smart Escrow's `Bytecode`, `Data` and `Gas` fields.
|
||||
*
|
||||
* The WASM engine that runs an escrow's bytecode is not in this build, so the
|
||||
* `Bytecode` field is opaque here: `EscrowCreate` weighs its size and nothing
|
||||
* reads it, and `EscrowFinish` has nothing to run it with. Everything around
|
||||
* the run is testable, and is tested below - the preflight and preclaim rules,
|
||||
* the fee the bytecode costs, the owner reserve it occupies, and the refund of
|
||||
* that reserve.
|
||||
*
|
||||
* What waits on the engine: a successful finish, tecBYTECODE_REJECTED, the
|
||||
* gas and return code the run reports in the transaction metadata, and the
|
||||
* `Data` a rejected contract leaves behind.
|
||||
*/
|
||||
struct EscrowSmart_test : public beast::unit_test::Suite
|
||||
{
|
||||
// A blob of `bytes` bytes, as hex. Opens with the WASM preamble - "\0asm"
|
||||
// and version 1 - so a reader can see what the field is meant to carry,
|
||||
// but nothing in this build parses it.
|
||||
static std::string
|
||||
bytecodeOfSize(std::size_t bytes)
|
||||
{
|
||||
static std::string const preamble = "0061736D01000000";
|
||||
std::string hex = preamble;
|
||||
hex.append(bytes * 2 - preamble.size(), 'A');
|
||||
return hex;
|
||||
}
|
||||
|
||||
// What EscrowCreate charges for `bytecodeHex`: ten base fees, plus five
|
||||
// drops per byte.
|
||||
static XRPAmount
|
||||
createFeeFor(jtx::Env const& env, std::string const& bytecodeHex)
|
||||
{
|
||||
return env.current()->fees().base * 10 + bytecodeHex.size() / 2 * 5;
|
||||
}
|
||||
|
||||
// What EscrowFinish charges for `gas`.
|
||||
static XRPAmount
|
||||
finishFeeFor(jtx::Env const& env, std::uint64_t gas)
|
||||
{
|
||||
return env.current()->fees().base +
|
||||
(gas * env.current()->fees().gasPrice) / microDropsPerDrop + 1;
|
||||
}
|
||||
|
||||
void
|
||||
testCreatePreflight(FeatureBitset features)
|
||||
{
|
||||
testcase("EscrowCreate preflight");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace std::chrono;
|
||||
|
||||
Account const alice{"alice"};
|
||||
Account const carol{"carol"};
|
||||
|
||||
auto const bytecode = bytecodeOfSize(64);
|
||||
|
||||
{
|
||||
// featureSmartEscrow disabled
|
||||
Env env(*this, features - featureSmartEscrow);
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
XRPAmount const txnFees = env.current()->fees().base + 1000;
|
||||
auto const escrowCreate = escrow::create(alice, carol, XRP(1000));
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 100s),
|
||||
Fee(txnFees),
|
||||
Ter(temDISABLED));
|
||||
env.close();
|
||||
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 100s),
|
||||
escrow::Data("00112233"),
|
||||
Fee(txnFees),
|
||||
Ter(temDISABLED));
|
||||
env.close();
|
||||
}
|
||||
|
||||
{
|
||||
// Bytecode > max length
|
||||
Env env(
|
||||
*this,
|
||||
envconfig([](std::unique_ptr<Config> cfg) {
|
||||
cfg->fees.bytecodeSizeLimit = 10; // 10 bytes
|
||||
return cfg;
|
||||
}),
|
||||
features);
|
||||
XRPAmount const txnFees = env.current()->fees().base + 1000;
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
|
||||
auto const escrowCreate = escrow::create(alice, carol, XRP(500));
|
||||
|
||||
// 11-byte string
|
||||
std::string const longBytecode = "00112233445566778899AA";
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(longBytecode),
|
||||
escrow::kCancelTime(env.now() + 100s),
|
||||
Fee(txnFees),
|
||||
Ter(temMALFORMED));
|
||||
env.close();
|
||||
}
|
||||
|
||||
{
|
||||
// gas limit set to 0
|
||||
Env env(
|
||||
*this,
|
||||
envconfig([](std::unique_ptr<Config> cfg) {
|
||||
// WASM runtime disabled
|
||||
cfg->fees.gasLimit = 0;
|
||||
return cfg;
|
||||
}),
|
||||
features);
|
||||
XRPAmount const txnFees = env.current()->fees().base + 1000;
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
|
||||
auto const escrowCreate = escrow::create(alice, carol, XRP(500));
|
||||
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 100s),
|
||||
Fee(txnFees),
|
||||
Ter(temTEMP_DISABLED));
|
||||
env.close();
|
||||
}
|
||||
|
||||
{
|
||||
// size limit set to 0
|
||||
Env env(
|
||||
*this,
|
||||
envconfig([](std::unique_ptr<Config> cfg) {
|
||||
cfg->fees.bytecodeSizeLimit = 0; // WASM upload disabled
|
||||
return cfg;
|
||||
}),
|
||||
features);
|
||||
XRPAmount const txnFees = env.current()->fees().base + 1000;
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
|
||||
auto const escrowCreate = escrow::create(alice, carol, XRP(500));
|
||||
|
||||
// 1-byte string
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode("AA"),
|
||||
escrow::kCancelTime(env.now() + 100s),
|
||||
Fee(txnFees),
|
||||
Ter(temTEMP_DISABLED));
|
||||
env.close();
|
||||
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 100s),
|
||||
Fee(txnFees),
|
||||
Ter(temTEMP_DISABLED));
|
||||
env.close();
|
||||
}
|
||||
|
||||
{
|
||||
// Data without Bytecode
|
||||
Env env(*this, features);
|
||||
XRPAmount const txnFees = env.current()->fees().base + 100000;
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
|
||||
auto const escrowCreate = escrow::create(alice, carol, XRP(500));
|
||||
|
||||
std::string const data(4, 'A');
|
||||
env(escrowCreate,
|
||||
escrow::Data(data),
|
||||
escrow::kFinishTime(env.now() + 100s),
|
||||
Fee(txnFees),
|
||||
Ter(temMALFORMED));
|
||||
env.close();
|
||||
}
|
||||
|
||||
{
|
||||
// Data > max length
|
||||
Env env(*this, features);
|
||||
XRPAmount const txnFees = env.current()->fees().base + 100000;
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
|
||||
auto const escrowCreate = escrow::create(alice, carol, XRP(500));
|
||||
|
||||
// string of length (kMaxWasmDataLength + 1) * 2
|
||||
std::string const longData((kMaxWasmDataLength + 1) * 2, 'B');
|
||||
env(escrowCreate,
|
||||
escrow::Data(longData),
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 100s),
|
||||
Fee(txnFees),
|
||||
Ter(temMALFORMED));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// Bytecode joins FinishAfter and Condition as a way to say how the
|
||||
// escrow completes, but it always needs a CancelAfter: nothing else
|
||||
// can free the funds if the contract never accepts.
|
||||
Env env(
|
||||
*this,
|
||||
envconfig([](std::unique_ptr<Config> cfg) {
|
||||
cfg->startUp = StartUpType::Fresh;
|
||||
return cfg;
|
||||
}),
|
||||
features);
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
|
||||
auto const escrowCreate = escrow::create(alice, carol, XRP(500));
|
||||
XRPAmount const txnFees = createFeeFor(env, bytecode);
|
||||
|
||||
// Success situations
|
||||
{
|
||||
// Bytecode + CancelAfter
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 20s),
|
||||
Fee(txnFees));
|
||||
env.close();
|
||||
}
|
||||
{
|
||||
// Bytecode + Condition + CancelAfter
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 30s),
|
||||
escrow::kCondition(escrow::kCb1),
|
||||
Fee(txnFees));
|
||||
env.close();
|
||||
}
|
||||
{
|
||||
// Bytecode + FinishAfter + CancelAfter
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 40s),
|
||||
escrow::kFinishTime(env.now() + 2s),
|
||||
Fee(txnFees));
|
||||
env.close();
|
||||
}
|
||||
{
|
||||
// Bytecode + Data + CancelAfter
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::Data("00112233"),
|
||||
escrow::kCancelTime(env.now() + 50s),
|
||||
Fee(txnFees));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// Failure situations (i.e. all other combinations)
|
||||
{
|
||||
// only Bytecode
|
||||
env(escrowCreate, escrow::Bytecode(bytecode), Fee(txnFees), Ter(temBAD_EXPIRATION));
|
||||
env.close();
|
||||
}
|
||||
{
|
||||
// Bytecode + FinishAfter
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kFinishTime(env.now() + 2s),
|
||||
Fee(txnFees),
|
||||
Ter(temBAD_EXPIRATION));
|
||||
env.close();
|
||||
}
|
||||
{
|
||||
// Bytecode + Condition
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCondition(escrow::kCb1),
|
||||
Fee(txnFees),
|
||||
Ter(temBAD_EXPIRATION));
|
||||
env.close();
|
||||
}
|
||||
{
|
||||
// Bytecode 0 length
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(""),
|
||||
escrow::kCancelTime(env.now() + 60s),
|
||||
Fee(txnFees),
|
||||
Ter(temMALFORMED));
|
||||
env.close();
|
||||
}
|
||||
{
|
||||
// Not enough fees
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 70s),
|
||||
Fee(txnFees - 1),
|
||||
Ter(telINSUF_FEE_P));
|
||||
env.close();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testCreateFeeAndReserve(FeatureBitset features)
|
||||
{
|
||||
testcase("EscrowCreate fee and reserve");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace std::chrono;
|
||||
|
||||
Account const alice{"alice"};
|
||||
Account const carol{"carol"};
|
||||
|
||||
{
|
||||
// The fee scales with the bytecode: ten base fees plus five drops
|
||||
// per byte, and a drop short of it is refused.
|
||||
Env env(*this, features);
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
env.close();
|
||||
|
||||
auto const bytecode = bytecodeOfSize(1'000);
|
||||
auto const escrowCreate = escrow::create(alice, carol, XRP(500));
|
||||
auto const fee = createFeeFor(env, bytecode);
|
||||
BEAST_EXPECT(fee == env.current()->fees().base * 10 + 5'000);
|
||||
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 100s),
|
||||
Fee(fee - 1),
|
||||
Ter(telINSUF_FEE_P));
|
||||
env.close();
|
||||
|
||||
auto const seq = env.seq(alice);
|
||||
env(escrowCreate,
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::Data("00112233"),
|
||||
escrow::kCancelTime(env.now() + 100s),
|
||||
Fee(fee));
|
||||
env.close();
|
||||
|
||||
// The ledger entry carries both fields.
|
||||
auto const sle = env.le(keylet::escrow(alice, SeqProxy::rawSequence(seq)));
|
||||
if (BEAST_EXPECT(sle))
|
||||
{
|
||||
BEAST_EXPECT(sle->isFieldPresent(sfBytecode));
|
||||
BEAST_EXPECT(strHex(sle->getFieldVL(sfBytecode)) == bytecode);
|
||||
BEAST_EXPECT(strHex(sle->getFieldVL(sfData)) == "00112233");
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// The reserve is one owner increment per 500 bytes beyond the
|
||||
// first 500, on top of the increment every escrow costs.
|
||||
Env env(*this, features);
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
env.close();
|
||||
|
||||
std::uint32_t expectedOwnerCount = 0;
|
||||
for (auto const size :
|
||||
{std::size_t{64},
|
||||
std::size_t{500},
|
||||
std::size_t{501},
|
||||
std::size_t{1'000},
|
||||
std::size_t{1'001}})
|
||||
{
|
||||
auto const bytecode = bytecodeOfSize(size);
|
||||
env(escrow::create(alice, carol, XRP(100)),
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 1000s),
|
||||
Fee(createFeeFor(env, bytecode)));
|
||||
env.close();
|
||||
|
||||
expectedOwnerCount += 1 + size / 500;
|
||||
BEAST_EXPECTS(
|
||||
env.ownerCount(alice) == expectedOwnerCount,
|
||||
std::to_string(size) + " bytes: " + std::to_string(env.ownerCount(alice)));
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// A bytecode escrow the owner cannot reserve is refused, where the
|
||||
// same escrow without bytecode would have been accepted.
|
||||
Env env(*this, features);
|
||||
// Base 200 XRP + 50 XRP per owner object, from the unit-test
|
||||
// config. Enough for one owner object, not for three.
|
||||
env.fund(XRP(300), alice, carol);
|
||||
env.close();
|
||||
|
||||
auto const bytecode = bytecodeOfSize(1'000);
|
||||
env(escrow::create(alice, carol, XRP(20)),
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 100s),
|
||||
Fee(createFeeFor(env, bytecode)),
|
||||
Ter(tecINSUFFICIENT_RESERVE));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.ownerCount(alice) == 0);
|
||||
|
||||
// The same escrow without bytecode costs one increment and fits.
|
||||
env(escrow::create(alice, carol, XRP(20)),
|
||||
escrow::kFinishTime(env.now() + 10s),
|
||||
escrow::kCancelTime(env.now() + 100s));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.ownerCount(alice) == 1);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testFinishPreflight(FeatureBitset features)
|
||||
{
|
||||
testcase("EscrowFinish preflight");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace std::chrono;
|
||||
|
||||
Account const alice{"alice"};
|
||||
Account const carol{"carol"};
|
||||
|
||||
{
|
||||
// featureSmartEscrow disabled
|
||||
Env env(*this, features - featureSmartEscrow);
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
env(escrow::finish(carol, alice, 1),
|
||||
Fee(env.current()->fees().base + 1000),
|
||||
escrow::Gas(4),
|
||||
Ter(temDISABLED));
|
||||
env.close();
|
||||
}
|
||||
|
||||
{
|
||||
// Gas > gas limit
|
||||
Env env(
|
||||
*this,
|
||||
envconfig([](std::unique_ptr<Config> cfg) {
|
||||
cfg->fees.gasLimit = 1'000; // in gas
|
||||
return cfg;
|
||||
}),
|
||||
features);
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
// Run past the flag ledger so that a Fee change vote occurs and
|
||||
// updates FeeSettings. (It also activates all supported
|
||||
// amendments.)
|
||||
for (auto i = env.current()->seq(); i <= 257; ++i)
|
||||
env.close();
|
||||
|
||||
auto const gas = 1'001;
|
||||
env(escrow::finish(carol, alice, 1),
|
||||
Fee(finishFeeFor(env, gas)),
|
||||
escrow::Gas(gas),
|
||||
Ter(temBAD_LIMIT));
|
||||
}
|
||||
|
||||
{
|
||||
// Gas of 0
|
||||
Env env(*this, features);
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
env.close();
|
||||
env(escrow::finish(carol, alice, 1),
|
||||
Fee(env.current()->fees().base + 1000),
|
||||
escrow::Gas(0),
|
||||
Ter(temBAD_LIMIT));
|
||||
}
|
||||
|
||||
{
|
||||
// WASM compute disabled after the escrow was created.
|
||||
//
|
||||
// The Escrow ledger object is added by hand, bypassing normal
|
||||
// transaction processing: the config cannot be changed mid-test,
|
||||
// and a Smart Escrow cannot be created while the gas limit is 0.
|
||||
Env env{*this, envconfig([](std::unique_ptr<Config> cfg) {
|
||||
cfg->fees.gasLimit = 0;
|
||||
return cfg;
|
||||
})};
|
||||
|
||||
env.fund(XRP(1000), alice);
|
||||
env.close();
|
||||
|
||||
auto const seq = env.seq(alice);
|
||||
auto const keylet = keylet::escrow(alice.id(), SeqProxy::rawSequence(seq));
|
||||
env(noop(alice)); // to align sequence numbers
|
||||
|
||||
env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
|
||||
auto sle = std::make_shared<SLE>(keylet);
|
||||
|
||||
sle->setAccountID(sfAccount, alice.id());
|
||||
sle->setFieldAmount(sfAmount, XRP(100));
|
||||
sle->setFieldU32(sfCancelAfter, 110);
|
||||
sle->setAccountID(sfDestination, alice.id());
|
||||
sle->setFieldVL(sfBytecode, strUnHex(bytecodeOfSize(64)).value());
|
||||
sle->setFieldU32(sfFlags, 0);
|
||||
sle->setFieldU64(sfOwnerNode, 0);
|
||||
uint256 tmp;
|
||||
BEAST_EXPECT(tmp.parseHex(
|
||||
"F63D1A452A96C19EFD77901FB37D236C59EAA746771A6"
|
||||
"85D1BBA57A2238B9401"));
|
||||
sle->setFieldH256(sfPreviousTxnID, tmp);
|
||||
sle->setFieldU32(sfPreviousTxnLgrSeq, 4);
|
||||
sle->setFieldU32(sfSequence, seq);
|
||||
|
||||
view.rawInsert(sle);
|
||||
return true;
|
||||
});
|
||||
BEAST_EXPECT(env.le(keylet));
|
||||
|
||||
env(escrow::finish(alice, alice, seq),
|
||||
escrow::Gas(1000),
|
||||
Fee(env.current()->fees().base + 1000),
|
||||
Ter(temTEMP_DISABLED));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testFinishBytecodePairing(FeatureBitset features)
|
||||
{
|
||||
testcase("EscrowFinish Bytecode and Gas must agree");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace std::chrono;
|
||||
|
||||
Account const alice{"alice"};
|
||||
Account const carol{"carol"};
|
||||
|
||||
Env env(*this, features);
|
||||
|
||||
// Run past the flag ledger so that a Fee change vote occurs and
|
||||
// updates FeeSettings. (It also activates all supported amendments.)
|
||||
for (auto i = env.current()->seq(); i <= 257; ++i)
|
||||
env.close();
|
||||
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
|
||||
auto const bytecode = bytecodeOfSize(64);
|
||||
auto const seq = env.seq(alice);
|
||||
env(escrow::create(alice, carol, XRP(500)),
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 100s),
|
||||
Fee(createFeeFor(env, bytecode)));
|
||||
env.close();
|
||||
|
||||
{
|
||||
// Bytecode on the escrow, no Gas on the finish
|
||||
env(escrow::finish(carol, alice, seq), Ter(tefBYTECODE_NOT_INCLUDED));
|
||||
}
|
||||
|
||||
{
|
||||
// Gas on the finish, no Bytecode on the escrow
|
||||
auto const plainSeq = env.seq(alice);
|
||||
env(escrow::create(alice, carol, XRP(500)),
|
||||
escrow::kFinishTime(env.now() + 10s),
|
||||
escrow::kCancelTime(env.now() + 100s));
|
||||
env.close();
|
||||
|
||||
auto const gas = 100;
|
||||
env(escrow::finish(carol, alice, plainSeq),
|
||||
Fee(finishFeeFor(env, gas)),
|
||||
escrow::Gas(gas),
|
||||
Ter(tefNO_BYTECODE));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testFinishFee(FeatureBitset features)
|
||||
{
|
||||
testcase("EscrowFinish fee");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace std::chrono;
|
||||
|
||||
Account const alice{"alice"};
|
||||
Account const carol{"carol"};
|
||||
|
||||
Env env(
|
||||
*this,
|
||||
envconfig([](std::unique_ptr<Config> cfg) {
|
||||
cfg->fees.gasPrice = 1'000'000; // 1 drop per gas
|
||||
return cfg;
|
||||
}),
|
||||
features);
|
||||
// Run past the flag ledger so that a Fee change vote occurs and
|
||||
// updates FeeSettings. (It also activates all supported amendments.)
|
||||
for (auto i = env.current()->seq(); i <= 257; ++i)
|
||||
env.close();
|
||||
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
|
||||
auto const bytecode = bytecodeOfSize(64);
|
||||
auto const seq = env.seq(alice);
|
||||
env(escrow::create(alice, carol, XRP(1000)),
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 100s),
|
||||
Fee(createFeeFor(env, bytecode)));
|
||||
env.close();
|
||||
|
||||
// A large gas allowance costs more than the allowance itself, and does
|
||||
// not wrap.
|
||||
auto const gas = 996'433;
|
||||
auto const fee = finishFeeFor(env, gas);
|
||||
BEAST_EXPECT(fee.drops() > gas);
|
||||
|
||||
// Intentional low value to test overflow handling
|
||||
env(escrow::finish(carol, alice, seq),
|
||||
Fee(drops(30)),
|
||||
escrow::Gas(gas),
|
||||
Ter(telINSUF_FEE_P));
|
||||
|
||||
env(escrow::finish(carol, alice, seq), Fee(fee - 1), escrow::Gas(gas), Ter(telINSUF_FEE_P));
|
||||
|
||||
// TODO(SmartEscrow): tesSUCCESS once the WASM engine lands. Until then
|
||||
// the fee is charged and the run reports that it could not happen.
|
||||
env(escrow::finish(carol, alice, seq),
|
||||
Fee(fee),
|
||||
escrow::Gas(gas),
|
||||
Ter(tecFAILED_PROCESSING));
|
||||
}
|
||||
|
||||
void
|
||||
testFinishWithoutEngine(FeatureBitset features)
|
||||
{
|
||||
testcase("EscrowFinish has no WASM engine");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace std::chrono;
|
||||
|
||||
Account const alice{"alice"};
|
||||
Account const carol{"carol"};
|
||||
|
||||
Env env(*this, features);
|
||||
env.fund(XRP(5000), alice, carol);
|
||||
env.close();
|
||||
|
||||
auto const bytecode = bytecodeOfSize(1'000);
|
||||
auto const seq = env.seq(alice);
|
||||
env(escrow::create(alice, carol, XRP(500)),
|
||||
escrow::Bytecode(bytecode),
|
||||
escrow::kCancelTime(env.now() + 20s),
|
||||
Fee(createFeeFor(env, bytecode)));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.ownerCount(alice) == 3);
|
||||
|
||||
// TODO(SmartEscrow): the contract decides this once the WASM engine
|
||||
// lands - tesSUCCESS on accept, tecBYTECODE_REJECTED on reject.
|
||||
auto const gas = 1'000;
|
||||
env(escrow::finish(carol, alice, seq),
|
||||
Fee(finishFeeFor(env, gas)),
|
||||
escrow::Gas(gas),
|
||||
Ter(tecFAILED_PROCESSING));
|
||||
env.close();
|
||||
|
||||
// The escrow and its reserve survive.
|
||||
BEAST_EXPECT(env.le(keylet::escrow(alice, SeqProxy::rawSequence(seq))));
|
||||
BEAST_EXPECT(env.ownerCount(alice) == 3);
|
||||
|
||||
// Cancelling returns the whole reserve, not just one increment.
|
||||
env.close(30s);
|
||||
env(escrow::cancel(carol, alice, seq));
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(keylet::escrow(alice, SeqProxy::rawSequence(seq))));
|
||||
BEAST_EXPECT(env.ownerCount(alice) == 0);
|
||||
}
|
||||
|
||||
void
|
||||
testWithFeats(FeatureBitset features)
|
||||
{
|
||||
testCreatePreflight(features);
|
||||
testCreateFeeAndReserve(features);
|
||||
testFinishPreflight(features);
|
||||
testFinishBytecodePairing(features);
|
||||
testFinishFee(features);
|
||||
testFinishWithoutEngine(features);
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
using namespace test::jtx;
|
||||
FeatureBitset const all{testableAmendments()};
|
||||
testWithFeats(all);
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(EscrowSmart, app, xrpl);
|
||||
|
||||
} // namespace xrpl::test
|
||||
@@ -1482,7 +1482,7 @@ struct Escrow_test : public beast::unit_test::Suite
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
Account const carol{"carol"};
|
||||
Account const dillon{"dillon "};
|
||||
Account const dillon{"dillon"};
|
||||
Account const zelda{"zelda"};
|
||||
|
||||
char const credType[] = "abcde";
|
||||
@@ -1639,6 +1639,8 @@ public:
|
||||
FeatureBitset const all{testableAmendments()};
|
||||
testWithFeats(all);
|
||||
testWithFeats(all - featureTokenEscrow);
|
||||
testWithFeats(all - featureSmartEscrow);
|
||||
testWithFeats(all - featureTokenEscrow - featureSmartEscrow);
|
||||
testTags(all - fixIncludeKeyletFields);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/JTx.h>
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/basics/strHex.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Rate.h>
|
||||
@@ -11,7 +14,10 @@
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
/**
|
||||
* Escrow operations.
|
||||
@@ -86,4 +92,73 @@ auto const kCondition = JTxFieldWrapper<BlobField>(sfCondition);
|
||||
|
||||
auto const kFulfillment = JTxFieldWrapper<BlobField>(sfFulfillment);
|
||||
|
||||
struct Bytecode
|
||||
{
|
||||
private:
|
||||
std::string value_;
|
||||
|
||||
public:
|
||||
explicit Bytecode(std::string func) : value_(std::move(func))
|
||||
{
|
||||
}
|
||||
|
||||
explicit Bytecode(Slice const& func) : value_(strHex(func))
|
||||
{
|
||||
}
|
||||
|
||||
template <size_t N>
|
||||
explicit Bytecode(std::array<std::uint8_t, N> const& f) : Bytecode(makeSlice(f))
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
operator()(Env&, JTx& jt) const
|
||||
{
|
||||
jt.jv[sfBytecode.jsonName] = value_;
|
||||
}
|
||||
};
|
||||
|
||||
struct Data
|
||||
{
|
||||
private:
|
||||
std::string value_;
|
||||
|
||||
public:
|
||||
explicit Data(std::string func) : value_(std::move(func))
|
||||
{
|
||||
}
|
||||
|
||||
explicit Data(Slice const& func) : value_(strHex(func))
|
||||
{
|
||||
}
|
||||
|
||||
template <size_t N>
|
||||
explicit Data(std::array<std::uint8_t, N> const& f) : Data(makeSlice(f))
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
operator()(Env&, JTx& jt) const
|
||||
{
|
||||
jt.jv[sfData.jsonName] = value_;
|
||||
}
|
||||
};
|
||||
|
||||
struct Gas
|
||||
{
|
||||
private:
|
||||
std::uint32_t value_;
|
||||
|
||||
public:
|
||||
explicit Gas(std::uint32_t const& value) : value_(value)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
operator()(Env&, JTx& jt) const
|
||||
{
|
||||
jt.jv[sfGas.jsonName] = value_;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace xrpl::test::jtx::escrow
|
||||
|
||||
Reference in New Issue
Block a user