From 362ea7a5d1ea67cd8a8bc11fc8d50635504734cd Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Wed, 12 Aug 2026 18:11:13 +0100 Subject: [PATCH] More tests --- .../libxrpl/tx/wasm/HostContextFixture.cpp | 17 + .../libxrpl/tx/wasm/HostContextFixture.h | 48 +++ src/tests/libxrpl/tx/wasm/MockHostFunctions.h | 366 +++++++++++++++++- .../tx/wasm/host_context/AccountKeylet.cpp | 126 ++++++ .../tx/wasm/host_context/AmmKeylet.cpp | 156 ++++++++ .../libxrpl/tx/wasm/host_context/BaseFee.cpp | 109 ++++++ .../tx/wasm/host_context/CacheLedgerObj.cpp | 81 ++++ .../tx/wasm/host_context/CheckKeylet.cpp | 131 +++++++ .../tx/wasm/host_context/CheckSignature.cpp | 63 +++ .../tx/wasm/host_context/CredentialKeylet.cpp | 179 +++++++++ .../host_context/CurrentLedgerObjArrayLen.cpp | 63 +++ .../host_context/CurrentLedgerObjField.cpp | 112 ++++++ .../CurrentLedgerObjNestedArrayLen.cpp | 78 ++++ .../CurrentLedgerObjNestedField.cpp | 121 ++++++ .../tx/wasm/host_context/DelegateKeylet.cpp | 138 +++++++ .../host_context/DepositPreauthKeylet.cpp | 147 +++++++ .../tx/wasm/host_context/DidKeylet.cpp | 126 ++++++ .../tx/wasm/host_context/EscrowKeylet.cpp | 152 ++++++++ .../libxrpl/tx/wasm/host_context/FloatAdd.cpp | 89 +++++ .../tx/wasm/host_context/FloatCompare.cpp | 64 +++ .../tx/wasm/host_context/FloatDivide.cpp | 89 +++++ .../tx/wasm/host_context/FloatFromInt.cpp | 84 ++++ .../tx/wasm/host_context/FloatFromMantExp.cpp | 73 ++++ .../wasm/host_context/FloatFromSTAmount.cpp | 102 +++++ .../wasm/host_context/FloatFromSTNumber.cpp | 110 ++++++ .../tx/wasm/host_context/FloatFromUint.cpp | 131 +++++++ .../tx/wasm/host_context/FloatMultiply.cpp | 89 +++++ .../tx/wasm/host_context/FloatPower.cpp | 103 +++++ .../tx/wasm/host_context/FloatRoot.cpp | 87 +++++ .../tx/wasm/host_context/FloatSubtract.cpp | 89 +++++ .../tx/wasm/host_context/FloatToInt.cpp | 81 ++++ .../tx/wasm/host_context/FloatToMantExp.cpp | 102 +++++ .../wasm/host_context/IsAmendmentEnabled.cpp | 108 ++++++ .../wasm/host_context/LedgerObjArrayLen.cpp | 84 ++++ .../tx/wasm/host_context/LedgerObjField.cpp | 137 +++++++ .../host_context/LedgerObjNestedArrayLen.cpp | 97 +++++ .../host_context/LedgerObjNestedField.cpp | 149 +++++++ .../tx/wasm/host_context/LedgerSqn.cpp | 76 ++++ .../host_context/MptokenIssuanceKeylet.cpp | 131 +++++++ .../tx/wasm/host_context/MptokenKeylet.cpp | 119 ++++++ .../libxrpl/tx/wasm/host_context/NFT.cpp | 142 +++++++ .../libxrpl/tx/wasm/host_context/NFTFlags.cpp | 81 ++++ .../tx/wasm/host_context/NFTIssuer.cpp | 106 +++++ .../tx/wasm/host_context/NFTSequence.cpp | 90 +++++ .../libxrpl/tx/wasm/host_context/NFTTaxon.cpp | 90 +++++ .../tx/wasm/host_context/NFTTransferFee.cpp | 66 ++++ .../wasm/host_context/NftokenOfferKeylet.cpp | 131 +++++++ .../tx/wasm/host_context/OfferKeylet.cpp | 131 +++++++ .../tx/wasm/host_context/OracleKeylet.cpp | 131 +++++++ .../tx/wasm/host_context/ParentLedgerHash.cpp | 86 ++++ .../tx/wasm/host_context/ParentLedgerTime.cpp | 75 ++++ .../tx/wasm/host_context/PaychannelKeylet.cpp | 152 ++++++++ .../host_context/PermissionedDomainKeylet.cpp | 131 +++++++ .../tx/wasm/host_context/Sha512Half.cpp | 81 ++++ .../tx/wasm/host_context/SignerListKeylet.cpp | 126 ++++++ .../tx/wasm/host_context/TicketKeylet.cpp | 131 +++++++ .../libxrpl/tx/wasm/host_context/Trace.cpp | 54 +++ .../tx/wasm/host_context/TrustLineKeylet.cpp | 180 +++++++++ .../tx/wasm/host_context/TxArrayLen.cpp | 56 +++ .../tx/wasm/host_context/TxNestedArrayLen.cpp | 76 ++++ .../tx/wasm/host_context/TxNestedField.cpp | 117 ++++++ .../tx/wasm/host_context/UpdateData.cpp | 58 +++ .../tx/wasm/host_context/VaultKeylet.cpp | 131 +++++++ 63 files changed, 6822 insertions(+), 7 deletions(-) create mode 100644 src/tests/libxrpl/tx/wasm/host_context/AccountKeylet.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/AmmKeylet.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/BaseFee.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/CacheLedgerObj.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/CheckKeylet.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/CheckSignature.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/CredentialKeylet.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/CurrentLedgerObjArrayLen.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/CurrentLedgerObjField.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/CurrentLedgerObjNestedArrayLen.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/CurrentLedgerObjNestedField.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/DelegateKeylet.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/DepositPreauthKeylet.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/DidKeylet.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/EscrowKeylet.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/FloatAdd.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/FloatCompare.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/FloatDivide.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/FloatFromInt.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/FloatFromMantExp.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/FloatFromSTAmount.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/FloatFromSTNumber.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/FloatFromUint.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/FloatMultiply.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/FloatPower.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/FloatRoot.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/FloatSubtract.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/FloatToInt.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/FloatToMantExp.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/IsAmendmentEnabled.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/LedgerObjArrayLen.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/LedgerObjField.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/LedgerObjNestedArrayLen.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/LedgerObjNestedField.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/LedgerSqn.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/MptokenIssuanceKeylet.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/MptokenKeylet.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/NFT.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/NFTFlags.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/NFTIssuer.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/NFTSequence.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/NFTTaxon.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/NFTTransferFee.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/NftokenOfferKeylet.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/OfferKeylet.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/OracleKeylet.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/ParentLedgerHash.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/ParentLedgerTime.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/PaychannelKeylet.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/PermissionedDomainKeylet.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/Sha512Half.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/SignerListKeylet.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/TicketKeylet.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/Trace.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/TrustLineKeylet.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/TxArrayLen.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/TxNestedArrayLen.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/TxNestedField.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/UpdateData.cpp create mode 100644 src/tests/libxrpl/tx/wasm/host_context/VaultKeylet.cpp diff --git a/src/tests/libxrpl/tx/wasm/HostContextFixture.cpp b/src/tests/libxrpl/tx/wasm/HostContextFixture.cpp index 341218a7d5..cf5efc0453 100644 --- a/src/tests/libxrpl/tx/wasm/HostContextFixture.cpp +++ b/src/tests/libxrpl/tx/wasm/HostContextFixture.cpp @@ -1,5 +1,9 @@ #include +#include + +#include + #include #include #include @@ -14,6 +18,19 @@ HostContextTest::bytesOf(Bytes const& bytes) return rust::Slice{bytes.data(), bytes.size()}; } +Bytes +HostContextTest::bytesOfSteps(std::vector 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) { } diff --git a/src/tests/libxrpl/tx/wasm/HostContextFixture.h b/src/tests/libxrpl/tx/wasm/HostContextFixture.h index 6499cccf59..1677014b8a 100644 --- a/src/tests/libxrpl/tx/wasm/HostContextFixture.h +++ b/src/tests/libxrpl/tx/wasm/HostContextFixture.h @@ -13,6 +13,7 @@ #include #include #include +#include #include namespace xrpl::test { @@ -24,6 +25,31 @@ struct HostContextTest : testing::Test static rust::Slice 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 + static Bytes + bytesOfScalar(T value) + { + static_assert(std::is_integral_v, "Only integral types"); + + auto const bits = static_cast>(value); + Bytes bytes(sizeof(bits)); + for (std::size_t i = 0; i < sizeof(bits); ++i) + { + bytes[i] = static_cast(bits >> (i * 8)); + } + return bytes; + } + + // A locator's wire form: each step as four little-endian bytes. + static Bytes + bytesOfSteps(std::vector 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 @@ -53,4 +79,26 @@ struct HostContextTest : testing::Test 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(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 diff --git a/src/tests/libxrpl/tx/wasm/MockHostFunctions.h b/src/tests/libxrpl/tx/wasm/MockHostFunctions.h index 096b627d66..b00fd055ae 100644 --- a/src/tests/libxrpl/tx/wasm/MockHostFunctions.h +++ b/src/tests/libxrpl/tx/wasm/MockHostFunctions.h @@ -1,8 +1,14 @@ #pragma once #include +#include #include +#include +#include #include +#include +#include +#include #include #include @@ -16,11 +22,12 @@ namespace xrpl::test { // A mock of the host the wasm engine calls back into. // -// Only a few of `HostFunctions`' methods are mocked here. That is the mock lagging the ABI, -// not the ABI lacking coverage: `crates/xrpl-host-functions/src/lib.rs` already declares all -// 61 entries. Each one not yet mocked keeps `HostFunctions`' own -// `std::unexpected(Unimplemented)`, so a contract reaching for it fails the way production -// would. Add a `MOCK_METHOD` here as tests for that method are written. +// 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` 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) @@ -35,12 +42,126 @@ struct MockHostFunctions : HostFunctions (), (const, override)); + MOCK_METHOD( + (std::expected), + getParentLedgerTime, + (), + (const, override)); + + MOCK_METHOD( + (std::expected), + getParentLedgerHash, + (), + (const, override)); + + MOCK_METHOD( + (std::expected), + getBaseFee, + (), + (const, override)); + + MOCK_METHOD( + (std::expected), + isAmendmentEnabled, + (uint256 const& amendmentId), + (const, override)); + + MOCK_METHOD( + (std::expected), + isAmendmentEnabled, + (std::string_view const& amendmentName), + (const, override)); + + MOCK_METHOD( + (std::expected), + cacheLedgerObj, + (uint256 const& objId, std::int32_t cacheIdx), + (override)); + + MOCK_METHOD( + (std::expected), + getTxField, + (SField const& fname), + (const, override)); + MOCK_METHOD( (std::expected), getCurrentLedgerObjField, (SField const& fname), (const, override)); + MOCK_METHOD( + (std::expected), + getLedgerObjField, + (std::int32_t cacheIdx, SField const& fname), + (const, override)); + + MOCK_METHOD( + (std::expected), + getTxNestedField, + (FieldLocator const& locator), + (const, override)); + + MOCK_METHOD( + (std::expected), + getCurrentLedgerObjNestedField, + (FieldLocator const& locator), + (const, override)); + + MOCK_METHOD( + (std::expected), + getLedgerObjNestedField, + (std::int32_t cacheIdx, FieldLocator const& locator), + (const, override)); + + MOCK_METHOD( + (std::expected), + getTxArrayLen, + (SField const& fname), + (const, override)); + + MOCK_METHOD( + (std::expected), + getCurrentLedgerObjArrayLen, + (SField const& fname), + (const, override)); + + MOCK_METHOD( + (std::expected), + getLedgerObjArrayLen, + (std::int32_t cacheIdx, SField const& fname), + (const, override)); + + MOCK_METHOD( + (std::expected), + getTxNestedArrayLen, + (FieldLocator const& locator), + (const, override)); + + MOCK_METHOD( + (std::expected), + getCurrentLedgerObjNestedArrayLen, + (FieldLocator const& locator), + (const, override)); + + MOCK_METHOD( + (std::expected), + getLedgerObjNestedArrayLen, + (std::int32_t cacheIdx, FieldLocator const& locator), + (const, override)); + + MOCK_METHOD( + (std::expected), + updateData, + (Slice const& data), + (override)); + + MOCK_METHOD( + (std::expected), + checkSignature, + (Slice const& message, Slice const& signature, Slice const& pubkey), + (const, override)); + MOCK_METHOD( (std::expected), computeSha512HalfHash, @@ -49,8 +170,152 @@ struct MockHostFunctions : HostFunctions MOCK_METHOD( (std::expected), - getTxField, - (SField const& fname), + accountKeylet, + (AccountID const& account), + (const, override)); + + MOCK_METHOD( + (std::expected), + ammKeylet, + (Asset const& issue1, Asset const& issue2), + (const, override)); + + MOCK_METHOD( + (std::expected), + checkKeylet, + (AccountID const& account, std::uint32_t seq), + (const, override)); + + MOCK_METHOD( + (std::expected), + credentialKeylet, + (AccountID const& subject, AccountID const& issuer, Slice const& credentialType), + (const, override)); + + MOCK_METHOD( + (std::expected), + didKeylet, + (AccountID const& account), + (const, override)); + + MOCK_METHOD( + (std::expected), + delegateKeylet, + (AccountID const& account, AccountID const& authorize), + (const, override)); + + MOCK_METHOD( + (std::expected), + depositPreauthKeylet, + (AccountID const& account, AccountID const& authorize), + (const, override)); + + MOCK_METHOD( + (std::expected), + escrowKeylet, + (AccountID const& account, std::uint32_t seq), + (const, override)); + + MOCK_METHOD( + (std::expected), + trustLineKeylet, + (AccountID const& account1, AccountID const& account2, Currency const& currency), + (const, override)); + + MOCK_METHOD( + (std::expected), + mptokenIssuanceKeylet, + (AccountID const& issuer, std::uint32_t seq), + (const, override)); + + MOCK_METHOD( + (std::expected), + mptokenKeylet, + (MPTID const& mptid, AccountID const& holder), + (const, override)); + + MOCK_METHOD( + (std::expected), + nftokenOfferKeylet, + (AccountID const& account, std::uint32_t seq), + (const, override)); + + MOCK_METHOD( + (std::expected), + offerKeylet, + (AccountID const& account, std::uint32_t seq), + (const, override)); + + MOCK_METHOD( + (std::expected), + oracleKeylet, + (AccountID const& account, std::uint32_t docId), + (const, override)); + + MOCK_METHOD( + (std::expected), + paychannelKeylet, + (AccountID const& account, AccountID const& destination, std::uint32_t seq), + (const, override)); + + MOCK_METHOD( + (std::expected), + permissionedDomainKeylet, + (AccountID const& account, std::uint32_t seq), + (const, override)); + + MOCK_METHOD( + (std::expected), + signerListKeylet, + (AccountID const& account), + (const, override)); + + MOCK_METHOD( + (std::expected), + ticketKeylet, + (AccountID const& account, std::uint32_t seq), + (const, override)); + + MOCK_METHOD( + (std::expected), + vaultKeylet, + (AccountID const& account, std::uint32_t seq), + (const, override)); + + MOCK_METHOD( + (std::expected), + getNFT, + (AccountID const& account, uint256 const& nftId), + (const, override)); + + MOCK_METHOD( + (std::expected), + getNFTIssuer, + (uint256 const& nftId), + (const, override)); + + MOCK_METHOD( + (std::expected), + getNFTTaxon, + (uint256 const& nftId), + (const, override)); + + MOCK_METHOD( + (std::expected), + getNFTFlags, + (uint256 const& nftId), + (const, override)); + + MOCK_METHOD( + (std::expected), + getNFTTransferFee, + (uint256 const& nftId), + (const, override)); + + MOCK_METHOD( + (std::expected), + getNFTSequence, + (uint256 const& nftId), (const, override)); // Takes the rendered text, not the guest's buffer: rendering is `HostContext`'s, so what @@ -60,10 +325,97 @@ struct MockHostFunctions : HostFunctions trace, (std::string_view const& msg, std::string_view const& data), (const, override)); + + MOCK_METHOD( + (std::expected), + floatFromInt, + (std::int64_t x, std::int32_t mode), + (const, override)); + + MOCK_METHOD( + (std::expected), + floatFromUint, + (std::uint64_t x, std::int32_t mode), + (const, override)); + + MOCK_METHOD( + (std::expected), + floatFromSTAmount, + (STAmount const& x, std::int32_t mode), + (const, override)); + + MOCK_METHOD( + (std::expected), + floatFromSTNumber, + (STNumber const& x, std::int32_t mode), + (const, override)); + + MOCK_METHOD( + (std::expected), + floatToInt, + (Slice const& x, std::int32_t mode), + (const, override)); + + MOCK_METHOD( + (std::expected), + floatToMantExp, + (Slice const& x), + (const, override)); + + MOCK_METHOD( + (std::expected), + floatFromMantExp, + (std::int64_t mantissa, std::int32_t exponent, std::int32_t mode), + (const, override)); + + MOCK_METHOD( + (std::expected), + floatCompare, + (Slice const& x, Slice const& y), + (const, override)); + + MOCK_METHOD( + (std::expected), + floatAdd, + (Slice const& x, Slice const& y, std::int32_t mode), + (const, override)); + + MOCK_METHOD( + (std::expected), + floatSubtract, + (Slice const& x, Slice const& y, std::int32_t mode), + (const, override)); + + MOCK_METHOD( + (std::expected), + floatMultiply, + (Slice const& x, Slice const& y, std::int32_t mode), + (const, override)); + + MOCK_METHOD( + (std::expected), + floatDivide, + (Slice const& x, Slice const& y, std::int32_t mode), + (const, override)); + + MOCK_METHOD( + (std::expected), + floatRoot, + (Slice const& x, std::int32_t n, std::int32_t mode), + (const, override)); + + MOCK_METHOD( + (std::expected), + 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(arg.data()), arg.size()} == diff --git a/src/tests/libxrpl/tx/wasm/host_context/AccountKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_context/AccountKeylet.cpp new file mode 100644 index 0000000000..e64ef2c073 --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/AccountKeylet.cpp @@ -0,0 +1,126 @@ +#include +#include + +#include +#include +#include + +#include +#include +#include + +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(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(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(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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/AmmKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_context/AmmKeylet.cpp new file mode 100644 index 0000000000..e734bdc464 --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/AmmKeylet.cpp @@ -0,0 +1,156 @@ +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +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(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(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(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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/BaseFee.cpp b/src/tests/libxrpl/tx/wasm/host_context/BaseFee.cpp new file mode 100644 index 0000000000..373a47f483 --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/BaseFee.cpp @@ -0,0 +1,109 @@ +#include + +#include +#include +#include + +#include +#include +#include + +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 { + 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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/CacheLedgerObj.cpp b/src/tests/libxrpl/tx/wasm/host_context/CacheLedgerObj.cpp new file mode 100644 index 0000000000..8c3d015362 --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/CacheLedgerObj.cpp @@ -0,0 +1,81 @@ +#include +#include + +#include +#include +#include + +#include +#include + +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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/CheckKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_context/CheckKeylet.cpp new file mode 100644 index 0000000000..60191ec484 --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/CheckKeylet.cpp @@ -0,0 +1,131 @@ +#include +#include + +#include +#include +#include + +#include +#include +#include + +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(seq))) + .WillOnce(testing::Return(keylet)); + + OutRegion out{32}; + EXPECT_EQ( + hostContext.checkKeylet(bytesOf(accountBytes), seq, out.slice()), + static_cast(keylet.size())); + EXPECT_TRUE(out.holds(bytesOf(keylet))); +} + +TEST_F(CheckKeyletCall, HostErrorBecomesContractReturnValue) +{ + EXPECT_CALL(host, checkKeylet(account, static_cast(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(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(seq))) + .WillOnce(testing::Return(keylet)); + + OutRegion out{keylet.size() - 1}; + EXPECT_EQ( + hostContext.checkKeylet(bytesOf(accountBytes), seq, out.slice()), + static_cast(keylet.size())); + EXPECT_FALSE(out.wasWritten()); +} + +TEST_F(CheckKeyletCall, OutRegionOfExactSizeIsWritten) +{ + Bytes const keylet(32, 0xab); + EXPECT_CALL(host, checkKeylet(account, static_cast(seq))) + .WillOnce(testing::Return(keylet)); + + OutRegion out{keylet.size()}; + EXPECT_EQ( + hostContext.checkKeylet(bytesOf(accountBytes), seq, out.slice()), + static_cast(keylet.size())); + EXPECT_TRUE(out.holds(bytesOf(keylet))); +} + +TEST_F(CheckKeyletCall, EmptyResultAnswersZeroAndWritesNothing) +{ + EXPECT_CALL(host, checkKeylet(account, static_cast(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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/CheckSignature.cpp b/src/tests/libxrpl/tx/wasm/host_context/CheckSignature.cpp new file mode 100644 index 0000000000..796c56665c --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/CheckSignature.cpp @@ -0,0 +1,63 @@ +#include +#include + +#include +#include +#include +#include + +#include +#include + +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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/CredentialKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_context/CredentialKeylet.cpp new file mode 100644 index 0000000000..09d4c7b2fc --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/CredentialKeylet.cpp @@ -0,0 +1,179 @@ +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +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(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(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(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(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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/CurrentLedgerObjArrayLen.cpp b/src/tests/libxrpl/tx/wasm/host_context/CurrentLedgerObjArrayLen.cpp new file mode 100644 index 0000000000..5ef9dbe7c3 --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/CurrentLedgerObjArrayLen.cpp @@ -0,0 +1,63 @@ +#include +#include + +#include +#include +#include + +#include +#include +#include + +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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/CurrentLedgerObjField.cpp b/src/tests/libxrpl/tx/wasm/host_context/CurrentLedgerObjField.cpp new file mode 100644 index 0000000000..ad00450c35 --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/CurrentLedgerObjField.cpp @@ -0,0 +1,112 @@ +#include +#include + +#include +#include +#include + +#include +#include +#include + +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(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(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(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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/CurrentLedgerObjNestedArrayLen.cpp b/src/tests/libxrpl/tx/wasm/host_context/CurrentLedgerObjNestedArrayLen.cpp new file mode 100644 index 0000000000..6a3f9f2263 --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/CurrentLedgerObjNestedArrayLen.cpp @@ -0,0 +1,78 @@ +#include + +#include +#include +#include + +#include +#include +#include +#include + +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 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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/CurrentLedgerObjNestedField.cpp b/src/tests/libxrpl/tx/wasm/host_context/CurrentLedgerObjNestedField.cpp new file mode 100644 index 0000000000..4cb04f864c --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/CurrentLedgerObjNestedField.cpp @@ -0,0 +1,121 @@ +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +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 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(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(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(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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/DelegateKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_context/DelegateKeylet.cpp new file mode 100644 index 0000000000..ecbbf2abab --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/DelegateKeylet.cpp @@ -0,0 +1,138 @@ +#include +#include + +#include +#include +#include + +#include +#include +#include + +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(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(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(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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/DepositPreauthKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_context/DepositPreauthKeylet.cpp new file mode 100644 index 0000000000..a6f2cc151c --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/DepositPreauthKeylet.cpp @@ -0,0 +1,147 @@ +#include +#include + +#include +#include +#include + +#include +#include +#include + +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(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(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(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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/DidKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_context/DidKeylet.cpp new file mode 100644 index 0000000000..872c6e9120 --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/DidKeylet.cpp @@ -0,0 +1,126 @@ +#include +#include + +#include +#include +#include + +#include +#include +#include + +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(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(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(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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/EscrowKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_context/EscrowKeylet.cpp new file mode 100644 index 0000000000..fbb4d2dbce --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/EscrowKeylet.cpp @@ -0,0 +1,152 @@ +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +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(seq))) + .WillOnce(testing::Return(keylet)); + + OutRegion out{32}; + EXPECT_EQ( + hostContext.escrowKeylet(bytesOf(accountBytes), seq, out.slice()), + static_cast(keylet.size())); + EXPECT_TRUE(out.holds(bytesOf(keylet))); +} + +TEST_F(EscrowKeyletCall, HostErrorBecomesContractReturnValue) +{ + EXPECT_CALL(host, escrowKeylet(account, static_cast(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(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(seq))) + .WillOnce(testing::Return(keylet)); + + OutRegion out{keylet.size() - 1}; + EXPECT_EQ( + hostContext.escrowKeylet(bytesOf(accountBytes), seq, out.slice()), + static_cast(keylet.size())); + EXPECT_FALSE(out.wasWritten()); +} + +TEST_F(EscrowKeyletCall, OutRegionOfExactSizeIsWritten) +{ + Bytes const keylet(32, 0xab); + EXPECT_CALL(host, escrowKeylet(account, static_cast(seq))) + .WillOnce(testing::Return(keylet)); + + OutRegion out{keylet.size()}; + EXPECT_EQ( + hostContext.escrowKeylet(bytesOf(accountBytes), seq, out.slice()), + static_cast(keylet.size())); + EXPECT_TRUE(out.holds(bytesOf(keylet))); +} + +TEST_F(EscrowKeyletCall, EmptyResultAnswersZeroAndWritesNothing) +{ + EXPECT_CALL(host, escrowKeylet(account, static_cast(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::max())) + .WillOnce(testing::Return(keylet)); + + OutRegion out{32}; + EXPECT_EQ( + hostContext.escrowKeylet(bytesOf(accountBytes), negativeSeq, out.slice()), + static_cast(keylet.size())); +} + +} // namespace xrpl::test diff --git a/src/tests/libxrpl/tx/wasm/host_context/FloatAdd.cpp b/src/tests/libxrpl/tx/wasm/host_context/FloatAdd.cpp new file mode 100644 index 0000000000..a6dadf0219 --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/FloatAdd.cpp @@ -0,0 +1,89 @@ +#include + +#include +#include +#include +#include + +#include +#include +#include + +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(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(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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/FloatCompare.cpp b/src/tests/libxrpl/tx/wasm/host_context/FloatCompare.cpp new file mode 100644 index 0000000000..dbd2fbcb65 --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/FloatCompare.cpp @@ -0,0 +1,64 @@ +#include + +#include +#include +#include +#include + +#include +#include + +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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/FloatDivide.cpp b/src/tests/libxrpl/tx/wasm/host_context/FloatDivide.cpp new file mode 100644 index 0000000000..552d172e34 --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/FloatDivide.cpp @@ -0,0 +1,89 @@ +#include + +#include +#include +#include +#include + +#include +#include +#include + +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(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(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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/FloatFromInt.cpp b/src/tests/libxrpl/tx/wasm/host_context/FloatFromInt.cpp new file mode 100644 index 0000000000..78e78d7aa1 --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/FloatFromInt.cpp @@ -0,0 +1,84 @@ +#include + +#include +#include +#include + +#include +#include +#include + +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(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(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(result.size())); + EXPECT_FALSE(out.wasWritten()); +} + +} // namespace xrpl::test diff --git a/src/tests/libxrpl/tx/wasm/host_context/FloatFromMantExp.cpp b/src/tests/libxrpl/tx/wasm/host_context/FloatFromMantExp.cpp new file mode 100644 index 0000000000..0f3b5d8acd --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/FloatFromMantExp.cpp @@ -0,0 +1,73 @@ +#include + +#include +#include +#include + +#include +#include +#include + +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(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(result.size())); + EXPECT_FALSE(out.wasWritten()); +} + +} // namespace xrpl::test diff --git a/src/tests/libxrpl/tx/wasm/host_context/FloatFromSTAmount.cpp b/src/tests/libxrpl/tx/wasm/host_context/FloatFromSTAmount.cpp new file mode 100644 index 0000000000..9a9c22e390 --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/FloatFromSTAmount.cpp @@ -0,0 +1,102 @@ +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +namespace xrpl::test { + +namespace { + +Bytes +serialized(STAmount const& amount) +{ + Serializer s; + amount.add(s); + return s.getData(); +} + +} // namespace + +// The only file exercising `parseST`. 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(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(result.size())); + EXPECT_FALSE(out.wasWritten()); +} + +} // namespace xrpl::test diff --git a/src/tests/libxrpl/tx/wasm/host_context/FloatFromSTNumber.cpp b/src/tests/libxrpl/tx/wasm/host_context/FloatFromSTNumber.cpp new file mode 100644 index 0000000000..c18e849026 --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/FloatFromSTNumber.cpp @@ -0,0 +1,110 @@ +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +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`. 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(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(result.size())); + EXPECT_FALSE(out.wasWritten()); +} + +} // namespace xrpl::test diff --git a/src/tests/libxrpl/tx/wasm/host_context/FloatFromUint.cpp b/src/tests/libxrpl/tx/wasm/host_context/FloatFromUint.cpp new file mode 100644 index 0000000000..b7c0460779 --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/FloatFromUint.cpp @@ -0,0 +1,131 @@ +#include + +#include +#include +#include + +#include +#include +#include +#include + +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(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(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(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(result.size())); + EXPECT_TRUE(out.holds(bytesOf(result))); +} + +} // namespace xrpl::test diff --git a/src/tests/libxrpl/tx/wasm/host_context/FloatMultiply.cpp b/src/tests/libxrpl/tx/wasm/host_context/FloatMultiply.cpp new file mode 100644 index 0000000000..939ecb6885 --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/FloatMultiply.cpp @@ -0,0 +1,89 @@ +#include + +#include +#include +#include +#include + +#include +#include +#include + +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(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(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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/FloatPower.cpp b/src/tests/libxrpl/tx/wasm/host_context/FloatPower.cpp new file mode 100644 index 0000000000..6b2c8087f4 --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/FloatPower.cpp @@ -0,0 +1,103 @@ +#include + +#include +#include +#include +#include + +#include +#include +#include + +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(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(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(result.size())); +} + +} // namespace xrpl::test diff --git a/src/tests/libxrpl/tx/wasm/host_context/FloatRoot.cpp b/src/tests/libxrpl/tx/wasm/host_context/FloatRoot.cpp new file mode 100644 index 0000000000..ae9d6057af --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/FloatRoot.cpp @@ -0,0 +1,87 @@ +#include + +#include +#include +#include +#include + +#include +#include +#include + +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(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(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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/FloatSubtract.cpp b/src/tests/libxrpl/tx/wasm/host_context/FloatSubtract.cpp new file mode 100644 index 0000000000..7f2a08ee1b --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/FloatSubtract.cpp @@ -0,0 +1,89 @@ +#include + +#include +#include +#include +#include + +#include +#include +#include + +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(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(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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/FloatToInt.cpp b/src/tests/libxrpl/tx/wasm/host_context/FloatToInt.cpp new file mode 100644 index 0000000000..ac4ff31ee1 --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/FloatToInt.cpp @@ -0,0 +1,81 @@ +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/FloatToMantExp.cpp b/src/tests/libxrpl/tx/wasm/host_context/FloatToMantExp.cpp new file mode 100644 index 0000000000..d314424802 --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/FloatToMantExp.cpp @@ -0,0 +1,102 @@ +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/IsAmendmentEnabled.cpp b/src/tests/libxrpl/tx/wasm/host_context/IsAmendmentEnabled.cpp new file mode 100644 index 0000000000..b0cbb6362c --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/IsAmendmentEnabled.cpp @@ -0,0 +1,108 @@ +#include +#include + +#include +#include +#include + +#include +#include +#include + +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(testing::Eq(id)))) + .WillOnce(testing::Return(1)); + EXPECT_CALL(host, isAmendmentEnabled(testing::Matcher(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(idBytes.data()), idBytes.size()}; + EXPECT_CALL(host, isAmendmentEnabled(testing::Matcher(testing::Eq(id)))) + .WillOnce(testing::Return(0)); + EXPECT_CALL( + host, + isAmendmentEnabled(testing::Matcher(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(testing::Eq(id)))) + .WillOnce(testing::Return(std::unexpected(HostFunctionError::Unimplemented))); + EXPECT_CALL(host, isAmendmentEnabled(testing::Matcher(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(testing::_))).Times(0); + EXPECT_CALL(host, isAmendmentEnabled(testing::Matcher(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(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(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(testing::Eq(name)))) + .WillOnce(testing::Return(1)); + + EXPECT_EQ(hostContext.isAmendmentEnabled(bytesOf(nameBytes)), 1); +} + +} // namespace xrpl::test diff --git a/src/tests/libxrpl/tx/wasm/host_context/LedgerObjArrayLen.cpp b/src/tests/libxrpl/tx/wasm/host_context/LedgerObjArrayLen.cpp new file mode 100644 index 0000000000..80df1bd313 --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/LedgerObjArrayLen.cpp @@ -0,0 +1,84 @@ +#include +#include + +#include +#include +#include + +#include +#include +#include + +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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/LedgerObjField.cpp b/src/tests/libxrpl/tx/wasm/host_context/LedgerObjField.cpp new file mode 100644 index 0000000000..8ee616b75a --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/LedgerObjField.cpp @@ -0,0 +1,137 @@ +#include +#include + +#include +#include +#include + +#include +#include +#include + +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(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(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(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(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(value.size())); +} + +} // namespace xrpl::test diff --git a/src/tests/libxrpl/tx/wasm/host_context/LedgerObjNestedArrayLen.cpp b/src/tests/libxrpl/tx/wasm/host_context/LedgerObjNestedArrayLen.cpp new file mode 100644 index 0000000000..8f2d0d59a0 --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/LedgerObjNestedArrayLen.cpp @@ -0,0 +1,97 @@ +#include + +#include +#include +#include + +#include +#include +#include +#include + +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 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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/LedgerObjNestedField.cpp b/src/tests/libxrpl/tx/wasm/host_context/LedgerObjNestedField.cpp new file mode 100644 index 0000000000..6410564d9a --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/LedgerObjNestedField.cpp @@ -0,0 +1,149 @@ +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +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 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(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(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(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(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(value.size())); +} + +} // namespace xrpl::test diff --git a/src/tests/libxrpl/tx/wasm/host_context/LedgerSqn.cpp b/src/tests/libxrpl/tx/wasm/host_context/LedgerSqn.cpp new file mode 100644 index 0000000000..442248b47a --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/LedgerSqn.cpp @@ -0,0 +1,76 @@ +#include + +#include +#include +#include + +#include +#include +#include + +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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/MptokenIssuanceKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_context/MptokenIssuanceKeylet.cpp new file mode 100644 index 0000000000..5a53b05eb5 --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/MptokenIssuanceKeylet.cpp @@ -0,0 +1,131 @@ +#include +#include + +#include +#include +#include + +#include +#include +#include + +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(seq))) + .WillOnce(testing::Return(keylet)); + + OutRegion out{32}; + EXPECT_EQ( + hostContext.mptokenIssuanceKeylet(bytesOf(issuerBytes), seq, out.slice()), + static_cast(keylet.size())); + EXPECT_TRUE(out.holds(bytesOf(keylet))); +} + +TEST_F(MptokenIssuanceKeyletCall, HostErrorBecomesContractReturnValue) +{ + EXPECT_CALL(host, mptokenIssuanceKeylet(issuer, static_cast(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(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(seq))) + .WillOnce(testing::Return(keylet)); + + OutRegion out{keylet.size() - 1}; + EXPECT_EQ( + hostContext.mptokenIssuanceKeylet(bytesOf(issuerBytes), seq, out.slice()), + static_cast(keylet.size())); + EXPECT_FALSE(out.wasWritten()); +} + +TEST_F(MptokenIssuanceKeyletCall, OutRegionOfExactSizeIsWritten) +{ + Bytes const keylet(32, 0xab); + EXPECT_CALL(host, mptokenIssuanceKeylet(issuer, static_cast(seq))) + .WillOnce(testing::Return(keylet)); + + OutRegion out{keylet.size()}; + EXPECT_EQ( + hostContext.mptokenIssuanceKeylet(bytesOf(issuerBytes), seq, out.slice()), + static_cast(keylet.size())); + EXPECT_TRUE(out.holds(bytesOf(keylet))); +} + +TEST_F(MptokenIssuanceKeyletCall, EmptyResultAnswersZeroAndWritesNothing) +{ + EXPECT_CALL(host, mptokenIssuanceKeylet(issuer, static_cast(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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/MptokenKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_context/MptokenKeylet.cpp new file mode 100644 index 0000000000..7f4fd2b8f3 --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/MptokenKeylet.cpp @@ -0,0 +1,119 @@ +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +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(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(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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/NFT.cpp b/src/tests/libxrpl/tx/wasm/host_context/NFT.cpp new file mode 100644 index 0000000000..ba34fff6b0 --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/NFT.cpp @@ -0,0 +1,142 @@ +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +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(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(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(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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/NFTFlags.cpp b/src/tests/libxrpl/tx/wasm/host_context/NFTFlags.cpp new file mode 100644 index 0000000000..3c18d53f1f --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/NFTFlags.cpp @@ -0,0 +1,81 @@ +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +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::min())); + + EXPECT_EQ( + hostContext.getNFTFlags(bytesOf(nftIdBytes)), + hfErrorToInt(HostFunctionError::InternalFatal)); +} + +} // namespace xrpl::test diff --git a/src/tests/libxrpl/tx/wasm/host_context/NFTIssuer.cpp b/src/tests/libxrpl/tx/wasm/host_context/NFTIssuer.cpp new file mode 100644 index 0000000000..7d0401bccb --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/NFTIssuer.cpp @@ -0,0 +1,106 @@ +#include +#include + +#include +#include +#include + +#include +#include +#include + +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(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(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(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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/NFTSequence.cpp b/src/tests/libxrpl/tx/wasm/host_context/NFTSequence.cpp new file mode 100644 index 0000000000..01bdf0e19a --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/NFTSequence.cpp @@ -0,0 +1,90 @@ +#include +#include + +#include +#include +#include + +#include +#include +#include + +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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/NFTTaxon.cpp b/src/tests/libxrpl/tx/wasm/host_context/NFTTaxon.cpp new file mode 100644 index 0000000000..4ddff82c78 --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/NFTTaxon.cpp @@ -0,0 +1,90 @@ +#include +#include + +#include +#include +#include + +#include +#include +#include + +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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/NFTTransferFee.cpp b/src/tests/libxrpl/tx/wasm/host_context/NFTTransferFee.cpp new file mode 100644 index 0000000000..d67c5fc5db --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/NFTTransferFee.cpp @@ -0,0 +1,66 @@ +#include +#include + +#include +#include +#include + +#include +#include +#include + +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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/NftokenOfferKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_context/NftokenOfferKeylet.cpp new file mode 100644 index 0000000000..c009321ad2 --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/NftokenOfferKeylet.cpp @@ -0,0 +1,131 @@ +#include +#include + +#include +#include +#include + +#include +#include +#include + +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(seq))) + .WillOnce(testing::Return(keylet)); + + OutRegion out{32}; + EXPECT_EQ( + hostContext.nftokenOfferKeylet(bytesOf(accountBytes), seq, out.slice()), + static_cast(keylet.size())); + EXPECT_TRUE(out.holds(bytesOf(keylet))); +} + +TEST_F(NftokenOfferKeyletCall, HostErrorBecomesContractReturnValue) +{ + EXPECT_CALL(host, nftokenOfferKeylet(account, static_cast(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(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(seq))) + .WillOnce(testing::Return(keylet)); + + OutRegion out{keylet.size() - 1}; + EXPECT_EQ( + hostContext.nftokenOfferKeylet(bytesOf(accountBytes), seq, out.slice()), + static_cast(keylet.size())); + EXPECT_FALSE(out.wasWritten()); +} + +TEST_F(NftokenOfferKeyletCall, OutRegionOfExactSizeIsWritten) +{ + Bytes const keylet(32, 0xab); + EXPECT_CALL(host, nftokenOfferKeylet(account, static_cast(seq))) + .WillOnce(testing::Return(keylet)); + + OutRegion out{keylet.size()}; + EXPECT_EQ( + hostContext.nftokenOfferKeylet(bytesOf(accountBytes), seq, out.slice()), + static_cast(keylet.size())); + EXPECT_TRUE(out.holds(bytesOf(keylet))); +} + +TEST_F(NftokenOfferKeyletCall, EmptyResultAnswersZeroAndWritesNothing) +{ + EXPECT_CALL(host, nftokenOfferKeylet(account, static_cast(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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/OfferKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_context/OfferKeylet.cpp new file mode 100644 index 0000000000..de1f36809d --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/OfferKeylet.cpp @@ -0,0 +1,131 @@ +#include +#include + +#include +#include +#include + +#include +#include +#include + +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(seq))) + .WillOnce(testing::Return(keylet)); + + OutRegion out{32}; + EXPECT_EQ( + hostContext.offerKeylet(bytesOf(accountBytes), seq, out.slice()), + static_cast(keylet.size())); + EXPECT_TRUE(out.holds(bytesOf(keylet))); +} + +TEST_F(OfferKeyletCall, HostErrorBecomesContractReturnValue) +{ + EXPECT_CALL(host, offerKeylet(account, static_cast(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(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(seq))) + .WillOnce(testing::Return(keylet)); + + OutRegion out{keylet.size() - 1}; + EXPECT_EQ( + hostContext.offerKeylet(bytesOf(accountBytes), seq, out.slice()), + static_cast(keylet.size())); + EXPECT_FALSE(out.wasWritten()); +} + +TEST_F(OfferKeyletCall, OutRegionOfExactSizeIsWritten) +{ + Bytes const keylet(32, 0xab); + EXPECT_CALL(host, offerKeylet(account, static_cast(seq))) + .WillOnce(testing::Return(keylet)); + + OutRegion out{keylet.size()}; + EXPECT_EQ( + hostContext.offerKeylet(bytesOf(accountBytes), seq, out.slice()), + static_cast(keylet.size())); + EXPECT_TRUE(out.holds(bytesOf(keylet))); +} + +TEST_F(OfferKeyletCall, EmptyResultAnswersZeroAndWritesNothing) +{ + EXPECT_CALL(host, offerKeylet(account, static_cast(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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/OracleKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_context/OracleKeylet.cpp new file mode 100644 index 0000000000..0355d05b8c --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/OracleKeylet.cpp @@ -0,0 +1,131 @@ +#include +#include + +#include +#include +#include + +#include +#include +#include + +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(docId))) + .WillOnce(testing::Return(keylet)); + + OutRegion out{32}; + EXPECT_EQ( + hostContext.oracleKeylet(bytesOf(accountBytes), docId, out.slice()), + static_cast(keylet.size())); + EXPECT_TRUE(out.holds(bytesOf(keylet))); +} + +TEST_F(OracleKeyletCall, HostErrorBecomesContractReturnValue) +{ + EXPECT_CALL(host, oracleKeylet(account, static_cast(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(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(docId))) + .WillOnce(testing::Return(keylet)); + + OutRegion out{keylet.size() - 1}; + EXPECT_EQ( + hostContext.oracleKeylet(bytesOf(accountBytes), docId, out.slice()), + static_cast(keylet.size())); + EXPECT_FALSE(out.wasWritten()); +} + +TEST_F(OracleKeyletCall, OutRegionOfExactSizeIsWritten) +{ + Bytes const keylet(32, 0xab); + EXPECT_CALL(host, oracleKeylet(account, static_cast(docId))) + .WillOnce(testing::Return(keylet)); + + OutRegion out{keylet.size()}; + EXPECT_EQ( + hostContext.oracleKeylet(bytesOf(accountBytes), docId, out.slice()), + static_cast(keylet.size())); + EXPECT_TRUE(out.holds(bytesOf(keylet))); +} + +TEST_F(OracleKeyletCall, EmptyResultAnswersZeroAndWritesNothing) +{ + EXPECT_CALL(host, oracleKeylet(account, static_cast(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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/ParentLedgerHash.cpp b/src/tests/libxrpl/tx/wasm/host_context/ParentLedgerHash.cpp new file mode 100644 index 0000000000..2c9d3f219b --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/ParentLedgerHash.cpp @@ -0,0 +1,86 @@ +#include +#include + +#include +#include +#include + +#include +#include +#include + +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`), 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(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(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(hashBytes.size())); + EXPECT_TRUE(out.holds(bytesOf(hashBytes))); +} + +} // namespace xrpl::test diff --git a/src/tests/libxrpl/tx/wasm/host_context/ParentLedgerTime.cpp b/src/tests/libxrpl/tx/wasm/host_context/ParentLedgerTime.cpp new file mode 100644 index 0000000000..71a02e995c --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/ParentLedgerTime.cpp @@ -0,0 +1,75 @@ +#include + +#include +#include +#include + +#include +#include +#include + +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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/PaychannelKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_context/PaychannelKeylet.cpp new file mode 100644 index 0000000000..a184882a1a --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/PaychannelKeylet.cpp @@ -0,0 +1,152 @@ +#include +#include + +#include +#include +#include + +#include +#include +#include + +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(seq))) + .WillOnce(testing::Return(keylet)); + + OutRegion out{32}; + EXPECT_EQ( + hostContext.paychannelKeylet( + bytesOf(accountBytes), bytesOf(destinationBytes), seq, out.slice()), + static_cast(keylet.size())); + EXPECT_TRUE(out.holds(bytesOf(keylet))); +} + +TEST_F(PaychannelKeyletCall, HostErrorBecomesContractReturnValue) +{ + EXPECT_CALL(host, paychannelKeylet(account, destination, static_cast(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(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(seq))) + .WillOnce(testing::Return(keylet)); + + OutRegion out{keylet.size() - 1}; + EXPECT_EQ( + hostContext.paychannelKeylet( + bytesOf(accountBytes), bytesOf(destinationBytes), seq, out.slice()), + static_cast(keylet.size())); + EXPECT_FALSE(out.wasWritten()); +} + +TEST_F(PaychannelKeyletCall, OutRegionOfExactSizeIsWritten) +{ + Bytes const keylet(32, 0xab); + EXPECT_CALL(host, paychannelKeylet(account, destination, static_cast(seq))) + .WillOnce(testing::Return(keylet)); + + OutRegion out{keylet.size()}; + EXPECT_EQ( + hostContext.paychannelKeylet( + bytesOf(accountBytes), bytesOf(destinationBytes), seq, out.slice()), + static_cast(keylet.size())); + EXPECT_TRUE(out.holds(bytesOf(keylet))); +} + +TEST_F(PaychannelKeyletCall, EmptyResultAnswersZeroAndWritesNothing) +{ + EXPECT_CALL(host, paychannelKeylet(account, destination, static_cast(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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/PermissionedDomainKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_context/PermissionedDomainKeylet.cpp new file mode 100644 index 0000000000..5b490954b5 --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/PermissionedDomainKeylet.cpp @@ -0,0 +1,131 @@ +#include +#include + +#include +#include +#include + +#include +#include +#include + +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(seq))) + .WillOnce(testing::Return(keylet)); + + OutRegion out{32}; + EXPECT_EQ( + hostContext.permissionedDomainKeylet(bytesOf(accountBytes), seq, out.slice()), + static_cast(keylet.size())); + EXPECT_TRUE(out.holds(bytesOf(keylet))); +} + +TEST_F(PermissionedDomainKeyletCall, HostErrorBecomesContractReturnValue) +{ + EXPECT_CALL(host, permissionedDomainKeylet(account, static_cast(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(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(seq))) + .WillOnce(testing::Return(keylet)); + + OutRegion out{keylet.size() - 1}; + EXPECT_EQ( + hostContext.permissionedDomainKeylet(bytesOf(accountBytes), seq, out.slice()), + static_cast(keylet.size())); + EXPECT_FALSE(out.wasWritten()); +} + +TEST_F(PermissionedDomainKeyletCall, OutRegionOfExactSizeIsWritten) +{ + Bytes const keylet(32, 0xab); + EXPECT_CALL(host, permissionedDomainKeylet(account, static_cast(seq))) + .WillOnce(testing::Return(keylet)); + + OutRegion out{keylet.size()}; + EXPECT_EQ( + hostContext.permissionedDomainKeylet(bytesOf(accountBytes), seq, out.slice()), + static_cast(keylet.size())); + EXPECT_TRUE(out.holds(bytesOf(keylet))); +} + +TEST_F(PermissionedDomainKeyletCall, EmptyResultAnswersZeroAndWritesNothing) +{ + EXPECT_CALL(host, permissionedDomainKeylet(account, static_cast(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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/Sha512Half.cpp b/src/tests/libxrpl/tx/wasm/host_context/Sha512Half.cpp new file mode 100644 index 0000000000..6745be70d3 --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/Sha512Half.cpp @@ -0,0 +1,81 @@ +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/SignerListKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_context/SignerListKeylet.cpp new file mode 100644 index 0000000000..29c179863c --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/SignerListKeylet.cpp @@ -0,0 +1,126 @@ +#include +#include + +#include +#include +#include + +#include +#include +#include + +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(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(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(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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/TicketKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_context/TicketKeylet.cpp new file mode 100644 index 0000000000..03dadd4079 --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/TicketKeylet.cpp @@ -0,0 +1,131 @@ +#include +#include + +#include +#include +#include + +#include +#include +#include + +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(seq))) + .WillOnce(testing::Return(keylet)); + + OutRegion out{32}; + EXPECT_EQ( + hostContext.ticketKeylet(bytesOf(accountBytes), seq, out.slice()), + static_cast(keylet.size())); + EXPECT_TRUE(out.holds(bytesOf(keylet))); +} + +TEST_F(TicketKeyletCall, HostErrorBecomesContractReturnValue) +{ + EXPECT_CALL(host, ticketKeylet(account, static_cast(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(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(seq))) + .WillOnce(testing::Return(keylet)); + + OutRegion out{keylet.size() - 1}; + EXPECT_EQ( + hostContext.ticketKeylet(bytesOf(accountBytes), seq, out.slice()), + static_cast(keylet.size())); + EXPECT_FALSE(out.wasWritten()); +} + +TEST_F(TicketKeyletCall, OutRegionOfExactSizeIsWritten) +{ + Bytes const keylet(32, 0xab); + EXPECT_CALL(host, ticketKeylet(account, static_cast(seq))) + .WillOnce(testing::Return(keylet)); + + OutRegion out{keylet.size()}; + EXPECT_EQ( + hostContext.ticketKeylet(bytesOf(accountBytes), seq, out.slice()), + static_cast(keylet.size())); + EXPECT_TRUE(out.holds(bytesOf(keylet))); +} + +TEST_F(TicketKeyletCall, EmptyResultAnswersZeroAndWritesNothing) +{ + EXPECT_CALL(host, ticketKeylet(account, static_cast(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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/Trace.cpp b/src/tests/libxrpl/tx/wasm/host_context/Trace.cpp new file mode 100644 index 0000000000..1ab714c01f --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/Trace.cpp @@ -0,0 +1,54 @@ +#include +#include +#include + +#include +#include +#include +// For `TraceDataType`, which the bridge declares and this header defines. +#include + +#include +#include + +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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/TrustLineKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_context/TrustLineKeylet.cpp new file mode 100644 index 0000000000..c4e4bccb01 --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/TrustLineKeylet.cpp @@ -0,0 +1,180 @@ +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +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(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(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(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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/TxArrayLen.cpp b/src/tests/libxrpl/tx/wasm/host_context/TxArrayLen.cpp new file mode 100644 index 0000000000..120a8069d7 --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/TxArrayLen.cpp @@ -0,0 +1,56 @@ +#include +#include + +#include +#include +#include + +#include +#include +#include + +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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/TxNestedArrayLen.cpp b/src/tests/libxrpl/tx/wasm/host_context/TxNestedArrayLen.cpp new file mode 100644 index 0000000000..0269f6fbbe --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/TxNestedArrayLen.cpp @@ -0,0 +1,76 @@ +#include + +#include +#include +#include + +#include +#include +#include +#include + +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 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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/TxNestedField.cpp b/src/tests/libxrpl/tx/wasm/host_context/TxNestedField.cpp new file mode 100644 index 0000000000..e19ec5d573 --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/TxNestedField.cpp @@ -0,0 +1,117 @@ +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +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 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(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(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(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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/UpdateData.cpp b/src/tests/libxrpl/tx/wasm/host_context/UpdateData.cpp new file mode 100644 index 0000000000..7d724205d5 --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/UpdateData.cpp @@ -0,0 +1,58 @@ +#include +#include + +#include +#include +#include +#include + +#include +#include + +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 diff --git a/src/tests/libxrpl/tx/wasm/host_context/VaultKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_context/VaultKeylet.cpp new file mode 100644 index 0000000000..a480b21ba2 --- /dev/null +++ b/src/tests/libxrpl/tx/wasm/host_context/VaultKeylet.cpp @@ -0,0 +1,131 @@ +#include +#include + +#include +#include +#include + +#include +#include +#include + +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(seq))) + .WillOnce(testing::Return(keylet)); + + OutRegion out{32}; + EXPECT_EQ( + hostContext.vaultKeylet(bytesOf(accountBytes), seq, out.slice()), + static_cast(keylet.size())); + EXPECT_TRUE(out.holds(bytesOf(keylet))); +} + +TEST_F(VaultKeyletCall, HostErrorBecomesContractReturnValue) +{ + EXPECT_CALL(host, vaultKeylet(account, static_cast(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(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(seq))) + .WillOnce(testing::Return(keylet)); + + OutRegion out{keylet.size() - 1}; + EXPECT_EQ( + hostContext.vaultKeylet(bytesOf(accountBytes), seq, out.slice()), + static_cast(keylet.size())); + EXPECT_FALSE(out.wasWritten()); +} + +TEST_F(VaultKeyletCall, OutRegionOfExactSizeIsWritten) +{ + Bytes const keylet(32, 0xab); + EXPECT_CALL(host, vaultKeylet(account, static_cast(seq))) + .WillOnce(testing::Return(keylet)); + + OutRegion out{keylet.size()}; + EXPECT_EQ( + hostContext.vaultKeylet(bytesOf(accountBytes), seq, out.slice()), + static_cast(keylet.size())); + EXPECT_TRUE(out.holds(bytesOf(keylet))); +} + +TEST_F(VaultKeyletCall, EmptyResultAnswersZeroAndWritesNothing) +{ + EXPECT_CALL(host, vaultKeylet(account, static_cast(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