Merge remote-tracking branch 'upstream/ripple/smart-escrow' into ripple/se/supported

This commit is contained in:
Mayukha Vadari
2026-02-10 17:24:57 -05:00
7 changed files with 193 additions and 57 deletions

View File

@@ -123,6 +123,7 @@ enum TEMcodes : TERUnderlyingType {
temINVALID_INNER_BATCH,
temBAD_WASM,
temTEMP_DISABLED,
};
//------------------------------------------------------------------------------

View File

@@ -202,6 +202,7 @@ transResults()
MAKE_ERROR(temBAD_TRANSFER_FEE, "Malformed: Transfer fee is outside valid range."),
MAKE_ERROR(temINVALID_INNER_BATCH, "Malformed: Invalid inner batch transaction."),
MAKE_ERROR(temBAD_WASM, "Malformed: Provided WASM code is invalid."),
MAKE_ERROR(temTEMP_DISABLED, "The transaction requires logic that is currently temporarily disabled."),
MAKE_ERROR(terRETRY, "Retry transaction."),
MAKE_ERROR(terFUNDS_SPENT, "DEPRECATED."),

View File

@@ -32,23 +32,22 @@ struct EscrowSmart_test : public beast::unit_test::suite
// Tests whether the ledger index is >= 5
// getLedgerSqn() >= 5}
static auto wasmHex = ledgerSqnWasmHex;
{
// featureSmartEscrow disabled
Env env(*this, features - featureSmartEscrow);
env.fund(XRP(5000), alice, carol);
XRPAmount const txnFees = env.current()->fees().base + 1000;
auto escrowCreate = escrow::create(alice, carol, XRP(1000));
auto const escrowCreate = escrow::create(alice, carol, XRP(1000));
env(escrowCreate,
escrow::finish_function(wasmHex),
escrow::finish_function(ledgerSqnWasmHex),
escrow::cancel_time(env.now() + 100s),
fee(txnFees),
ter(temDISABLED));
env.close();
env(escrowCreate,
escrow::finish_function(wasmHex),
escrow::finish_function(ledgerSqnWasmHex),
escrow::cancel_time(env.now() + 100s),
escrow::data("00112233"),
fee(txnFees),
@@ -69,10 +68,10 @@ struct EscrowSmart_test : public beast::unit_test::suite
// create escrow
env.fund(XRP(5000), alice, carol);
auto escrowCreate = escrow::create(alice, carol, XRP(500));
auto const escrowCreate = escrow::create(alice, carol, XRP(500));
// 11-byte string
std::string longWasmHex = "00112233445566778899AA";
std::string const longWasmHex = "00112233445566778899AA";
env(escrowCreate,
escrow::finish_function(longWasmHex),
escrow::cancel_time(env.now() + 100s),
@@ -81,6 +80,62 @@ struct EscrowSmart_test : public beast::unit_test::suite
env.close();
}
{
// compute limit set to 0
Env env(
*this,
envconfig([](std::unique_ptr<Config> cfg) {
// WASM runtime disabled
cfg->FEES.extension_compute_limit = 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::finish_function(ledgerSqnWasmHex),
escrow::cancel_time(env.now() + 100s),
escrow::comp_allowance(100),
fee(txnFees),
ter(temMALFORMED));
env.close();
}
{
// size limit set to 0
Env env(
*this,
envconfig([](std::unique_ptr<Config> cfg) {
cfg->FEES.extension_size_limit = 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::finish_function("AA"),
escrow::cancel_time(env.now() + 100s),
fee(txnFees),
ter(temTEMP_DISABLED));
env.close();
env(escrowCreate,
escrow::finish_function(ledgerSqnWasmHex),
escrow::cancel_time(env.now() + 100s),
fee(txnFees),
ter(temTEMP_DISABLED));
env.close();
}
{
// Data without FinishFunction
Env env(*this, features);
@@ -88,9 +143,9 @@ struct EscrowSmart_test : public beast::unit_test::suite
// create escrow
env.fund(XRP(5000), alice, carol);
auto escrowCreate = escrow::create(alice, carol, XRP(500));
auto const escrowCreate = escrow::create(alice, carol, XRP(500));
std::string longData(4, 'A');
std::string const longData(4, 'A');
env(escrowCreate,
escrow::data(longData),
escrow::finish_time(env.now() + 100s),
@@ -106,13 +161,13 @@ struct EscrowSmart_test : public beast::unit_test::suite
// create escrow
env.fund(XRP(5000), alice, carol);
auto escrowCreate = escrow::create(alice, carol, XRP(500));
auto const escrowCreate = escrow::create(alice, carol, XRP(500));
// string of length maxWasmDataLength * 2 + 2
std::string longData(maxWasmDataLength * 2 + 2, 'B');
std::string const longData((maxWasmDataLength + 1) * 2, 'B');
env(escrowCreate,
escrow::data(longData),
escrow::finish_function(wasmHex),
escrow::finish_function(ledgerSqnWasmHex),
escrow::cancel_time(env.now() + 100s),
fee(txnFees),
ter(temMALFORMED));
@@ -126,7 +181,7 @@ struct EscrowSmart_test : public beast::unit_test::suite
return cfg;
}),
features);
XRPAmount const txnFees = env.current()->fees().base * 10 + wasmHex.size() / 2 * 5;
XRPAmount const txnFees = env.current()->fees().base * 10 + ledgerSqnWasmHex.size() / 2 * 5;
// create escrow
env.fund(XRP(5000), alice, carol);
@@ -135,13 +190,16 @@ struct EscrowSmart_test : public beast::unit_test::suite
// Success situations
{
// FinishFunction + CancelAfter
env(escrowCreate, escrow::finish_function(wasmHex), escrow::cancel_time(env.now() + 20s), fee(txnFees));
env(escrowCreate,
escrow::finish_function(ledgerSqnWasmHex),
escrow::cancel_time(env.now() + 20s),
fee(txnFees));
env.close();
}
{
// FinishFunction + Condition + CancelAfter
env(escrowCreate,
escrow::finish_function(wasmHex),
escrow::finish_function(ledgerSqnWasmHex),
escrow::cancel_time(env.now() + 30s),
escrow::condition(escrow::cb1),
fee(txnFees));
@@ -150,7 +208,7 @@ struct EscrowSmart_test : public beast::unit_test::suite
{
// FinishFunction + FinishAfter + CancelAfter
env(escrowCreate,
escrow::finish_function(wasmHex),
escrow::finish_function(ledgerSqnWasmHex),
escrow::cancel_time(env.now() + 40s),
escrow::finish_time(env.now() + 2s),
fee(txnFees));
@@ -159,7 +217,7 @@ struct EscrowSmart_test : public beast::unit_test::suite
{
// FinishFunction + FinishAfter + Condition + CancelAfter
env(escrowCreate,
escrow::finish_function(wasmHex),
escrow::finish_function(ledgerSqnWasmHex),
escrow::cancel_time(env.now() + 50s),
escrow::condition(escrow::cb1),
escrow::finish_time(env.now() + 2s),
@@ -170,13 +228,13 @@ struct EscrowSmart_test : public beast::unit_test::suite
// Failure situations (i.e. all other combinations)
{
// only FinishFunction
env(escrowCreate, escrow::finish_function(wasmHex), fee(txnFees), ter(temBAD_EXPIRATION));
env(escrowCreate, escrow::finish_function(ledgerSqnWasmHex), fee(txnFees), ter(temBAD_EXPIRATION));
env.close();
}
{
// FinishFunction + FinishAfter
env(escrowCreate,
escrow::finish_function(wasmHex),
escrow::finish_function(ledgerSqnWasmHex),
escrow::finish_time(env.now() + 2s),
fee(txnFees),
ter(temBAD_EXPIRATION));
@@ -185,7 +243,7 @@ struct EscrowSmart_test : public beast::unit_test::suite
{
// FinishFunction + Condition
env(escrowCreate,
escrow::finish_function(wasmHex),
escrow::finish_function(ledgerSqnWasmHex),
escrow::condition(escrow::cb1),
fee(txnFees),
ter(temBAD_EXPIRATION));
@@ -194,7 +252,7 @@ struct EscrowSmart_test : public beast::unit_test::suite
{
// FinishFunction + FinishAfter + Condition
env(escrowCreate,
escrow::finish_function(wasmHex),
escrow::finish_function(ledgerSqnWasmHex),
escrow::condition(escrow::cb1),
escrow::finish_time(env.now() + 2s),
fee(txnFees),
@@ -213,7 +271,7 @@ struct EscrowSmart_test : public beast::unit_test::suite
{
// Not enough fees
env(escrowCreate,
escrow::finish_function(wasmHex),
escrow::finish_function(ledgerSqnWasmHex),
escrow::cancel_time(env.now() + 70s),
fee(txnFees - 1),
ter(telINSUF_FEE_P));
@@ -257,13 +315,12 @@ struct EscrowSmart_test : public beast::unit_test::suite
// Tests whether the ledger index is >= 5
// getLedgerSqn() >= 5}
static auto wasmHex = ledgerSqnWasmHex;
{
// featureSmartEscrow disabled
Env env(*this, features - featureSmartEscrow);
env.fund(XRP(5000), alice, carol);
XRPAmount const txnFees = env.current()->fees().base * 10 + wasmHex.size() / 2 * 5;
XRPAmount const txnFees = env.current()->fees().base * 10 + ledgerSqnWasmHex.size() / 2 * 5;
env(escrow::finish(carol, alice, 1), fee(txnFees), escrow::comp_allowance(4), ter(temDISABLED));
env.close();
}
@@ -291,6 +348,56 @@ struct EscrowSmart_test : public beast::unit_test::suite
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.extension_compute_limit = 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().openLedger().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(sfFinishFunction, strUnHex(ledgerSqnWasmHex).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::comp_allowance(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
@@ -299,13 +406,13 @@ struct EscrowSmart_test : public beast::unit_test::suite
for (auto i = env.current()->seq(); i <= 257; ++i)
env.close();
XRPAmount const txnFees = env.current()->fees().base * 10 + wasmHex.size() / 2 * 5;
XRPAmount const txnFees = env.current()->fees().base * 10 + ledgerSqnWasmHex.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::finish_function(wasmHex),
escrow::finish_function(ledgerSqnWasmHex),
escrow::cancel_time(env.now() + 100s),
fee(txnFees));
env.close();
@@ -778,7 +885,6 @@ struct EscrowSmart_test : public beast::unit_test::suite
using namespace jtx;
using namespace std::chrono;
// TODO: create wasm module for all host functions
Account const alice{"alice"};
Account const carol{"carol"};

View File

@@ -23,13 +23,11 @@ pushLeb128(std::vector<uint8_t>& buf, uint32_t val)
}
// Helper: append bytes from a C-style array to a vector
// Uses a loop to avoid GCC false positive -Werror=stringop-overflow with insert()
template <std::size_t N>
void
appendBytes(std::vector<uint8_t>& buf, uint8_t const (&arr)[N])
{
for (std::size_t i = 0; i < N; ++i)
buf.push_back(arr[i]);
buf.insert(buf.end(), &arr[0], &arr[N]);
}
// Helper: append bytes from a vector to a vector
@@ -59,6 +57,7 @@ std::vector<uint8_t>
generateCodeBlob(uint32_t num_instructions)
{
std::vector<uint8_t> wasm;
wasm.reserve(sizeof(WASM_HEADER) + sizeof(TYPE_EMPTY_FUNC) + sizeof(FUNC_TYPE0) + sizeof(EXPORT_FINISH));
appendBytes(wasm, WASM_HEADER);
appendBytes(wasm, TYPE_EMPTY_FUNC);
appendBytes(wasm, FUNC_TYPE0);
@@ -82,6 +81,7 @@ std::vector<uint8_t>
generateDataBlob(uint32_t data_size)
{
std::vector<uint8_t> wasm;
wasm.reserve(sizeof(WASM_HEADER) + sizeof(TYPE_EMPTY_FUNC) + sizeof(FUNC_TYPE0));
appendBytes(wasm, WASM_HEADER);
appendBytes(wasm, TYPE_EMPTY_FUNC);
appendBytes(wasm, FUNC_TYPE0);

View File

@@ -10,7 +10,7 @@
namespace wasm_constants {
// Magic + version header
static constexpr uint8_t WASM_HEADER[] = {
uint8_t const WASM_HEADER[] = {
0x00,
0x61,
0x73,
@@ -22,31 +22,31 @@ static constexpr uint8_t WASM_HEADER[] = {
};
// Type section: () -> ()
static constexpr uint8_t TYPE_EMPTY_FUNC[] = {0x01, 0x04, 0x01, 0x60, 0x00, 0x00};
uint8_t const TYPE_EMPTY_FUNC[] = {0x01, 0x04, 0x01, 0x60, 0x00, 0x00};
// Function section: one function using type 0
static constexpr uint8_t FUNC_TYPE0[] = {0x03, 0x02, 0x01, 0x00};
uint8_t const FUNC_TYPE0[] = {0x03, 0x02, 0x01, 0x00};
// Export section: export func 0 as "finish"
static constexpr uint8_t EXPORT_FINISH[] = {0x07, 0x0a, 0x01, 0x06, 'f', 'i', 'n', 'i', 's', 'h', 0x00, 0x00};
uint8_t const EXPORT_FINISH[] = {0x07, 0x0a, 0x01, 0x06, 'f', 'i', 'n', 'i', 's', 'h', 0x00, 0x00};
// Empty function body: 0 locals, end
static constexpr uint8_t EMPTY_BODY[] = {0x00, 0x0b};
uint8_t const EMPTY_BODY[] = {0x00, 0x0b};
// Data segment offset: i32.const 0, end
static constexpr uint8_t DATA_OFFSET_ZERO[] = {0x41, 0x00, 0x0b};
uint8_t const DATA_OFFSET_ZERO[] = {0x41, 0x00, 0x0b};
// Section IDs
static constexpr uint8_t SECTION_MEMORY = 0x05;
static constexpr uint8_t SECTION_CODE = 0x0a;
static constexpr uint8_t SECTION_DATA = 0x0b;
uint8_t const SECTION_MEMORY = 0x05;
uint8_t const SECTION_CODE = 0x0a;
uint8_t const SECTION_DATA = 0x0b;
// Instructions
static constexpr uint8_t INSTR_NOP = 0x01;
static constexpr uint8_t INSTR_END = 0x0b;
uint8_t const INSTR_NOP = 0x01;
uint8_t const INSTR_END = 0x0b;
// Fill byte for data section bloat
static constexpr uint8_t DATA_FILL_BYTE = 0xEE;
uint8_t const DATA_FILL_BYTE = 0xEE;
// Generator for WASM module with large code section (many NOPs)
std::vector<uint8_t>

View File

@@ -197,12 +197,32 @@ EscrowCreate::preflight(PreflightContext const& ctx)
if (ctx.tx.isFieldPresent(sfFinishFunction))
{
if (ctx.app.config().FEES.extension_size_limit == 0 || ctx.app.config().FEES.extension_compute_limit == 0)
{
JLOG(ctx.j.debug()) << "WASM runtime deactivated by fee voting";
return temTEMP_DISABLED;
}
auto const code = ctx.tx.getFieldVL(sfFinishFunction);
if (code.size() == 0 || code.size() > ctx.app.config().FEES.extension_size_limit)
{
JLOG(ctx.j.debug()) << "EscrowCreate.FinishFunction bad size " << code.size();
return temMALFORMED;
}
// actual validity of WASM code happens in `preflightSigValidated`
// (after the signature is checked)
}
return tesSUCCESS;
}
NotTEC
EscrowCreate::preflightSigValidated(PreflightContext const& ctx)
{
if (ctx.tx.isFieldPresent(sfFinishFunction))
{
auto const code = ctx.tx.getFieldVL(sfFinishFunction);
// basic checks happen in `preflight`
auto mock(std::make_shared<HostFunctions>(ctx.j));
auto const re = preflightEscrowWasm(code, mock, ESCROW_FUNCTION_NAME);
@@ -622,6 +642,27 @@ EscrowFinish::preflight(PreflightContext const& ctx)
return temMALFORMED;
}
if (auto const allowance = ctx.tx[~sfComputationAllowance]; allowance)
{
if (ctx.app.config().FEES.extension_compute_limit == 0)
{
JLOG(ctx.j.debug()) << "WASM runtime deactivated by fee voting";
return temTEMP_DISABLED;
}
if (*allowance == 0)
{
return temBAD_LIMIT;
}
if (*allowance > ctx.app.config().FEES.extension_compute_limit)
{
JLOG(ctx.j.debug()) << "ComputationAllowance too large: " << *allowance;
return temBAD_LIMIT;
}
}
if (auto const err = credentials::checkFields(ctx.tx, ctx.j); !isTesSuccess(err))
return err;
return tesSUCCESS;
}
@@ -650,22 +691,6 @@ EscrowFinish::preflightSigValidated(PreflightContext const& ctx)
}
}
if (auto const allowance = ctx.tx[~sfComputationAllowance]; allowance)
{
if (*allowance == 0)
{
return temBAD_LIMIT;
}
if (*allowance > ctx.app.config().FEES.extension_compute_limit)
{
JLOG(ctx.j.debug()) << "ComputationAllowance too large: " << *allowance;
return temBAD_LIMIT;
}
}
if (auto const err = credentials::checkFields(ctx.tx, ctx.j); !isTesSuccess(err))
return err;
return tesSUCCESS;
}

View File

@@ -25,6 +25,9 @@ public:
static NotTEC
preflight(PreflightContext const& ctx);
static NotTEC
preflightSigValidated(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);