mirror of
https://github.com/XRPLF/rippled.git
synced 2026-08-21 14:20:56 +00:00
Fixed review comments
This commit is contained in:
@@ -18,10 +18,11 @@ bridge.
|
||||
`ApplyContext&`. Bodies are split across `HostFuncImpl*.cpp` by category.
|
||||
- **`HostContext.h`** — the bridge's C++ half: an ABI-shaped, `noexcept` view of
|
||||
`HostFunctions` that the engine calls back into. Nothing may unwind into Rust, so every
|
||||
method routes through one `guarded()`.
|
||||
method routes through `guarded()`.
|
||||
- **`WasmCommon.h`** — the shared vocabulary: `HostFunctionError` (the codes a contract
|
||||
sees), `Bytes`, `FieldLocator`, `WasmTER`, and `adjustWasmEndianess`, which is where the
|
||||
boundary's byte order is decided.
|
||||
sees), `Bytes`, `FieldLocator`, `WasmTER`, `adjustWasmEndianess`, which is where the
|
||||
boundary's byte order is decided, and `guarded()`, the one catch every crossing of the
|
||||
bridge's C++ half goes through.
|
||||
|
||||
## Host functions
|
||||
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/contract.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
|
||||
#include <bit>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <optional>
|
||||
#include <source_location>
|
||||
#include <stdexcept>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
@@ -146,4 +150,28 @@ hfErrorToInt(HostFunctionError e)
|
||||
return static_cast<int32_t>(e);
|
||||
}
|
||||
|
||||
template <class Body>
|
||||
std::invoke_result_t<Body>
|
||||
guarded(
|
||||
beast::Journal journal,
|
||||
std::invoke_result_t<Body> onThrow,
|
||||
Body&& body,
|
||||
std::source_location const location = std::source_location::current()) noexcept
|
||||
{
|
||||
try
|
||||
{
|
||||
return body();
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
JLOG(journal.error()) << "wasm: " << location.function_name() << " threw: " << e.what();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
JLOG(journal.error()) << "wasm: " << location.function_name() << " threw";
|
||||
}
|
||||
|
||||
return onThrow;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -22,15 +22,12 @@ std::string_view inline constexpr escrowFunctionName = "escrow_finish";
|
||||
// when the number means anything, the gas to write to transaction metadata: a contract
|
||||
// that traps or exhausts its budget is charged for what it burned, while a `tecINTERNAL`
|
||||
// reports no cost because the fault is the node's rather than the transaction's.
|
||||
//
|
||||
// Does not throw. Every way a run can end - including a Rust panic inside the engine or
|
||||
// a C++ exception thrown by a host function - arrives as one of those two answers.
|
||||
std::expected<EscrowResult, WasmTER>
|
||||
runEscrowWasm(
|
||||
Bytes const& wasmCode,
|
||||
HostFunctions& hfs,
|
||||
std::int64_t gasLimit,
|
||||
std::string_view funcName = escrowFunctionName);
|
||||
std::string_view funcName = escrowFunctionName) noexcept;
|
||||
|
||||
// Screen `wasmCode`: whether `runEscrowWasm` would refuse it before the contract's
|
||||
// first instruction. Compiles the module and reads its imports and exports; runs
|
||||
@@ -44,12 +41,10 @@ runEscrowWasm(
|
||||
// engine cannot run, so it is refused before it can reach the ledger.
|
||||
// `telFAILED_PROCESSING` if the engine itself failed: nothing was learned about the
|
||||
// module, and a defect here is not evidence that the transaction is malformed.
|
||||
//
|
||||
// Does not throw.
|
||||
NotTEC
|
||||
preflightEscrowWasm(
|
||||
Bytes const& wasmCode,
|
||||
beast::Journal j,
|
||||
std::string_view funcName = escrowFunctionName);
|
||||
std::string_view funcName = escrowFunctionName) noexcept;
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
#include <xrpl/tx/wasm/HostContext.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/tx/wasm/HostFunc.h>
|
||||
#include <xrpl/tx/wasm/WasmCommon.h>
|
||||
@@ -11,16 +9,15 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
#include <source_location>
|
||||
#include <string_view>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
namespace {
|
||||
|
||||
// What a host call answers when it could not be served at all. The engine reads -1 as its
|
||||
// fatal `Internal`, stops the run and reports `tecINTERNAL`.
|
||||
// What a host call answers when it could not be served at all: every method below hands it
|
||||
// to `guarded` as the answer for a body that throws. The engine reads -1 as its fatal
|
||||
// `Internal`, stops the run and reports `tecINTERNAL`.
|
||||
//
|
||||
// `HostFunctionError` spells -1 `Unimplemented`, so the two share a code. They also share
|
||||
// a meaning worth keeping together - "the host could not serve this call, and the contract
|
||||
@@ -28,39 +25,6 @@ namespace {
|
||||
// site reads as what it is rather than as "unimplemented".
|
||||
constexpr std::int32_t kHostInternal = hfErrorToInt(HostFunctionError::Unimplemented);
|
||||
|
||||
// Nothing may unwind out of a host call: the frames that called it are Rust, which cannot
|
||||
// run a C++ landing pad. Every method below goes through here, so the catch is not a thing
|
||||
// any one of them can forget.
|
||||
//
|
||||
// The caller names itself: the default argument is evaluated at the call site, so the log
|
||||
// line gets the enclosing method without anyone passing a string that could drift from the
|
||||
// method it labels. `__func__` would expand to `operator()` inside the lambda, which is why
|
||||
// this is a defaulted parameter rather than something the body reads.
|
||||
template <class Body>
|
||||
std::int32_t
|
||||
guarded(
|
||||
beast::Journal journal,
|
||||
Body&& body,
|
||||
std::source_location const location = std::source_location::current()) noexcept
|
||||
{
|
||||
try
|
||||
{
|
||||
return body();
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
JLOG(journal.warn()) << "wasm host call threw in " << location.function_name() << ": "
|
||||
<< e.what();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
JLOG(journal.warn()) << "wasm host call threw a non-exception in "
|
||||
<< location.function_name();
|
||||
}
|
||||
|
||||
return kHostInternal;
|
||||
}
|
||||
|
||||
// Copy `value` into `out` only if the whole of it fits, and answer its true length either
|
||||
// way. A value too large for the guest's buffer must reach it in no part: a prefix would
|
||||
// be a wrong answer where a length is a usable one.
|
||||
@@ -96,12 +60,11 @@ HostContext::HostContext(HostFunctions& hostFunctions) : hostFunctions_(hostFunc
|
||||
std::int32_t
|
||||
HostContext::getLedgerSqn(rust::Slice<std::uint8_t> out) const noexcept
|
||||
{
|
||||
return guarded(hostFunctions_.getJournal(), [&] {
|
||||
return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
|
||||
auto const sqn = hostFunctions_.getLedgerSqn();
|
||||
if (!sqn)
|
||||
return hfErrorToInt(sqn.error());
|
||||
|
||||
// Four bytes the guest reads back with `u32::from_le_bytes`.
|
||||
return answerScalar(out, *sqn);
|
||||
});
|
||||
}
|
||||
@@ -110,7 +73,7 @@ std::int32_t
|
||||
HostContext::getCurrentLedgerObjField(std::int32_t field, rust::Slice<std::uint8_t> out)
|
||||
const noexcept
|
||||
{
|
||||
return guarded(hostFunctions_.getJournal(), [&] {
|
||||
return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
|
||||
auto const& knownSFields = SField::getKnownCodeToField();
|
||||
auto const it = knownSFields.find(field);
|
||||
if (it == knownSFields.end())
|
||||
@@ -128,8 +91,8 @@ std::int32_t
|
||||
HostContext::sha512Half(rust::Slice<std::uint8_t const> data, rust::Slice<std::uint8_t> out)
|
||||
const noexcept
|
||||
{
|
||||
return guarded(hostFunctions_.getJournal(), [&] {
|
||||
auto const digest = hostFunctions_.computeSha512HalfHash(Slice(data.data(), data.size()));
|
||||
return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
|
||||
auto const digest = hostFunctions_.computeSha512HalfHash(Slice{data.data(), data.size()});
|
||||
if (!digest)
|
||||
return hfErrorToInt(digest.error());
|
||||
|
||||
@@ -140,9 +103,9 @@ HostContext::sha512Half(rust::Slice<std::uint8_t const> data, rust::Slice<std::u
|
||||
std::int32_t
|
||||
HostContext::trace(rust::Str msg, rust::Slice<std::uint8_t const> data, bool asHex) const noexcept
|
||||
{
|
||||
return guarded(hostFunctions_.getJournal(), [&] {
|
||||
return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
|
||||
auto const status = hostFunctions_.trace(
|
||||
std::string_view(msg.data(), msg.size()), Slice(data.data(), data.size()), asHex);
|
||||
std::string_view{msg.data(), msg.size()}, Slice{data.data(), data.size()}, asHex);
|
||||
if (!status)
|
||||
return hfErrorToInt(status.error());
|
||||
|
||||
@@ -153,9 +116,9 @@ HostContext::trace(rust::Str msg, rust::Slice<std::uint8_t const> data, bool asH
|
||||
std::int32_t
|
||||
HostContext::traceNum(rust::Str msg, std::int64_t number) const noexcept
|
||||
{
|
||||
return guarded(hostFunctions_.getJournal(), [&] {
|
||||
return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
|
||||
auto const status =
|
||||
hostFunctions_.traceNum(std::string_view(msg.data(), msg.size()), number);
|
||||
hostFunctions_.traceNum(std::string_view{msg.data(), msg.size()}, number);
|
||||
if (!status)
|
||||
return hfErrorToInt(status.error());
|
||||
|
||||
|
||||
@@ -125,8 +125,8 @@ getAnyFieldData(FieldValue const& variantObj)
|
||||
return Bytes((*u)->begin(), (*u)->end());
|
||||
|
||||
// Unreachable: the variant only holds the two alternatives above. If not, it is an
|
||||
// xrpld bug, and `HostContext::guarded` turns the throw into the engine's fatal
|
||||
// `Internal` -> tecINTERNAL.
|
||||
// xrpld bug, and `guarded` turns the throw into the engine's fatal `Internal` ->
|
||||
// tecINTERNAL.
|
||||
Throw<std::runtime_error>("field value variant holds neither alternative"); // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
|
||||
@@ -10,11 +10,9 @@
|
||||
#include <xrpl_wasm_vm_ffi_cxxbridge/lib.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <expected>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
@@ -45,7 +43,7 @@ outcome(rs::wasm_vm::RunResult const& run)
|
||||
// The cost is the whole limit: XLS-0102 halts the guest the instant the meter runs
|
||||
// out, and the run is charged for all of it.
|
||||
case RunStatus::OutOfGas:
|
||||
return std::unexpected(WasmTER{.ter = tecOUT_OF_GAS, .cost = cost});
|
||||
return std::unexpected{WasmTER{.ter = tecOUT_OF_GAS, .cost = cost}};
|
||||
|
||||
// The contract's own fault - it trapped, or it never exported the linear memory
|
||||
// its host calls need - so it is charged for what it burned reaching that point.
|
||||
@@ -57,7 +55,7 @@ outcome(rs::wasm_vm::RunResult const& run)
|
||||
// refused here. It is a deterministic property of the code either way, and one
|
||||
// this node's own conduct had no part in.
|
||||
case RunStatus::Instantiate:
|
||||
return std::unexpected(WasmTER{.ter = tecFAILED_PROCESSING, .cost = cost});
|
||||
return std::unexpected{WasmTER{.ter = tecFAILED_PROCESSING, .cost = cost}};
|
||||
|
||||
// A module that will not compile, or does not expose the entry point, should have
|
||||
// been refused at preflight with `temBAD_WASM`: screening decides both from the
|
||||
@@ -71,40 +69,9 @@ outcome(rs::wasm_vm::RunResult const& run)
|
||||
// The engine panicked: a defect in the engine, reported rather than fatal to the
|
||||
// node.
|
||||
case RunStatus::Panic:
|
||||
return std::unexpected(WasmTER{.ter = tecINTERNAL, .cost = std::nullopt});
|
||||
return std::unexpected{WasmTER{.ter = tecINTERNAL, .cost = std::nullopt}};
|
||||
}
|
||||
std::unreachable();
|
||||
}
|
||||
|
||||
// Call into the engine, answering `onThrow` if the call throws.
|
||||
//
|
||||
// The engine reports every outcome as a status rather than an exception, so anything
|
||||
// caught here is xrpld's own: a bad allocation, or a `funcName` that is not valid UTF-8
|
||||
// and so cannot become a `rust::Str`. Both entry points answer such a failure the way
|
||||
// they answer a defect in the engine itself.
|
||||
//
|
||||
// The counterpart of the engine's own `guarded`, which stops a Rust panic on the other
|
||||
// side of the bridge. Neither side may unwind into the other, and this is this side's
|
||||
// half. `HostContext`'s methods are `noexcept` rather than leaving this to cxx because
|
||||
// cxx's own `trycatch` catches only `std::exception`, and only for `Result` returns.
|
||||
template <class Call>
|
||||
std::invoke_result_t<Call>
|
||||
guarded(beast::Journal j, std::invoke_result_t<Call> onThrow, Call&& call)
|
||||
{
|
||||
try
|
||||
{
|
||||
return call();
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
JLOG(j.error()) << "wasm: engine call threw: " << e.what();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
JLOG(j.error()) << "wasm: engine call threw a non-exception";
|
||||
}
|
||||
|
||||
return onThrow;
|
||||
UNREACHABLE("Unexpected RunStatus value");
|
||||
}
|
||||
|
||||
// A screening verdict as a TER.
|
||||
@@ -137,7 +104,7 @@ verdict(CheckStatus status)
|
||||
case CheckStatus::Panic:
|
||||
return telFAILED_PROCESSING;
|
||||
}
|
||||
std::unreachable();
|
||||
UNREACHABLE("Unexpected CheckStatus value");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -153,9 +120,9 @@ runEscrowWasm(
|
||||
// non-positive limit means is a transaction-validity rule; the engine's own budget is
|
||||
// therefore an unsigned quantity with no invalid value to represent.
|
||||
if (gasLimit <= 0)
|
||||
return std::unexpected(WasmTER{.ter = temBAD_AMOUNT, .cost = std::nullopt});
|
||||
return std::unexpected{WasmTER{.ter = temBAD_AMOUNT, .cost = std::nullopt}};
|
||||
|
||||
auto const nodeSideFault = std::unexpected(WasmTER{.ter = tecINTERNAL, .cost = std::nullopt});
|
||||
auto const nodeSideFault = std::unexpected{WasmTER{.ter = tecINTERNAL, .cost = std::nullopt}};
|
||||
|
||||
return guarded(hfs.getJournal(), nodeSideFault, [&]() -> std::expected<EscrowResult, WasmTER> {
|
||||
// The host caches the current ledger object, the slot table and the
|
||||
@@ -170,15 +137,15 @@ runEscrowWasm(
|
||||
HostContext ctx{hfs};
|
||||
auto const run = rs::wasm_vm::run_escrow(
|
||||
ctx,
|
||||
rust::Slice<std::uint8_t const>(wasmCode.data(), wasmCode.size()),
|
||||
rust::Slice<std::uint8_t const>{wasmCode.data(), wasmCode.size()},
|
||||
static_cast<std::uint64_t>(gasLimit),
|
||||
rust::Str(funcName.data(), funcName.size()));
|
||||
rust::Str{funcName.data(), funcName.size()});
|
||||
|
||||
auto const result = outcome(run);
|
||||
if (!result)
|
||||
{
|
||||
JLOG(hfs.getJournal().warn())
|
||||
<< "wasm: " << std::string_view(run.detail.data(), run.detail.size())
|
||||
<< "wasm: " << std::string_view{run.detail.data(), run.detail.size()}
|
||||
<< ", ter: " << transToken(result.error().ter);
|
||||
}
|
||||
return result;
|
||||
@@ -190,14 +157,14 @@ preflightEscrowWasm(Bytes const& wasmCode, beast::Journal j, std::string_view fu
|
||||
{
|
||||
return guarded(j, NotTEC{telFAILED_PROCESSING}, [&]() {
|
||||
auto const checked = rs::wasm_vm::check_escrow(
|
||||
rust::Slice<std::uint8_t const>(wasmCode.data(), wasmCode.size()),
|
||||
rust::Str(funcName.data(), funcName.size()));
|
||||
rust::Slice<std::uint8_t const>{wasmCode.data(), wasmCode.size()},
|
||||
rust::Str{funcName.data(), funcName.size()});
|
||||
|
||||
auto const ter = verdict(checked.status);
|
||||
if (!isTesSuccess(ter))
|
||||
{
|
||||
JLOG(j.warn()) << "wasm: "
|
||||
<< std::string_view(checked.detail.data(), checked.detail.size())
|
||||
<< std::string_view{checked.detail.data(), checked.detail.size()}
|
||||
<< ", ter: " << transToken(ter);
|
||||
}
|
||||
return ter;
|
||||
|
||||
@@ -1,310 +0,0 @@
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#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/WasmFixture.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
namespace {
|
||||
|
||||
using testing::Return;
|
||||
|
||||
// The code a host error crosses as. The guest sees it as the host function's return value,
|
||||
// so a soft failure is the contract's to interpret rather than the engine's to trap on.
|
||||
std::int32_t
|
||||
code(HostFunctionError error)
|
||||
{
|
||||
return hfErrorToInt(error);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// ---------------------------------------------------------------------------------------
|
||||
// ldgr_index — no input, one scalar output
|
||||
// ---------------------------------------------------------------------------------------
|
||||
|
||||
class LedgerSqnCall : public HostCallTest
|
||||
{
|
||||
protected:
|
||||
[[nodiscard]] std::string
|
||||
wat() const override
|
||||
{
|
||||
return std::string{R"wat(
|
||||
(module
|
||||
(import "host_lib" "ldgr_index" (func $ldgr_index (param i32 i32) (result i32)))
|
||||
(memory (export "memory") 1)
|
||||
|
||||
;; Four bytes is what the value needs. Returns what the host wrote, or its error code.
|
||||
(func (export "escrow_finish") (result i32)
|
||||
(local $n i32)
|
||||
(local.set $n (call $ldgr_index (i32.const 0) (i32.const 4)))
|
||||
(select (local.get $n) (i32.load (i32.const 0)) (i32.lt_s (local.get $n) (i32.const 0))))
|
||||
|
||||
;; Two bytes is not enough for the value. Returns the host's code when memory is still
|
||||
;; zero, or 1 if anything was written into it - so a refused write is visibly a refusal
|
||||
;; and not a truncation.
|
||||
(func (export "into_two_bytes") (result i32)
|
||||
(local $n i32)
|
||||
(local.set $n (call $ldgr_index (i32.const 0) (i32.const 2)))
|
||||
(select (local.get $n) (i32.const 1) (i32.eqz (i32.load (i32.const 0))))))
|
||||
)wat"};
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(LedgerSqnCall, SequenceReachesGuestAsFourLittleEndianBytes)
|
||||
{
|
||||
EXPECT_CALL(host_, getLedgerSqn()).WillOnce(Return(0x01020304u));
|
||||
|
||||
// Read back with `i32.load`, which is little-endian by the wasm spec — so the value
|
||||
// arriving intact is the byte order being right.
|
||||
EXPECT_EQ(hostAnswer(), 0x01020304);
|
||||
}
|
||||
|
||||
TEST_F(LedgerSqnCall, HostErrorBecomesContractReturnValue)
|
||||
{
|
||||
EXPECT_CALL(host_, getLedgerSqn())
|
||||
.WillOnce(Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), code(HostFunctionError::LedgerObjNotFound));
|
||||
}
|
||||
|
||||
// The engine decides the fit, not the host: the host is never told the guest's capacity, it
|
||||
// reports the value's true length and the engine turns a length past the buffer into
|
||||
// `BufferTooSmall` — with nothing written.
|
||||
TEST_F(LedgerSqnCall, BufferTooSmallIsRefusedWholeNotTruncated)
|
||||
{
|
||||
EXPECT_CALL(host_, getLedgerSqn()).WillOnce(Return(0x01020304u));
|
||||
|
||||
EXPECT_EQ(hostAnswer("into_two_bytes"), code(HostFunctionError::BufferTooSmall));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------
|
||||
// home_le_field — a scalar field code in, bytes out
|
||||
// ---------------------------------------------------------------------------------------
|
||||
|
||||
class CurrentLedgerObjFieldCall : public HostCallTest
|
||||
{
|
||||
protected:
|
||||
// The field code the guest asks for. A real one, so the shim's `SField` lookup has
|
||||
// something to find.
|
||||
std::int32_t fieldCode_ = sfBalance.getCode();
|
||||
|
||||
[[nodiscard]] std::string
|
||||
wat() const override
|
||||
{
|
||||
return std::string{R"wat(
|
||||
(module
|
||||
(import "host_lib" "home_le_field" (func $home_le_field (param i32 i32 i32) (result i32)))
|
||||
(memory (export "memory") 1)
|
||||
(func (export "escrow_finish") (result i32)
|
||||
(call $home_le_field (i32.const )wat"} +
|
||||
std::to_string(fieldCode_) + R"wat() (i32.const 0) (i32.const 32))))
|
||||
)wat";
|
||||
}
|
||||
};
|
||||
|
||||
// The shim turns the guest's `i32` into the `SField` the C++ interface takes; asserting on
|
||||
// the argument is what pins that translation rather than assuming it.
|
||||
TEST_F(CurrentLedgerObjFieldCall, FieldCodeBecomesSFieldHostIsAskedFor)
|
||||
{
|
||||
EXPECT_CALL(host_, getCurrentLedgerObjField(testing::Ref(sfBalance)))
|
||||
.WillOnce(Return(Bytes{1, 2, 3}));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), 3) << "the length the host reported";
|
||||
}
|
||||
|
||||
TEST_F(CurrentLedgerObjFieldCall, UnknownFieldCodeIsRefusedWithoutAskingHost)
|
||||
{
|
||||
fieldCode_ = 0x7fff'0000; // a type nothing is registered under
|
||||
EXPECT_CALL(host_, getCurrentLedgerObjField).Times(0);
|
||||
|
||||
EXPECT_EQ(hostAnswer(), code(HostFunctionError::InvalidField));
|
||||
}
|
||||
|
||||
TEST_F(CurrentLedgerObjFieldCall, HostErrorBecomesContractReturnValue)
|
||||
{
|
||||
EXPECT_CALL(host_, getCurrentLedgerObjField)
|
||||
.WillOnce(Return(std::unexpected(HostFunctionError::FieldNotFound)));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), code(HostFunctionError::FieldNotFound));
|
||||
}
|
||||
|
||||
// The field cap bounds the status, not just the bytes: a host reporting a length past
|
||||
// `kMaxWasmDataLength` is too large whatever the guest's buffer was.
|
||||
TEST_F(CurrentLedgerObjFieldCall, FieldPastProtocolCapIsTooLarge)
|
||||
{
|
||||
EXPECT_CALL(host_, getCurrentLedgerObjField)
|
||||
.WillOnce(Return(Bytes(kMaxWasmDataLength + 1, 0xab)));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), code(HostFunctionError::DataFieldTooLarge));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------
|
||||
// sha512_half — bytes in and bytes out, the shape that needs the engine's output buffer
|
||||
// ---------------------------------------------------------------------------------------
|
||||
|
||||
class Sha512HalfCall : public HostCallTest
|
||||
{
|
||||
protected:
|
||||
[[nodiscard]] std::string
|
||||
wat() const override
|
||||
{
|
||||
return std::string{R"wat(
|
||||
(module
|
||||
(import "host_lib" "sha512_half" (func $sha512_half (param i32 i32 i32 i32) (result i32)))
|
||||
(memory (export "memory") 1)
|
||||
(data (i32.const 64) "abc")
|
||||
|
||||
;; Hashes the three bytes at 64 into the 32 at 0, then returns the first four bytes of the
|
||||
;; digest so the answer is shown to have arrived, not just been counted.
|
||||
(func (export "escrow_finish") (result i32)
|
||||
(local $n i32)
|
||||
(local.set $n (call $sha512_half (i32.const 64) (i32.const 3) (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 the host gave, for the cases where the digest itself is not the point.
|
||||
(func (export "digest_length") (result i32)
|
||||
(call $sha512_half (i32.const 64) (i32.const 3) (i32.const 0) (i32.const 32))))
|
||||
)wat"};
|
||||
}
|
||||
|
||||
// A digest whose first four bytes are distinctive, so the load below cannot pass by
|
||||
// accident.
|
||||
static Hash
|
||||
digest()
|
||||
{
|
||||
Hash value;
|
||||
value.begin()[0] = 0x0d;
|
||||
value.begin()[1] = 0x0c;
|
||||
value.begin()[2] = 0x0b;
|
||||
value.begin()[3] = 0x0a;
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
// Both directions in one call: the guest's bytes reach the host borrowed from its memory, and
|
||||
// the answer comes back into the same memory through the engine's buffer.
|
||||
TEST_F(Sha512HalfCall, GuestBytesReachHostAndDigestComesBack)
|
||||
{
|
||||
EXPECT_CALL(host_, computeSha512HalfHash(BytesAre("abc"))).WillOnce(Return(digest()));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), 0x0a0b0c0d) << "the digest's first four bytes, little-endian";
|
||||
}
|
||||
|
||||
TEST_F(Sha512HalfCall, DigestIsThirtyTwoBytes)
|
||||
{
|
||||
EXPECT_CALL(host_, computeSha512HalfHash).WillOnce(Return(digest()));
|
||||
|
||||
EXPECT_EQ(hostAnswer("digest_length"), 32);
|
||||
}
|
||||
|
||||
TEST_F(Sha512HalfCall, HostErrorBecomesContractReturnValue)
|
||||
{
|
||||
EXPECT_CALL(host_, computeSha512HalfHash)
|
||||
.WillOnce(Return(std::unexpected(HostFunctionError::InvalidParams)));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), code(HostFunctionError::InvalidParams));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------
|
||||
// trace — two byte inputs and a flag, no output
|
||||
// ---------------------------------------------------------------------------------------
|
||||
|
||||
class TraceCall : public HostCallTest
|
||||
{
|
||||
protected:
|
||||
[[nodiscard]] std::string
|
||||
wat() const override
|
||||
{
|
||||
return std::string{R"wat(
|
||||
(module
|
||||
(import "host_lib" "trace" (func $trace (param i32 i32 i32 i32 i32) (result i32)))
|
||||
(memory (export "memory") 1)
|
||||
(data (i32.const 0) "note")
|
||||
(data (i32.const 16) "\07\08")
|
||||
|
||||
(func (export "escrow_finish") (result i32)
|
||||
(call $trace (i32.const 0) (i32.const 4) (i32.const 16) (i32.const 2) (i32.const 1)))
|
||||
|
||||
(func (export "not_as_hex") (result i32)
|
||||
(call $trace (i32.const 0) (i32.const 4) (i32.const 16) (i32.const 2) (i32.const 0))))
|
||||
)wat"};
|
||||
}
|
||||
};
|
||||
|
||||
// Two borrowed regions in one call, which is the shape a single-input helper could not
|
||||
// express — so this pins that both arrive intact, and the flag with them.
|
||||
TEST_F(TraceCall, MessageDataAndFlagAllArrive)
|
||||
{
|
||||
EXPECT_CALL(host_, trace(std::string_view("note"), BytesAre("\x07\x08"), true))
|
||||
.WillOnce(Return(0));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), 0) << "a call with nothing to report answers 0";
|
||||
}
|
||||
|
||||
TEST_F(TraceCall, HexFlagIsGuestsToChoose)
|
||||
{
|
||||
EXPECT_CALL(host_, trace(testing::_, testing::_, false)).WillOnce(Return(0));
|
||||
|
||||
EXPECT_EQ(hostAnswer("not_as_hex"), 0);
|
||||
}
|
||||
|
||||
TEST_F(TraceCall, HostErrorBecomesContractReturnValue)
|
||||
{
|
||||
EXPECT_CALL(host_, trace).WillOnce(Return(std::unexpected(HostFunctionError::InvalidParams)));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), code(HostFunctionError::InvalidParams));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------
|
||||
// trace_num — a string and an i64, the ABI's only 64-bit parameter
|
||||
// ---------------------------------------------------------------------------------------
|
||||
|
||||
class TraceNumCall : public HostCallTest
|
||||
{
|
||||
protected:
|
||||
[[nodiscard]] std::string
|
||||
wat() const override
|
||||
{
|
||||
return std::string{R"wat(
|
||||
(module
|
||||
(import "host_lib" "trace_num" (func $trace_num (param i32 i32 i64) (result i32)))
|
||||
(memory (export "memory") 1)
|
||||
(data (i32.const 0) "count")
|
||||
|
||||
(func (export "escrow_finish") (result i32)
|
||||
(call $trace_num (i32.const 0) (i32.const 5) (i64.const -9223372036854775808))))
|
||||
)wat"};
|
||||
}
|
||||
};
|
||||
|
||||
// The extreme value on purpose: an `i64` that a truncating or sign-losing conversion anywhere
|
||||
// on the wire would visibly mangle.
|
||||
TEST_F(TraceNumCall, I64ArrivesWholeIncludingMostNegativeValue)
|
||||
{
|
||||
EXPECT_CALL(
|
||||
host_, traceNum(std::string_view("count"), std::numeric_limits<std::int64_t>::min()))
|
||||
.WillOnce(Return(0));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), 0);
|
||||
}
|
||||
|
||||
TEST_F(TraceNumCall, HostErrorBecomesContractReturnValue)
|
||||
{
|
||||
EXPECT_CALL(host_, traceNum)
|
||||
.WillOnce(Return(std::unexpected(HostFunctionError::IndexOutOfBounds)));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), code(HostFunctionError::IndexOutOfBounds));
|
||||
}
|
||||
|
||||
} // namespace xrpl::test
|
||||
@@ -21,26 +21,10 @@ namespace xrpl::test {
|
||||
// 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!`.
|
||||
//
|
||||
// Every mocked method defaults to what the base class would have done, for the same reason:
|
||||
// gmock's own default for `std::expected<T, E>` is a *successful* `T{}`, so an un-stubbed
|
||||
// call would answer `0` and a test could pass on an answer nobody chose. `checkSelf`
|
||||
// defaults to `true` because that is the base's answer and `runEscrowWasm` refuses a host
|
||||
// that reports itself dirty.
|
||||
class MockHostFunctions : public HostFunctions
|
||||
struct MockHostFunctions : HostFunctions
|
||||
{
|
||||
public:
|
||||
explicit MockHostFunctions(beast::Journal journal) : HostFunctions(journal)
|
||||
{
|
||||
using testing::Return;
|
||||
auto const unimplemented = std::unexpected(HostFunctionError::Unimplemented);
|
||||
|
||||
ON_CALL(*this, checkSelf()).WillByDefault(Return(true));
|
||||
ON_CALL(*this, getLedgerSqn()).WillByDefault(Return(unimplemented));
|
||||
ON_CALL(*this, getCurrentLedgerObjField).WillByDefault(Return(unimplemented));
|
||||
ON_CALL(*this, computeSha512HalfHash).WillByDefault(Return(unimplemented));
|
||||
ON_CALL(*this, trace).WillByDefault(Return(unimplemented));
|
||||
ON_CALL(*this, traceNum).WillByDefault(Return(unimplemented));
|
||||
}
|
||||
|
||||
MOCK_METHOD(bool, checkSelf, (), (const, override));
|
||||
@@ -80,8 +64,8 @@ public:
|
||||
// an expectation can say *what* the guest asked the host to work on.
|
||||
MATCHER_P(BytesAre, expected, "")
|
||||
{
|
||||
return std::string_view(reinterpret_cast<char const*>(arg.data()), arg.size()) ==
|
||||
std::string_view(expected);
|
||||
return std::string_view{reinterpret_cast<char const*>(arg.data()), arg.size()} ==
|
||||
std::string_view{expected};
|
||||
}
|
||||
|
||||
} // namespace xrpl::test
|
||||
|
||||
@@ -27,27 +27,26 @@ constexpr std::string_view kRunnableWat = R"wat(
|
||||
// `preflightEscrowWasm` takes no host, so this fixture holds none - which is the point of
|
||||
// the signature, and what deriving from `WasmTest` would hide. Only a journal, to read the
|
||||
// refusal out of.
|
||||
class PreflightTest : public testing::Test
|
||||
struct PreflightTest : testing::Test
|
||||
{
|
||||
protected:
|
||||
CapturingSink sink_;
|
||||
CaptureSink sink{beast::Severity::Warning};
|
||||
|
||||
NotTEC
|
||||
preflight(std::string_view wat, std::string_view funcName = escrowFunctionName)
|
||||
{
|
||||
return preflightEscrowWasm(assembleWat(wat), beast::Journal{sink_}, funcName);
|
||||
return preflightEscrowWasm(assembleWat(wat), beast::Journal{sink}, funcName);
|
||||
}
|
||||
|
||||
NotTEC
|
||||
preflightBytes(Bytes const& wasm, std::string_view funcName = escrowFunctionName)
|
||||
{
|
||||
return preflightEscrowWasm(wasm, beast::Journal{sink_}, funcName);
|
||||
return preflightEscrowWasm(wasm, beast::Journal{sink}, funcName);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string const&
|
||||
[[nodiscard]] std::string
|
||||
logged() const
|
||||
{
|
||||
return sink_.text();
|
||||
return sink.messages();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -208,7 +207,7 @@ TEST_F(PreflightTest, ScreeningAgreesWithARun)
|
||||
// The run's own verdict on the same bytes. A refused module must not reach the
|
||||
// contract's first instruction; an accepted one must get past the entry-point
|
||||
// lookup, whatever it then does.
|
||||
testing::StrictMock<MockHostFunctions> host{beast::Journal{sink_}};
|
||||
testing::StrictMock<MockHostFunctions> host{beast::Journal{sink}};
|
||||
EXPECT_CALL(host, checkSelf()).WillRepeatedly(testing::Return(true));
|
||||
EXPECT_CALL(host, getLedgerSqn()).WillRepeatedly(testing::Return(7u));
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <helpers/CaptureSink.h>
|
||||
#include <tx/wasm/MockHostFunctions.h>
|
||||
#include <xrpl_wasm_testkit_cxxbridge/lib.h>
|
||||
|
||||
@@ -16,37 +17,6 @@
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
// Keeps what a run logged. The host's default journal is a null sink, which would let a
|
||||
// swallowed condition pass a test that only checks the TER.
|
||||
class CapturingSink : public beast::Journal::Sink
|
||||
{
|
||||
std::string text_;
|
||||
|
||||
public:
|
||||
CapturingSink() : Sink(beast::Severity::Warning, false)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
write(beast::Severity level, std::string const& text) override
|
||||
{
|
||||
writeAlways(level, text);
|
||||
}
|
||||
|
||||
void
|
||||
writeAlways(beast::Severity, std::string const& text) override
|
||||
{
|
||||
text_ += text;
|
||||
text_ += '\n';
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string const&
|
||||
text() const
|
||||
{
|
||||
return text_;
|
||||
}
|
||||
};
|
||||
|
||||
// Assemble `wat`. Throws `rust::Error` on a typo, which gtest reports against the test that
|
||||
// holds it.
|
||||
//
|
||||
@@ -55,7 +25,7 @@ public:
|
||||
inline Bytes
|
||||
assembleWat(std::string_view wat)
|
||||
{
|
||||
auto const wasm = rs::wasm_testkit::compile_wat(rust::Str(wat.data(), wat.size()));
|
||||
auto const wasm = rs::wasm_testkit::compile_wat(rust::Str{wat.data(), wat.size()});
|
||||
return Bytes{wasm.begin(), wasm.end()};
|
||||
}
|
||||
|
||||
@@ -66,18 +36,19 @@ assembleWat(std::string_view wat)
|
||||
// a test-only crate: the engine itself refuses text
|
||||
// (`the_vm_refuses_a_text_format_module`), because a text assembler on the consensus path
|
||||
// would make a transaction's validity a build flag.
|
||||
class WasmTest : public testing::Test
|
||||
struct WasmTest : testing::Test
|
||||
{
|
||||
protected:
|
||||
// Enough for every module here to run to completion; a test about budgets passes its own.
|
||||
static constexpr std::int64_t kAmpleGas = 100'000;
|
||||
|
||||
CapturingSink sink_;
|
||||
// Keeps what a run logged. The host's default journal is a null sink, which would let a
|
||||
// swallowed condition pass a test that only checks the TER.
|
||||
CaptureSink sink{beast::Severity::Warning};
|
||||
|
||||
// Strict: a host call no test asked for is a failure, not a warning. These modules import
|
||||
// exactly what they mean to exercise, so an unplanned call means the engine reached for
|
||||
// something on its own — which is the kind of surprise a test suite exists to catch.
|
||||
testing::StrictMock<MockHostFunctions> host_{beast::Journal{sink_}};
|
||||
testing::StrictMock<MockHostFunctions> host{beast::Journal{sink}};
|
||||
|
||||
WasmTest()
|
||||
{
|
||||
@@ -85,7 +56,7 @@ protected:
|
||||
// every test would have to say so. Declared once here, and any number of times
|
||||
// (including none, for the runs refused before the engine is reached). A test that
|
||||
// cares says otherwise and its own expectation wins.
|
||||
EXPECT_CALL(host_, checkSelf()).WillRepeatedly(testing::Return(true));
|
||||
EXPECT_CALL(host, checkSelf()).WillRepeatedly(testing::Return(true));
|
||||
}
|
||||
|
||||
static Bytes
|
||||
@@ -99,7 +70,7 @@ protected:
|
||||
std::int64_t gas = kAmpleGas,
|
||||
std::string_view entryPoint = escrowFunctionName)
|
||||
{
|
||||
return runEscrowWasm(assemble(wat), host_, gas, entryPoint);
|
||||
return runEscrowWasm(assemble(wat), host, gas, entryPoint);
|
||||
}
|
||||
|
||||
std::expected<EscrowResult, WasmTER>
|
||||
@@ -108,22 +79,21 @@ protected:
|
||||
std::int64_t gas = kAmpleGas,
|
||||
std::string_view entryPoint = escrowFunctionName)
|
||||
{
|
||||
return runEscrowWasm(wasm, host_, gas, entryPoint);
|
||||
return runEscrowWasm(wasm, host, gas, entryPoint);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string const&
|
||||
[[nodiscard]] std::string
|
||||
logged() const
|
||||
{
|
||||
return sink_.text();
|
||||
return sink.messages();
|
||||
}
|
||||
};
|
||||
|
||||
// Base for the per-host-function fixtures. Each derives, supplies the module that exercises
|
||||
// its own import, and runs it through `callHost()` — so a test says only what the host was
|
||||
// asked and what came back.
|
||||
class HostCallTest : public WasmTest
|
||||
struct HostCallTest : WasmTest
|
||||
{
|
||||
protected:
|
||||
// The module under test. One import, one `escrow_finish` that calls it.
|
||||
[[nodiscard]] virtual std::string
|
||||
wat() const = 0;
|
||||
|
||||
@@ -99,7 +99,7 @@ TEST_F(WasmVMTest, BudgetTooSmallToRunIsOutOfGas)
|
||||
// asked to run anything.
|
||||
TEST_F(WasmVMTest, NoGasIsRefusedAsMalformedRatherThanRun)
|
||||
{
|
||||
for (std::int64_t const gas : {std::int64_t{0}, std::int64_t{-1}})
|
||||
for (auto const gas : {std::int64_t{0}, std::int64_t{-1}})
|
||||
{
|
||||
auto const outcome = run(kEngineWat, gas);
|
||||
|
||||
@@ -126,13 +126,13 @@ TEST_F(WasmVMTest, HostCallWithNoExportedMemoryFails)
|
||||
TEST_F(WasmVMTest, ModuleThatWillNotInstantiateIsChargedToTheContract)
|
||||
{
|
||||
// 129 pages, not exported, so nothing outside the module declares it.
|
||||
constexpr std::string_view wat = R"wat(
|
||||
static constexpr std::string_view wat = R"wat(
|
||||
(module
|
||||
(memory 129)
|
||||
(func (export "escrow_finish") (result i32) (i32.const 0)))
|
||||
)wat";
|
||||
|
||||
EXPECT_EQ(preflightEscrowWasm(assembleWat(wat), beast::Journal{sink_}), tesSUCCESS)
|
||||
EXPECT_EQ(preflightEscrowWasm(assembleWat(wat), beast::Journal{sink}), tesSUCCESS)
|
||||
<< "screening cannot see an unexported memory";
|
||||
|
||||
auto const outcome = run(wat);
|
||||
@@ -147,7 +147,7 @@ TEST_F(WasmVMTest, ModuleThatWillNotInstantiateIsChargedToTheContract)
|
||||
// have screened.
|
||||
TEST_F(WasmVMTest, TrappingStartSectionIsChargedToTheContract)
|
||||
{
|
||||
constexpr std::string_view wat = R"wat(
|
||||
static constexpr std::string_view wat = R"wat(
|
||||
(module
|
||||
(memory (export "memory") 1)
|
||||
(func $init (unreachable))
|
||||
@@ -167,21 +167,26 @@ TEST_F(WasmVMTest, TrappingStartSectionIsChargedToTheContract)
|
||||
// did not happen, which is the node's fault and not the transaction's.
|
||||
TEST_F(WasmVMTest, UnrunnableModuleIsNodeSideFault)
|
||||
{
|
||||
struct
|
||||
struct Case
|
||||
{
|
||||
char const* what;
|
||||
Bytes code;
|
||||
std::string_view entryPoint;
|
||||
} const cases[] = {
|
||||
{.what = "not wasm at all", .code = Bytes{0, 1, 2, 3}, .entryPoint = escrowFunctionName},
|
||||
{.what = "empty", .code = Bytes{}, .entryPoint = escrowFunctionName},
|
||||
{.what = "no such export", .code = assemble(kEngineWat), .entryPoint = "no_such_export"},
|
||||
{.what = "export is not a function",
|
||||
.code = assemble(kEngineWat),
|
||||
.entryPoint = "not_a_function"},
|
||||
{.what = "export takes a parameter",
|
||||
.code = assemble(kEngineWat),
|
||||
.entryPoint = "wrong_signature"},
|
||||
};
|
||||
std::array const cases = {
|
||||
Case{
|
||||
.what = "not wasm at all", .code = Bytes{0, 1, 2, 3}, .entryPoint = escrowFunctionName},
|
||||
Case{.what = "empty", .code = Bytes{}, .entryPoint = escrowFunctionName},
|
||||
Case{
|
||||
.what = "no such export", .code = assemble(kEngineWat), .entryPoint = "no_such_export"},
|
||||
Case{
|
||||
.what = "export is not a function",
|
||||
.code = assemble(kEngineWat),
|
||||
.entryPoint = "not_a_function"},
|
||||
Case{
|
||||
.what = "export takes a parameter",
|
||||
.code = assemble(kEngineWat),
|
||||
.entryPoint = "wrong_signature"},
|
||||
};
|
||||
|
||||
for (auto const& c : cases)
|
||||
@@ -213,7 +218,7 @@ TEST_F(WasmVMTest, TextFormatModuleIsRejected)
|
||||
// contract's state.
|
||||
TEST_F(WasmVMTest, DirtyHostIsRefusedBeforeContractRuns)
|
||||
{
|
||||
EXPECT_CALL(host_, checkSelf()).WillOnce(testing::Return(false));
|
||||
EXPECT_CALL(host, checkSelf()).WillOnce(testing::Return(false));
|
||||
|
||||
auto const outcome = run(kEngineWat);
|
||||
|
||||
@@ -236,7 +241,7 @@ TEST_F(WasmVMTest, DirtyHostIsRefusedBeforeContractRuns)
|
||||
// -14 `NoMemExported`.
|
||||
TEST_F(WasmVMTest, SoftHostErrorCodesCrossUnchanged)
|
||||
{
|
||||
constexpr HostFunctionError kSoftErrors[] = {
|
||||
static constexpr HostFunctionError kSoftErrors[] = {
|
||||
HostFunctionError::FieldNotFound,
|
||||
HostFunctionError::BufferTooSmall,
|
||||
HostFunctionError::NoArray,
|
||||
@@ -258,7 +263,7 @@ TEST_F(WasmVMTest, SoftHostErrorCodesCrossUnchanged)
|
||||
};
|
||||
|
||||
auto refused = HostFunctionError::FieldNotFound;
|
||||
EXPECT_CALL(host_, getLedgerSqn())
|
||||
EXPECT_CALL(host, getLedgerSqn())
|
||||
.WillRepeatedly([&refused]() -> std::expected<std::uint32_t, HostFunctionError> {
|
||||
return std::unexpected(refused);
|
||||
});
|
||||
@@ -279,7 +284,7 @@ TEST_F(WasmVMTest, SoftHostErrorCodesCrossUnchanged)
|
||||
TEST_F(WasmVMTest, FatalHostErrorStopsRun)
|
||||
{
|
||||
auto refused = HostFunctionError::Unimplemented;
|
||||
EXPECT_CALL(host_, getLedgerSqn())
|
||||
EXPECT_CALL(host, getLedgerSqn())
|
||||
.WillRepeatedly([&refused]() -> std::expected<std::uint32_t, HostFunctionError> {
|
||||
return std::unexpected(refused);
|
||||
});
|
||||
@@ -298,7 +303,7 @@ TEST_F(WasmVMTest, FatalHostErrorStopsRun)
|
||||
// the host, and must not take the node with it.
|
||||
TEST_F(WasmVMTest, ThrowingHostFunctionBecomesInternal)
|
||||
{
|
||||
EXPECT_CALL(host_, getLedgerSqn())
|
||||
EXPECT_CALL(host, getLedgerSqn())
|
||||
.WillOnce([]() -> std::expected<std::uint32_t, HostFunctionError> {
|
||||
Throw<std::runtime_error>("the ledger came apart");
|
||||
});
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
#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/WasmFixture.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <string>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
using testing::Return;
|
||||
|
||||
// home_le_field — a scalar field code in, bytes out.
|
||||
struct CurrentLedgerObjFieldCall : HostCallTest
|
||||
{
|
||||
// The field code the guest asks for. A real one, so the shim's `SField` lookup has
|
||||
// something to find.
|
||||
std::int32_t fieldCode = sfBalance.getCode();
|
||||
|
||||
[[nodiscard]] std::string
|
||||
wat() const override
|
||||
{
|
||||
return std::string{R"wat(
|
||||
(module
|
||||
(import "host_lib" "home_le_field" (func $home_le_field (param i32 i32 i32) (result i32)))
|
||||
(memory (export "memory") 1)
|
||||
(func (export "escrow_finish") (result i32)
|
||||
(call $home_le_field (i32.const )wat"} +
|
||||
std::to_string(fieldCode) + R"wat() (i32.const 0) (i32.const 32))))
|
||||
)wat";
|
||||
}
|
||||
};
|
||||
|
||||
// The shim turns the guest's `i32` into the `SField` the C++ interface takes; asserting on
|
||||
// the argument is what pins that translation rather than assuming it.
|
||||
TEST_F(CurrentLedgerObjFieldCall, FieldCodeBecomesSFieldHostIsAskedFor)
|
||||
{
|
||||
EXPECT_CALL(host, getCurrentLedgerObjField(testing::Ref(sfBalance)))
|
||||
.WillOnce(Return(Bytes{1, 2, 3}));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), 3) << "the length the host reported";
|
||||
}
|
||||
|
||||
TEST_F(CurrentLedgerObjFieldCall, UnknownFieldCodeIsRefusedWithoutAskingHost)
|
||||
{
|
||||
fieldCode = 0x7fff'0000; // a type nothing is registered under
|
||||
EXPECT_CALL(host, getCurrentLedgerObjField).Times(0);
|
||||
|
||||
EXPECT_EQ(hostAnswer(), hfErrorToInt(HostFunctionError::InvalidField));
|
||||
}
|
||||
|
||||
TEST_F(CurrentLedgerObjFieldCall, HostErrorBecomesContractReturnValue)
|
||||
{
|
||||
EXPECT_CALL(host, getCurrentLedgerObjField)
|
||||
.WillOnce(Return(std::unexpected(HostFunctionError::FieldNotFound)));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), hfErrorToInt(HostFunctionError::FieldNotFound));
|
||||
}
|
||||
|
||||
// The field cap bounds the status, not just the bytes: a host reporting a length past
|
||||
// `kMaxWasmDataLength` is too large whatever the guest's buffer was.
|
||||
TEST_F(CurrentLedgerObjFieldCall, FieldPastProtocolCapIsTooLarge)
|
||||
{
|
||||
EXPECT_CALL(host, getCurrentLedgerObjField)
|
||||
.WillOnce(Return(Bytes(kMaxWasmDataLength + 1, 0xab)));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), hfErrorToInt(HostFunctionError::DataFieldTooLarge));
|
||||
}
|
||||
|
||||
} // namespace xrpl::test
|
||||
69
src/tests/libxrpl/tx/wasm/host_calls/LedgerSqn.cpp
Normal file
69
src/tests/libxrpl/tx/wasm/host_calls/LedgerSqn.cpp
Normal file
@@ -0,0 +1,69 @@
|
||||
#include <xrpl/tx/wasm/WasmCommon.h>
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <tx/wasm/WasmFixture.h>
|
||||
|
||||
#include <expected>
|
||||
#include <string>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
using testing::Return;
|
||||
|
||||
// ldgr_index — no input, one scalar output.
|
||||
struct LedgerSqnCall : HostCallTest
|
||||
{
|
||||
[[nodiscard]] std::string
|
||||
wat() const override
|
||||
{
|
||||
return std::string{R"wat(
|
||||
(module
|
||||
(import "host_lib" "ldgr_index" (func $ldgr_index (param i32 i32) (result i32)))
|
||||
(memory (export "memory") 1)
|
||||
|
||||
;; Four bytes is what the value needs. Returns what the host wrote, or its error code.
|
||||
(func (export "escrow_finish") (result i32)
|
||||
(local $n i32)
|
||||
(local.set $n (call $ldgr_index (i32.const 0) (i32.const 4)))
|
||||
(select (local.get $n) (i32.load (i32.const 0)) (i32.lt_s (local.get $n) (i32.const 0))))
|
||||
|
||||
;; Two bytes is not enough for the value. Returns the host's code when memory is still
|
||||
;; zero, or 1 if anything was written into it - so a refused write is visibly a refusal
|
||||
;; and not a truncation.
|
||||
(func (export "into_two_bytes") (result i32)
|
||||
(local $n i32)
|
||||
(local.set $n (call $ldgr_index (i32.const 0) (i32.const 2)))
|
||||
(select (local.get $n) (i32.const 1) (i32.eqz (i32.load (i32.const 0))))))
|
||||
)wat"};
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(LedgerSqnCall, SequenceReachesGuestAsFourLittleEndianBytes)
|
||||
{
|
||||
EXPECT_CALL(host, getLedgerSqn()).WillOnce(Return(0x01020304u));
|
||||
|
||||
// Read back with `i32.load`, which is little-endian by the wasm spec — so the value
|
||||
// arriving intact is the byte order being right.
|
||||
EXPECT_EQ(hostAnswer(), 0x01020304);
|
||||
}
|
||||
|
||||
TEST_F(LedgerSqnCall, HostErrorBecomesContractReturnValue)
|
||||
{
|
||||
EXPECT_CALL(host, getLedgerSqn())
|
||||
.WillOnce(Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), hfErrorToInt(HostFunctionError::LedgerObjNotFound));
|
||||
}
|
||||
|
||||
// The engine decides the fit, not the host: the host is never told the guest's capacity, it
|
||||
// reports the value's true length and the engine turns a length past the buffer into
|
||||
// `BufferTooSmall` — with nothing written.
|
||||
TEST_F(LedgerSqnCall, BufferTooSmallIsRefusedWholeNotTruncated)
|
||||
{
|
||||
EXPECT_CALL(host, getLedgerSqn()).WillOnce(Return(0x01020304u));
|
||||
|
||||
EXPECT_EQ(hostAnswer("into_two_bytes"), hfErrorToInt(HostFunctionError::BufferTooSmall));
|
||||
}
|
||||
|
||||
} // namespace xrpl::test
|
||||
78
src/tests/libxrpl/tx/wasm/host_calls/Sha512Half.cpp
Normal file
78
src/tests/libxrpl/tx/wasm/host_calls/Sha512Half.cpp
Normal file
@@ -0,0 +1,78 @@
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/tx/wasm/WasmCommon.h>
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <tx/wasm/WasmFixture.h>
|
||||
|
||||
#include <expected>
|
||||
#include <string>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
using testing::Return;
|
||||
|
||||
// sha512_half — bytes in and bytes out, the shape that needs the engine's output buffer.
|
||||
struct Sha512HalfCall : HostCallTest
|
||||
{
|
||||
[[nodiscard]] std::string
|
||||
wat() const override
|
||||
{
|
||||
return std::string{R"wat(
|
||||
(module
|
||||
(import "host_lib" "sha512_half" (func $sha512_half (param i32 i32 i32 i32) (result i32)))
|
||||
(memory (export "memory") 1)
|
||||
(data (i32.const 64) "abc")
|
||||
|
||||
;; Hashes the three bytes at 64 into the 32 at 0, then returns the first four bytes of the
|
||||
;; digest so the answer is shown to have arrived, not just been counted.
|
||||
(func (export "escrow_finish") (result i32)
|
||||
(local $n i32)
|
||||
(local.set $n (call $sha512_half (i32.const 64) (i32.const 3) (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 the host gave, for the cases where the digest itself is not the point.
|
||||
(func (export "digest_length") (result i32)
|
||||
(call $sha512_half (i32.const 64) (i32.const 3) (i32.const 0) (i32.const 32))))
|
||||
)wat"};
|
||||
}
|
||||
|
||||
// A digest whose first four bytes are distinctive, so the load below cannot pass by
|
||||
// accident.
|
||||
static Hash
|
||||
digest()
|
||||
{
|
||||
Hash value;
|
||||
value.begin()[0] = 0x0d;
|
||||
value.begin()[1] = 0x0c;
|
||||
value.begin()[2] = 0x0b;
|
||||
value.begin()[3] = 0x0a;
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
// Both directions in one call: the guest's bytes reach the host borrowed from its memory, and
|
||||
// the answer comes back into the same memory through the engine's buffer.
|
||||
TEST_F(Sha512HalfCall, GuestBytesReachHostAndDigestComesBack)
|
||||
{
|
||||
EXPECT_CALL(host, computeSha512HalfHash(BytesAre("abc"))).WillOnce(Return(digest()));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), 0x0a0b0c0d) << "the digest's first four bytes, little-endian";
|
||||
}
|
||||
|
||||
TEST_F(Sha512HalfCall, DigestIsThirtyTwoBytes)
|
||||
{
|
||||
EXPECT_CALL(host, computeSha512HalfHash).WillOnce(Return(digest()));
|
||||
|
||||
EXPECT_EQ(hostAnswer("digest_length"), 32);
|
||||
}
|
||||
|
||||
TEST_F(Sha512HalfCall, HostErrorBecomesContractReturnValue)
|
||||
{
|
||||
EXPECT_CALL(host, computeSha512HalfHash)
|
||||
.WillOnce(Return(std::unexpected(HostFunctionError::InvalidParams)));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), hfErrorToInt(HostFunctionError::InvalidParams));
|
||||
}
|
||||
|
||||
} // namespace xrpl::test
|
||||
61
src/tests/libxrpl/tx/wasm/host_calls/Trace.cpp
Normal file
61
src/tests/libxrpl/tx/wasm/host_calls/Trace.cpp
Normal file
@@ -0,0 +1,61 @@
|
||||
#include <xrpl/tx/wasm/WasmCommon.h>
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <tx/wasm/WasmFixture.h>
|
||||
|
||||
#include <expected>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
using testing::Return;
|
||||
|
||||
// trace — two byte inputs and a flag, no output.
|
||||
struct TraceCall : HostCallTest
|
||||
{
|
||||
[[nodiscard]] std::string
|
||||
wat() const override
|
||||
{
|
||||
return std::string{R"wat(
|
||||
(module
|
||||
(import "host_lib" "trace" (func $trace (param i32 i32 i32 i32 i32) (result i32)))
|
||||
(memory (export "memory") 1)
|
||||
(data (i32.const 0) "note")
|
||||
(data (i32.const 16) "\07\08")
|
||||
|
||||
(func (export "escrow_finish") (result i32)
|
||||
(call $trace (i32.const 0) (i32.const 4) (i32.const 16) (i32.const 2) (i32.const 1)))
|
||||
|
||||
(func (export "not_as_hex") (result i32)
|
||||
(call $trace (i32.const 0) (i32.const 4) (i32.const 16) (i32.const 2) (i32.const 0))))
|
||||
)wat"};
|
||||
}
|
||||
};
|
||||
|
||||
// Two borrowed regions in one call, which is the shape a single-input helper could not
|
||||
// express — so this pins that both arrive intact, and the flag with them.
|
||||
TEST_F(TraceCall, MessageDataAndFlagAllArrive)
|
||||
{
|
||||
EXPECT_CALL(host, trace(std::string_view("note"), BytesAre("\x07\x08"), true))
|
||||
.WillOnce(Return(0));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), 0) << "a call with nothing to report answers 0";
|
||||
}
|
||||
|
||||
TEST_F(TraceCall, HexFlagIsGuestsToChoose)
|
||||
{
|
||||
EXPECT_CALL(host, trace(testing::_, testing::_, false)).WillOnce(Return(0));
|
||||
|
||||
EXPECT_EQ(hostAnswer("not_as_hex"), 0);
|
||||
}
|
||||
|
||||
TEST_F(TraceCall, HostErrorBecomesContractReturnValue)
|
||||
{
|
||||
EXPECT_CALL(host, trace).WillOnce(Return(std::unexpected(HostFunctionError::InvalidParams)));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), hfErrorToInt(HostFunctionError::InvalidParams));
|
||||
}
|
||||
|
||||
} // namespace xrpl::test
|
||||
53
src/tests/libxrpl/tx/wasm/host_calls/TraceNum.cpp
Normal file
53
src/tests/libxrpl/tx/wasm/host_calls/TraceNum.cpp
Normal file
@@ -0,0 +1,53 @@
|
||||
#include <xrpl/tx/wasm/WasmCommon.h>
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <tx/wasm/WasmFixture.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
using testing::Return;
|
||||
|
||||
// trace_num — a string and an i64, the ABI's only 64-bit parameter.
|
||||
struct TraceNumCall : HostCallTest
|
||||
{
|
||||
[[nodiscard]] std::string
|
||||
wat() const override
|
||||
{
|
||||
return std::string{R"wat(
|
||||
(module
|
||||
(import "host_lib" "trace_num" (func $trace_num (param i32 i32 i64) (result i32)))
|
||||
(memory (export "memory") 1)
|
||||
(data (i32.const 0) "count")
|
||||
|
||||
(func (export "escrow_finish") (result i32)
|
||||
(call $trace_num (i32.const 0) (i32.const 5) (i64.const -9223372036854775808))))
|
||||
)wat"};
|
||||
}
|
||||
};
|
||||
|
||||
// The extreme value on purpose: an `i64` that a truncating or sign-losing conversion anywhere
|
||||
// on the wire would visibly mangle.
|
||||
TEST_F(TraceNumCall, I64ArrivesWholeIncludingMostNegativeValue)
|
||||
{
|
||||
EXPECT_CALL(host, traceNum(std::string_view("count"), std::numeric_limits<std::int64_t>::min()))
|
||||
.WillOnce(Return(0));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), 0);
|
||||
}
|
||||
|
||||
TEST_F(TraceNumCall, HostErrorBecomesContractReturnValue)
|
||||
{
|
||||
EXPECT_CALL(host, traceNum)
|
||||
.WillOnce(Return(std::unexpected(HostFunctionError::IndexOutOfBounds)));
|
||||
|
||||
EXPECT_EQ(hostAnswer(), hfErrorToInt(HostFunctionError::IndexOutOfBounds));
|
||||
}
|
||||
|
||||
} // namespace xrpl::test
|
||||
Reference in New Issue
Block a user