diff --git a/src/test/app/EscrowSmart_test.cpp b/src/test/app/EscrowSmart_test.cpp index ec173b2cbd..f499db7893 100644 --- a/src/test/app/EscrowSmart_test.cpp +++ b/src/test/app/EscrowSmart_test.cpp @@ -12,6 +12,7 @@ #include #include +#include namespace xrpl { namespace test { @@ -982,6 +983,129 @@ struct EscrowSmart_test : public beast::unit_test::suite } } + 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 const& wasm, + std::optional 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 cfg) { + cfg->FEES.extension_size_limit = *sizeLimit; + return cfg; + }), + features); + else + 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::finish_function(wasmHex), + escrow::cancel_time(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 sizeLimit; + ExpectedStatus expected; + }; + + std::vector const testCases = { + // Code blob tests + {TestCase::BlobType::Code, + 99'959, + std::nullopt, + ExpectedStatus::Success}, // just under 100kb + {TestCase::BlobType::Code, + 99'961, + std::nullopt, + ExpectedStatus::Malformed}, // just over 100kb + {TestCase::BlobType::Code, + 200'000, + 10'000'000, + ExpectedStatus::Success}, // ~200kb + {TestCase::BlobType::Code, + 490'000, + 10'000'000, + ExpectedStatus::Success}, // just under 1MB JSON + {TestCase::BlobType::Code, + 999'999, + 10'000'000, + ExpectedStatus::Crash}, // just over 1MB JSON + // Data blob tests + {TestCase::BlobType::Data, + 99'946, + std::nullopt, + ExpectedStatus::Success}, // just under 100kb + {TestCase::BlobType::Data, + 99'948, + std::nullopt, + ExpectedStatus::Malformed}, // just over 100kb + {TestCase::BlobType::Data, + 200'000, + 10'000'000, + ExpectedStatus::Success}, // ~200kb + {TestCase::BlobType::Data, + 490'000, + 10'000'000, + ExpectedStatus::Success}, // just under 1MB JSON + {TestCase::BlobType::Data, + 999'950, + 10'000'000, + 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) { @@ -994,6 +1118,8 @@ struct EscrowSmart_test : public beast::unit_test::suite // TODO: Update module with new host functions testAllHostFunctions(features); testKeyletHostFunctions(features); + + testLargeWasmModules(features); } public: diff --git a/src/test/app/wasm_fixtures/fixtures.cpp b/src/test/app/wasm_fixtures/fixtures.cpp index 43f0bb5db4..748d262d22 100644 --- a/src/test/app/wasm_fixtures/fixtures.cpp +++ b/src/test/app/wasm_fixtures/fixtures.cpp @@ -2,6 +2,116 @@ #include +#include + +namespace wasm_constants { + +namespace { + +// Helper: Variable-length integer encoding (LEB128) +void +pushLeb128(std::vector& buf, uint32_t val) +{ + do + { + uint8_t byte = val & 0x7F; + val >>= 7; + if (val != 0) + byte |= 0x80; + buf.push_back(byte); + } while (val != 0); +} + +// Helper: append bytes from an array to a vector +template +void +appendBytes(std::vector& buf, T const& arr) +{ + buf.insert(buf.end(), std::begin(arr), std::end(arr)); +} + +// Helper: append a WASM section (ID + LEB128 size + content) +// extraSize is added to the encoded size (for trailing fill bytes) +void +appendSection( + std::vector& wasm, + uint8_t sectionId, + std::vector const& content, + uint32_t extraSize = 0) +{ + wasm.push_back(sectionId); + pushLeb128(wasm, static_cast(content.size() + extraSize)); + appendBytes(wasm, content); +} + +} // namespace + +std::vector +generateCodeBlob(uint32_t num_instructions) +{ + std::vector wasm; + appendBytes(wasm, WASM_HEADER); + appendBytes(wasm, TYPE_EMPTY_FUNC); + appendBytes(wasm, FUNC_TYPE0); + appendBytes(wasm, EXPORT_FINISH); + + std::vector body; + pushLeb128(body, 0); // No locals + body.insert(body.end(), num_instructions, INSTR_NOP); + body.push_back(INSTR_END); + + std::vector section; + pushLeb128(section, 1); // 1 function + pushLeb128(section, static_cast(body.size())); + appendBytes(section, body); + + appendSection(wasm, SECTION_CODE, section); + return wasm; +} + +std::vector +generateDataBlob(uint32_t data_size) +{ + std::vector wasm; + appendBytes(wasm, WASM_HEADER); + appendBytes(wasm, TYPE_EMPTY_FUNC); + appendBytes(wasm, FUNC_TYPE0); + + // Memory Section: must be large enough for data_size + uint32_t pages = (data_size + 65535) / 65536; + std::vector mem_p; + pushLeb128(mem_p, 1); // 1 memory defined + mem_p.push_back(0x00); // Flags (minimum only) + pushLeb128(mem_p, pages); // Page count + appendSection(wasm, SECTION_MEMORY, mem_p); + + appendBytes(wasm, EXPORT_FINISH); + + // Code Section: MUST come before Data Section per WASM spec + std::vector code_p; + pushLeb128(code_p, 1); // 1 function body + pushLeb128(code_p, static_cast(std::size(EMPTY_BODY))); + appendBytes(code_p, EMPTY_BODY); + appendSection(wasm, SECTION_CODE, code_p); + + // Data Section: the actual bloat + std::vector data_seg; + data_seg.push_back(0x00); // Memory index 0 + appendBytes(data_seg, DATA_OFFSET_ZERO); + pushLeb128(data_seg, data_size); + + std::vector data_p; + pushLeb128(data_p, 1); // 1 data segment + appendBytes(data_p, data_seg); + + appendSection(wasm, SECTION_DATA, data_p, data_size); + wasm.insert(wasm.end(), data_size, DATA_FILL_BYTE); + + return wasm; +} + +} // namespace wasm_constants + extern std::string const fibWasmHex = "0061736d0100000001090260000060017f017f0303020001071b02115f5f" "7761736d5f63616c6c5f63746f727300000366696200010a440202000b3f" diff --git a/src/test/app/wasm_fixtures/fixtures.h b/src/test/app/wasm_fixtures/fixtures.h index 91eadba0d3..bba34e6353 100644 --- a/src/test/app/wasm_fixtures/fixtures.h +++ b/src/test/app/wasm_fixtures/fixtures.h @@ -2,7 +2,63 @@ // TODO: consider moving these to separate files (and figure out the build) +#include #include +#include + +// WASM binary format constants and helpers for building test modules +namespace wasm_constants { + +// Magic + version header +static constexpr uint8_t WASM_HEADER[] = { + 0x00, + 0x61, + 0x73, + 0x6d, // magic: \0asm + 0x01, + 0x00, + 0x00, + 0x00 // version: 1 +}; + +// Type section: () -> () +static constexpr uint8_t 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}; + +// 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}; + +// Empty function body: 0 locals, end +static constexpr uint8_t EMPTY_BODY[] = {0x00, 0x0b}; + +// Data segment offset: i32.const 0, end +static constexpr uint8_t 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; + +// Instructions +static constexpr uint8_t INSTR_NOP = 0x01; +static constexpr uint8_t INSTR_END = 0x0b; + +// Fill byte for data section bloat +static constexpr uint8_t DATA_FILL_BYTE = 0xEE; + +// Generator for WASM module with large code section (many NOPs) +std::vector +generateCodeBlob(uint32_t num_instructions); + +// Generator for WASM module with large data section +std::vector +generateDataBlob(uint32_t data_size); + +} // namespace wasm_constants extern std::string const ledgerSqnWasmHex;