mirror of
https://github.com/XRPLF/rippled.git
synced 2026-09-27 07:26:51 +00:00
Merge ripple/smart-escrow
This commit is contained in:
@@ -143,9 +143,11 @@ Linear, at roughly **800 bytes per compile**. Within the suite this is why every
|
||||
7.9 GB at 25 repetitions, after which every later case in the binary failed to compile — 720 errored
|
||||
rows, all blaming cases that were innocent.
|
||||
|
||||
**Outside the suite it is worth a look.** A validator compiles twice per programmable-escrow
|
||||
transaction against that same static engine. Whether that is unbounded growth in production depends
|
||||
on wasmi internals not checked here — this is the C++-visible symptom, not a diagnosis.
|
||||
**Outside the suite it is worth a look.** A validator compiles once to screen an `EscrowCreate`
|
||||
and again for every `EscrowFinish` that runs the contract — with no module cache between them, and
|
||||
once per apply attempt rather than once per transaction — all against that same static engine.
|
||||
Whether that is unbounded growth in production depends on wasmi internals not checked here (wasmi
|
||||
2.0.0, wasmparser 0.228): this is the C++-visible symptom, not a diagnosis.
|
||||
|
||||
## Gotchas, each of which has already cost someone an afternoon
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,155 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// WASM binary format constants and helpers for building test modules
|
||||
namespace wasm_constants {
|
||||
|
||||
// Magic + version header
|
||||
uint8_t const kWasmHeader[] = {
|
||||
0x00,
|
||||
0x61,
|
||||
0x73,
|
||||
0x6d, // magic: \0asm
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00 // version: 1
|
||||
};
|
||||
|
||||
// Type section: () -> ()
|
||||
uint8_t const kTypeEmptyFunc[] = {0x01, 0x04, 0x01, 0x60, 0x00, 0x00};
|
||||
|
||||
// Function section: one function using type 0
|
||||
uint8_t const kFuncTypE0[] = {0x03, 0x02, 0x01, 0x00};
|
||||
|
||||
// Export section: export func 0 as "escrow_finish"
|
||||
uint8_t const kExportFinish[] = {
|
||||
0x07,
|
||||
0x11,
|
||||
0x01,
|
||||
0x0d,
|
||||
'e',
|
||||
's',
|
||||
'c',
|
||||
'r',
|
||||
'o',
|
||||
'w',
|
||||
'_',
|
||||
'f',
|
||||
'i',
|
||||
'n',
|
||||
'i',
|
||||
's',
|
||||
'h',
|
||||
0x00,
|
||||
0x00};
|
||||
|
||||
// Empty function body: 0 locals, end
|
||||
uint8_t const kEmptyBody[] = {0x00, 0x0b};
|
||||
|
||||
// Data segment offset: i32.const 0, end
|
||||
uint8_t const kDataOffsetZero[] = {0x41, 0x00, 0x0b};
|
||||
|
||||
// Section IDs
|
||||
uint8_t const kSectionMemory = 0x05;
|
||||
uint8_t const kSectionCode = 0x0a;
|
||||
uint8_t const kSectionData = 0x0b;
|
||||
|
||||
// Instructions
|
||||
uint8_t const kInstrNop = 0x01;
|
||||
uint8_t const kInstrEnd = 0x0b;
|
||||
|
||||
// Fill byte for data section bloat
|
||||
uint8_t const kDataFillByte = 0xEE;
|
||||
|
||||
// Generator for WASM module with large code section (many NOPs)
|
||||
std::vector<uint8_t>
|
||||
generateCodeBlob(uint32_t numInstructions);
|
||||
|
||||
// Generator for WASM module with large data section
|
||||
std::vector<uint8_t>
|
||||
generateDataBlob(uint32_t dataSize);
|
||||
|
||||
} // namespace wasm_constants
|
||||
|
||||
extern std::string const kLedgerSqnWasmHex;
|
||||
extern std::string const kAllHostFunctionsWasmHex;
|
||||
extern std::string const kAllKeyletsWasmHex;
|
||||
extern std::string const kCodecovTestsWasmHex;
|
||||
|
||||
extern std::string const kFibWasmHex;
|
||||
|
||||
extern std::string const kFloatTestsWasmHex;
|
||||
extern std::string const kFloat0Hex;
|
||||
extern std::string const kDisabledFloatHex;
|
||||
|
||||
extern std::string const kMemoryPointerAtLimitHex;
|
||||
extern std::string const kMemoryPointerOverLimitHex;
|
||||
extern std::string const kMemoryOffsetOverLimitHex;
|
||||
extern std::string const kMemoryEndOfWordOverLimitHex;
|
||||
extern std::string const kMemoryGrow0To1PageHex;
|
||||
extern std::string const kMemoryGrow1To0PageHex;
|
||||
extern std::string const kMemoryLastByteOf8MbHex;
|
||||
extern std::string const kMemoryGrow1MoreThan8MbHex;
|
||||
extern std::string const kMemoryGrow0MoreThan8MbHex;
|
||||
extern std::string const kMemoryInit1MoreThan8MbHex;
|
||||
extern std::string const kMemoryNegativeAddressHex;
|
||||
|
||||
extern std::string const kTable64ElementsHex;
|
||||
extern std::string const kTable65ElementsHex;
|
||||
extern std::string const kTable2TablesHex;
|
||||
extern std::string const kTable0ElementsHex;
|
||||
extern std::string const kTableUintMaxHex;
|
||||
|
||||
extern std::string const kProposalMutableGlobalHex;
|
||||
extern std::string const kProposalGcStructNewHex;
|
||||
extern std::string const kProposalMultiValueHex;
|
||||
extern std::string const kProposalSignExtHex;
|
||||
extern std::string const kProposalFloatToIntHex;
|
||||
extern std::string const kProposalBulkMemoryHex;
|
||||
extern std::string const kProposalRefTypesHex;
|
||||
extern std::string const kProposalTailCallHex;
|
||||
extern std::string const kProposalExtendedConstHex;
|
||||
extern std::string const kProposalMultiMemoryHex;
|
||||
extern std::string const kProposalCustomPageSizesHex;
|
||||
extern std::string const kProposalMemory64Hex;
|
||||
extern std::string const kProposalWideArithmeticHex;
|
||||
|
||||
extern std::string const kTrapDivideBy0Hex;
|
||||
extern std::string const kTrapIntOverflowHex;
|
||||
extern std::string const kTrapUnreachableHex;
|
||||
extern std::string const kTrapNullCallHex;
|
||||
extern std::string const kTrapFuncSigMismatchHex;
|
||||
|
||||
extern std::string const kWasiGetTimeHex;
|
||||
extern std::string const kWasiPrintHex;
|
||||
|
||||
extern std::string const kBadMagicNumberHex;
|
||||
extern std::string const kBadVersionNumberHex;
|
||||
extern std::string const kLyingHeaderHex;
|
||||
extern std::string const kNeverEndingNumberHex;
|
||||
extern std::string const kVectorLieHex;
|
||||
extern std::string const kSectionOrderingHex;
|
||||
extern std::string const kGhostPayloadHex;
|
||||
extern std::string const kJunkAfterSectionHex;
|
||||
extern std::string const kInvalidSectionIdHex;
|
||||
extern std::string const kLocalVariableBombHex;
|
||||
|
||||
extern std::string const kDeepRecursionHex;
|
||||
extern std::string const kInfiniteLoopWasmHex;
|
||||
extern std::string const kStartLoopHex;
|
||||
|
||||
extern std::string const kBadAlignWasmHex;
|
||||
|
||||
extern std::string const kThousandParamsHex;
|
||||
extern std::string const kThousand1ParamsHex;
|
||||
extern std::string const kLocals10kHex;
|
||||
extern std::string const kFunctions5kHex;
|
||||
|
||||
extern std::string const kOpcReservedHex;
|
||||
|
||||
extern std::string const kImpExpHex;
|
||||
extern std::string const kUpdateDataWasmHex;
|
||||
@@ -1,11 +0,0 @@
|
||||
#include <stdint.h>
|
||||
|
||||
int32_t set_data(uint8_t const *, int32_t);
|
||||
|
||||
int escrow_finish()
|
||||
{
|
||||
uint8_t buf[] = "Data";
|
||||
set_data(buf, sizeof(buf) - 1);
|
||||
|
||||
return -256;
|
||||
}
|
||||
@@ -179,6 +179,10 @@ private:
|
||||
*/
|
||||
class TestServiceRegistry : public ServiceRegistry
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief The fee settings a test environment starts with.
|
||||
*/
|
||||
static Fees
|
||||
defaultFees()
|
||||
{
|
||||
@@ -189,6 +193,7 @@ class TestServiceRegistry : public ServiceRegistry
|
||||
return fees;
|
||||
}
|
||||
|
||||
private:
|
||||
TestLogs logs_{beast::Severity::Warning};
|
||||
boost::asio::io_context ioContext_;
|
||||
TestFamily family_{logs_.journal("TestFamily")};
|
||||
@@ -500,6 +505,15 @@ public:
|
||||
return fees_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Override the fee settings the transactors see.
|
||||
*/
|
||||
void
|
||||
setFees(Fees const& fees)
|
||||
{
|
||||
fees_ = fees;
|
||||
}
|
||||
|
||||
// Temporary: Get the underlying Application
|
||||
Application&
|
||||
getApp() override
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxMeta.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/protocol_autogen/ledger_entries/AccountRoot.h>
|
||||
#include <xrpl/protocol_autogen/ledger_entries/RippleState.h>
|
||||
#include <xrpl/protocol_autogen/transactions/AccountSet.h>
|
||||
@@ -24,6 +26,7 @@
|
||||
|
||||
#include <helpers/Account.h>
|
||||
#include <helpers/IOU.h>
|
||||
#include <helpers/TestServiceRegistry.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
@@ -55,11 +58,21 @@ allFeatures()
|
||||
return kFeatures;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// TxTest free helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
std::uint32_t
|
||||
closeTimeOffset(TxTest const& env, std::uint32_t seconds)
|
||||
{
|
||||
return static_cast<std::uint32_t>(env.getCloseTime().time_since_epoch().count()) + seconds;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// TxTest
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
TxTest::TxTest(std::optional<FeatureBitset> features)
|
||||
TxTest::TxTest(std::optional<FeatureBitset> features, std::optional<Fees> feesOverride)
|
||||
{
|
||||
// Convert FeatureBitset to unordered_set for Rules constructor
|
||||
auto const featureBits = features.value_or(allFeatures());
|
||||
@@ -68,8 +81,9 @@ TxTest::TxTest(std::optional<FeatureBitset> features)
|
||||
// Create rules with the specified features
|
||||
rules_.emplace(featureSet_);
|
||||
|
||||
// Default fees for testing
|
||||
Fees const fees{XRPAmount{10}, XRPAmount{10000000}, XRPAmount{2000000}};
|
||||
// One fee set for both the view and the registry.
|
||||
Fees const fees = feesOverride.value_or(TestServiceRegistry::defaultFees());
|
||||
registry_.setFees(fees);
|
||||
|
||||
// Create a genesis ledger as the base
|
||||
closedLedger_ = std::make_shared<Ledger>(
|
||||
@@ -155,6 +169,18 @@ TxTest::getAccountRoot(AccountID const& id) const
|
||||
return ledger_entries::AccountRoot{std::const_pointer_cast<SLE const>(sle)};
|
||||
}
|
||||
|
||||
std::uint32_t
|
||||
TxTest::getOwnerCount(AccountID const& id) const
|
||||
{
|
||||
return getAccountRoot(id).getOwnerCount();
|
||||
}
|
||||
|
||||
XRPAmount
|
||||
TxTest::getXrpBalance(AccountID const& id) const
|
||||
{
|
||||
return getAccountRoot(id).getBalance().xrp();
|
||||
}
|
||||
|
||||
OpenView&
|
||||
TxTest::getOpenLedger()
|
||||
{
|
||||
@@ -194,6 +220,7 @@ TxTest::close()
|
||||
for (auto const& tx : pendingTxs_)
|
||||
txSet.insert(tx);
|
||||
|
||||
closedMetadata_.clear();
|
||||
{
|
||||
OpenView accum(&*newLedger);
|
||||
for (auto const& [key, tx] : txSet)
|
||||
@@ -203,6 +230,11 @@ TxTest::close()
|
||||
{
|
||||
throw std::runtime_error("TxTest::close: failed to apply transaction");
|
||||
}
|
||||
// `accum` is not an open view, so this is the apply that produces metadata.
|
||||
if (result.metadata.has_value())
|
||||
{
|
||||
closedMetadata_.emplace(tx->getTransactionID(), *std::move(result).metadata);
|
||||
}
|
||||
}
|
||||
accum.apply(*newLedger);
|
||||
}
|
||||
@@ -218,6 +250,17 @@ TxTest::close()
|
||||
std::make_shared<OpenView>(kOpenLedger, closedLedger_.get(), *rules_, closedLedger_);
|
||||
}
|
||||
|
||||
std::optional<TxMeta>
|
||||
TxTest::getMetadata(uint256 const& txId) const
|
||||
{
|
||||
auto const it = closedMetadata_.find(txId);
|
||||
if (it == std::end(closedMetadata_))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
|
||||
void
|
||||
TxTest::advanceTime(NetClock::duration duration)
|
||||
{
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/beast/hash/uhash.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/Ledger.h>
|
||||
#include <xrpl/ledger/OpenView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Fees.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/Rules.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
@@ -30,6 +30,7 @@
|
||||
#include <cmath>
|
||||
#include <concepts>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
@@ -163,6 +164,19 @@ struct TxResult
|
||||
std::shared_ptr<STTx const> tx; ///< Pointer to the submitted transaction.
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Result of a transaction submission that has been closed into a ledger.
|
||||
*
|
||||
* `TxResult::metadata` is always `std::nullopt`, because metadata is only built for a view
|
||||
* that is not open. This is what `TxTest::submitAndClose` returns instead: the result code
|
||||
* paired with the metadata that closing produced.
|
||||
*/
|
||||
struct ClosedResult
|
||||
{
|
||||
TER ter; ///< The transaction engine result code.
|
||||
std::optional<TxMeta> meta; ///< Metadata from the close, absent if none was produced.
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A lightweight transaction testing harness.
|
||||
*
|
||||
@@ -191,8 +205,16 @@ public:
|
||||
*
|
||||
* @param features Optional set of features to enable. If not specified,
|
||||
* uses all testable amendments.
|
||||
* @param fees Optional fee settings. If not specified, uses
|
||||
* `TestServiceRegistry::defaultFees()`. Applied to **both** the genesis
|
||||
* ledger and the service registry, because transactors read limits from the
|
||||
* registry (`ctx.registry.get().getFees()`) while `calculateBaseFee` reads
|
||||
* `view.fees()` — a test whose fees disagree across the two is testing a
|
||||
* state no ledger can be in.
|
||||
*/
|
||||
explicit TxTest(std::optional<FeatureBitset> features = std::nullopt);
|
||||
explicit TxTest(
|
||||
std::optional<FeatureBitset> features = std::nullopt,
|
||||
std::optional<Fees> fees = std::nullopt);
|
||||
|
||||
/**
|
||||
* @brief Check if a feature is enabled.
|
||||
@@ -218,17 +240,22 @@ public:
|
||||
* @tparam T A type derived from TransactionBuilderBase.
|
||||
* @param builder The transaction builder.
|
||||
* @param signer The account to sign with.
|
||||
* @param fee The fee to pay. The 10 drop default is below what some transactions
|
||||
* require: an `EscrowCreate` carrying `sfBytecode` owes
|
||||
* `base * 10 + 5 * bytecodeBytes` (`EscrowCreate::calculateBaseFee`), and an
|
||||
* `EscrowFinish` carrying `sfGas` owes the allowance priced at `gasPrice`.
|
||||
* Those submissions would fail on the fee rather than on whatever they meant
|
||||
* to test, so they must pass one explicitly.
|
||||
* @return TxResult containing the result code, applied status, and metadata.
|
||||
*/
|
||||
template <typename T>
|
||||
requires std::
|
||||
derived_from<std::decay_t<T>, transactions::TransactionBuilderBase<std::decay_t<T>>>
|
||||
[[nodiscard]] TxResult
|
||||
submit(T&& builder, Account const& signer)
|
||||
submit(T&& builder, Account const& signer, XRPAmount fee = XRPAmount{10})
|
||||
{
|
||||
auto const& obj = builder.getSTObject();
|
||||
auto accountId = obj[sfAccount];
|
||||
// Only set sequence if not using a ticket (ticket sets sequence to 0)
|
||||
if (!obj.isFieldPresent(sfTicketSequence))
|
||||
{
|
||||
builder.setSequence(getAccountRoot(accountId).getSequence());
|
||||
@@ -237,10 +264,35 @@ public:
|
||||
{
|
||||
builder.setSequence(0);
|
||||
}
|
||||
builder.setFee(XRPAmount(10));
|
||||
builder.setFee(fee);
|
||||
return submit(builder.build(signer.pk(), signer.sk()).getSTTx());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Submit a transaction, then close the ledger and return its metadata.
|
||||
*
|
||||
* Metadata comes into being at `close`, not at `submit` (see `close`), so any assertion
|
||||
* about `sfGasUsed`, `sfVMReturnCode`, or a delivered amount needs this three-step
|
||||
* sequence rather than `submit` alone. Closing also advances time by one close
|
||||
* interval, which matters to a test sensitive to a `FinishAfter` or an expiry.
|
||||
*
|
||||
* @tparam T A type derived from TransactionBuilderBase.
|
||||
* @param builder The transaction builder.
|
||||
* @param signer The account to sign with.
|
||||
* @param fee The fee to pay; see `submit` for when the default is not enough.
|
||||
* @return The result code and the metadata produced by the close.
|
||||
*/
|
||||
template <typename T>
|
||||
requires std::
|
||||
derived_from<std::decay_t<T>, transactions::TransactionBuilderBase<std::decay_t<T>>>
|
||||
[[nodiscard]] ClosedResult
|
||||
submitAndClose(T&& builder, Account const& signer, XRPAmount fee = XRPAmount{10})
|
||||
{
|
||||
auto const result = submit(std::forward<T>(builder), signer, fee);
|
||||
close();
|
||||
return ClosedResult{.ter = result.ter, .meta = getMetadata(result.tx->getTransactionID())};
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Submit a transaction to the open ledger.
|
||||
*
|
||||
@@ -281,6 +333,28 @@ public:
|
||||
[[nodiscard]] ledger_entries::AccountRoot
|
||||
getAccountRoot(AccountID const& id) const;
|
||||
|
||||
/**
|
||||
* @brief Get an account's owner count.
|
||||
* @param id The account ID.
|
||||
* @return The number of ledger objects the account owns.
|
||||
* @throws std::runtime_error if the account does not exist.
|
||||
*/
|
||||
[[nodiscard]] std::uint32_t
|
||||
getOwnerCount(AccountID const& id) const;
|
||||
|
||||
/**
|
||||
* @brief Get an account's XRP balance.
|
||||
*
|
||||
* The IOU overload of `getBalance` covers trust lines; this covers the account's own
|
||||
* drops, which is what a fee- or reserve-sensitive test needs to assert on.
|
||||
*
|
||||
* @param id The account ID.
|
||||
* @return The balance in drops.
|
||||
* @throws std::runtime_error if the account does not exist.
|
||||
*/
|
||||
[[nodiscard]] XRPAmount
|
||||
getXrpBalance(AccountID const& id) const;
|
||||
|
||||
/**
|
||||
* @brief Get the current open ledger view.
|
||||
* @return A mutable reference to the open ledger.
|
||||
@@ -307,10 +381,26 @@ public:
|
||||
*
|
||||
* Creates a new closed ledger from the current open ledger.
|
||||
* All pending transactions are re-applied in canonical order.
|
||||
*
|
||||
* @note This is where transaction **metadata** comes into being: it is only built for a
|
||||
* view that is not open (`ApplyStateTable::apply`), so `submit` cannot return any.
|
||||
* Each closed transaction's metadata is retained for `getMetadata`.
|
||||
*/
|
||||
void
|
||||
close();
|
||||
|
||||
/**
|
||||
* @brief Get the metadata of a transaction in the most recently closed ledger.
|
||||
*
|
||||
* Metadata is a property of a *closed* ledger, so the sequence is submit → `close` →
|
||||
* `getMetadata`. Only the latest close is retained.
|
||||
*
|
||||
* @param txId The transaction's ID (`TxResult::tx->getTransactionID()`).
|
||||
* @return The metadata, or `std::nullopt` if that transaction was not in the last close.
|
||||
*/
|
||||
[[nodiscard]] std::optional<TxMeta>
|
||||
getMetadata(uint256 const& txId) const;
|
||||
|
||||
/**
|
||||
* @brief Advance time without closing the ledger.
|
||||
*
|
||||
@@ -345,9 +435,14 @@ public:
|
||||
|
||||
/**
|
||||
* @brief Get the service registry.
|
||||
*
|
||||
* Returns the concrete test type so a test can reach its setters — `setFees` in
|
||||
* particular, for the cases that need a limit to change *after* setup, which the
|
||||
* constructor's `fees` parameter cannot express.
|
||||
*
|
||||
* @return A reference to the service registry.
|
||||
*/
|
||||
ServiceRegistry&
|
||||
TestServiceRegistry&
|
||||
getServiceRegistry()
|
||||
{
|
||||
return registry_;
|
||||
@@ -365,10 +460,52 @@ private:
|
||||
*/
|
||||
std::vector<std::shared_ptr<STTx const>> pendingTxs_;
|
||||
|
||||
/**
|
||||
* Metadata from the most recent close, keyed by transaction ID. Replaced each close.
|
||||
*/
|
||||
std::map<uint256, TxMeta> closedMetadata_;
|
||||
|
||||
/**
|
||||
* Current time (can be advanced arbitrarily for testing).
|
||||
*/
|
||||
NetClock::time_point now_;
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// TxTest free helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief A ledger-close-time deadline `seconds` in the future.
|
||||
*
|
||||
* Time fields on the wire are `std::uint32_t` seconds since the XRPL epoch, while the
|
||||
* environment reports a `NetClock::time_point`. Every `CancelAfter` / `FinishAfter` needs
|
||||
* the same cast, and getting it wrong yields a deadline in the past — which a transactor
|
||||
* reports as `temBAD_EXPIRATION`, a failure that looks like the case under test.
|
||||
*
|
||||
* @param env The environment whose close time the deadline is relative to.
|
||||
* @param seconds How far past the current close time the deadline should sit.
|
||||
* @return The deadline, as a transaction field expects it.
|
||||
*/
|
||||
[[nodiscard]] std::uint32_t
|
||||
closeTimeOffset(TxTest const& env, std::uint32_t seconds);
|
||||
|
||||
/**
|
||||
* @brief Create and fund several accounts with the same balance.
|
||||
*
|
||||
* @code
|
||||
* createAccounts(env, XRP(5'000), alice, carol);
|
||||
* @endcode
|
||||
*
|
||||
* @param env The environment to create the accounts in.
|
||||
* @param xrp The initial balance for each account.
|
||||
* @param accounts The accounts to create.
|
||||
*/
|
||||
template <std::same_as<Account>... Accounts>
|
||||
void
|
||||
createAccounts(TxTest& env, XRPAmount xrp, Accounts const&... accounts)
|
||||
{
|
||||
(env.createAccount(accounts, xrp), ...);
|
||||
}
|
||||
|
||||
} // namespace xrpl::test
|
||||
|
||||
@@ -1,60 +1,60 @@
|
||||
# WASM host-function tests — layering
|
||||
|
||||
These tests are deliberately **layered**: each layer isolates one thing, so a failure points at
|
||||
one place instead of "somewhere in the stack." If a folder looks thin, the breadth it seems to be
|
||||
These tests are deliberately **layered**: each isolates one thing, so a failure points at one
|
||||
place instead of "somewhere in the stack." If a folder looks thin, the breadth it seems to be
|
||||
missing lives in a sibling layer.
|
||||
|
||||
## The layers
|
||||
|
||||
| Layer | Location | host | VM | ledger | Answers |
|
||||
| ------------------------------------- | --------------------------------------------------- | ---- | --- | ------ | ----------------------------------------------------------------------------------- |
|
||||
| Engine / gas / limits / ABI | `crates/xrpl-wasm-vm`, `crates/xrpl-host-functions` | mock | ✓ | ✗ | gas, transfer budget, memory/field limits, preflight, VM limits, generated ABI |
|
||||
| `host_context/` (`HostContextTest`) | `.../host_context` | mock | ✗ | ✗ | the `HostContext` marshalling shim alone (byte order, buffer sizing, `SField` xlat) |
|
||||
| `host_calls/` (`HostCallTest`) | `.../host_calls` | mock | ✓ | ✗ | per-function **wire contract** — what the host was asked, what came back |
|
||||
| `host_functions/` (`RealHostFixture`) | `.../host_functions` | real | ✗ | real | each function's **actual answer** vs. a real `TxTest` ledger |
|
||||
| `e2e/` (`RealVmTest`) | `.../e2e` | real | ✓ | real | **full-stack integration** — VM + `HostContext` + real impl + real ledger |
|
||||
| Layer | Location | host | VM | ledger | Answers |
|
||||
| ------------------------------------- | --------------------------------------------------- | ---- | --- | ------ | ---------------------------------------------------------------------------------------- |
|
||||
| Engine / gas / limits / ABI | `crates/xrpl-wasm-vm`, `crates/xrpl-host-functions` | mock | ✓ | ✗ | gas, transfer budget, memory/field limits, preflight, VM limits, generated ABI |
|
||||
| `host_context/` (`HostContextTest`) | `.../host_context` | mock | ✗ | ✗ | the `HostContext` marshalling shim alone (byte order, buffer sizing, `SField` xlat) |
|
||||
| `host_calls/` (`HostCallTest`) | `.../host_calls` | mock | ✓ | ✗ | per-function **wire contract** — what the host was asked, what came back |
|
||||
| `host_functions/` (`RealHostFixture`) | `.../host_functions` | real | ✗ | real | each function's **actual answer** vs. a real `TxTest` ledger |
|
||||
| `e2e/` (`RealVmTest`) | `.../e2e` | real | ✓ | real | **full-stack integration** — VM + `HostContext` + real impl + real ledger |
|
||||
| `transactor/` (`TxTest`) | `.../transactor` | real | ✓ | real | the **transactor** around a contract — fees, reserves, limits, what each failure reports |
|
||||
|
||||
Run the C++ side with:
|
||||
|
||||
```bash
|
||||
./build/xrpl_tests --gtest_filter='*Impl.*:*Call.*:*E2e.*:WasmVMTest.*:WasmVMDeathTest.*:PreflightTest.*'
|
||||
./build/xrpl_tests --gtest_filter='*Impl.*:*Call.*:*E2e.*:WasmVMTest.*:WasmVMDeathTest.*:PreflightTest.*:BytecodeSize.*:BytecodePreflight.*:FinishFailures.*:BytecodeRun.*:GasFees.*:DataOnReject.*'
|
||||
```
|
||||
|
||||
(707 tests, 136 suites.) The engine-level coverage is Rust: `cd crates && cargo test`.
|
||||
(744 tests, 142 suites.) The engine-level coverage is Rust: `cd crates && cargo test`.
|
||||
|
||||
## `fixtures/` — split by whether it needs a test framework
|
||||
|
||||
| | |
|
||||
| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **No GTest** — the `xrpl.testkit.wasm` library | `WasmLedger` (real genesis ledger + the real host over it), `WasmRun` (WAT assembler), `NftSetup`, `FloatConstants` |
|
||||
| **GTest** → `xrpl_tests` | `RealHostFixture` (`: testing::Test, WasmLedger` + `expectValue`/`expectError`/`expectKeyletMatches`), `FloatFixture`, `NFTFixture`, `MockHostFunctions`, `WasmFixture`, `RealVmTest`, `HostContextFixture` |
|
||||
| | |
|
||||
| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **No GTest** — the `xrpl.testkit.wasm` library | `WasmLedger` (real genesis ledger + the real host over it), `WasmRun` (WAT assembler), `NftSetup`, `FloatConstants` |
|
||||
| **GTest** → `xrpl_tests` | `RealHostFixture` (`: testing::Test, WasmLedger` + `expectValue`/`expectError`/`expectKeyletMatches`), `FloatFixture`, `NFTFixture`, `MockHostFunctions`, `WasmFixture`, `RealVmTest`, `HostContextFixture`, `EscrowWasm` (transactor contracts + fee arithmetic), `ModuleBuilder` |
|
||||
|
||||
A benchmark wants a ledger and a host, not GTest's lifecycle. Both binaries link the library;
|
||||
The split exists because a benchmark wants a ledger and a host, not GTest's lifecycle:
|
||||
`xrpl.bench.wasm` links no GTest and no GMock at all.
|
||||
|
||||
Setup steps in `WasmLedger` and `NftSetup` **throw** (`fixtureFailed`) rather than using `EXPECT_`.
|
||||
Not stylistic: an `EXPECT_` outside a running test is recorded and discarded, so a benchmark whose
|
||||
escrow was never created would still run its host call, take the not-found path, and report a
|
||||
cheap, plausible, completely wrong price. **If you add a setup step that can fail, throw.**
|
||||
An `EXPECT_` outside a running test is recorded and discarded, so a benchmark whose escrow was
|
||||
never created would still run its host call, take the not-found path, and report a cheap,
|
||||
plausible, completely wrong price. **If you add a setup step that can fail, throw.**
|
||||
|
||||
## Gas calibration
|
||||
|
||||
The benchmarks that price these host functions live in `src/benchmarks/libxrpl/wasm/`, mirroring
|
||||
this tree one file per function, and have their own README. They link `xrpl.testkit.wasm` (above)
|
||||
for the ledger and host, and no test framework.
|
||||
The benchmarks pricing these host functions live in `src/benchmarks/libxrpl/wasm/`, mirroring this
|
||||
tree one file per function, with their own README.
|
||||
|
||||
## What `e2e/` covers — the rule
|
||||
|
||||
**`e2e/` covers every marshalling shape and cross-call convention exactly once. It does not cover
|
||||
every function.** That is a completeness claim on the axis e2e uniquely tests, not a sample.
|
||||
|
||||
`host_calls` pins what the bridge _asks_ with a _canned_ answer; `host_functions` pins what the
|
||||
real impl _answers_. The type system guarantees they agree on signatures. Nothing guarantees they
|
||||
`host_calls` pins what the bridge _asks_ with a canned answer; `host_functions` pins what the real
|
||||
impl _answers_. The type system guarantees they agree on signatures, but nothing guarantees they
|
||||
agree on **conventions** — units, endianness, buffer layout — because in neither test does a real
|
||||
guest write bytes a real host reads. That is exactly the `seq`-as-little-endian-region bug: every
|
||||
internal test passed, and it was caught by cross-checking the guest SDK.
|
||||
guest write bytes a real host reads. That is the `seq`-as-little-endian-region bug: every internal
|
||||
test passed, and it was caught by cross-checking the guest SDK.
|
||||
|
||||
Convention mismatch is a property of a call's **shape**, not of the function. All 19 keylets share
|
||||
Convention mismatch is a property of a call's **shape**, not of the function — all 19 keylets share
|
||||
one shape, so a 19th keylet e2e proves nothing the 1st did. The inventory is meant to be exhaustive:
|
||||
|
||||
| Shape / convention | Covered by | Why it is its own row |
|
||||
@@ -75,16 +75,18 @@ breadth lives in `host_functions/` and `host_calls/`, one case each.
|
||||
|
||||
## Out of scope
|
||||
|
||||
**The guest SDK** (`xrpl-std` / `xrpl-escrow`, external `xrpl-wasm-stdlib` repo) is not exercised
|
||||
here — that is the SDK repo's own suite. These tests hand-write the ABI in WAT (raw imports,
|
||||
literal field codes, hand-built byte layouts), deliberately bypassing all SDK code. Agreement is
|
||||
verified _transitively_: the SDK repo tests the SDK against the ABI spec, this repo tests the host
|
||||
against the same spec. That would not catch a drift where both diverge on an ambiguous point;
|
||||
closing it needs a **cross-repo integration test** (compiled guests against a real host) in CI
|
||||
where the Rust→wasm toolchain exists.
|
||||
**The guest SDK** (`xrpl-std` / `xrpl-escrow`, external `xrpl-wasm-stdlib` repo) is the SDK repo's
|
||||
own suite. These tests hand-write the ABI in WAT (raw imports, literal field codes, hand-built byte
|
||||
layouts), deliberately bypassing all SDK code. Agreement is verified _transitively_: the SDK repo
|
||||
tests the SDK against the ABI spec, this repo tests the host against the same spec. That would not
|
||||
catch a drift where both diverge on an ambiguous point; closing it needs a **cross-repo integration
|
||||
test** (compiled guests against a real host) in CI, where the Rust→wasm toolchain exists.
|
||||
|
||||
**Transactor-level (L5) tests** are deferred: the redesign does not yet wire `runEscrowWasm` into
|
||||
the `EscrowFinish` transactor, so there is no caller under `src/xrpld`. When it is wired, these
|
||||
need a home as C++ transactor tests over a real `Env` — `set_data` persistence (including on
|
||||
`tecBYTECODE_REJECTED`), `sfGasUsed` / `sfVMReturnCode` in transaction metadata, and owner-reserve
|
||||
accounting for a bytecode-bearing escrow. The layers here deliberately stop at the VM boundary.
|
||||
## Adding to `transactor/`
|
||||
|
||||
Things to know:
|
||||
|
||||
- **Fees live in two places and must agree.** A transactor reads its limits from the service
|
||||
registry (`ctx.registry.get().getFees()`), while `calculateBaseFee` reads `view.fees()`. Pass a
|
||||
`Fees` to `TxTest`'s constructor to set both; reach for `getServiceRegistry().setFees` only when
|
||||
a limit has to change _after_ setup.
|
||||
|
||||
49
src/tests/libxrpl/tx/wasm/fixtures/EscrowWasm.cpp
Normal file
49
src/tests/libxrpl/tx/wasm/fixtures/EscrowWasm.cpp
Normal file
@@ -0,0 +1,49 @@
|
||||
#include <tx/wasm/fixtures/EscrowWasm.h>
|
||||
|
||||
#include <xrpl/protocol/Fees.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/wasm/WasmCommon.h>
|
||||
|
||||
#include <helpers/TxTest.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <format>
|
||||
#include <string>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
std::string
|
||||
gatedOnLedgerSqn(std::uint32_t threshold)
|
||||
{
|
||||
return std::format(
|
||||
R"wat(
|
||||
(module
|
||||
(import "host_lib" "ldgr_index" (func $ldgr_index (param i32 i32) (result i32)))
|
||||
(memory (export "memory") 1)
|
||||
(func (export "escrow_finish") (result i32)
|
||||
(drop (call $ldgr_index (i32.const 0) (i32.const 4)))
|
||||
(if (result i32) (i32.ge_u (i32.load (i32.const 0)) (i32.const {}))
|
||||
(then (i32.const 5))
|
||||
(else (i32.const 0)))))
|
||||
)wat",
|
||||
threshold);
|
||||
}
|
||||
|
||||
XRPAmount
|
||||
escrowCreateFee(TxTest const& env, Bytes const& bytecode)
|
||||
{
|
||||
return (env.getOpenLedger().fees().base * 10) +
|
||||
XRPAmount{static_cast<std::int64_t>(bytecode.size()) * 5};
|
||||
}
|
||||
|
||||
XRPAmount
|
||||
escrowFinishFee(TxTest const& env, std::uint32_t allowance)
|
||||
{
|
||||
auto const& fees = env.getOpenLedger().fees();
|
||||
// Integer division rounds down, so the transactor adds one drop; match it exactly or
|
||||
// the submission fails on the fee rather than on what it meant to test.
|
||||
auto const gasFee = ((std::uint64_t{allowance} * fees.gasPrice) / microDropsPerDrop) + 1;
|
||||
return fees.base + XRPAmount{static_cast<std::int64_t>(gasFee)};
|
||||
}
|
||||
|
||||
} // namespace xrpl::test
|
||||
74
src/tests/libxrpl/tx/wasm/fixtures/EscrowWasm.h
Normal file
74
src/tests/libxrpl/tx/wasm/fixtures/EscrowWasm.h
Normal file
@@ -0,0 +1,74 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/wasm/WasmCommon.h>
|
||||
|
||||
#include <helpers/TxTest.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
// Contracts and fee arithmetic shared by the transactor-level escrow tests.
|
||||
|
||||
// Reads the ledger sequence and returns 5. A minimal *working* contract: it makes a real
|
||||
// host call, so it exercises more than validation, but what it returns is uninteresting.
|
||||
inline constexpr auto kReadsLedgerSqn = std::string_view{R"wat(
|
||||
(module
|
||||
(import "host_lib" "ldgr_index" (func $ldgr_index (param i32 i32) (result i32)))
|
||||
(memory (export "memory") 1)
|
||||
(func (export "escrow_finish") (result i32)
|
||||
(drop (call $ldgr_index (i32.const 0) (i32.const 4)))
|
||||
(i32.const 5)))
|
||||
)wat"};
|
||||
|
||||
// Traps. A fault rather than a rejection: no return code, and nothing it wrote survives.
|
||||
inline constexpr auto kTraps = std::string_view{R"wat(
|
||||
(module
|
||||
(memory (export "memory") 1)
|
||||
(func (export "escrow_finish") (result i32)
|
||||
(unreachable)))
|
||||
)wat"};
|
||||
|
||||
// Loops forever, so the only way it stops is by exhausting its gas allowance.
|
||||
inline constexpr auto kLoopsForever = std::string_view{R"wat(
|
||||
(module
|
||||
(memory (export "memory") 1)
|
||||
(func (export "escrow_finish") (result i32)
|
||||
(loop $forever (br $forever))
|
||||
(i32.const 1)))
|
||||
)wat"};
|
||||
|
||||
// Imports a host function that does not exist, so screening refuses it. Well-formed wasm —
|
||||
// the refusal is about the import list, not the bytes.
|
||||
inline constexpr auto kImportsUnknownHostFunction = std::string_view{R"wat(
|
||||
(module
|
||||
(import "host_lib" "bad" (func $bad (result i32)))
|
||||
(memory (export "memory") 1)
|
||||
(func (export "escrow_finish") (result i32)
|
||||
(call $bad)))
|
||||
)wat"};
|
||||
|
||||
// A contract that approves only once the ledger has reached `threshold`: returns 5 at or
|
||||
// past it, 0 (a rejection) before.
|
||||
//
|
||||
// The escrow's release condition is thus a real predicate over ledger state that changes
|
||||
// from false to true while the escrow sits there — the shape the whole feature exists for,
|
||||
// and the one a fixed contract cannot express. Built at runtime because the threshold has
|
||||
// to be chosen relative to the environment's current sequence.
|
||||
std::string
|
||||
gatedOnLedgerSqn(std::uint32_t threshold);
|
||||
|
||||
// What an `EscrowCreate` carrying this bytecode must pay: ten base fees plus five drops a
|
||||
// byte (`EscrowCreate::calculateBaseFee`).
|
||||
XRPAmount
|
||||
escrowCreateFee(TxTest const& env, Bytes const& bytecode);
|
||||
|
||||
// What an `EscrowFinish` carrying this gas allowance must pay: the base fee plus the
|
||||
// allowance priced at `gasPrice`, rounded up (`EscrowFinish::calculateBaseFee`).
|
||||
XRPAmount
|
||||
escrowFinishFee(TxTest const& env, std::uint32_t allowance);
|
||||
|
||||
} // namespace xrpl::test
|
||||
177
src/tests/libxrpl/tx/wasm/fixtures/ModuleBuilder.cpp
Normal file
177
src/tests/libxrpl/tx/wasm/fixtures/ModuleBuilder.cpp
Normal file
@@ -0,0 +1,177 @@
|
||||
#include <tx/wasm/fixtures/ModuleBuilder.h>
|
||||
|
||||
#include <xrpl/tx/wasm/WasmCommon.h>
|
||||
#include <xrpl/tx/wasm/WasmVM.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string_view>
|
||||
|
||||
namespace xrpl::test {
|
||||
namespace {
|
||||
|
||||
// Section ids, from the binary format's fixed table.
|
||||
constexpr std::uint8_t kSectionType = 0x01;
|
||||
constexpr std::uint8_t kSectionFunction = 0x03;
|
||||
constexpr std::uint8_t kSectionMemory = 0x05;
|
||||
constexpr std::uint8_t kSectionExport = 0x07;
|
||||
constexpr std::uint8_t kSectionCode = 0x0A;
|
||||
constexpr std::uint8_t kSectionData = 0x0B;
|
||||
|
||||
constexpr std::uint8_t kOpcodeNop = 0x01;
|
||||
constexpr std::uint8_t kOpcodeEnd = 0x0B;
|
||||
constexpr std::uint8_t kOpcodeI32Const = 0x41;
|
||||
|
||||
constexpr std::uint8_t kTypeI32 = 0x7F;
|
||||
constexpr std::uint8_t kTypeFunc = 0x60;
|
||||
|
||||
constexpr std::uint32_t kPageBytes = 65'536;
|
||||
|
||||
// Anything that isn't obviously zero-filled is 0xEE, so a dump of a failing module shows at
|
||||
// a glance which bytes are padding.
|
||||
constexpr std::uint8_t kDataFillByte = 0xEE;
|
||||
|
||||
void
|
||||
appendU32Leb(Bytes& out, std::uint32_t value)
|
||||
{
|
||||
do
|
||||
{
|
||||
auto byte = static_cast<std::uint8_t>(value & 0x7F);
|
||||
value >>= 7;
|
||||
if (value != 0U)
|
||||
{
|
||||
byte |= 0x80;
|
||||
}
|
||||
out.push_back(byte);
|
||||
} while (value != 0U);
|
||||
}
|
||||
|
||||
void
|
||||
appendSection(Bytes& out, std::uint8_t section, Bytes const& payload)
|
||||
{
|
||||
out.push_back(section);
|
||||
appendU32Leb(out, static_cast<std::uint32_t>(payload.size()));
|
||||
out.insert(std::end(out), std::begin(payload), std::end(payload));
|
||||
}
|
||||
|
||||
// A function body: no locals, `code`, `end` — prefixed by its own byte length.
|
||||
void
|
||||
appendBody(Bytes& out, Bytes const& code)
|
||||
{
|
||||
auto body = Bytes{0x00}; // local declaration count
|
||||
body.insert(std::end(body), std::begin(code), std::end(code));
|
||||
body.push_back(kOpcodeEnd);
|
||||
|
||||
appendU32Leb(out, static_cast<std::uint32_t>(body.size()));
|
||||
out.insert(std::end(out), std::begin(body), std::end(body));
|
||||
}
|
||||
|
||||
Bytes
|
||||
header()
|
||||
{
|
||||
return Bytes{0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00}; // "\0asm", version 1
|
||||
}
|
||||
|
||||
// Two types: `() -> ()` for filler functions, `() -> i32` for the entry point.
|
||||
constexpr std::uint8_t kTypeVoid = 0;
|
||||
constexpr std::uint8_t kTypeReturnsI32 = 1;
|
||||
|
||||
void
|
||||
appendTypeSection(Bytes& out)
|
||||
{
|
||||
auto payload = Bytes{0x02}; // two types
|
||||
payload.insert(std::end(payload), {kTypeFunc, 0x00, 0x00});
|
||||
payload.insert(std::end(payload), {kTypeFunc, 0x00, 0x01, kTypeI32});
|
||||
appendSection(out, kSectionType, payload);
|
||||
}
|
||||
|
||||
// `fillerCount` functions of type `() -> ()`, then the entry point of type `() -> i32`.
|
||||
void
|
||||
appendFunctionSection(Bytes& out, std::uint32_t fillerCount)
|
||||
{
|
||||
auto payload = Bytes{};
|
||||
appendU32Leb(payload, fillerCount + 1);
|
||||
payload.insert(std::end(payload), fillerCount, kTypeVoid);
|
||||
payload.push_back(kTypeReturnsI32);
|
||||
appendSection(out, kSectionFunction, payload);
|
||||
}
|
||||
|
||||
// Export the entry point, which is the last function declared.
|
||||
void
|
||||
appendExportSection(Bytes& out, std::uint32_t fillerCount, bool exportMemory)
|
||||
{
|
||||
auto payload = Bytes{};
|
||||
appendU32Leb(payload, exportMemory ? 2 : 1);
|
||||
|
||||
if (exportMemory)
|
||||
{
|
||||
static constexpr auto kMemory = std::string_view{"memory"};
|
||||
appendU32Leb(payload, static_cast<std::uint32_t>(kMemory.size()));
|
||||
payload.insert(std::end(payload), std::begin(kMemory), std::end(kMemory));
|
||||
payload.push_back(0x02); // export kind: memory
|
||||
payload.push_back(0x00); // memory index
|
||||
}
|
||||
|
||||
appendU32Leb(payload, static_cast<std::uint32_t>(escrowFunctionName.size()));
|
||||
payload.insert(std::end(payload), std::begin(escrowFunctionName), std::end(escrowFunctionName));
|
||||
payload.push_back(0x00); // export kind: function
|
||||
appendU32Leb(payload, fillerCount);
|
||||
|
||||
appendSection(out, kSectionExport, payload);
|
||||
}
|
||||
|
||||
// `i32.const 1` — a completed run that the transactor reads as success.
|
||||
Bytes
|
||||
entryPointCode()
|
||||
{
|
||||
return Bytes{kOpcodeI32Const, 0x01};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Bytes
|
||||
codeHeavyModule(std::uint32_t instructionCount)
|
||||
{
|
||||
// One filler function holding every `nop`, plus the entry point.
|
||||
constexpr std::uint32_t kFillerCount = 1;
|
||||
|
||||
auto out = header();
|
||||
appendTypeSection(out);
|
||||
appendFunctionSection(out, kFillerCount);
|
||||
appendExportSection(out, kFillerCount, /*exportMemory*/ false);
|
||||
|
||||
auto codePayload = Bytes{};
|
||||
appendU32Leb(codePayload, kFillerCount + 1);
|
||||
appendBody(codePayload, Bytes(instructionCount, kOpcodeNop));
|
||||
appendBody(codePayload, entryPointCode());
|
||||
|
||||
appendSection(out, kSectionCode, codePayload);
|
||||
return out;
|
||||
}
|
||||
|
||||
Bytes
|
||||
dataHeavyModule(std::uint32_t dataBytes)
|
||||
{
|
||||
auto out = header();
|
||||
appendTypeSection(out);
|
||||
appendFunctionSection(out, /*fillerCount*/ 0);
|
||||
|
||||
auto memoryPayload = Bytes{0x01, 0x00}; // one memory, minimum-only limits
|
||||
appendU32Leb(memoryPayload, (dataBytes + kPageBytes - 1) / kPageBytes);
|
||||
appendSection(out, kSectionMemory, memoryPayload);
|
||||
|
||||
appendExportSection(out, /*fillerCount*/ 0, /*exportMemory*/ true);
|
||||
|
||||
auto codePayload = Bytes{0x01}; // one function body
|
||||
appendBody(codePayload, entryPointCode());
|
||||
appendSection(out, kSectionCode, codePayload);
|
||||
|
||||
auto dataPayload = Bytes{0x01, 0x00}; // one segment, memory 0
|
||||
dataPayload.insert(std::end(dataPayload), {kOpcodeI32Const, 0x00, kOpcodeEnd}); // offset 0
|
||||
appendU32Leb(dataPayload, dataBytes);
|
||||
dataPayload.insert(std::end(dataPayload), dataBytes, kDataFillByte);
|
||||
appendSection(out, kSectionData, dataPayload);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace xrpl::test
|
||||
47
src/tests/libxrpl/tx/wasm/fixtures/ModuleBuilder.h
Normal file
47
src/tests/libxrpl/tx/wasm/fixtures/ModuleBuilder.h
Normal file
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/tx/wasm/WasmCommon.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
// Modules built to a *byte size* rather than to a behaviour.
|
||||
//
|
||||
// Everything else in this tree writes WAT, which is the right default: it says what the
|
||||
// contract does. These two say only how big it is, for the transactor's `bytecodeSizeLimit`
|
||||
// screening, where the boundary cases sit five bytes apart (99'950 accepted, 99'955
|
||||
// refused). Assembling text cannot hit a byte count on the nose, and the WAT for a
|
||||
// hundred thousand `nop`s would be a ~500 KB string, so these emit the binary directly.
|
||||
//
|
||||
// Both produce a module that *passes preflight* when it is under the size limit: a real
|
||||
// `escrow_finish` exported with type `() -> i32`. That is not incidental — screening
|
||||
// checks the entry point's signature (`PreflightTest.EntryPointOfTheWrongTypeIsRefused`),
|
||||
// so a module that got it wrong would be refused for that reason at every size and the
|
||||
// sweep would measure nothing.
|
||||
|
||||
// A module of `instructionCount` `nop`s in a single function, doing nothing.
|
||||
//
|
||||
// All of them in one function, deliberately: there appears to be no per-function size limit
|
||||
// below the module limit, so one function may occupy the whole module. `wasmparser` defines
|
||||
// `MAX_WASM_FUNCTION_SIZE` = 128 KiB, which looks like such a limit, but a single body of a
|
||||
// million instructions preflights clean — pinned by
|
||||
// `BytecodeSize.ASingleFunctionBodyIsNotSeparatelyCapped`. Splitting the `nop`s across
|
||||
// functions would imply a constraint that is not there, and would make the byte count the
|
||||
// boundary tests depend on harder to predict.
|
||||
//
|
||||
// The returned module is a few dozen bytes larger than `instructionCount` (the sections
|
||||
// around the code). Callers that care about an exact total should measure `.size()` rather
|
||||
// than assume it.
|
||||
Bytes
|
||||
codeHeavyModule(std::uint32_t instructionCount);
|
||||
|
||||
// A module carrying `dataBytes` bytes in a data segment, with memory declared to fit.
|
||||
//
|
||||
// The size lands in a data section rather than a code section, so the two builders
|
||||
// together separate "large because there is a lot to translate" from "large because there
|
||||
// is a lot to copy".
|
||||
Bytes
|
||||
dataHeavyModule(std::uint32_t dataBytes);
|
||||
|
||||
} // namespace xrpl::test
|
||||
198
src/tests/libxrpl/tx/wasm/transactor/BytecodePreflight.cpp
Normal file
198
src/tests/libxrpl/tx/wasm/transactor/BytecodePreflight.cpp
Normal file
@@ -0,0 +1,198 @@
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/protocol_autogen/transactions/EscrowCreate.h>
|
||||
#include <xrpl/tx/wasm/WasmCommon.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <helpers/Account.h>
|
||||
#include <helpers/TestServiceRegistry.h>
|
||||
#include <helpers/TxTest.h>
|
||||
#include <tx/wasm/fixtures/EscrowWasm.h>
|
||||
#include <tx/wasm/fixtures/WasmRun.h>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace xrpl::test {
|
||||
namespace {
|
||||
|
||||
// What `EscrowCreate` refuses before a contract ever runs. Everything here is decided in
|
||||
// preflight, so the shape of the transaction is the whole subject: no ledger state matters
|
||||
// beyond the account existing.
|
||||
struct BytecodePreflight : testing::Test
|
||||
{
|
||||
Account const alice{"alice"};
|
||||
Account const carol{"carol"};
|
||||
|
||||
// An `EscrowCreate` with everything a valid one needs, for callers to spoil one field at
|
||||
// a time. `cancelAfter` is set because without an expiry the transaction is refused for
|
||||
// that reason first, and every bytecode case would report `temBAD_EXPIRATION` instead of
|
||||
// what it meant to check.
|
||||
transactions::EscrowCreateBuilder
|
||||
escrowCreate(TxTest const& env, Bytes const& bytecode)
|
||||
{
|
||||
auto builder = transactions::EscrowCreateBuilder{alice, carol, STAmount{XRP(500)}};
|
||||
builder.setBytecode(makeSlice(bytecode));
|
||||
builder.setCancelAfter(closeTimeOffset(env, 100));
|
||||
return builder;
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(BytecodePreflight, BytecodeIsRefusedWhileSmartEscrowIsDisabled)
|
||||
{
|
||||
auto env = TxTest{allFeatures() - featureSmartEscrow};
|
||||
createAccounts(env, XRP(5'000), alice, carol);
|
||||
|
||||
auto const wasm = assembleWat(kReadsLedgerSqn);
|
||||
auto const fee = escrowCreateFee(env, wasm);
|
||||
|
||||
EXPECT_EQ(env.submit(escrowCreate(env, wasm), alice, fee).ter, temDISABLED);
|
||||
|
||||
// Also with a data field, which is the other half of the feature's surface.
|
||||
auto builder = escrowCreate(env, wasm);
|
||||
builder.setData(makeSlice(Bytes{0x00, 0x11, 0x22, 0x33}));
|
||||
EXPECT_EQ(env.submit(builder, alice, fee).ter, temDISABLED);
|
||||
}
|
||||
|
||||
// A zero limit is how fee voting turns the runtime off, and it has to be distinguishable
|
||||
// from "your contract is too big" — `temTEMP_DISABLED` says come back later, `temMALFORMED`
|
||||
// says never.
|
||||
TEST_F(BytecodePreflight, AZeroSizeLimitDisablesUploadsRatherThanRejectingThem)
|
||||
{
|
||||
auto fees = TestServiceRegistry::defaultFees();
|
||||
fees.bytecodeSizeLimit = 0;
|
||||
auto env = TxTest{std::nullopt, fees};
|
||||
createAccounts(env, XRP(5'000), alice, carol);
|
||||
|
||||
auto const wasm = assembleWat(kReadsLedgerSqn);
|
||||
EXPECT_EQ(
|
||||
env.submit(escrowCreate(env, wasm), alice, escrowCreateFee(env, wasm)).ter,
|
||||
temTEMP_DISABLED);
|
||||
}
|
||||
|
||||
TEST_F(BytecodePreflight, AZeroGasLimitDisablesUploads)
|
||||
{
|
||||
auto fees = TestServiceRegistry::defaultFees();
|
||||
fees.gasLimit = 0;
|
||||
auto env = TxTest{std::nullopt, fees};
|
||||
createAccounts(env, XRP(5'000), alice, carol);
|
||||
|
||||
auto const wasm = assembleWat(kReadsLedgerSqn);
|
||||
EXPECT_EQ(
|
||||
env.submit(escrowCreate(env, wasm), alice, escrowCreateFee(env, wasm)).ter,
|
||||
temTEMP_DISABLED);
|
||||
}
|
||||
|
||||
TEST_F(BytecodePreflight, EmptyBytecodeIsRefused)
|
||||
{
|
||||
auto env = TxTest{};
|
||||
createAccounts(env, XRP(5'000), alice, carol);
|
||||
|
||||
EXPECT_EQ(
|
||||
env.submit(escrowCreate(env, Bytes{}), alice, escrowCreateFee(env, Bytes{})).ter,
|
||||
temMALFORMED);
|
||||
}
|
||||
|
||||
// Screening reaches into the module: this one is structurally valid wasm that asks for a
|
||||
// host function nobody serves.
|
||||
TEST_F(BytecodePreflight, BytecodeImportingAnUnknownHostFunctionIsRefused)
|
||||
{
|
||||
auto env = TxTest{};
|
||||
createAccounts(env, XRP(5'000), alice, carol);
|
||||
|
||||
auto const wasm = assembleWat(kImportsUnknownHostFunction);
|
||||
EXPECT_EQ(
|
||||
env.submit(escrowCreate(env, wasm), alice, escrowCreateFee(env, wasm)).ter,
|
||||
temINVALID_BYTECODE);
|
||||
}
|
||||
|
||||
TEST_F(BytecodePreflight, DataWithoutBytecodeIsRefused)
|
||||
{
|
||||
auto env = TxTest{};
|
||||
createAccounts(env, XRP(5'000), alice, carol);
|
||||
|
||||
auto builder = transactions::EscrowCreateBuilder{alice, carol, STAmount{XRP(500)}};
|
||||
builder.setData(makeSlice(Bytes{0x41, 0x41, 0x41, 0x41}));
|
||||
builder.setCancelAfter(closeTimeOffset(env, 100));
|
||||
|
||||
EXPECT_EQ(env.submit(builder, alice, XRPAmount{100'000}).ter, temMALFORMED);
|
||||
}
|
||||
|
||||
TEST_F(BytecodePreflight, DataPastItsMaximumIsRefused)
|
||||
{
|
||||
auto env = TxTest{};
|
||||
createAccounts(env, XRP(5'000), alice, carol);
|
||||
|
||||
auto const wasm = assembleWat(kReadsLedgerSqn);
|
||||
auto builder = escrowCreate(env, wasm);
|
||||
builder.setData(makeSlice(Bytes(kMaxWasmDataLength + 1, 0x42)));
|
||||
|
||||
EXPECT_EQ(env.submit(builder, alice, XRPAmount{100'000}).ter, temMALFORMED);
|
||||
}
|
||||
|
||||
// A contract needs a deadline. Without `CancelAfter` the escrow could never be reclaimed if
|
||||
// the contract never approves, so every combination lacking it is refused — including the
|
||||
// ones that look complete because they carry a `FinishAfter` or a condition.
|
||||
TEST_F(BytecodePreflight, BytecodeWithoutACancelTimeIsRefused)
|
||||
{
|
||||
auto env = TxTest{};
|
||||
createAccounts(env, XRP(5'000), alice, carol);
|
||||
|
||||
auto const wasm = assembleWat(kReadsLedgerSqn);
|
||||
auto const fee = escrowCreateFee(env, wasm);
|
||||
|
||||
auto bare = [&] {
|
||||
auto builder = transactions::EscrowCreateBuilder{alice, carol, STAmount{XRP(500)}};
|
||||
builder.setBytecode(makeSlice(wasm));
|
||||
return builder;
|
||||
};
|
||||
|
||||
EXPECT_EQ(env.submit(bare(), alice, fee).ter, temBAD_EXPIRATION);
|
||||
|
||||
auto withFinish = bare();
|
||||
withFinish.setFinishAfter(closeTimeOffset(env, 2));
|
||||
EXPECT_EQ(env.submit(withFinish, alice, fee).ter, temBAD_EXPIRATION);
|
||||
}
|
||||
|
||||
TEST_F(BytecodePreflight, BytecodeWithACancelTimeIsAccepted)
|
||||
{
|
||||
auto env = TxTest{};
|
||||
createAccounts(env, XRP(5'000), alice, carol);
|
||||
|
||||
auto const wasm = assembleWat(kReadsLedgerSqn);
|
||||
EXPECT_EQ(
|
||||
env.submit(escrowCreate(env, wasm), alice, escrowCreateFee(env, wasm)).ter, tesSUCCESS);
|
||||
}
|
||||
|
||||
TEST_F(BytecodePreflight, BytecodeWithAFinishAndCancelTimeIsAccepted)
|
||||
{
|
||||
auto env = TxTest{};
|
||||
createAccounts(env, XRP(5'000), alice, carol);
|
||||
|
||||
auto const wasm = assembleWat(kReadsLedgerSqn);
|
||||
auto builder = escrowCreate(env, wasm);
|
||||
builder.setFinishAfter(closeTimeOffset(env, 2));
|
||||
|
||||
EXPECT_EQ(env.submit(builder, alice, escrowCreateFee(env, wasm)).ter, tesSUCCESS);
|
||||
}
|
||||
|
||||
// The per-byte charge is enforced, not advisory. One drop short is refused — which also
|
||||
// confirms the fee helper the other tests rely on is computing the real number rather than
|
||||
// something merely generous.
|
||||
TEST_F(BytecodePreflight, AFeeOneDropShortIsRefused)
|
||||
{
|
||||
TxTest env;
|
||||
createAccounts(env, XRP(5'000), alice, carol);
|
||||
|
||||
auto const wasm = assembleWat(kReadsLedgerSqn);
|
||||
auto const fee = escrowCreateFee(env, wasm);
|
||||
|
||||
EXPECT_EQ(env.submit(escrowCreate(env, wasm), alice, fee - XRPAmount{1}).ter, telINSUF_FEE_P);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace xrpl::test
|
||||
192
src/tests/libxrpl/tx/wasm/transactor/BytecodeRun.cpp
Normal file
192
src/tests/libxrpl/tx/wasm/transactor/BytecodeRun.cpp
Normal file
@@ -0,0 +1,192 @@
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/SeqProxy.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxMeta.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/protocol_autogen/transactions/EscrowCreate.h>
|
||||
#include <xrpl/protocol_autogen/transactions/EscrowFinish.h>
|
||||
#include <xrpl/tx/wasm/WasmCommon.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <helpers/Account.h>
|
||||
#include <helpers/TxTest.h>
|
||||
#include <tx/wasm/fixtures/EscrowWasm.h>
|
||||
#include <tx/wasm/fixtures/WasmRun.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl::test {
|
||||
namespace {
|
||||
|
||||
// A contract deciding whether an escrow releases, end to end through the transactor. The
|
||||
// other transactor files are about refusals; this one is about the feature working.
|
||||
|
||||
constexpr std::uint32_t kAllowance = 10'000;
|
||||
|
||||
// A preimage-sha256 pair, copied from jtx because `src/test/jtx` is not linked here.
|
||||
constexpr auto kFulfillment = std::array<std::uint8_t, 4>{{0xA0, 0x02, 0x80, 0x00}};
|
||||
constexpr auto kCondition = std::array<std::uint8_t, 39>{
|
||||
{0xA0, 0x25, 0x80, 0x20, 0xE3, 0xB0, 0xC4, 0x42, 0x98, 0xFC, 0x1C, 0x14, 0x9A,
|
||||
0xFB, 0xF4, 0xC8, 0x99, 0x6F, 0xB9, 0x24, 0x27, 0xAE, 0x41, 0xE4, 0x64, 0x9B,
|
||||
0x93, 0x4C, 0xA4, 0x95, 0x99, 0x1B, 0x78, 0x52, 0xB8, 0x55, 0x81, 0x01, 0x00}};
|
||||
|
||||
struct BytecodeRun : testing::Test
|
||||
{
|
||||
TxTest env;
|
||||
Account const alice{"alice"};
|
||||
Account const carol{"carol"};
|
||||
|
||||
BytecodeRun()
|
||||
{
|
||||
createAccounts(env, XRP(5'000), alice, carol);
|
||||
}
|
||||
|
||||
std::uint32_t
|
||||
currentSeq() const
|
||||
{
|
||||
return env.getOpenLedger().header().seq;
|
||||
}
|
||||
|
||||
struct Created
|
||||
{
|
||||
std::uint32_t seq;
|
||||
XRPAmount fee;
|
||||
};
|
||||
|
||||
Created
|
||||
createEscrow(Bytes const& wasm, bool withCondition = false)
|
||||
{
|
||||
auto const seq = env.getAccountRoot(alice).getSequence();
|
||||
|
||||
auto builder = transactions::EscrowCreateBuilder{alice, carol, STAmount{XRP(1'000)}};
|
||||
builder.setBytecode(makeSlice(wasm));
|
||||
builder.setCancelAfter(closeTimeOffset(env, 1'000));
|
||||
if (withCondition)
|
||||
{
|
||||
builder.setCondition(makeSlice(kCondition));
|
||||
}
|
||||
|
||||
auto const fee = escrowCreateFee(env, wasm);
|
||||
EXPECT_EQ(env.submit(builder, alice, fee).ter, tesSUCCESS);
|
||||
env.close();
|
||||
return Created{.seq = seq, .fee = fee};
|
||||
}
|
||||
|
||||
[[nodiscard]] ClosedResult
|
||||
finish(std::uint32_t seq, bool withFulfillment = false)
|
||||
{
|
||||
auto builder = transactions::EscrowFinishBuilder{carol, alice, seq};
|
||||
builder.setGas(kAllowance);
|
||||
|
||||
auto fee = escrowFinishFee(env, kAllowance);
|
||||
if (withFulfillment)
|
||||
{
|
||||
builder.setCondition(makeSlice(kCondition));
|
||||
builder.setFulfillment(makeSlice(kFulfillment));
|
||||
fee += env.getOpenLedger().fees().base * (32 + (kFulfillment.size() / 16));
|
||||
}
|
||||
|
||||
return env.submitAndClose(builder, carol, fee);
|
||||
}
|
||||
|
||||
bool
|
||||
escrowExists(std::uint32_t seq) const
|
||||
{
|
||||
return env.getOpenLedger().read(keylet::escrow(alice, SeqProxy::rawSequence(seq))) !=
|
||||
nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
// The whole point: it refuses while its predicate is false and releases once the ledger
|
||||
// makes it true, with nothing resubmitted differently.
|
||||
TEST_F(BytecodeRun, AContractRejectsUntilItsConditionHoldsThenReleases)
|
||||
{
|
||||
auto const threshold = currentSeq() + 3;
|
||||
auto const wasm = assembleWat(gatedOnLedgerSqn(threshold));
|
||||
auto const created = createEscrow(wasm);
|
||||
|
||||
ASSERT_LT(currentSeq(), threshold);
|
||||
auto const rejected = finish(created.seq);
|
||||
EXPECT_EQ(rejected.ter, tecBYTECODE_REJECTED);
|
||||
EXPECT_TRUE(escrowExists(created.seq)) << "a rejected escrow must survive";
|
||||
|
||||
ASSERT_TRUE(rejected.meta.has_value());
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
EXPECT_EQ(rejected.meta->getAsObject().getFieldI32(sfVMReturnCode), 0);
|
||||
|
||||
while (currentSeq() < threshold)
|
||||
{
|
||||
env.close();
|
||||
}
|
||||
|
||||
auto const approved = finish(created.seq);
|
||||
EXPECT_EQ(approved.ter, tesSUCCESS);
|
||||
EXPECT_FALSE(escrowExists(created.seq)) << "an approved escrow must be destroyed";
|
||||
|
||||
ASSERT_TRUE(approved.meta.has_value());
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
auto const meta = approved.meta->getAsObject();
|
||||
EXPECT_EQ(meta.getFieldI32(sfVMReturnCode), 5);
|
||||
EXPECT_TRUE(meta.isFieldPresent(sfGasUsed));
|
||||
}
|
||||
|
||||
TEST_F(BytecodeRun, TheBytecodeReserveIsHeldWhileTheEscrowLivesAndReleasedWhenItGoes)
|
||||
{
|
||||
EXPECT_EQ(env.getOwnerCount(alice), 0U);
|
||||
|
||||
auto const threshold = currentSeq() + 2;
|
||||
auto const wasm = assembleWat(gatedOnLedgerSqn(threshold));
|
||||
auto const created = createEscrow(wasm);
|
||||
|
||||
// `calculateAdditionalReserve`: one increment for the escrow, plus one per 500 bytes.
|
||||
auto const expected = 1U + static_cast<std::uint32_t>(wasm.size() / 500);
|
||||
EXPECT_EQ(env.getOwnerCount(alice), expected);
|
||||
|
||||
while (currentSeq() < threshold)
|
||||
{
|
||||
env.close();
|
||||
}
|
||||
ASSERT_EQ(finish(created.seq).ter, tesSUCCESS);
|
||||
|
||||
EXPECT_EQ(env.getOwnerCount(alice), 0U);
|
||||
}
|
||||
|
||||
TEST_F(BytecodeRun, CreatingChargesTheAmountAndTheFee)
|
||||
{
|
||||
auto const before = env.getXrpBalance(alice);
|
||||
|
||||
auto const wasm = assembleWat(gatedOnLedgerSqn(currentSeq() + 2));
|
||||
auto const created = createEscrow(wasm);
|
||||
|
||||
EXPECT_EQ(env.getXrpBalance(alice), before - XRP(1'000) - created.fee);
|
||||
EXPECT_EQ(env.getXrpBalance(carol), XRP(5'000));
|
||||
}
|
||||
|
||||
// The condition is the outer gate: without a fulfillment the contract is never reached, even
|
||||
// though it would have approved.
|
||||
TEST_F(BytecodeRun, AConditionIsCheckedBeforeTheContractRuns)
|
||||
{
|
||||
auto const threshold = currentSeq() + 2;
|
||||
auto const wasm = assembleWat(gatedOnLedgerSqn(threshold));
|
||||
auto const created = createEscrow(wasm, /*withCondition*/ true);
|
||||
|
||||
while (currentSeq() < threshold)
|
||||
{
|
||||
env.close();
|
||||
}
|
||||
|
||||
EXPECT_EQ(finish(created.seq).ter, tecCRYPTOCONDITION_ERROR);
|
||||
EXPECT_TRUE(escrowExists(created.seq));
|
||||
|
||||
auto const approved = finish(created.seq, /*withFulfillment*/ true);
|
||||
EXPECT_EQ(approved.ter, tesSUCCESS);
|
||||
EXPECT_FALSE(escrowExists(created.seq));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace xrpl::test
|
||||
122
src/tests/libxrpl/tx/wasm/transactor/BytecodeSize.cpp
Normal file
122
src/tests/libxrpl/tx/wasm/transactor/BytecodeSize.cpp
Normal file
@@ -0,0 +1,122 @@
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/Fees.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol_autogen/transactions/EscrowCreate.h>
|
||||
#include <xrpl/tx/wasm/WasmCommon.h>
|
||||
#include <xrpl/tx/wasm/WasmVM.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <helpers/Account.h>
|
||||
#include <helpers/TestServiceRegistry.h>
|
||||
#include <helpers/TxTest.h>
|
||||
#include <tx/wasm/fixtures/EscrowWasm.h>
|
||||
#include <tx/wasm/fixtures/ModuleBuilder.h>
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl::test {
|
||||
namespace {
|
||||
|
||||
TER
|
||||
createEscrowWith(TxTest& env, Account const& account, Bytes const& bytecode)
|
||||
{
|
||||
auto builder = transactions::EscrowCreateBuilder{account, account, STAmount{XRP(1'000)}};
|
||||
builder.setBytecode(makeSlice(bytecode));
|
||||
builder.setCancelAfter(closeTimeOffset(env, 100));
|
||||
|
||||
return env.submit(builder, account, escrowCreateFee(env, bytecode)).ter;
|
||||
}
|
||||
|
||||
// Rich enough for the owner reserve a 200 KB contract demands: 401 increments, 802 XRP.
|
||||
Account
|
||||
fundedAccount(TxTest& env)
|
||||
{
|
||||
auto const alice = Account{"alice"};
|
||||
env.createAccount(alice, XRP(2'000'000));
|
||||
return alice;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// The transactor screens `sfBytecode` against `bytecodeSizeLimit` before the module reaches
|
||||
// the engine. Compilation is unmetered, so that limit is the only thing bounding it.
|
||||
|
||||
// Footing for the rest: without this, "too big" and "malformed" are indistinguishable.
|
||||
TEST(BytecodeSize, TheBuildersProduceAModuleTheEngineAccepts)
|
||||
{
|
||||
auto env = TxTest{};
|
||||
auto const alice = fundedAccount(env);
|
||||
|
||||
EXPECT_EQ(createEscrowWith(env, alice, codeHeavyModule(1'000)), tesSUCCESS);
|
||||
EXPECT_EQ(createEscrowWith(env, alice, dataHeavyModule(1'000)), tesSUCCESS);
|
||||
}
|
||||
|
||||
TEST(BytecodeSize, AModuleUnderTheLimitIsAccepted)
|
||||
{
|
||||
auto env = TxTest{};
|
||||
auto const alice = fundedAccount(env);
|
||||
|
||||
auto const wasm = codeHeavyModule(90'000);
|
||||
ASSERT_LT(wasm.size(), env.getOpenLedger().fees().bytecodeSizeLimit);
|
||||
|
||||
EXPECT_EQ(createEscrowWith(env, alice, wasm), tesSUCCESS);
|
||||
}
|
||||
|
||||
TEST(BytecodeSize, AModuleOverTheLimitIsRefused)
|
||||
{
|
||||
auto env = TxTest{};
|
||||
auto const alice = fundedAccount(env);
|
||||
|
||||
auto const wasm = codeHeavyModule(110'000);
|
||||
ASSERT_GT(wasm.size(), env.getOpenLedger().fees().bytecodeSizeLimit);
|
||||
|
||||
EXPECT_EQ(createEscrowWith(env, alice, wasm), temMALFORMED);
|
||||
}
|
||||
|
||||
// The limit is on the module, not the code section — moving the bulk into a data segment
|
||||
// does not walk around it.
|
||||
TEST(BytecodeSize, ADataSegmentCountsTowardTheLimit)
|
||||
{
|
||||
auto env = TxTest{};
|
||||
auto const alice = fundedAccount(env);
|
||||
|
||||
auto const wasm = dataHeavyModule(110'000);
|
||||
ASSERT_GT(wasm.size(), env.getOpenLedger().fees().bytecodeSizeLimit);
|
||||
|
||||
EXPECT_EQ(createEscrowWith(env, alice, wasm), temMALFORMED);
|
||||
}
|
||||
|
||||
// The limit is a fee setting, so it moves. If raising it admits nothing new, some other cap
|
||||
// is really in charge.
|
||||
TEST(BytecodeSize, RaisingTheLimitAdmitsALargerModule)
|
||||
{
|
||||
auto fees = TestServiceRegistry::defaultFees();
|
||||
fees.bytecodeSizeLimit = kMaxBytecodeSizeLimit;
|
||||
auto env = TxTest{std::nullopt, fees};
|
||||
auto const alice = fundedAccount(env);
|
||||
|
||||
auto const wasm = codeHeavyModule(150'000);
|
||||
ASSERT_GT(wasm.size(), TestServiceRegistry::defaultFees().bytecodeSizeLimit);
|
||||
ASSERT_LT(wasm.size(), kMaxBytecodeSizeLimit);
|
||||
|
||||
EXPECT_EQ(createEscrowWith(env, alice, wasm), tesSUCCESS);
|
||||
}
|
||||
|
||||
// No per-function limit sits below the module limit: one function may occupy the entire
|
||||
// module. `wasmparser` defines `MAX_WASM_FUNCTION_SIZE` = 128 KiB, but nothing on this path
|
||||
// appears to enforce it — a lone body of a million instructions is accepted.
|
||||
//
|
||||
// So `bytecodeSizeLimit` is not defence in depth; it is the only bound on how much there is
|
||||
// to compile, and raising it raises the worst case with nothing behind it. A failure here
|
||||
// means a second limit has appeared, and that reasoning needs revisiting.
|
||||
TEST(BytecodeSize, ASingleFunctionBodyIsNotSeparatelyCapped)
|
||||
{
|
||||
auto const wasm = codeHeavyModule(1'000'000);
|
||||
ASSERT_GT(wasm.size(), 128U * 1024U) << "the module must exceed MAX_WASM_FUNCTION_SIZE";
|
||||
|
||||
EXPECT_EQ(preflightEscrowWasm(wasm, beast::Journal{beast::Journal::getNullSink()}), tesSUCCESS);
|
||||
}
|
||||
|
||||
} // namespace xrpl::test
|
||||
122
src/tests/libxrpl/tx/wasm/transactor/DataOnReject.cpp
Normal file
122
src/tests/libxrpl/tx/wasm/transactor/DataOnReject.cpp
Normal file
@@ -0,0 +1,122 @@
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/basics/strHex.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/SeqProxy.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxMeta.h>
|
||||
#include <xrpl/protocol_autogen/transactions/EscrowCreate.h>
|
||||
#include <xrpl/protocol_autogen/transactions/EscrowFinish.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <helpers/Account.h>
|
||||
#include <helpers/TxTest.h>
|
||||
#include <tx/wasm/fixtures/EscrowWasm.h>
|
||||
#include <tx/wasm/fixtures/WasmRun.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace xrpl::test {
|
||||
namespace {
|
||||
|
||||
// Writes "Data" and then rejects. `set_data` stores through the host, and the `-256` return
|
||||
// puts `EscrowFinish` on its `reValue <= 0` path — a contract-defined rejection, distinct
|
||||
// from a fault. The escrow survives, so whether the write survives with it is the question.
|
||||
constexpr auto kWritesThenRejects = std::string_view{R"wat(
|
||||
(module
|
||||
(import "host_lib" "set_data" (func $set_data (param i32 i32) (result i32)))
|
||||
(memory (export "memory") 1)
|
||||
(data (i32.const 0) "Data")
|
||||
(func (export "escrow_finish") (result i32)
|
||||
(drop (call $set_data (i32.const 0) (i32.const 4)))
|
||||
(i32.const -256)))
|
||||
)wat"};
|
||||
|
||||
constexpr std::int32_t kRejectCode = -256;
|
||||
|
||||
// Enough to run the contract; the test is about persistence, not budgets.
|
||||
constexpr std::uint32_t kAllowance = 100'000;
|
||||
|
||||
struct DataOnReject : testing::Test
|
||||
{
|
||||
TxTest env;
|
||||
Account const alice{"alice"};
|
||||
std::uint32_t escrowSeq{};
|
||||
|
||||
void
|
||||
SetUp() override
|
||||
{
|
||||
testing::Test::SetUp();
|
||||
env.createAccount(alice, XRP(5'000));
|
||||
|
||||
auto const wasm = assembleWat(kWritesThenRejects);
|
||||
escrowSeq = env.getAccountRoot(alice).getSequence();
|
||||
|
||||
auto builder = transactions::EscrowCreateBuilder{alice, alice, STAmount{XRP(1'000)}};
|
||||
builder.setBytecode(makeSlice(wasm));
|
||||
builder.setCancelAfter(closeTimeOffset(env, 1'000));
|
||||
|
||||
ASSERT_EQ(env.submit(builder, alice, escrowCreateFee(env, wasm)).ter, tesSUCCESS);
|
||||
env.close();
|
||||
}
|
||||
|
||||
[[nodiscard]] ClosedResult
|
||||
finish()
|
||||
{
|
||||
auto builder = transactions::EscrowFinishBuilder{alice, alice, escrowSeq};
|
||||
builder.setGas(kAllowance);
|
||||
|
||||
return env.submitAndClose(builder, alice, escrowFinishFee(env, kAllowance));
|
||||
}
|
||||
};
|
||||
|
||||
// The point of the whole shape: a contract that rejects can still leave a record of why.
|
||||
// `EscrowFinish` writes `sfData` *before* returning `tecBYTECODE_REJECTED`, and a `tec`
|
||||
// keeps its ledger changes, so the escrow survives carrying what the contract wrote.
|
||||
TEST_F(DataOnReject, ARejectingContractStillPersistsItsData)
|
||||
{
|
||||
auto const result = finish();
|
||||
EXPECT_EQ(result.ter, tecBYTECODE_REJECTED);
|
||||
|
||||
auto const sle =
|
||||
env.getOpenLedger().read(keylet::escrow(alice, SeqProxy::rawSequence(escrowSeq)));
|
||||
ASSERT_NE(sle, nullptr) << "a rejected finish must leave the escrow in place";
|
||||
ASSERT_TRUE(sle->isFieldPresent(sfData));
|
||||
|
||||
auto const data = sle->getFieldVL(sfData);
|
||||
EXPECT_EQ(std::string(data.begin(), data.end()), "Data") << strHex(data);
|
||||
}
|
||||
|
||||
// The reject code reaches the metadata, which is the only way a client learns *which*
|
||||
// rejection it was — every contract-defined reject shares one TER.
|
||||
TEST_F(DataOnReject, TheRejectCodeIsReportedInTheMetadata)
|
||||
{
|
||||
auto const result = finish();
|
||||
ASSERT_TRUE(result.meta.has_value());
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
auto const meta = result.meta->getAsObject();
|
||||
ASSERT_TRUE(meta.isFieldPresent(sfVMReturnCode));
|
||||
|
||||
EXPECT_EQ(meta.getFieldI32(sfVMReturnCode), kRejectCode);
|
||||
}
|
||||
|
||||
// Gas is reported even though the run ended in a rejection: the engine has a trustworthy
|
||||
// number whenever the contract ran to completion, and a reject is a completed run.
|
||||
TEST_F(DataOnReject, GasIsChargedAndReportedForARejectedRun)
|
||||
{
|
||||
auto const result = finish();
|
||||
ASSERT_TRUE(result.meta.has_value());
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
auto const meta = result.meta->getAsObject();
|
||||
ASSERT_TRUE(meta.isFieldPresent(sfGasUsed));
|
||||
|
||||
auto const used = meta.getFieldU32(sfGasUsed);
|
||||
EXPECT_GT(used, 0U);
|
||||
EXPECT_LE(used, kAllowance) << "the engine cannot spend more than it was given";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace xrpl::test
|
||||
198
src/tests/libxrpl/tx/wasm/transactor/FinishFailures.cpp
Normal file
198
src/tests/libxrpl/tx/wasm/transactor/FinishFailures.cpp
Normal file
@@ -0,0 +1,198 @@
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxMeta.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/protocol_autogen/transactions/EscrowCreate.h>
|
||||
#include <xrpl/protocol_autogen/transactions/EscrowFinish.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <helpers/Account.h>
|
||||
#include <helpers/TestServiceRegistry.h>
|
||||
#include <helpers/TxTest.h>
|
||||
#include <tx/wasm/fixtures/EscrowWasm.h>
|
||||
#include <tx/wasm/fixtures/WasmRun.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
|
||||
namespace xrpl::test {
|
||||
namespace {
|
||||
|
||||
// The ways an `EscrowFinish` against a contract-bearing escrow fails, and what each reports.
|
||||
// A rejection, a fault, and running out of gas are three outcomes; only one carries a return
|
||||
// code.
|
||||
struct FinishFailures : testing::Test
|
||||
{
|
||||
TxTest env;
|
||||
Account const alice{"alice"};
|
||||
Account const carol{"carol"};
|
||||
|
||||
FinishFailures()
|
||||
{
|
||||
createAccounts(env, XRP(5'000), alice, carol);
|
||||
}
|
||||
|
||||
std::uint32_t
|
||||
createEscrow(std::string_view wat)
|
||||
{
|
||||
auto const wasm = assembleWat(wat);
|
||||
auto const seq = env.getAccountRoot(alice).getSequence();
|
||||
|
||||
auto builder = transactions::EscrowCreateBuilder{alice, carol, STAmount{XRP(500)}};
|
||||
builder.setBytecode(makeSlice(wasm));
|
||||
builder.setCancelAfter(closeTimeOffset(env, 1'000));
|
||||
|
||||
EXPECT_EQ(env.submit(builder, alice, escrowCreateFee(env, wasm)).ter, tesSUCCESS);
|
||||
env.close();
|
||||
return seq;
|
||||
}
|
||||
|
||||
std::uint32_t
|
||||
createPlainEscrow()
|
||||
{
|
||||
auto const seq = env.getAccountRoot(alice).getSequence();
|
||||
|
||||
auto builder = transactions::EscrowCreateBuilder{alice, carol, STAmount{XRP(500)}};
|
||||
// A contract-free escrow needs a `FinishAfter` or a condition; `CancelAfter` alone is
|
||||
// `temMALFORMED`.
|
||||
builder.setFinishAfter(closeTimeOffset(env, 1));
|
||||
builder.setCancelAfter(closeTimeOffset(env, 1'000));
|
||||
|
||||
EXPECT_EQ(env.submit(builder, alice, XRPAmount{100'000}).ter, tesSUCCESS);
|
||||
env.close();
|
||||
env.close(); // past the finish time
|
||||
return seq;
|
||||
}
|
||||
|
||||
[[nodiscard]] ClosedResult
|
||||
finish(std::uint32_t seq, std::optional<std::uint32_t> allowance, XRPAmount fee)
|
||||
{
|
||||
auto builder = transactions::EscrowFinishBuilder{carol, alice, seq};
|
||||
if (allowance)
|
||||
{
|
||||
builder.setGas(*allowance);
|
||||
}
|
||||
|
||||
return env.submitAndClose(builder, carol, fee);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(FinishFailures, FinishIsRefusedWhileSmartEscrowIsDisabled)
|
||||
{
|
||||
auto disabled = TxTest{allFeatures() - featureSmartEscrow};
|
||||
createAccounts(disabled, XRP(5'000), alice, carol);
|
||||
|
||||
auto builder = transactions::EscrowFinishBuilder{carol, alice, 1};
|
||||
builder.setGas(4);
|
||||
|
||||
EXPECT_EQ(disabled.submit(builder, carol, XRPAmount{100'000}).ter, temDISABLED);
|
||||
}
|
||||
|
||||
// Execution cannot be bought unbounded just by asking for it.
|
||||
TEST_F(FinishFailures, AnAllowancePastTheGasLimitIsRefused)
|
||||
{
|
||||
auto fees = TestServiceRegistry::defaultFees();
|
||||
fees.gasLimit = 1'000;
|
||||
env.getServiceRegistry().setFees(fees);
|
||||
|
||||
auto builder = transactions::EscrowFinishBuilder{carol, alice, 1};
|
||||
builder.setGas(1'001);
|
||||
|
||||
EXPECT_EQ(env.submit(builder, carol, XRPAmount{10'000'000}).ter, temBAD_LIMIT);
|
||||
}
|
||||
|
||||
// A zero gas limit turns the runtime off.
|
||||
TEST_F(FinishFailures, AZeroGasLimitDisablesFinishing)
|
||||
{
|
||||
auto const seq = createEscrow(kReadsLedgerSqn);
|
||||
|
||||
auto fees = TestServiceRegistry::defaultFees();
|
||||
fees.gasLimit = 0;
|
||||
env.getServiceRegistry().setFees(fees);
|
||||
|
||||
auto builder = transactions::EscrowFinishBuilder{carol, alice, seq};
|
||||
builder.setGas(1'000);
|
||||
|
||||
EXPECT_EQ(env.submit(builder, carol, XRPAmount{10'000'000}).ter, temTEMP_DISABLED);
|
||||
}
|
||||
|
||||
TEST_F(FinishFailures, AFinishWithoutAGasFieldIsRefused)
|
||||
{
|
||||
auto const seq = createEscrow(kReadsLedgerSqn);
|
||||
|
||||
EXPECT_EQ(finish(seq, std::nullopt, XRPAmount{100'000}).ter, tefBYTECODE_NOT_INCLUDED);
|
||||
}
|
||||
|
||||
TEST_F(FinishFailures, AZeroAllowanceIsRefused)
|
||||
{
|
||||
auto const seq = createEscrow(kReadsLedgerSqn);
|
||||
|
||||
auto builder = transactions::EscrowFinishBuilder{carol, alice, seq};
|
||||
builder.setGas(0);
|
||||
|
||||
EXPECT_EQ(env.submit(builder, carol, XRPAmount{100'000}).ter, temBAD_LIMIT);
|
||||
}
|
||||
|
||||
// The allowance is paid up front, so under-paying is caught before anything runs.
|
||||
TEST_F(FinishFailures, AFeeThatDoesNotCoverTheAllowanceIsRefused)
|
||||
{
|
||||
auto const seq = createEscrow(kReadsLedgerSqn);
|
||||
constexpr std::uint32_t kAllowance = 1'000;
|
||||
|
||||
auto const fee = escrowFinishFee(env, kAllowance) - XRPAmount{1};
|
||||
EXPECT_EQ(finish(seq, kAllowance, fee).ter, telINSUF_FEE_P);
|
||||
}
|
||||
|
||||
TEST_F(FinishFailures, GasAgainstAnEscrowWithoutBytecodeIsRefused)
|
||||
{
|
||||
auto const seq = createPlainEscrow();
|
||||
constexpr std::uint32_t kAllowance = 100;
|
||||
|
||||
EXPECT_EQ(finish(seq, kAllowance, escrowFinishFee(env, kAllowance)).ter, tefNO_BYTECODE);
|
||||
}
|
||||
|
||||
// A band rather than an equality: the meter stops at the last instruction it could afford,
|
||||
// leaving a few units unspent. That band is what separates this from a trap, which stops
|
||||
// early and reports a small fraction.
|
||||
TEST_F(FinishFailures, RunningOutOfGasConsumesEssentiallyTheWholeAllowanceAndReportsNoReturnCode)
|
||||
{
|
||||
auto const seq = createEscrow(kLoopsForever);
|
||||
constexpr std::uint32_t kAllowance = 10'000;
|
||||
|
||||
auto const result = finish(seq, kAllowance, escrowFinishFee(env, kAllowance));
|
||||
EXPECT_EQ(result.ter, tecOUT_OF_GAS);
|
||||
|
||||
ASSERT_TRUE(result.meta.has_value());
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
auto const meta = result.meta->getAsObject();
|
||||
ASSERT_TRUE(meta.isFieldPresent(sfGasUsed));
|
||||
|
||||
auto const used = meta.getFieldU32(sfGasUsed);
|
||||
EXPECT_LE(used, kAllowance) << "the engine cannot spend more than it was given";
|
||||
EXPECT_GT(used, kAllowance - (kAllowance / 100));
|
||||
EXPECT_FALSE(meta.isFieldPresent(sfVMReturnCode));
|
||||
}
|
||||
|
||||
// A trap is a fault, not a rejection: gas actually burned, and no return code.
|
||||
TEST_F(FinishFailures, ATrapReportsPartialGasAndNoReturnCode)
|
||||
{
|
||||
auto const seq = createEscrow(kTraps);
|
||||
constexpr std::uint32_t kAllowance = 1'000;
|
||||
|
||||
auto const result = finish(seq, kAllowance, escrowFinishFee(env, kAllowance));
|
||||
EXPECT_EQ(result.ter, tecFAILED_PROCESSING);
|
||||
|
||||
ASSERT_TRUE(result.meta.has_value());
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
auto const meta = result.meta->getAsObject();
|
||||
ASSERT_TRUE(meta.isFieldPresent(sfGasUsed));
|
||||
EXPECT_LT(meta.getFieldU32(sfGasUsed), kAllowance);
|
||||
EXPECT_FALSE(meta.isFieldPresent(sfVMReturnCode));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace xrpl::test
|
||||
99
src/tests/libxrpl/tx/wasm/transactor/GasFees.cpp
Normal file
99
src/tests/libxrpl/tx/wasm/transactor/GasFees.cpp
Normal file
@@ -0,0 +1,99 @@
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/protocol_autogen/transactions/EscrowCreate.h>
|
||||
#include <xrpl/protocol_autogen/transactions/EscrowFinish.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <helpers/Account.h>
|
||||
#include <helpers/TxTest.h>
|
||||
#include <tx/wasm/fixtures/EscrowWasm.h>
|
||||
#include <tx/wasm/fixtures/WasmRun.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace xrpl::test {
|
||||
namespace {
|
||||
|
||||
// allowance × gasPrice is a product of two 32-bit values, so a narrow intermediate would wrap
|
||||
// and let a large allowance be bought for almost nothing. These pin that it costs a big fee.
|
||||
|
||||
// Near the default gas limit, so the product is as large as the transactor ever computes.
|
||||
constexpr std::uint32_t kBigAllowance = 996'433;
|
||||
|
||||
struct GasFees : testing::Test
|
||||
{
|
||||
TxTest env;
|
||||
Account const alice{"alice"};
|
||||
Account const carol{"carol"};
|
||||
std::uint32_t escrowSeq{};
|
||||
|
||||
void
|
||||
SetUp() override
|
||||
{
|
||||
testing::Test::SetUp();
|
||||
createAccounts(env, XRP(5'000), alice, carol);
|
||||
|
||||
auto const wasm = assembleWat(kReadsLedgerSqn);
|
||||
escrowSeq = env.getAccountRoot(alice).getSequence();
|
||||
|
||||
auto builder = transactions::EscrowCreateBuilder{alice, carol, STAmount{XRP(1'000)}};
|
||||
builder.setBytecode(makeSlice(wasm));
|
||||
builder.setCancelAfter(closeTimeOffset(env, 1'000));
|
||||
|
||||
ASSERT_EQ(env.submit(builder, alice, escrowCreateFee(env, wasm)).ter, tesSUCCESS);
|
||||
env.close();
|
||||
}
|
||||
|
||||
[[nodiscard]] TER
|
||||
finishPaying(XRPAmount fee)
|
||||
{
|
||||
auto builder = transactions::EscrowFinishBuilder{carol, alice, escrowSeq};
|
||||
builder.setGas(kBigAllowance);
|
||||
return env.submit(builder, carol, fee).ter;
|
||||
}
|
||||
};
|
||||
|
||||
// If the product ever wrapped, this is the test that notices: 30 drops would start looking
|
||||
// sufficient.
|
||||
TEST_F(GasFees, ALargeAllowanceCannotBeBoughtForAFewDrops)
|
||||
{
|
||||
auto const owed = escrowFinishFee(env, kBigAllowance);
|
||||
ASSERT_GT(owed.drops(), kBigAllowance) << "the fee must scale with the allowance";
|
||||
|
||||
EXPECT_EQ(finishPaying(XRPAmount{30}), telINSUF_FEE_P);
|
||||
}
|
||||
|
||||
TEST_F(GasFees, AFeeOneDropShortOfTheAllowanceIsRefused)
|
||||
{
|
||||
EXPECT_EQ(finishPaying(escrowFinishFee(env, kBigAllowance) - XRPAmount{1}), telINSUF_FEE_P);
|
||||
}
|
||||
|
||||
// Otherwise the two refusals above prove nothing: any fee at all might be rejected.
|
||||
TEST_F(GasFees, TheExactFeeIsAccepted)
|
||||
{
|
||||
EXPECT_EQ(finishPaying(escrowFinishFee(env, kBigAllowance)), tesSUCCESS);
|
||||
}
|
||||
|
||||
// Asking for a near-limit budget does not mean spending it.
|
||||
TEST_F(GasFees, OnlyTheGasActuallyUsedIsReported)
|
||||
{
|
||||
auto builder = transactions::EscrowFinishBuilder{carol, alice, escrowSeq};
|
||||
builder.setGas(kBigAllowance);
|
||||
|
||||
auto const result = env.submitAndClose(builder, carol, escrowFinishFee(env, kBigAllowance));
|
||||
ASSERT_EQ(result.ter, tesSUCCESS);
|
||||
|
||||
ASSERT_TRUE(result.meta.has_value());
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
auto const obj = result.meta->getAsObject();
|
||||
ASSERT_TRUE(obj.isFieldPresent(sfGasUsed));
|
||||
|
||||
EXPECT_LT(obj.getFieldU32(sfGasUsed), kBigAllowance);
|
||||
EXPECT_EQ(obj.getFieldI32(sfVMReturnCode), 5);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace xrpl::test
|
||||
Reference in New Issue
Block a user