Compare commits

..

14 Commits

Author SHA1 Message Date
TimothyBanks
82876ca47c chore: Address review comments 2026-08-19 16:04:31 -04:00
TimothyBanks
ec64f23736 fix: Additional AccountKeyley test cases 2026-08-18 13:07:52 -04:00
TimothyBanks
dc0326a2a0 fix: Use local variable for makeHost where applicable 2026-08-18 13:00:21 -04:00
TimothyBanks
80d8e827d7 fix: Update CacheLedgerObjImpl to use implicit parameter 2026-08-18 11:57:56 -04:00
TimothyBanks
b84d25fbed fix: Update test names on AmmKeylet tests 2026-08-18 11:35:59 -04:00
TimothyBanks
fda9c6995e fix: Add std::source_location to expectValue and expectError functions 2026-08-18 11:32:58 -04:00
TimothyBanks
c6c68090f2 fix: Move float constants under FloatTest 2026-08-18 11:28:18 -04:00
TimothyBanks
6c32ca3538 fix: Cleanup per self review of code 2026-08-17 15:10:35 -04:00
TimothyBanks
7122bb8a43 fix: Make Wasm_tests.cpp run again 2026-08-17 15:01:46 -04:00
TimothyBanks
c5524e4881 fix: Port WASM host function tests to new WASM design 2026-08-17 14:24:34 -04:00
TimothyBanks
e863db5061 feat: Porting wasm host function tests to new design 2026-08-15 19:41:32 -04:00
Sergey Kuznetsov
d469fc2cdf Fix clang-tidy 2026-08-13 14:14:16 +01:00
Sergey Kuznetsov
362ea7a5d1 More tests 2026-08-13 13:59:25 +01:00
Sergey Kuznetsov
1b4fede15b Add unit tests for host function 2026-08-12 18:04:54 +01:00
137 changed files with 11016 additions and 6691 deletions

View File

@@ -821,4 +821,24 @@ mod tests {
"one {MAX_FIELD_BYTES}-byte value against a {TRANSFER_LIMIT_BYTES}-byte budget"
);
}
#[test]
fn read_u32_arg_success() {
let number: u32 = 0x12345678;
let le_array: [u8; 4] = number.to_le_bytes();
assert_eq!(le_array, [0x78, 0x56, 0x34, 0x12]);
let result = read_u32_arg(&le_array);
assert!(result.is_ok());
assert_eq!(result.unwrap(), number.try_into().unwrap());
}
#[test]
fn read_u32_arg_invalid_length() {
let le_array = [0x56, 0x34, 0x12];
let result = read_u32_arg(&le_array);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), HostError::InvalidParams);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -140,7 +140,9 @@ public:
return Bytes{s.begin(), s.end()};
}
return std::unexpected(HostFunctionError::Unimplemented);
// FieldNotFound is a guest-returnable code (the contract handles a negative result);
// Unimplemented now maps to a fatal Fault::Internal (tecINTERNAL) that stops the run.
return std::unexpected(HostFunctionError::FieldNotFound);
}
[[nodiscard]] std::expected<Bytes, HostFunctionError>

View File

@@ -1,10 +1,3 @@
// Not built. These suites drive a C++ wasm engine interface -- WasmVM over the wasm.h C API,
// HostFuncWrapper, WasmImportsHelper -- that this tree does not provide; the VM lives in the
// Rust crates. Kept as the coverage target for the port. The body is one comment block, and
// the fixtures it reads (wasm_fixtures/fixtures.cpp) are disabled the same way; re-enabling
// the suites means uncommenting both.
/*
#include <expected>
#ifdef _DEBUG
// #define DEBUG_OUTPUT 1
@@ -18,15 +11,11 @@
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/tx/wasm/HostFunc.h>
#include <xrpl/tx/wasm/HostFuncWrapper.h> // IWYU pragma: keep
#include <xrpl/tx/wasm/WasmCommon.h>
#include <xrpl/tx/wasm/WasmImportsHelper.h>
#include <xrpl/tx/wasm/WasmVM.h>
#include <boost/algorithm/hex.hpp>
#include <wasm.h>
#include <cstdint>
#include <limits>
#include <source_location>
@@ -35,20 +24,6 @@
namespace xrpl::test {
bool
testGetDataIncrement();
using Add_proto = int32_t(int32_t, int32_t);
static wasm_trap_t*
add(HostFunctions&, wasm_val_vec_t const* params, wasm_val_vec_t* results)
{
int32_t const val1 = params->data[0].of.i32;
int32_t const val2 = params->data[1].of.i32;
// printf("Host function \"Add\": %d + %d\n", Val1, Val2);
results->data[0] = WASM_I32_VAL(val1 + val2);
return nullptr;
}
std::vector<uint8_t>
hexToBytes(std::string const& hex)
{
@@ -73,59 +48,6 @@ struct Wasm_test : public beast::unit_test::Suite
}
}
void
testGetDataHelperFunctions()
{
testcase("getData helper functions");
BEAST_EXPECT(testGetDataIncrement());
}
void
testWasmLib()
{
testcase("wasm lib test");
// clang-format off
// The WASM module buffer. //
Bytes const wasm = {// WASM header //
0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00,
// Type section //
0x01, 0x07, 0x01,
// function type {i32, i32} -> {i32} //
0x60, 0x02, 0x7F, 0x7F, 0x01, 0x7F,
// Import section //
0x02, 0x13, 0x01,
// module name: "extern" //
0x06, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6E,
// extern name: "func-add" //
0x08, 0x66, 0x75, 0x6E, 0x63, 0x2D, 0x61, 0x64, 0x64,
// import desc: func 0 //
0x00, 0x00,
// Function section //
0x03, 0x02, 0x01, 0x00,
// Export section //
0x07, 0x0A, 0x01,
// export name: "addTwo" //
0x06, 0x61, 0x64, 0x64, 0x54, 0x77, 0x6F,
// export desc: func 0 //
0x00, 0x01,
// Code section //
0x0A, 0x0A, 0x01,
// code body //
0x08, 0x00, 0x20, 0x00, 0x20, 0x01, 0x10, 0x00, 0x0B};
// clang-format on
auto& vm = WasmEngine::instance();
HostFunctions hfs;
ImportVec imports;
WasmImpFunc<Add_proto>(imports, "func-add", add, hfs);
auto re = vm.run(wasm, hfs, 10'000'000, "addTwo", wasmParams(1234, 5678), imports);
// if (res) printf("invokeAdd get the result: %d\n", res.value());
checkResult(re, 6'912, 59);
}
void
testBadWasm()
{
@@ -140,7 +62,7 @@ struct Wasm_test : public beast::unit_test::Suite
auto wasm = hexToBytes("00000000");
std::string const funcName("mock_escrow");
auto re = runEscrowWasm(wasm, hfs, 15, funcName, {});
auto re = runEscrowWasm(wasm, hfs, 15, funcName);
BEAST_EXPECT(!re);
}
@@ -148,7 +70,7 @@ struct Wasm_test : public beast::unit_test::Suite
auto wasm = hexToBytes("00112233445566778899AA");
std::string const funcName("mock_escrow");
auto const re = preflightEscrowWasm(wasm, hfs, funcName);
auto const re = preflightEscrowWasm(wasm, env.journal, funcName);
BEAST_EXPECT(!isTesSuccess(re));
}
@@ -169,112 +91,11 @@ struct Wasm_test : public beast::unit_test::Suite
"732b087369676e2d6578742b0f7265666572656e63652d74797065732b0a"
"6d756c746976616c7565");
auto const re = preflightEscrowWasm(badWasm, hfs, escrowFunctionName);
auto const re = preflightEscrowWasm(badWasm, env.journal, escrowFunctionName);
BEAST_EXPECT(!isTesSuccess(re));
}
}
void
testWasmLedgerSqn()
{
testcase("Wasm get ledger sequence");
auto ledgerSqnWasm = hexToBytes(kLedgerSqnWasmHex);
using namespace test::jtx;
Env env{*this};
TestLedgerDataProvider hfs(env);
ImportVec imports;
WASM_IMPORT_FUNC2(imports, getLedgerSqn, "ldgr_index", hfs, 33);
auto& engine = WasmEngine::instance();
auto re =
engine.run(ledgerSqnWasm, hfs, 1'000'000, escrowFunctionName, {}, imports, env.journal);
checkResult(re, 0, 440);
env.close();
env.close();
// empty module, throwing exception
re = engine.run({}, hfs, 1'000'000, escrowFunctionName, {}, imports, env.journal);
BEAST_EXPECT(!re);
env.close();
}
void
testHFCost()
{
testcase("wasm test host functions cost");
using namespace test::jtx;
Env env(*this);
{
auto const allHostFuncWasm = hexToBytes(kAllHostFunctionsWasmHex);
auto& engine = WasmEngine::instance();
TestHostFunctions hfs(env);
auto imp = createWasmImport(hfs);
for (auto& i : imp)
i.second.second.gas = 0;
auto re = engine.run(
allHostFuncWasm, hfs, 1'000'000, escrowFunctionName, {}, imp, env.journal);
checkResult(re, 1, 30'760);
env.close();
}
env.close();
env.close();
env.close();
env.close();
env.close();
{
auto const allHostFuncWasm = hexToBytes(kAllHostFunctionsWasmHex);
auto& engine = WasmEngine::instance();
TestHostFunctions hfs(env);
auto const imp = createWasmImport(hfs);
auto re = engine.run(
allHostFuncWasm, hfs, 1'000'000, escrowFunctionName, {}, imp, env.journal);
checkResult(re, 1, 48'580);
env.close();
}
// not enough gas
{
auto const allHostFuncWasm = hexToBytes(kAllHostFunctionsWasmHex);
auto& engine = WasmEngine::instance();
TestHostFunctions hfs(env);
auto const imp = createWasmImport(hfs);
auto re =
engine.run(allHostFuncWasm, hfs, 200, escrowFunctionName, {}, imp, env.journal);
if (BEAST_EXPECT(!re))
{
// Running out of gas now terminates with tecOUT_OF_GAS (was
// previously collapsed into tecFAILED_PROCESSING).
BEAST_EXPECTS(
re.error().ter == tecOUT_OF_GAS, std::to_string(TERtoInt(re.error().ter)));
}
env.close();
}
}
void
testEscrowWasmDN()
{
@@ -286,14 +107,14 @@ struct Wasm_test : public beast::unit_test::Suite
Env env{*this};
{
TestHostFunctions hfs(env);
auto re = runEscrowWasm(allHFWasm, hfs, 100'000, escrowFunctionName, {});
auto re = runEscrowWasm(allHFWasm, hfs, 100'000, escrowFunctionName);
checkResult(re, 1, 48'580);
}
{
// Invalid gas limit (0) should be rejected (boundary condition)
TestHostFunctions hfs(env);
auto re = runEscrowWasm(allHFWasm, hfs, -1, escrowFunctionName, {});
auto re = runEscrowWasm(allHFWasm, hfs, -1, escrowFunctionName);
BEAST_EXPECT(!re.has_value());
BEAST_EXPECT(re.error().ter == temBAD_AMOUNT);
}
@@ -301,7 +122,7 @@ struct Wasm_test : public beast::unit_test::Suite
{
// Invalid gas limit (-1) should be rejected
TestHostFunctions hfs(env);
auto re = runEscrowWasm(allHFWasm, hfs, 0, escrowFunctionName, {});
auto re = runEscrowWasm(allHFWasm, hfs, 0, escrowFunctionName);
BEAST_EXPECT(!re.has_value());
BEAST_EXPECT(re.error().ter == temBAD_AMOUNT);
}
@@ -310,7 +131,7 @@ struct Wasm_test : public beast::unit_test::Suite
// max<int64_t>() gas
TestHostFunctions hfs(env);
auto re = runEscrowWasm(
allHFWasm, hfs, std::numeric_limits<int64_t>::max(), escrowFunctionName, {});
allHFWasm, hfs, std::numeric_limits<int64_t>::max(), escrowFunctionName);
checkResult(re, 1, 48'580);
}
@@ -328,7 +149,7 @@ struct Wasm_test : public beast::unit_test::Suite
};
FieldNotFoundHostFunctions hfs(env);
auto re = runEscrowWasm(allHFWasm, hfs, 100'000, escrowFunctionName, {});
auto re = runEscrowWasm(allHFWasm, hfs, 100'000, escrowFunctionName);
checkResult(re, -201, 28'329);
}
@@ -346,7 +167,7 @@ struct Wasm_test : public beast::unit_test::Suite
};
OversizedFieldHostFunctions hfs(env);
auto re = runEscrowWasm(allHFWasm, hfs, 100'000, escrowFunctionName, {});
auto re = runEscrowWasm(allHFWasm, hfs, 100'000, escrowFunctionName);
checkResult(re, -201, 28'329);
}
}
@@ -364,39 +185,11 @@ struct Wasm_test : public beast::unit_test::Suite
TestHostFunctions hfs(env);
auto const allowance = 125'667;
auto re = runEscrowWasm(codecovWasm, hfs, allowance, escrowFunctionName, {});
auto re = runEscrowWasm(codecovWasm, hfs, allowance, escrowFunctionName);
checkResult(re, 1, allowance);
}
void
testBadAlign()
{
testcase("Wasm Bad Align");
// bad_align.c
auto const badAlignWasm = hexToBytes(kBadAlignWasmHex);
using namespace test::jtx;
Env env{*this};
TestHostFunctions hfs(env);
auto imports = createWasmImport(hfs);
{ // Calls float_from_uint with bad alignment.
// Can be checked through codecov
auto& engine = WasmEngine::instance();
auto re = engine.run(badAlignWasm, hfs, 1'000'000, "test", {}, imports, env.journal);
if (BEAST_EXPECTS(re, transToken(re.error().ter)))
{
BEAST_EXPECTS(re->result == 0x47308594, std::to_string(re->result));
}
}
env.close();
}
void
testSwapBytes()
{
@@ -454,19 +247,9 @@ struct Wasm_test : public beast::unit_test::Suite
void
run() override
{
using namespace test::jtx;
testGetDataHelperFunctions();
testWasmLib();
testBadWasm();
testWasmLedgerSqn();
testHFCost();
testEscrowWasmDN();
testCodecovWasm();
testBadAlign();
testSwapBytes();
}
};
@@ -474,4 +257,3 @@ struct Wasm_test : public beast::unit_test::Suite
BEAST_DEFINE_TESTSUITE(Wasm, app, xrpl);
} // namespace xrpl::test
*/

View File

@@ -1,10 +1,3 @@
// Not built. The only reader of these blobs is the disabled suite in Wasm_test.cpp, so they
// are left out of the build and cost neither a translation unit nor static-init time.
// Regenerate the hex with copyFixtures.py.
//
// TODO: consider moving these to separate files (and figure out the build)
/*
#include <test/app/wasm_fixtures/fixtures.h>
#include <string>
@@ -654,4 +647,3 @@ extern std::string const kBadAlignWasmHex =
"32393538616631656533303861373930636664623432626432343732302900490f7461726765745f66656174757265"
"73042b0f6d757461626c652d676c6f62616c732b087369676e2d6578742b0f7265666572656e63652d74797065732b"
"0a6d756c746976616c7565";
*/

View File

@@ -1,7 +1,5 @@
#pragma once
// TODO: consider moving these to separate files (and figure out the build)
#include <string>
extern std::string const kLedgerSqnWasmHex;

View File

@@ -1,13 +1,22 @@
#pragma once
#include <xrpl/basics/Log.h>
#include <xrpl/basics/UnorderedContainers.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/core/HashRouter.h>
#include <xrpl/core/NetworkIDService.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/json/json_value.h>
#include <xrpl/ledger/AmendmentTable.h>
#include <xrpl/ledger/PendingSaves.h>
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/Rules.h>
#include <xrpl/protocol/STValidation.h>
#include <xrpl/server/LoadFeeTrack.h>
#include <boost/asio/io_context.hpp>
@@ -15,11 +24,15 @@
#include <helpers/TestFamily.h>
#include <helpers/TestSink.h>
#include <chrono>
#include <cstdint>
#include <map>
#include <memory>
#include <optional>
#include <set>
#include <stdexcept>
#include <string>
#include <vector>
namespace xrpl::test {
@@ -40,6 +53,100 @@ public:
}
};
/**
* Minimal AmendmentTable for tests.
*/
class TestAmendmentTable final : public AmendmentTable
{
public:
[[nodiscard]] uint256
find(std::string const& name) const override
{
return getRegisteredFeature(name).value_or(uint256{});
}
bool
veto(uint256 const&) override
{
throw std::logic_error("TestAmendmentTable::veto not implemented");
}
bool
unVeto(uint256 const&) override
{
throw std::logic_error("TestAmendmentTable::unVeto not implemented");
}
bool
enable(uint256 const&) override
{
throw std::logic_error("TestAmendmentTable::enable not implemented");
}
[[nodiscard]] bool
isEnabled(uint256 const&) const override
{
throw std::logic_error("TestAmendmentTable::isEnabled not implemented");
}
[[nodiscard]] bool
isSupported(uint256 const&) const override
{
throw std::logic_error("TestAmendmentTable::isSupported not implemented");
}
[[nodiscard]] bool
hasUnsupportedEnabled() const override
{
throw std::logic_error("TestAmendmentTable::hasUnsupportedEnabled not implemented");
}
[[nodiscard]] std::optional<NetClock::time_point>
firstUnsupportedExpected() const override
{
throw std::logic_error("TestAmendmentTable::firstUnsupportedExpected not implemented");
}
[[nodiscard]] json::Value
getJson(bool) const override
{
throw std::logic_error("TestAmendmentTable::getJson not implemented");
}
[[nodiscard]] json::Value
getJson(uint256 const&, bool) const override
{
throw std::logic_error("TestAmendmentTable::getJson(amendment) not implemented");
}
[[nodiscard]] bool
needValidatedLedger(LedgerIndex) const override
{
throw std::logic_error("TestAmendmentTable::needValidatedLedger not implemented");
}
void
doValidatedLedger(LedgerIndex, std::set<uint256> const&, majorityAmendments_t const&) override
{
throw std::logic_error("TestAmendmentTable::doValidatedLedger not implemented");
}
void
trustChanged(hash_set<PublicKey> const&) override
{
throw std::logic_error("TestAmendmentTable::trustChanged not implemented");
}
std::map<uint256, std::uint32_t>
doVoting(
Rules const&,
NetClock::time_point,
std::set<uint256> const&,
majorityAmendments_t const&,
std::vector<std::shared_ptr<STValidation>> const&) override
{
throw std::logic_error("TestAmendmentTable::doVoting not implemented");
}
[[nodiscard]] std::vector<uint256>
doValidation(std::set<uint256> const&) const override
{
throw std::logic_error("TestAmendmentTable::doValidation not implemented");
}
[[nodiscard]] std::vector<uint256>
getDesired() const override
{
throw std::logic_error("TestAmendmentTable::getDesired not implemented");
}
};
/**
* Simple NetworkIDService implementation for tests.
*/
@@ -91,6 +198,7 @@ class TestServiceRegistry : public ServiceRegistry
logs_.journal("TaggedCache")};
PendingSaves pendingSaves_;
std::optional<uint256> trapTxID_;
TestAmendmentTable amendmentTable_;
public:
TestServiceRegistry() = default;
@@ -143,7 +251,7 @@ public:
AmendmentTable&
getAmendmentTable() override
{
throw std::logic_error("TestServiceRegistry::getAmendmentTable() not implemented");
return amendmentTable_;
}
HashRouter&

View File

@@ -0,0 +1,50 @@
#pragma once
#include <xrpl/basics/Slice.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <tx/wasm/RealHostFixture.h>
#include <cstdint>
#include <limits>
#include <string_view>
namespace xrpl::test {
struct FloatTest : RealHostFixture
{
static constexpr std::int64_t kMin64 = std::numeric_limits<std::int64_t>::min();
static constexpr std::int64_t kMax64 = std::numeric_limits<std::int64_t>::max();
static constexpr std::int32_t kNormalExp = 18;
static constexpr std::string_view const kInvalidData = "invalid_data";
// clang-format off
static inline Bytes const kIntMin = {0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00}; // -2^63 (rounds to -(2^63-1))
static inline Bytes const kIntZero = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00}; // 0
static inline Bytes const kIntMax = {0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00}; // 2^63-1
static inline Bytes const kUintMax = {0x19, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9A, 0x00, 0x00, 0x00, 0x01}; // 2^64-1
static inline Bytes const kMaxExp = {0x0D, 0xE0, 0xB6, 0xB3, 0xA7, 0x64, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00}; // 1e(kMaxExponent + kNormalExp)
static inline Bytes const kPreMaxExp = {0x0D, 0xE0, 0xB6, 0xB3, 0xA7, 0x64, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF}; // 1e(kMaxExponent + kNormalExp - 1)
static inline Bytes const kMinusMaxExp = {0xF2, 0x1F, 0x49, 0x4C, 0x58, 0x9C, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00}; // -1e(kMaxExponent + kNormalExp)
static inline Bytes const kMinExp = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00}; // 1e(kMinExponent - kNormalExp)
static inline Bytes const kMax = {0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x80, 0x00}; // kMaxRep e(kMaxExponent - kNormalExp)
static inline Bytes const kMaxIOU = {0x0D, 0xE0, 0xB6, 0xB3, 0xA7, 0x63, 0xFF, 0x9C, 0x00, 0x00, 0x00, 0x4E}; // 9999999999999999e(96)
static inline Bytes const kMinIOU = {0x0D, 0xE0, 0xB6, 0xB3, 0xA7, 0x64, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x9D}; // 1e(-81)
static inline Bytes const kOne = {0x0D, 0xE0, 0xB6, 0xB3, 0xA7, 0x64, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xEE}; // 1
static inline Bytes const kMinusOne = {0xF2, 0x1F, 0x49, 0x4C, 0x58, 0x9C, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xEE}; // -1
static inline Bytes const kOneMore = {0x0D, 0xE0, 0xB6, 0xB3, 0xA7, 0x64, 0x03, 0xE8, 0xFF, 0xFF, 0xFF, 0xEE}; // 1.000000000000001
static inline Bytes const kTwo = {0x1B, 0xC1, 0x6D, 0x67, 0x4E, 0xC8, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xEE}; // 2
static inline Bytes const kTen = {0x0D, 0xE0, 0xB6, 0xB3, 0xA7, 0x64, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xEF}; // 10
static inline Bytes const kPi = {0x2B, 0x99, 0x2D, 0xDF, 0xA2, 0x32, 0x48, 0xE8, 0xFF, 0xFF, 0xFF, 0xEE}; // 3.141592653589793
static inline Bytes const kInvalidZero = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x81, 0x00, 0x00, 0x00}; // non-canonical zero
static inline Bytes const kMinusThree = {0xD6, 0x5D, 0xDB, 0xE5, 0x09, 0xD4, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xEE}; // -3
// clang-format on
static Slice
slice(Bytes const& b)
{
return Slice{b.data(), b.size()};
}
};
} // namespace xrpl::test

View File

@@ -0,0 +1,69 @@
#include <tx/wasm/HostContextFixture.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <rust/cxx.h>
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
namespace xrpl::test {
rust::Slice<std::uint8_t const>
HostContextTest::bytesOf(Bytes const& bytes)
{
return rust::Slice<std::uint8_t const>{bytes.data(), bytes.size()};
}
Bytes
HostContextTest::bytesOfSteps(std::vector<std::int32_t> const& steps)
{
Bytes bytes;
bytes.reserve(steps.size() * sizeof(std::int32_t));
for (auto const step : steps)
{
auto const wire = bytesOfScalar(step);
bytes.insert(bytes.end(), wire.begin(), wire.end());
}
return bytes;
}
HostContextTest::OutRegion::OutRegion(std::size_t capacity) : bytes(capacity, kSentinel)
{
}
rust::Slice<std::uint8_t>
HostContextTest::OutRegion::slice()
{
return rust::Slice<std::uint8_t>{bytes.data(), bytes.size()};
}
bool
HostContextTest::OutRegion::wasWritten() const
{
return std::ranges::any_of(bytes, [](std::uint8_t b) { return b != kSentinel; });
}
bool
HostContextTest::OutRegion::holds(rust::Slice<std::uint8_t const> expected) const
{
if (expected.size() > bytes.size())
{
return false;
}
auto want = std::vector<std::uint8_t>(bytes.size(), kSentinel);
std::ranges::copy(expected, want.begin());
return bytes == want;
}
std::string
HostContextTest::logged() const
{
return sink.messages();
}
} // namespace xrpl::test

View File

@@ -0,0 +1,104 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/tx/wasm/HostContext.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <helpers/CaptureSink.h>
#include <rust/cxx.h>
#include <tx/wasm/MockHostFunctions.h>
#include <cstddef>
#include <cstdint>
#include <string>
#include <type_traits>
#include <vector>
namespace xrpl::test {
// Base for the tests that construct `HostContext` directly, rather than reaching it through
// an assembled module.
struct HostContextTest : testing::Test
{
static rust::Slice<std::uint8_t const>
bytesOf(Bytes const& bytes);
// A scalar's wire form: its bytes little-endian, the way a wasm guest lays them out in
// memory.
//
// Spelled out with shifts rather than a `memcpy` of the value, which would mirror what
// `answerScalar` does and so assert nothing about the byte order. That is the whole reason
// this exists, so keep it a shift.
template <class T>
static Bytes
bytesOfScalar(T value)
{
static_assert(std::is_integral_v<T>, "Only integral types");
auto const bits = static_cast<std::make_unsigned_t<T>>(value);
Bytes bytes(sizeof(bits));
for (std::size_t i = 0; i < sizeof(bits); ++i)
{
bytes[i] = static_cast<std::uint8_t>(bits >> (i * 8));
}
return bytes;
}
// A locator's wire form: each step as four little-endian bytes.
static Bytes
bytesOfSteps(std::vector<std::int32_t> const& steps);
// Filled with a sentinel rather than left at zero: an answer can itself be all zero, so
// only a byte no answer produces tells "wrote nothing" apart from "wrote zeros".
struct OutRegion
{
static constexpr std::uint8_t kSentinel = 0xcd;
std::vector<std::uint8_t> bytes;
explicit OutRegion(std::size_t capacity);
rust::Slice<std::uint8_t>
slice();
[[nodiscard]] bool
wasWritten() const;
// Means "this value and nothing past it".
[[nodiscard]] bool
holds(rust::Slice<std::uint8_t const> expected) const;
};
CaptureSink sink{beast::Severity::Warning};
testing::StrictMock<MockHostFunctions> host{beast::Journal{sink}};
HostContext hostContext{host};
[[nodiscard]] std::string
logged() const;
};
// `FieldLocator` has no `operator==` and is move-only, so an `EXPECT_CALL` needs a matcher
// rather than `testing::Ref`/`testing::Eq`. `invokeWithLocator` builds it as a local that is
// gone once the call returns, so the check has to happen inside the matcher.
//
// `MATCHER_P` emits a function of this name, and gmock matchers are CamelCase by convention.
// NOLINTNEXTLINE(readability-identifier-naming)
MATCHER_P(LocatorEquals, steps, "")
{
if (arg.size() != static_cast<std::uint32_t>(steps.size()))
{
return false;
}
for (std::uint32_t i = 0; i < arg.size(); ++i)
{
if (arg[i] != steps[i])
{
return false;
}
}
return true;
}
} // namespace xrpl::test

View File

@@ -1,8 +1,14 @@
#pragma once
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STNumber.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/tx/wasm/HostFunc.h>
#include <xrpl/tx/wasm/WasmCommon.h>
@@ -16,10 +22,12 @@ namespace xrpl::test {
// A mock of the host the wasm engine calls back into.
//
// Only the methods the ABI currently declares are mocked, and that is deliberate: the ~60
// others keep `HostFunctions`' own `std::unexpected(Unimplemented)`, so a contract reaching
// for something the ABI has not declared yet fails the way production would. Add a
// `MOCK_METHOD` here when the matching entry is added to `host_functions!`.
// One `MOCK_METHOD` per `HostFunctions` entry, in that header's order, each signature taken
// verbatim from it. The extra parentheses around a return type are what keeps the comma in
// `std::expected<T, HostFunctionError>` from splitting the macro's arguments.
//
// No `ON_CALL` defaults, deliberately: this is always used through `StrictMock`, which fails
// a call to a method carrying no `EXPECT_CALL`.
struct MockHostFunctions : HostFunctions
{
explicit MockHostFunctions(beast::Journal journal) : HostFunctions(journal)
@@ -34,18 +42,282 @@ struct MockHostFunctions : HostFunctions
(),
(const, override));
MOCK_METHOD(
(std::expected<std::uint32_t, HostFunctionError>),
getParentLedgerTime,
(),
(const, override));
MOCK_METHOD(
(std::expected<Hash, HostFunctionError>),
getParentLedgerHash,
(),
(const, override));
MOCK_METHOD(
(std::expected<std::uint32_t, HostFunctionError>),
getBaseFee,
(),
(const, override));
MOCK_METHOD(
(std::expected<std::int32_t, HostFunctionError>),
isAmendmentEnabled,
(uint256 const& amendmentId),
(const, override));
MOCK_METHOD(
(std::expected<std::int32_t, HostFunctionError>),
isAmendmentEnabled,
(std::string_view const& amendmentName),
(const, override));
MOCK_METHOD(
(std::expected<std::int32_t, HostFunctionError>),
cacheLedgerObj,
(uint256 const& objId, std::int32_t cacheIdx),
(override));
MOCK_METHOD(
(std::expected<Bytes, HostFunctionError>),
getTxField,
(SField const& fname),
(const, override));
MOCK_METHOD(
(std::expected<Bytes, HostFunctionError>),
getCurrentLedgerObjField,
(SField const& fname),
(const, override));
MOCK_METHOD(
(std::expected<Bytes, HostFunctionError>),
getLedgerObjField,
(std::int32_t cacheIdx, SField const& fname),
(const, override));
MOCK_METHOD(
(std::expected<Bytes, HostFunctionError>),
getTxNestedField,
(FieldLocator const& locator),
(const, override));
MOCK_METHOD(
(std::expected<Bytes, HostFunctionError>),
getCurrentLedgerObjNestedField,
(FieldLocator const& locator),
(const, override));
MOCK_METHOD(
(std::expected<Bytes, HostFunctionError>),
getLedgerObjNestedField,
(std::int32_t cacheIdx, FieldLocator const& locator),
(const, override));
MOCK_METHOD(
(std::expected<std::int32_t, HostFunctionError>),
getTxArrayLen,
(SField const& fname),
(const, override));
MOCK_METHOD(
(std::expected<std::int32_t, HostFunctionError>),
getCurrentLedgerObjArrayLen,
(SField const& fname),
(const, override));
MOCK_METHOD(
(std::expected<std::int32_t, HostFunctionError>),
getLedgerObjArrayLen,
(std::int32_t cacheIdx, SField const& fname),
(const, override));
MOCK_METHOD(
(std::expected<std::int32_t, HostFunctionError>),
getTxNestedArrayLen,
(FieldLocator const& locator),
(const, override));
MOCK_METHOD(
(std::expected<std::int32_t, HostFunctionError>),
getCurrentLedgerObjNestedArrayLen,
(FieldLocator const& locator),
(const, override));
MOCK_METHOD(
(std::expected<std::int32_t, HostFunctionError>),
getLedgerObjNestedArrayLen,
(std::int32_t cacheIdx, FieldLocator const& locator),
(const, override));
MOCK_METHOD(
(std::expected<std::int32_t, HostFunctionError>),
updateData,
(Slice const& data),
(override));
MOCK_METHOD(
(std::expected<std::int32_t, HostFunctionError>),
checkSignature,
(Slice const& message, Slice const& signature, Slice const& pubkey),
(const, override));
MOCK_METHOD(
(std::expected<Hash, HostFunctionError>),
computeSha512HalfHash,
(Slice const& data),
(const, override));
MOCK_METHOD(
(std::expected<Bytes, HostFunctionError>),
accountKeylet,
(AccountID const& account),
(const, override));
MOCK_METHOD(
(std::expected<Bytes, HostFunctionError>),
ammKeylet,
(Asset const& issue1, Asset const& issue2),
(const, override));
MOCK_METHOD(
(std::expected<Bytes, HostFunctionError>),
checkKeylet,
(AccountID const& account, std::uint32_t seq),
(const, override));
MOCK_METHOD(
(std::expected<Bytes, HostFunctionError>),
credentialKeylet,
(AccountID const& subject, AccountID const& issuer, Slice const& credentialType),
(const, override));
MOCK_METHOD(
(std::expected<Bytes, HostFunctionError>),
didKeylet,
(AccountID const& account),
(const, override));
MOCK_METHOD(
(std::expected<Bytes, HostFunctionError>),
delegateKeylet,
(AccountID const& account, AccountID const& authorize),
(const, override));
MOCK_METHOD(
(std::expected<Bytes, HostFunctionError>),
depositPreauthKeylet,
(AccountID const& account, AccountID const& authorize),
(const, override));
MOCK_METHOD(
(std::expected<Bytes, HostFunctionError>),
escrowKeylet,
(AccountID const& account, std::uint32_t seq),
(const, override));
MOCK_METHOD(
(std::expected<Bytes, HostFunctionError>),
trustLineKeylet,
(AccountID const& account1, AccountID const& account2, Currency const& currency),
(const, override));
MOCK_METHOD(
(std::expected<Bytes, HostFunctionError>),
mptokenIssuanceKeylet,
(AccountID const& issuer, std::uint32_t seq),
(const, override));
MOCK_METHOD(
(std::expected<Bytes, HostFunctionError>),
mptokenKeylet,
(MPTID const& mptid, AccountID const& holder),
(const, override));
MOCK_METHOD(
(std::expected<Bytes, HostFunctionError>),
nftokenOfferKeylet,
(AccountID const& account, std::uint32_t seq),
(const, override));
MOCK_METHOD(
(std::expected<Bytes, HostFunctionError>),
offerKeylet,
(AccountID const& account, std::uint32_t seq),
(const, override));
MOCK_METHOD(
(std::expected<Bytes, HostFunctionError>),
oracleKeylet,
(AccountID const& account, std::uint32_t docId),
(const, override));
MOCK_METHOD(
(std::expected<Bytes, HostFunctionError>),
paychannelKeylet,
(AccountID const& account, AccountID const& destination, std::uint32_t seq),
(const, override));
MOCK_METHOD(
(std::expected<Bytes, HostFunctionError>),
permissionedDomainKeylet,
(AccountID const& account, std::uint32_t seq),
(const, override));
MOCK_METHOD(
(std::expected<Bytes, HostFunctionError>),
signerListKeylet,
(AccountID const& account),
(const, override));
MOCK_METHOD(
(std::expected<Bytes, HostFunctionError>),
ticketKeylet,
(AccountID const& account, std::uint32_t seq),
(const, override));
MOCK_METHOD(
(std::expected<Bytes, HostFunctionError>),
vaultKeylet,
(AccountID const& account, std::uint32_t seq),
(const, override));
MOCK_METHOD(
(std::expected<Bytes, HostFunctionError>),
getNFT,
(AccountID const& account, uint256 const& nftId),
(const, override));
MOCK_METHOD(
(std::expected<Bytes, HostFunctionError>),
getNFTIssuer,
(uint256 const& nftId),
(const, override));
MOCK_METHOD(
(std::expected<std::uint32_t, HostFunctionError>),
getNFTTaxon,
(uint256 const& nftId),
(const, override));
MOCK_METHOD(
(std::expected<std::int32_t, HostFunctionError>),
getNFTFlags,
(uint256 const& nftId),
(const, override));
MOCK_METHOD(
(std::expected<std::int32_t, HostFunctionError>),
getNFTTransferFee,
(uint256 const& nftId),
(const, override));
MOCK_METHOD(
(std::expected<std::uint32_t, HostFunctionError>),
getNFTSequence,
(uint256 const& nftId),
(const, override));
// Takes the rendered text, not the guest's buffer: rendering is `HostContext`'s, so what
// a test asserts here is the log line a node would write.
MOCK_METHOD(
@@ -53,10 +325,97 @@ struct MockHostFunctions : HostFunctions
trace,
(std::string_view const& msg, std::string_view const& data),
(const, override));
MOCK_METHOD(
(std::expected<Bytes, HostFunctionError>),
floatFromInt,
(std::int64_t x, std::int32_t mode),
(const, override));
MOCK_METHOD(
(std::expected<Bytes, HostFunctionError>),
floatFromUint,
(std::uint64_t x, std::int32_t mode),
(const, override));
MOCK_METHOD(
(std::expected<Bytes, HostFunctionError>),
floatFromSTAmount,
(STAmount const& x, std::int32_t mode),
(const, override));
MOCK_METHOD(
(std::expected<Bytes, HostFunctionError>),
floatFromSTNumber,
(STNumber const& x, std::int32_t mode),
(const, override));
MOCK_METHOD(
(std::expected<std::int64_t, HostFunctionError>),
floatToInt,
(Slice const& x, std::int32_t mode),
(const, override));
MOCK_METHOD(
(std::expected<FloatPair, HostFunctionError>),
floatToMantExp,
(Slice const& x),
(const, override));
MOCK_METHOD(
(std::expected<Bytes, HostFunctionError>),
floatFromMantExp,
(std::int64_t mantissa, std::int32_t exponent, std::int32_t mode),
(const, override));
MOCK_METHOD(
(std::expected<std::int32_t, HostFunctionError>),
floatCompare,
(Slice const& x, Slice const& y),
(const, override));
MOCK_METHOD(
(std::expected<Bytes, HostFunctionError>),
floatAdd,
(Slice const& x, Slice const& y, std::int32_t mode),
(const, override));
MOCK_METHOD(
(std::expected<Bytes, HostFunctionError>),
floatSubtract,
(Slice const& x, Slice const& y, std::int32_t mode),
(const, override));
MOCK_METHOD(
(std::expected<Bytes, HostFunctionError>),
floatMultiply,
(Slice const& x, Slice const& y, std::int32_t mode),
(const, override));
MOCK_METHOD(
(std::expected<Bytes, HostFunctionError>),
floatDivide,
(Slice const& x, Slice const& y, std::int32_t mode),
(const, override));
MOCK_METHOD(
(std::expected<Bytes, HostFunctionError>),
floatRoot,
(Slice const& x, std::int32_t n, std::int32_t mode),
(const, override));
MOCK_METHOD(
(std::expected<Bytes, HostFunctionError>),
floatPower,
(Slice const& x, std::int32_t n, std::int32_t mode),
(const, override));
};
// Matches a `Slice` (or anything with `data()`/`size()`) against the bytes of a string, so
// an expectation can say *what* the guest asked the host to work on.
//
// `MATCHER_P` emits a function of this name, and gmock matchers are CamelCase by convention.
// NOLINTNEXTLINE(readability-identifier-naming)
MATCHER_P(BytesAre, expected, "")
{
return std::string_view{reinterpret_cast<char const*>(arg.data()), arg.size()} ==

View File

@@ -0,0 +1,52 @@
#include <tx/wasm/NFTFixture.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/nft.h>
#include <xrpl/protocol_autogen/transactions/NFTokenMint.h> // IWYU pragma: keep
#include <xrpl/tx/transactors/nft/NFTokenMint.h>
#include <gtest/gtest.h>
#include <helpers/Account.h>
#include <optional>
#include <string_view>
namespace xrpl::test {
uint256
NFTTest::makeNftId(AccountID const& issuer)
{
return NFTokenMint::createNFTokenID(kFlags, kFee, issuer, nft::toTaxon(kTaxon), kSequence);
}
uint256
NFTTest::mintNFT(Account const& issuer, std::optional<std::string_view> uri)
{
auto builder = transactions::NFTokenMintBuilder{issuer.id(), 0u};
if (uri)
builder.setURI(Slice{uri->data(), uri->size()});
auto const r = ledger.submit(builder, issuer);
EXPECT_EQ(r.ter, tesSUCCESS) << transToken(r.ter);
ledger.close();
// The single minted token lives in the owner's first NFTokenPage.
auto const& view = ledger.getOpenLedger();
auto const first = keylet::nftokenPageMin(issuer.id()).key;
auto const last = keylet::nftokenPageMax(issuer.id()).key;
auto const pageKey = view.succ(first, last.next());
EXPECT_TRUE(pageKey.has_value());
auto const page = pageKey ? view.read(Keylet{ltNFTOKEN_PAGE, *pageKey}) : nullptr;
EXPECT_NE(page, nullptr);
if (!page)
return uint256{};
auto const& tokens = page->getFieldArray(sfNFTokens);
EXPECT_FALSE(tokens.empty());
return tokens.empty() ? uint256{} : tokens[0].getFieldH256(sfNFTokenID);
}
} // namespace xrpl::test

View File

@@ -0,0 +1,33 @@
#pragma once
#include <xrpl/basics/base_uint.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/nft.h>
#include <helpers/Account.h>
#include <tx/wasm/RealHostFixture.h>
#include <cstdint>
#include <optional>
#include <string_view>
namespace xrpl::test {
struct NFTTest : RealHostFixture
{
static constexpr std::uint16_t kFlags = nft::kFlagTransferable | nft::kFlagBurnable;
static constexpr std::uint16_t kFee = 314;
static constexpr std::uint32_t kTaxon = 12345;
static constexpr std::uint32_t kSequence = 7;
static uint256
makeNftId(AccountID const& issuer);
// Mint a real NFToken owned by `issuer` (taxon 0) and return its id, read back from the
// owner's NFTokenPage. TxTest applies to the open ledger, which produces no metadata, so
// the id is recovered from ledger state rather than from the mint's metadata.
uint256
mintNFT(Account const& issuer, std::optional<std::string_view> uri = std::nullopt);
};
} // namespace xrpl::test

View File

@@ -0,0 +1,297 @@
#include <tx/wasm/RealHostFixture.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/KeyType.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STNumber.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/protocol_autogen/transactions/SignerListSet.h> // IWYU pragma: keep
#include <xrpl/tx/ApplyContext.h>
#include <xrpl/tx/wasm/HostFuncImpl.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gtest/gtest.h>
#include <helpers/Account.h>
#include <helpers/TxTest.h>
#include <cstdint>
#include <expected>
#include <functional>
#include <iterator>
#include <memory>
#include <span>
#include <string_view>
#include <utility>
#include <vector>
namespace xrpl::test {
Bytes
toBytes(std::uint8_t value)
{
return {value};
}
Bytes
toBytes(std::uint16_t value)
{
return {static_cast<std::uint8_t>(value), static_cast<std::uint8_t>(value >> 8)};
}
Bytes
toBytes(std::uint32_t value)
{
return {
static_cast<std::uint8_t>(value),
static_cast<std::uint8_t>(value >> 8),
static_cast<std::uint8_t>(value >> 16),
static_cast<std::uint8_t>(value >> 24)};
}
Bytes
toBytes(uint256 const& value)
{
return Bytes{std::begin(value), std::end(value)};
}
Bytes
toBytes(std::string_view value)
{
return Bytes{std::begin(value), std::end(value)};
}
Bytes
toBytes(std::span<std::uint8_t const> value)
{
return Bytes{std::begin(value), std::end(value)};
}
Bytes
toBytes(AccountID const& account)
{
return Bytes{std::begin(account), std::end(account)};
}
Bytes
toBytes(Issue const& issue)
{
auto s = Serializer{};
s.addBitString(issue.currency);
if (!isXRP(issue.currency))
s.addBitString(issue.account);
return s.getData();
}
Bytes
toBytes(Asset const& asset)
{
if (asset.holds<Issue>())
return toBytes(asset.get<Issue>());
auto const& mptIssue = asset.get<MPTIssue>();
auto const& mptID = mptIssue.getMptID();
return Bytes{mptID.cbegin(), mptID.cend()};
}
Bytes
toBytes(STAmount const& amount)
{
auto msg = Serializer{};
amount.add(msg);
return msg.getData();
}
Bytes
toBytes(STNumber const& number)
{
auto msg = Serializer{};
number.add(msg);
return msg.getData();
}
void
expectKeyletMatches(std::expected<Bytes, HostFunctionError> const& result, Keylet const& expected)
{
expectValue(result, toBytes(expected.key));
}
SignedMessage
signMessage(std::string_view message, KeyType keyType)
{
auto const [pk, sk] = randomKeyPair(keyType);
auto const msg = Bytes{std::begin(message), std::end(message)};
auto const sig = sign(pk, sk, Slice{msg.data(), msg.size()});
return {
.message = msg,
.signature = Bytes{sig.data(), sig.data() + sig.size()},
.publicKey = Bytes{pk.data(), pk.data() + pk.size()}};
}
uint256
credentialId(std::string_view hex)
{
auto id = uint256{};
EXPECT_TRUE(id.parseHex(std::string{hex}));
return id;
}
STObject
makeMemo(Bytes const& data)
{
auto memo = STObject::makeInnerObject(sfMemo);
memo.setFieldVL(sfMemoData, data);
return memo;
}
TxAssembler
bareTx(TxType type)
{
return {.type = type, .build = [](STObject&) {}};
}
TxAssembler
escrowFinishTx(TxTest& ledger, Account const& acct)
{
return {.type = ttESCROW_FINISH, .build = [&ledger, acct](STObject& obj) {
auto credId = uint256{};
EXPECT_TRUE(credId.parseHex(
"0011223344556677889900112233445566778899001122334455667788990011"));
obj.setAccountID(sfAccount, acct.id());
obj.setAccountID(sfOwner, acct.id());
obj.setFieldU32(sfOfferSequence, ledger.getAccountRoot(acct.id()).getSequence());
obj.setFieldArray(sfMemos, STArray{});
auto credIds = STVector256{};
credIds.pushBack(credId);
obj.setFieldV256(sfCredentialIDs, credIds);
}};
}
TxAssembler
ammDepositTx(Account const& acct, Asset const& asset1, Asset const& asset2)
{
return {.type = ttAMM_DEPOSIT, .build = [acct, asset1, asset2](STObject& obj) {
obj.setAccountID(sfAccount, acct.id());
obj.setFieldIssue(sfAsset, STIssue{sfAsset, asset1});
obj.setFieldIssue(sfAsset2, STIssue{sfAsset2, asset2});
}};
}
TxAssembler
mptIssuanceCreateTx(Account const& acct, std::uint8_t scale)
{
return {.type = ttMPTOKEN_ISSUANCE_CREATE, .build = [acct, scale](STObject& obj) {
obj.setAccountID(sfAccount, acct.id());
obj.setFieldU8(sfAssetScale, scale);
}};
}
WasmHost::WasmHost(
std::shared_ptr<STTx const> tx,
std::unique_ptr<ApplyContext> context,
std::unique_ptr<WasmHostFunctionsImpl> host)
: tx_{std::move(tx)}, context_{std::move(context)}, host_{std::move(host)}
{
}
WasmHostFunctionsImpl*
WasmHost::operator->() const
{
return host_.get();
}
WasmHostFunctionsImpl&
WasmHost::operator*() const
{
return *host_;
}
Account
RealHostFixture::fund(char const* name, XRPAmount amount)
{
auto const account = Account{name};
ledger.createAccount(account, amount);
return account;
}
WasmHost
RealHostFixture::makeHost(
beast::Journal journal,
Keylet const& leKey,
TxType txType,
std::function<void(STObject&)> assembler)
{
auto tx = std::make_shared<STTx>(
txType, [assembler = std::move(assembler)](STObject& obj) { assembler(obj); });
auto context = std::make_unique<ApplyContext>(
ledger.getServiceRegistry(),
ledger.getOpenLedger(),
*tx,
tesSUCCESS,
ledger.getOpenLedger().fees().base,
TapNone,
journal);
auto host = std::make_unique<WasmHostFunctionsImpl>(*context, leKey);
return WasmHost{std::move(tx), std::move(context), std::move(host)};
}
WasmHost
RealHostFixture::makeHost(
Keylet const& leKey,
TxType txType,
std::function<void(STObject&)> assembler)
{
return makeHost(
beast::Journal{beast::Journal::getNullSink()}, leKey, txType, std::move(assembler));
}
WasmHost
RealHostFixture::makeTracingHost(
Keylet const& leKey,
TxType txType,
std::function<void(STObject&)> assembler)
{
return makeHost(beast::Journal{traceSink_}, leKey, txType, std::move(assembler));
}
std::string
RealHostFixture::logged() const
{
return traceSink_.messages();
}
void
RealHostFixture::makeSignerList(
Account const& owner,
std::uint32_t quorum,
std::vector<std::pair<Account, std::uint16_t>> const& signers)
{
auto entries = STArray{};
for (auto const& [signer, weight] : signers)
{
auto entry = STObject::makeInnerObject(sfSignerEntry);
entry.setAccountID(sfAccount, signer.id());
entry.setFieldU16(sfSignerWeight, weight);
entries.push_back(std::move(entry));
}
auto const r = ledger.submit(
transactions::SignerListSetBuilder{owner.id(), quorum}.setSignerEntries(entries), owner);
EXPECT_EQ(r.ter, tesSUCCESS) << transToken(r.ter);
ledger.close();
}
} // namespace xrpl::test

View File

@@ -0,0 +1,189 @@
#pragma once
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Indexes.h> // keylet::account
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/KeyType.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STNumber.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/ApplyContext.h>
#include <xrpl/tx/wasm/HostFuncImpl.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gtest/gtest.h>
#include <helpers/Account.h>
#include <helpers/CaptureSink.h>
#include <helpers/TxTest.h>
#include <cstdint>
#include <expected>
#include <functional>
#include <memory>
#include <source_location>
#include <span>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
namespace xrpl::test {
Bytes
toBytes(std::uint8_t value);
Bytes
toBytes(std::uint16_t value);
Bytes
toBytes(std::uint32_t value);
Bytes
toBytes(uint256 const& value);
Bytes
toBytes(std::string_view value);
Bytes
toBytes(std::span<std::uint8_t const> value);
Bytes
toBytes(AccountID const& account);
Bytes
toBytes(Issue const& issue);
Bytes
toBytes(Asset const& asset);
Bytes
toBytes(STAmount const& amount);
Bytes
toBytes(STNumber const& number);
template <typename T, typename U>
void
expectValue(
std::expected<T, HostFunctionError> const& result,
U const& expected,
std::source_location loc = std::source_location::current())
{
auto trace = testing::ScopedTrace{loc.file_name(), static_cast<int>(loc.line()), ""};
ASSERT_TRUE(result.has_value())
<< "expected a value, got error " << static_cast<int>(result.error());
EXPECT_EQ(*result, expected);
}
template <typename T>
void
expectError(
std::expected<T, HostFunctionError> const& result,
HostFunctionError expected,
std::source_location loc = std::source_location::current())
{
auto trace = testing::ScopedTrace{loc.file_name(), static_cast<int>(loc.line()), ""};
ASSERT_FALSE(result.has_value()) << "expected error, got a value";
EXPECT_EQ(result.error(), expected);
}
void
expectKeyletMatches(std::expected<Bytes, HostFunctionError> const& result, Keylet const& expected);
struct SignedMessage
{
Bytes message;
Bytes signature;
Bytes publicKey;
};
SignedMessage
signMessage(std::string_view message, KeyType keyType = KeyType::Secp256k1);
uint256
credentialId(
std::string_view hex = "0011223344556677889900112233445566778899001122334455667788990011");
STObject
makeMemo(Bytes const& data);
struct TxAssembler
{
TxType type;
std::function<void(STObject&)> build;
};
TxAssembler
bareTx(TxType type = ttESCROW_FINISH);
TxAssembler
escrowFinishTx(TxTest& ledger, Account const& acct);
TxAssembler
ammDepositTx(Account const& acct, Asset const& asset1, Asset const& asset2);
TxAssembler
mptIssuanceCreateTx(Account const& acct, std::uint8_t scale);
class WasmHost
{
public:
WasmHost(
std::shared_ptr<STTx const> tx,
std::unique_ptr<ApplyContext> context,
std::unique_ptr<WasmHostFunctionsImpl> host);
WasmHostFunctionsImpl*
operator->() const;
WasmHostFunctionsImpl&
operator*() const;
private:
std::shared_ptr<STTx const> tx_;
std::unique_ptr<ApplyContext> context_;
std::unique_ptr<WasmHostFunctionsImpl> host_;
};
class RealHostFixture : public testing::Test
{
public:
TxTest ledger;
Account
fund(char const* name, XRPAmount amount = XRP(1000));
WasmHost
makeHost(
beast::Journal journal,
Keylet const& leKey = keylet::account(AccountID{}),
TxType txType = ttESCROW_FINISH,
std::function<void(STObject&)> assembler = [](STObject&) {});
// The common case: a host that discards its log output.
WasmHost
makeHost(
Keylet const& leKey = keylet::account(AccountID{}),
TxType txType = ttESCROW_FINISH,
std::function<void(STObject&)> assembler = [](STObject&) {});
// A host whose `trace` output is captured, so a test can read it back with `logged()`.
// The sink is a fixture member, so it outlives the host and accumulates across a test.
WasmHost
makeTracingHost(
Keylet const& leKey = keylet::account(AccountID{}),
TxType txType = ttESCROW_FINISH,
std::function<void(STObject&)> assembler = [](STObject&) {});
// Everything `trace` has written to the tracing host so far.
[[nodiscard]] std::string
logged() const;
// Submit a real SignerListSet so `keylet::signerList(owner)` exists — the object the
// signer-list nested-field / array-length getters read. `signers` pairs each signer
// account with its weight.
void
makeSignerList(
Account const& owner,
std::uint32_t quorum,
std::vector<std::pair<Account, std::uint16_t>> const& signers);
private:
CaptureSink traceSink_{beast::Severity::Trace};
};
} // namespace xrpl::test

View File

@@ -0,0 +1,126 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
struct AccountKeyletCall : HostContextTest
{
Bytes const accountBytes{0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a,
0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34};
AccountID const account = AccountID::fromVoid(accountBytes.data());
};
TEST_F(AccountKeyletCall, AccountIsForwardedKeyletIsWritten)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, accountKeylet(account)).WillOnce(testing::Return(keylet));
OutRegion out{32};
EXPECT_EQ(
hostContext.accountKeylet(bytesOf(accountBytes), out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_TRUE(out.holds(bytesOf(keylet)));
}
TEST_F(AccountKeyletCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, accountKeylet(account))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
OutRegion out{32};
EXPECT_EQ(
hostContext.accountKeylet(bytesOf(accountBytes), out.slice()),
hfErrorToInt(HostFunctionError::LedgerObjNotFound));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(AccountKeyletCall, ShortAccountIsRefusedWithoutAskingHost)
{
Bytes const shortAccount(AccountID::size() - 1, 0x01);
EXPECT_CALL(host, accountKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.accountKeylet(bytesOf(shortAccount), out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(AccountKeyletCall, LongAccountIsRefusedWithoutAskingHost)
{
Bytes const longAccount(AccountID::size() + 1, 0x01);
EXPECT_CALL(host, accountKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.accountKeylet(bytesOf(longAccount), out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(AccountKeyletCall, EmptyAccountIsRefusedWithoutAskingHost)
{
EXPECT_CALL(host, accountKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.accountKeylet(bytesOf(Bytes{}), out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(AccountKeyletCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, accountKeylet(account))
.WillOnce(testing::Throw(std::runtime_error{"account keylet came apart"}));
OutRegion out{32};
EXPECT_EQ(
hostContext.accountKeylet(bytesOf(accountBytes), out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("account keylet came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("accountKeylet"));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(AccountKeyletCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, accountKeylet(account)).WillOnce(testing::Return(keylet));
OutRegion out{keylet.size() - 1};
EXPECT_EQ(
hostContext.accountKeylet(bytesOf(accountBytes), out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(AccountKeyletCall, OutRegionOfExactSizeIsWritten)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, accountKeylet(account)).WillOnce(testing::Return(keylet));
OutRegion out{keylet.size()};
EXPECT_EQ(
hostContext.accountKeylet(bytesOf(accountBytes), out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_TRUE(out.holds(bytesOf(keylet)));
}
TEST_F(AccountKeyletCall, EmptyResultAnswersZeroAndWritesNothing)
{
EXPECT_CALL(host, accountKeylet(account)).WillOnce(testing::Return(Bytes{}));
OutRegion out{32};
EXPECT_EQ(hostContext.accountKeylet(bytesOf(accountBytes), out.slice()), 0);
EXPECT_FALSE(out.wasWritten());
}
} // namespace xrpl::test

View File

@@ -0,0 +1,156 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
namespace {
Bytes
concatBytes(Bytes const& first, Bytes const& second)
{
Bytes bytes = first;
bytes.insert(bytes.end(), second.begin(), second.end());
return bytes;
}
} // namespace
// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not
// here. This is `parseAsset`'s only coverage, so every branch of its length-based dispatch
// is pinned below.
struct AmmKeyletCall : HostContextTest
{
Bytes const mptWire = Bytes(24, 0x7a);
Bytes const xrpWire = Bytes(20, 0x00);
Bytes const currencyWire = Bytes(20, 0x42);
Bytes const accountWire = Bytes(20, 0x99);
Bytes const issueWire = concatBytes(currencyWire, accountWire);
Asset const mptAsset{MPTID::fromVoid(mptWire.data())};
Asset const xrpAsset{xrpIssue()};
Asset const issueAsset{
Issue{Currency::fromVoid(currencyWire.data()), AccountID::fromVoid(accountWire.data())}};
Bytes const keylet = Bytes(32, 0xab);
};
TEST_F(AmmKeyletCall, MptAndIssueAssetsForwardedAndKeyletWritten)
{
EXPECT_CALL(host, ammKeylet(testing::Eq(mptAsset), testing::Eq(issueAsset)))
.WillOnce(testing::Return(keylet));
OutRegion out{32};
EXPECT_EQ(
hostContext.ammKeylet(bytesOf(mptWire), bytesOf(issueWire), out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_TRUE(out.holds(bytesOf(keylet)));
}
TEST_F(AmmKeyletCall, BareXrpCurrencyBytesBecomeNativeAssetHostIsAskedFor)
{
EXPECT_CALL(host, ammKeylet(testing::Eq(xrpAsset), testing::Eq(mptAsset)))
.WillOnce(testing::Return(keylet));
OutRegion out{32};
EXPECT_EQ(
hostContext.ammKeylet(bytesOf(xrpWire), bytesOf(mptWire), out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_TRUE(out.holds(bytesOf(keylet)));
}
TEST_F(AmmKeyletCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, ammKeylet(testing::Eq(mptAsset), testing::Eq(issueAsset)))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
OutRegion out{32};
EXPECT_EQ(
hostContext.ammKeylet(bytesOf(mptWire), bytesOf(issueWire), out.slice()),
hfErrorToInt(HostFunctionError::LedgerObjNotFound));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(AmmKeyletCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, ammKeylet(testing::Eq(mptAsset), testing::Eq(issueAsset)))
.WillOnce(testing::Throw(std::runtime_error{"amm keylet came apart"}));
OutRegion out{32};
EXPECT_EQ(
hostContext.ammKeylet(bytesOf(mptWire), bytesOf(issueWire), out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("amm keylet came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("ammKeylet"));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(AmmKeyletCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
EXPECT_CALL(host, ammKeylet(testing::Eq(mptAsset), testing::Eq(issueAsset)))
.WillOnce(testing::Return(keylet));
OutRegion out{keylet.size() - 1};
EXPECT_EQ(
hostContext.ammKeylet(bytesOf(mptWire), bytesOf(issueWire), out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(AmmKeyletCall, BareNonXrpCurrencyIsRefusedWithoutAskingHost)
{
EXPECT_CALL(host, ammKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.ammKeylet(bytesOf(currencyWire), bytesOf(mptWire), out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(AmmKeyletCall, IssueWithNativeCurrencyIsRefusedWithoutAskingHost)
{
Bytes const nativeIssueWire = concatBytes(xrpWire, accountWire);
EXPECT_CALL(host, ammKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.ammKeylet(bytesOf(nativeIssueWire), bytesOf(mptWire), out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(AmmKeyletCall, EmptyAssetIsRefusedWithoutAskingHost)
{
EXPECT_CALL(host, ammKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.ammKeylet(bytesOf(Bytes{}), bytesOf(mptWire), out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
// asset1 is parsed before asset2, but `parseAsset` answers the same `InvalidParams` for every
// malformed shape, so which one was rejected is not observable here. The two are malformed for
// different reasons so the case is at least not a duplicate of the single-asset ones above.
TEST_F(AmmKeyletCall, BothAssetsMalformedIsRefusedWithoutAskingHost)
{
Bytes const wrongLength{1, 2, 3, 4, 5};
EXPECT_CALL(host, ammKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.ammKeylet(bytesOf(wrongLength), bytesOf(currencyWire), out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
} // namespace xrpl::test

View File

@@ -0,0 +1,109 @@
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// No D or F axis: `getBaseFee` takes no argument, so there is nothing to decode wrong and
// nothing whose forwarded identity to check.
struct BaseFeeCall : HostContextTest
{
static constexpr std::uint32_t kBaseFee = 0x12345678;
Bytes const expectedBytes = bytesOfScalar(kBaseFee);
};
TEST_F(BaseFeeCall, HostValueIsWrittenAsLittleEndianBytes)
{
EXPECT_CALL(host, getBaseFee()).WillOnce(testing::Return(kBaseFee));
OutRegion out{32};
EXPECT_EQ(hostContext.getBaseFee(out.slice()), 4);
EXPECT_TRUE(out.holds(bytesOf(expectedBytes)));
}
TEST_F(BaseFeeCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, getBaseFee())
.WillOnce(testing::Return(std::unexpected(HostFunctionError::Unimplemented)));
OutRegion out{4};
EXPECT_EQ(hostContext.getBaseFee(out.slice()), hfErrorToInt(HostFunctionError::Unimplemented));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(BaseFeeCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, getBaseFee())
.WillOnce(testing::Throw(std::runtime_error{"base fee came apart"}));
OutRegion out{4};
EXPECT_EQ(hostContext.getBaseFee(out.slice()), hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("base fee came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("getBaseFee"));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(BaseFeeCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
EXPECT_CALL(host, getBaseFee()).WillOnce(testing::Return(kBaseFee));
OutRegion out{3};
EXPECT_EQ(hostContext.getBaseFee(out.slice()), 4);
EXPECT_FALSE(out.wasWritten());
}
TEST_F(BaseFeeCall, OutRegionOfExactSizeIsWritten)
{
EXPECT_CALL(host, getBaseFee()).WillOnce(testing::Return(kBaseFee));
OutRegion out{4};
EXPECT_EQ(hostContext.getBaseFee(out.slice()), 4);
EXPECT_TRUE(out.holds(bytesOf(expectedBytes)));
}
// Cross-cutting: every `HostFunctionError` code crosses `hfErrorToInt` unchanged at this layer.
// Unlike the engine-side `WasmVMTest.SoftHostErrorCodesCrossUnchanged`, nothing is excluded
// here - `HostContext` does not distinguish a soft code from a fatal one, so `Unimplemented`
// and `NoMemExported` cross the same as any other. `InternalFatal` sits outside the -1..-20
// run other codes occupy (it is `INT32_MIN`), and crosses the same whether the host returns it
// directly or `guarded` supplies it for a throw.
TEST_F(BaseFeeCall, EveryHostFunctionErrorCodeCrossesHfErrorToIntUnchanged)
{
static constexpr HostFunctionError kAllErrors[] = {
HostFunctionError::Unimplemented, HostFunctionError::FieldNotFound,
HostFunctionError::BufferTooSmall, HostFunctionError::NoArray,
HostFunctionError::NotLeafField, HostFunctionError::LocatorMalformed,
HostFunctionError::SlotOutRange, HostFunctionError::SlotsFull,
HostFunctionError::EmptySlot, HostFunctionError::LedgerObjNotFound,
HostFunctionError::OutOfTransferLimit, HostFunctionError::DataFieldTooLarge,
HostFunctionError::PointerOutOfBounds, HostFunctionError::NoMemExported,
HostFunctionError::InvalidParams, HostFunctionError::InvalidAccount,
HostFunctionError::InvalidField, HostFunctionError::IndexOutOfBounds,
HostFunctionError::FloatInputMalformed, HostFunctionError::FloatComputationError,
HostFunctionError::InternalFatal,
};
auto refused = HostFunctionError::Unimplemented;
EXPECT_CALL(host, getBaseFee())
.WillRepeatedly([&refused]() -> std::expected<std::uint32_t, HostFunctionError> {
return std::unexpected(refused);
});
for (auto const error : kAllErrors)
{
refused = error;
OutRegion out{4};
EXPECT_EQ(hostContext.getBaseFee(out.slice()), hfErrorToInt(error));
EXPECT_FALSE(out.wasWritten());
}
}
} // namespace xrpl::test

View File

@@ -0,0 +1,81 @@
#include <xrpl/basics/base_uint.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// `cacheLedgerObj` mutates the host's slot table, so it is non-`const`; it answers the slot
// used directly, with no out region.
struct CacheLedgerObjCall : HostContextTest
{
Bytes const objIdBytes = Bytes(uint256::size(), 0x33);
uint256 const objId = uint256::fromVoid(objIdBytes.data());
};
TEST_F(CacheLedgerObjCall, ObjIdAndCacheIdxForwardedSlotIsReturned)
{
EXPECT_CALL(host, cacheLedgerObj(testing::Eq(objId), 5)).WillOnce(testing::Return(7));
EXPECT_EQ(hostContext.cacheLedgerObj(bytesOf(objIdBytes), 5), 7);
}
TEST_F(CacheLedgerObjCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, cacheLedgerObj(testing::Eq(objId), 5))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::SlotsFull)));
EXPECT_EQ(
hostContext.cacheLedgerObj(bytesOf(objIdBytes), 5),
hfErrorToInt(HostFunctionError::SlotsFull));
}
TEST_F(CacheLedgerObjCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, cacheLedgerObj(testing::Eq(objId), 5))
.WillOnce(testing::Throw(std::runtime_error{"cache slot came apart"}));
EXPECT_EQ(
hostContext.cacheLedgerObj(bytesOf(objIdBytes), 5),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("cache slot came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("cacheLedgerObj"));
}
TEST_F(CacheLedgerObjCall, MalformedObjIdIsRefusedWithoutAskingHost)
{
Bytes const malformed(uint256::size() - 1, 0x33);
EXPECT_CALL(host, cacheLedgerObj).Times(0);
EXPECT_EQ(
hostContext.cacheLedgerObj(bytesOf(malformed), 5),
hfErrorToInt(HostFunctionError::InvalidParams));
}
// 0 selects a free slot at the host - a meaningful argument here, not an absent one - and must
// still cross unchanged.
TEST_F(CacheLedgerObjCall, ZeroCacheIdxIsForwardedVerbatim)
{
EXPECT_CALL(host, cacheLedgerObj(testing::Eq(objId), 0)).WillOnce(testing::Return(0));
EXPECT_EQ(hostContext.cacheLedgerObj(bytesOf(objIdBytes), 0), 0);
}
// Unlike `seq` elsewhere in this file's shape family, `cacheIdx` is not reinterpreted as
// unsigned: a negative value reaches the host as itself.
TEST_F(CacheLedgerObjCall, NegativeCacheIdxIsForwardedVerbatim)
{
EXPECT_CALL(host, cacheLedgerObj(testing::Eq(objId), -1))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::SlotOutRange)));
EXPECT_EQ(
hostContext.cacheLedgerObj(bytesOf(objIdBytes), -1),
hfErrorToInt(HostFunctionError::SlotOutRange));
}
} // namespace xrpl::test

View File

@@ -0,0 +1,131 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
struct CheckKeyletCall : HostContextTest
{
Bytes const accountBytes{0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a,
0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54};
AccountID const account = AccountID::fromVoid(accountBytes.data());
std::int32_t const seq = 54321;
};
TEST_F(CheckKeyletCall, AccountAndSeqAreForwardedKeyletIsWritten)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, checkKeylet(account, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(keylet));
OutRegion out{32};
EXPECT_EQ(
hostContext.checkKeylet(bytesOf(accountBytes), seq, out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_TRUE(out.holds(bytesOf(keylet)));
}
TEST_F(CheckKeyletCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, checkKeylet(account, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
OutRegion out{32};
EXPECT_EQ(
hostContext.checkKeylet(bytesOf(accountBytes), seq, out.slice()),
hfErrorToInt(HostFunctionError::LedgerObjNotFound));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(CheckKeyletCall, ShortAccountIsRefusedWithoutAskingHost)
{
Bytes const shortAccount(AccountID::size() - 1, 0x01);
EXPECT_CALL(host, checkKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.checkKeylet(bytesOf(shortAccount), seq, out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(CheckKeyletCall, LongAccountIsRefusedWithoutAskingHost)
{
Bytes const longAccount(AccountID::size() + 1, 0x01);
EXPECT_CALL(host, checkKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.checkKeylet(bytesOf(longAccount), seq, out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(CheckKeyletCall, EmptyAccountIsRefusedWithoutAskingHost)
{
EXPECT_CALL(host, checkKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.checkKeylet(bytesOf(Bytes{}), seq, out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(CheckKeyletCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, checkKeylet(account, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Throw(std::runtime_error{"check keylet came apart"}));
OutRegion out{32};
EXPECT_EQ(
hostContext.checkKeylet(bytesOf(accountBytes), seq, out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("check keylet came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("checkKeylet"));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(CheckKeyletCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, checkKeylet(account, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(keylet));
OutRegion out{keylet.size() - 1};
EXPECT_EQ(
hostContext.checkKeylet(bytesOf(accountBytes), seq, out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(CheckKeyletCall, OutRegionOfExactSizeIsWritten)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, checkKeylet(account, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(keylet));
OutRegion out{keylet.size()};
EXPECT_EQ(
hostContext.checkKeylet(bytesOf(accountBytes), seq, out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_TRUE(out.holds(bytesOf(keylet)));
}
TEST_F(CheckKeyletCall, EmptyResultAnswersZeroAndWritesNothing)
{
EXPECT_CALL(host, checkKeylet(account, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(Bytes{}));
OutRegion out{32};
EXPECT_EQ(hostContext.checkKeylet(bytesOf(accountBytes), seq, out.slice()), 0);
EXPECT_FALSE(out.wasWritten());
}
} // namespace xrpl::test

View File

@@ -0,0 +1,63 @@
#include <xrpl/basics/Slice.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <tx/wasm/MockHostFunctions.h>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// `checkSignature` validates nothing: message, signature and pubkey reach the host exactly as
// given, with no length check on any of them - deliberately, not by oversight.
struct CheckSignatureCall : HostContextTest
{
Bytes const message{'m', 's', 'g'};
Bytes const signature{'s', 'i', 'g'};
Bytes const pubkey{'k', 'e', 'y'};
};
TEST_F(CheckSignatureCall, MessageSignatureAndPubkeyForwardedVerbatim)
{
EXPECT_CALL(host, checkSignature(BytesAre("msg"), BytesAre("sig"), BytesAre("key")))
.WillOnce(testing::Return(1));
EXPECT_EQ(hostContext.checkSignature(bytesOf(message), bytesOf(signature), bytesOf(pubkey)), 1);
}
// The absence of any length check is a decision, not an oversight: empty slices are not a
// malformed shape here, they reach the host like any other.
TEST_F(CheckSignatureCall, EmptySlicesReachHostUnvalidated)
{
auto const isEmpty = testing::Property(&Slice::empty, true);
EXPECT_CALL(host, checkSignature(isEmpty, isEmpty, isEmpty)).WillOnce(testing::Return(0));
EXPECT_EQ(hostContext.checkSignature(bytesOf(Bytes{}), bytesOf(Bytes{}), bytesOf(Bytes{})), 0);
}
TEST_F(CheckSignatureCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, checkSignature(BytesAre("msg"), BytesAre("sig"), BytesAre("key")))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::InvalidParams)));
EXPECT_EQ(
hostContext.checkSignature(bytesOf(message), bytesOf(signature), bytesOf(pubkey)),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(CheckSignatureCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, checkSignature(BytesAre("msg"), BytesAre("sig"), BytesAre("key")))
.WillOnce(testing::Throw(std::runtime_error{"signature check came apart"}));
EXPECT_EQ(
hostContext.checkSignature(bytesOf(message), bytesOf(signature), bytesOf(pubkey)),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("signature check came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("checkSignature"));
}
} // namespace xrpl::test

View File

@@ -0,0 +1,179 @@
#include <xrpl/basics/Slice.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
//
// `subject` and `issuer` are distinct byte patterns: a happy path built from two copies of the
// same account would still pass if the two were swapped.
struct CredentialKeyletCall : HostContextTest
{
Bytes const subjectBytes{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14};
Bytes const issuerBytes{0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a,
0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54};
Bytes const credentialTypeBytes{0x74, 0x65, 0x72, 0x6d, 0x73};
AccountID const subject = AccountID::fromVoid(subjectBytes.data());
AccountID const issuer = AccountID::fromVoid(issuerBytes.data());
Slice const credentialType{credentialTypeBytes.data(), credentialTypeBytes.size()};
};
// `credentialType` crosses unvalidated: whatever bytes the guest gives reach the host as-is.
TEST_F(CredentialKeyletCall, SubjectAndIssuerAreForwardedCredentialTypeUnvalidatedKeyletIsWritten)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, credentialKeylet(subject, issuer, credentialType))
.WillOnce(testing::Return(keylet));
OutRegion out{32};
EXPECT_EQ(
hostContext.credentialKeylet(
bytesOf(subjectBytes), bytesOf(issuerBytes), bytesOf(credentialTypeBytes), out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_TRUE(out.holds(bytesOf(keylet)));
}
// The deliberate edge of "unvalidated": an empty `credentialType` is not a length the ABI
// rejects, so it reaches the host as an empty `Slice` and the call still succeeds.
TEST_F(CredentialKeyletCall, EmptyCredentialTypeIsForwardedUnvalidatedKeyletIsWritten)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, credentialKeylet(subject, issuer, Slice{})).WillOnce(testing::Return(keylet));
OutRegion out{32};
EXPECT_EQ(
hostContext.credentialKeylet(
bytesOf(subjectBytes), bytesOf(issuerBytes), bytesOf(Bytes{}), out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_TRUE(out.holds(bytesOf(keylet)));
}
TEST_F(CredentialKeyletCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, credentialKeylet(subject, issuer, credentialType))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
OutRegion out{32};
EXPECT_EQ(
hostContext.credentialKeylet(
bytesOf(subjectBytes), bytesOf(issuerBytes), bytesOf(credentialTypeBytes), out.slice()),
hfErrorToInt(HostFunctionError::LedgerObjNotFound));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(CredentialKeyletCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, credentialKeylet(subject, issuer, credentialType))
.WillOnce(testing::Throw(std::runtime_error{"credential keylet came apart"}));
OutRegion out{32};
EXPECT_EQ(
hostContext.credentialKeylet(
bytesOf(subjectBytes), bytesOf(issuerBytes), bytesOf(credentialTypeBytes), out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("credential keylet came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("credentialKeylet"));
}
TEST_F(CredentialKeyletCall, MalformedSubjectIsRefusedWithoutAskingHost)
{
Bytes const malformedSubject(AccountID::size() - 1, 0x01);
EXPECT_CALL(host, credentialKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.credentialKeylet(
bytesOf(malformedSubject),
bytesOf(issuerBytes),
bytesOf(credentialTypeBytes),
out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(CredentialKeyletCall, MalformedIssuerIsRefusedWithoutAskingHost)
{
Bytes const malformedIssuer(AccountID::size() + 1, 0x41);
EXPECT_CALL(host, credentialKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.credentialKeylet(
bytesOf(subjectBytes),
bytesOf(malformedIssuer),
bytesOf(credentialTypeBytes),
out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
// Both ids fail one combined length check, so a call malformed in both places answers the
// same `InvalidParams` as either alone; what's observable is that the host is never asked.
TEST_F(CredentialKeyletCall, BothAccountsMalformedIsRefusedWithoutAskingHost)
{
Bytes const malformedSubject(AccountID::size() - 1, 0x01);
Bytes const malformedIssuer(AccountID::size() - 1, 0x41);
EXPECT_CALL(host, credentialKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.credentialKeylet(
bytesOf(malformedSubject),
bytesOf(malformedIssuer),
bytesOf(credentialTypeBytes),
out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(CredentialKeyletCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, credentialKeylet(subject, issuer, credentialType))
.WillOnce(testing::Return(keylet));
OutRegion out{keylet.size() - 1};
EXPECT_EQ(
hostContext.credentialKeylet(
bytesOf(subjectBytes), bytesOf(issuerBytes), bytesOf(credentialTypeBytes), out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(CredentialKeyletCall, OutRegionOfExactSizeIsWritten)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, credentialKeylet(subject, issuer, credentialType))
.WillOnce(testing::Return(keylet));
OutRegion out{keylet.size()};
EXPECT_EQ(
hostContext.credentialKeylet(
bytesOf(subjectBytes), bytesOf(issuerBytes), bytesOf(credentialTypeBytes), out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_TRUE(out.holds(bytesOf(keylet)));
}
TEST_F(CredentialKeyletCall, EmptyResultAnswersZeroAndWritesNothing)
{
EXPECT_CALL(host, credentialKeylet(subject, issuer, credentialType))
.WillOnce(testing::Return(Bytes{}));
OutRegion out{32};
EXPECT_EQ(
hostContext.credentialKeylet(
bytesOf(subjectBytes), bytesOf(issuerBytes), bytesOf(credentialTypeBytes), out.slice()),
0);
EXPECT_FALSE(out.wasWritten());
}
} // namespace xrpl::test

View File

@@ -0,0 +1,63 @@
#include <xrpl/protocol/SField.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// `getCurrentLedgerObjArrayLen` answers its count directly rather than through an out region:
// no axis E, no `OutRegion`, and the happy path asserts the returned count.
struct CurrentLedgerObjArrayLenCall : HostContextTest
{
std::int32_t fieldCode = sfBalance.getCode();
};
TEST_F(CurrentLedgerObjArrayLenCall, FieldCodeBecomesSFieldHostIsAskedFor)
{
EXPECT_CALL(host, getCurrentLedgerObjArrayLen(testing::Ref(sfBalance)))
.WillOnce(testing::Return(5));
EXPECT_EQ(hostContext.getCurrentLedgerObjArrayLen(fieldCode), 5);
}
// `NoArray` is what a field that is not an array actually answers, so it stands in for axis B
// here rather than an arbitrary code.
TEST_F(CurrentLedgerObjArrayLenCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, getCurrentLedgerObjArrayLen(testing::Ref(sfBalance)))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::NoArray)));
EXPECT_EQ(
hostContext.getCurrentLedgerObjArrayLen(fieldCode),
hfErrorToInt(HostFunctionError::NoArray));
}
TEST_F(CurrentLedgerObjArrayLenCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, getCurrentLedgerObjArrayLen(testing::Ref(sfBalance)))
.WillOnce(testing::Throw(std::runtime_error{"current ledger obj array len came apart"}));
EXPECT_EQ(
hostContext.getCurrentLedgerObjArrayLen(fieldCode),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("current ledger obj array len came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("getCurrentLedgerObjArrayLen"));
}
TEST_F(CurrentLedgerObjArrayLenCall, UnknownFieldCodeIsRefusedWithoutAskingHost)
{
fieldCode = 0x7fff'0000; // a code nothing is registered under
EXPECT_CALL(host, getCurrentLedgerObjArrayLen).Times(0);
EXPECT_EQ(
hostContext.getCurrentLedgerObjArrayLen(fieldCode),
hfErrorToInt(HostFunctionError::InvalidField));
}
} // namespace xrpl::test

View File

@@ -0,0 +1,112 @@
#include <xrpl/protocol/SField.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// The engine's own rules - buffer-fit, the field cap, guest memory - are tested on the Rust
// side, not here. The cross-cutting cases over this shape - a non-`std::exception` throw, and
// a length past `kMaxWasmDataLength` - already live in `TxField.cpp`.
//
// Named `CurrentLedgerObjFieldDirectCall`, not `CurrentLedgerObjFieldCall`:
// `host_calls/CurrentLedgerObjField.cpp` already owns that name in the same gtest binary.
struct CurrentLedgerObjFieldDirectCall : HostContextTest
{
std::int32_t fieldCode = sfBalance.getCode();
};
TEST_F(CurrentLedgerObjFieldDirectCall, FieldCodeBecomesSFieldHostIsAskedFor)
{
Bytes const value{1, 2, 3};
EXPECT_CALL(host, getCurrentLedgerObjField(testing::Ref(sfBalance)))
.WillOnce(testing::Return(value));
OutRegion out{32};
EXPECT_EQ(
hostContext.getCurrentLedgerObjField(fieldCode, out.slice()),
static_cast<std::int32_t>(value.size()));
EXPECT_TRUE(out.holds(bytesOf(value)));
}
TEST_F(CurrentLedgerObjFieldDirectCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, getCurrentLedgerObjField(testing::Ref(sfBalance)))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::FieldNotFound)));
OutRegion out{32};
EXPECT_EQ(
hostContext.getCurrentLedgerObjField(fieldCode, out.slice()),
hfErrorToInt(HostFunctionError::FieldNotFound));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(CurrentLedgerObjFieldDirectCall, UnknownFieldCodeIsRefusedWithoutAskingHost)
{
fieldCode = 0x7fff'0000; // a code nothing is registered under
EXPECT_CALL(host, getCurrentLedgerObjField).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.getCurrentLedgerObjField(fieldCode, out.slice()),
hfErrorToInt(HostFunctionError::InvalidField));
}
TEST_F(CurrentLedgerObjFieldDirectCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, getCurrentLedgerObjField(testing::Ref(sfBalance)))
.WillOnce(testing::Throw(std::runtime_error{"current ledger obj field came apart"}));
OutRegion out{32};
EXPECT_EQ(
hostContext.getCurrentLedgerObjField(fieldCode, out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("current ledger obj field came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("getCurrentLedgerObjField"));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(CurrentLedgerObjFieldDirectCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
Bytes const value{1, 2, 3};
EXPECT_CALL(host, getCurrentLedgerObjField(testing::Ref(sfBalance)))
.WillOnce(testing::Return(value));
OutRegion out{value.size() - 1};
EXPECT_EQ(
hostContext.getCurrentLedgerObjField(fieldCode, out.slice()),
static_cast<std::int32_t>(value.size()));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(CurrentLedgerObjFieldDirectCall, OutRegionOfExactSizeIsWritten)
{
Bytes const value{1, 2, 3};
EXPECT_CALL(host, getCurrentLedgerObjField(testing::Ref(sfBalance)))
.WillOnce(testing::Return(value));
OutRegion out{value.size()};
EXPECT_EQ(
hostContext.getCurrentLedgerObjField(fieldCode, out.slice()),
static_cast<std::int32_t>(value.size()));
EXPECT_TRUE(out.holds(bytesOf(value)));
}
TEST_F(CurrentLedgerObjFieldDirectCall, EmptyResultAnswersZeroAndWritesNothing)
{
EXPECT_CALL(host, getCurrentLedgerObjField(testing::Ref(sfBalance)))
.WillOnce(testing::Return(Bytes{}));
OutRegion out{32};
EXPECT_EQ(hostContext.getCurrentLedgerObjField(fieldCode, out.slice()), 0);
EXPECT_FALSE(out.wasWritten());
}
} // namespace xrpl::test

View File

@@ -0,0 +1,78 @@
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
#include <vector>
namespace xrpl::test {
// The engine's own rules - buffer-fit, the field cap, guest memory - are tested on the Rust
// side, not here.
//
// No out region and no axis E: `getCurrentLedgerObjNestedArrayLen` answers the array's
// element count directly rather than through a written buffer.
struct CurrentLedgerObjNestedArrayLenCall : HostContextTest
{
std::vector<std::int32_t> const steps{5, -12, 130};
Bytes const locatorBytes = bytesOfSteps(steps);
};
TEST_F(CurrentLedgerObjNestedArrayLenCall, LocatorBytesBecomeFieldLocatorHostReturnsCount)
{
EXPECT_CALL(host, getCurrentLedgerObjNestedArrayLen(LocatorEquals(steps)))
.WillOnce(testing::Return(7));
EXPECT_EQ(hostContext.getCurrentLedgerObjNestedArrayLen(bytesOf(locatorBytes)), 7);
}
// `NoArray` - the field the locator resolves to is not an array - is the error this shape
// most plausibly returns, so it stands in for axis B.
TEST_F(CurrentLedgerObjNestedArrayLenCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, getCurrentLedgerObjNestedArrayLen(LocatorEquals(steps)))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::NoArray)));
EXPECT_EQ(
hostContext.getCurrentLedgerObjNestedArrayLen(bytesOf(locatorBytes)),
hfErrorToInt(HostFunctionError::NoArray));
}
TEST_F(CurrentLedgerObjNestedArrayLenCall, EmptyLocatorIsRefusedWithoutAskingHost)
{
EXPECT_CALL(host, getCurrentLedgerObjNestedArrayLen).Times(0);
EXPECT_EQ(
hostContext.getCurrentLedgerObjNestedArrayLen(bytesOf(Bytes{})),
hfErrorToInt(HostFunctionError::LocatorMalformed));
}
// Distinct from an empty locator: `invokeWithLocator` checks the two conditions separately.
TEST_F(CurrentLedgerObjNestedArrayLenCall, MisalignedLocatorLengthIsRefusedWithoutAskingHost)
{
Bytes const oddLength{1, 2, 3};
EXPECT_CALL(host, getCurrentLedgerObjNestedArrayLen).Times(0);
EXPECT_EQ(
hostContext.getCurrentLedgerObjNestedArrayLen(bytesOf(oddLength)),
hfErrorToInt(HostFunctionError::LocatorMalformed));
}
TEST_F(CurrentLedgerObjNestedArrayLenCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, getCurrentLedgerObjNestedArrayLen(LocatorEquals(steps)))
.WillOnce(
testing::Throw(std::runtime_error{"current ledger obj nested array len came apart"}));
EXPECT_EQ(
hostContext.getCurrentLedgerObjNestedArrayLen(bytesOf(locatorBytes)),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("current ledger obj nested array len came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("getCurrentLedgerObjNestedArrayLen"));
}
} // namespace xrpl::test

View File

@@ -0,0 +1,120 @@
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
#include <vector>
namespace xrpl::test {
// The engine's own rules - buffer-fit, the field cap, guest memory - are tested on the Rust
// side, not here.
struct CurrentLedgerObjNestedFieldCall : HostContextTest
{
std::vector<std::int32_t> const steps{5, -12, 130};
Bytes const locatorBytes = bytesOfSteps(steps);
};
TEST_F(CurrentLedgerObjNestedFieldCall, LocatorBytesBecomeFieldLocatorHostIsAskedFor)
{
Bytes const value{1, 2, 3};
EXPECT_CALL(host, getCurrentLedgerObjNestedField(LocatorEquals(steps)))
.WillOnce(testing::Return(value));
OutRegion out{32};
EXPECT_EQ(
hostContext.getCurrentLedgerObjNestedField(bytesOf(locatorBytes), out.slice()),
static_cast<std::int32_t>(value.size()));
EXPECT_TRUE(out.holds(bytesOf(value)));
}
TEST_F(CurrentLedgerObjNestedFieldCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, getCurrentLedgerObjNestedField(LocatorEquals(steps)))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::NotLeafField)));
OutRegion out{32};
EXPECT_EQ(
hostContext.getCurrentLedgerObjNestedField(bytesOf(locatorBytes), out.slice()),
hfErrorToInt(HostFunctionError::NotLeafField));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(CurrentLedgerObjNestedFieldCall, EmptyLocatorIsRefusedWithoutAskingHost)
{
EXPECT_CALL(host, getCurrentLedgerObjNestedField).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.getCurrentLedgerObjNestedField(bytesOf(Bytes{}), out.slice()),
hfErrorToInt(HostFunctionError::LocatorMalformed));
}
// Distinct from an empty locator: `invokeWithLocator` checks the two conditions separately.
TEST_F(CurrentLedgerObjNestedFieldCall, MisalignedLocatorLengthIsRefusedWithoutAskingHost)
{
Bytes const oddLength{1, 2, 3};
EXPECT_CALL(host, getCurrentLedgerObjNestedField).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.getCurrentLedgerObjNestedField(bytesOf(oddLength), out.slice()),
hfErrorToInt(HostFunctionError::LocatorMalformed));
}
TEST_F(CurrentLedgerObjNestedFieldCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, getCurrentLedgerObjNestedField(LocatorEquals(steps)))
.WillOnce(testing::Throw(std::runtime_error{"current ledger obj nested field came apart"}));
OutRegion out{32};
EXPECT_EQ(
hostContext.getCurrentLedgerObjNestedField(bytesOf(locatorBytes), out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("current ledger obj nested field came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("getCurrentLedgerObjNestedField"));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(CurrentLedgerObjNestedFieldCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
Bytes const value{1, 2, 3};
EXPECT_CALL(host, getCurrentLedgerObjNestedField(LocatorEquals(steps)))
.WillOnce(testing::Return(value));
OutRegion out{value.size() - 1};
EXPECT_EQ(
hostContext.getCurrentLedgerObjNestedField(bytesOf(locatorBytes), out.slice()),
static_cast<std::int32_t>(value.size()));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(CurrentLedgerObjNestedFieldCall, OutRegionOfExactSizeIsWritten)
{
Bytes const value{1, 2, 3};
EXPECT_CALL(host, getCurrentLedgerObjNestedField(LocatorEquals(steps)))
.WillOnce(testing::Return(value));
OutRegion out{value.size()};
EXPECT_EQ(
hostContext.getCurrentLedgerObjNestedField(bytesOf(locatorBytes), out.slice()),
static_cast<std::int32_t>(value.size()));
EXPECT_TRUE(out.holds(bytesOf(value)));
}
TEST_F(CurrentLedgerObjNestedFieldCall, EmptyResultAnswersZeroAndWritesNothing)
{
EXPECT_CALL(host, getCurrentLedgerObjNestedField(LocatorEquals(steps)))
.WillOnce(testing::Return(Bytes{}));
OutRegion out{32};
EXPECT_EQ(hostContext.getCurrentLedgerObjNestedField(bytesOf(locatorBytes), out.slice()), 0);
EXPECT_FALSE(out.wasWritten());
}
} // namespace xrpl::test

View File

@@ -0,0 +1,138 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
//
// `account` and `authorize` are distinct byte patterns: a happy path built from two copies of
// the same account would still pass if the two were swapped.
struct DelegateKeyletCall : HostContextTest
{
Bytes const accountBytes{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14};
Bytes const authorizeBytes{0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea,
0xeb, 0xec, 0xed, 0xee, 0xef, 0xf0, 0xf1, 0xf2, 0xf3, 0xf4};
AccountID const account = AccountID::fromVoid(accountBytes.data());
AccountID const authorize = AccountID::fromVoid(authorizeBytes.data());
};
TEST_F(DelegateKeyletCall, AccountAndAuthorizeAreForwardedInOrderKeyletIsWritten)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, delegateKeylet(account, authorize)).WillOnce(testing::Return(keylet));
OutRegion out{32};
EXPECT_EQ(
hostContext.delegateKeylet(bytesOf(accountBytes), bytesOf(authorizeBytes), out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_TRUE(out.holds(bytesOf(keylet)));
}
TEST_F(DelegateKeyletCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, delegateKeylet(account, authorize))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
OutRegion out{32};
EXPECT_EQ(
hostContext.delegateKeylet(bytesOf(accountBytes), bytesOf(authorizeBytes), out.slice()),
hfErrorToInt(HostFunctionError::LedgerObjNotFound));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(DelegateKeyletCall, MalformedAccountIsRefusedWithoutAskingHost)
{
Bytes const malformedAccount(AccountID::size() - 1, 0x01);
EXPECT_CALL(host, delegateKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.delegateKeylet(bytesOf(malformedAccount), bytesOf(authorizeBytes), out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(DelegateKeyletCall, MalformedAuthorizeIsRefusedWithoutAskingHost)
{
Bytes const malformedAuthorize(AccountID::size() + 1, 0xe1);
EXPECT_CALL(host, delegateKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.delegateKeylet(bytesOf(accountBytes), bytesOf(malformedAuthorize), out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
// Both ids fail one combined length check, so a call malformed in both places answers the
// same `InvalidParams` as either alone; what's observable is that the host is never asked.
TEST_F(DelegateKeyletCall, BothAccountsMalformedIsRefusedWithoutAskingHost)
{
Bytes const malformedAccount(AccountID::size() - 1, 0x01);
Bytes const malformedAuthorize(AccountID::size() - 1, 0xe1);
EXPECT_CALL(host, delegateKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.delegateKeylet(
bytesOf(malformedAccount), bytesOf(malformedAuthorize), out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(DelegateKeyletCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, delegateKeylet(account, authorize))
.WillOnce(testing::Throw(std::runtime_error{"delegate keylet came apart"}));
OutRegion out{32};
EXPECT_EQ(
hostContext.delegateKeylet(bytesOf(accountBytes), bytesOf(authorizeBytes), out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("delegate keylet came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("delegateKeylet"));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(DelegateKeyletCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, delegateKeylet(account, authorize)).WillOnce(testing::Return(keylet));
OutRegion out{keylet.size() - 1};
EXPECT_EQ(
hostContext.delegateKeylet(bytesOf(accountBytes), bytesOf(authorizeBytes), out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(DelegateKeyletCall, OutRegionOfExactSizeIsWritten)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, delegateKeylet(account, authorize)).WillOnce(testing::Return(keylet));
OutRegion out{keylet.size()};
EXPECT_EQ(
hostContext.delegateKeylet(bytesOf(accountBytes), bytesOf(authorizeBytes), out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_TRUE(out.holds(bytesOf(keylet)));
}
TEST_F(DelegateKeyletCall, EmptyResultAnswersZeroAndWritesNothing)
{
EXPECT_CALL(host, delegateKeylet(account, authorize)).WillOnce(testing::Return(Bytes{}));
OutRegion out{32};
EXPECT_EQ(
hostContext.delegateKeylet(bytesOf(accountBytes), bytesOf(authorizeBytes), out.slice()), 0);
EXPECT_FALSE(out.wasWritten());
}
} // namespace xrpl::test

View File

@@ -0,0 +1,147 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
//
// `account` and `authorize` are distinct byte patterns: a happy path built from two copies of
// the same account would still pass if the two were swapped.
struct DepositPreauthKeyletCall : HostContextTest
{
Bytes const accountBytes{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14};
Bytes const authorizeBytes{0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a,
0x9b, 0x9c, 0x9d, 0x9e, 0x9f, 0xa0, 0xa1, 0xa2, 0xa3, 0xa4};
AccountID const account = AccountID::fromVoid(accountBytes.data());
AccountID const authorize = AccountID::fromVoid(authorizeBytes.data());
};
TEST_F(DepositPreauthKeyletCall, AccountAndAuthorizeAreForwardedInOrderKeyletIsWritten)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, depositPreauthKeylet(account, authorize)).WillOnce(testing::Return(keylet));
OutRegion out{32};
EXPECT_EQ(
hostContext.depositPreauthKeylet(
bytesOf(accountBytes), bytesOf(authorizeBytes), out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_TRUE(out.holds(bytesOf(keylet)));
}
TEST_F(DepositPreauthKeyletCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, depositPreauthKeylet(account, authorize))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
OutRegion out{32};
EXPECT_EQ(
hostContext.depositPreauthKeylet(
bytesOf(accountBytes), bytesOf(authorizeBytes), out.slice()),
hfErrorToInt(HostFunctionError::LedgerObjNotFound));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(DepositPreauthKeyletCall, MalformedAccountIsRefusedWithoutAskingHost)
{
Bytes const malformedAccount(AccountID::size() - 1, 0x01);
EXPECT_CALL(host, depositPreauthKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.depositPreauthKeylet(
bytesOf(malformedAccount), bytesOf(authorizeBytes), out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(DepositPreauthKeyletCall, MalformedAuthorizeIsRefusedWithoutAskingHost)
{
Bytes const malformedAuthorize(AccountID::size() + 1, 0x91);
EXPECT_CALL(host, depositPreauthKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.depositPreauthKeylet(
bytesOf(accountBytes), bytesOf(malformedAuthorize), out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
// Both ids fail one combined length check, so a call malformed in both places answers the
// same `InvalidParams` as either alone; what's observable is that the host is never asked.
TEST_F(DepositPreauthKeyletCall, BothAccountsMalformedIsRefusedWithoutAskingHost)
{
Bytes const malformedAccount(AccountID::size() - 1, 0x01);
Bytes const malformedAuthorize(AccountID::size() - 1, 0x91);
EXPECT_CALL(host, depositPreauthKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.depositPreauthKeylet(
bytesOf(malformedAccount), bytesOf(malformedAuthorize), out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(DepositPreauthKeyletCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, depositPreauthKeylet(account, authorize))
.WillOnce(testing::Throw(std::runtime_error{"deposit preauth keylet came apart"}));
OutRegion out{32};
EXPECT_EQ(
hostContext.depositPreauthKeylet(
bytesOf(accountBytes), bytesOf(authorizeBytes), out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("deposit preauth keylet came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("depositPreauthKeylet"));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(DepositPreauthKeyletCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, depositPreauthKeylet(account, authorize)).WillOnce(testing::Return(keylet));
OutRegion out{keylet.size() - 1};
EXPECT_EQ(
hostContext.depositPreauthKeylet(
bytesOf(accountBytes), bytesOf(authorizeBytes), out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(DepositPreauthKeyletCall, OutRegionOfExactSizeIsWritten)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, depositPreauthKeylet(account, authorize)).WillOnce(testing::Return(keylet));
OutRegion out{keylet.size()};
EXPECT_EQ(
hostContext.depositPreauthKeylet(
bytesOf(accountBytes), bytesOf(authorizeBytes), out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_TRUE(out.holds(bytesOf(keylet)));
}
TEST_F(DepositPreauthKeyletCall, EmptyResultAnswersZeroAndWritesNothing)
{
EXPECT_CALL(host, depositPreauthKeylet(account, authorize)).WillOnce(testing::Return(Bytes{}));
OutRegion out{32};
EXPECT_EQ(
hostContext.depositPreauthKeylet(
bytesOf(accountBytes), bytesOf(authorizeBytes), out.slice()),
0);
EXPECT_FALSE(out.wasWritten());
}
} // namespace xrpl::test

View File

@@ -0,0 +1,126 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
struct DidKeyletCall : HostContextTest
{
Bytes const accountBytes{0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a,
0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40, 0x41, 0x42, 0x43, 0x44};
AccountID const account = AccountID::fromVoid(accountBytes.data());
};
TEST_F(DidKeyletCall, AccountIsForwardedKeyletIsWritten)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, didKeylet(account)).WillOnce(testing::Return(keylet));
OutRegion out{32};
EXPECT_EQ(
hostContext.didKeylet(bytesOf(accountBytes), out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_TRUE(out.holds(bytesOf(keylet)));
}
TEST_F(DidKeyletCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, didKeylet(account))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
OutRegion out{32};
EXPECT_EQ(
hostContext.didKeylet(bytesOf(accountBytes), out.slice()),
hfErrorToInt(HostFunctionError::LedgerObjNotFound));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(DidKeyletCall, ShortAccountIsRefusedWithoutAskingHost)
{
Bytes const shortAccount(AccountID::size() - 1, 0x01);
EXPECT_CALL(host, didKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.didKeylet(bytesOf(shortAccount), out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(DidKeyletCall, LongAccountIsRefusedWithoutAskingHost)
{
Bytes const longAccount(AccountID::size() + 1, 0x01);
EXPECT_CALL(host, didKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.didKeylet(bytesOf(longAccount), out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(DidKeyletCall, EmptyAccountIsRefusedWithoutAskingHost)
{
EXPECT_CALL(host, didKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.didKeylet(bytesOf(Bytes{}), out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(DidKeyletCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, didKeylet(account))
.WillOnce(testing::Throw(std::runtime_error{"did keylet came apart"}));
OutRegion out{32};
EXPECT_EQ(
hostContext.didKeylet(bytesOf(accountBytes), out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("did keylet came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("didKeylet"));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(DidKeyletCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, didKeylet(account)).WillOnce(testing::Return(keylet));
OutRegion out{keylet.size() - 1};
EXPECT_EQ(
hostContext.didKeylet(bytesOf(accountBytes), out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(DidKeyletCall, OutRegionOfExactSizeIsWritten)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, didKeylet(account)).WillOnce(testing::Return(keylet));
OutRegion out{keylet.size()};
EXPECT_EQ(
hostContext.didKeylet(bytesOf(accountBytes), out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_TRUE(out.holds(bytesOf(keylet)));
}
TEST_F(DidKeyletCall, EmptyResultAnswersZeroAndWritesNothing)
{
EXPECT_CALL(host, didKeylet(account)).WillOnce(testing::Return(Bytes{}));
OutRegion out{32};
EXPECT_EQ(hostContext.didKeylet(bytesOf(accountBytes), out.slice()), 0);
EXPECT_FALSE(out.wasWritten());
}
} // namespace xrpl::test

View File

@@ -0,0 +1,152 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <limits>
#include <stdexcept>
namespace xrpl::test {
// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
//
// The first file over the account-in, keylet-out shape `invokeWithAccount` gives eleven other
// methods, so `account` is a distinctive 20 bytes rather than all-zero: a forwarding mistake
// (a swapped byte, a truncated copy) would still pass against an all-zero id.
struct EscrowKeyletCall : HostContextTest
{
Bytes const accountBytes{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14};
AccountID const account = AccountID::fromVoid(accountBytes.data());
std::int32_t const seq = 12345;
};
TEST_F(EscrowKeyletCall, AccountAndSeqAreForwardedKeyletIsWritten)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, escrowKeylet(account, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(keylet));
OutRegion out{32};
EXPECT_EQ(
hostContext.escrowKeylet(bytesOf(accountBytes), seq, out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_TRUE(out.holds(bytesOf(keylet)));
}
TEST_F(EscrowKeyletCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, escrowKeylet(account, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
OutRegion out{32};
EXPECT_EQ(
hostContext.escrowKeylet(bytesOf(accountBytes), seq, out.slice()),
hfErrorToInt(HostFunctionError::LedgerObjNotFound));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(EscrowKeyletCall, ShortAccountIsRefusedWithoutAskingHost)
{
Bytes const shortAccount(AccountID::size() - 1, 0x01);
EXPECT_CALL(host, escrowKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.escrowKeylet(bytesOf(shortAccount), seq, out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(EscrowKeyletCall, LongAccountIsRefusedWithoutAskingHost)
{
Bytes const longAccount(AccountID::size() + 1, 0x01);
EXPECT_CALL(host, escrowKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.escrowKeylet(bytesOf(longAccount), seq, out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(EscrowKeyletCall, EmptyAccountIsRefusedWithoutAskingHost)
{
EXPECT_CALL(host, escrowKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.escrowKeylet(bytesOf(Bytes{}), seq, out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(EscrowKeyletCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, escrowKeylet(account, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Throw(std::runtime_error{"escrow keylet came apart"}));
OutRegion out{32};
EXPECT_EQ(
hostContext.escrowKeylet(bytesOf(accountBytes), seq, out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("escrow keylet came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("escrowKeylet"));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(EscrowKeyletCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, escrowKeylet(account, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(keylet));
OutRegion out{keylet.size() - 1};
EXPECT_EQ(
hostContext.escrowKeylet(bytesOf(accountBytes), seq, out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(EscrowKeyletCall, OutRegionOfExactSizeIsWritten)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, escrowKeylet(account, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(keylet));
OutRegion out{keylet.size()};
EXPECT_EQ(
hostContext.escrowKeylet(bytesOf(accountBytes), seq, out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_TRUE(out.holds(bytesOf(keylet)));
}
TEST_F(EscrowKeyletCall, EmptyResultAnswersZeroAndWritesNothing)
{
EXPECT_CALL(host, escrowKeylet(account, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(Bytes{}));
OutRegion out{32};
EXPECT_EQ(hostContext.escrowKeylet(bytesOf(accountBytes), seq, out.slice()), 0);
EXPECT_FALSE(out.wasWritten());
}
// `seq` crosses the ABI as an `i32` bit pattern, not a signed count: the guest's
// `4294967295u` is this `-1`, and `escrowKeylet` must hand the host back `4294967295u`, not a
// sign-extended or clamped value.
TEST_F(EscrowKeyletCall, NegativeSeqArrivesAtHostAsUnsignedBitPattern)
{
std::int32_t const negativeSeq = -1;
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, escrowKeylet(account, std::numeric_limits<std::uint32_t>::max()))
.WillOnce(testing::Return(keylet));
OutRegion out{32};
EXPECT_EQ(
hostContext.escrowKeylet(bytesOf(accountBytes), negativeSeq, out.slice()),
static_cast<std::int32_t>(keylet.size()));
}
} // namespace xrpl::test

View File

@@ -0,0 +1,89 @@
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <tx/wasm/MockHostFunctions.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// Every input slice passes straight through to the host, unlike `invokeWithAccount`'s
// twenty-byte check or `parseUint64`'s eight: nothing here is validated, so there is no D
// axis. `x` and `y` carry different content, so a call that swapped them would fail to match.
struct FloatAddCall : HostContextTest
{
Bytes const x{'a', 'd', 'd', '-', 'x'};
Bytes const y{'a', 'd', 'd', '-', 'y', 'y'};
std::int32_t const mode = 7;
};
TEST_F(FloatAddCall, OperandsAndModeAreForwardedResultIsWritten)
{
Bytes const result{9, 8, 7};
EXPECT_CALL(host, floatAdd(BytesAre("add-x"), BytesAre("add-yy"), mode))
.WillOnce(testing::Return(result));
OutRegion out{32};
EXPECT_EQ(
hostContext.floatAdd(bytesOf(x), bytesOf(y), mode, out.slice()),
static_cast<std::int32_t>(result.size()));
EXPECT_TRUE(out.holds(bytesOf(result)));
}
TEST_F(FloatAddCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, floatAdd(BytesAre("add-x"), BytesAre("add-yy"), mode))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::FloatComputationError)));
OutRegion out{32};
EXPECT_EQ(
hostContext.floatAdd(bytesOf(x), bytesOf(y), mode, out.slice()),
hfErrorToInt(HostFunctionError::FloatComputationError));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(FloatAddCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, floatAdd(BytesAre("add-x"), BytesAre("add-yy"), mode))
.WillOnce(testing::Throw(std::runtime_error{"float add came apart"}));
OutRegion out{32};
EXPECT_EQ(
hostContext.floatAdd(bytesOf(x), bytesOf(y), mode, out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("float add came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("floatAdd"));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(FloatAddCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
Bytes const result{9, 8, 7};
EXPECT_CALL(host, floatAdd(BytesAre("add-x"), BytesAre("add-yy"), mode))
.WillOnce(testing::Return(result));
OutRegion out{result.size() - 1};
EXPECT_EQ(
hostContext.floatAdd(bytesOf(x), bytesOf(y), mode, out.slice()),
static_cast<std::int32_t>(result.size()));
EXPECT_FALSE(out.wasWritten());
}
// No length rule exists at this layer: a differently sized operand still reaches the host
// rather than being refused.
TEST_F(FloatAddCall, OddSizedOperandReachesHostUnchanged)
{
Bytes const shortX{0x2a};
EXPECT_CALL(host, floatAdd(testing::_, BytesAre("add-yy"), mode))
.WillOnce(testing::Return(Bytes{1}));
OutRegion out{32};
EXPECT_EQ(hostContext.floatAdd(bytesOf(shortX), bytesOf(y), mode, out.slice()), 1);
}
} // namespace xrpl::test

View File

@@ -0,0 +1,64 @@
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <tx/wasm/MockHostFunctions.h>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// Every input slice passes straight through to the host, unlike `invokeWithAccount`'s
// twenty-byte check or `parseUint64`'s eight: nothing here is validated, so there is no D axis.
// `x` and `y` carry different content, so a call that swapped them would fail to match.
// `floatCompare` answers its comparison directly rather than through `answer`, so there is no
// out region and no axis E.
struct FloatCompareCall : HostContextTest
{
Bytes const x{'c', 'm', 'p', '-', 'x'};
Bytes const y{'c', 'm', 'p', '-', 'y', 'y'};
};
TEST_F(FloatCompareCall, XAndYAreForwardedResultReturnedDirectly)
{
EXPECT_CALL(host, floatCompare(BytesAre("cmp-x"), BytesAre("cmp-yy")))
.WillOnce(testing::Return(1));
EXPECT_EQ(hostContext.floatCompare(bytesOf(x), bytesOf(y)), 1);
}
TEST_F(FloatCompareCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, floatCompare(BytesAre("cmp-x"), BytesAre("cmp-yy")))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::FloatComputationError)));
EXPECT_EQ(
hostContext.floatCompare(bytesOf(x), bytesOf(y)),
hfErrorToInt(HostFunctionError::FloatComputationError));
}
TEST_F(FloatCompareCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, floatCompare(BytesAre("cmp-x"), BytesAre("cmp-yy")))
.WillOnce(testing::Throw(std::runtime_error{"float compare came apart"}));
EXPECT_EQ(
hostContext.floatCompare(bytesOf(x), bytesOf(y)),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("float compare came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("floatCompare"));
}
// No length rule exists at this layer: a differently sized operand still reaches the host
// rather than being refused.
TEST_F(FloatCompareCall, OddSizedOperandReachesHostUnchanged)
{
Bytes const oddX{0x2a};
EXPECT_CALL(host, floatCompare(testing::_, BytesAre("cmp-yy"))).WillOnce(testing::Return(0));
EXPECT_EQ(hostContext.floatCompare(bytesOf(oddX), bytesOf(y)), 0);
}
} // namespace xrpl::test

View File

@@ -0,0 +1,89 @@
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <tx/wasm/MockHostFunctions.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// Every input slice passes straight through to the host, unlike `invokeWithAccount`'s
// twenty-byte check or `parseUint64`'s eight: nothing here is validated, so there is no D
// axis. `x` and `y` carry different content, so a call that swapped them would fail to match.
struct FloatDivideCall : HostContextTest
{
Bytes const x{'d', 'i', 'v', '-', 'x'};
Bytes const y{'d', 'i', 'v', '-', 'y', 'y'};
std::int32_t const mode = 42;
};
TEST_F(FloatDivideCall, OperandsAndModeAreForwardedResultIsWritten)
{
Bytes const result{9, 8, 7};
EXPECT_CALL(host, floatDivide(BytesAre("div-x"), BytesAre("div-yy"), mode))
.WillOnce(testing::Return(result));
OutRegion out{32};
EXPECT_EQ(
hostContext.floatDivide(bytesOf(x), bytesOf(y), mode, out.slice()),
static_cast<std::int32_t>(result.size()));
EXPECT_TRUE(out.holds(bytesOf(result)));
}
TEST_F(FloatDivideCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, floatDivide(BytesAre("div-x"), BytesAre("div-yy"), mode))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::FloatComputationError)));
OutRegion out{32};
EXPECT_EQ(
hostContext.floatDivide(bytesOf(x), bytesOf(y), mode, out.slice()),
hfErrorToInt(HostFunctionError::FloatComputationError));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(FloatDivideCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, floatDivide(BytesAre("div-x"), BytesAre("div-yy"), mode))
.WillOnce(testing::Throw(std::runtime_error{"float divide came apart"}));
OutRegion out{32};
EXPECT_EQ(
hostContext.floatDivide(bytesOf(x), bytesOf(y), mode, out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("float divide came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("floatDivide"));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(FloatDivideCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
Bytes const result{9, 8, 7};
EXPECT_CALL(host, floatDivide(BytesAre("div-x"), BytesAre("div-yy"), mode))
.WillOnce(testing::Return(result));
OutRegion out{result.size() - 1};
EXPECT_EQ(
hostContext.floatDivide(bytesOf(x), bytesOf(y), mode, out.slice()),
static_cast<std::int32_t>(result.size()));
EXPECT_FALSE(out.wasWritten());
}
// No length rule exists at this layer: a differently sized operand still reaches the host
// rather than being refused.
TEST_F(FloatDivideCall, OddSizedOperandReachesHostUnchanged)
{
Bytes const shortX{0x2a};
EXPECT_CALL(host, floatDivide(testing::_, BytesAre("div-yy"), mode))
.WillOnce(testing::Return(Bytes{1}));
OutRegion out{32};
EXPECT_EQ(hostContext.floatDivide(bytesOf(shortX), bytesOf(y), mode, out.slice()), 1);
}
} // namespace xrpl::test

View File

@@ -0,0 +1,84 @@
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// `x` arrives as a wasm scalar, not as bytes to decode, so there is nothing here to get wrong
// about its shape - no D axis.
struct FloatFromIntCall : HostContextTest
{
std::int64_t const x = 123456789;
std::int32_t const mode = 1;
};
TEST_F(FloatFromIntCall, ValueAndModeAreForwardedResultIsWritten)
{
Bytes const result{1, 2, 3};
EXPECT_CALL(host, floatFromInt(x, mode)).WillOnce(testing::Return(result));
OutRegion out{32};
EXPECT_EQ(
hostContext.floatFromInt(x, mode, out.slice()), static_cast<std::int32_t>(result.size()));
EXPECT_TRUE(out.holds(bytesOf(result)));
}
// `mode` is forwarded verbatim: this layer validates nothing about it, so a nonsense value
// still reaches the host unchanged.
TEST_F(FloatFromIntCall, ModeIsForwardedVerbatim)
{
std::int32_t const nonsenseMode = -12345;
Bytes const result{1};
EXPECT_CALL(host, floatFromInt(x, nonsenseMode)).WillOnce(testing::Return(result));
OutRegion out{32};
EXPECT_EQ(
hostContext.floatFromInt(x, nonsenseMode, out.slice()),
static_cast<std::int32_t>(result.size()));
}
TEST_F(FloatFromIntCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, floatFromInt(x, mode))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::FloatComputationError)));
OutRegion out{32};
EXPECT_EQ(
hostContext.floatFromInt(x, mode, out.slice()),
hfErrorToInt(HostFunctionError::FloatComputationError));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(FloatFromIntCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, floatFromInt(x, mode))
.WillOnce(testing::Throw(std::runtime_error{"float from int came apart"}));
OutRegion out{32};
EXPECT_EQ(
hostContext.floatFromInt(x, mode, out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("float from int came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("floatFromInt"));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(FloatFromIntCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
Bytes const result{1, 2, 3};
EXPECT_CALL(host, floatFromInt(x, mode)).WillOnce(testing::Return(result));
OutRegion out{result.size() - 1};
EXPECT_EQ(
hostContext.floatFromInt(x, mode, out.slice()), static_cast<std::int32_t>(result.size()));
EXPECT_FALSE(out.wasWritten());
}
} // namespace xrpl::test

View File

@@ -0,0 +1,73 @@
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// `mantissa`, `exponent` and `mode` all arrive as wasm scalars, not as bytes to decode, so
// there is nothing here to get wrong about their shape - no D axis.
struct FloatFromMantExpCall : HostContextTest
{
std::int64_t const mantissa = 123456789;
std::int32_t const exponent = -5;
std::int32_t const mode = 1;
};
TEST_F(FloatFromMantExpCall, MantissaExponentAndModeAreForwardedResultIsWritten)
{
Bytes const result{1, 2, 3};
EXPECT_CALL(host, floatFromMantExp(mantissa, exponent, mode)).WillOnce(testing::Return(result));
OutRegion out{32};
EXPECT_EQ(
hostContext.floatFromMantExp(mantissa, exponent, mode, out.slice()),
static_cast<std::int32_t>(result.size()));
EXPECT_TRUE(out.holds(bytesOf(result)));
}
TEST_F(FloatFromMantExpCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, floatFromMantExp(mantissa, exponent, mode))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::FloatComputationError)));
OutRegion out{32};
EXPECT_EQ(
hostContext.floatFromMantExp(mantissa, exponent, mode, out.slice()),
hfErrorToInt(HostFunctionError::FloatComputationError));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(FloatFromMantExpCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, floatFromMantExp(mantissa, exponent, mode))
.WillOnce(testing::Throw(std::runtime_error{"float from mant exp came apart"}));
OutRegion out{32};
EXPECT_EQ(
hostContext.floatFromMantExp(mantissa, exponent, mode, out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("float from mant exp came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("floatFromMantExp"));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(FloatFromMantExpCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
Bytes const result{1, 2, 3};
EXPECT_CALL(host, floatFromMantExp(mantissa, exponent, mode)).WillOnce(testing::Return(result));
OutRegion out{result.size() - 1};
EXPECT_EQ(
hostContext.floatFromMantExp(mantissa, exponent, mode, out.slice()),
static_cast<std::int32_t>(result.size()));
EXPECT_FALSE(out.wasWritten());
}
} // namespace xrpl::test

View File

@@ -0,0 +1,102 @@
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
namespace {
Bytes
serialized(STAmount const& amount)
{
Serializer s;
amount.add(s);
return s.getData();
}
} // namespace
// The only file exercising `parseST<STAmount>`. A malformed buffer throws inside `STAmount`'s
// deserializing constructor; `parseST` catches that itself, so the host is never asked - unlike
// a `guarded`-caught throw from the host's own body.
struct FloatFromSTAmountCall : HostContextTest
{
STAmount const amount{XRPAmount{1000}};
Bytes const wireBytes = serialized(amount);
std::int32_t const mode = 1;
};
TEST_F(FloatFromSTAmountCall, SerializedAmountDecodesToValueHostIsAskedFor)
{
Bytes const result{1, 2, 3};
EXPECT_CALL(host, floatFromSTAmount(testing::Eq(amount), mode))
.WillOnce(testing::Return(result));
OutRegion out{32};
EXPECT_EQ(
hostContext.floatFromSTAmount(bytesOf(wireBytes), mode, out.slice()),
static_cast<std::int32_t>(result.size()));
EXPECT_TRUE(out.holds(bytesOf(result)));
}
TEST_F(FloatFromSTAmountCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, floatFromSTAmount(testing::Eq(amount), mode))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::FloatComputationError)));
OutRegion out{32};
EXPECT_EQ(
hostContext.floatFromSTAmount(bytesOf(wireBytes), mode, out.slice()),
hfErrorToInt(HostFunctionError::FloatComputationError));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(FloatFromSTAmountCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, floatFromSTAmount(testing::Eq(amount), mode))
.WillOnce(testing::Throw(std::runtime_error{"float from st amount came apart"}));
OutRegion out{32};
EXPECT_EQ(
hostContext.floatFromSTAmount(bytesOf(wireBytes), mode, out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("float from st amount came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("floatFromSTAmount"));
}
// `parseST` catches its own failure: a malformed buffer never reaches the host at all.
TEST_F(FloatFromSTAmountCall, MalformedBytesAreRefusedWithoutAskingHost)
{
Bytes const malformedBytes{0xff, 0xff, 0xff};
EXPECT_CALL(host, floatFromSTAmount).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.floatFromSTAmount(bytesOf(malformedBytes), mode, out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(FloatFromSTAmountCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
Bytes const result{1, 2, 3};
EXPECT_CALL(host, floatFromSTAmount(testing::Eq(amount), mode))
.WillOnce(testing::Return(result));
OutRegion out{result.size() - 1};
EXPECT_EQ(
hostContext.floatFromSTAmount(bytesOf(wireBytes), mode, out.slice()),
static_cast<std::int32_t>(result.size()));
EXPECT_FALSE(out.wasWritten());
}
} // namespace xrpl::test

View File

@@ -0,0 +1,110 @@
#include <xrpl/basics/Number.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STNumber.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
namespace {
// The wire form `STNumber(SerialIter&, SField const&)` expects: an eight-byte mantissa
// followed by a four-byte exponent. Built directly rather than through `STNumber::add`, which
// asserts its field is bound to `STI_NUMBER` - an assertion `sfGeneric` does not satisfy.
Bytes
serialized(std::int64_t mantissa, std::int32_t exponent)
{
Serializer s;
s.add64(mantissa);
s.add32(exponent);
return s.getData();
}
} // namespace
// The only file exercising `parseST<STNumber>`. A malformed buffer throws inside `STNumber`'s
// deserializing constructor; `parseST` catches that itself, so the host is never asked - unlike
// a `guarded`-caught throw from the host's own body.
struct FloatFromSTNumberCall : HostContextTest
{
std::int64_t const mantissa = 123456789;
std::int32_t const exponent = -5;
STNumber const number{sfGeneric, Number{mantissa, exponent}};
Bytes const wireBytes = serialized(mantissa, exponent);
std::int32_t const mode = 1;
};
TEST_F(FloatFromSTNumberCall, SerializedNumberDecodesToValueHostIsAskedFor)
{
Bytes const result{1, 2, 3};
EXPECT_CALL(host, floatFromSTNumber(testing::Eq(number), mode))
.WillOnce(testing::Return(result));
OutRegion out{32};
EXPECT_EQ(
hostContext.floatFromSTNumber(bytesOf(wireBytes), mode, out.slice()),
static_cast<std::int32_t>(result.size()));
EXPECT_TRUE(out.holds(bytesOf(result)));
}
TEST_F(FloatFromSTNumberCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, floatFromSTNumber(testing::Eq(number), mode))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::FloatComputationError)));
OutRegion out{32};
EXPECT_EQ(
hostContext.floatFromSTNumber(bytesOf(wireBytes), mode, out.slice()),
hfErrorToInt(HostFunctionError::FloatComputationError));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(FloatFromSTNumberCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, floatFromSTNumber(testing::Eq(number), mode))
.WillOnce(testing::Throw(std::runtime_error{"float from st number came apart"}));
OutRegion out{32};
EXPECT_EQ(
hostContext.floatFromSTNumber(bytesOf(wireBytes), mode, out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("float from st number came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("floatFromSTNumber"));
}
// `parseST` catches its own failure: a malformed buffer never reaches the host at all.
TEST_F(FloatFromSTNumberCall, MalformedBytesAreRefusedWithoutAskingHost)
{
Bytes const malformedBytes{0xff, 0xff, 0xff};
EXPECT_CALL(host, floatFromSTNumber).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.floatFromSTNumber(bytesOf(malformedBytes), mode, out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(FloatFromSTNumberCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
Bytes const result{1, 2, 3};
EXPECT_CALL(host, floatFromSTNumber(testing::Eq(number), mode))
.WillOnce(testing::Return(result));
OutRegion out{result.size() - 1};
EXPECT_EQ(
hostContext.floatFromSTNumber(bytesOf(wireBytes), mode, out.slice()),
static_cast<std::int32_t>(result.size()));
EXPECT_FALSE(out.wasWritten());
}
} // namespace xrpl::test

View File

@@ -0,0 +1,130 @@
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// The only file exercising `parseUint64`: exactly eight bytes, little-endian.
struct FloatFromUintCall : HostContextTest
{
// Every byte distinct, so a byte-order mistake in `parseUint64` would decode to a
// different value rather than the same one by coincidence.
std::uint64_t const value = 0x0102'0304'0506'0708ULL;
Bytes const wireBytes = bytesOfScalar(value);
std::int32_t const mode = 1;
};
TEST_F(FloatFromUintCall, LittleEndianWireBytesDecodeToValueHostIsAskedFor)
{
Bytes const result{1, 2, 3};
EXPECT_CALL(host, floatFromUint(value, mode)).WillOnce(testing::Return(result));
OutRegion out{32};
EXPECT_EQ(
hostContext.floatFromUint(bytesOf(wireBytes), mode, out.slice()),
static_cast<std::int32_t>(result.size()));
EXPECT_TRUE(out.holds(bytesOf(result)));
}
TEST_F(FloatFromUintCall, ModeIsForwardedVerbatim)
{
std::int32_t const nonsenseMode = -12345;
Bytes const result{1};
EXPECT_CALL(host, floatFromUint(value, nonsenseMode)).WillOnce(testing::Return(result));
OutRegion out{32};
EXPECT_EQ(
hostContext.floatFromUint(bytesOf(wireBytes), nonsenseMode, out.slice()),
static_cast<std::int32_t>(result.size()));
}
TEST_F(FloatFromUintCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, floatFromUint(value, mode))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::FloatInputMalformed)));
OutRegion out{32};
EXPECT_EQ(
hostContext.floatFromUint(bytesOf(wireBytes), mode, out.slice()),
hfErrorToInt(HostFunctionError::FloatInputMalformed));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(FloatFromUintCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, floatFromUint(value, mode))
.WillOnce(testing::Throw(std::runtime_error{"uint came apart"}));
OutRegion out{32};
EXPECT_EQ(
hostContext.floatFromUint(bytesOf(wireBytes), mode, out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("uint came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("floatFromUint"));
}
TEST_F(FloatFromUintCall, SevenByteRegionIsRefusedWithoutAskingHost)
{
Bytes const shortBytes(7, 0);
EXPECT_CALL(host, floatFromUint).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.floatFromUint(bytesOf(shortBytes), mode, out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(FloatFromUintCall, NineByteRegionIsRefusedWithoutAskingHost)
{
Bytes const longBytes(9, 0);
EXPECT_CALL(host, floatFromUint).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.floatFromUint(bytesOf(longBytes), mode, out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(FloatFromUintCall, EmptyRegionIsRefusedWithoutAskingHost)
{
EXPECT_CALL(host, floatFromUint).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.floatFromUint(bytesOf(Bytes{}), mode, out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(FloatFromUintCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
Bytes const result{1, 2, 3};
EXPECT_CALL(host, floatFromUint(value, mode)).WillOnce(testing::Return(result));
OutRegion out{result.size() - 1};
EXPECT_EQ(
hostContext.floatFromUint(bytesOf(wireBytes), mode, out.slice()),
static_cast<std::int32_t>(result.size()));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(FloatFromUintCall, OutRegionOfExactSizeIsWritten)
{
Bytes const result{1, 2, 3};
EXPECT_CALL(host, floatFromUint(value, mode)).WillOnce(testing::Return(result));
OutRegion out{result.size()};
EXPECT_EQ(
hostContext.floatFromUint(bytesOf(wireBytes), mode, out.slice()),
static_cast<std::int32_t>(result.size()));
EXPECT_TRUE(out.holds(bytesOf(result)));
}
} // namespace xrpl::test

View File

@@ -0,0 +1,89 @@
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <tx/wasm/MockHostFunctions.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// Every input slice passes straight through to the host, unlike `invokeWithAccount`'s
// twenty-byte check or `parseUint64`'s eight: nothing here is validated, so there is no D
// axis. `x` and `y` carry different content, so a call that swapped them would fail to match.
struct FloatMultiplyCall : HostContextTest
{
Bytes const x{'m', 'u', 'l', '-', 'x'};
Bytes const y{'m', 'u', 'l', '-', 'y', 'y'};
std::int32_t const mode = 21;
};
TEST_F(FloatMultiplyCall, OperandsAndModeAreForwardedResultIsWritten)
{
Bytes const result{9, 8, 7};
EXPECT_CALL(host, floatMultiply(BytesAre("mul-x"), BytesAre("mul-yy"), mode))
.WillOnce(testing::Return(result));
OutRegion out{32};
EXPECT_EQ(
hostContext.floatMultiply(bytesOf(x), bytesOf(y), mode, out.slice()),
static_cast<std::int32_t>(result.size()));
EXPECT_TRUE(out.holds(bytesOf(result)));
}
TEST_F(FloatMultiplyCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, floatMultiply(BytesAre("mul-x"), BytesAre("mul-yy"), mode))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::FloatComputationError)));
OutRegion out{32};
EXPECT_EQ(
hostContext.floatMultiply(bytesOf(x), bytesOf(y), mode, out.slice()),
hfErrorToInt(HostFunctionError::FloatComputationError));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(FloatMultiplyCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, floatMultiply(BytesAre("mul-x"), BytesAre("mul-yy"), mode))
.WillOnce(testing::Throw(std::runtime_error{"float multiply came apart"}));
OutRegion out{32};
EXPECT_EQ(
hostContext.floatMultiply(bytesOf(x), bytesOf(y), mode, out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("float multiply came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("floatMultiply"));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(FloatMultiplyCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
Bytes const result{9, 8, 7};
EXPECT_CALL(host, floatMultiply(BytesAre("mul-x"), BytesAre("mul-yy"), mode))
.WillOnce(testing::Return(result));
OutRegion out{result.size() - 1};
EXPECT_EQ(
hostContext.floatMultiply(bytesOf(x), bytesOf(y), mode, out.slice()),
static_cast<std::int32_t>(result.size()));
EXPECT_FALSE(out.wasWritten());
}
// No length rule exists at this layer: a differently sized operand still reaches the host
// rather than being refused.
TEST_F(FloatMultiplyCall, OddSizedOperandReachesHostUnchanged)
{
Bytes const shortX{0x2a};
EXPECT_CALL(host, floatMultiply(testing::_, BytesAre("mul-yy"), mode))
.WillOnce(testing::Return(Bytes{1}));
OutRegion out{32};
EXPECT_EQ(hostContext.floatMultiply(bytesOf(shortX), bytesOf(y), mode, out.slice()), 1);
}
} // namespace xrpl::test

View File

@@ -0,0 +1,103 @@
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <tx/wasm/MockHostFunctions.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// Every input slice passes straight through to the host, unlike `invokeWithAccount`'s
// twenty-byte check or `parseUint64`'s eight: nothing here is validated, so there is no D
// axis. `n` and `mode` carry different values, so a call that swapped them would fail to
// match.
struct FloatPowerCall : HostContextTest
{
Bytes const x{'p', 'o', 'w', '-', 'x'};
std::int32_t const n = 4;
std::int32_t const mode = 22;
};
TEST_F(FloatPowerCall, OperandNAndModeAreForwardedResultIsWritten)
{
Bytes const result{4, 5, 6};
EXPECT_CALL(host, floatPower(BytesAre("pow-x"), n, mode)).WillOnce(testing::Return(result));
OutRegion out{32};
EXPECT_EQ(
hostContext.floatPower(bytesOf(x), n, mode, out.slice()),
static_cast<std::int32_t>(result.size()));
EXPECT_TRUE(out.holds(bytesOf(result)));
}
TEST_F(FloatPowerCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, floatPower(BytesAre("pow-x"), n, mode))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::FloatComputationError)));
OutRegion out{32};
EXPECT_EQ(
hostContext.floatPower(bytesOf(x), n, mode, out.slice()),
hfErrorToInt(HostFunctionError::FloatComputationError));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(FloatPowerCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, floatPower(BytesAre("pow-x"), n, mode))
.WillOnce(testing::Throw(std::runtime_error{"float power came apart"}));
OutRegion out{32};
EXPECT_EQ(
hostContext.floatPower(bytesOf(x), n, mode, out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("float power came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("floatPower"));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(FloatPowerCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
Bytes const result{4, 5, 6};
EXPECT_CALL(host, floatPower(BytesAre("pow-x"), n, mode)).WillOnce(testing::Return(result));
OutRegion out{result.size() - 1};
EXPECT_EQ(
hostContext.floatPower(bytesOf(x), n, mode, out.slice()),
static_cast<std::int32_t>(result.size()));
EXPECT_FALSE(out.wasWritten());
}
// No length rule exists at this layer: a differently sized operand still reaches the host
// rather than being refused.
TEST_F(FloatPowerCall, OddSizedOperandReachesHostUnchanged)
{
Bytes const shortX{0x2a};
EXPECT_CALL(host, floatPower(testing::_, n, mode)).WillOnce(testing::Return(Bytes{1}));
OutRegion out{32};
EXPECT_EQ(hostContext.floatPower(bytesOf(shortX), n, mode, out.slice()), 1);
}
// `mode` and `n` validate nothing at this layer and cross verbatim, including values with no
// real meaning. Worth pinning once across the float family rather than in every file.
TEST_F(FloatPowerCall, ModeAndNAreForwardedVerbatim)
{
std::int32_t const nonsenseN = -999;
std::int32_t const nonsenseMode = 424242;
Bytes const result{1};
EXPECT_CALL(host, floatPower(BytesAre("pow-x"), nonsenseN, nonsenseMode))
.WillOnce(testing::Return(result));
OutRegion out{32};
EXPECT_EQ(
hostContext.floatPower(bytesOf(x), nonsenseN, nonsenseMode, out.slice()),
static_cast<std::int32_t>(result.size()));
}
} // namespace xrpl::test

View File

@@ -0,0 +1,87 @@
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <tx/wasm/MockHostFunctions.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// Every input slice passes straight through to the host, unlike `invokeWithAccount`'s
// twenty-byte check or `parseUint64`'s eight: nothing here is validated, so there is no D
// axis. `n` and `mode` carry different values, so a call that swapped them would fail to
// match.
struct FloatRootCall : HostContextTest
{
Bytes const x{'r', 'o', 'o', 't', '-', 'x'};
std::int32_t const n = 3;
std::int32_t const mode = 11;
};
TEST_F(FloatRootCall, OperandNAndModeAreForwardedResultIsWritten)
{
Bytes const result{4, 5, 6};
EXPECT_CALL(host, floatRoot(BytesAre("root-x"), n, mode)).WillOnce(testing::Return(result));
OutRegion out{32};
EXPECT_EQ(
hostContext.floatRoot(bytesOf(x), n, mode, out.slice()),
static_cast<std::int32_t>(result.size()));
EXPECT_TRUE(out.holds(bytesOf(result)));
}
TEST_F(FloatRootCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, floatRoot(BytesAre("root-x"), n, mode))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::FloatComputationError)));
OutRegion out{32};
EXPECT_EQ(
hostContext.floatRoot(bytesOf(x), n, mode, out.slice()),
hfErrorToInt(HostFunctionError::FloatComputationError));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(FloatRootCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, floatRoot(BytesAre("root-x"), n, mode))
.WillOnce(testing::Throw(std::runtime_error{"float root came apart"}));
OutRegion out{32};
EXPECT_EQ(
hostContext.floatRoot(bytesOf(x), n, mode, out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("float root came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("floatRoot"));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(FloatRootCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
Bytes const result{4, 5, 6};
EXPECT_CALL(host, floatRoot(BytesAre("root-x"), n, mode)).WillOnce(testing::Return(result));
OutRegion out{result.size() - 1};
EXPECT_EQ(
hostContext.floatRoot(bytesOf(x), n, mode, out.slice()),
static_cast<std::int32_t>(result.size()));
EXPECT_FALSE(out.wasWritten());
}
// No length rule exists at this layer: a differently sized operand still reaches the host
// rather than being refused.
TEST_F(FloatRootCall, OddSizedOperandReachesHostUnchanged)
{
Bytes const shortX{0x2a};
EXPECT_CALL(host, floatRoot(testing::_, n, mode)).WillOnce(testing::Return(Bytes{1}));
OutRegion out{32};
EXPECT_EQ(hostContext.floatRoot(bytesOf(shortX), n, mode, out.slice()), 1);
}
} // namespace xrpl::test

View File

@@ -0,0 +1,89 @@
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <tx/wasm/MockHostFunctions.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// Every input slice passes straight through to the host, unlike `invokeWithAccount`'s
// twenty-byte check or `parseUint64`'s eight: nothing here is validated, so there is no D
// axis. `x` and `y` carry different content, so a call that swapped them would fail to match.
struct FloatSubtractCall : HostContextTest
{
Bytes const x{'s', 'u', 'b', '-', 'x'};
Bytes const y{'s', 'u', 'b', '-', 'y', 'y'};
std::int32_t const mode = 13;
};
TEST_F(FloatSubtractCall, OperandsAndModeAreForwardedResultIsWritten)
{
Bytes const result{9, 8, 7};
EXPECT_CALL(host, floatSubtract(BytesAre("sub-x"), BytesAre("sub-yy"), mode))
.WillOnce(testing::Return(result));
OutRegion out{32};
EXPECT_EQ(
hostContext.floatSubtract(bytesOf(x), bytesOf(y), mode, out.slice()),
static_cast<std::int32_t>(result.size()));
EXPECT_TRUE(out.holds(bytesOf(result)));
}
TEST_F(FloatSubtractCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, floatSubtract(BytesAre("sub-x"), BytesAre("sub-yy"), mode))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::FloatComputationError)));
OutRegion out{32};
EXPECT_EQ(
hostContext.floatSubtract(bytesOf(x), bytesOf(y), mode, out.slice()),
hfErrorToInt(HostFunctionError::FloatComputationError));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(FloatSubtractCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, floatSubtract(BytesAre("sub-x"), BytesAre("sub-yy"), mode))
.WillOnce(testing::Throw(std::runtime_error{"float subtract came apart"}));
OutRegion out{32};
EXPECT_EQ(
hostContext.floatSubtract(bytesOf(x), bytesOf(y), mode, out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("float subtract came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("floatSubtract"));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(FloatSubtractCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
Bytes const result{9, 8, 7};
EXPECT_CALL(host, floatSubtract(BytesAre("sub-x"), BytesAre("sub-yy"), mode))
.WillOnce(testing::Return(result));
OutRegion out{result.size() - 1};
EXPECT_EQ(
hostContext.floatSubtract(bytesOf(x), bytesOf(y), mode, out.slice()),
static_cast<std::int32_t>(result.size()));
EXPECT_FALSE(out.wasWritten());
}
// No length rule exists at this layer: a differently sized operand still reaches the host
// rather than being refused.
TEST_F(FloatSubtractCall, OddSizedOperandReachesHostUnchanged)
{
Bytes const shortX{0x2a};
EXPECT_CALL(host, floatSubtract(testing::_, BytesAre("sub-yy"), mode))
.WillOnce(testing::Return(Bytes{1}));
OutRegion out{32};
EXPECT_EQ(hostContext.floatSubtract(bytesOf(shortX), bytesOf(y), mode, out.slice()), 1);
}
} // namespace xrpl::test

View File

@@ -0,0 +1,80 @@
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <tx/wasm/MockHostFunctions.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// The input slice passes straight through to the host, unlike `invokeWithAccount`'s twenty-byte
// check or `parseUint64`'s eight: nothing here is validated, so there is no D axis.
struct FloatToIntCall : HostContextTest
{
Bytes const x{'t', 'o', 'i', 'n', 't'};
std::int32_t const mode = 3;
};
TEST_F(FloatToIntCall, OperandAndModeAreForwardedResultWrittenAsLittleEndianBytes)
{
std::int64_t const value = -123456789;
EXPECT_CALL(host, floatToInt(BytesAre("toint"), mode)).WillOnce(testing::Return(value));
OutRegion out{32};
EXPECT_EQ(hostContext.floatToInt(bytesOf(x), mode, out.slice()), 8);
EXPECT_TRUE(out.holds(bytesOf(bytesOfScalar(value))));
}
TEST_F(FloatToIntCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, floatToInt(BytesAre("toint"), mode))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::FloatComputationError)));
OutRegion out{32};
EXPECT_EQ(
hostContext.floatToInt(bytesOf(x), mode, out.slice()),
hfErrorToInt(HostFunctionError::FloatComputationError));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(FloatToIntCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, floatToInt(BytesAre("toint"), mode))
.WillOnce(testing::Throw(std::runtime_error{"float to int came apart"}));
OutRegion out{32};
EXPECT_EQ(
hostContext.floatToInt(bytesOf(x), mode, out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("float to int came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("floatToInt"));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(FloatToIntCall, SevenByteOutRegionWritesNothingAndReturnsTrueLength)
{
std::int64_t const value = 42;
EXPECT_CALL(host, floatToInt(BytesAre("toint"), mode)).WillOnce(testing::Return(value));
OutRegion out{7};
EXPECT_EQ(hostContext.floatToInt(bytesOf(x), mode, out.slice()), 8);
EXPECT_FALSE(out.wasWritten());
}
// No length rule exists at this layer: a differently sized operand still reaches the host
// rather than being refused.
TEST_F(FloatToIntCall, OddSizedOperandReachesHostUnchanged)
{
Bytes const oddX{0x2a};
EXPECT_CALL(host, floatToInt(testing::_, mode)).WillOnce(testing::Return(std::int64_t{7}));
OutRegion out{32};
EXPECT_EQ(hostContext.floatToInt(bytesOf(oddX), mode, out.slice()), 8);
}
} // namespace xrpl::test

View File

@@ -0,0 +1,101 @@
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <tx/wasm/MockHostFunctions.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// The input slice passes straight through to the host, unlike `invokeWithAccount`'s twenty-byte
// check or `parseUint64`'s eight: nothing here is validated, so there is no D axis. The two out
// regions are each checked and written independently; the return is their summed true length.
struct FloatToMantExpCall : HostContextTest
{
Bytes const x{'m', 'a', 'n', 't', 'e', 'x', 'p'};
std::int64_t const mantissa = 0x0102'0304'0506'0708LL;
std::int32_t const exponent = -5;
FloatPair const pair{mantissa, exponent};
};
TEST_F(FloatToMantExpCall, OperandIsForwardedMantissaAndExponentWrittenAsLittleEndianBytes)
{
EXPECT_CALL(host, floatToMantExp(BytesAre("mantexp"))).WillOnce(testing::Return(pair));
OutRegion mantissaOut{8};
OutRegion exponentOut{4};
EXPECT_EQ(hostContext.floatToMantExp(bytesOf(x), mantissaOut.slice(), exponentOut.slice()), 12);
EXPECT_TRUE(mantissaOut.holds(bytesOf(bytesOfScalar(mantissa))));
EXPECT_TRUE(exponentOut.holds(bytesOf(bytesOfScalar(exponent))));
}
TEST_F(FloatToMantExpCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, floatToMantExp(BytesAre("mantexp")))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::FloatComputationError)));
OutRegion mantissaOut{8};
OutRegion exponentOut{4};
EXPECT_EQ(
hostContext.floatToMantExp(bytesOf(x), mantissaOut.slice(), exponentOut.slice()),
hfErrorToInt(HostFunctionError::FloatComputationError));
EXPECT_FALSE(mantissaOut.wasWritten());
EXPECT_FALSE(exponentOut.wasWritten());
}
TEST_F(FloatToMantExpCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, floatToMantExp(BytesAre("mantexp")))
.WillOnce(testing::Throw(std::runtime_error{"float to mant exp came apart"}));
OutRegion mantissaOut{8};
OutRegion exponentOut{4};
EXPECT_EQ(
hostContext.floatToMantExp(bytesOf(x), mantissaOut.slice(), exponentOut.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("float to mant exp came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("floatToMantExp"));
}
// Each region is checked independently: a short mantissa region does not stop the exponent
// from being written, and the sum still counts the mantissa's true length.
TEST_F(FloatToMantExpCall, ShortMantissaRegionWritesNothingThereSumStillCountsIt)
{
EXPECT_CALL(host, floatToMantExp(BytesAre("mantexp"))).WillOnce(testing::Return(pair));
OutRegion mantissaOut{7};
OutRegion exponentOut{4};
EXPECT_EQ(hostContext.floatToMantExp(bytesOf(x), mantissaOut.slice(), exponentOut.slice()), 12);
EXPECT_FALSE(mantissaOut.wasWritten());
EXPECT_TRUE(exponentOut.holds(bytesOf(bytesOfScalar(exponent))));
}
TEST_F(FloatToMantExpCall, ShortExponentRegionWritesNothingThereSumStillCountsIt)
{
EXPECT_CALL(host, floatToMantExp(BytesAre("mantexp"))).WillOnce(testing::Return(pair));
OutRegion mantissaOut{8};
OutRegion exponentOut{3};
EXPECT_EQ(hostContext.floatToMantExp(bytesOf(x), mantissaOut.slice(), exponentOut.slice()), 12);
EXPECT_TRUE(mantissaOut.holds(bytesOf(bytesOfScalar(mantissa))));
EXPECT_FALSE(exponentOut.wasWritten());
}
// No length rule exists at this layer: a differently sized operand still reaches the host
// rather than being refused.
TEST_F(FloatToMantExpCall, OddSizedOperandReachesHostUnchanged)
{
Bytes const oddX{0x2a};
EXPECT_CALL(host, floatToMantExp(testing::_)).WillOnce(testing::Return(pair));
OutRegion mantissaOut{8};
OutRegion exponentOut{4};
EXPECT_EQ(
hostContext.floatToMantExp(bytesOf(oddX), mantissaOut.slice(), exponentOut.slice()), 12);
}
} // namespace xrpl::test

View File

@@ -0,0 +1,108 @@
#include <xrpl/basics/base_uint.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <expected>
#include <stdexcept>
#include <string_view>
namespace xrpl::test {
// The whole point of this file: a 32-byte input tries as an amendment id first, and falls back
// to a name lookup - on those same bytes - only if that id lookup does not answer enabled.
struct IsAmendmentEnabledCall : HostContextTest
{
Bytes const idBytes = Bytes(uint256::size(), 0x11);
uint256 const id = uint256::fromVoid(idBytes.data());
};
TEST_F(IsAmendmentEnabledCall, ThirtyTwoByteEnabledIdAnswersOneWithoutNameLookup)
{
EXPECT_CALL(host, isAmendmentEnabled(testing::Matcher<uint256 const&>(testing::Eq(id))))
.WillOnce(testing::Return(1));
EXPECT_CALL(host, isAmendmentEnabled(testing::Matcher<std::string_view const&>(testing::_)))
.Times(0);
EXPECT_EQ(hostContext.isAmendmentEnabled(bytesOf(idBytes)), 1);
}
// The same 32 bytes, read first as an id and, once that is not an enabled one, as a name.
TEST_F(IsAmendmentEnabledCall, ThirtyTwoByteDisabledIdFallsThroughToNameLookupWithSameBytes)
{
std::string_view const nameFromBytes{
reinterpret_cast<char const*>(idBytes.data()), idBytes.size()};
EXPECT_CALL(host, isAmendmentEnabled(testing::Matcher<uint256 const&>(testing::Eq(id))))
.WillOnce(testing::Return(0));
EXPECT_CALL(
host,
isAmendmentEnabled(testing::Matcher<std::string_view const&>(testing::Eq(nameFromBytes))))
.WillOnce(testing::Return(1));
EXPECT_EQ(hostContext.isAmendmentEnabled(bytesOf(idBytes)), 1);
}
// An id lookup that errors is treated the same as one that says no: both fall through to the
// name lookup rather than surfacing the error.
TEST_F(IsAmendmentEnabledCall, ThirtyTwoByteIdLookupErrorFallsThroughToNameLookup)
{
EXPECT_CALL(host, isAmendmentEnabled(testing::Matcher<uint256 const&>(testing::Eq(id))))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::Unimplemented)));
EXPECT_CALL(host, isAmendmentEnabled(testing::Matcher<std::string_view const&>(testing::_)))
.WillOnce(testing::Return(1));
EXPECT_EQ(hostContext.isAmendmentEnabled(bytesOf(idBytes)), 1);
}
// Over 64 bytes cannot be a 32-byte id nor a name short enough to matter, so it is refused
// before either overload runs.
TEST_F(IsAmendmentEnabledCall, InputOverSixtyFourBytesIsRefusedWithoutAskingHost)
{
Bytes const tooLong(65, 0x22);
EXPECT_CALL(host, isAmendmentEnabled(testing::Matcher<uint256 const&>(testing::_))).Times(0);
EXPECT_CALL(host, isAmendmentEnabled(testing::Matcher<std::string_view const&>(testing::_)))
.Times(0);
EXPECT_EQ(
hostContext.isAmendmentEnabled(bytesOf(tooLong)),
hfErrorToInt(HostFunctionError::DataFieldTooLarge));
}
TEST_F(IsAmendmentEnabledCall, HostErrorBecomesContractReturnValue)
{
Bytes const name{'F', 'e', 'a', 't', 'u', 'r', 'e'};
EXPECT_CALL(host, isAmendmentEnabled(testing::Matcher<std::string_view const&>(testing::_)))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::FieldNotFound)));
EXPECT_EQ(
hostContext.isAmendmentEnabled(bytesOf(name)),
hfErrorToInt(HostFunctionError::FieldNotFound));
}
TEST_F(IsAmendmentEnabledCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
Bytes const name{'F', 'e', 'a', 't', 'u', 'r', 'e'};
EXPECT_CALL(host, isAmendmentEnabled(testing::Matcher<std::string_view const&>(testing::_)))
.WillOnce(testing::Throw(std::runtime_error{"amendment lookup came apart"}));
EXPECT_EQ(
hostContext.isAmendmentEnabled(bytesOf(name)),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("amendment lookup came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("isAmendmentEnabled"));
}
TEST_F(IsAmendmentEnabledCall, NameBytesForwardedVerbatimToNameLookup)
{
std::string_view const name{"MyAmendment"};
Bytes const nameBytes{name.begin(), name.end()};
EXPECT_CALL(
host, isAmendmentEnabled(testing::Matcher<std::string_view const&>(testing::Eq(name))))
.WillOnce(testing::Return(1));
EXPECT_EQ(hostContext.isAmendmentEnabled(bytesOf(nameBytes)), 1);
}
} // namespace xrpl::test

View File

@@ -0,0 +1,84 @@
#include <xrpl/protocol/SField.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// `getLedgerObjArrayLen` answers its count directly rather than through an out region: no axis
// E, no `OutRegion`, and the happy path asserts the returned count.
struct LedgerObjArrayLenCall : HostContextTest
{
std::int32_t fieldCode = sfBalance.getCode();
std::int32_t cacheIdx = 7;
};
TEST_F(LedgerObjArrayLenCall, FieldCodeBecomesSFieldHostIsAskedFor)
{
EXPECT_CALL(host, getLedgerObjArrayLen(cacheIdx, testing::Ref(sfBalance)))
.WillOnce(testing::Return(5));
EXPECT_EQ(hostContext.getLedgerObjArrayLen(cacheIdx, fieldCode), 5);
}
// `NoArray` is what a field that is not an array actually answers, so it stands in for axis B
// here rather than an arbitrary code.
TEST_F(LedgerObjArrayLenCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, getLedgerObjArrayLen(cacheIdx, testing::Ref(sfBalance)))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::NoArray)));
EXPECT_EQ(
hostContext.getLedgerObjArrayLen(cacheIdx, fieldCode),
hfErrorToInt(HostFunctionError::NoArray));
}
TEST_F(LedgerObjArrayLenCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, getLedgerObjArrayLen(cacheIdx, testing::Ref(sfBalance)))
.WillOnce(testing::Throw(std::runtime_error{"ledger obj array len came apart"}));
EXPECT_EQ(
hostContext.getLedgerObjArrayLen(cacheIdx, fieldCode),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("ledger obj array len came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("getLedgerObjArrayLen"));
}
TEST_F(LedgerObjArrayLenCall, UnknownFieldCodeIsRefusedWithoutAskingHost)
{
fieldCode = 0x7fff'0000; // a code nothing is registered under
EXPECT_CALL(host, getLedgerObjArrayLen).Times(0);
EXPECT_EQ(
hostContext.getLedgerObjArrayLen(cacheIdx, fieldCode),
hfErrorToInt(HostFunctionError::InvalidField));
}
// `cacheIdx` is forwarded verbatim, including the two values a guest is likeliest to send: 0
// (pick a free slot) and a negative one.
TEST_F(LedgerObjArrayLenCall, CacheIdxOfZeroIsForwardedVerbatim)
{
cacheIdx = 0;
EXPECT_CALL(host, getLedgerObjArrayLen(0, testing::Ref(sfBalance)))
.WillOnce(testing::Return(5));
EXPECT_EQ(hostContext.getLedgerObjArrayLen(cacheIdx, fieldCode), 5);
}
TEST_F(LedgerObjArrayLenCall, NegativeCacheIdxIsForwardedVerbatim)
{
cacheIdx = -7;
EXPECT_CALL(host, getLedgerObjArrayLen(-7, testing::Ref(sfBalance)))
.WillOnce(testing::Return(5));
EXPECT_EQ(hostContext.getLedgerObjArrayLen(cacheIdx, fieldCode), 5);
}
} // namespace xrpl::test

View File

@@ -0,0 +1,137 @@
#include <xrpl/protocol/SField.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// The engine's own rules - buffer-fit, the field cap, guest memory - are tested on the Rust
// side, not here. The cross-cutting cases over this shape already live in `TxField.cpp`.
struct LedgerObjFieldCall : HostContextTest
{
std::int32_t fieldCode = sfBalance.getCode();
std::int32_t cacheIdx = 7;
};
TEST_F(LedgerObjFieldCall, FieldCodeBecomesSFieldHostIsAskedFor)
{
Bytes const value{1, 2, 3};
EXPECT_CALL(host, getLedgerObjField(cacheIdx, testing::Ref(sfBalance)))
.WillOnce(testing::Return(value));
OutRegion out{32};
EXPECT_EQ(
hostContext.getLedgerObjField(cacheIdx, fieldCode, out.slice()),
static_cast<std::int32_t>(value.size()));
EXPECT_TRUE(out.holds(bytesOf(value)));
}
TEST_F(LedgerObjFieldCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, getLedgerObjField(cacheIdx, testing::Ref(sfBalance)))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::FieldNotFound)));
OutRegion out{32};
EXPECT_EQ(
hostContext.getLedgerObjField(cacheIdx, fieldCode, out.slice()),
hfErrorToInt(HostFunctionError::FieldNotFound));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(LedgerObjFieldCall, UnknownFieldCodeIsRefusedWithoutAskingHost)
{
fieldCode = 0x7fff'0000; // a code nothing is registered under
EXPECT_CALL(host, getLedgerObjField).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.getLedgerObjField(cacheIdx, fieldCode, out.slice()),
hfErrorToInt(HostFunctionError::InvalidField));
}
TEST_F(LedgerObjFieldCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, getLedgerObjField(cacheIdx, testing::Ref(sfBalance)))
.WillOnce(testing::Throw(std::runtime_error{"ledger obj field came apart"}));
OutRegion out{32};
EXPECT_EQ(
hostContext.getLedgerObjField(cacheIdx, fieldCode, out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("ledger obj field came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("getLedgerObjField"));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(LedgerObjFieldCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
Bytes const value{1, 2, 3};
EXPECT_CALL(host, getLedgerObjField(cacheIdx, testing::Ref(sfBalance)))
.WillOnce(testing::Return(value));
OutRegion out{value.size() - 1};
EXPECT_EQ(
hostContext.getLedgerObjField(cacheIdx, fieldCode, out.slice()),
static_cast<std::int32_t>(value.size()));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(LedgerObjFieldCall, OutRegionOfExactSizeIsWritten)
{
Bytes const value{1, 2, 3};
EXPECT_CALL(host, getLedgerObjField(cacheIdx, testing::Ref(sfBalance)))
.WillOnce(testing::Return(value));
OutRegion out{value.size()};
EXPECT_EQ(
hostContext.getLedgerObjField(cacheIdx, fieldCode, out.slice()),
static_cast<std::int32_t>(value.size()));
EXPECT_TRUE(out.holds(bytesOf(value)));
}
TEST_F(LedgerObjFieldCall, EmptyResultAnswersZeroAndWritesNothing)
{
EXPECT_CALL(host, getLedgerObjField(cacheIdx, testing::Ref(sfBalance)))
.WillOnce(testing::Return(Bytes{}));
OutRegion out{32};
EXPECT_EQ(hostContext.getLedgerObjField(cacheIdx, fieldCode, out.slice()), 0);
EXPECT_FALSE(out.wasWritten());
}
// `cacheIdx` is forwarded verbatim, including the two values a guest is likeliest to send: 0
// (pick a free slot) and a negative one.
TEST_F(LedgerObjFieldCall, CacheIdxOfZeroIsForwardedVerbatim)
{
cacheIdx = 0;
Bytes const value{1, 2, 3};
EXPECT_CALL(host, getLedgerObjField(0, testing::Ref(sfBalance)))
.WillOnce(testing::Return(value));
OutRegion out{32};
EXPECT_EQ(
hostContext.getLedgerObjField(cacheIdx, fieldCode, out.slice()),
static_cast<std::int32_t>(value.size()));
}
TEST_F(LedgerObjFieldCall, NegativeCacheIdxIsForwardedVerbatim)
{
cacheIdx = -7;
Bytes const value{1, 2, 3};
EXPECT_CALL(host, getLedgerObjField(-7, testing::Ref(sfBalance)))
.WillOnce(testing::Return(value));
OutRegion out{32};
EXPECT_EQ(
hostContext.getLedgerObjField(cacheIdx, fieldCode, out.slice()),
static_cast<std::int32_t>(value.size()));
}
} // namespace xrpl::test

View File

@@ -0,0 +1,97 @@
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
#include <vector>
namespace xrpl::test {
// The engine's own rules - buffer-fit, the field cap, guest memory - are tested on the Rust
// side, not here.
//
// No out region and no axis E: `getLedgerObjNestedArrayLen` answers the array's element
// count directly rather than through a written buffer.
struct LedgerObjNestedArrayLenCall : HostContextTest
{
std::int32_t const cacheIdx = 7;
std::vector<std::int32_t> const steps{5, -12, 130};
Bytes const locatorBytes = bytesOfSteps(steps);
};
TEST_F(LedgerObjNestedArrayLenCall, LocatorBytesBecomeFieldLocatorHostReturnsCount)
{
EXPECT_CALL(host, getLedgerObjNestedArrayLen(cacheIdx, LocatorEquals(steps)))
.WillOnce(testing::Return(7));
EXPECT_EQ(hostContext.getLedgerObjNestedArrayLen(cacheIdx, bytesOf(locatorBytes)), 7);
}
// `NoArray` - the field the locator resolves to is not an array - is the error this shape
// most plausibly returns, so it stands in for axis B.
TEST_F(LedgerObjNestedArrayLenCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, getLedgerObjNestedArrayLen(cacheIdx, LocatorEquals(steps)))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::NoArray)));
EXPECT_EQ(
hostContext.getLedgerObjNestedArrayLen(cacheIdx, bytesOf(locatorBytes)),
hfErrorToInt(HostFunctionError::NoArray));
}
TEST_F(LedgerObjNestedArrayLenCall, EmptyLocatorIsRefusedWithoutAskingHost)
{
EXPECT_CALL(host, getLedgerObjNestedArrayLen).Times(0);
EXPECT_EQ(
hostContext.getLedgerObjNestedArrayLen(cacheIdx, bytesOf(Bytes{})),
hfErrorToInt(HostFunctionError::LocatorMalformed));
}
// Distinct from an empty locator: `invokeWithLocator` checks the two conditions separately.
TEST_F(LedgerObjNestedArrayLenCall, MisalignedLocatorLengthIsRefusedWithoutAskingHost)
{
Bytes const oddLength{1, 2, 3};
EXPECT_CALL(host, getLedgerObjNestedArrayLen).Times(0);
EXPECT_EQ(
hostContext.getLedgerObjNestedArrayLen(cacheIdx, bytesOf(oddLength)),
hfErrorToInt(HostFunctionError::LocatorMalformed));
}
TEST_F(LedgerObjNestedArrayLenCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, getLedgerObjNestedArrayLen(cacheIdx, LocatorEquals(steps)))
.WillOnce(testing::Throw(std::runtime_error{"ledger obj nested array len came apart"}));
EXPECT_EQ(
hostContext.getLedgerObjNestedArrayLen(cacheIdx, bytesOf(locatorBytes)),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("ledger obj nested array len came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("getLedgerObjNestedArrayLen"));
}
// `cacheIdx` crosses to the host as its own `std::int32_t`, unlike a keylet method's `seq`:
// no cast to an unsigned bit pattern, so 0 and a negative slot both cross unchanged.
TEST_F(LedgerObjNestedArrayLenCall, ZeroCacheIdxArrivesAtHostUnchanged)
{
EXPECT_CALL(host, getLedgerObjNestedArrayLen(0, LocatorEquals(steps)))
.WillOnce(testing::Return(7));
EXPECT_EQ(hostContext.getLedgerObjNestedArrayLen(0, bytesOf(locatorBytes)), 7);
}
TEST_F(LedgerObjNestedArrayLenCall, NegativeCacheIdxArrivesAtHostUnchanged)
{
std::int32_t const negativeCacheIdx = -3;
EXPECT_CALL(host, getLedgerObjNestedArrayLen(negativeCacheIdx, LocatorEquals(steps)))
.WillOnce(testing::Return(7));
EXPECT_EQ(hostContext.getLedgerObjNestedArrayLen(negativeCacheIdx, bytesOf(locatorBytes)), 7);
}
} // namespace xrpl::test

View File

@@ -0,0 +1,148 @@
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
#include <vector>
namespace xrpl::test {
// The engine's own rules - buffer-fit, the field cap, guest memory - are tested on the Rust
// side, not here.
struct LedgerObjNestedFieldCall : HostContextTest
{
std::int32_t const cacheIdx = 7;
std::vector<std::int32_t> const steps{5, -12, 130};
Bytes const locatorBytes = bytesOfSteps(steps);
};
TEST_F(LedgerObjNestedFieldCall, LocatorBytesBecomeFieldLocatorHostIsAskedFor)
{
Bytes const value{1, 2, 3};
EXPECT_CALL(host, getLedgerObjNestedField(cacheIdx, LocatorEquals(steps)))
.WillOnce(testing::Return(value));
OutRegion out{32};
EXPECT_EQ(
hostContext.getLedgerObjNestedField(cacheIdx, bytesOf(locatorBytes), out.slice()),
static_cast<std::int32_t>(value.size()));
EXPECT_TRUE(out.holds(bytesOf(value)));
}
TEST_F(LedgerObjNestedFieldCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, getLedgerObjNestedField(cacheIdx, LocatorEquals(steps)))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::NotLeafField)));
OutRegion out{32};
EXPECT_EQ(
hostContext.getLedgerObjNestedField(cacheIdx, bytesOf(locatorBytes), out.slice()),
hfErrorToInt(HostFunctionError::NotLeafField));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(LedgerObjNestedFieldCall, EmptyLocatorIsRefusedWithoutAskingHost)
{
EXPECT_CALL(host, getLedgerObjNestedField).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.getLedgerObjNestedField(cacheIdx, bytesOf(Bytes{}), out.slice()),
hfErrorToInt(HostFunctionError::LocatorMalformed));
}
// Distinct from an empty locator: `invokeWithLocator` checks the two conditions separately.
TEST_F(LedgerObjNestedFieldCall, MisalignedLocatorLengthIsRefusedWithoutAskingHost)
{
Bytes const oddLength{1, 2, 3};
EXPECT_CALL(host, getLedgerObjNestedField).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.getLedgerObjNestedField(cacheIdx, bytesOf(oddLength), out.slice()),
hfErrorToInt(HostFunctionError::LocatorMalformed));
}
TEST_F(LedgerObjNestedFieldCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, getLedgerObjNestedField(cacheIdx, LocatorEquals(steps)))
.WillOnce(testing::Throw(std::runtime_error{"ledger obj nested field came apart"}));
OutRegion out{32};
EXPECT_EQ(
hostContext.getLedgerObjNestedField(cacheIdx, bytesOf(locatorBytes), out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("ledger obj nested field came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("getLedgerObjNestedField"));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(LedgerObjNestedFieldCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
Bytes const value{1, 2, 3};
EXPECT_CALL(host, getLedgerObjNestedField(cacheIdx, LocatorEquals(steps)))
.WillOnce(testing::Return(value));
OutRegion out{value.size() - 1};
EXPECT_EQ(
hostContext.getLedgerObjNestedField(cacheIdx, bytesOf(locatorBytes), out.slice()),
static_cast<std::int32_t>(value.size()));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(LedgerObjNestedFieldCall, OutRegionOfExactSizeIsWritten)
{
Bytes const value{1, 2, 3};
EXPECT_CALL(host, getLedgerObjNestedField(cacheIdx, LocatorEquals(steps)))
.WillOnce(testing::Return(value));
OutRegion out{value.size()};
EXPECT_EQ(
hostContext.getLedgerObjNestedField(cacheIdx, bytesOf(locatorBytes), out.slice()),
static_cast<std::int32_t>(value.size()));
EXPECT_TRUE(out.holds(bytesOf(value)));
}
TEST_F(LedgerObjNestedFieldCall, EmptyResultAnswersZeroAndWritesNothing)
{
EXPECT_CALL(host, getLedgerObjNestedField(cacheIdx, LocatorEquals(steps)))
.WillOnce(testing::Return(Bytes{}));
OutRegion out{32};
EXPECT_EQ(hostContext.getLedgerObjNestedField(cacheIdx, bytesOf(locatorBytes), out.slice()), 0);
EXPECT_FALSE(out.wasWritten());
}
// `cacheIdx` crosses to the host as its own `std::int32_t`, unlike a keylet method's `seq`:
// no cast to an unsigned bit pattern, so 0 and a negative slot both cross unchanged.
TEST_F(LedgerObjNestedFieldCall, ZeroCacheIdxArrivesAtHostUnchanged)
{
Bytes const value{1, 2, 3};
EXPECT_CALL(host, getLedgerObjNestedField(0, LocatorEquals(steps)))
.WillOnce(testing::Return(value));
OutRegion out{32};
EXPECT_EQ(
hostContext.getLedgerObjNestedField(0, bytesOf(locatorBytes), out.slice()),
static_cast<std::int32_t>(value.size()));
}
TEST_F(LedgerObjNestedFieldCall, NegativeCacheIdxArrivesAtHostUnchanged)
{
std::int32_t const negativeCacheIdx = -3;
Bytes const value{1, 2, 3};
EXPECT_CALL(host, getLedgerObjNestedField(negativeCacheIdx, LocatorEquals(steps)))
.WillOnce(testing::Return(value));
OutRegion out{32};
EXPECT_EQ(
hostContext.getLedgerObjNestedField(negativeCacheIdx, bytesOf(locatorBytes), out.slice()),
static_cast<std::int32_t>(value.size()));
}
} // namespace xrpl::test

View File

@@ -0,0 +1,76 @@
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// No D or F axis: `getLedgerSqn` takes no argument, so there is nothing to decode wrong and
// nothing whose forwarded identity to check.
//
// Named `LedgerSqnDirectCall`, not `LedgerSqnCall`: `host_calls/LedgerSqn.cpp` already owns
// that name in the same gtest binary.
struct LedgerSqnDirectCall : HostContextTest
{
static constexpr std::uint32_t kLedgerSqn = 0x12345678;
Bytes const expectedBytes = bytesOfScalar(kLedgerSqn);
};
TEST_F(LedgerSqnDirectCall, HostValueIsWrittenAsLittleEndianBytes)
{
EXPECT_CALL(host, getLedgerSqn()).WillOnce(testing::Return(kLedgerSqn));
OutRegion out{32};
EXPECT_EQ(hostContext.getLedgerSqn(out.slice()), 4);
EXPECT_TRUE(out.holds(bytesOf(expectedBytes)));
}
TEST_F(LedgerSqnDirectCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, getLedgerSqn())
.WillOnce(testing::Return(std::unexpected(HostFunctionError::Unimplemented)));
OutRegion out{4};
EXPECT_EQ(
hostContext.getLedgerSqn(out.slice()), hfErrorToInt(HostFunctionError::Unimplemented));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(LedgerSqnDirectCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, getLedgerSqn())
.WillOnce(testing::Throw(std::runtime_error{"ledger sqn came apart"}));
OutRegion out{4};
EXPECT_EQ(
hostContext.getLedgerSqn(out.slice()), hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("ledger sqn came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("getLedgerSqn"));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(LedgerSqnDirectCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
EXPECT_CALL(host, getLedgerSqn()).WillOnce(testing::Return(kLedgerSqn));
OutRegion out{3};
EXPECT_EQ(hostContext.getLedgerSqn(out.slice()), 4);
EXPECT_FALSE(out.wasWritten());
}
TEST_F(LedgerSqnDirectCall, OutRegionOfExactSizeIsWritten)
{
EXPECT_CALL(host, getLedgerSqn()).WillOnce(testing::Return(kLedgerSqn));
OutRegion out{4};
EXPECT_EQ(hostContext.getLedgerSqn(out.slice()), 4);
EXPECT_TRUE(out.holds(bytesOf(expectedBytes)));
}
} // namespace xrpl::test

View File

@@ -0,0 +1,131 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
struct MptokenIssuanceKeyletCall : HostContextTest
{
Bytes const issuerBytes{0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a,
0x5b, 0x5c, 0x5d, 0x5e, 0x5f, 0x60, 0x61, 0x62, 0x63, 0x64};
AccountID const issuer = AccountID::fromVoid(issuerBytes.data());
std::int32_t const seq = 98765;
};
TEST_F(MptokenIssuanceKeyletCall, IssuerAndSeqAreForwardedKeyletIsWritten)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, mptokenIssuanceKeylet(issuer, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(keylet));
OutRegion out{32};
EXPECT_EQ(
hostContext.mptokenIssuanceKeylet(bytesOf(issuerBytes), seq, out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_TRUE(out.holds(bytesOf(keylet)));
}
TEST_F(MptokenIssuanceKeyletCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, mptokenIssuanceKeylet(issuer, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
OutRegion out{32};
EXPECT_EQ(
hostContext.mptokenIssuanceKeylet(bytesOf(issuerBytes), seq, out.slice()),
hfErrorToInt(HostFunctionError::LedgerObjNotFound));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(MptokenIssuanceKeyletCall, ShortIssuerIsRefusedWithoutAskingHost)
{
Bytes const shortIssuer(AccountID::size() - 1, 0x01);
EXPECT_CALL(host, mptokenIssuanceKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.mptokenIssuanceKeylet(bytesOf(shortIssuer), seq, out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(MptokenIssuanceKeyletCall, LongIssuerIsRefusedWithoutAskingHost)
{
Bytes const longIssuer(AccountID::size() + 1, 0x01);
EXPECT_CALL(host, mptokenIssuanceKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.mptokenIssuanceKeylet(bytesOf(longIssuer), seq, out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(MptokenIssuanceKeyletCall, EmptyIssuerIsRefusedWithoutAskingHost)
{
EXPECT_CALL(host, mptokenIssuanceKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.mptokenIssuanceKeylet(bytesOf(Bytes{}), seq, out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(MptokenIssuanceKeyletCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, mptokenIssuanceKeylet(issuer, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Throw(std::runtime_error{"mptoken issuance keylet came apart"}));
OutRegion out{32};
EXPECT_EQ(
hostContext.mptokenIssuanceKeylet(bytesOf(issuerBytes), seq, out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("mptoken issuance keylet came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("mptokenIssuanceKeylet"));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(MptokenIssuanceKeyletCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, mptokenIssuanceKeylet(issuer, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(keylet));
OutRegion out{keylet.size() - 1};
EXPECT_EQ(
hostContext.mptokenIssuanceKeylet(bytesOf(issuerBytes), seq, out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(MptokenIssuanceKeyletCall, OutRegionOfExactSizeIsWritten)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, mptokenIssuanceKeylet(issuer, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(keylet));
OutRegion out{keylet.size()};
EXPECT_EQ(
hostContext.mptokenIssuanceKeylet(bytesOf(issuerBytes), seq, out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_TRUE(out.holds(bytesOf(keylet)));
}
TEST_F(MptokenIssuanceKeyletCall, EmptyResultAnswersZeroAndWritesNothing)
{
EXPECT_CALL(host, mptokenIssuanceKeylet(issuer, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(Bytes{}));
OutRegion out{32};
EXPECT_EQ(hostContext.mptokenIssuanceKeylet(bytesOf(issuerBytes), seq, out.slice()), 0);
EXPECT_FALSE(out.wasWritten());
}
} // namespace xrpl::test

View File

@@ -0,0 +1,119 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
// `mptid` and `holder` are checked together in one condition rather than through
// `invokeWithAccount`, so which one fired is not observable when both are malformed.
struct MptokenKeyletCall : HostContextTest
{
Bytes const mptidBytes = Bytes(24, 0x7a);
Bytes const holderBytes{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14};
MPTID const mptid = MPTID::fromVoid(mptidBytes.data());
AccountID const holder = AccountID::fromVoid(holderBytes.data());
Bytes const keylet = Bytes(32, 0xab);
};
TEST_F(MptokenKeyletCall, MptidAndHolderForwardedAndKeyletWritten)
{
EXPECT_CALL(host, mptokenKeylet(testing::Eq(mptid), testing::Eq(holder)))
.WillOnce(testing::Return(keylet));
OutRegion out{32};
EXPECT_EQ(
hostContext.mptokenKeylet(bytesOf(mptidBytes), bytesOf(holderBytes), out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_TRUE(out.holds(bytesOf(keylet)));
}
TEST_F(MptokenKeyletCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, mptokenKeylet(testing::Eq(mptid), testing::Eq(holder)))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
OutRegion out{32};
EXPECT_EQ(
hostContext.mptokenKeylet(bytesOf(mptidBytes), bytesOf(holderBytes), out.slice()),
hfErrorToInt(HostFunctionError::LedgerObjNotFound));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(MptokenKeyletCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, mptokenKeylet(testing::Eq(mptid), testing::Eq(holder)))
.WillOnce(testing::Throw(std::runtime_error{"mptoken keylet came apart"}));
OutRegion out{32};
EXPECT_EQ(
hostContext.mptokenKeylet(bytesOf(mptidBytes), bytesOf(holderBytes), out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("mptoken keylet came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("mptokenKeylet"));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(MptokenKeyletCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
EXPECT_CALL(host, mptokenKeylet(testing::Eq(mptid), testing::Eq(holder)))
.WillOnce(testing::Return(keylet));
OutRegion out{keylet.size() - 1};
EXPECT_EQ(
hostContext.mptokenKeylet(bytesOf(mptidBytes), bytesOf(holderBytes), out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(MptokenKeyletCall, MalformedMptidIsRefusedWithoutAskingHost)
{
Bytes const malformedMptid(MPTID::size() - 1, 0x7a);
EXPECT_CALL(host, mptokenKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.mptokenKeylet(bytesOf(malformedMptid), bytesOf(holderBytes), out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
// Distinct from a malformed mptid: the mptid is well-formed here, so this exercises the
// holder's own check rather than the mptid's.
TEST_F(MptokenKeyletCall, MalformedHolderIsRefusedWithoutAskingHost)
{
Bytes const malformedHolder(AccountID::size() - 1, 0x01);
EXPECT_CALL(host, mptokenKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.mptokenKeylet(bytesOf(mptidBytes), bytesOf(malformedHolder), out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
// Both lengths are checked in one condition and both answer the same `InvalidParams`, so
// which one fired is not observable here. What is: neither argument reaches the host.
TEST_F(MptokenKeyletCall, BothArgumentsMalformedIsRefusedWithoutAskingHost)
{
Bytes const malformedMptid(MPTID::size() - 1, 0x7a);
Bytes const malformedHolder(AccountID::size() - 1, 0x01);
EXPECT_CALL(host, mptokenKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.mptokenKeylet(bytesOf(malformedMptid), bytesOf(malformedHolder), out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
} // namespace xrpl::test

View File

@@ -0,0 +1,142 @@
#include <xrpl/basics/base_uint.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// The engine's own rules - buffer-fit, the field cap, guest memory - are tested on the Rust
// side, not here.
struct NFTCall : HostContextTest
{
Bytes const accountBytes{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14};
Bytes const nftIdBytes{0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b,
0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36,
0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40};
AccountID const account = AccountID::fromVoid(accountBytes.data());
uint256 const nftId = uint256::fromVoid(nftIdBytes.data());
};
TEST_F(NFTCall, AccountAndNftIdBecomeTypedArgumentsHostIsAskedFor)
{
Bytes const value{1, 2, 3};
EXPECT_CALL(host, getNFT(testing::Eq(account), testing::Eq(nftId)))
.WillOnce(testing::Return(value));
OutRegion out{32};
EXPECT_EQ(
hostContext.getNFT(bytesOf(accountBytes), bytesOf(nftIdBytes), out.slice()),
static_cast<std::int32_t>(value.size()));
EXPECT_TRUE(out.holds(bytesOf(value)));
}
TEST_F(NFTCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, getNFT(testing::Eq(account), testing::Eq(nftId)))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
OutRegion out{32};
EXPECT_EQ(
hostContext.getNFT(bytesOf(accountBytes), bytesOf(nftIdBytes), out.slice()),
hfErrorToInt(HostFunctionError::LedgerObjNotFound));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(NFTCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, getNFT(testing::Eq(account), testing::Eq(nftId)))
.WillOnce(testing::Throw(std::runtime_error{"nft came apart"}));
OutRegion out{32};
EXPECT_EQ(
hostContext.getNFT(bytesOf(accountBytes), bytesOf(nftIdBytes), out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("nft came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("getNFT"));
}
TEST_F(NFTCall, MalformedAccountIsRefusedWithoutAskingHost)
{
Bytes const malformedAccount(AccountID::size() - 1, 0xff);
EXPECT_CALL(host, getNFT).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.getNFT(bytesOf(malformedAccount), bytesOf(nftIdBytes), out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
// Distinct from a malformed account: the account is well-formed here, so this exercises the
// nft id's own check rather than the account's.
TEST_F(NFTCall, MalformedNftIdIsRefusedWithoutAskingHost)
{
Bytes const malformedNftId(uint256::size() - 1, 0xff);
EXPECT_CALL(host, getNFT).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.getNFT(bytesOf(accountBytes), bytesOf(malformedNftId), out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
// The account's length is checked before the nft id's, but both checks answer `InvalidParams`,
// so which one fired is not observable here. What is: neither argument reaches the host.
TEST_F(NFTCall, BothArgumentsMalformedIsRefusedWithoutAskingHost)
{
Bytes const malformedAccount(AccountID::size() - 1, 0xff);
Bytes const malformedNftId(uint256::size() - 1, 0xff);
EXPECT_CALL(host, getNFT).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.getNFT(bytesOf(malformedAccount), bytesOf(malformedNftId), out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(NFTCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
Bytes const value{1, 2, 3};
EXPECT_CALL(host, getNFT(testing::Eq(account), testing::Eq(nftId)))
.WillOnce(testing::Return(value));
OutRegion out{value.size() - 1};
EXPECT_EQ(
hostContext.getNFT(bytesOf(accountBytes), bytesOf(nftIdBytes), out.slice()),
static_cast<std::int32_t>(value.size()));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(NFTCall, OutRegionOfExactSizeIsWritten)
{
Bytes const value{1, 2, 3};
EXPECT_CALL(host, getNFT(testing::Eq(account), testing::Eq(nftId)))
.WillOnce(testing::Return(value));
OutRegion out{value.size()};
EXPECT_EQ(
hostContext.getNFT(bytesOf(accountBytes), bytesOf(nftIdBytes), out.slice()),
static_cast<std::int32_t>(value.size()));
EXPECT_TRUE(out.holds(bytesOf(value)));
}
TEST_F(NFTCall, EmptyResultAnswersZeroAndWritesNothing)
{
EXPECT_CALL(host, getNFT(testing::Eq(account), testing::Eq(nftId)))
.WillOnce(testing::Return(Bytes{}));
OutRegion out{32};
EXPECT_EQ(hostContext.getNFT(bytesOf(accountBytes), bytesOf(nftIdBytes), out.slice()), 0);
EXPECT_FALSE(out.wasWritten());
}
} // namespace xrpl::test

View File

@@ -0,0 +1,81 @@
#include <xrpl/basics/base_uint.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <limits>
#include <stdexcept>
namespace xrpl::test {
// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
// `getNFTFlags` answers its value directly rather than through `answer`, so there is no out
// region and no axis E.
struct NFTFlagsCall : HostContextTest
{
Bytes const nftIdBytes{0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xbb,
0xbc, 0xbd, 0xbe, 0xbf, 0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6,
0xc7, 0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xce, 0xcf, 0xd0};
uint256 const nftId = uint256::fromVoid(nftIdBytes.data());
};
TEST_F(NFTFlagsCall, NftIdBytesBecomeTypedArgumentHostIsAskedFor)
{
static constexpr std::int32_t kFlags = 0x0b;
EXPECT_CALL(host, getNFTFlags(testing::Eq(nftId))).WillOnce(testing::Return(kFlags));
EXPECT_EQ(hostContext.getNFTFlags(bytesOf(nftIdBytes)), kFlags);
}
TEST_F(NFTFlagsCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, getNFTFlags(testing::Eq(nftId)))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
EXPECT_EQ(
hostContext.getNFTFlags(bytesOf(nftIdBytes)),
hfErrorToInt(HostFunctionError::LedgerObjNotFound));
}
TEST_F(NFTFlagsCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, getNFTFlags(testing::Eq(nftId)))
.WillOnce(testing::Throw(std::runtime_error{"nft flags came apart"}));
EXPECT_EQ(
hostContext.getNFTFlags(bytesOf(nftIdBytes)),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("nft flags came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("getNFTFlags"));
}
TEST_F(NFTFlagsCall, MalformedNftIdIsRefusedWithoutAskingHost)
{
Bytes const malformedNftId(uint256::size() - 1, 0xff);
EXPECT_CALL(host, getNFTFlags).Times(0);
EXPECT_EQ(
hostContext.getNFTFlags(bytesOf(malformedNftId)),
hfErrorToInt(HostFunctionError::InvalidParams));
}
// `getNFTFlags` answers its value directly rather than through `answer`, so a legitimate
// flags word with the high bit set is bit-for-bit the same value as
// `HostFunctionError::InternalFatal` (`INT32_MIN`) - the code `guarded` supplies for a thrown
// exception. The ABI at this layer has no way to tell the two apart; this is a property of
// the shape, not a bug to fix.
TEST_F(NFTFlagsCall, HighBitFlagsAreIndistinguishableFromInternalFatal)
{
EXPECT_CALL(host, getNFTFlags(testing::Eq(nftId)))
.WillOnce(testing::Return(std::numeric_limits<std::int32_t>::min()));
EXPECT_EQ(
hostContext.getNFTFlags(bytesOf(nftIdBytes)),
hfErrorToInt(HostFunctionError::InternalFatal));
}
} // namespace xrpl::test

View File

@@ -0,0 +1,106 @@
#include <xrpl/basics/base_uint.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
struct NFTIssuerCall : HostContextTest
{
Bytes const nftIdBytes{0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b,
0x5c, 0x5d, 0x5e, 0x5f, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66,
0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70};
uint256 const nftId = uint256::fromVoid(nftIdBytes.data());
};
TEST_F(NFTIssuerCall, NftIdBytesBecomeTypedArgumentHostIsAskedFor)
{
Bytes const value{1, 2, 3};
EXPECT_CALL(host, getNFTIssuer(testing::Eq(nftId))).WillOnce(testing::Return(value));
OutRegion out{32};
EXPECT_EQ(
hostContext.getNFTIssuer(bytesOf(nftIdBytes), out.slice()),
static_cast<std::int32_t>(value.size()));
EXPECT_TRUE(out.holds(bytesOf(value)));
}
TEST_F(NFTIssuerCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, getNFTIssuer(testing::Eq(nftId)))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
OutRegion out{32};
EXPECT_EQ(
hostContext.getNFTIssuer(bytesOf(nftIdBytes), out.slice()),
hfErrorToInt(HostFunctionError::LedgerObjNotFound));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(NFTIssuerCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, getNFTIssuer(testing::Eq(nftId)))
.WillOnce(testing::Throw(std::runtime_error{"nft issuer came apart"}));
OutRegion out{32};
EXPECT_EQ(
hostContext.getNFTIssuer(bytesOf(nftIdBytes), out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("nft issuer came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("getNFTIssuer"));
}
TEST_F(NFTIssuerCall, MalformedNftIdIsRefusedWithoutAskingHost)
{
Bytes const malformedNftId(uint256::size() - 1, 0xff);
EXPECT_CALL(host, getNFTIssuer).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.getNFTIssuer(bytesOf(malformedNftId), out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(NFTIssuerCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
Bytes const value{1, 2, 3};
EXPECT_CALL(host, getNFTIssuer(testing::Eq(nftId))).WillOnce(testing::Return(value));
OutRegion out{value.size() - 1};
EXPECT_EQ(
hostContext.getNFTIssuer(bytesOf(nftIdBytes), out.slice()),
static_cast<std::int32_t>(value.size()));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(NFTIssuerCall, OutRegionOfExactSizeIsWritten)
{
Bytes const value{1, 2, 3};
EXPECT_CALL(host, getNFTIssuer(testing::Eq(nftId))).WillOnce(testing::Return(value));
OutRegion out{value.size()};
EXPECT_EQ(
hostContext.getNFTIssuer(bytesOf(nftIdBytes), out.slice()),
static_cast<std::int32_t>(value.size()));
EXPECT_TRUE(out.holds(bytesOf(value)));
}
TEST_F(NFTIssuerCall, EmptyResultAnswersZeroAndWritesNothing)
{
EXPECT_CALL(host, getNFTIssuer(testing::Eq(nftId))).WillOnce(testing::Return(Bytes{}));
OutRegion out{32};
EXPECT_EQ(hostContext.getNFTIssuer(bytesOf(nftIdBytes), out.slice()), 0);
EXPECT_FALSE(out.wasWritten());
}
} // namespace xrpl::test

View File

@@ -0,0 +1,90 @@
#include <xrpl/basics/base_uint.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
struct NFTSequenceCall : HostContextTest
{
Bytes const nftIdBytes{0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0x9b,
0x9c, 0x9d, 0x9e, 0x9f, 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6,
0xa7, 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf, 0xb0};
uint256 const nftId = uint256::fromVoid(nftIdBytes.data());
static constexpr std::uint32_t kSequence = 0x89abcdef;
Bytes const expectedBytes = bytesOfScalar(kSequence);
};
TEST_F(NFTSequenceCall, NftIdBytesBecomeTypedArgumentHostIsAskedFor)
{
EXPECT_CALL(host, getNFTSequence(testing::Eq(nftId))).WillOnce(testing::Return(kSequence));
OutRegion out{32};
EXPECT_EQ(hostContext.getNFTSequence(bytesOf(nftIdBytes), out.slice()), 4);
EXPECT_TRUE(out.holds(bytesOf(expectedBytes)));
}
TEST_F(NFTSequenceCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, getNFTSequence(testing::Eq(nftId)))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
OutRegion out{4};
EXPECT_EQ(
hostContext.getNFTSequence(bytesOf(nftIdBytes), out.slice()),
hfErrorToInt(HostFunctionError::LedgerObjNotFound));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(NFTSequenceCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, getNFTSequence(testing::Eq(nftId)))
.WillOnce(testing::Throw(std::runtime_error{"nft sequence came apart"}));
OutRegion out{4};
EXPECT_EQ(
hostContext.getNFTSequence(bytesOf(nftIdBytes), out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("nft sequence came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("getNFTSequence"));
}
TEST_F(NFTSequenceCall, MalformedNftIdIsRefusedWithoutAskingHost)
{
Bytes const malformedNftId(uint256::size() - 1, 0xff);
EXPECT_CALL(host, getNFTSequence).Times(0);
OutRegion out{4};
EXPECT_EQ(
hostContext.getNFTSequence(bytesOf(malformedNftId), out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(NFTSequenceCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
EXPECT_CALL(host, getNFTSequence(testing::Eq(nftId))).WillOnce(testing::Return(kSequence));
OutRegion out{3};
EXPECT_EQ(hostContext.getNFTSequence(bytesOf(nftIdBytes), out.slice()), 4);
EXPECT_FALSE(out.wasWritten());
}
TEST_F(NFTSequenceCall, OutRegionOfExactSizeIsWritten)
{
EXPECT_CALL(host, getNFTSequence(testing::Eq(nftId))).WillOnce(testing::Return(kSequence));
OutRegion out{4};
EXPECT_EQ(hostContext.getNFTSequence(bytesOf(nftIdBytes), out.slice()), 4);
EXPECT_TRUE(out.holds(bytesOf(expectedBytes)));
}
} // namespace xrpl::test

View File

@@ -0,0 +1,90 @@
#include <xrpl/basics/base_uint.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
struct NFTTaxonCall : HostContextTest
{
Bytes const nftIdBytes{0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b,
0x7c, 0x7d, 0x7e, 0x7f, 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86,
0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f, 0x90};
uint256 const nftId = uint256::fromVoid(nftIdBytes.data());
static constexpr std::uint32_t kTaxon = 0x12345678;
Bytes const expectedBytes = bytesOfScalar(kTaxon);
};
TEST_F(NFTTaxonCall, NftIdBytesBecomeTypedArgumentHostIsAskedFor)
{
EXPECT_CALL(host, getNFTTaxon(testing::Eq(nftId))).WillOnce(testing::Return(kTaxon));
OutRegion out{32};
EXPECT_EQ(hostContext.getNFTTaxon(bytesOf(nftIdBytes), out.slice()), 4);
EXPECT_TRUE(out.holds(bytesOf(expectedBytes)));
}
TEST_F(NFTTaxonCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, getNFTTaxon(testing::Eq(nftId)))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
OutRegion out{4};
EXPECT_EQ(
hostContext.getNFTTaxon(bytesOf(nftIdBytes), out.slice()),
hfErrorToInt(HostFunctionError::LedgerObjNotFound));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(NFTTaxonCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, getNFTTaxon(testing::Eq(nftId)))
.WillOnce(testing::Throw(std::runtime_error{"nft taxon came apart"}));
OutRegion out{4};
EXPECT_EQ(
hostContext.getNFTTaxon(bytesOf(nftIdBytes), out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("nft taxon came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("getNFTTaxon"));
}
TEST_F(NFTTaxonCall, MalformedNftIdIsRefusedWithoutAskingHost)
{
Bytes const malformedNftId(uint256::size() - 1, 0xff);
EXPECT_CALL(host, getNFTTaxon).Times(0);
OutRegion out{4};
EXPECT_EQ(
hostContext.getNFTTaxon(bytesOf(malformedNftId), out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(NFTTaxonCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
EXPECT_CALL(host, getNFTTaxon(testing::Eq(nftId))).WillOnce(testing::Return(kTaxon));
OutRegion out{3};
EXPECT_EQ(hostContext.getNFTTaxon(bytesOf(nftIdBytes), out.slice()), 4);
EXPECT_FALSE(out.wasWritten());
}
TEST_F(NFTTaxonCall, OutRegionOfExactSizeIsWritten)
{
EXPECT_CALL(host, getNFTTaxon(testing::Eq(nftId))).WillOnce(testing::Return(kTaxon));
OutRegion out{4};
EXPECT_EQ(hostContext.getNFTTaxon(bytesOf(nftIdBytes), out.slice()), 4);
EXPECT_TRUE(out.holds(bytesOf(expectedBytes)));
}
} // namespace xrpl::test

View File

@@ -0,0 +1,66 @@
#include <xrpl/basics/base_uint.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
// `getNFTTransferFee` answers its value directly rather than through `answer`, so there is no
// out region and no axis E.
struct NFTTransferFeeCall : HostContextTest
{
Bytes const nftIdBytes{0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xdb,
0xdc, 0xdd, 0xde, 0xdf, 0xe0, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6,
0xe7, 0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 0xef, 0xf0};
uint256 const nftId = uint256::fromVoid(nftIdBytes.data());
};
TEST_F(NFTTransferFeeCall, NftIdBytesBecomeTypedArgumentHostIsAskedFor)
{
static constexpr std::int32_t kTransferFee = 314;
EXPECT_CALL(host, getNFTTransferFee(testing::Eq(nftId)))
.WillOnce(testing::Return(kTransferFee));
EXPECT_EQ(hostContext.getNFTTransferFee(bytesOf(nftIdBytes)), kTransferFee);
}
TEST_F(NFTTransferFeeCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, getNFTTransferFee(testing::Eq(nftId)))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
EXPECT_EQ(
hostContext.getNFTTransferFee(bytesOf(nftIdBytes)),
hfErrorToInt(HostFunctionError::LedgerObjNotFound));
}
TEST_F(NFTTransferFeeCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, getNFTTransferFee(testing::Eq(nftId)))
.WillOnce(testing::Throw(std::runtime_error{"nft transfer fee came apart"}));
EXPECT_EQ(
hostContext.getNFTTransferFee(bytesOf(nftIdBytes)),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("nft transfer fee came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("getNFTTransferFee"));
}
TEST_F(NFTTransferFeeCall, MalformedNftIdIsRefusedWithoutAskingHost)
{
Bytes const malformedNftId(uint256::size() - 1, 0xff);
EXPECT_CALL(host, getNFTTransferFee).Times(0);
EXPECT_EQ(
hostContext.getNFTTransferFee(bytesOf(malformedNftId)),
hfErrorToInt(HostFunctionError::InvalidParams));
}
} // namespace xrpl::test

View File

@@ -0,0 +1,131 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
struct NftokenOfferKeyletCall : HostContextTest
{
Bytes const accountBytes{0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a,
0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, 0x71, 0x72, 0x73, 0x74};
AccountID const account = AccountID::fromVoid(accountBytes.data());
std::int32_t const seq = 13579;
};
TEST_F(NftokenOfferKeyletCall, AccountAndSeqAreForwardedKeyletIsWritten)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, nftokenOfferKeylet(account, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(keylet));
OutRegion out{32};
EXPECT_EQ(
hostContext.nftokenOfferKeylet(bytesOf(accountBytes), seq, out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_TRUE(out.holds(bytesOf(keylet)));
}
TEST_F(NftokenOfferKeyletCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, nftokenOfferKeylet(account, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
OutRegion out{32};
EXPECT_EQ(
hostContext.nftokenOfferKeylet(bytesOf(accountBytes), seq, out.slice()),
hfErrorToInt(HostFunctionError::LedgerObjNotFound));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(NftokenOfferKeyletCall, ShortAccountIsRefusedWithoutAskingHost)
{
Bytes const shortAccount(AccountID::size() - 1, 0x01);
EXPECT_CALL(host, nftokenOfferKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.nftokenOfferKeylet(bytesOf(shortAccount), seq, out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(NftokenOfferKeyletCall, LongAccountIsRefusedWithoutAskingHost)
{
Bytes const longAccount(AccountID::size() + 1, 0x01);
EXPECT_CALL(host, nftokenOfferKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.nftokenOfferKeylet(bytesOf(longAccount), seq, out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(NftokenOfferKeyletCall, EmptyAccountIsRefusedWithoutAskingHost)
{
EXPECT_CALL(host, nftokenOfferKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.nftokenOfferKeylet(bytesOf(Bytes{}), seq, out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(NftokenOfferKeyletCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, nftokenOfferKeylet(account, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Throw(std::runtime_error{"nftoken offer keylet came apart"}));
OutRegion out{32};
EXPECT_EQ(
hostContext.nftokenOfferKeylet(bytesOf(accountBytes), seq, out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("nftoken offer keylet came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("nftokenOfferKeylet"));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(NftokenOfferKeyletCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, nftokenOfferKeylet(account, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(keylet));
OutRegion out{keylet.size() - 1};
EXPECT_EQ(
hostContext.nftokenOfferKeylet(bytesOf(accountBytes), seq, out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(NftokenOfferKeyletCall, OutRegionOfExactSizeIsWritten)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, nftokenOfferKeylet(account, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(keylet));
OutRegion out{keylet.size()};
EXPECT_EQ(
hostContext.nftokenOfferKeylet(bytesOf(accountBytes), seq, out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_TRUE(out.holds(bytesOf(keylet)));
}
TEST_F(NftokenOfferKeyletCall, EmptyResultAnswersZeroAndWritesNothing)
{
EXPECT_CALL(host, nftokenOfferKeylet(account, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(Bytes{}));
OutRegion out{32};
EXPECT_EQ(hostContext.nftokenOfferKeylet(bytesOf(accountBytes), seq, out.slice()), 0);
EXPECT_FALSE(out.wasWritten());
}
} // namespace xrpl::test

View File

@@ -0,0 +1,131 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
struct OfferKeyletCall : HostContextTest
{
Bytes const accountBytes{0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a,
0x7b, 0x7c, 0x7d, 0x7e, 0x7f, 0x80, 0x81, 0x82, 0x83, 0x84};
AccountID const account = AccountID::fromVoid(accountBytes.data());
std::int32_t const seq = 24680;
};
TEST_F(OfferKeyletCall, AccountAndSeqAreForwardedKeyletIsWritten)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, offerKeylet(account, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(keylet));
OutRegion out{32};
EXPECT_EQ(
hostContext.offerKeylet(bytesOf(accountBytes), seq, out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_TRUE(out.holds(bytesOf(keylet)));
}
TEST_F(OfferKeyletCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, offerKeylet(account, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
OutRegion out{32};
EXPECT_EQ(
hostContext.offerKeylet(bytesOf(accountBytes), seq, out.slice()),
hfErrorToInt(HostFunctionError::LedgerObjNotFound));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(OfferKeyletCall, ShortAccountIsRefusedWithoutAskingHost)
{
Bytes const shortAccount(AccountID::size() - 1, 0x01);
EXPECT_CALL(host, offerKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.offerKeylet(bytesOf(shortAccount), seq, out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(OfferKeyletCall, LongAccountIsRefusedWithoutAskingHost)
{
Bytes const longAccount(AccountID::size() + 1, 0x01);
EXPECT_CALL(host, offerKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.offerKeylet(bytesOf(longAccount), seq, out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(OfferKeyletCall, EmptyAccountIsRefusedWithoutAskingHost)
{
EXPECT_CALL(host, offerKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.offerKeylet(bytesOf(Bytes{}), seq, out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(OfferKeyletCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, offerKeylet(account, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Throw(std::runtime_error{"offer keylet came apart"}));
OutRegion out{32};
EXPECT_EQ(
hostContext.offerKeylet(bytesOf(accountBytes), seq, out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("offer keylet came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("offerKeylet"));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(OfferKeyletCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, offerKeylet(account, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(keylet));
OutRegion out{keylet.size() - 1};
EXPECT_EQ(
hostContext.offerKeylet(bytesOf(accountBytes), seq, out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(OfferKeyletCall, OutRegionOfExactSizeIsWritten)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, offerKeylet(account, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(keylet));
OutRegion out{keylet.size()};
EXPECT_EQ(
hostContext.offerKeylet(bytesOf(accountBytes), seq, out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_TRUE(out.holds(bytesOf(keylet)));
}
TEST_F(OfferKeyletCall, EmptyResultAnswersZeroAndWritesNothing)
{
EXPECT_CALL(host, offerKeylet(account, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(Bytes{}));
OutRegion out{32};
EXPECT_EQ(hostContext.offerKeylet(bytesOf(accountBytes), seq, out.slice()), 0);
EXPECT_FALSE(out.wasWritten());
}
} // namespace xrpl::test

View File

@@ -0,0 +1,131 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
struct OracleKeyletCall : HostContextTest
{
Bytes const accountBytes{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14};
AccountID const account = AccountID::fromVoid(accountBytes.data());
std::int32_t const docId = 12345;
};
TEST_F(OracleKeyletCall, AccountAndDocIdAreForwardedKeyletIsWritten)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, oracleKeylet(account, static_cast<std::uint32_t>(docId)))
.WillOnce(testing::Return(keylet));
OutRegion out{32};
EXPECT_EQ(
hostContext.oracleKeylet(bytesOf(accountBytes), docId, out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_TRUE(out.holds(bytesOf(keylet)));
}
TEST_F(OracleKeyletCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, oracleKeylet(account, static_cast<std::uint32_t>(docId)))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
OutRegion out{32};
EXPECT_EQ(
hostContext.oracleKeylet(bytesOf(accountBytes), docId, out.slice()),
hfErrorToInt(HostFunctionError::LedgerObjNotFound));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(OracleKeyletCall, ShortAccountIsRefusedWithoutAskingHost)
{
Bytes const shortAccount(AccountID::size() - 1, 0x01);
EXPECT_CALL(host, oracleKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.oracleKeylet(bytesOf(shortAccount), docId, out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(OracleKeyletCall, LongAccountIsRefusedWithoutAskingHost)
{
Bytes const longAccount(AccountID::size() + 1, 0x01);
EXPECT_CALL(host, oracleKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.oracleKeylet(bytesOf(longAccount), docId, out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(OracleKeyletCall, EmptyAccountIsRefusedWithoutAskingHost)
{
EXPECT_CALL(host, oracleKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.oracleKeylet(bytesOf(Bytes{}), docId, out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(OracleKeyletCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, oracleKeylet(account, static_cast<std::uint32_t>(docId)))
.WillOnce(testing::Throw(std::runtime_error{"oracle keylet came apart"}));
OutRegion out{32};
EXPECT_EQ(
hostContext.oracleKeylet(bytesOf(accountBytes), docId, out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("oracle keylet came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("oracleKeylet"));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(OracleKeyletCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, oracleKeylet(account, static_cast<std::uint32_t>(docId)))
.WillOnce(testing::Return(keylet));
OutRegion out{keylet.size() - 1};
EXPECT_EQ(
hostContext.oracleKeylet(bytesOf(accountBytes), docId, out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(OracleKeyletCall, OutRegionOfExactSizeIsWritten)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, oracleKeylet(account, static_cast<std::uint32_t>(docId)))
.WillOnce(testing::Return(keylet));
OutRegion out{keylet.size()};
EXPECT_EQ(
hostContext.oracleKeylet(bytesOf(accountBytes), docId, out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_TRUE(out.holds(bytesOf(keylet)));
}
TEST_F(OracleKeyletCall, EmptyResultAnswersZeroAndWritesNothing)
{
EXPECT_CALL(host, oracleKeylet(account, static_cast<std::uint32_t>(docId)))
.WillOnce(testing::Return(Bytes{}));
OutRegion out{32};
EXPECT_EQ(hostContext.oracleKeylet(bytesOf(accountBytes), docId, out.slice()), 0);
EXPECT_FALSE(out.wasWritten());
}
} // namespace xrpl::test

View File

@@ -0,0 +1,86 @@
#include <xrpl/basics/base_uint.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// No D or F axis: `getParentLedgerHash` takes no argument, so there is nothing to decode wrong
// and nothing whose forwarded identity to check.
//
// Unlike `getLedgerSqn`/`getParentLedgerTime`, the result is a `Hash` (a `uint256`) written
// whole through `answer` (`invoke<false>`), not a scalar through `answerScalar` - so it is
// asserted as bytes, the way `TxField.cpp` asserts its `Bytes` result, rather than as a
// little-endian scalar.
struct ParentLedgerHashCall : HostContextTest
{
Bytes const hashBytes{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b,
0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16,
0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20};
Hash const hash = uint256::fromVoid(hashBytes.data());
};
TEST_F(ParentLedgerHashCall, HostValueIsWrittenAsBytes)
{
EXPECT_CALL(host, getParentLedgerHash()).WillOnce(testing::Return(hash));
OutRegion out{32};
EXPECT_EQ(
hostContext.getParentLedgerHash(out.slice()), static_cast<std::int32_t>(hashBytes.size()));
EXPECT_TRUE(out.holds(bytesOf(hashBytes)));
}
TEST_F(ParentLedgerHashCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, getParentLedgerHash())
.WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
OutRegion out{32};
EXPECT_EQ(
hostContext.getParentLedgerHash(out.slice()),
hfErrorToInt(HostFunctionError::LedgerObjNotFound));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(ParentLedgerHashCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, getParentLedgerHash())
.WillOnce(testing::Throw(std::runtime_error{"parent ledger hash came apart"}));
OutRegion out{32};
EXPECT_EQ(
hostContext.getParentLedgerHash(out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("parent ledger hash came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("getParentLedgerHash"));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(ParentLedgerHashCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
EXPECT_CALL(host, getParentLedgerHash()).WillOnce(testing::Return(hash));
OutRegion out{hashBytes.size() - 1};
EXPECT_EQ(
hostContext.getParentLedgerHash(out.slice()), static_cast<std::int32_t>(hashBytes.size()));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(ParentLedgerHashCall, OutRegionOfExactSizeIsWritten)
{
EXPECT_CALL(host, getParentLedgerHash()).WillOnce(testing::Return(hash));
OutRegion out{hashBytes.size()};
EXPECT_EQ(
hostContext.getParentLedgerHash(out.slice()), static_cast<std::int32_t>(hashBytes.size()));
EXPECT_TRUE(out.holds(bytesOf(hashBytes)));
}
} // namespace xrpl::test

View File

@@ -0,0 +1,75 @@
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// No D or F axis: `getParentLedgerTime` takes no argument, so there is nothing to decode wrong
// and nothing whose forwarded identity to check.
struct ParentLedgerTimeCall : HostContextTest
{
static constexpr std::uint32_t kParentLedgerTime = 0x12345678;
Bytes const expectedBytes = bytesOfScalar(kParentLedgerTime);
};
TEST_F(ParentLedgerTimeCall, HostValueIsWrittenAsLittleEndianBytes)
{
EXPECT_CALL(host, getParentLedgerTime()).WillOnce(testing::Return(kParentLedgerTime));
OutRegion out{32};
EXPECT_EQ(hostContext.getParentLedgerTime(out.slice()), 4);
EXPECT_TRUE(out.holds(bytesOf(expectedBytes)));
}
TEST_F(ParentLedgerTimeCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, getParentLedgerTime())
.WillOnce(testing::Return(std::unexpected(HostFunctionError::Unimplemented)));
OutRegion out{4};
EXPECT_EQ(
hostContext.getParentLedgerTime(out.slice()),
hfErrorToInt(HostFunctionError::Unimplemented));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(ParentLedgerTimeCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, getParentLedgerTime())
.WillOnce(testing::Throw(std::runtime_error{"parent ledger time came apart"}));
OutRegion out{4};
EXPECT_EQ(
hostContext.getParentLedgerTime(out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("parent ledger time came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("getParentLedgerTime"));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(ParentLedgerTimeCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
EXPECT_CALL(host, getParentLedgerTime()).WillOnce(testing::Return(kParentLedgerTime));
OutRegion out{3};
EXPECT_EQ(hostContext.getParentLedgerTime(out.slice()), 4);
EXPECT_FALSE(out.wasWritten());
}
TEST_F(ParentLedgerTimeCall, OutRegionOfExactSizeIsWritten)
{
EXPECT_CALL(host, getParentLedgerTime()).WillOnce(testing::Return(kParentLedgerTime));
OutRegion out{4};
EXPECT_EQ(hostContext.getParentLedgerTime(out.slice()), 4);
EXPECT_TRUE(out.holds(bytesOf(expectedBytes)));
}
} // namespace xrpl::test

View File

@@ -0,0 +1,152 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
//
// `account` and `destination` are distinct byte patterns: a happy path built from two copies of
// the same account would still pass if the two were swapped.
struct PaychannelKeyletCall : HostContextTest
{
Bytes const accountBytes{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14};
Bytes const destinationBytes{0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a,
0x7b, 0x7c, 0x7d, 0x7e, 0x7f, 0x80, 0x81, 0x82, 0x83, 0x84};
AccountID const account = AccountID::fromVoid(accountBytes.data());
AccountID const destination = AccountID::fromVoid(destinationBytes.data());
std::int32_t const seq = 54321;
};
TEST_F(PaychannelKeyletCall, AccountsAndSeqAreForwardedKeyletIsWritten)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, paychannelKeylet(account, destination, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(keylet));
OutRegion out{32};
EXPECT_EQ(
hostContext.paychannelKeylet(
bytesOf(accountBytes), bytesOf(destinationBytes), seq, out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_TRUE(out.holds(bytesOf(keylet)));
}
TEST_F(PaychannelKeyletCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, paychannelKeylet(account, destination, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
OutRegion out{32};
EXPECT_EQ(
hostContext.paychannelKeylet(
bytesOf(accountBytes), bytesOf(destinationBytes), seq, out.slice()),
hfErrorToInt(HostFunctionError::LedgerObjNotFound));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(PaychannelKeyletCall, MalformedAccountIsRefusedWithoutAskingHost)
{
Bytes const malformedAccount(AccountID::size() - 1, 0x01);
EXPECT_CALL(host, paychannelKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.paychannelKeylet(
bytesOf(malformedAccount), bytesOf(destinationBytes), seq, out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(PaychannelKeyletCall, MalformedDestinationIsRefusedWithoutAskingHost)
{
Bytes const malformedDestination(AccountID::size() + 1, 0x71);
EXPECT_CALL(host, paychannelKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.paychannelKeylet(
bytesOf(accountBytes), bytesOf(malformedDestination), seq, out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
// Both ids fail one combined length check, so a call malformed in both places answers the
// same `InvalidParams` as either alone; what's observable is that the host is never asked.
TEST_F(PaychannelKeyletCall, BothAccountsMalformedIsRefusedWithoutAskingHost)
{
Bytes const malformedAccount(AccountID::size() - 1, 0x01);
Bytes const malformedDestination(AccountID::size() - 1, 0x71);
EXPECT_CALL(host, paychannelKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.paychannelKeylet(
bytesOf(malformedAccount), bytesOf(malformedDestination), seq, out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(PaychannelKeyletCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, paychannelKeylet(account, destination, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Throw(std::runtime_error{"paychannel keylet came apart"}));
OutRegion out{32};
EXPECT_EQ(
hostContext.paychannelKeylet(
bytesOf(accountBytes), bytesOf(destinationBytes), seq, out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("paychannel keylet came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("paychannelKeylet"));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(PaychannelKeyletCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, paychannelKeylet(account, destination, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(keylet));
OutRegion out{keylet.size() - 1};
EXPECT_EQ(
hostContext.paychannelKeylet(
bytesOf(accountBytes), bytesOf(destinationBytes), seq, out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(PaychannelKeyletCall, OutRegionOfExactSizeIsWritten)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, paychannelKeylet(account, destination, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(keylet));
OutRegion out{keylet.size()};
EXPECT_EQ(
hostContext.paychannelKeylet(
bytesOf(accountBytes), bytesOf(destinationBytes), seq, out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_TRUE(out.holds(bytesOf(keylet)));
}
TEST_F(PaychannelKeyletCall, EmptyResultAnswersZeroAndWritesNothing)
{
EXPECT_CALL(host, paychannelKeylet(account, destination, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(Bytes{}));
OutRegion out{32};
EXPECT_EQ(
hostContext.paychannelKeylet(
bytesOf(accountBytes), bytesOf(destinationBytes), seq, out.slice()),
0);
EXPECT_FALSE(out.wasWritten());
}
} // namespace xrpl::test

View File

@@ -0,0 +1,131 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
struct PermissionedDomainKeyletCall : HostContextTest
{
Bytes const accountBytes{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14};
AccountID const account = AccountID::fromVoid(accountBytes.data());
std::int32_t const seq = 12345;
};
TEST_F(PermissionedDomainKeyletCall, AccountAndSeqAreForwardedKeyletIsWritten)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, permissionedDomainKeylet(account, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(keylet));
OutRegion out{32};
EXPECT_EQ(
hostContext.permissionedDomainKeylet(bytesOf(accountBytes), seq, out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_TRUE(out.holds(bytesOf(keylet)));
}
TEST_F(PermissionedDomainKeyletCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, permissionedDomainKeylet(account, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
OutRegion out{32};
EXPECT_EQ(
hostContext.permissionedDomainKeylet(bytesOf(accountBytes), seq, out.slice()),
hfErrorToInt(HostFunctionError::LedgerObjNotFound));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(PermissionedDomainKeyletCall, ShortAccountIsRefusedWithoutAskingHost)
{
Bytes const shortAccount(AccountID::size() - 1, 0x01);
EXPECT_CALL(host, permissionedDomainKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.permissionedDomainKeylet(bytesOf(shortAccount), seq, out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(PermissionedDomainKeyletCall, LongAccountIsRefusedWithoutAskingHost)
{
Bytes const longAccount(AccountID::size() + 1, 0x01);
EXPECT_CALL(host, permissionedDomainKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.permissionedDomainKeylet(bytesOf(longAccount), seq, out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(PermissionedDomainKeyletCall, EmptyAccountIsRefusedWithoutAskingHost)
{
EXPECT_CALL(host, permissionedDomainKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.permissionedDomainKeylet(bytesOf(Bytes{}), seq, out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(PermissionedDomainKeyletCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, permissionedDomainKeylet(account, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Throw(std::runtime_error{"permissioned domain keylet came apart"}));
OutRegion out{32};
EXPECT_EQ(
hostContext.permissionedDomainKeylet(bytesOf(accountBytes), seq, out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("permissioned domain keylet came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("permissionedDomainKeylet"));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(PermissionedDomainKeyletCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, permissionedDomainKeylet(account, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(keylet));
OutRegion out{keylet.size() - 1};
EXPECT_EQ(
hostContext.permissionedDomainKeylet(bytesOf(accountBytes), seq, out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(PermissionedDomainKeyletCall, OutRegionOfExactSizeIsWritten)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, permissionedDomainKeylet(account, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(keylet));
OutRegion out{keylet.size()};
EXPECT_EQ(
hostContext.permissionedDomainKeylet(bytesOf(accountBytes), seq, out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_TRUE(out.holds(bytesOf(keylet)));
}
TEST_F(PermissionedDomainKeyletCall, EmptyResultAnswersZeroAndWritesNothing)
{
EXPECT_CALL(host, permissionedDomainKeylet(account, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(Bytes{}));
OutRegion out{32};
EXPECT_EQ(hostContext.permissionedDomainKeylet(bytesOf(accountBytes), seq, out.slice()), 0);
EXPECT_FALSE(out.wasWritten());
}
} // namespace xrpl::test

View File

@@ -0,0 +1,81 @@
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <tx/wasm/MockHostFunctions.h>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// `host_calls/Sha512Half.cpp` runs the digest through the engine; what is left at this layer is
// its own contract - the out-region rule, `guarded`, and an empty input.
struct Sha512HalfDirectCall : HostContextTest
{
Bytes const data{'a', 'b', 'c'};
Bytes const digestBytes = Bytes(32, 0x0a);
Hash const digest = uint256::fromVoid(digestBytes.data());
};
TEST_F(Sha512HalfDirectCall, DataForwardedAndDigestWritten)
{
EXPECT_CALL(host, computeSha512HalfHash(BytesAre("abc"))).WillOnce(testing::Return(digest));
OutRegion out{32};
EXPECT_EQ(hostContext.sha512Half(bytesOf(data), out.slice()), 32);
EXPECT_TRUE(out.holds(bytesOf(digestBytes)));
}
TEST_F(Sha512HalfDirectCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, computeSha512HalfHash)
.WillOnce(testing::Return(std::unexpected(HostFunctionError::InvalidParams)));
OutRegion out{32};
EXPECT_EQ(
hostContext.sha512Half(bytesOf(data), out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(Sha512HalfDirectCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, computeSha512HalfHash)
.WillOnce(testing::Throw(std::runtime_error{"sha512 half came apart"}));
OutRegion out{32};
EXPECT_EQ(
hostContext.sha512Half(bytesOf(data), out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("sha512 half came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("sha512Half"));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(Sha512HalfDirectCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
EXPECT_CALL(host, computeSha512HalfHash(BytesAre("abc"))).WillOnce(testing::Return(digest));
OutRegion out{31};
EXPECT_EQ(hostContext.sha512Half(bytesOf(data), out.slice()), 32);
EXPECT_FALSE(out.wasWritten());
}
// Nothing in the hash requires a non-empty input, so an empty slice is hashed like any other,
// not refused.
TEST_F(Sha512HalfDirectCall, EmptyInputIsHashedLikeAnyOther)
{
EXPECT_CALL(host, computeSha512HalfHash(testing::Property(&Slice::empty, true)))
.WillOnce(testing::Return(digest));
OutRegion out{32};
EXPECT_EQ(hostContext.sha512Half(bytesOf(Bytes{}), out.slice()), 32);
EXPECT_TRUE(out.holds(bytesOf(digestBytes)));
}
} // namespace xrpl::test

View File

@@ -0,0 +1,126 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
struct SignerListKeyletCall : HostContextTest
{
Bytes const accountBytes{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14};
AccountID const account = AccountID::fromVoid(accountBytes.data());
};
TEST_F(SignerListKeyletCall, AccountIsForwardedKeyletIsWritten)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, signerListKeylet(account)).WillOnce(testing::Return(keylet));
OutRegion out{32};
EXPECT_EQ(
hostContext.signerListKeylet(bytesOf(accountBytes), out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_TRUE(out.holds(bytesOf(keylet)));
}
TEST_F(SignerListKeyletCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, signerListKeylet(account))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
OutRegion out{32};
EXPECT_EQ(
hostContext.signerListKeylet(bytesOf(accountBytes), out.slice()),
hfErrorToInt(HostFunctionError::LedgerObjNotFound));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(SignerListKeyletCall, ShortAccountIsRefusedWithoutAskingHost)
{
Bytes const shortAccount(AccountID::size() - 1, 0x01);
EXPECT_CALL(host, signerListKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.signerListKeylet(bytesOf(shortAccount), out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(SignerListKeyletCall, LongAccountIsRefusedWithoutAskingHost)
{
Bytes const longAccount(AccountID::size() + 1, 0x01);
EXPECT_CALL(host, signerListKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.signerListKeylet(bytesOf(longAccount), out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(SignerListKeyletCall, EmptyAccountIsRefusedWithoutAskingHost)
{
EXPECT_CALL(host, signerListKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.signerListKeylet(bytesOf(Bytes{}), out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(SignerListKeyletCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, signerListKeylet(account))
.WillOnce(testing::Throw(std::runtime_error{"signer list keylet came apart"}));
OutRegion out{32};
EXPECT_EQ(
hostContext.signerListKeylet(bytesOf(accountBytes), out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("signer list keylet came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("signerListKeylet"));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(SignerListKeyletCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, signerListKeylet(account)).WillOnce(testing::Return(keylet));
OutRegion out{keylet.size() - 1};
EXPECT_EQ(
hostContext.signerListKeylet(bytesOf(accountBytes), out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(SignerListKeyletCall, OutRegionOfExactSizeIsWritten)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, signerListKeylet(account)).WillOnce(testing::Return(keylet));
OutRegion out{keylet.size()};
EXPECT_EQ(
hostContext.signerListKeylet(bytesOf(accountBytes), out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_TRUE(out.holds(bytesOf(keylet)));
}
TEST_F(SignerListKeyletCall, EmptyResultAnswersZeroAndWritesNothing)
{
EXPECT_CALL(host, signerListKeylet(account)).WillOnce(testing::Return(Bytes{}));
OutRegion out{32};
EXPECT_EQ(hostContext.signerListKeylet(bytesOf(accountBytes), out.slice()), 0);
EXPECT_FALSE(out.wasWritten());
}
} // namespace xrpl::test

View File

@@ -0,0 +1,131 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
struct TicketKeyletCall : HostContextTest
{
Bytes const accountBytes{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14};
AccountID const account = AccountID::fromVoid(accountBytes.data());
std::int32_t const seq = 12345;
};
TEST_F(TicketKeyletCall, AccountAndSeqAreForwardedKeyletIsWritten)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, ticketKeylet(account, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(keylet));
OutRegion out{32};
EXPECT_EQ(
hostContext.ticketKeylet(bytesOf(accountBytes), seq, out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_TRUE(out.holds(bytesOf(keylet)));
}
TEST_F(TicketKeyletCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, ticketKeylet(account, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
OutRegion out{32};
EXPECT_EQ(
hostContext.ticketKeylet(bytesOf(accountBytes), seq, out.slice()),
hfErrorToInt(HostFunctionError::LedgerObjNotFound));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(TicketKeyletCall, ShortAccountIsRefusedWithoutAskingHost)
{
Bytes const shortAccount(AccountID::size() - 1, 0x01);
EXPECT_CALL(host, ticketKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.ticketKeylet(bytesOf(shortAccount), seq, out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(TicketKeyletCall, LongAccountIsRefusedWithoutAskingHost)
{
Bytes const longAccount(AccountID::size() + 1, 0x01);
EXPECT_CALL(host, ticketKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.ticketKeylet(bytesOf(longAccount), seq, out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(TicketKeyletCall, EmptyAccountIsRefusedWithoutAskingHost)
{
EXPECT_CALL(host, ticketKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.ticketKeylet(bytesOf(Bytes{}), seq, out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(TicketKeyletCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, ticketKeylet(account, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Throw(std::runtime_error{"ticket keylet came apart"}));
OutRegion out{32};
EXPECT_EQ(
hostContext.ticketKeylet(bytesOf(accountBytes), seq, out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("ticket keylet came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("ticketKeylet"));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(TicketKeyletCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, ticketKeylet(account, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(keylet));
OutRegion out{keylet.size() - 1};
EXPECT_EQ(
hostContext.ticketKeylet(bytesOf(accountBytes), seq, out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(TicketKeyletCall, OutRegionOfExactSizeIsWritten)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, ticketKeylet(account, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(keylet));
OutRegion out{keylet.size()};
EXPECT_EQ(
hostContext.ticketKeylet(bytesOf(accountBytes), seq, out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_TRUE(out.holds(bytesOf(keylet)));
}
TEST_F(TicketKeyletCall, EmptyResultAnswersZeroAndWritesNothing)
{
EXPECT_CALL(host, ticketKeylet(account, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(Bytes{}));
OutRegion out{32};
EXPECT_EQ(hostContext.ticketKeylet(bytesOf(accountBytes), seq, out.slice()), 0);
EXPECT_FALSE(out.wasWritten());
}
} // namespace xrpl::test

View File

@@ -0,0 +1,54 @@
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
// For `TraceDataType`, which the bridge declares and this header defines.
#include <xrpl_wasm_vm_ffi_cxxbridge/lib.h>
#include <stdexcept>
#include <string_view>
namespace xrpl::test {
// `trace` returns `void` and answers the guest nothing in every case, success or failure alike.
// It is not wrapped in `guarded`: it has its own try/catch, so a throw is swallowed here rather
// than escaping a `noexcept` method. `host_calls/Trace.cpp` already renders all seven
// `TraceDataType`s through the engine; this file does not repeat that.
struct TraceDirectCall : HostContextTest
{
};
// One rendering, to show this layer forwards at all - every `TraceDataType` is
// `host_calls/Trace.cpp`'s job.
TEST_F(TraceDirectCall, MessageAndDataReachHostAsRenderedText)
{
EXPECT_CALL(host, trace(std::string_view("note"), std::string_view("hi")));
hostContext.trace("note", bytesOf(Bytes{'h', 'i'}), TraceDataType::AsText);
}
// The catch sits in `trace` itself, not in `guarded`. It logs at trace level, below the
// fixture's default threshold, so the threshold is lowered to observe it.
TEST_F(TraceDirectCall, HostExceptionIsSwallowedRatherThanEscaping)
{
sink.threshold(beast::Severity::Trace);
EXPECT_CALL(host, trace).WillOnce(testing::Throw(std::runtime_error{"trace sink came apart"}));
hostContext.trace("note", bytesOf(Bytes{'h', 'i'}), TraceDataType::AsText);
EXPECT_THAT(logged(), testing::HasSubstr("trace sink came apart"));
}
// The cap is on message and data together, not on data alone.
TEST_F(TraceDirectCall, MessagePlusDataPastCapIsDroppedWithoutAskingHost)
{
Bytes const data(kMaxWasmDataLength, 0x41);
EXPECT_CALL(host, trace).Times(0);
hostContext.trace("x", bytesOf(data), TraceDataType::AsText);
}
} // namespace xrpl::test

View File

@@ -0,0 +1,180 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
//
// `account1` and `account2` are distinct byte patterns: a happy path built from two copies of
// the same account would still pass if the two were swapped.
struct TrustLineKeyletCall : HostContextTest
{
Bytes const account1Bytes{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14};
Bytes const account2Bytes{0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a,
0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40, 0x41, 0x42, 0x43, 0x44};
Bytes const currencyBytes{0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a,
0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, 0x71, 0x72, 0x73, 0x74};
AccountID const account1 = AccountID::fromVoid(account1Bytes.data());
AccountID const account2 = AccountID::fromVoid(account2Bytes.data());
Currency const currency = Currency::fromVoid(currencyBytes.data());
};
TEST_F(TrustLineKeyletCall, AccountsAndCurrencyAreForwardedKeyletIsWritten)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, trustLineKeylet(account1, account2, currency))
.WillOnce(testing::Return(keylet));
OutRegion out{32};
EXPECT_EQ(
hostContext.trustLineKeylet(
bytesOf(account1Bytes), bytesOf(account2Bytes), bytesOf(currencyBytes), out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_TRUE(out.holds(bytesOf(keylet)));
}
TEST_F(TrustLineKeyletCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, trustLineKeylet(account1, account2, currency))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
OutRegion out{32};
EXPECT_EQ(
hostContext.trustLineKeylet(
bytesOf(account1Bytes), bytesOf(account2Bytes), bytesOf(currencyBytes), out.slice()),
hfErrorToInt(HostFunctionError::LedgerObjNotFound));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(TrustLineKeyletCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, trustLineKeylet(account1, account2, currency))
.WillOnce(testing::Throw(std::runtime_error{"trust line keylet came apart"}));
OutRegion out{32};
EXPECT_EQ(
hostContext.trustLineKeylet(
bytesOf(account1Bytes), bytesOf(account2Bytes), bytesOf(currencyBytes), out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("trust line keylet came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("trustLineKeylet"));
}
TEST_F(TrustLineKeyletCall, MalformedAccount1IsRefusedWithoutAskingHost)
{
Bytes const malformedAccount1(AccountID::size() - 1, 0x01);
EXPECT_CALL(host, trustLineKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.trustLineKeylet(
bytesOf(malformedAccount1),
bytesOf(account2Bytes),
bytesOf(currencyBytes),
out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(TrustLineKeyletCall, MalformedAccount2IsRefusedWithoutAskingHost)
{
Bytes const malformedAccount2(AccountID::size() + 1, 0x31);
EXPECT_CALL(host, trustLineKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.trustLineKeylet(
bytesOf(account1Bytes),
bytesOf(malformedAccount2),
bytesOf(currencyBytes),
out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(TrustLineKeyletCall, MalformedCurrencyIsRefusedWithoutAskingHost)
{
Bytes const malformedCurrency(Currency::size() - 1, 0x61);
EXPECT_CALL(host, trustLineKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.trustLineKeylet(
bytesOf(account1Bytes),
bytesOf(account2Bytes),
bytesOf(malformedCurrency),
out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
// The currency length is checked before either account's, but every malformed shape answers
// the same `InvalidParams`, so a call malformed in both places cannot show which check fired.
// What's observable: the host is never asked.
TEST_F(TrustLineKeyletCall, CurrencyAndAccountBothMalformedIsRefusedWithoutAskingHost)
{
Bytes const malformedCurrency(Currency::size() - 1, 0x61);
Bytes const malformedAccount1(AccountID::size() - 1, 0x01);
EXPECT_CALL(host, trustLineKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.trustLineKeylet(
bytesOf(malformedAccount1),
bytesOf(account2Bytes),
bytesOf(malformedCurrency),
out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(TrustLineKeyletCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, trustLineKeylet(account1, account2, currency))
.WillOnce(testing::Return(keylet));
OutRegion out{keylet.size() - 1};
EXPECT_EQ(
hostContext.trustLineKeylet(
bytesOf(account1Bytes), bytesOf(account2Bytes), bytesOf(currencyBytes), out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(TrustLineKeyletCall, OutRegionOfExactSizeIsWritten)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, trustLineKeylet(account1, account2, currency))
.WillOnce(testing::Return(keylet));
OutRegion out{keylet.size()};
EXPECT_EQ(
hostContext.trustLineKeylet(
bytesOf(account1Bytes), bytesOf(account2Bytes), bytesOf(currencyBytes), out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_TRUE(out.holds(bytesOf(keylet)));
}
TEST_F(TrustLineKeyletCall, EmptyResultAnswersZeroAndWritesNothing)
{
EXPECT_CALL(host, trustLineKeylet(account1, account2, currency))
.WillOnce(testing::Return(Bytes{}));
OutRegion out{32};
EXPECT_EQ(
hostContext.trustLineKeylet(
bytesOf(account1Bytes), bytesOf(account2Bytes), bytesOf(currencyBytes), out.slice()),
0);
EXPECT_FALSE(out.wasWritten());
}
} // namespace xrpl::test

View File

@@ -0,0 +1,56 @@
#include <xrpl/protocol/SField.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// `getTxArrayLen` answers its count directly rather than through an out region: no axis E, no
// `OutRegion`, and the happy path asserts the returned count.
struct TxArrayLenCall : HostContextTest
{
std::int32_t fieldCode = sfBalance.getCode();
};
TEST_F(TxArrayLenCall, FieldCodeBecomesSFieldHostIsAskedFor)
{
EXPECT_CALL(host, getTxArrayLen(testing::Ref(sfBalance))).WillOnce(testing::Return(5));
EXPECT_EQ(hostContext.getTxArrayLen(fieldCode), 5);
}
// `NoArray` is what a field that is not an array actually answers, so it stands in for axis B
// here rather than an arbitrary code.
TEST_F(TxArrayLenCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, getTxArrayLen(testing::Ref(sfBalance)))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::NoArray)));
EXPECT_EQ(hostContext.getTxArrayLen(fieldCode), hfErrorToInt(HostFunctionError::NoArray));
}
TEST_F(TxArrayLenCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, getTxArrayLen(testing::Ref(sfBalance)))
.WillOnce(testing::Throw(std::runtime_error{"tx array len came apart"}));
EXPECT_EQ(hostContext.getTxArrayLen(fieldCode), hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("tx array len came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("getTxArrayLen"));
}
TEST_F(TxArrayLenCall, UnknownFieldCodeIsRefusedWithoutAskingHost)
{
fieldCode = 0x7fff'0000; // a code nothing is registered under
EXPECT_CALL(host, getTxArrayLen).Times(0);
EXPECT_EQ(hostContext.getTxArrayLen(fieldCode), hfErrorToInt(HostFunctionError::InvalidField));
}
} // namespace xrpl::test

View File

@@ -0,0 +1,126 @@
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// The engine's own rules - buffer-fit, the field cap, guest memory - are tested on the Rust
// side, not here.
struct TxFieldCall : HostContextTest
{
std::int32_t fieldCode = sfBalance.getCode();
};
TEST_F(TxFieldCall, FieldCodeBecomesSFieldHostIsAskedFor)
{
Bytes const value{1, 2, 3};
EXPECT_CALL(host, getTxField(testing::Ref(sfBalance))).WillOnce(testing::Return(value));
OutRegion out{32};
EXPECT_EQ(
hostContext.getTxField(fieldCode, out.slice()), static_cast<std::int32_t>(value.size()));
EXPECT_TRUE(out.holds(bytesOf(value)));
}
TEST_F(TxFieldCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, getTxField(testing::Ref(sfBalance)))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::FieldNotFound)));
OutRegion out{32};
EXPECT_EQ(
hostContext.getTxField(fieldCode, out.slice()),
hfErrorToInt(HostFunctionError::FieldNotFound));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(TxFieldCall, UnknownFieldCodeIsRefusedWithoutAskingHost)
{
fieldCode = 0x7fff'0000; // a code nothing is registered under
EXPECT_CALL(host, getTxField).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.getTxField(fieldCode, out.slice()),
hfErrorToInt(HostFunctionError::InvalidField));
}
TEST_F(TxFieldCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, getTxField(testing::Ref(sfBalance)))
.WillOnce(testing::Throw(std::runtime_error{"balance field came apart"}));
OutRegion out{32};
EXPECT_EQ(
hostContext.getTxField(fieldCode, out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("balance field came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("getTxField"));
}
// `guarded`'s `catch (...)` arm, for a thrown value that is not a `std::exception`.
TEST_F(TxFieldCall, NonStandardThrowBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, getTxField(testing::Ref(sfBalance))).WillOnce(testing::Throw(42));
OutRegion out{32};
EXPECT_EQ(
hostContext.getTxField(fieldCode, out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("getTxField"));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(TxFieldCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
Bytes const value{1, 2, 3};
EXPECT_CALL(host, getTxField(testing::Ref(sfBalance))).WillOnce(testing::Return(value));
OutRegion out{value.size() - 1};
EXPECT_EQ(
hostContext.getTxField(fieldCode, out.slice()), static_cast<std::int32_t>(value.size()));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(TxFieldCall, OutRegionOfExactSizeIsWritten)
{
Bytes const value{1, 2, 3};
EXPECT_CALL(host, getTxField(testing::Ref(sfBalance))).WillOnce(testing::Return(value));
OutRegion out{value.size()};
EXPECT_EQ(
hostContext.getTxField(fieldCode, out.slice()), static_cast<std::int32_t>(value.size()));
EXPECT_TRUE(out.holds(bytesOf(value)));
}
// `kMaxWasmDataLength` is the engine's cap, not `HostContext`'s: a length past it crosses
// unchanged here, where the sibling engine test sees `DataFieldTooLarge` instead.
TEST_F(TxFieldCall, LengthPastProtocolCapCrossesUnchanged)
{
Bytes const value(kMaxWasmDataLength + 1, 0xab);
EXPECT_CALL(host, getTxField(testing::Ref(sfBalance))).WillOnce(testing::Return(value));
OutRegion out{value.size()};
EXPECT_EQ(
hostContext.getTxField(fieldCode, out.slice()), static_cast<std::int32_t>(value.size()));
}
TEST_F(TxFieldCall, EmptyResultAnswersZeroAndWritesNothing)
{
EXPECT_CALL(host, getTxField(testing::Ref(sfBalance))).WillOnce(testing::Return(Bytes{}));
OutRegion out{32};
EXPECT_EQ(hostContext.getTxField(fieldCode, out.slice()), 0);
EXPECT_FALSE(out.wasWritten());
}
} // namespace xrpl::test

View File

@@ -0,0 +1,76 @@
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
#include <vector>
namespace xrpl::test {
// The engine's own rules - buffer-fit, the field cap, guest memory - are tested on the Rust
// side, not here.
//
// No out region and no axis E: `getTxNestedArrayLen` answers the array's element count
// directly rather than through a written buffer.
struct TxNestedArrayLenCall : HostContextTest
{
std::vector<std::int32_t> const steps{5, -12, 130};
Bytes const locatorBytes = bytesOfSteps(steps);
};
TEST_F(TxNestedArrayLenCall, LocatorBytesBecomeFieldLocatorHostReturnsCount)
{
EXPECT_CALL(host, getTxNestedArrayLen(LocatorEquals(steps))).WillOnce(testing::Return(7));
EXPECT_EQ(hostContext.getTxNestedArrayLen(bytesOf(locatorBytes)), 7);
}
// `NoArray` - the field the locator resolves to is not an array - is the error this shape
// most plausibly returns, so it stands in for axis B.
TEST_F(TxNestedArrayLenCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, getTxNestedArrayLen(LocatorEquals(steps)))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::NoArray)));
EXPECT_EQ(
hostContext.getTxNestedArrayLen(bytesOf(locatorBytes)),
hfErrorToInt(HostFunctionError::NoArray));
}
TEST_F(TxNestedArrayLenCall, EmptyLocatorIsRefusedWithoutAskingHost)
{
EXPECT_CALL(host, getTxNestedArrayLen).Times(0);
EXPECT_EQ(
hostContext.getTxNestedArrayLen(bytesOf(Bytes{})),
hfErrorToInt(HostFunctionError::LocatorMalformed));
}
// Distinct from an empty locator: `invokeWithLocator` checks the two conditions separately.
TEST_F(TxNestedArrayLenCall, MisalignedLocatorLengthIsRefusedWithoutAskingHost)
{
Bytes const oddLength{1, 2, 3};
EXPECT_CALL(host, getTxNestedArrayLen).Times(0);
EXPECT_EQ(
hostContext.getTxNestedArrayLen(bytesOf(oddLength)),
hfErrorToInt(HostFunctionError::LocatorMalformed));
}
TEST_F(TxNestedArrayLenCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, getTxNestedArrayLen(LocatorEquals(steps)))
.WillOnce(testing::Throw(std::runtime_error{"tx nested array len came apart"}));
EXPECT_EQ(
hostContext.getTxNestedArrayLen(bytesOf(locatorBytes)),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("tx nested array len came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("getTxNestedArrayLen"));
}
} // namespace xrpl::test

View File

@@ -0,0 +1,116 @@
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
#include <vector>
namespace xrpl::test {
// The engine's own rules - buffer-fit, the field cap, guest memory - are tested on the Rust
// side, not here.
struct TxNestedFieldCall : HostContextTest
{
std::vector<std::int32_t> const steps{5, -12, 130};
Bytes const locatorBytes = bytesOfSteps(steps);
};
TEST_F(TxNestedFieldCall, LocatorBytesBecomeFieldLocatorHostIsAskedFor)
{
Bytes const value{1, 2, 3};
EXPECT_CALL(host, getTxNestedField(LocatorEquals(steps))).WillOnce(testing::Return(value));
OutRegion out{32};
EXPECT_EQ(
hostContext.getTxNestedField(bytesOf(locatorBytes), out.slice()),
static_cast<std::int32_t>(value.size()));
EXPECT_TRUE(out.holds(bytesOf(value)));
}
TEST_F(TxNestedFieldCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, getTxNestedField(LocatorEquals(steps)))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::NotLeafField)));
OutRegion out{32};
EXPECT_EQ(
hostContext.getTxNestedField(bytesOf(locatorBytes), out.slice()),
hfErrorToInt(HostFunctionError::NotLeafField));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(TxNestedFieldCall, EmptyLocatorIsRefusedWithoutAskingHost)
{
EXPECT_CALL(host, getTxNestedField).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.getTxNestedField(bytesOf(Bytes{}), out.slice()),
hfErrorToInt(HostFunctionError::LocatorMalformed));
}
// Distinct from an empty locator: `invokeWithLocator` checks the two conditions separately.
TEST_F(TxNestedFieldCall, MisalignedLocatorLengthIsRefusedWithoutAskingHost)
{
Bytes const oddLength{1, 2, 3};
EXPECT_CALL(host, getTxNestedField).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.getTxNestedField(bytesOf(oddLength), out.slice()),
hfErrorToInt(HostFunctionError::LocatorMalformed));
}
TEST_F(TxNestedFieldCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, getTxNestedField(LocatorEquals(steps)))
.WillOnce(testing::Throw(std::runtime_error{"nested field came apart"}));
OutRegion out{32};
EXPECT_EQ(
hostContext.getTxNestedField(bytesOf(locatorBytes), out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("nested field came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("getTxNestedField"));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(TxNestedFieldCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
Bytes const value{1, 2, 3};
EXPECT_CALL(host, getTxNestedField(LocatorEquals(steps))).WillOnce(testing::Return(value));
OutRegion out{value.size() - 1};
EXPECT_EQ(
hostContext.getTxNestedField(bytesOf(locatorBytes), out.slice()),
static_cast<std::int32_t>(value.size()));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(TxNestedFieldCall, OutRegionOfExactSizeIsWritten)
{
Bytes const value{1, 2, 3};
EXPECT_CALL(host, getTxNestedField(LocatorEquals(steps))).WillOnce(testing::Return(value));
OutRegion out{value.size()};
EXPECT_EQ(
hostContext.getTxNestedField(bytesOf(locatorBytes), out.slice()),
static_cast<std::int32_t>(value.size()));
EXPECT_TRUE(out.holds(bytesOf(value)));
}
TEST_F(TxNestedFieldCall, EmptyResultAnswersZeroAndWritesNothing)
{
EXPECT_CALL(host, getTxNestedField(LocatorEquals(steps))).WillOnce(testing::Return(Bytes{}));
OutRegion out{32};
EXPECT_EQ(hostContext.getTxNestedField(bytesOf(locatorBytes), out.slice()), 0);
EXPECT_FALSE(out.wasWritten());
}
} // namespace xrpl::test

View File

@@ -0,0 +1,58 @@
#include <xrpl/basics/Slice.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <tx/wasm/MockHostFunctions.h>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// The other non-`const` host method; it answers the byte count stored directly, with no out
// region.
struct UpdateDataCall : HostContextTest
{
Bytes const data{'h', 'e', 'l', 'l', 'o'};
};
TEST_F(UpdateDataCall, DataForwardedByteCountReturned)
{
EXPECT_CALL(host, updateData(BytesAre("hello"))).WillOnce(testing::Return(5));
EXPECT_EQ(hostContext.updateData(bytesOf(data)), 5);
}
TEST_F(UpdateDataCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, updateData(BytesAre("hello")))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::DataFieldTooLarge)));
EXPECT_EQ(
hostContext.updateData(bytesOf(data)), hfErrorToInt(HostFunctionError::DataFieldTooLarge));
}
TEST_F(UpdateDataCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, updateData(BytesAre("hello")))
.WillOnce(testing::Throw(std::runtime_error{"update data came apart"}));
EXPECT_EQ(
hostContext.updateData(bytesOf(data)), hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("update data came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("updateData"));
}
// An empty `rust::Slice` has a null `data()`; `updateData` forwards it as an empty `Slice`
// rather than treating it as malformed.
TEST_F(UpdateDataCall, EmptyInputRegionForwardsAsEmptySlice)
{
EXPECT_CALL(host, updateData(testing::Property(&Slice::empty, true)))
.WillOnce(testing::Return(0));
EXPECT_EQ(hostContext.updateData(bytesOf(Bytes{})), 0);
}
} // namespace xrpl::test

View File

@@ -0,0 +1,131 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/HostContextFixture.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
namespace xrpl::test {
// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
struct VaultKeyletCall : HostContextTest
{
Bytes const accountBytes{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14};
AccountID const account = AccountID::fromVoid(accountBytes.data());
std::int32_t const seq = 12345;
};
TEST_F(VaultKeyletCall, AccountAndSeqAreForwardedKeyletIsWritten)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, vaultKeylet(account, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(keylet));
OutRegion out{32};
EXPECT_EQ(
hostContext.vaultKeylet(bytesOf(accountBytes), seq, out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_TRUE(out.holds(bytesOf(keylet)));
}
TEST_F(VaultKeyletCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, vaultKeylet(account, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
OutRegion out{32};
EXPECT_EQ(
hostContext.vaultKeylet(bytesOf(accountBytes), seq, out.slice()),
hfErrorToInt(HostFunctionError::LedgerObjNotFound));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(VaultKeyletCall, ShortAccountIsRefusedWithoutAskingHost)
{
Bytes const shortAccount(AccountID::size() - 1, 0x01);
EXPECT_CALL(host, vaultKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.vaultKeylet(bytesOf(shortAccount), seq, out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(VaultKeyletCall, LongAccountIsRefusedWithoutAskingHost)
{
Bytes const longAccount(AccountID::size() + 1, 0x01);
EXPECT_CALL(host, vaultKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.vaultKeylet(bytesOf(longAccount), seq, out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(VaultKeyletCall, EmptyAccountIsRefusedWithoutAskingHost)
{
EXPECT_CALL(host, vaultKeylet).Times(0);
OutRegion out{32};
EXPECT_EQ(
hostContext.vaultKeylet(bytesOf(Bytes{}), seq, out.slice()),
hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(VaultKeyletCall, HostExceptionBecomesInternalFatalAndIsLogged)
{
EXPECT_CALL(host, vaultKeylet(account, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Throw(std::runtime_error{"vault keylet came apart"}));
OutRegion out{32};
EXPECT_EQ(
hostContext.vaultKeylet(bytesOf(accountBytes), seq, out.slice()),
hfErrorToInt(HostFunctionError::InternalFatal));
EXPECT_THAT(logged(), testing::HasSubstr("vault keylet came apart"));
EXPECT_THAT(logged(), testing::HasSubstr("vaultKeylet"));
}
// The out-region contract: write only if the whole value fits, and return the true length
// either way.
TEST_F(VaultKeyletCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, vaultKeylet(account, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(keylet));
OutRegion out{keylet.size() - 1};
EXPECT_EQ(
hostContext.vaultKeylet(bytesOf(accountBytes), seq, out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_FALSE(out.wasWritten());
}
TEST_F(VaultKeyletCall, OutRegionOfExactSizeIsWritten)
{
Bytes const keylet(32, 0xab);
EXPECT_CALL(host, vaultKeylet(account, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(keylet));
OutRegion out{keylet.size()};
EXPECT_EQ(
hostContext.vaultKeylet(bytesOf(accountBytes), seq, out.slice()),
static_cast<std::int32_t>(keylet.size()));
EXPECT_TRUE(out.holds(bytesOf(keylet)));
}
TEST_F(VaultKeyletCall, EmptyResultAnswersZeroAndWritesNothing)
{
EXPECT_CALL(host, vaultKeylet(account, static_cast<std::uint32_t>(seq)))
.WillOnce(testing::Return(Bytes{}));
OutRegion out{32};
EXPECT_EQ(hostContext.vaultKeylet(bytesOf(accountBytes), seq, out.slice()), 0);
EXPECT_FALSE(out.wasWritten());
}
} // namespace xrpl::test

View File

@@ -0,0 +1,33 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gtest/gtest.h>
#include <helpers/Account.h>
#include <tx/wasm/RealHostFixture.h>
namespace xrpl::test {
struct AccountKeyletImpl : RealHostFixture
{
};
TEST_F(AccountKeyletImpl, MatchesAccountKeyletFunction)
{
auto const owner = fund("owner");
expectKeyletMatches(makeHost()->accountKeylet(owner), keylet::account(owner.id()));
}
TEST_F(AccountKeyletImpl, NonExistentAccountStillComputesKeylet)
{
auto const nobody = Account{"nobody"};
expectKeyletMatches(makeHost()->accountKeylet(nobody), keylet::account(nobody.id()));
}
TEST_F(AccountKeyletImpl, UnsetAccountIsInvalidAccount)
{
expectError(makeHost()->accountKeylet(AccountID{}), HostFunctionError::InvalidAccount);
}
} // namespace xrpl::test

View File

@@ -0,0 +1,38 @@
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gtest/gtest.h>
#include <helpers/Account.h>
#include <tx/wasm/RealHostFixture.h>
namespace xrpl::test {
struct AmmKeyletImpl : RealHostFixture
{
};
TEST_F(AmmKeyletImpl, MatchesAmmKeyletFunction)
{
auto const owner = fund("owner");
auto usdIssue = Issue{toCurrency("USD"), owner.id()};
expectKeyletMatches(
makeHost()->ammKeylet(usdIssue, xrpIssue()), keylet::amm(xrpIssue(), usdIssue));
}
TEST_F(AmmKeyletImpl, InvalidParameters)
{
auto const owner = fund("owner");
auto baseMpt = makeMptID(1, owner.id());
auto h = makeHost();
expectError(h->ammKeylet(xrpIssue(), xrpIssue()), HostFunctionError::InvalidParams);
expectError(h->ammKeylet(xrpIssue(), baseMpt), HostFunctionError::InvalidParams);
expectError(h->ammKeylet(baseMpt, xrpIssue()), HostFunctionError::InvalidParams);
}
} // namespace xrpl::test

View File

@@ -0,0 +1,15 @@
#include <gtest/gtest.h>
#include <tx/wasm/RealHostFixture.h>
namespace xrpl::test {
struct BaseFeeImpl : RealHostFixture
{
};
TEST_F(BaseFeeImpl, MatchesLedger)
{
expectValue(makeHost()->getBaseFee(), ledger.getOpenLedger().fees().base.drops());
}
} // namespace xrpl::test

View File

@@ -0,0 +1,98 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gtest/gtest.h>
#include <helpers/Account.h>
#include <helpers/TxTest.h>
#include <tx/wasm/RealHostFixture.h>
#include <cstdint>
namespace xrpl::test {
struct CacheLedgerObjImpl : RealHostFixture
{
void
runMatchesLedger(bool implicit)
{
auto const owner = Account{"owner"};
ledger.createAccount(owner, XRP(1000));
auto h = makeHost();
auto const key = keylet::account(owner.id()).key;
for (auto i = int32_t{1}; i < 257; ++i)
{
auto const slot = h->cacheLedgerObj(key, implicit ? 0 : i);
ASSERT_TRUE(slot.has_value()) << "cacheLedgerObj should find the created account";
EXPECT_EQ(*slot, i);
auto const account = h->getLedgerObjField(*slot, sfAccount);
ASSERT_TRUE(account.has_value());
Bytes const ownerBytes{owner.id().begin(), owner.id().end()};
EXPECT_EQ(*account, ownerBytes);
auto const sle = ledger.getOpenLedger().read(keylet::account(owner.id()));
ASSERT_NE(sle, nullptr);
auto const& ledgerAccount = sle->getAccountID(sfAccount);
EXPECT_EQ(*account, (Bytes{ledgerAccount.begin(), ledgerAccount.end()}));
}
// Every slot is now occupied, so asking to auto-allocate (cacheIdx == 0) has nowhere
// to put the object.
auto const result = h->cacheLedgerObj(key, 0);
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(), HostFunctionError::SlotsFull);
}
};
TEST_F(CacheLedgerObjImpl, MatchesLedgerExplicitIndices)
{
runMatchesLedger(false);
}
TEST_F(CacheLedgerObjImpl, MatchesLedgerImplicitIndices)
{
runMatchesLedger(true);
}
TEST_F(CacheLedgerObjImpl, OutOfRange)
{
auto h = makeHost();
auto result = h->cacheLedgerObj(uint256{}, -1);
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(), HostFunctionError::SlotOutRange);
result = h->cacheLedgerObj(uint256{}, 257);
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(), HostFunctionError::SlotOutRange);
}
TEST_F(CacheLedgerObjImpl, LedgerObjNotFound)
{
auto const ghost = keylet::account(Account{"ghost"}.id()).key;
auto result = makeHost()->cacheLedgerObj(ghost, 0);
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(), HostFunctionError::LedgerObjNotFound);
}
// Two hosts built from the same fixture are fully independent: each owns its own slot
// table, so caching into one leaves the other's slots empty. (This is what the `WasmHost`
// handle buys over the old shared-fixture-state design.)
TEST_F(CacheLedgerObjImpl, IndependentHostsDoNotShareSlots)
{
auto const owner = fund("owner");
auto const key = keylet::account(owner.id()).key;
auto a = makeHost();
auto b = makeHost();
ASSERT_TRUE(a->cacheLedgerObj(key, 1).has_value());
expectValue(a->getLedgerObjField(1, sfAccount), toBytes(owner.id()));
// `b` never cached anything, so its slot 1 is still empty.
expectError(b->getLedgerObjField(1, sfAccount), HostFunctionError::EmptySlot);
}
} // namespace xrpl::test

View File

@@ -0,0 +1,30 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/SeqProxy.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gtest/gtest.h>
#include <helpers/Account.h>
#include <tx/wasm/RealHostFixture.h>
namespace xrpl::test {
struct CheckKeyletImpl : RealHostFixture
{
};
TEST_F(CheckKeyletImpl, MatchesCheckKeyletFunction)
{
auto const owner = fund("owner");
expectKeyletMatches(
makeHost()->checkKeylet(owner.id(), 1u),
keylet::check(owner.id(), SeqProxy::rawSequence(1u)));
}
TEST_F(CheckKeyletImpl, UnsetAccountIsInvalidAccount)
{
expectError(makeHost()->checkKeylet(AccountID{}, 1u), HostFunctionError::InvalidAccount);
}
} // namespace xrpl::test

View File

@@ -0,0 +1,80 @@
#include <xrpl/protocol/KeyType.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Seed.h>
#include <gtest/gtest.h>
#include <tx/wasm/RealHostFixture.h>
#include <cstdint>
namespace xrpl::test {
struct CheckSignatureImpl : RealHostFixture
{
};
TEST_F(CheckSignatureImpl, ValidSignature)
{
auto const kp = generateKeyPair(KeyType::Secp256k1, randomSeed());
auto const& pk = kp.first;
auto const& sk = kp.second;
auto const& message = std::string{"hello signature"};
auto const sig = sign(pk, sk, Slice(message.data(), message.size()));
auto const result = makeHost()->checkSignature(
Slice{message.data(), message.size()}, Slice{sig.data(), sig.size()}, pk);
expectValue(result, std::int32_t{1});
}
TEST_F(CheckSignatureImpl, InvalidSignature)
{
auto const kp = generateKeyPair(KeyType::Secp256k1, randomSeed());
auto const& pk = kp.first;
auto const& sk = kp.second;
auto const& message = std::string{"hello signature"};
auto const sig = sign(pk, sk, Slice(message.data(), message.size()));
auto const badSignature = std::string(sig.size(), 0xFF);
auto const result = makeHost()->checkSignature(
Slice{message.data(), message.size()}, Slice{badSignature.data(), badSignature.size()}, pk);
expectValue(result, std::int32_t{0});
}
TEST_F(CheckSignatureImpl, InvalidPublicKey)
{
auto const kp = generateKeyPair(KeyType::Secp256k1, randomSeed());
auto const kp2 = generateKeyPair(KeyType::Secp256k1, randomSeed());
auto const& pk = kp.first;
auto const& sk = kp.second;
auto const& message = std::string{"hello signature"};
auto const sig = sign(pk, sk, Slice(message.data(), message.size()));
auto const result = makeHost()->checkSignature(
Slice{message.data(), message.size()}, Slice{sig.data(), sig.size()}, kp2.first);
expectValue(result, std::int32_t{0});
}
TEST_F(CheckSignatureImpl, EmptySignature)
{
auto const kp = generateKeyPair(KeyType::Secp256k1, randomSeed());
auto const& pk = kp.first;
auto const& message = std::string{"hello signature"};
auto const result =
makeHost()->checkSignature(Slice{message.data(), message.size()}, Slice{}, pk);
expectValue(result, std::int32_t{0});
}
TEST_F(CheckSignatureImpl, EmptyMessage)
{
auto const kp = generateKeyPair(KeyType::Secp256k1, randomSeed());
auto const& pk = kp.first;
auto const& sk = kp.second;
auto const& message = std::string{"hello signature"};
auto const sig = sign(pk, sk, Slice(message.data(), message.size()));
auto const result = makeHost()->checkSignature(Slice{}, Slice{sig.data(), sig.size()}, pk);
expectValue(result, std::int32_t{0});
}
} // namespace xrpl::test

View File

@@ -0,0 +1,59 @@
#include <xrpl/basics/Slice.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gtest/gtest.h>
#include <helpers/Account.h>
#include <tx/wasm/RealHostFixture.h>
namespace xrpl::test {
struct CredentialKeyletImpl : RealHostFixture
{
};
TEST_F(CredentialKeyletImpl, MatchesCredentialKeyletFunction)
{
auto const owner = fund("owner");
auto const credTypeStr = std::string{"test"};
auto const credType = Slice{credTypeStr.data(), credTypeStr.size()};
expectKeyletMatches(
makeHost()->credentialKeylet(owner.id(), owner.id(), credType),
keylet::credential(owner.id(), owner.id(), credType));
}
TEST_F(CredentialKeyletImpl, CredentialTypeStringTooLong)
{
auto const owner = fund("owner");
auto constexpr credTypeStr = std::string_view{
"abcdefghijklmnopqrstuvwxyz01234567890qwertyuiop[]"
"asdfghjkl;'zxcvbnm8237tr28weufwldebvfv8734t07p"};
static_assert(credTypeStr.size() > kMaxCredentialTypeLength);
auto const credType = Slice{credTypeStr.data(), credTypeStr.size()};
expectError(
makeHost()->credentialKeylet(owner.id(), owner.id(), credType),
HostFunctionError::InvalidParams);
}
TEST_F(CredentialKeyletImpl, InvalidAccount)
{
auto const owner = fund("owner");
auto const credTypeStr = std::string{"test"};
auto const credType = Slice{credTypeStr.data(), credTypeStr.size()};
auto h = makeHost();
expectError(
h->credentialKeylet(AccountID{}, owner.id(), credType), HostFunctionError::InvalidAccount);
expectError(
h->credentialKeylet(owner.id(), AccountID{}, credType), HostFunctionError::InvalidAccount);
}
} // namespace xrpl::test

View File

@@ -0,0 +1,56 @@
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gtest/gtest.h>
#include <helpers/Account.h>
#include <tx/wasm/RealHostFixture.h>
#include <utility>
namespace xrpl::test {
struct CurrentLedgerObjArrayLenImpl : RealHostFixture
{
using RealHostFixture::makeHost;
WasmHost
makeHost(Account const& acct)
{
makeSignerList(acct, 2, {{Account{"alice"}, 1}, {Account{"becky"}, 1}});
auto assembler = bareTx();
return makeHost(keylet::signerList(acct.id()), assembler.type, std::move(assembler.build));
}
};
TEST_F(CurrentLedgerObjArrayLenImpl, SignerEntriesLength)
{
auto const owner = fund("owner");
auto h = makeHost(owner);
expectValue(h->getCurrentLedgerObjArrayLen(sfSignerEntries), 2);
}
TEST_F(CurrentLedgerObjArrayLenImpl, NonArrayFieldNoArray)
{
auto const owner = fund("owner");
auto h = makeHost(owner);
expectError(h->getCurrentLedgerObjArrayLen(sfAccount), HostFunctionError::NoArray);
}
TEST_F(CurrentLedgerObjArrayLenImpl, MissingArrayFieldNotFound)
{
auto const owner = fund("owner");
auto h = makeHost(owner);
expectError(h->getCurrentLedgerObjArrayLen(sfMemos), HostFunctionError::FieldNotFound);
}
TEST_F(CurrentLedgerObjArrayLenImpl, MissingCurrentObjectNotFound)
{
auto const owner = fund("owner");
auto assembler = bareTx();
auto h = makeHost(keylet::signerList(owner.id()), assembler.type, std::move(assembler.build));
expectError(
h->getCurrentLedgerObjArrayLen(sfSignerEntries), HostFunctionError::LedgerObjNotFound);
}
} // namespace xrpl::test

View File

@@ -0,0 +1,94 @@
#include <xrpl/basics/base_uint.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/SeqProxy.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol_autogen/transactions/EscrowCreate.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gtest/gtest.h>
#include <helpers/Account.h>
#include <helpers/TxTest.h>
#include <tx/wasm/RealHostFixture.h>
namespace xrpl::test {
struct CurrentLedgerObjFieldImpl : RealHostFixture
{
// Create an escrow owned by `owner` and return its keylet (the object the host will
// read as its "current" object).
Keylet
makeEscrow(Account const& owner, Account const& dest, uint256* transactionId = nullptr)
{
ledger.createAccount(owner, XRP(1000));
ledger.createAccount(dest, XRP(1000));
auto const ownerSeq = ledger.getAccountRoot(owner.id()).getSequence();
// A finish time comfortably after the genesis close time.
auto const r = ledger.submit(
transactions::EscrowCreateBuilder{owner.id(), dest.id(), XRP(100)}.setFinishAfter(
900'000'000),
owner);
EXPECT_EQ(r.ter, tesSUCCESS) << transToken(r.ter);
if (transactionId != nullptr)
{
*transactionId = r.tx->getTransactionID();
}
ledger.close();
return keylet::escrow(owner.id(), SeqProxy::rawSequence(ownerSeq));
}
};
TEST_F(CurrentLedgerObjFieldImpl, ReadsfAccount)
{
auto const owner = Account{"owner"};
auto const escrow = makeEscrow(owner, Account{"dest"});
ASSERT_NE(ledger.getOpenLedger().read(escrow), nullptr) << "escrow object should exist";
expectValue(makeHost(escrow)->getCurrentLedgerObjField(sfAccount), toBytes(owner.id()));
}
TEST_F(CurrentLedgerObjFieldImpl, ReadsfAccountDummyEscrow)
{
auto const owner = Account{"owner"};
ledger.createAccount(owner, XRP(1000));
auto const ownerSeq = ledger.getAccountRoot(owner.id()).getSequence();
auto const escrow = keylet::escrow(owner.id(), SeqProxy::rawSequence(ownerSeq));
expectError(
makeHost(escrow)->getCurrentLedgerObjField(sfAccount),
HostFunctionError::LedgerObjNotFound);
}
TEST_F(CurrentLedgerObjFieldImpl, ReadAmount)
{
auto const owner = Account{"owner"};
auto const escrow = makeEscrow(owner, Account{"dest"});
ASSERT_NE(ledger.getOpenLedger().read(escrow), nullptr) << "escrow object should exist";
expectValue(makeHost(escrow)->getCurrentLedgerObjField(sfAmount), toBytes(XRP(100)));
}
TEST_F(CurrentLedgerObjFieldImpl, ReadPreviousTxnID)
{
auto const owner = Account{"owner"};
auto transactionId = uint256{};
auto const escrow = makeEscrow(owner, Account{"dest"}, &transactionId);
ASSERT_NE(ledger.getOpenLedger().read(escrow), nullptr) << "escrow object should exist";
expectValue(
makeHost(escrow)->getCurrentLedgerObjField(sfPreviousTxnID), toBytes(transactionId));
}
TEST_F(CurrentLedgerObjFieldImpl, ReadOwner)
{
auto const owner = Account{"owner"};
auto const escrow = makeEscrow(owner, Account{"dest"});
ASSERT_NE(ledger.getOpenLedger().read(escrow), nullptr) << "escrow object should exist";
expectError(
makeHost(escrow)->getCurrentLedgerObjField(sfOwner), HostFunctionError::FieldNotFound);
}
} // namespace xrpl::test

View File

@@ -0,0 +1,61 @@
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gtest/gtest.h>
#include <helpers/Account.h>
#include <tx/wasm/RealHostFixture.h>
#include <utility>
namespace xrpl::test {
struct CurrentLedgerObjNestedArrayLenImpl : RealHostFixture
{
using RealHostFixture::makeHost;
WasmHost
makeHost(Account const& acct)
{
makeSignerList(acct, 2, {{Account{"alice"}, 1}, {Account{"becky"}, 1}});
auto assembler = bareTx();
return makeHost(keylet::signerList(acct.id()), assembler.type, std::move(assembler.build));
}
};
TEST_F(CurrentLedgerObjNestedArrayLenImpl, SignerEntriesLength)
{
auto const owner = fund("owner");
auto h = makeHost(owner);
expectValue(h->getCurrentLedgerObjNestedArrayLen(FieldLocator{{sfSignerEntries.getCode()}}), 2);
}
TEST_F(CurrentLedgerObjNestedArrayLenImpl, NonArrayFieldNoArray)
{
auto const owner = fund("owner");
auto h = makeHost(owner);
expectError(
h->getCurrentLedgerObjNestedArrayLen(FieldLocator{{sfSignerQuorum.getCode()}}),
HostFunctionError::NoArray);
}
TEST_F(CurrentLedgerObjNestedArrayLenImpl, MissingFieldNotFound)
{
auto const owner = fund("owner");
auto h = makeHost(owner);
expectError(
h->getCurrentLedgerObjNestedArrayLen(FieldLocator{{sfSigners.getCode()}}),
HostFunctionError::FieldNotFound);
}
TEST_F(CurrentLedgerObjNestedArrayLenImpl, MissingCurrentObjectNotFound)
{
auto const owner = fund("owner");
auto assembler = bareTx();
auto h = makeHost(keylet::signerList(owner.id()), assembler.type, std::move(assembler.build));
expectError(
h->getCurrentLedgerObjNestedArrayLen(FieldLocator{{sfSignerEntries.getCode()}}),
HostFunctionError::LedgerObjNotFound);
}
} // namespace xrpl::test

View File

@@ -0,0 +1,122 @@
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gtest/gtest.h>
#include <helpers/Account.h>
#include <tx/wasm/RealHostFixture.h>
#include <cstdint>
#include <utility>
namespace xrpl::test {
struct CurrentLedgerObjNestedFieldImpl : RealHostFixture
{
using RealHostFixture::makeHost;
WasmHost
makeHost(Account const& acct)
{
makeSignerList(acct, 2, {{Account{"alice"}, 1}, {Account{"becky"}, 1}});
auto assembler = bareTx();
return makeHost(keylet::signerList(acct.id()), assembler.type, std::move(assembler.build));
}
};
TEST_F(CurrentLedgerObjNestedFieldImpl, MatchesNestedSignerQuorum)
{
auto const owner = fund("owner");
auto h = makeHost(owner);
expectValue(
h->getCurrentLedgerObjNestedField(FieldLocator{{sfSignerQuorum.getCode()}}),
toBytes(static_cast<std::uint32_t>(2)));
}
TEST_F(CurrentLedgerObjNestedFieldImpl, MatchesNestedSignerWeight)
{
auto const owner = fund("owner");
auto h = makeHost(owner);
expectValue(
h->getCurrentLedgerObjNestedField(
FieldLocator{{sfSignerEntries.getCode(), 0, sfSignerWeight.getCode()}}),
toBytes(static_cast<std::uint16_t>(1)));
}
TEST_F(CurrentLedgerObjNestedFieldImpl, MatchesNestedSignerAccount)
{
auto const owner = fund("owner");
auto h = makeHost(owner);
auto const sle = ledger.getOpenLedger().read(keylet::signerList(owner.id()));
ASSERT_NE(sle, nullptr);
auto const& entry0 = sle->getFieldArray(sfSignerEntries)[0];
expectValue(
h->getCurrentLedgerObjNestedField(
FieldLocator{{sfSignerEntries.getCode(), 0, sfAccount.getCode()}}),
toBytes(entry0.getAccountID(sfAccount)));
}
TEST_F(CurrentLedgerObjNestedFieldImpl, MissingFieldNotFound)
{
auto const owner = fund("owner");
auto h = makeHost(owner);
expectError(
h->getCurrentLedgerObjNestedField(
FieldLocator{{sfSigners.getCode(), 0, sfAccount.getCode()}}),
HostFunctionError::FieldNotFound);
}
TEST_F(CurrentLedgerObjNestedFieldImpl, IndexOutOfBounds)
{
auto const owner = fund("owner");
auto h = makeHost(owner);
auto const err = HostFunctionError::IndexOutOfBounds;
expectError(
h->getCurrentLedgerObjNestedField(
FieldLocator{{sfSignerEntries.getCode(), 2, sfAccount.getCode()}}),
err);
expectError(
h->getCurrentLedgerObjNestedField(
FieldLocator{{sfSignerEntries.getCode(), -1, sfAccount.getCode()}}),
err);
}
TEST_F(CurrentLedgerObjNestedFieldImpl, UnknownFieldCodeInvalidField)
{
auto const owner = fund("owner");
auto h = makeHost(owner);
auto const err = HostFunctionError::InvalidField;
expectError(h->getCurrentLedgerObjNestedField(FieldLocator{{fieldCode(20000, 20000)}}), err);
expectError(
h->getCurrentLedgerObjNestedField(
FieldLocator{{sfSignerEntries.getCode(), 0, fieldCode(20000, 20000)}}),
err);
}
TEST_F(CurrentLedgerObjNestedFieldImpl, NestIntoNonContainerMalformed)
{
auto const owner = fund("owner");
auto h = makeHost(owner);
expectError(
h->getCurrentLedgerObjNestedField(
FieldLocator{{sfSignerQuorum.getCode(), 0, sfAccount.getCode()}}),
HostFunctionError::LocatorMalformed);
}
TEST_F(CurrentLedgerObjNestedFieldImpl, MissingCurrentObjectNotFound)
{
auto const owner = fund("owner");
auto assembler = bareTx();
auto h = makeHost(keylet::signerList(owner.id()), assembler.type, std::move(assembler.build));
expectError(
h->getCurrentLedgerObjNestedField(FieldLocator{{sfSignerQuorum.getCode()}}),
HostFunctionError::LedgerObjNotFound);
}
} // namespace xrpl::test

View File

@@ -0,0 +1,42 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gtest/gtest.h>
#include <helpers/Account.h>
#include <tx/wasm/RealHostFixture.h>
namespace xrpl::test {
struct DelegateKeyletImpl : RealHostFixture
{
};
TEST_F(DelegateKeyletImpl, MatchesDelegateKeyletFunction)
{
auto const owner = fund("owner");
auto const delegate = fund("delegate");
expectKeyletMatches(
makeHost()->delegateKeylet(owner.id(), delegate.id()),
keylet::delegate(owner.id(), delegate.id()));
}
TEST_F(DelegateKeyletImpl, CantDelegateToSelf)
{
auto const owner = fund("owner");
expectError(
makeHost()->delegateKeylet(owner.id(), owner.id()), HostFunctionError::InvalidParams);
}
TEST_F(DelegateKeyletImpl, InvalidAccount)
{
auto const owner = fund("owner");
auto h = makeHost();
expectError(h->delegateKeylet(AccountID{}, owner.id()), HostFunctionError::InvalidAccount);
expectError(h->delegateKeylet(owner.id(), AccountID{}), HostFunctionError::InvalidAccount);
}
} // namespace xrpl::test

View File

@@ -0,0 +1,44 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gtest/gtest.h>
#include <helpers/Account.h>
#include <tx/wasm/RealHostFixture.h>
namespace xrpl::test {
struct DepositPreauthKeyletImpl : RealHostFixture
{
};
TEST_F(DepositPreauthKeyletImpl, MatchesDepositPreauthKeyletFunction)
{
auto const owner = fund("owner");
auto const destination = fund("destination");
expectKeyletMatches(
makeHost()->depositPreauthKeylet(owner.id(), destination.id()),
keylet::depositPreauth(owner.id(), destination.id()));
}
TEST_F(DepositPreauthKeyletImpl, CantPreauthToSelf)
{
auto const owner = fund("owner");
expectError(
makeHost()->depositPreauthKeylet(owner.id(), owner.id()), HostFunctionError::InvalidParams);
}
TEST_F(DepositPreauthKeyletImpl, InvalidAccount)
{
auto const owner = fund("owner");
auto h = makeHost();
expectError(
h->depositPreauthKeylet(AccountID{}, owner.id()), HostFunctionError::InvalidAccount);
expectError(
h->depositPreauthKeylet(owner.id(), AccountID{}), HostFunctionError::InvalidAccount);
}
} // namespace xrpl::test

View File

@@ -0,0 +1,27 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gtest/gtest.h>
#include <helpers/Account.h>
#include <tx/wasm/RealHostFixture.h>
namespace xrpl::test {
struct DidKeyletImpl : RealHostFixture
{
};
TEST_F(DidKeyletImpl, MatchesDidKeyletFunction)
{
auto const owner = fund("owner");
expectKeyletMatches(makeHost()->didKeylet(owner.id()), keylet::did(owner.id()));
}
TEST_F(DidKeyletImpl, InvalidAccount)
{
expectError(makeHost()->didKeylet(AccountID{}), HostFunctionError::InvalidAccount);
}
} // namespace xrpl::test

View File

@@ -0,0 +1,43 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/SeqProxy.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gtest/gtest.h>
#include <helpers/Account.h>
#include <tx/wasm/RealHostFixture.h>
#include <cstdint>
namespace xrpl::test {
struct EscrowKeyletImpl : RealHostFixture
{
};
TEST_F(EscrowKeyletImpl, MatchesLedgerKeyletFunction)
{
auto const owner = Account{"owner"};
auto const seq = std::uint32_t{42};
expectKeyletMatches(
makeHost()->escrowKeylet(owner.id(), seq),
keylet::escrow(owner.id(), SeqProxy::rawSequence(seq)));
}
TEST_F(EscrowKeyletImpl, DifferentAccountsGiveDifferentKeylets)
{
auto h = makeHost();
auto const a = h->escrowKeylet(Account{"alice"}.id(), 7);
auto const b = h->escrowKeylet(Account{"becky"}.id(), 7);
ASSERT_TRUE(a.has_value() && b.has_value());
EXPECT_NE(*a, *b);
}
TEST_F(EscrowKeyletImpl, UnsetAccountIsInvalidAccount)
{
expectError(makeHost()->escrowKeylet(AccountID{}, 1), HostFunctionError::InvalidAccount);
}
} // namespace xrpl::test

View File

@@ -0,0 +1,49 @@
#include <xrpl/basics/Slice.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gtest/gtest.h>
#include <tx/wasm/FloatFixture.h>
#include <tx/wasm/RealHostFixture.h>
namespace xrpl::test {
struct FloatAddImpl : FloatTest
{
};
TEST_F(FloatAddImpl, BadModeIsMalformed)
{
expectError(
makeHost()->floatAdd(slice(FloatTest::kOne), slice(FloatTest::kOne), -1),
HostFunctionError::FloatInputMalformed);
}
TEST_F(FloatAddImpl, MalformedInput)
{
expectError(
makeHost()->floatAdd(slice(FloatTest::kOne), Slice{}, 0),
HostFunctionError::FloatInputMalformed);
}
TEST_F(FloatAddImpl, MaxIouPlusMaxExpIsMaxExp)
{
expectValue(
makeHost()->floatAdd(slice(FloatTest::kMaxIOU), slice(FloatTest::kMaxExp), 0),
FloatTest::kMaxExp);
}
TEST_F(FloatAddImpl, MinPlusZeroIsMin)
{
expectValue(
makeHost()->floatAdd(slice(FloatTest::kIntMin), slice(FloatTest::kIntZero), 0),
FloatTest::kIntMin);
}
TEST_F(FloatAddImpl, MaxPlusMinIsZero)
{
expectValue(
makeHost()->floatAdd(slice(FloatTest::kIntMax), slice(FloatTest::kIntMin), 0),
FloatTest::kIntZero);
}
} // namespace xrpl::test

View File

@@ -0,0 +1,48 @@
#include <xrpl/basics/Slice.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gtest/gtest.h>
#include <tx/wasm/FloatFixture.h>
#include <tx/wasm/RealHostFixture.h>
namespace xrpl::test {
struct FloatCompareImpl : FloatTest
{
};
TEST_F(FloatCompareImpl, MalformedInputs)
{
// A wrong-size (here empty) buffer is malformed; the impl normalizes any well-formed
// 12-byte buffer, so size is the only rejection.
auto h = makeHost();
expectError(h->floatCompare(Slice{}, Slice{}), HostFunctionError::FloatInputMalformed);
expectError(
h->floatCompare(slice(FloatTest::kOne), Slice{}), HostFunctionError::FloatInputMalformed);
}
TEST_F(FloatCompareImpl, Less)
{
expectValue(makeHost()->floatCompare(slice(FloatTest::kIntMin), slice(FloatTest::kIntZero)), 2);
}
TEST_F(FloatCompareImpl, Greater)
{
expectValue(makeHost()->floatCompare(slice(FloatTest::kIntMax), slice(FloatTest::kIntZero)), 1);
}
TEST_F(FloatCompareImpl, Equal)
{
expectValue(makeHost()->floatCompare(slice(FloatTest::kOne), slice(FloatTest::kOne)), 0);
}
// A non-canonical encoding of 10 (mantissa 100000, exponent -4) is normalized on decode, so
// it compares equal to the canonical 10 — the impl accepts any well-formed 12-byte buffer.
TEST_F(FloatCompareImpl, NonCanonicalNormalizes)
{
Bytes const nonCanonicalTen{
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x86, 0xA0, 0xFF, 0xFF, 0xFF, 0xFC};
expectValue(makeHost()->floatCompare(slice(nonCanonicalTen), slice(FloatTest::kTen)), 0);
}
} // namespace xrpl::test

View File

@@ -0,0 +1,73 @@
#include <xrpl/basics/Slice.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gtest/gtest.h>
#include <tx/wasm/FloatFixture.h>
#include <tx/wasm/RealHostFixture.h>
namespace xrpl::test {
struct FloatDivideImpl : FloatTest
{
};
TEST_F(FloatDivideImpl, BadModeIsMalformed)
{
expectError(
makeHost()->floatDivide(slice(FloatTest::kOne), slice(FloatTest::kOne), -1),
HostFunctionError::FloatInputMalformed);
}
TEST_F(FloatDivideImpl, MalformedInput)
{
expectError(
makeHost()->floatDivide(slice(FloatTest::kOne), Slice{}, 0),
HostFunctionError::FloatInputMalformed);
}
TEST_F(FloatDivideImpl, DivideByZeroIsComputationError)
{
expectError(
makeHost()->floatDivide(slice(FloatTest::kOne), slice(FloatTest::kIntZero), 0),
HostFunctionError::FloatComputationError);
}
TEST_F(FloatDivideImpl, OverflowIsComputationError)
{
// A divisor just below 1, so max / it overflows.
auto h = makeHost();
auto const y = h->floatFromMantExp(STAmount::kMaxValue, -FloatTest::kNormalExp - 1, 0);
ASSERT_TRUE(y.has_value());
expectError(
h->floatDivide(slice(FloatTest::kMax), slice(*y), 0),
HostFunctionError::FloatComputationError);
}
TEST_F(FloatDivideImpl, ZeroDividedByOneIsZero)
{
expectValue(
makeHost()->floatDivide(slice(FloatTest::kIntZero), slice(FloatTest::kOne), 0),
FloatTest::kIntZero);
}
TEST_F(FloatDivideImpl, MaxExpDividedByTenIsPreMaxExp)
{
expectValue(
makeHost()->floatDivide(slice(FloatTest::kMaxExp), slice(FloatTest::kTen), 0),
FloatTest::kPreMaxExp);
}
// The rounding mode changes an inexact result: 1/3 rounded Downward differs from Upward.
TEST_F(FloatDivideImpl, RoundingModeAffectsInexactResult)
{
auto h = makeHost();
auto const three = h->floatFromInt(3, 0);
ASSERT_TRUE(three.has_value());
auto const down = h->floatDivide(slice(FloatTest::kOne), slice(*three), 2);
auto const up = h->floatDivide(slice(FloatTest::kOne), slice(*three), 3);
ASSERT_TRUE(down.has_value() && up.has_value());
EXPECT_NE(*down, *up);
}
} // namespace xrpl::test

View File

@@ -0,0 +1,35 @@
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gtest/gtest.h>
#include <tx/wasm/FloatFixture.h>
#include <tx/wasm/RealHostFixture.h>
namespace xrpl::test {
struct FloatFromIntImpl : FloatTest
{
};
TEST_F(FloatFromIntImpl, BadModeIsMalformed)
{
auto h = makeHost();
expectError(h->floatFromInt(kMin64, -1), HostFunctionError::FloatInputMalformed);
expectError(h->floatFromInt(kMin64, 4), HostFunctionError::FloatInputMalformed);
}
TEST_F(FloatFromIntImpl, MinInt)
{
expectValue(makeHost()->floatFromInt(kMin64, 0), FloatTest::kIntMin);
}
TEST_F(FloatFromIntImpl, Zero)
{
expectValue(makeHost()->floatFromInt(0, 0), FloatTest::kIntZero);
}
TEST_F(FloatFromIntImpl, MaxInt)
{
expectValue(makeHost()->floatFromInt(kMax64, 0), FloatTest::kIntMax);
}
} // namespace xrpl::test

View File

@@ -0,0 +1,69 @@
#include <xrpl/basics/Number.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gtest/gtest.h>
#include <tx/wasm/FloatFixture.h>
#include <tx/wasm/RealHostFixture.h>
namespace xrpl::test {
struct FloatFromMantExpImpl : FloatTest
{
static constexpr int kMaxRawExp = Number::kMaxExponent + FloatTest::kNormalExp;
static constexpr int kMinRawExp = Number::kMinExponent + FloatTest::kNormalExp;
};
TEST_F(FloatFromMantExpImpl, BadModeIsMalformed)
{
auto h = makeHost();
expectError(h->floatFromMantExp(1, 0, -1), HostFunctionError::FloatInputMalformed);
expectError(h->floatFromMantExp(1, 0, 4), HostFunctionError::FloatInputMalformed);
}
TEST_F(FloatFromMantExpImpl, ExponentTooHighIsMalformed)
{
expectError(
makeHost()->floatFromMantExp(1, kMaxRawExp + 1, 0), HostFunctionError::FloatInputMalformed);
}
TEST_F(FloatFromMantExpImpl, UnderflowIsZero)
{
expectValue(makeHost()->floatFromMantExp(1, kMinRawExp - 1, 0), FloatTest::kIntZero);
}
TEST_F(FloatFromMantExpImpl, MaxExponent)
{
expectValue(makeHost()->floatFromMantExp(1, kMaxRawExp, 0), FloatTest::kMaxExp);
}
TEST_F(FloatFromMantExpImpl, MinusMaxExponent)
{
expectValue(makeHost()->floatFromMantExp(-1, kMaxRawExp, 0), FloatTest::kMinusMaxExp);
}
TEST_F(FloatFromMantExpImpl, PreMaxExponent)
{
expectValue(makeHost()->floatFromMantExp(1, kMaxRawExp - 1, 0), FloatTest::kPreMaxExp);
}
TEST_F(FloatFromMantExpImpl, MaxIou)
{
expectValue(
makeHost()->floatFromMantExp(STAmount::kMaxValue, STAmount::kMaxOffset, 0),
FloatTest::kMaxIOU);
}
TEST_F(FloatFromMantExpImpl, MinExponent)
{
expectValue(
makeHost()->floatFromMantExp(1, Number::kMinExponent - FloatTest::kNormalExp, 0),
FloatTest::kMinExp);
}
TEST_F(FloatFromMantExpImpl, TenTimesTenthIsOne)
{
expectValue(makeHost()->floatFromMantExp(10, -1, 0), FloatTest::kOne);
}
} // namespace xrpl::test

View File

@@ -0,0 +1,71 @@
#include <xrpl/protocol/IOUAmount.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gtest/gtest.h>
#include <helpers/Account.h>
#include <helpers/TxTest.h>
#include <tx/wasm/FloatFixture.h>
#include <tx/wasm/RealHostFixture.h>
#include <cstdint>
namespace xrpl::test {
struct FloatFromStAmountImpl : FloatTest
{
static Issue
usd()
{
return Issue{toCurrency("USD"), Account{"gw"}.id()};
}
};
TEST_F(FloatFromStAmountImpl, BadModeIsMalformed)
{
auto h = makeHost();
auto const amount = STAmount{XRP(100)};
expectError(h->floatFromSTAmount(amount, -1), HostFunctionError::FloatInputMalformed);
expectError(h->floatFromSTAmount(amount, 4), HostFunctionError::FloatInputMalformed);
}
TEST_F(FloatFromStAmountImpl, ZeroXrp)
{
expectValue(makeHost()->floatFromSTAmount(STAmount{XRP(0)}, 0), FloatTest::kIntZero);
}
TEST_F(FloatFromStAmountImpl, MinusOneXrp)
{
// -1 XRP == -1'000'000 drops.
auto h = makeHost();
auto const expected = h->floatFromMantExp(-1'000'000, 0, 0);
ASSERT_TRUE(expected.has_value());
expectValue(h->floatFromSTAmount(STAmount{XRP(-1)}, 0), *expected);
}
TEST_F(FloatFromStAmountImpl, MaxDrops)
{
auto h = makeHost();
static constexpr int64_t kTestValue{9'223'372'036'854'776};
auto const expected = h->floatFromMantExp(kTestValue, 3, 0);
ASSERT_TRUE(expected.has_value());
expectValue(h->floatFromSTAmount(STAmount{noIssue(), kMax64}, 0), *expected);
}
TEST_F(FloatFromStAmountImpl, MinIou)
{
auto const amount = STAmount{
IOUAmount{static_cast<std::int64_t>(STAmount::kMinValue), STAmount::kMinOffset}, usd()};
expectValue(makeHost()->floatFromSTAmount(amount, 0), FloatTest::kMinIOU);
}
TEST_F(FloatFromStAmountImpl, MaxIou)
{
auto const amount = STAmount{
IOUAmount{static_cast<std::int64_t>(STAmount::kMaxValue), STAmount::kMaxOffset}, usd()};
expectValue(makeHost()->floatFromSTAmount(amount, 0), FloatTest::kMaxIOU);
}
} // namespace xrpl::test

View File

@@ -0,0 +1,40 @@
#include <xrpl/basics/Number.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STNumber.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gtest/gtest.h>
#include <tx/wasm/FloatFixture.h>
#include <tx/wasm/RealHostFixture.h>
#include <cstdint>
#include <limits>
namespace xrpl::test {
struct FloatFromStNumberImpl : FloatTest
{
};
TEST_F(FloatFromStNumberImpl, BadModeIsMalformed)
{
auto h = makeHost();
auto const n = STNumber{sfNumber, Number(123, 0)};
expectError(h->floatFromSTNumber(n, -1), HostFunctionError::FloatInputMalformed);
expectError(h->floatFromSTNumber(n, 4), HostFunctionError::FloatInputMalformed);
}
TEST_F(FloatFromStNumberImpl, MaxUint)
{
auto const n = STNumber{
sfNumber, Number(std::numeric_limits<std::uint64_t>::max(), 0, Number::Normalized{})};
expectValue(makeHost()->floatFromSTNumber(n, 0), FloatTest::kUintMax);
}
TEST_F(FloatFromStNumberImpl, MinusMaxExponent)
{
auto const n = STNumber{sfNumber, Number(-1, Number::kMaxExponent + FloatTest::kNormalExp)};
expectValue(makeHost()->floatFromSTNumber(n, 0), FloatTest::kMinusMaxExp);
}
} // namespace xrpl::test

View File

@@ -0,0 +1,34 @@
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gtest/gtest.h>
#include <tx/wasm/FloatFixture.h>
#include <tx/wasm/RealHostFixture.h>
#include <cstdint>
#include <limits>
namespace xrpl::test {
struct FloatFromUintImpl : FloatTest
{
static constexpr std::uint64_t kMaxU64 = std::numeric_limits<std::uint64_t>::max();
};
TEST_F(FloatFromUintImpl, BadModeIsMalformed)
{
auto h = makeHost();
expectError(h->floatFromUint(0, -1), HostFunctionError::FloatInputMalformed);
expectError(h->floatFromUint(0, 4), HostFunctionError::FloatInputMalformed);
}
TEST_F(FloatFromUintImpl, Zero)
{
expectValue(makeHost()->floatFromUint(0, 0), FloatTest::kIntZero);
}
TEST_F(FloatFromUintImpl, MaxUint)
{
expectValue(makeHost()->floatFromUint(kMaxU64, 0), FloatTest::kUintMax);
}
} // namespace xrpl::test

View File

@@ -0,0 +1,56 @@
#include <xrpl/basics/Slice.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gtest/gtest.h>
#include <tx/wasm/FloatFixture.h>
#include <tx/wasm/RealHostFixture.h>
namespace xrpl::test {
struct FloatMultiplyImpl : FloatTest
{
};
TEST_F(FloatMultiplyImpl, BadModeIsMalformed)
{
expectError(
makeHost()->floatMultiply(slice(FloatTest::kOne), slice(FloatTest::kOne), -1),
HostFunctionError::FloatInputMalformed);
}
TEST_F(FloatMultiplyImpl, MalformedInput)
{
expectError(
makeHost()->floatMultiply(slice(FloatTest::kOne), Slice{}, 0),
HostFunctionError::FloatInputMalformed);
}
TEST_F(FloatMultiplyImpl, OverflowIsComputationError)
{
expectError(
makeHost()->floatMultiply(slice(FloatTest::kMax), slice(FloatTest::kOneMore), 0),
HostFunctionError::FloatComputationError);
}
TEST_F(FloatMultiplyImpl, OneTimesOneIsOne)
{
expectValue(
makeHost()->floatMultiply(slice(FloatTest::kOne), slice(FloatTest::kOne), 0),
FloatTest::kOne);
}
TEST_F(FloatMultiplyImpl, ZeroTimesMaxIouIsZero)
{
expectValue(
makeHost()->floatMultiply(slice(FloatTest::kIntZero), slice(FloatTest::kMaxIOU), 0),
FloatTest::kIntZero);
}
TEST_F(FloatMultiplyImpl, TenTimesPreMaxExpIsMaxExp)
{
expectValue(
makeHost()->floatMultiply(slice(FloatTest::kTen), slice(FloatTest::kPreMaxExp), 0),
FloatTest::kMaxExp);
}
} // namespace xrpl::test

Some files were not shown because too many files have changed in this diff Show More