Pin the wire contract of every contract host function

Completes the `host_calls/` layer: a mock host behind the real VM, with
each module hand-writing its own imports, literal regions and literal
lengths, so what is tested is the wire rather than whatever an SDK sends.

The order of two same-shaped arguments is the thing worth asserting here,
because nothing downstream would notice them swapped: a nested read takes
the outer key first, and an array read takes the key before the index
while the host takes them the other way round. Each is asserted against a
value the other could not be.

What the shim refuses before the host is reached, now stated: an account
region that is not twenty bytes, a key that is not UTF-8, a negative
index, a transaction type that does not fit in sixteen bits, a field code
the protocol does not name, bytes that are not a transaction, bytes that
are not an object, and an empty value region.

`SliceIs` is added beside `BytesAre` for data that is not text. `BytesAre`
compares against a string and so stops at the first NUL, which most
serialized fields contain — an amount matched as "@" and passed.
This commit is contained in:
Mayukha Vadari
2026-09-15 18:44:24 -04:00
parent 77d8e5bf2e
commit fed023680a
15 changed files with 1087 additions and 0 deletions

View File

@@ -0,0 +1,64 @@
#pragma once
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/STJson.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <tx/wasm/fixtures/WasmFixture.h>
#include <cstdint>
#include <format>
#include <string>
// What a contract's `host_calls` tests share: the guest-side bytes each call carries.
//
// These modules hand-write the ABI — literal regions, literal lengths — so they hold the
// host to the wire rather than to whatever an SDK happens to send.
namespace xrpl::test {
struct ContractCallTest : HostCallTest
{
// A 20-byte account the guest holds as bytes. Distinctive at both ends, so a region read
// at the wrong offset or the wrong length cannot match it.
static AccountID
account()
{
AccountID id;
id.begin()[0] = 0xae;
id.begin()[19] = 0xea;
return id;
}
// Bytes as a WAT data-segment string.
static std::string
escaped(Bytes const& bytes)
{
std::string out;
for (auto const byte : bytes)
out += std::format("\\{:02x}", byte);
return out;
}
static std::string
escapedAccount()
{
auto const id = account();
return escaped(Bytes{id.begin(), id.end()});
}
// The bytes a guest writes for a `set_data_*` value: its one-byte type, then its
// serialization.
static Bytes
valueBytes(STJson::Value const& value)
{
Bytes wire{static_cast<std::uint8_t>(value->getSType())};
Serializer s;
value->add(s);
wire.insert(wire.end(), s.peekData().begin(), s.peekData().end());
return wire;
}
};
} // namespace xrpl::test

View File

@@ -545,6 +545,14 @@ MATCHER_P(BytesAre, expected, "")
std::string_view{expected};
}
// Matches a `Slice` against exact bytes. `BytesAre` compares against a string and so stops
// at the first NUL, which most serialized fields contain.
// NOLINTNEXTLINE(readability-identifier-naming)
MATCHER_P(SliceIs, expected, "")
{
return Bytes{arg.data(), arg.data() + arg.size()} == expected;
}
// Matches an `AccountID` against another, so an expectation can name *whose* data a
// contract asked the host for.
// NOLINTNEXTLINE(readability-identifier-naming)

View File

@@ -0,0 +1,81 @@
#include <xrpl/protocol/SField.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/fixtures/ContractCallFixture.h>
#include <tx/wasm/fixtures/MockHostFunctions.h>
#include <expected>
#include <format>
#include <string>
namespace xrpl::test {
using testing::Ref;
using testing::Return;
// add_txn_field — a field code the shim translates into an `SField`, plus the bytes that
// field is to be built from.
struct AddTxnFieldCall : ContractCallTest
{
[[nodiscard]] std::string
wat() const override
{
return std::format(
R"wat(
(module
(import "host_lib" "add_txn_field"
(func $add_txn_field (param i32 i32 i32 i32) (result i32)))
(memory (export "memory") 1)
(data (i32.const 0) "\40\00\00\00\00\00\00\c0")
(func (export "escrow_finish") (result i32)
(call $add_txn_field (i32.const 1) (i32.const {}) (i32.const 0) (i32.const 8)))
;; A field code the protocol does not name: a 16-bit integer field nothing declares.
(func (export "unknown_field") (result i32)
(call $add_txn_field (i32.const 1) (i32.const {}) (i32.const 0) (i32.const 8)))
(func (export "negative_index") (result i32)
(call $add_txn_field (i32.const -1) (i32.const {}) (i32.const 0) (i32.const 8))))
)wat",
sfAmount.getCode(),
(static_cast<int>(STI_UINT16) << 16) | 9999,
sfAmount.getCode());
}
};
TEST_F(AddTxnFieldCall, TheFieldCodeBecomesAnSFieldAndTheBytesArrive)
{
auto const amount = Bytes{0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0};
EXPECT_CALL(host, addTxnField(1U, Ref(sfAmount), SliceIs(amount))).WillOnce(Return(0));
EXPECT_EQ(hostAnswer(), 0);
}
// A code the protocol does not name has no `SField` to translate to, so the host is never
// asked which field the contract meant.
TEST_F(AddTxnFieldCall, AnUnknownFieldCodeIsRefusedWithoutAskingTheHost)
{
EXPECT_CALL(host, addTxnField).Times(0);
EXPECT_EQ(hostAnswer("unknown_field"), hfErrorToInt(HostFunctionError::InvalidField));
}
TEST_F(AddTxnFieldCall, ANegativeIndexIsRefusedWithoutAskingTheHost)
{
EXPECT_CALL(host, addTxnField).Times(0);
EXPECT_EQ(hostAnswer("negative_index"), hfErrorToInt(HostFunctionError::IndexOutOfBounds));
}
TEST_F(AddTxnFieldCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, addTxnField)
.WillOnce(Return(std::unexpected(HostFunctionError::FieldNotFound)));
EXPECT_EQ(hostAnswer(), hfErrorToInt(HostFunctionError::FieldNotFound));
}
} // namespace xrpl::test

View File

@@ -0,0 +1,72 @@
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/fixtures/ContractCallFixture.h>
#include <tx/wasm/fixtures/MockHostFunctions.h>
#include <expected>
#include <string>
namespace xrpl::test {
using testing::Return;
// build_txn — one scalar in, one out. The narrowest call in the ABI, and the only one whose
// argument has to be narrowed: a transaction type is sixteen bits inside wasm's thirty-two.
struct BuildTxnCall : ContractCallTest
{
[[nodiscard]] std::string
wat() const override
{
return std::string{R"wat(
(module
(import "host_lib" "build_txn" (func $build_txn (param i32) (result i32)))
(memory (export "memory") 1)
(func (export "escrow_finish") (result i32)
(call $build_txn (i32.const 0)))
;; A type that does not fit in sixteen bits, which no transaction type can be.
(func (export "type_too_large") (result i32)
(call $build_txn (i32.const 65536)))
(func (export "negative_type") (result i32)
(call $build_txn (i32.const -1))))
)wat"};
}
};
TEST_F(BuildTxnCall, TheTypeReachesTheHostAndTheIndexComesBack)
{
EXPECT_CALL(host, buildTxn(static_cast<std::uint16_t>(ttPAYMENT))).WillOnce(Return(3));
EXPECT_EQ(hostAnswer(), 3);
}
// Narrowing an out-of-range type would ask the host to build something else entirely, so it
// is refused instead.
TEST_F(BuildTxnCall, ATypeThatDoesNotFitIsRefusedWithoutAskingTheHost)
{
EXPECT_CALL(host, buildTxn).Times(0);
EXPECT_EQ(hostAnswer("type_too_large"), hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(BuildTxnCall, ANegativeTypeIsRefusedWithoutAskingTheHost)
{
EXPECT_CALL(host, buildTxn).Times(0);
EXPECT_EQ(hostAnswer("negative_type"), hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(BuildTxnCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, buildTxn)
.WillOnce(Return(std::unexpected(HostFunctionError::SubmitTxnFailure)));
EXPECT_EQ(hostAnswer(), hfErrorToInt(HostFunctionError::SubmitTxnFailure));
}
} // namespace xrpl::test

View File

@@ -0,0 +1,84 @@
#include <xrpl/protocol/STJson.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/fixtures/ContractCallFixture.h>
#include <tx/wasm/fixtures/MockHostFunctions.h>
#include <expected>
#include <format>
#include <string>
namespace xrpl::test {
using testing::Return;
// emit_event — a name and a serialized object in, a scalar out. The second of the two wire
// formats a guest writes itself, and a different parser from the one a value goes through.
struct EmitEventCall : ContractCallTest
{
static STJson
event()
{
return STJson{STJson::Map{
{"count",
std::static_pointer_cast<STBase>(std::make_shared<STUInt32>(sfSequence, 32U))}}};
}
[[nodiscard]] std::string
wat() const override
{
auto const blob = event().toBlob();
return std::format(
R"wat(
(module
(import "host_lib" "emit_event" (func $emit_event (param i32 i32 i32 i32) (result i32)))
(memory (export "memory") 1)
(data (i32.const 0) "transferred")
(data (i32.const 32) "{}")
(data (i32.const 2048) "not an object")
(func (export "escrow_finish") (result i32)
(call $emit_event (i32.const 0) (i32.const 11) (i32.const 32) (i32.const {})))
(func (export "not_an_object") (result i32)
(call $emit_event (i32.const 0) (i32.const 11) (i32.const 2048) (i32.const 13)))
(func (export "empty_data") (result i32)
(call $emit_event (i32.const 0) (i32.const 11) (i32.const 32) (i32.const 0))))
)wat",
escaped(Bytes{blob.begin(), blob.end()}),
blob.size());
}
};
TEST_F(EmitEventCall, TheNameAndTheDecodedObjectReachTheHost)
{
EXPECT_CALL(host, emitEvent(BytesAre("transferred"), EventJsonEq(event()))).WillOnce(Return(0));
EXPECT_EQ(hostAnswer(), 0);
}
TEST_F(EmitEventCall, BytesThatAreNotAnObjectAreRefusedWithoutAskingTheHost)
{
EXPECT_CALL(host, emitEvent).Times(0);
EXPECT_EQ(hostAnswer("not_an_object"), hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(EmitEventCall, AnEmptyRegionIsNotAnObject)
{
EXPECT_CALL(host, emitEvent).Times(0);
EXPECT_EQ(hostAnswer("empty_data"), hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(EmitEventCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, emitEvent).WillOnce(Return(std::unexpected(HostFunctionError::InvalidState)));
EXPECT_EQ(hostAnswer(), hfErrorToInt(HostFunctionError::InvalidState));
}
} // namespace xrpl::test

View File

@@ -0,0 +1,109 @@
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/fixtures/ContractCallFixture.h>
#include <tx/wasm/fixtures/MockHostFunctions.h>
#include <expected>
#include <format>
#include <string>
namespace xrpl::test {
using testing::Return;
// emit_txn — a serialized transaction in, a TER out through the guest's buffer.
//
// The shim deserializes, so the host is handed a transaction rather than bytes: a guest that
// writes something that is not one never reaches it.
struct EmitTxnCall : ContractCallTest
{
// A transaction as a contract would have serialized it.
static STTx
transaction()
{
return STTx{ttPAYMENT, [](STObject& obj) {
obj.setAccountID(sfAccount, account());
obj.setAccountID(sfDestination, account());
obj.setFieldAmount(sfAmount, STAmount{192});
obj.setFieldAmount(sfFee, STAmount{0});
obj.setFieldU32(sfSequence, 1);
obj.setFieldU32(sfFlags, tfInnerBatchTxn);
obj.setFieldVL(sfSigningPubKey, Blob{});
}};
}
static Bytes
serialized()
{
auto const s = transaction().getSerializer();
return Bytes{s.peekData().begin(), s.peekData().end()};
}
[[nodiscard]] std::string
wat() const override
{
auto const bytes = serialized();
return std::format(
R"wat(
(module
(import "host_lib" "emit_txn" (func $emit_txn (param i32 i32 i32 i32) (result i32)))
(memory (export "memory") 1)
(data (i32.const 0) "{}")
(data (i32.const 2048) "not a transaction")
;; Emits the serialized transaction and returns the TER it wrote.
(func (export "escrow_finish") (result i32)
(local $n i32)
(local.set $n
(call $emit_txn (i32.const 0) (i32.const {}) (i32.const 3072) (i32.const 4)))
(select (local.get $n) (i32.load (i32.const 3072)) (i32.lt_s (local.get $n) (i32.const 0))))
(func (export "not_a_transaction") (result i32)
(call $emit_txn (i32.const 2048) (i32.const 17) (i32.const 3072) (i32.const 4))))
)wat",
escaped(bytes),
bytes.size());
}
};
TEST_F(EmitTxnCall, TheGuestsBytesReachTheHostAsATransaction)
{
EXPECT_CALL(host, emitTxn(StTxIdIs(transaction().getTransactionID())))
.WillOnce(Return(TERtoInt(tesSUCCESS)));
EXPECT_EQ(hostAnswer(), TERtoInt(tesSUCCESS));
}
// The TER travels in the buffer because it may be negative, and a negative return is a host
// error. This is the case that would otherwise have stopped the run.
TEST_F(EmitTxnCall, ANegativeTerArrivesIntact)
{
EXPECT_CALL(host, emitTxn).WillOnce(Return(TERtoInt(tefPAST_SEQ)));
EXPECT_EQ(hostAnswer(), TERtoInt(tefPAST_SEQ));
}
TEST_F(EmitTxnCall, BytesThatAreNotATransactionAreRefusedWithoutAskingTheHost)
{
EXPECT_CALL(host, emitTxn).Times(0);
EXPECT_EQ(hostAnswer("not_a_transaction"), hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(EmitTxnCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, emitTxn)
.WillOnce(Return(std::unexpected(HostFunctionError::SubmitTxnFailure)));
EXPECT_EQ(hostAnswer(), hfErrorToInt(HostFunctionError::SubmitTxnFailure));
}
} // namespace xrpl::test

View File

@@ -0,0 +1,80 @@
#include <xrpl/protocol/SField.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/fixtures/ContractCallFixture.h>
#include <tx/wasm/fixtures/MockHostFunctions.h>
#include <expected>
#include <string>
namespace xrpl::test {
using testing::Return;
// function_param — the same shape as `instance_param`, over the call's own parameters.
// Which of the two tables the host is asked about is the whole of the difference.
struct FunctionParamCall : ContractCallTest
{
[[nodiscard]] std::string
wat() const override
{
return std::string{R"wat(
(module
(import "host_lib" "function_param"
(func $function_param (param i32 i32 i32 i32) (result i32)))
(memory (export "memory") 1)
;; Asks for parameter 2 as an STI_UINT32 (2) and returns the four bytes that landed.
(func (export "escrow_finish") (result i32)
(local $n i32)
(local.set $n
(call $function_param (i32.const 2) (i32.const 2) (i32.const 0) (i32.const 32)))
(select (local.get $n) (i32.load (i32.const 0)) (i32.lt_s (local.get $n) (i32.const 0))))
;; Reports the length, for the cases where the value itself is not the point.
(func (export "value_length") (result i32)
(call $function_param (i32.const 2) (i32.const 2) (i32.const 0) (i32.const 32)))
;; A negative index, which the shim refuses rather than casting.
(func (export "negative_index") (result i32)
(call $function_param (i32.const -1) (i32.const 2) (i32.const 0) (i32.const 32))))
)wat"};
}
};
TEST_F(FunctionParamCall, TheIndexAndTypeReachTheHostAndTheBytesComeBack)
{
EXPECT_CALL(host, functionParam(2U, STI_UINT32))
.WillOnce(Return(Bytes{0x0d, 0x0c, 0x0b, 0x0a}));
EXPECT_CALL(host, instanceParam).Times(0);
EXPECT_EQ(hostAnswer(), 0x0a0b0c0d) << "the value's bytes, read back little-endian";
}
TEST_F(FunctionParamCall, TheAnswerIsTheValuesLength)
{
EXPECT_CALL(host, functionParam).WillOnce(Return(Bytes{1, 2, 3, 4}));
EXPECT_EQ(hostAnswer("value_length"), 4);
}
// A negative index is out of range, not a very large one: casting it would ask the host
// about a parameter no contract could have meant.
TEST_F(FunctionParamCall, ANegativeIndexIsRefusedWithoutAskingTheHost)
{
EXPECT_CALL(host, functionParam).Times(0);
EXPECT_EQ(hostAnswer("negative_index"), hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(FunctionParamCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, functionParam)
.WillOnce(Return(std::unexpected(HostFunctionError::IndexOutOfBounds)));
EXPECT_EQ(hostAnswer(), hfErrorToInt(HostFunctionError::IndexOutOfBounds));
}
} // 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/fixtures/ContractCallFixture.h>
#include <tx/wasm/fixtures/MockHostFunctions.h>
#include <expected>
#include <format>
#include <string>
namespace xrpl::test {
using testing::Return;
// get_data_array_element_field — the shape with a scalar *between* two regions: account,
// key, index, out.
//
// The key comes before the index on the wire, and the host takes them the other way round.
// A test that only checked both arrived would pass with them swapped, so this asserts each
// against a value the other could not be.
struct GetDataArrayElementFieldCall : ContractCallTest
{
[[nodiscard]] std::string
wat() const override
{
return std::format(
R"wat(
(module
(import "host_lib" "get_data_array_element_field"
(func $get_data_array_element_field (param i32 i32 i32 i32 i32 i32 i32) (result i32)))
(memory (export "memory") 1)
(data (i32.const 0) "{}")
(data (i32.const 32) "amount")
(func (export "escrow_finish") (result i32)
(local $n i32)
(local.set $n (call $get_data_array_element_field
(i32.const 0) (i32.const 20) (i32.const 32) (i32.const 6) (i32.const 7)
(i32.const 64) (i32.const 32)))
(select (local.get $n) (i32.load (i32.const 64)) (i32.lt_s (local.get $n) (i32.const 0))))
;; A negative element index, which the shim refuses rather than casting to a huge one.
(func (export "negative_index") (result i32)
(call $get_data_array_element_field
(i32.const 0) (i32.const 20) (i32.const 32) (i32.const 6) (i32.const -1)
(i32.const 64) (i32.const 32))))
)wat",
escapedAccount());
}
};
TEST_F(GetDataArrayElementFieldCall, TheKeyIsTheRegionAndTheIndexIsTheScalar)
{
EXPECT_CALL(host, getDataArrayElementField(AccountIs(account()), 7U, BytesAre("amount")))
.WillOnce(Return(Bytes{0x0d, 0x0c, 0x0b, 0x0a}));
EXPECT_EQ(hostAnswer(), 0x0a0b0c0d);
}
TEST_F(GetDataArrayElementFieldCall, ANegativeIndexIsRefusedWithoutAskingTheHost)
{
EXPECT_CALL(host, getDataArrayElementField).Times(0);
EXPECT_EQ(hostAnswer("negative_index"), hfErrorToInt(HostFunctionError::IndexOutOfBounds));
}
TEST_F(GetDataArrayElementFieldCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, getDataArrayElementField)
.WillOnce(Return(std::unexpected(HostFunctionError::InvalidState)));
EXPECT_EQ(hostAnswer(), hfErrorToInt(HostFunctionError::InvalidState));
}
} // 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/fixtures/ContractCallFixture.h>
#include <tx/wasm/fixtures/MockHostFunctions.h>
#include <expected>
#include <format>
#include <string>
namespace xrpl::test {
using testing::Return;
// get_data_nested_array_element_field — the widest shape in the ABI: two string keys with a
// scalar index between them, then the output region.
struct GetDataNestedArrayElementFieldCall : ContractCallTest
{
[[nodiscard]] std::string
wat() const override
{
return std::format(
R"wat(
(module
(import "host_lib" "get_data_nested_array_element_field"
(func $get_data_nested_array_element_field
(param i32 i32 i32 i32 i32 i32 i32 i32 i32) (result i32)))
(memory (export "memory") 1)
(data (i32.const 0) "{}")
(data (i32.const 32) "items")
(data (i32.const 48) "price")
(func (export "escrow_finish") (result i32)
(local $n i32)
(local.set $n (call $get_data_nested_array_element_field
(i32.const 0) (i32.const 20) (i32.const 32) (i32.const 5) (i32.const 7)
(i32.const 48) (i32.const 5) (i32.const 64) (i32.const 32)))
(select (local.get $n) (i32.load (i32.const 64)) (i32.lt_s (local.get $n) (i32.const 0))))
(func (export "negative_index") (result i32)
(call $get_data_nested_array_element_field
(i32.const 0) (i32.const 20) (i32.const 32) (i32.const 5) (i32.const -1)
(i32.const 48) (i32.const 5) (i32.const 64) (i32.const 32))))
)wat",
escapedAccount());
}
};
TEST_F(GetDataNestedArrayElementFieldCall, EachRegionAndTheIndexArriveInOrder)
{
EXPECT_CALL(
host,
getDataNestedArrayElementField(
AccountIs(account()), BytesAre("items"), 7U, BytesAre("price")))
.WillOnce(Return(Bytes{0x0d, 0x0c, 0x0b, 0x0a}));
EXPECT_EQ(hostAnswer(), 0x0a0b0c0d);
}
TEST_F(GetDataNestedArrayElementFieldCall, ANegativeIndexIsRefusedWithoutAskingTheHost)
{
EXPECT_CALL(host, getDataNestedArrayElementField).Times(0);
EXPECT_EQ(hostAnswer("negative_index"), hfErrorToInt(HostFunctionError::IndexOutOfBounds));
}
TEST_F(GetDataNestedArrayElementFieldCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, getDataNestedArrayElementField)
.WillOnce(Return(std::unexpected(HostFunctionError::InvalidField)));
EXPECT_EQ(hostAnswer(), hfErrorToInt(HostFunctionError::InvalidField));
}
} // 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/fixtures/ContractCallFixture.h>
#include <tx/wasm/fixtures/MockHostFunctions.h>
#include <expected>
#include <format>
#include <string>
namespace xrpl::test {
using testing::Return;
// get_data_nested_object_field — two string keys in, bytes out.
//
// The outer key comes first. Two regions of the same kind are the easiest pair to hand over
// backwards, and nothing downstream would notice, so the order is asserted with keys that
// could not be mistaken for one another.
struct GetDataNestedObjectFieldCall : ContractCallTest
{
[[nodiscard]] std::string
wat() const override
{
return std::format(
R"wat(
(module
(import "host_lib" "get_data_nested_object_field"
(func $get_data_nested_object_field (param i32 i32 i32 i32 i32 i32 i32 i32) (result i32)))
(memory (export "memory") 1)
(data (i32.const 0) "{}")
(data (i32.const 32) "stats")
(data (i32.const 48) "score")
(func (export "escrow_finish") (result i32)
(local $n i32)
(local.set $n (call $get_data_nested_object_field
(i32.const 0) (i32.const 20) (i32.const 32) (i32.const 5) (i32.const 48) (i32.const 5)
(i32.const 64) (i32.const 32)))
(select (local.get $n) (i32.load (i32.const 64)) (i32.lt_s (local.get $n) (i32.const 0)))))
)wat",
escapedAccount());
}
};
TEST_F(GetDataNestedObjectFieldCall, TheOuterKeyIsTheFirstRegion)
{
EXPECT_CALL(
host, getDataNestedObjectField(AccountIs(account()), BytesAre("stats"), BytesAre("score")))
.WillOnce(Return(Bytes{0x0d, 0x0c, 0x0b, 0x0a}));
EXPECT_EQ(hostAnswer(), 0x0a0b0c0d);
}
TEST_F(GetDataNestedObjectFieldCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, getDataNestedObjectField)
.WillOnce(Return(std::unexpected(HostFunctionError::InvalidField)));
EXPECT_EQ(hostAnswer(), hfErrorToInt(HostFunctionError::InvalidField));
}
} // namespace xrpl::test

View File

@@ -0,0 +1,85 @@
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/fixtures/ContractCallFixture.h>
#include <tx/wasm/fixtures/MockHostFunctions.h>
#include <expected>
#include <format>
#include <string>
namespace xrpl::test {
using testing::Return;
// get_data_object_field — an account region and a string key in, bytes out.
//
// The key is declared `&str`, so the engine checks it is UTF-8 before the host sees it: a
// region that is not becomes `InvalidParams` without a call.
struct GetDataObjectFieldCall : ContractCallTest
{
[[nodiscard]] std::string
wat() const override
{
return std::format(
R"wat(
(module
(import "host_lib" "get_data_object_field"
(func $get_data_object_field (param i32 i32 i32 i32 i32 i32) (result i32)))
(memory (export "memory") 1)
(data (i32.const 0) "{}")
(data (i32.const 32) "count")
(data (i32.const 48) "\ff\fe")
(func (export "escrow_finish") (result i32)
(local $n i32)
(local.set $n (call $get_data_object_field
(i32.const 0) (i32.const 20) (i32.const 32) (i32.const 5) (i32.const 64) (i32.const 32)))
(select (local.get $n) (i32.load (i32.const 64)) (i32.lt_s (local.get $n) (i32.const 0))))
;; A nineteen-byte account, which is not an account id.
(func (export "short_account") (result i32)
(call $get_data_object_field
(i32.const 0) (i32.const 19) (i32.const 32) (i32.const 5) (i32.const 64) (i32.const 32)))
;; A key region that is not UTF-8.
(func (export "invalid_key") (result i32)
(call $get_data_object_field
(i32.const 0) (i32.const 20) (i32.const 48) (i32.const 2) (i32.const 64) (i32.const 32))))
)wat",
escapedAccount());
}
};
TEST_F(GetDataObjectFieldCall, TheAccountAndKeyReachTheHostAndTheValueComesBack)
{
EXPECT_CALL(host, getDataObjectField(AccountIs(account()), BytesAre("count")))
.WillOnce(Return(Bytes{0x0d, 0x0c, 0x0b, 0x0a}));
EXPECT_EQ(hostAnswer(), 0x0a0b0c0d);
}
TEST_F(GetDataObjectFieldCall, AnAccountRegionOfTheWrongLengthIsRefusedWithoutAskingTheHost)
{
EXPECT_CALL(host, getDataObjectField).Times(0);
EXPECT_EQ(hostAnswer("short_account"), hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(GetDataObjectFieldCall, AKeyThatIsNotTextIsRefusedWithoutAskingTheHost)
{
EXPECT_CALL(host, getDataObjectField).Times(0);
EXPECT_EQ(hostAnswer("invalid_key"), hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(GetDataObjectFieldCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, getDataObjectField)
.WillOnce(Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
EXPECT_EQ(hostAnswer(), hfErrorToInt(HostFunctionError::LedgerObjNotFound));
}
} // namespace xrpl::test

View File

@@ -0,0 +1,79 @@
#include <xrpl/protocol/SField.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/fixtures/ContractCallFixture.h>
#include <tx/wasm/fixtures/MockHostFunctions.h>
#include <expected>
#include <string>
namespace xrpl::test {
using testing::Return;
// instance_param — two scalars in, bytes out. The scalars are wasm's own `i32`, so nothing
// is marshalled on the way in and the index reaches the host as it was written.
struct InstanceParamCall : ContractCallTest
{
[[nodiscard]] std::string
wat() const override
{
return std::string{R"wat(
(module
(import "host_lib" "instance_param"
(func $instance_param (param i32 i32 i32 i32) (result i32)))
(memory (export "memory") 1)
;; Asks for parameter 2 as an STI_UINT32 (2) and returns the four bytes that landed.
(func (export "escrow_finish") (result i32)
(local $n i32)
(local.set $n
(call $instance_param (i32.const 2) (i32.const 2) (i32.const 0) (i32.const 32)))
(select (local.get $n) (i32.load (i32.const 0)) (i32.lt_s (local.get $n) (i32.const 0))))
;; Reports the length, for the cases where the value itself is not the point.
(func (export "value_length") (result i32)
(call $instance_param (i32.const 2) (i32.const 2) (i32.const 0) (i32.const 32)))
;; A negative index, which the shim refuses rather than casting.
(func (export "negative_index") (result i32)
(call $instance_param (i32.const -1) (i32.const 2) (i32.const 0) (i32.const 32))))
)wat"};
}
};
TEST_F(InstanceParamCall, TheIndexAndTypeReachTheHostAndTheBytesComeBack)
{
EXPECT_CALL(host, instanceParam(2U, STI_UINT32))
.WillOnce(Return(Bytes{0x0d, 0x0c, 0x0b, 0x0a}));
EXPECT_EQ(hostAnswer(), 0x0a0b0c0d) << "the value's bytes, read back little-endian";
}
TEST_F(InstanceParamCall, TheAnswerIsTheValuesLength)
{
EXPECT_CALL(host, instanceParam).WillOnce(Return(Bytes{1, 2, 3, 4}));
EXPECT_EQ(hostAnswer("value_length"), 4);
}
// A negative index is out of range, not a very large one: casting it would ask the host
// about a parameter no contract could have meant.
TEST_F(InstanceParamCall, ANegativeIndexIsRefusedWithoutAskingTheHost)
{
EXPECT_CALL(host, instanceParam).Times(0);
EXPECT_EQ(hostAnswer("negative_index"), hfErrorToInt(HostFunctionError::InvalidParams));
}
TEST_F(InstanceParamCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, instanceParam)
.WillOnce(Return(std::unexpected(HostFunctionError::IndexOutOfBounds)));
EXPECT_EQ(hostAnswer(), hfErrorToInt(HostFunctionError::IndexOutOfBounds));
}
} // namespace xrpl::test

View File

@@ -0,0 +1,73 @@
#include <xrpl/protocol/SField.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/fixtures/ContractCallFixture.h>
#include <tx/wasm/fixtures/MockHostFunctions.h>
#include <expected>
#include <format>
#include <string>
namespace xrpl::test {
using testing::Return;
// set_data_array_element_field — a key, a scalar index between the regions, and a value.
struct SetDataArrayElementFieldCall : ContractCallTest
{
[[nodiscard]] std::string
wat() const override
{
return std::format(
R"wat(
(module
(import "host_lib" "set_data_array_element_field"
(func $set_data_array_element_field (param i32 i32 i32 i32 i32 i32 i32) (result i32)))
(memory (export "memory") 1)
(data (i32.const 0) "{}")
(data (i32.const 32) "amount")
(data (i32.const 64) "\10\2a")
(func (export "escrow_finish") (result i32)
(call $set_data_array_element_field
(i32.const 0) (i32.const 20) (i32.const 32) (i32.const 6) (i32.const 7)
(i32.const 64) (i32.const 2)))
(func (export "negative_index") (result i32)
(call $set_data_array_element_field
(i32.const 0) (i32.const 20) (i32.const 32) (i32.const 6) (i32.const -1)
(i32.const 64) (i32.const 2))))
)wat",
escapedAccount());
}
};
TEST_F(SetDataArrayElementFieldCall, TheKeyTheIndexAndTheValueAllArrive)
{
EXPECT_CALL(
host,
setDataArrayElementField(
AccountIs(account()), 7U, BytesAre("amount"), JsonValueIs(STI_UINT8, Bytes{0x2a})))
.WillOnce(Return(0));
EXPECT_EQ(hostAnswer(), 0);
}
TEST_F(SetDataArrayElementFieldCall, ANegativeIndexIsRefusedWithoutAskingTheHost)
{
EXPECT_CALL(host, setDataArrayElementField).Times(0);
EXPECT_EQ(hostAnswer("negative_index"), hfErrorToInt(HostFunctionError::IndexOutOfBounds));
}
TEST_F(SetDataArrayElementFieldCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, setDataArrayElementField)
.WillOnce(Return(std::unexpected(HostFunctionError::InvalidState)));
EXPECT_EQ(hostAnswer(), hfErrorToInt(HostFunctionError::InvalidState));
}
} // namespace xrpl::test

View File

@@ -0,0 +1,68 @@
#include <xrpl/protocol/SField.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/fixtures/ContractCallFixture.h>
#include <tx/wasm/fixtures/MockHostFunctions.h>
#include <expected>
#include <format>
#include <string>
namespace xrpl::test {
using testing::Return;
// set_data_nested_array_element_field — every kind of argument the ABI has in one call: an
// account, two string keys, a scalar index between them, and a typed value.
struct SetDataNestedArrayElementFieldCall : ContractCallTest
{
[[nodiscard]] std::string
wat() const override
{
return std::format(
R"wat(
(module
(import "host_lib" "set_data_nested_array_element_field"
(func $set_data_nested_array_element_field
(param i32 i32 i32 i32 i32 i32 i32 i32 i32) (result i32)))
(memory (export "memory") 1)
(data (i32.const 0) "{}")
(data (i32.const 32) "items")
(data (i32.const 48) "price")
(data (i32.const 64) "\10\2a")
(func (export "escrow_finish") (result i32)
(call $set_data_nested_array_element_field
(i32.const 0) (i32.const 20) (i32.const 32) (i32.const 5) (i32.const 7)
(i32.const 48) (i32.const 5) (i32.const 64) (i32.const 2))))
)wat",
escapedAccount());
}
};
TEST_F(SetDataNestedArrayElementFieldCall, EveryArgumentArrivesInItsOwnPlace)
{
EXPECT_CALL(
host,
setDataNestedArrayElementField(
AccountIs(account()),
BytesAre("items"),
7U,
BytesAre("price"),
JsonValueIs(STI_UINT8, Bytes{0x2a})))
.WillOnce(Return(0));
EXPECT_EQ(hostAnswer(), 0);
}
TEST_F(SetDataNestedArrayElementFieldCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, setDataNestedArrayElementField)
.WillOnce(Return(std::unexpected(HostFunctionError::InvalidState)));
EXPECT_EQ(hostAnswer(), hfErrorToInt(HostFunctionError::InvalidState));
}
} // namespace xrpl::test

View File

@@ -0,0 +1,68 @@
#include <xrpl/protocol/SField.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/fixtures/ContractCallFixture.h>
#include <tx/wasm/fixtures/MockHostFunctions.h>
#include <expected>
#include <format>
#include <string>
namespace xrpl::test {
using testing::Return;
// set_data_nested_object_field — two string keys and a typed value.
//
// The value's type byte decides what the shim builds, and the outer key comes first, so
// both are asserted against something the other could not be.
struct SetDataNestedObjectFieldCall : ContractCallTest
{
[[nodiscard]] std::string
wat() const override
{
return std::format(
R"wat(
(module
(import "host_lib" "set_data_nested_object_field"
(func $set_data_nested_object_field (param i32 i32 i32 i32 i32 i32 i32 i32) (result i32)))
(memory (export "memory") 1)
(data (i32.const 0) "{}")
(data (i32.const 32) "stats")
(data (i32.const 48) "score")
(data (i32.const 64) "\02\00\00\27\0f")
(func (export "escrow_finish") (result i32)
(call $set_data_nested_object_field
(i32.const 0) (i32.const 20) (i32.const 32) (i32.const 5) (i32.const 48) (i32.const 5)
(i32.const 64) (i32.const 5))))
)wat",
escapedAccount());
}
};
TEST_F(SetDataNestedObjectFieldCall, TheOuterKeyComesFirstAndTheValueIsDecoded)
{
EXPECT_CALL(
host,
setDataNestedObjectField(
AccountIs(account()),
BytesAre("stats"),
BytesAre("score"),
JsonValueIs(STI_UINT32, (Bytes{0x00, 0x00, 0x27, 0x0f}))))
.WillOnce(Return(0));
EXPECT_EQ(hostAnswer(), 0);
}
TEST_F(SetDataNestedObjectFieldCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, setDataNestedObjectField)
.WillOnce(Return(std::unexpected(HostFunctionError::InvalidState)));
EXPECT_EQ(hostAnswer(), hfErrorToInt(HostFunctionError::InvalidState));
}
} // namespace xrpl::test