From 4318b2ebf7031d472833459bf1a34b42cf12db3f Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Fri, 6 Feb 2026 12:44:05 -0500 Subject: [PATCH 1/2] fix build issue --- src/test/app/wasm_fixtures/fixtures.cpp | 6 +++--- src/test/app/wasm_fixtures/fixtures.h | 24 ++++++++++++------------ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/test/app/wasm_fixtures/fixtures.cpp b/src/test/app/wasm_fixtures/fixtures.cpp index f5d4ac53ec..9e1783ef50 100644 --- a/src/test/app/wasm_fixtures/fixtures.cpp +++ b/src/test/app/wasm_fixtures/fixtures.cpp @@ -23,13 +23,11 @@ pushLeb128(std::vector& 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 void appendBytes(std::vector& 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 generateCodeBlob(uint32_t num_instructions) { std::vector 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 generateDataBlob(uint32_t data_size) { std::vector 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); diff --git a/src/test/app/wasm_fixtures/fixtures.h b/src/test/app/wasm_fixtures/fixtures.h index 7dcba6eb89..50ab5d653e 100644 --- a/src/test/app/wasm_fixtures/fixtures.h +++ b/src/test/app/wasm_fixtures/fixtures.h @@ -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 From fd14054f173c60cc7b3a41eb483e2a109c7dffa5 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Mon, 9 Feb 2026 17:36:02 -0500 Subject: [PATCH 2/2] update preflight checks (#6094) --- include/xrpl/protocol/TER.h | 1 + src/libxrpl/protocol/TER.cpp | 1 + src/test/app/EscrowSmart_test.cpp | 158 ++++++++++++++++++++++++----- src/xrpld/app/tx/detail/Escrow.cpp | 57 ++++++++--- src/xrpld/app/tx/detail/Escrow.h | 3 + 5 files changed, 178 insertions(+), 42 deletions(-) diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index fcc123a05f..e5be836a0f 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -123,6 +123,7 @@ enum TEMcodes : TERUnderlyingType { temINVALID_INNER_BATCH, temBAD_WASM, + temTEMP_DISABLED, }; //------------------------------------------------------------------------------ diff --git a/src/libxrpl/protocol/TER.cpp b/src/libxrpl/protocol/TER.cpp index 46f1c82bc9..1e33ee5490 100644 --- a/src/libxrpl/protocol/TER.cpp +++ b/src/libxrpl/protocol/TER.cpp @@ -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."), diff --git a/src/test/app/EscrowSmart_test.cpp b/src/test/app/EscrowSmart_test.cpp index 093e65cb1b..465d934f9b 100644 --- a/src/test/app/EscrowSmart_test.cpp +++ b/src/test/app/EscrowSmart_test.cpp @@ -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 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 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 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(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"}; diff --git a/src/xrpld/app/tx/detail/Escrow.cpp b/src/xrpld/app/tx/detail/Escrow.cpp index 20cb83b64f..bfce567f55 100644 --- a/src/xrpld/app/tx/detail/Escrow.cpp +++ b/src/xrpld/app/tx/detail/Escrow.cpp @@ -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(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; } diff --git a/src/xrpld/app/tx/detail/Escrow.h b/src/xrpld/app/tx/detail/Escrow.h index 6050116311..c501f6bd1d 100644 --- a/src/xrpld/app/tx/detail/Escrow.h +++ b/src/xrpld/app/tx/detail/Escrow.h @@ -25,6 +25,9 @@ public: static NotTEC preflight(PreflightContext const& ctx); + static NotTEC + preflightSigValidated(PreflightContext const& ctx); + static TER preclaim(PreclaimContext const& ctx);