Files
rippled/src/test/app/EscrowSmart_test.cpp
2026-08-08 23:13:33 -04:00

1313 lines
48 KiB
C++

#include <test/app/wasm_fixtures/fixtures.h>
#include <test/jtx/AMM.h>
#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/credentials.h>
#include <test/jtx/delegate.h>
#include <test/jtx/deposit.h>
#include <test/jtx/did.h>
#include <test/jtx/envconfig.h>
#include <test/jtx/escrow.h>
#include <test/jtx/fee.h>
#include <test/jtx/mpt.h>
#include <test/jtx/multisign.h>
#include <test/jtx/noop.h>
#include <test/jtx/offer.h>
#include <test/jtx/pay.h>
#include <test/jtx/permissioned_domains.h>
#include <test/jtx/ter.h>
#include <test/jtx/ticket.h>
#include <test/jtx/token.h>
#include <test/jtx/trust.h>
#include <test/jtx/txflags.h>
#include <test/jtx/vault.h>
#include <xrpld/core/Config.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/core/ServiceRegistry.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/Issue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/XRPAmount.h>
#include <cstdint>
#include <exception>
#include <memory>
#include <optional>
#include <source_location>
#include <string>
#include <utility>
#include <vector>
namespace xrpl::test {
struct EscrowSmart_test : public beast::unit_test::Suite
{
void
testCreateBytecodePreflight(FeatureBitset features)
{
testcase("Test preflight checks involving Bytecode");
using namespace jtx;
using namespace std::chrono;
Account const alice{"alice"};
Account const carol{"carol"};
// Tests whether the ledger index is >= 5
// getLedgerSqn() >= 5}
{
// 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(kLedgerSqnWasmHex),
escrow::kCancelTime(env.now() + 100s),
Fee(txnFees),
Ter(temDISABLED));
env.close();
env(escrowCreate,
escrow::Bytecode(kLedgerSqnWasmHex),
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;
// create escrow
env.fund(XRP(5000), alice, carol);
auto const escrowCreate = escrow::create(alice, carol, XRP(500));
// 11-byte string
std::string const longWasmHex = "00112233445566778899AA";
env(escrowCreate,
escrow::Bytecode(longWasmHex),
escrow::kCancelTime(env.now() + 100s),
Fee(txnFees),
Ter(temMALFORMED));
env.close();
}
{
// compute 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;
// create escrow
env.fund(XRP(5000), alice, carol);
auto const escrowCreate = escrow::create(alice, carol, XRP(500));
env(escrowCreate,
escrow::Bytecode(kLedgerSqnWasmHex),
escrow::kCancelTime(env.now() + 100s),
escrow::Gas(100),
Fee(txnFees),
Ter(temMALFORMED));
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;
// create escrow
env.fund(XRP(5000), alice, carol);
auto const escrowCreate = escrow::create(alice, carol, XRP(500));
// 2-byte string
env(escrowCreate,
escrow::Bytecode("AA"),
escrow::kCancelTime(env.now() + 100s),
Fee(txnFees),
Ter(temTEMP_DISABLED));
env.close();
env(escrowCreate,
escrow::Bytecode(kLedgerSqnWasmHex),
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;
// create escrow
env.fund(XRP(5000), alice, carol);
auto const escrowCreate = escrow::create(alice, carol, XRP(500));
std::string const longData(4, 'A');
env(escrowCreate,
escrow::Data(longData),
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;
// create escrow
env.fund(XRP(5000), alice, carol);
auto const escrowCreate = escrow::create(alice, carol, XRP(500));
// string of length kMaxWasmDataLength * 2 + 2
std::string const longData((kMaxWasmDataLength + 1) * 2, 'B');
env(escrowCreate,
escrow::Data(longData),
escrow::Bytecode(kLedgerSqnWasmHex),
escrow::kCancelTime(env.now() + 100s),
Fee(txnFees),
Ter(temMALFORMED));
env.close();
}
Env env(
*this,
envconfig([](std::unique_ptr<Config> cfg) {
cfg->startUp = StartUpType::Fresh;
return cfg;
}),
features);
XRPAmount const txnFees =
env.current()->fees().base * 10 + kLedgerSqnWasmHex.size() / 2 * 5;
// create escrow
env.fund(XRP(5000), alice, carol);
auto escrowCreate = escrow::create(alice, carol, XRP(500));
// Success situations
{
// Bytecode + CancelAfter
env(escrowCreate,
escrow::Bytecode(kLedgerSqnWasmHex),
escrow::kCancelTime(env.now() + 20s),
Fee(txnFees));
env.close();
}
{
// Bytecode + Condition + CancelAfter
env(escrowCreate,
escrow::Bytecode(kLedgerSqnWasmHex),
escrow::kCancelTime(env.now() + 30s),
escrow::kCondition(escrow::kCb1),
Fee(txnFees));
env.close();
}
{
// Bytecode + FinishAfter + CancelAfter
env(escrowCreate,
escrow::Bytecode(kLedgerSqnWasmHex),
escrow::kCancelTime(env.now() + 40s),
escrow::kFinishTime(env.now() + 2s),
Fee(txnFees));
env.close();
}
{
// Bytecode + FinishAfter + Condition + CancelAfter
env(escrowCreate,
escrow::Bytecode(kLedgerSqnWasmHex),
escrow::kCancelTime(env.now() + 50s),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 2s),
Fee(txnFees));
env.close();
}
// Failure situations (i.e. all other combinations)
{
// only Bytecode
env(escrowCreate,
escrow::Bytecode(kLedgerSqnWasmHex),
Fee(txnFees),
Ter(temBAD_EXPIRATION));
env.close();
}
{
// Bytecode + FinishAfter
env(escrowCreate,
escrow::Bytecode(kLedgerSqnWasmHex),
escrow::kFinishTime(env.now() + 2s),
Fee(txnFees),
Ter(temBAD_EXPIRATION));
env.close();
}
{
// Bytecode + Condition
env(escrowCreate,
escrow::Bytecode(kLedgerSqnWasmHex),
escrow::kCondition(escrow::kCb1),
Fee(txnFees),
Ter(temBAD_EXPIRATION));
env.close();
}
{
// Bytecode + FinishAfter + Condition
env(escrowCreate,
escrow::Bytecode(kLedgerSqnWasmHex),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 2s),
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(kLedgerSqnWasmHex),
escrow::kCancelTime(env.now() + 70s),
Fee(txnFees - 1),
Ter(telINSUF_FEE_P));
env.close();
}
{
// Bytecode nonexistent host function
// pub fn finish() -> bool {
// unsafe { host_lib::bad() >= 5 }
// }
auto const badWasmHex =
"0061736d010000000105016000017f02100108686f73745f6c696203626164"
"00000302010005030100100611027f00418080c0000b7f00418080c0000b07"
"2e04066d656d6f727902000666696e69736800010a5f5f646174615f656e64"
"03000b5f5f686561705f6261736503010a09010700100041044a0b004d0970"
"726f64756365727302086c616e6775616765010452757374000c70726f6365"
"737365642d6279010572757374631d312e38352e3120283465623136313235"
"3020323032352d30332d31352900490f7461726765745f6665617475726573"
"042b0f6d757461626c652d676c6f62616c732b087369676e2d6578742b0f72"
"65666572656e63652d74797065732b0a6d756c746976616c7565";
env(escrowCreate,
escrow::Bytecode(badWasmHex),
escrow::kCancelTime(env.now() + 100s),
Fee(txnFees),
Ter(temINVALID_BYTECODE));
env.close();
}
}
void
testFinishWasmFailures(FeatureBitset features)
{
testcase("EscrowFinish Smart Escrow failures");
using namespace jtx;
using namespace std::chrono;
Account const alice{"alice"};
Account const carol{"carol"};
// Tests whether the ledger index is >= 5
// getLedgerSqn() >= 5}
{
// featureSmartEscrow disabled
Env env(*this, features - featureSmartEscrow);
env.fund(XRP(5000), alice, carol);
XRPAmount const txnFees =
env.current()->fees().base * 10 + kLedgerSqnWasmHex.size() / 2 * 5;
env(escrow::finish(carol, alice, 1), Fee(txnFees), escrow::Gas(4), Ter(temDISABLED));
env.close();
}
{
// Gas > max compute 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 allowance = 1'001;
env(escrow::finish(carol, alice, 1),
Fee(env.current()->fees().base + allowance),
escrow::Gas(allowance),
Ter(temBAD_LIMIT));
}
{
// WASM compute disabled
using namespace test::jtx;
using namespace std::chrono;
Env env{*this, envconfig([](std::unique_ptr<Config> cfg) {
cfg->fees.gasLimit = 0;
return cfg;
})};
Account const alice{"alice"};
env.fund(XRP(1000), alice);
env.close();
auto const seq = env.seq(alice);
auto const keylet = keylet::escrow(alice.id(), seq);
env(noop(alice)); // to align sequence numbers
// This adds the Escrow ledger object by hand, bypassing normal
// transaction processing This is necessary because the config
// cannot be updated in the middle of a test, and we cannot easily
// create a Smart Escrow while the compute limit is set to 0
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(kLedgerSqnWasmHex).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));
}
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();
XRPAmount const txnFees =
env.current()->fees().base * 10 + kLedgerSqnWasmHex.size() / 2 * 5;
env.fund(XRP(5000), alice, carol);
// create escrow
auto const seq = env.seq(alice);
env(escrow::create(alice, carol, XRP(500)),
escrow::Bytecode(kLedgerSqnWasmHex),
escrow::kCancelTime(env.now() + 100s),
Fee(txnFees));
env.close();
{
// no Gas field
env(escrow::finish(carol, alice, seq), Ter(tefBYTECODE_NOT_INCLUDED));
}
{
// Gas value of 0
env(escrow::finish(carol, alice, seq), escrow::Gas(0), Ter(temBAD_LIMIT));
}
{
// not enough fees
// This function takes 4 gas
// In testing, 1 gas costs 1 drop
auto const finishFee = env.current()->fees().base + 3;
env(escrow::finish(carol, alice, seq),
Fee(finishFee),
escrow::Gas(4),
Ter(telINSUF_FEE_P));
}
{
// not enough gas
// This function takes 4 gas
// In testing, 1 gas costs 1 drop
auto const finishFee = env.current()->fees().base + 4;
env(escrow::finish(carol, alice, seq),
Fee(finishFee),
escrow::Gas(2),
Ter(tecOUT_OF_GAS));
// Running out of gas still reports the gas consumed, which is the
// whole allowance. The function did not run to completion, so
// there is no return code to report.
auto const txMeta = env.meta();
if (BEAST_EXPECT(txMeta && txMeta->isFieldPresent(sfGasUsed)))
{
BEAST_EXPECTS(
txMeta->getFieldU32(sfGasUsed) == 2,
std::to_string(txMeta->getFieldU32(sfGasUsed)));
}
BEAST_EXPECT(txMeta && !txMeta->isFieldPresent(sfVMReturnCode));
}
{
// Gas field included w/no Bytecode on
// escrow
auto const seq2 = env.seq(alice);
env(escrow::create(alice, carol, XRP(500)),
escrow::kFinishTime(env.now() + 10s),
escrow::kCancelTime(env.now() + 100s));
env.close();
auto const allowance = 100;
env(escrow::finish(carol, alice, seq2),
Fee(env.current()->fees().base +
(allowance * env.current()->fees().gasPrice) / microDropsPerDrop + 1),
escrow::Gas(allowance),
Ter(tefNO_BYTECODE));
}
{
// a trap in the wasm code reports the gas it burned, which is only
// part of the allowance
auto const trapSeq = env.seq(alice);
env(escrow::create(alice, carol, XRP(500)),
escrow::Bytecode(kTrapUnreachableHex),
escrow::kCancelTime(env.now() + 100s),
Fee(env.current()->fees().base * 10 + kTrapUnreachableHex.size() / 2 * 5));
env.close();
std::uint32_t const allowance = 1000;
env(escrow::finish(carol, alice, trapSeq),
Fee(env.current()->fees().base +
(allowance * env.current()->fees().gasPrice) / microDropsPerDrop + 1),
escrow::Gas(allowance),
Ter(tecFAILED_PROCESSING));
auto const txMeta = env.meta();
if (BEAST_EXPECT(txMeta && txMeta->isFieldPresent(sfGasUsed)))
{
auto const gasUsed = txMeta->getFieldU32(sfGasUsed);
BEAST_EXPECTS(gasUsed < allowance, std::to_string(gasUsed));
}
BEAST_EXPECT(txMeta && !txMeta->isFieldPresent(sfVMReturnCode));
}
}
void
testBytecode(FeatureBitset features)
{
testcase("Example escrow function");
using namespace jtx;
using namespace std::chrono;
Account const alice{"alice"};
Account const carol{"carol"};
// Tests whether the ledger index is >= 5
// getLedgerSqn() >= 5}
std::uint32_t const allowance = 467;
auto escrowCreate = escrow::create(alice, carol, XRP(1000));
auto [createFee, finishFee] = [&]() {
Env const env(*this, features);
auto createFee = env.current()->fees().base * 10 + kLedgerSqnWasmHex.size() / 2 * 5;
auto finishFee = env.current()->fees().base +
(allowance * env.current()->fees().gasPrice) / microDropsPerDrop + 1;
return std::make_pair(createFee, finishFee);
}();
{
// basic Bytecode situation
Env env(*this, features);
// create escrow
env.fund(XRP(5000), alice, carol);
auto const seq = env.seq(alice);
BEAST_EXPECT(env.ownerCount(alice) == 0);
env(escrowCreate,
escrow::Bytecode(kLedgerSqnWasmHex),
escrow::kCancelTime(env.now() + 100s),
Fee(createFee));
env.close();
if (BEAST_EXPECT(env.ownerCount(alice) == 2))
{
env.require(Balance(alice, XRP(4000) - createFee));
env.require(Balance(carol, XRP(5000)));
env(escrow::finish(carol, alice, seq),
escrow::Gas(allowance),
Fee(finishFee),
Ter(tecBYTECODE_REJECTED));
env(escrow::finish(alice, alice, seq),
escrow::Gas(allowance),
Fee(finishFee),
Ter(tecBYTECODE_REJECTED));
env(escrow::finish(alice, alice, seq),
escrow::Gas(allowance),
Fee(finishFee),
Ter(tecBYTECODE_REJECTED));
env(escrow::finish(carol, alice, seq),
escrow::Gas(allowance),
Fee(finishFee),
Ter(tecBYTECODE_REJECTED));
env(escrow::finish(carol, alice, seq),
escrow::Gas(allowance),
Fee(finishFee),
Ter(tecBYTECODE_REJECTED));
env.close();
{
auto const txMeta = env.meta();
if (BEAST_EXPECT(txMeta->isFieldPresent(sfGasUsed)))
{
BEAST_EXPECTS(
env.meta()->getFieldU32(sfGasUsed) == allowance,
std::to_string(env.meta()->getFieldU32(sfGasUsed)));
}
}
env(escrow::finish(alice, alice, seq),
Fee(finishFee),
escrow::Gas(allowance),
Ter(tesSUCCESS));
auto const txMeta = env.meta();
if (BEAST_EXPECT(txMeta->isFieldPresent(sfGasUsed)))
{
BEAST_EXPECTS(
txMeta->getFieldU32(sfGasUsed) == allowance,
std::to_string(txMeta->getFieldU32(sfGasUsed)));
}
if (BEAST_EXPECT(txMeta->isFieldPresent(sfVMReturnCode)))
{
BEAST_EXPECTS(
txMeta->getFieldI32(sfVMReturnCode) == 5,
std::to_string(txMeta->getFieldI32(sfVMReturnCode)));
}
BEAST_EXPECT(env.ownerCount(alice) == 0);
}
}
{
// Bytecode + Condition
Env env(*this, features);
env.fund(XRP(5000), alice, carol);
BEAST_EXPECT(env.ownerCount(alice) == 0);
auto const seq = env.seq(alice);
// create escrow
env(escrowCreate,
escrow::Bytecode(kLedgerSqnWasmHex),
escrow::kCondition(escrow::kCb1),
escrow::kCancelTime(env.now() + 100s),
Fee(createFee));
env.close();
auto const conditionFinishFee =
finishFee + env.current()->fees().base * (32 + (escrow::kFb1.size() / 16));
if (BEAST_EXPECT(env.ownerCount(alice) == 2))
{
env.require(Balance(alice, XRP(4000) - createFee));
env.require(Balance(carol, XRP(5000)));
// no fulfillment provided, function fails
env(escrow::finish(carol, alice, seq),
escrow::Gas(allowance),
Fee(finishFee),
Ter(tecCRYPTOCONDITION_ERROR));
// fulfillment provided, function fails
env(escrow::finish(carol, alice, seq),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
escrow::Gas(allowance),
Fee(conditionFinishFee),
Ter(tecBYTECODE_REJECTED));
if (BEAST_EXPECT(env.meta()->isFieldPresent(sfGasUsed)))
{
BEAST_EXPECTS(
env.meta()->getFieldU32(sfGasUsed) == allowance,
std::to_string(env.meta()->getFieldU32(sfGasUsed)));
}
env.close();
// no fulfillment provided, function succeeds
env(escrow::finish(alice, alice, seq),
escrow::Gas(allowance),
Fee(conditionFinishFee),
Ter(tecCRYPTOCONDITION_ERROR));
// wrong fulfillment provided, function succeeds
env(escrow::finish(alice, alice, seq),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb2),
escrow::Gas(allowance),
Fee(conditionFinishFee),
Ter(tecCRYPTOCONDITION_ERROR));
// fulfillment provided, function succeeds, tx succeeds
env(escrow::finish(alice, alice, seq),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
escrow::Gas(allowance),
Fee(conditionFinishFee),
Ter(tesSUCCESS));
auto const txMeta = env.meta();
if (BEAST_EXPECT(txMeta->isFieldPresent(sfGasUsed)))
{
BEAST_EXPECTS(
txMeta->getFieldU32(sfGasUsed) == allowance,
std::to_string(txMeta->getFieldU32(sfGasUsed)));
}
if (BEAST_EXPECT(txMeta->isFieldPresent(sfVMReturnCode)))
{
BEAST_EXPECTS(
txMeta->getFieldI32(sfVMReturnCode) == 5,
std::to_string(txMeta->getFieldI32(sfVMReturnCode)));
}
env.close();
BEAST_EXPECT(env.ownerCount(alice) == 0);
}
}
{
// Bytecode + FinishAfter
Env env(*this, features);
// create escrow
env.fund(XRP(5000), alice, carol);
auto const seq = env.seq(alice);
BEAST_EXPECT(env.ownerCount(alice) == 0);
auto const ts = env.now() + 97s;
env(escrowCreate,
escrow::Bytecode(kLedgerSqnWasmHex),
escrow::kFinishTime(ts),
escrow::kCancelTime(env.now() + 1000s),
Fee(createFee));
env.close();
if (BEAST_EXPECT(env.ownerCount(alice) == 2))
{
env.require(Balance(alice, XRP(4000) - createFee));
env.require(Balance(carol, XRP(5000)));
// finish time hasn't passed, function fails
env(escrow::finish(carol, alice, seq),
escrow::Gas(allowance),
Fee(finishFee + 1),
Ter(tecNO_PERMISSION));
env.close();
// finish time hasn't passed, function succeeds
for (; env.now() < ts; env.close())
{
env(escrow::finish(carol, alice, seq),
escrow::Gas(allowance),
Fee(finishFee + 2),
Ter(tecNO_PERMISSION));
}
env(escrow::finish(carol, alice, seq),
escrow::Gas(allowance),
Fee(finishFee + 1),
Ter(tesSUCCESS));
auto const txMeta = env.meta();
if (BEAST_EXPECT(txMeta->isFieldPresent(sfGasUsed)))
BEAST_EXPECT(txMeta->getFieldU32(sfGasUsed) == allowance);
if (BEAST_EXPECT(txMeta->isFieldPresent(sfVMReturnCode)))
{
BEAST_EXPECTS(
txMeta->getFieldI32(sfVMReturnCode) == 5,
std::to_string(txMeta->getFieldI32(sfVMReturnCode)));
}
BEAST_EXPECT(env.ownerCount(alice) == 0);
}
}
{
// Bytecode + FinishAfter #2
Env env(*this, features);
// create escrow
env.fund(XRP(5000), alice, carol);
auto const seq = env.seq(alice);
BEAST_EXPECT(env.ownerCount(alice) == 0);
env(escrowCreate,
escrow::Bytecode(kLedgerSqnWasmHex),
escrow::kFinishTime(env.now() + 2s),
escrow::kCancelTime(env.now() + 100s),
Fee(createFee));
// Don't close the ledger here
if (BEAST_EXPECT(env.ownerCount(alice) == 2))
{
env.require(Balance(alice, XRP(4000) - createFee));
env.require(Balance(carol, XRP(5000)));
// finish time hasn't passed, function fails
env(escrow::finish(carol, alice, seq),
escrow::Gas(allowance),
Fee(finishFee),
Ter(tecNO_PERMISSION));
env.close();
// finish time has passed, function fails
env(escrow::finish(carol, alice, seq),
escrow::Gas(allowance),
Fee(finishFee),
Ter(tecBYTECODE_REJECTED));
if (BEAST_EXPECT(env.meta()->isFieldPresent(sfGasUsed)))
{
BEAST_EXPECTS(
env.meta()->getFieldU32(sfGasUsed) == allowance,
std::to_string(env.meta()->getFieldU32(sfGasUsed)));
}
env.close();
// finish time has passed, function succeeds, tx succeeds
env(escrow::finish(carol, alice, seq),
escrow::Gas(allowance),
Fee(finishFee),
Ter(tesSUCCESS));
auto const txMeta = env.meta();
if (BEAST_EXPECT(txMeta->isFieldPresent(sfGasUsed)))
BEAST_EXPECT(txMeta->getFieldU32(sfGasUsed) == allowance);
if (BEAST_EXPECT(txMeta->isFieldPresent(sfVMReturnCode)))
{
BEAST_EXPECTS(
txMeta->getFieldI32(sfVMReturnCode) == 5,
std::to_string(txMeta->getFieldI32(sfVMReturnCode)));
}
env.close();
BEAST_EXPECT(env.ownerCount(alice) == 0);
}
}
}
void
testUpdateDataOnFailure(FeatureBitset features)
{
testcase("Update escrow data on failure");
using namespace jtx;
using namespace std::chrono;
// wasm that always fails
Account const alice{"alice"};
Account const carol{"carol"};
Env env(*this, features);
// create escrow
env.fund(XRP(5000), alice);
auto const seq = env.seq(alice);
BEAST_EXPECT(env.ownerCount(alice) == 0);
auto escrowCreate = escrow::create(alice, alice, XRP(1000));
XRPAmount const txnFees =
env.current()->fees().base * 10 + kUpdateDataWasmHex.size() / 2 * 5;
env(escrowCreate,
escrow::Bytecode(kUpdateDataWasmHex),
escrow::kFinishTime(env.now() + 2s),
escrow::kCancelTime(env.now() + 100s),
Fee(txnFees));
env.close();
env.close();
env.close();
if (BEAST_EXPECT(env.ownerCount(alice) == (1 + (kUpdateDataWasmHex.size() / 2 / 500))))
{
env.require(Balance(alice, XRP(4000) - txnFees));
auto const allowance = 1420;
XRPAmount const finishFee = env.current()->fees().base +
(allowance * env.current()->fees().gasPrice) / microDropsPerDrop + 1;
// FinishAfter time hasn't passed
env(escrow::finish(alice, alice, seq),
escrow::Gas(allowance),
Fee(finishFee),
Ter(tecBYTECODE_REJECTED));
auto const txMeta = env.meta();
if (BEAST_EXPECT(txMeta && txMeta->isFieldPresent(sfGasUsed)))
{
BEAST_EXPECTS(
txMeta->getFieldU32(sfGasUsed) == allowance,
std::to_string(txMeta->getFieldU32(sfGasUsed)));
}
if (BEAST_EXPECT(txMeta->isFieldPresent(sfVMReturnCode)))
{
BEAST_EXPECTS(
txMeta->getFieldI32(sfVMReturnCode) == -256,
std::to_string(txMeta->getFieldI32(sfVMReturnCode)));
}
auto const sle = env.le(keylet::escrow(alice, seq));
if (BEAST_EXPECT(sle && sle->isFieldPresent(sfData)))
BEAST_EXPECTS(checkVL(sle, sfData, "Data"), strHex(sle->getFieldVL(sfData)));
}
}
void
testFees(FeatureBitset features)
{
testcase("Fees");
using namespace jtx;
using namespace std::chrono;
Account const alice{"alice"};
Account const carol{"carol"};
// Tests whether the ledger index is >= 5
// getLedgerSqn() >= 5}
uint64_t const allowance = 467;
auto escrowCreate = escrow::create(alice, carol, XRP(1000));
auto createFee = [&]() {
Env const env(*this, features);
auto createFee = env.current()->fees().base * 10 + kLedgerSqnWasmHex.size() / 2 * 5;
return createFee;
}();
{
// ensure fees don't overflow
Env env(
*this,
envconfig([](std::unique_ptr<Config> cfg) {
cfg->fees.gasPrice = 1'000'000; // in 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();
// create escrow
env.fund(XRP(5000), alice, carol);
auto const seq = env.seq(alice);
BEAST_EXPECT(env.ownerCount(alice) == 0);
env(escrowCreate,
escrow::Bytecode(kLedgerSqnWasmHex),
escrow::kCancelTime(env.now() + 100s),
Fee(createFee));
env.close();
if (BEAST_EXPECT(env.ownerCount(alice) == 2))
{
env.require(Balance(alice, XRP(4000) - createFee));
env.require(Balance(carol, XRP(5000)));
env.close();
auto const bigAllowance = 996'433;
uint64_t const partialFeeCalc =
((static_cast<uint64_t>(bigAllowance) * 1'000'000) / microDropsPerDrop) + 1;
auto finishFee = env.current()->fees().base + partialFeeCalc;
BEAST_EXPECT(finishFee.drops() > bigAllowance);
// Intentional low value to test overflow handling
auto finishFeeOverflow = drops(30);
env(escrow::finish(alice, alice, seq),
Fee(finishFeeOverflow), // enough if there's an overflow
escrow::Gas(bigAllowance),
Ter(telINSUF_FEE_P));
env(escrow::finish(alice, alice, seq),
Fee(finishFee - 1),
escrow::Gas(bigAllowance),
Ter(telINSUF_FEE_P));
env(escrow::finish(alice, alice, seq),
Fee(finishFee),
escrow::Gas(bigAllowance),
Ter(tesSUCCESS));
auto const txMeta = env.meta();
if (BEAST_EXPECT(txMeta->isFieldPresent(sfGasUsed)))
{
BEAST_EXPECTS(
txMeta->getFieldU32(sfGasUsed) == allowance,
std::to_string(txMeta->getFieldU32(sfGasUsed)));
}
if (BEAST_EXPECT(txMeta->isFieldPresent(sfVMReturnCode)))
{
BEAST_EXPECTS(
txMeta->getFieldI32(sfVMReturnCode) == 5,
std::to_string(txMeta->getFieldI32(sfVMReturnCode)));
}
BEAST_EXPECT(env.ownerCount(alice) == 0);
}
}
}
void
testAllHostFunctions(FeatureBitset features)
{
testcase("Test all host functions");
using namespace jtx;
using namespace std::chrono;
Account const alice{"alice"};
Account const carol{"carol"};
{
Env env(*this, features);
// create escrow
env.fund(XRP(5000), alice, carol);
auto const seq = env.seq(alice);
BEAST_EXPECT(env.ownerCount(alice) == 0);
auto escrowCreate = escrow::create(alice, carol, XRP(1000));
XRPAmount const txnFees =
env.current()->fees().base * 10 + kAllHostFunctionsWasmHex.size() / 2 * 5;
env(escrowCreate,
escrow::Bytecode(kAllHostFunctionsWasmHex),
escrow::kFinishTime(env.now() + 11s),
escrow::kCancelTime(env.now() + 100s),
escrow::Data("1000000000"), // 1000 XRP in drops
Fee(txnFees));
env.close();
if (BEAST_EXPECT(
env.ownerCount(alice) == (1 + (kAllHostFunctionsWasmHex.size() / 2 / 500))))
{
env.require(Balance(alice, XRP(4000) - txnFees));
env.require(Balance(carol, XRP(5000)));
auto const allowance = 1'000'000;
XRPAmount const finishFee = env.current()->fees().base +
(allowance * env.current()->fees().gasPrice) / microDropsPerDrop + 1;
// FinishAfter time hasn't passed
env(escrow::finish(carol, alice, seq),
escrow::Gas(allowance),
Fee(finishFee),
Ter(tecNO_PERMISSION));
env.close();
env.close();
env.close();
// reduce the destination balance
env(pay(carol, alice, XRP(4500)));
env.close();
env.close();
env(escrow::finish(alice, alice, seq),
escrow::Gas(allowance),
Fee(finishFee),
Ter(tesSUCCESS));
auto const txMeta = env.meta();
if (BEAST_EXPECT(txMeta && txMeta->isFieldPresent(sfGasUsed)))
{
BEAST_EXPECTS(
txMeta->getFieldU32(sfGasUsed) == 68'041,
std::to_string(txMeta->getFieldU32(sfGasUsed)));
}
if (BEAST_EXPECT(txMeta->isFieldPresent(sfVMReturnCode)))
BEAST_EXPECT(txMeta->getFieldI32(sfVMReturnCode) == 1);
env.close();
BEAST_EXPECT(env.ownerCount(alice) == 0);
}
}
}
void
testKeyletHostFunctions(FeatureBitset features)
{
testcase("Test all keylet host functions");
using namespace jtx;
using namespace std::chrono;
// TODO: create wasm module for all host functions
Account const alice{"alice"};
Account const carol{"carol"};
{
Env env{*this};
env.fund(XRP(10000), alice, carol);
BEAST_EXPECT(env.seq(alice) == 4);
BEAST_EXPECT(env.ownerCount(alice) == 0);
// base objects that need to be created first
auto const tokenId = token::getNextID(env, alice, 0, tfTransferable);
env(token::mint(alice, 0u), Txflags(tfTransferable));
env(trust(alice, carol["USD"](1'000'000)));
env.close();
BEAST_EXPECT(env.seq(alice) == 6);
BEAST_EXPECT(env.ownerCount(alice) == 2);
// set up a bunch of objects to check their keylets
AMM const amm(env, carol, XRP(10), carol["USD"](1000));
env(check::create(alice, carol, XRP(100)));
env(credentials::create(alice, alice, "termsandconditions"));
env(delegate::set(alice, carol, {"TrustSet"}));
env(deposit::auth(alice, carol));
env(did::set(alice), did::Data("alice_did"));
env(escrow::create(alice, carol, XRP(100)), escrow::kFinishTime(env.now() + 100s));
MPTTester mptTester{env, alice, {.fund = false}};
mptTester.create();
mptTester.authorize({.account = carol});
env(token::createOffer(carol, tokenId, XRP(100)), token::Owner(alice));
env(offer(alice, carol["GBP"](0.1), XRP(100)));
env(paychan::create(alice, carol, XRP(1000), 100s, alice.pk()));
pdomain::Credentials const credentials{
{.issuer = alice, .credType = "first credential"}};
env(pdomain::setTx(alice, credentials));
env(signers(alice, 1, {{carol, 1}}));
env(ticket::create(alice, 1));
Vault const vault{env};
auto [tx, _keylet] = vault.create({.owner = alice, .asset = xrpIssue()});
env(tx);
env.close();
BEAST_EXPECTS(env.ownerCount(alice) == 17, std::to_string(env.ownerCount(alice)));
if (BEAST_EXPECTS(env.seq(alice) == 20, std::to_string(env.seq(alice))))
{
auto const seq = env.seq(alice);
XRPAmount const txnFees =
env.current()->fees().base * 10 + kAllKeyletsWasmHex.size() / 2 * 5;
env(escrow::create(alice, carol, XRP(1000)),
escrow::Bytecode(kAllKeyletsWasmHex),
escrow::kFinishTime(env.now() + 2s),
escrow::kCancelTime(env.now() + 100s),
Fee(txnFees));
env.close();
env.close();
env.close();
auto const allowance = 184'375;
auto const finishFee = env.current()->fees().base +
(allowance * env.current()->fees().gasPrice) / microDropsPerDrop + 1;
env(escrow::finish(carol, alice, seq), escrow::Gas(allowance), Fee(finishFee));
env.close();
auto const txMeta = env.meta();
if (BEAST_EXPECT(txMeta && txMeta->isFieldPresent(sfGasUsed)))
{
auto const gasUsed = txMeta->getFieldU32(sfGasUsed);
BEAST_EXPECTS(gasUsed == allowance, std::to_string(gasUsed));
}
BEAST_EXPECTS(env.ownerCount(alice) == 17, std::to_string(env.ownerCount(alice)));
}
}
}
void
testLargeWasmModules(FeatureBitset features)
{
testcase("Test large wasm modules");
using namespace jtx;
using namespace std::chrono;
using namespace wasm_constants;
enum class ExpectedStatus { Success, Malformed, Crash };
auto runTest = [&](std::vector<uint8_t> const& wasm,
std::optional<uint32_t> sizeLimit,
ExpectedStatus expectedStatus,
std::source_location const& loc = std::source_location::current()) {
auto makeEnv = [&]() -> Env {
if (sizeLimit)
{
return Env(
*this,
envconfig([&sizeLimit](std::unique_ptr<Config> cfg) {
cfg->fees.bytecodeSizeLimit = *sizeLimit;
return cfg;
}),
features);
}
return Env(*this, features);
};
Env env = makeEnv();
auto const alice = Account("alice");
env.fund(XRP(1'000'000), alice);
env.close();
auto const wasmHex = strHex(wasm);
try
{
env(escrow::create(alice, alice, XRP(1000)),
escrow::Bytecode(wasmHex),
escrow::kCancelTime(env.now() + 100s),
Fee(env.current()->fees().base * 10 + wasmHex.size() / 2 * 5),
Ter(expectedStatus == ExpectedStatus::Success ? TER{tesSUCCESS}
: TER{temMALFORMED}));
if (expectedStatus == ExpectedStatus::Crash)
{
fail("Expected crash", loc.file_name(), loc.line());
}
else
{
pass();
}
}
catch (std::exception const& e)
{
if (expectedStatus == ExpectedStatus::Crash)
{
pass();
}
else
{
fail(e.what(), loc.file_name(), loc.line());
}
}
};
// Table-driven test cases
struct TestCase
{
enum class BlobType { Code, Data };
BlobType type;
uint32_t size;
std::optional<uint32_t> sizeLimit;
ExpectedStatus expected;
};
std::vector<TestCase> const testCases = {
// Code blob tests
{.type = TestCase::BlobType::Code,
.size = 99'950,
.sizeLimit = std::nullopt,
.expected = ExpectedStatus::Success}, // just under 100kb
{.type = TestCase::BlobType::Code,
.size = 99'955,
.sizeLimit = std::nullopt,
.expected = ExpectedStatus::Malformed}, // just over 100kb
{.type = TestCase::BlobType::Code,
.size = 200'000,
.sizeLimit = 10'000'000,
.expected = ExpectedStatus::Success}, // ~200kb
{.type = TestCase::BlobType::Code,
.size = 490'000,
.sizeLimit = 10'000'000,
.expected = ExpectedStatus::Success}, // just under 1MB JSON
{.type = TestCase::BlobType::Code,
.size = 999'999,
.sizeLimit = 10'000'000,
.expected = ExpectedStatus::Crash}, // just over 1MB JSON
// Data blob tests
{.type = TestCase::BlobType::Data,
.size = 99'939,
.sizeLimit = std::nullopt,
.expected = ExpectedStatus::Success}, // just under 100kb
{.type = TestCase::BlobType::Data,
.size = 99'941,
.sizeLimit = std::nullopt,
.expected = ExpectedStatus::Malformed}, // just over 100kb
{.type = TestCase::BlobType::Data,
.size = 200'000,
.sizeLimit = 10'000'000,
.expected = ExpectedStatus::Success}, // ~200kb
{.type = TestCase::BlobType::Data,
.size = 490'000,
.sizeLimit = 10'000'000,
.expected = ExpectedStatus::Success}, // just under 1MB JSON
{.type = TestCase::BlobType::Data,
.size = 999'950,
.sizeLimit = 10'000'000,
.expected = ExpectedStatus::Crash}, // just over 1MB JSON
};
for (auto const& tc : testCases)
{
auto const wasm = tc.type == TestCase::BlobType::Code ? generateCodeBlob(tc.size)
: generateDataBlob(tc.size);
runTest(wasm, tc.sizeLimit, tc.expected);
}
}
void
testWithFeats(FeatureBitset features)
{
testCreateBytecodePreflight(features);
testFinishWasmFailures(features);
testBytecode(features);
testUpdateDataOnFailure(features);
testFees(features);
// TODO: Update module with new host functions
testAllHostFunctions(features);
testKeyletHostFunctions(features);
testLargeWasmModules(features);
}
public:
void
run() override
{
using namespace test::jtx;
FeatureBitset const all{testableAmendments()};
testWithFeats(all);
}
};
BEAST_DEFINE_TESTSUITE(EscrowSmart, app, xrpl);
} // namespace xrpl::test