mirror of
https://github.com/XRPLF/rippled.git
synced 2026-08-24 07:40:53 +00:00
Compare commits
14 Commits
audit_wasm
...
timothyban
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82876ca47c | ||
|
|
ec64f23736 | ||
|
|
dc0326a2a0 | ||
|
|
80d8e827d7 | ||
|
|
b84d25fbed | ||
|
|
fda9c6995e | ||
|
|
c6c68090f2 | ||
|
|
6c32ca3538 | ||
|
|
7122bb8a43 | ||
|
|
c5524e4881 | ||
|
|
e863db5061 | ||
|
|
d469fc2cdf | ||
|
|
362ea7a5d1 | ||
|
|
1b4fede15b |
@@ -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
@@ -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>
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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";
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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&
|
||||
|
||||
50
src/tests/libxrpl/tx/wasm/FloatFixture.h
Normal file
50
src/tests/libxrpl/tx/wasm/FloatFixture.h
Normal 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
|
||||
69
src/tests/libxrpl/tx/wasm/HostContextFixture.cpp
Normal file
69
src/tests/libxrpl/tx/wasm/HostContextFixture.cpp
Normal 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
|
||||
104
src/tests/libxrpl/tx/wasm/HostContextFixture.h
Normal file
104
src/tests/libxrpl/tx/wasm/HostContextFixture.h
Normal 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
|
||||
@@ -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()} ==
|
||||
|
||||
52
src/tests/libxrpl/tx/wasm/NFTFixture.cpp
Normal file
52
src/tests/libxrpl/tx/wasm/NFTFixture.cpp
Normal 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
|
||||
33
src/tests/libxrpl/tx/wasm/NFTFixture.h
Normal file
33
src/tests/libxrpl/tx/wasm/NFTFixture.h
Normal 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
|
||||
297
src/tests/libxrpl/tx/wasm/RealHostFixture.cpp
Normal file
297
src/tests/libxrpl/tx/wasm/RealHostFixture.cpp
Normal 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
|
||||
189
src/tests/libxrpl/tx/wasm/RealHostFixture.h
Normal file
189
src/tests/libxrpl/tx/wasm/RealHostFixture.h
Normal 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
|
||||
126
src/tests/libxrpl/tx/wasm/host_context/AccountKeylet.cpp
Normal file
126
src/tests/libxrpl/tx/wasm/host_context/AccountKeylet.cpp
Normal 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
|
||||
156
src/tests/libxrpl/tx/wasm/host_context/AmmKeylet.cpp
Normal file
156
src/tests/libxrpl/tx/wasm/host_context/AmmKeylet.cpp
Normal 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
|
||||
109
src/tests/libxrpl/tx/wasm/host_context/BaseFee.cpp
Normal file
109
src/tests/libxrpl/tx/wasm/host_context/BaseFee.cpp
Normal 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
|
||||
81
src/tests/libxrpl/tx/wasm/host_context/CacheLedgerObj.cpp
Normal file
81
src/tests/libxrpl/tx/wasm/host_context/CacheLedgerObj.cpp
Normal 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
|
||||
131
src/tests/libxrpl/tx/wasm/host_context/CheckKeylet.cpp
Normal file
131
src/tests/libxrpl/tx/wasm/host_context/CheckKeylet.cpp
Normal 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
|
||||
63
src/tests/libxrpl/tx/wasm/host_context/CheckSignature.cpp
Normal file
63
src/tests/libxrpl/tx/wasm/host_context/CheckSignature.cpp
Normal 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
|
||||
179
src/tests/libxrpl/tx/wasm/host_context/CredentialKeylet.cpp
Normal file
179
src/tests/libxrpl/tx/wasm/host_context/CredentialKeylet.cpp
Normal 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
|
||||
@@ -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
|
||||
112
src/tests/libxrpl/tx/wasm/host_context/CurrentLedgerObjField.cpp
Normal file
112
src/tests/libxrpl/tx/wasm/host_context/CurrentLedgerObjField.cpp
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
138
src/tests/libxrpl/tx/wasm/host_context/DelegateKeylet.cpp
Normal file
138
src/tests/libxrpl/tx/wasm/host_context/DelegateKeylet.cpp
Normal 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
|
||||
147
src/tests/libxrpl/tx/wasm/host_context/DepositPreauthKeylet.cpp
Normal file
147
src/tests/libxrpl/tx/wasm/host_context/DepositPreauthKeylet.cpp
Normal 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
|
||||
126
src/tests/libxrpl/tx/wasm/host_context/DidKeylet.cpp
Normal file
126
src/tests/libxrpl/tx/wasm/host_context/DidKeylet.cpp
Normal 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
|
||||
152
src/tests/libxrpl/tx/wasm/host_context/EscrowKeylet.cpp
Normal file
152
src/tests/libxrpl/tx/wasm/host_context/EscrowKeylet.cpp
Normal 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
|
||||
89
src/tests/libxrpl/tx/wasm/host_context/FloatAdd.cpp
Normal file
89
src/tests/libxrpl/tx/wasm/host_context/FloatAdd.cpp
Normal 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
|
||||
64
src/tests/libxrpl/tx/wasm/host_context/FloatCompare.cpp
Normal file
64
src/tests/libxrpl/tx/wasm/host_context/FloatCompare.cpp
Normal 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
|
||||
89
src/tests/libxrpl/tx/wasm/host_context/FloatDivide.cpp
Normal file
89
src/tests/libxrpl/tx/wasm/host_context/FloatDivide.cpp
Normal 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
|
||||
84
src/tests/libxrpl/tx/wasm/host_context/FloatFromInt.cpp
Normal file
84
src/tests/libxrpl/tx/wasm/host_context/FloatFromInt.cpp
Normal 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
|
||||
73
src/tests/libxrpl/tx/wasm/host_context/FloatFromMantExp.cpp
Normal file
73
src/tests/libxrpl/tx/wasm/host_context/FloatFromMantExp.cpp
Normal 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
|
||||
102
src/tests/libxrpl/tx/wasm/host_context/FloatFromSTAmount.cpp
Normal file
102
src/tests/libxrpl/tx/wasm/host_context/FloatFromSTAmount.cpp
Normal 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
|
||||
110
src/tests/libxrpl/tx/wasm/host_context/FloatFromSTNumber.cpp
Normal file
110
src/tests/libxrpl/tx/wasm/host_context/FloatFromSTNumber.cpp
Normal 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
|
||||
130
src/tests/libxrpl/tx/wasm/host_context/FloatFromUint.cpp
Normal file
130
src/tests/libxrpl/tx/wasm/host_context/FloatFromUint.cpp
Normal 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
|
||||
89
src/tests/libxrpl/tx/wasm/host_context/FloatMultiply.cpp
Normal file
89
src/tests/libxrpl/tx/wasm/host_context/FloatMultiply.cpp
Normal 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
|
||||
103
src/tests/libxrpl/tx/wasm/host_context/FloatPower.cpp
Normal file
103
src/tests/libxrpl/tx/wasm/host_context/FloatPower.cpp
Normal 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
|
||||
87
src/tests/libxrpl/tx/wasm/host_context/FloatRoot.cpp
Normal file
87
src/tests/libxrpl/tx/wasm/host_context/FloatRoot.cpp
Normal 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
|
||||
89
src/tests/libxrpl/tx/wasm/host_context/FloatSubtract.cpp
Normal file
89
src/tests/libxrpl/tx/wasm/host_context/FloatSubtract.cpp
Normal 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
|
||||
80
src/tests/libxrpl/tx/wasm/host_context/FloatToInt.cpp
Normal file
80
src/tests/libxrpl/tx/wasm/host_context/FloatToInt.cpp
Normal 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
|
||||
101
src/tests/libxrpl/tx/wasm/host_context/FloatToMantExp.cpp
Normal file
101
src/tests/libxrpl/tx/wasm/host_context/FloatToMantExp.cpp
Normal 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
|
||||
108
src/tests/libxrpl/tx/wasm/host_context/IsAmendmentEnabled.cpp
Normal file
108
src/tests/libxrpl/tx/wasm/host_context/IsAmendmentEnabled.cpp
Normal 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
|
||||
84
src/tests/libxrpl/tx/wasm/host_context/LedgerObjArrayLen.cpp
Normal file
84
src/tests/libxrpl/tx/wasm/host_context/LedgerObjArrayLen.cpp
Normal 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
|
||||
137
src/tests/libxrpl/tx/wasm/host_context/LedgerObjField.cpp
Normal file
137
src/tests/libxrpl/tx/wasm/host_context/LedgerObjField.cpp
Normal 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
|
||||
@@ -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
|
||||
148
src/tests/libxrpl/tx/wasm/host_context/LedgerObjNestedField.cpp
Normal file
148
src/tests/libxrpl/tx/wasm/host_context/LedgerObjNestedField.cpp
Normal 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
|
||||
76
src/tests/libxrpl/tx/wasm/host_context/LedgerSqn.cpp
Normal file
76
src/tests/libxrpl/tx/wasm/host_context/LedgerSqn.cpp
Normal 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
|
||||
131
src/tests/libxrpl/tx/wasm/host_context/MptokenIssuanceKeylet.cpp
Normal file
131
src/tests/libxrpl/tx/wasm/host_context/MptokenIssuanceKeylet.cpp
Normal 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
|
||||
119
src/tests/libxrpl/tx/wasm/host_context/MptokenKeylet.cpp
Normal file
119
src/tests/libxrpl/tx/wasm/host_context/MptokenKeylet.cpp
Normal 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
|
||||
142
src/tests/libxrpl/tx/wasm/host_context/NFT.cpp
Normal file
142
src/tests/libxrpl/tx/wasm/host_context/NFT.cpp
Normal 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
|
||||
81
src/tests/libxrpl/tx/wasm/host_context/NFTFlags.cpp
Normal file
81
src/tests/libxrpl/tx/wasm/host_context/NFTFlags.cpp
Normal 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
|
||||
106
src/tests/libxrpl/tx/wasm/host_context/NFTIssuer.cpp
Normal file
106
src/tests/libxrpl/tx/wasm/host_context/NFTIssuer.cpp
Normal 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
|
||||
90
src/tests/libxrpl/tx/wasm/host_context/NFTSequence.cpp
Normal file
90
src/tests/libxrpl/tx/wasm/host_context/NFTSequence.cpp
Normal 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
|
||||
90
src/tests/libxrpl/tx/wasm/host_context/NFTTaxon.cpp
Normal file
90
src/tests/libxrpl/tx/wasm/host_context/NFTTaxon.cpp
Normal 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
|
||||
66
src/tests/libxrpl/tx/wasm/host_context/NFTTransferFee.cpp
Normal file
66
src/tests/libxrpl/tx/wasm/host_context/NFTTransferFee.cpp
Normal 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
|
||||
131
src/tests/libxrpl/tx/wasm/host_context/NftokenOfferKeylet.cpp
Normal file
131
src/tests/libxrpl/tx/wasm/host_context/NftokenOfferKeylet.cpp
Normal 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
|
||||
131
src/tests/libxrpl/tx/wasm/host_context/OfferKeylet.cpp
Normal file
131
src/tests/libxrpl/tx/wasm/host_context/OfferKeylet.cpp
Normal 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
|
||||
131
src/tests/libxrpl/tx/wasm/host_context/OracleKeylet.cpp
Normal file
131
src/tests/libxrpl/tx/wasm/host_context/OracleKeylet.cpp
Normal 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
|
||||
86
src/tests/libxrpl/tx/wasm/host_context/ParentLedgerHash.cpp
Normal file
86
src/tests/libxrpl/tx/wasm/host_context/ParentLedgerHash.cpp
Normal 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
|
||||
75
src/tests/libxrpl/tx/wasm/host_context/ParentLedgerTime.cpp
Normal file
75
src/tests/libxrpl/tx/wasm/host_context/ParentLedgerTime.cpp
Normal 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
|
||||
152
src/tests/libxrpl/tx/wasm/host_context/PaychannelKeylet.cpp
Normal file
152
src/tests/libxrpl/tx/wasm/host_context/PaychannelKeylet.cpp
Normal 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
|
||||
@@ -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
|
||||
81
src/tests/libxrpl/tx/wasm/host_context/Sha512Half.cpp
Normal file
81
src/tests/libxrpl/tx/wasm/host_context/Sha512Half.cpp
Normal 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
|
||||
126
src/tests/libxrpl/tx/wasm/host_context/SignerListKeylet.cpp
Normal file
126
src/tests/libxrpl/tx/wasm/host_context/SignerListKeylet.cpp
Normal 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
|
||||
131
src/tests/libxrpl/tx/wasm/host_context/TicketKeylet.cpp
Normal file
131
src/tests/libxrpl/tx/wasm/host_context/TicketKeylet.cpp
Normal 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
|
||||
54
src/tests/libxrpl/tx/wasm/host_context/Trace.cpp
Normal file
54
src/tests/libxrpl/tx/wasm/host_context/Trace.cpp
Normal 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
|
||||
180
src/tests/libxrpl/tx/wasm/host_context/TrustLineKeylet.cpp
Normal file
180
src/tests/libxrpl/tx/wasm/host_context/TrustLineKeylet.cpp
Normal 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
|
||||
56
src/tests/libxrpl/tx/wasm/host_context/TxArrayLen.cpp
Normal file
56
src/tests/libxrpl/tx/wasm/host_context/TxArrayLen.cpp
Normal 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
|
||||
126
src/tests/libxrpl/tx/wasm/host_context/TxField.cpp
Normal file
126
src/tests/libxrpl/tx/wasm/host_context/TxField.cpp
Normal 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
|
||||
76
src/tests/libxrpl/tx/wasm/host_context/TxNestedArrayLen.cpp
Normal file
76
src/tests/libxrpl/tx/wasm/host_context/TxNestedArrayLen.cpp
Normal 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
|
||||
116
src/tests/libxrpl/tx/wasm/host_context/TxNestedField.cpp
Normal file
116
src/tests/libxrpl/tx/wasm/host_context/TxNestedField.cpp
Normal 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
|
||||
58
src/tests/libxrpl/tx/wasm/host_context/UpdateData.cpp
Normal file
58
src/tests/libxrpl/tx/wasm/host_context/UpdateData.cpp
Normal 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
|
||||
131
src/tests/libxrpl/tx/wasm/host_context/VaultKeylet.cpp
Normal file
131
src/tests/libxrpl/tx/wasm/host_context/VaultKeylet.cpp
Normal 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
|
||||
33
src/tests/libxrpl/tx/wasm/host_functions/AccountKeylet.cpp
Normal file
33
src/tests/libxrpl/tx/wasm/host_functions/AccountKeylet.cpp
Normal 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
|
||||
38
src/tests/libxrpl/tx/wasm/host_functions/AmmKeylet.cpp
Normal file
38
src/tests/libxrpl/tx/wasm/host_functions/AmmKeylet.cpp
Normal 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
|
||||
15
src/tests/libxrpl/tx/wasm/host_functions/BaseFee.cpp
Normal file
15
src/tests/libxrpl/tx/wasm/host_functions/BaseFee.cpp
Normal 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
|
||||
98
src/tests/libxrpl/tx/wasm/host_functions/CacheLedgerObj.cpp
Normal file
98
src/tests/libxrpl/tx/wasm/host_functions/CacheLedgerObj.cpp
Normal 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
|
||||
30
src/tests/libxrpl/tx/wasm/host_functions/CheckKeylet.cpp
Normal file
30
src/tests/libxrpl/tx/wasm/host_functions/CheckKeylet.cpp
Normal 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
|
||||
80
src/tests/libxrpl/tx/wasm/host_functions/CheckSignature.cpp
Normal file
80
src/tests/libxrpl/tx/wasm/host_functions/CheckSignature.cpp
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
42
src/tests/libxrpl/tx/wasm/host_functions/DelegateKeylet.cpp
Normal file
42
src/tests/libxrpl/tx/wasm/host_functions/DelegateKeylet.cpp
Normal 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
|
||||
@@ -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
|
||||
27
src/tests/libxrpl/tx/wasm/host_functions/DidKeylet.cpp
Normal file
27
src/tests/libxrpl/tx/wasm/host_functions/DidKeylet.cpp
Normal 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
|
||||
43
src/tests/libxrpl/tx/wasm/host_functions/EscrowKeylet.cpp
Normal file
43
src/tests/libxrpl/tx/wasm/host_functions/EscrowKeylet.cpp
Normal 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
|
||||
49
src/tests/libxrpl/tx/wasm/host_functions/FloatAdd.cpp
Normal file
49
src/tests/libxrpl/tx/wasm/host_functions/FloatAdd.cpp
Normal 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
|
||||
48
src/tests/libxrpl/tx/wasm/host_functions/FloatCompare.cpp
Normal file
48
src/tests/libxrpl/tx/wasm/host_functions/FloatCompare.cpp
Normal 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
|
||||
73
src/tests/libxrpl/tx/wasm/host_functions/FloatDivide.cpp
Normal file
73
src/tests/libxrpl/tx/wasm/host_functions/FloatDivide.cpp
Normal 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
|
||||
35
src/tests/libxrpl/tx/wasm/host_functions/FloatFromInt.cpp
Normal file
35
src/tests/libxrpl/tx/wasm/host_functions/FloatFromInt.cpp
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
34
src/tests/libxrpl/tx/wasm/host_functions/FloatFromUint.cpp
Normal file
34
src/tests/libxrpl/tx/wasm/host_functions/FloatFromUint.cpp
Normal 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
|
||||
56
src/tests/libxrpl/tx/wasm/host_functions/FloatMultiply.cpp
Normal file
56
src/tests/libxrpl/tx/wasm/host_functions/FloatMultiply.cpp
Normal 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
Reference in New Issue
Block a user