From fe75c5d441c5c4e93496372edda268f04b624406 Mon Sep 17 00:00:00 2001 From: Oleksandr <115580134+oleks-rip@users.noreply.github.com> Date: Tue, 11 Nov 2025 12:50:20 -0500 Subject: [PATCH] Wasm SmartEscrow --- cfg/rippled-example.cfg | 33 + include/xrpl/ledger/ApplyViewImpl.h | 14 + include/xrpl/ledger/detail/ApplyStateTable.h | 2 + include/xrpl/protocol/Fees.h | 6 + include/xrpl/protocol/Indexes.h | 6 + include/xrpl/protocol/TxMeta.h | 32 + include/xrpl/protocol/detail/features.macro | 2 +- .../xrpl/protocol/detail/ledger_entries.macro | 7 + include/xrpl/protocol/detail/sfields.macro | 9 +- .../xrpl/protocol/detail/transactions.macro | 9 +- include/xrpl/protocol/jss.h | 9 +- src/libxrpl/ledger/ApplyStateTable.cpp | 4 + src/libxrpl/ledger/ApplyViewImpl.cpp | 11 +- src/libxrpl/protocol/STValidation.cpp | 4 + src/libxrpl/protocol/TxMeta.cpp | 6 + src/test/app/EscrowSmart_test.cpp | 916 ++++++++++++++++++ src/test/app/Escrow_test.cpp | 4 +- src/test/app/HostFuncImpl_test.cpp | 19 - src/test/app/NetworkOPs_test.cpp | 6 +- src/test/app/PseudoTx_test.cpp | 8 + src/test/app/TestHostFunctions.h | 20 +- src/test/jtx/escrow.h | 70 ++ src/test/jtx/impl/envconfig.cpp | 5 + src/test/rpc/Subscribe_test.cpp | 19 +- src/xrpld/app/ledger/Ledger.cpp | 48 +- src/xrpld/app/misc/NetworkOPs.cpp | 49 +- src/xrpld/app/tx/detail/ApplyContext.cpp | 5 + src/xrpld/app/tx/detail/ApplyContext.h | 16 + src/xrpld/app/tx/detail/Escrow.cpp | 279 +++++- src/xrpld/app/tx/detail/Escrow.h | 6 + src/xrpld/app/tx/detail/Transactor.cpp | 37 +- src/xrpld/core/Config.h | 9 + src/xrpld/core/detail/Config.cpp | 6 + 33 files changed, 1588 insertions(+), 88 deletions(-) create mode 100644 src/test/app/EscrowSmart_test.cpp diff --git a/cfg/rippled-example.cfg b/cfg/rippled-example.cfg index 5db008431d..95b97f7a3c 100644 --- a/cfg/rippled-example.cfg +++ b/cfg/rippled-example.cfg @@ -1290,6 +1290,39 @@ # Example: # owner_reserve = 2000000 # 2 XRP # +# extension_compute_limit = +# +# The extension compute limit is the maximum amount of gas that can be +# consumed by a single transaction. The gas limit is used to prevent +# transactions from consuming too many resources. +# +# If this parameter is unspecified, rippled will use an internal +# default. Don't change this without understanding the consequences. +# +# Example: +# extension_compute_limit = 1000000 # 1 million gas +# +# extension_size_limit = +# +# The extension size limit is the maximum size of a WASM extension in +# bytes. The size limit is used to prevent extensions from consuming +# too many resources. +# +# If this parameter is unspecified, rippled will use an internal +# default. Don't change this without understanding the consequences. +# +# Example: +# extension_size_limit = 100000 # 100 kb +# +# gas_price = +# +# The gas price is the conversion between WASM gas and its price in drops. +# +# If this parameter is unspecified, rippled will use an internal +# default. Don't change this without understanding the consequences. +# +# Example: +# gas_price = 1000000 # 1 drop per gas #------------------------------------------------------------------------------- # # 9. Misc Settings diff --git a/include/xrpl/ledger/ApplyViewImpl.h b/include/xrpl/ledger/ApplyViewImpl.h index c1e9ccd359..8a8b9b4e86 100644 --- a/include/xrpl/ledger/ApplyViewImpl.h +++ b/include/xrpl/ledger/ApplyViewImpl.h @@ -55,6 +55,18 @@ public: deliver_ = amount; } + void + setGasUsed(std::optional const gasUsed) + { + gasUsed_ = gasUsed; + } + + void + setWasmReturnCode(std::int32_t const wasmReturnCode) + { + wasmReturnCode_ = wasmReturnCode; + } + /** Get the number of modified entries */ std::size_t @@ -73,6 +85,8 @@ public: private: std::optional deliver_; + std::optional gasUsed_; + std::optional wasmReturnCode_; }; } // namespace ripple diff --git a/include/xrpl/ledger/detail/ApplyStateTable.h b/include/xrpl/ledger/detail/ApplyStateTable.h index 887e2e7770..a5b1e549f0 100644 --- a/include/xrpl/ledger/detail/ApplyStateTable.h +++ b/include/xrpl/ledger/detail/ApplyStateTable.h @@ -53,6 +53,8 @@ public: TER ter, std::optional const& deliver, std::optional const& parentBatchId, + std::optional const& gasUsed, + std::optional const& wasmReturnCode, bool isDryRun, beast::Journal j); diff --git a/include/xrpl/protocol/Fees.h b/include/xrpl/protocol/Fees.h index 43ba6c9552..0ad350dc35 100644 --- a/include/xrpl/protocol/Fees.h +++ b/include/xrpl/protocol/Fees.h @@ -5,6 +5,8 @@ namespace ripple { +constexpr std::uint32_t MICRO_DROPS_PER_DROP{1'000'000}; + /** Reflects the fee settings for a particular ledger. The fees are always the same for any transactions applied @@ -15,6 +17,10 @@ struct Fees XRPAmount base{0}; // Reference tx cost (drops) XRPAmount reserve{0}; // Reserve base (drops) XRPAmount increment{0}; // Reserve increment (drops) + std::uint32_t extensionComputeLimit{ + 0}; // Extension compute limit (instructions) + std::uint32_t extensionSizeLimit{0}; // Extension size limit (bytes) + std::uint32_t gasPrice{0}; // price of WASM gas (micro-drops) explicit Fees() = default; Fees(Fees const&) = default; diff --git a/include/xrpl/protocol/Indexes.h b/include/xrpl/protocol/Indexes.h index 69418fbd25..e24af94221 100644 --- a/include/xrpl/protocol/Indexes.h +++ b/include/xrpl/protocol/Indexes.h @@ -212,6 +212,12 @@ page(Keylet const& root, std::uint64_t index = 0) noexcept Keylet escrow(AccountID const& src, std::uint32_t seq) noexcept; +inline Keylet +escrow(uint256 const& key) noexcept +{ + return {ltESCROW, key}; +} + /** A PaymentChannel */ Keylet payChan(AccountID const& src, AccountID const& dst, std::uint32_t seq) noexcept; diff --git a/include/xrpl/protocol/TxMeta.h b/include/xrpl/protocol/TxMeta.h index 3ab58c9d0a..0741caee38 100644 --- a/include/xrpl/protocol/TxMeta.h +++ b/include/xrpl/protocol/TxMeta.h @@ -85,6 +85,12 @@ public: if (obj.isFieldPresent(sfParentBatchID)) parentBatchID_ = obj.getFieldH256(sfParentBatchID); + + if (obj.isFieldPresent(sfGasUsed)) + gasUsed_ = obj.getFieldU32(sfGasUsed); + + if (obj.isFieldPresent(sfWasmReturnCode)) + wasmReturnCode_ = obj.getFieldI32(sfWasmReturnCode); } std::optional const& @@ -105,6 +111,30 @@ public: parentBatchID_ = id; } + void + setGasUsed(std::optional const gasUsed) + { + gasUsed_ = gasUsed; + } + + std::optional const& + getGasUsed() const + { + return gasUsed_; + } + + void + setWasmReturnCode(std::optional const wasmReturnCode) + { + wasmReturnCode_ = wasmReturnCode; + } + + std::optional const& + getWasmReturnCode() const + { + return wasmReturnCode_; + } + private: uint256 transactionID_; std::uint32_t ledgerSeq_; @@ -113,6 +143,8 @@ private: std::optional deliveredAmount_; std::optional parentBatchID_; + std::optional gasUsed_; + std::optional wasmReturnCode_; STArray nodes_; }; diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 5c8d2aa198..1bd60a4a3f 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -16,6 +16,7 @@ // Add new amendments to the top of this list. // Keep it sorted in reverse chronological order. +XRPL_FEATURE(SmartEscrow, Supported::no, VoteBehavior::DefaultNo) XRPL_FEATURE(LendingProtocol, Supported::no, VoteBehavior::DefaultNo) XRPL_FEATURE(PermissionDelegationV1_1, Supported::no, VoteBehavior::DefaultNo) XRPL_FIX (DirectoryLimit, Supported::yes, VoteBehavior::DefaultNo) @@ -32,7 +33,6 @@ XRPL_FEATURE(PermissionedDEX, Supported::yes, VoteBehavior::DefaultNo XRPL_FEATURE(Batch, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(SingleAssetVault, Supported::no, VoteBehavior::DefaultNo) XRPL_FIX (PayChanCancelAfter, Supported::yes, VoteBehavior::DefaultNo) -// Check flags in Credential transactions XRPL_FIX (InvalidTxFlags, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (FrozenLPTokenTransfer, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(DeepFreeze, Supported::yes, VoteBehavior::DefaultNo) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 1034c35895..4f61c0bf56 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -302,6 +302,11 @@ LEDGER_ENTRY(ltFEE_SETTINGS, 0x0073, FeeSettings, fee, ({ {sfBaseFeeDrops, soeOPTIONAL}, {sfReserveBaseDrops, soeOPTIONAL}, {sfReserveIncrementDrops, soeOPTIONAL}, + // Smart Escrow fields + {sfExtensionComputeLimit, soeOPTIONAL}, + {sfExtensionSizeLimit, soeOPTIONAL}, + {sfGasPrice, soeOPTIONAL}, + {sfPreviousTxnID, soeOPTIONAL}, {sfPreviousTxnLgrSeq, soeOPTIONAL}, })) @@ -332,6 +337,8 @@ LEDGER_ENTRY(ltESCROW, 0x0075, Escrow, escrow, ({ {sfCondition, soeOPTIONAL}, {sfCancelAfter, soeOPTIONAL}, {sfFinishAfter, soeOPTIONAL}, + {sfFinishFunction, soeOPTIONAL}, + {sfData, soeOPTIONAL}, {sfSourceTag, soeOPTIONAL}, {sfDestinationTag, soeOPTIONAL}, {sfOwnerNode, soeREQUIRED}, diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index d5c5d9447f..40ecb5f5fe 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -114,6 +114,11 @@ TYPED_SFIELD(sfInterestRate, UINT32, 65) // 1/10 basis points (bi TYPED_SFIELD(sfLateInterestRate, UINT32, 66) // 1/10 basis points (bips) TYPED_SFIELD(sfCloseInterestRate, UINT32, 67) // 1/10 basis points (bips) TYPED_SFIELD(sfOverpaymentInterestRate, UINT32, 68) // 1/10 basis points (bips) +TYPED_SFIELD(sfExtensionComputeLimit, UINT32, 69) +TYPED_SFIELD(sfExtensionSizeLimit, UINT32, 70) +TYPED_SFIELD(sfGasPrice, UINT32, 71) +TYPED_SFIELD(sfComputationAllowance, UINT32, 72) +TYPED_SFIELD(sfGasUsed, UINT32, 73) // 64-bit integers (common) TYPED_SFIELD(sfIndexNext, UINT64, 1) @@ -226,6 +231,7 @@ TYPED_SFIELD(sfManagementFeeOutstanding, NUMBER, 17) // int32 TYPED_SFIELD(sfLoanScale, INT32, 1) +TYPED_SFIELD(sfWasmReturnCode, INT32, 2) // currency amount (common) TYPED_SFIELD(sfAmount, AMOUNT, 1) @@ -255,7 +261,7 @@ TYPED_SFIELD(sfBaseFeeDrops, AMOUNT, 22) TYPED_SFIELD(sfReserveBaseDrops, AMOUNT, 23) TYPED_SFIELD(sfReserveIncrementDrops, AMOUNT, 24) -// currency amount (AMM) +// currency amount (more) TYPED_SFIELD(sfLPTokenOut, AMOUNT, 25) TYPED_SFIELD(sfLPTokenIn, AMOUNT, 26) TYPED_SFIELD(sfEPrice, AMOUNT, 27) @@ -297,6 +303,7 @@ TYPED_SFIELD(sfAssetClass, VL, 28) TYPED_SFIELD(sfProvider, VL, 29) TYPED_SFIELD(sfMPTokenMetadata, VL, 30) TYPED_SFIELD(sfCredentialType, VL, 31) +TYPED_SFIELD(sfFinishFunction, VL, 32) // account (common) TYPED_SFIELD(sfAccount, ACCOUNT, 1) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 6d2d833440..29d760097a 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -50,11 +50,13 @@ TRANSACTION(ttESCROW_CREATE, 1, EscrowCreate, noPriv, ({ {sfDestination, soeREQUIRED}, + {sfDestinationTag, soeOPTIONAL}, {sfAmount, soeREQUIRED, soeMPTSupported}, {sfCondition, soeOPTIONAL}, {sfCancelAfter, soeOPTIONAL}, {sfFinishAfter, soeOPTIONAL}, - {sfDestinationTag, soeOPTIONAL}, + {sfFinishFunction, soeOPTIONAL}, + {sfData, soeOPTIONAL}, })) /** This transaction type completes an existing escrow. */ @@ -68,6 +70,7 @@ TRANSACTION(ttESCROW_FINISH, 2, EscrowFinish, {sfFulfillment, soeOPTIONAL}, {sfCondition, soeOPTIONAL}, {sfCredentialIDs, soeOPTIONAL}, + {sfComputationAllowance, soeOPTIONAL}, })) @@ -1092,6 +1095,10 @@ TRANSACTION(ttFEE, 101, SetFee, {sfBaseFeeDrops, soeOPTIONAL}, {sfReserveBaseDrops, soeOPTIONAL}, {sfReserveIncrementDrops, soeOPTIONAL}, + // Smart Escrow fields + {sfExtensionComputeLimit, soeOPTIONAL}, + {sfExtensionSizeLimit, soeOPTIONAL}, + {sfGasPrice, soeOPTIONAL}, })) /** This system-generated transaction type is used to update the network's negative UNL diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index b9a8945d21..138f3d81fb 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -255,6 +255,9 @@ JSS(expected_date_UTC); // out: any (warnings) JSS(expected_ledger_size); // out: TxQ JSS(expiration); // out: AccountOffers, AccountChannels, // ValidatorList, amm_info +JSS(extension_compute); // out: NetworkOps +JSS(extension_size); // out: NetworkOps +JSS(gas_price); // out: NetworkOps JSS(fail_hard); // in: Sign, Submit JSS(failed); // out: InboundLedger JSS(feature); // in: Feature @@ -709,11 +712,11 @@ JSS(write_load); // out: GetCounts #pragma push_macro("LEDGER_ENTRY_DUPLICATE") #undef LEDGER_ENTRY_DUPLICATE -#define LEDGER_ENTRY(tag, value, name, rpcName, ...) \ - JSS(name); \ +#define LEDGER_ENTRY(tag, value, name, rpcName, fields) \ + JSS(name); \ JSS(rpcName); -#define LEDGER_ENTRY_DUPLICATE(tag, value, name, rpcName, ...) JSS(rpcName); +#define LEDGER_ENTRY_DUPLICATE(tag, value, name, rpcName, fields) JSS(rpcName); #include diff --git a/src/libxrpl/ledger/ApplyStateTable.cpp b/src/libxrpl/ledger/ApplyStateTable.cpp index c236f0d1b5..a992d72b5f 100644 --- a/src/libxrpl/ledger/ApplyStateTable.cpp +++ b/src/libxrpl/ledger/ApplyStateTable.cpp @@ -97,6 +97,8 @@ ApplyStateTable::apply( TER ter, std::optional const& deliver, std::optional const& parentBatchId, + std::optional const& gasUsed, + std::optional const& wasmReturnCode, bool isDryRun, beast::Journal j) { @@ -111,6 +113,8 @@ ApplyStateTable::apply( meta.setDeliveredAmount(deliver); meta.setParentBatchID(parentBatchId); + meta.setGasUsed(gasUsed); + meta.setWasmReturnCode(wasmReturnCode); Mods newMod; for (auto& item : items_) diff --git a/src/libxrpl/ledger/ApplyViewImpl.cpp b/src/libxrpl/ledger/ApplyViewImpl.cpp index 90652dd45b..f204bc9c6a 100644 --- a/src/libxrpl/ledger/ApplyViewImpl.cpp +++ b/src/libxrpl/ledger/ApplyViewImpl.cpp @@ -16,7 +16,16 @@ ApplyViewImpl::apply( bool isDryRun, beast::Journal j) { - return items_.apply(to, tx, ter, deliver_, parentBatchId, isDryRun, j); + return items_.apply( + to, + tx, + ter, + deliver_, + parentBatchId, + gasUsed_, + wasmReturnCode_, + isDryRun, + j); } std::size_t diff --git a/src/libxrpl/protocol/STValidation.cpp b/src/libxrpl/protocol/STValidation.cpp index 3c89f31896..85a0791c93 100644 --- a/src/libxrpl/protocol/STValidation.cpp +++ b/src/libxrpl/protocol/STValidation.cpp @@ -58,6 +58,10 @@ STValidation::validationFormat() {sfBaseFeeDrops, soeOPTIONAL}, {sfReserveBaseDrops, soeOPTIONAL}, {sfReserveIncrementDrops, soeOPTIONAL}, + // featureSmartEscrow + {sfExtensionComputeLimit, soeOPTIONAL}, + {sfExtensionSizeLimit, soeOPTIONAL}, + {sfGasPrice, soeOPTIONAL}, }; // clang-format on diff --git a/src/libxrpl/protocol/TxMeta.cpp b/src/libxrpl/protocol/TxMeta.cpp index ebc1d87b14..19d4a56ef3 100644 --- a/src/libxrpl/protocol/TxMeta.cpp +++ b/src/libxrpl/protocol/TxMeta.cpp @@ -211,6 +211,12 @@ TxMeta::getAsObject() const if (parentBatchID_.has_value()) metaData.setFieldH256(sfParentBatchID, *parentBatchID_); + if (gasUsed_.has_value()) + metaData.setFieldU32(sfGasUsed, *gasUsed_); + + if (wasmReturnCode_.has_value()) + metaData.setFieldI32(sfWasmReturnCode, *wasmReturnCode_); + return metaData; } diff --git a/src/test/app/EscrowSmart_test.cpp b/src/test/app/EscrowSmart_test.cpp new file mode 100644 index 0000000000..6623522637 --- /dev/null +++ b/src/test/app/EscrowSmart_test.cpp @@ -0,0 +1,916 @@ +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +namespace ripple { +namespace test { + +struct EscrowSmart_test : public beast::unit_test::suite +{ + void + testCreateFinishFunctionPreflight(FeatureBitset features) + { + testcase("Test preflight checks involving FinishFunction"); + + using namespace jtx; + using namespace std::chrono; + + Account const alice{"alice"}; + Account const carol{"carol"}; + + // Tests whether the ledger index is >= 5 + // getLedgerSqn() >= 5} + static auto wasmHex = ledgerSqnWasmHex; + + { + // featureSmartEscrow disabled + Env env(*this, features - featureSmartEscrow); + env.fund(XRP(5000), alice, carol); + XRPAmount const txnFees = env.current()->fees().base + 1000; + auto escrowCreate = escrow::create(alice, carol, XRP(1000)); + env(escrowCreate, + escrow::finish_function(wasmHex), + escrow::cancel_time(env.now() + 100s), + fee(txnFees), + ter(temDISABLED)); + env.close(); + + env(escrowCreate, + escrow::finish_function(wasmHex), + escrow::cancel_time(env.now() + 100s), + escrow::data("00112233"), + fee(txnFees), + ter(temDISABLED)); + env.close(); + } + + { + // FinishFunction > max length + Env env( + *this, + envconfig([](std::unique_ptr cfg) { + cfg->FEES.extension_size_limit = 10; // 10 bytes + return cfg; + }), + features); + XRPAmount const txnFees = env.current()->fees().base + 1000; + // create escrow + env.fund(XRP(5000), alice, carol); + + auto escrowCreate = escrow::create(alice, carol, XRP(500)); + + // 11-byte string + std::string longWasmHex = "00112233445566778899AA"; + env(escrowCreate, + escrow::finish_function(longWasmHex), + escrow::cancel_time(env.now() + 100s), + fee(txnFees), + ter(temMALFORMED)); + env.close(); + } + + { + // Data without FinishFunction + Env env(*this, features); + XRPAmount const txnFees = env.current()->fees().base + 100000; + // create escrow + env.fund(XRP(5000), alice, carol); + + auto escrowCreate = escrow::create(alice, carol, XRP(500)); + + std::string longData(4, 'A'); + env(escrowCreate, + escrow::data(longData), + escrow::finish_time(env.now() + 100s), + fee(txnFees), + ter(temMALFORMED)); + env.close(); + } + + { + // Data > max length + Env env(*this, features); + XRPAmount const txnFees = env.current()->fees().base + 100000; + // create escrow + env.fund(XRP(5000), alice, carol); + + auto escrowCreate = escrow::create(alice, carol, XRP(500)); + + // string of length maxWasmDataLength * 2 + 2 + std::string longData(maxWasmDataLength * 2 + 2, 'B'); + env(escrowCreate, + escrow::data(longData), + escrow::finish_function(wasmHex), + escrow::cancel_time(env.now() + 100s), + fee(txnFees), + ter(temMALFORMED)); + env.close(); + } + + Env env( + *this, + envconfig([](std::unique_ptr cfg) { + cfg->START_UP = Config::FRESH; + return cfg; + }), + features); + XRPAmount const txnFees = + env.current()->fees().base * 10 + wasmHex.size() / 2 * 5; + // create escrow + env.fund(XRP(5000), alice, carol); + + auto escrowCreate = escrow::create(alice, carol, XRP(500)); + + // Success situations + { + // FinishFunction + CancelAfter + env(escrowCreate, + escrow::finish_function(wasmHex), + escrow::cancel_time(env.now() + 20s), + fee(txnFees)); + env.close(); + } + { + // FinishFunction + Condition + CancelAfter + env(escrowCreate, + escrow::finish_function(wasmHex), + escrow::cancel_time(env.now() + 30s), + escrow::condition(escrow::cb1), + fee(txnFees)); + env.close(); + } + { + // FinishFunction + FinishAfter + CancelAfter + env(escrowCreate, + escrow::finish_function(wasmHex), + escrow::cancel_time(env.now() + 40s), + escrow::finish_time(env.now() + 2s), + fee(txnFees)); + env.close(); + } + { + // FinishFunction + FinishAfter + Condition + CancelAfter + env(escrowCreate, + escrow::finish_function(wasmHex), + escrow::cancel_time(env.now() + 50s), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 2s), + fee(txnFees)); + env.close(); + } + + // Failure situations (i.e. all other combinations) + { + // only FinishFunction + env(escrowCreate, + escrow::finish_function(wasmHex), + fee(txnFees), + ter(temBAD_EXPIRATION)); + env.close(); + } + { + // FinishFunction + FinishAfter + env(escrowCreate, + escrow::finish_function(wasmHex), + escrow::finish_time(env.now() + 2s), + fee(txnFees), + ter(temBAD_EXPIRATION)); + env.close(); + } + { + // FinishFunction + Condition + env(escrowCreate, + escrow::finish_function(wasmHex), + escrow::condition(escrow::cb1), + fee(txnFees), + ter(temBAD_EXPIRATION)); + env.close(); + } + { + // FinishFunction + FinishAfter + Condition + env(escrowCreate, + escrow::finish_function(wasmHex), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 2s), + fee(txnFees), + ter(temBAD_EXPIRATION)); + env.close(); + } + { + // FinishFunction 0 length + env(escrowCreate, + escrow::finish_function(""), + escrow::cancel_time(env.now() + 60s), + fee(txnFees), + ter(temMALFORMED)); + env.close(); + } + { + // Not enough fees + env(escrowCreate, + escrow::finish_function(wasmHex), + escrow::cancel_time(env.now() + 70s), + fee(txnFees - 1), + ter(telINSUF_FEE_P)); + env.close(); + } + + { + // FinishFunction nonexistent host function + // pub fn finish() -> bool { + // unsafe { host_lib::bad() >= 5 } + // } + auto const badWasmHex = + "0061736d010000000105016000017f02100108686f73745f6c696203626164" + "00000302010005030100100611027f00418080c0000b7f00418080c0000b07" + "2e04066d656d6f727902000666696e69736800010a5f5f646174615f656e64" + "03000b5f5f686561705f6261736503010a09010700100041044a0b004d0970" + "726f64756365727302086c616e6775616765010452757374000c70726f6365" + "737365642d6279010572757374631d312e38352e3120283465623136313235" + "3020323032352d30332d31352900490f7461726765745f6665617475726573" + "042b0f6d757461626c652d676c6f62616c732b087369676e2d6578742b0f72" + "65666572656e63652d74797065732b0a6d756c746976616c7565"; + env(escrowCreate, + escrow::finish_function(badWasmHex), + escrow::cancel_time(env.now() + 100s), + fee(txnFees), + ter(temBAD_WASM)); + env.close(); + } + } + + void + testFinishWasmFailures(FeatureBitset features) + { + testcase("EscrowFinish Smart Escrow failures"); + + using namespace jtx; + using namespace std::chrono; + + Account const alice{"alice"}; + Account const carol{"carol"}; + + // Tests whether the ledger index is >= 5 + // getLedgerSqn() >= 5} + static auto wasmHex = ledgerSqnWasmHex; + + { + // featureSmartEscrow disabled + Env env(*this, features - featureSmartEscrow); + env.fund(XRP(5000), alice, carol); + XRPAmount const txnFees = + env.current()->fees().base * 10 + wasmHex.size() / 2 * 5; + env(escrow::finish(carol, alice, 1), + fee(txnFees), + escrow::comp_allowance(4), + ter(temDISABLED)); + env.close(); + } + + { + // ComputationAllowance > max compute limit + Env env( + *this, + envconfig([](std::unique_ptr cfg) { + cfg->FEES.extension_compute_limit = 1'000; // in gas + return cfg; + }), + features); + env.fund(XRP(5000), alice, carol); + // Run past the flag ledger so that a Fee change vote occurs and + // updates FeeSettings. (It also activates all supported + // amendments.) + for (auto i = env.current()->seq(); i <= 257; ++i) + env.close(); + + auto const allowance = 1'001; + env(escrow::finish(carol, alice, 1), + fee(env.current()->fees().base + allowance), + escrow::comp_allowance(allowance), + ter(temBAD_LIMIT)); + } + + Env env(*this, features); + + // Run past the flag ledger so that a Fee change vote occurs and + // updates FeeSettings. (It also activates all supported + // amendments.) + for (auto i = env.current()->seq(); i <= 257; ++i) + env.close(); + + XRPAmount const txnFees = + env.current()->fees().base * 10 + wasmHex.size() / 2 * 5; + env.fund(XRP(5000), alice, carol); + + // create escrow + auto const seq = env.seq(alice); + env(escrow::create(alice, carol, XRP(500)), + escrow::finish_function(wasmHex), + escrow::cancel_time(env.now() + 100s), + fee(txnFees)); + env.close(); + + { + // no ComputationAllowance field + env(escrow::finish(carol, alice, seq), + ter(tefWASM_FIELD_NOT_INCLUDED)); + } + + { + // ComputationAllowance value of 0 + env(escrow::finish(carol, alice, seq), + escrow::comp_allowance(0), + ter(temBAD_LIMIT)); + } + + { + // not enough fees + // This function takes 4 gas + // In testing, 1 gas costs 1 drop + auto const finishFee = env.current()->fees().base; + env(escrow::finish(carol, alice, seq), + fee(finishFee), + escrow::comp_allowance(4), + ter(telINSUF_FEE_P)); + } + + { + // not enough gas + // This function takes 4 gas + // In testing, 1 gas costs 1 drop + auto const finishFee = env.current()->fees().base + 4; + env(escrow::finish(carol, alice, seq), + fee(finishFee), + escrow::comp_allowance(2), + ter(tecFAILED_PROCESSING)); + } + + { + // ComputationAllowance field included w/no FinishFunction on + // escrow + auto const seq2 = env.seq(alice); + env(escrow::create(alice, carol, XRP(500)), + escrow::finish_time(env.now() + 10s), + escrow::cancel_time(env.now() + 100s)); + env.close(); + + auto const allowance = 100; + env(escrow::finish(carol, alice, seq2), + fee(env.current()->fees().base + + (allowance * env.current()->fees().gasPrice) / + MICRO_DROPS_PER_DROP + + 1), + escrow::comp_allowance(allowance), + ter(tefNO_WASM)); + } + } + + void + testFinishFunction(FeatureBitset features) + { + testcase("Example escrow function"); + + using namespace jtx; + using namespace std::chrono; + + Account const alice{"alice"}; + Account const carol{"carol"}; + + // Tests whether the ledger index is >= 5 + // getLedgerSqn() >= 5} + auto const& wasmHex = ledgerSqnWasmHex; + std::uint32_t const allowance = 5; + auto escrowCreate = escrow::create(alice, carol, XRP(1000)); + auto [createFee, finishFee] = [&]() { + Env env(*this, features); + auto createFee = + env.current()->fees().base * 10 + wasmHex.size() / 2 * 5; + auto finishFee = env.current()->fees().base + + (allowance * env.current()->fees().gasPrice) / + MICRO_DROPS_PER_DROP + + 1; + return std::make_pair(createFee, finishFee); + }(); + + { + // basic FinishFunction situation + Env env(*this, features); + // create escrow + env.fund(XRP(5000), alice, carol); + auto const seq = env.seq(alice); + BEAST_EXPECT(env.ownerCount(alice) == 0); + env(escrowCreate, + escrow::finish_function(wasmHex), + escrow::cancel_time(env.now() + 100s), + fee(createFee)); + env.close(); + + if (BEAST_EXPECT(env.ownerCount(alice) == 2)) + { + env.require(balance(alice, XRP(4000) - createFee)); + env.require(balance(carol, XRP(5000))); + + env(escrow::finish(carol, alice, seq), + escrow::comp_allowance(allowance), + fee(finishFee), + ter(tecWASM_REJECTED)); + env(escrow::finish(alice, alice, seq), + escrow::comp_allowance(allowance), + fee(finishFee), + ter(tecWASM_REJECTED)); + env(escrow::finish(alice, alice, seq), + escrow::comp_allowance(allowance), + fee(finishFee), + ter(tecWASM_REJECTED)); + env(escrow::finish(carol, alice, seq), + escrow::comp_allowance(allowance), + fee(finishFee), + ter(tecWASM_REJECTED)); + env(escrow::finish(carol, alice, seq), + escrow::comp_allowance(allowance), + fee(finishFee), + ter(tecWASM_REJECTED)); + env.close(); + + { + auto const txMeta = env.meta(); + if (BEAST_EXPECT(txMeta->isFieldPresent(sfGasUsed))) + BEAST_EXPECTS( + env.meta()->getFieldU32(sfGasUsed) == allowance, + std::to_string(env.meta()->getFieldU32(sfGasUsed))); + } + + env(escrow::finish(alice, alice, seq), + fee(finishFee), + escrow::comp_allowance(allowance), + ter(tesSUCCESS)); + + auto const txMeta = env.meta(); + if (BEAST_EXPECT(txMeta->isFieldPresent(sfGasUsed))) + BEAST_EXPECTS( + txMeta->getFieldU32(sfGasUsed) == allowance, + std::to_string(txMeta->getFieldU32(sfGasUsed))); + if (BEAST_EXPECT(txMeta->isFieldPresent(sfWasmReturnCode))) + BEAST_EXPECTS( + txMeta->getFieldI32(sfWasmReturnCode) == 5, + std::to_string(txMeta->getFieldI32(sfWasmReturnCode))); + + BEAST_EXPECT(env.ownerCount(alice) == 0); + } + } + + { + // FinishFunction + Condition + Env env(*this, features); + env.fund(XRP(5000), alice, carol); + BEAST_EXPECT(env.ownerCount(alice) == 0); + auto const seq = env.seq(alice); + // create escrow + env(escrowCreate, + escrow::finish_function(wasmHex), + escrow::condition(escrow::cb1), + escrow::cancel_time(env.now() + 100s), + fee(createFee)); + env.close(); + auto const conditionFinishFee = finishFee + + env.current()->fees().base * (32 + (escrow::fb1.size() / 16)); + + if (BEAST_EXPECT(env.ownerCount(alice) == 2)) + { + env.require(balance(alice, XRP(4000) - createFee)); + env.require(balance(carol, XRP(5000))); + + // no fulfillment provided, function fails + env(escrow::finish(carol, alice, seq), + escrow::comp_allowance(allowance), + fee(finishFee), + ter(tecCRYPTOCONDITION_ERROR)); + // fulfillment provided, function fails + env(escrow::finish(carol, alice, seq), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + escrow::comp_allowance(allowance), + fee(conditionFinishFee), + ter(tecWASM_REJECTED)); + if (BEAST_EXPECT(env.meta()->isFieldPresent(sfGasUsed))) + BEAST_EXPECTS( + env.meta()->getFieldU32(sfGasUsed) == allowance, + std::to_string(env.meta()->getFieldU32(sfGasUsed))); + env.close(); + // no fulfillment provided, function succeeds + env(escrow::finish(alice, alice, seq), + escrow::comp_allowance(allowance), + fee(conditionFinishFee), + ter(tecCRYPTOCONDITION_ERROR)); + // wrong fulfillment provided, function succeeds + env(escrow::finish(alice, alice, seq), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb2), + escrow::comp_allowance(allowance), + fee(conditionFinishFee), + ter(tecCRYPTOCONDITION_ERROR)); + // fulfillment provided, function succeeds, tx succeeds + env(escrow::finish(alice, alice, seq), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + escrow::comp_allowance(allowance), + fee(conditionFinishFee), + ter(tesSUCCESS)); + + auto const txMeta = env.meta(); + if (BEAST_EXPECT(txMeta->isFieldPresent(sfGasUsed))) + BEAST_EXPECT(txMeta->getFieldU32(sfGasUsed) == allowance); + if (BEAST_EXPECT(txMeta->isFieldPresent(sfWasmReturnCode))) + BEAST_EXPECTS( + txMeta->getFieldI32(sfWasmReturnCode) == 6, + std::to_string(txMeta->getFieldI32(sfWasmReturnCode))); + + env.close(); + BEAST_EXPECT(env.ownerCount(alice) == 0); + } + } + + { + // FinishFunction + FinishAfter + Env env(*this, features); + // create escrow + env.fund(XRP(5000), alice, carol); + auto const seq = env.seq(alice); + BEAST_EXPECT(env.ownerCount(alice) == 0); + auto const ts = env.now() + 97s; + env(escrowCreate, + escrow::finish_function(wasmHex), + escrow::finish_time(ts), + escrow::cancel_time(env.now() + 1000s), + fee(createFee)); + env.close(); + + if (BEAST_EXPECT(env.ownerCount(alice) == 2)) + { + env.require(balance(alice, XRP(4000) - createFee)); + env.require(balance(carol, XRP(5000))); + + // finish time hasn't passed, function fails + env(escrow::finish(carol, alice, seq), + escrow::comp_allowance(allowance), + fee(finishFee + 1), + ter(tecNO_PERMISSION)); + env.close(); + // finish time hasn't passed, function succeeds + for (; env.now() < ts; env.close()) + env(escrow::finish(carol, alice, seq), + escrow::comp_allowance(allowance), + fee(finishFee + 2), + ter(tecNO_PERMISSION)); + + env(escrow::finish(carol, alice, seq), + escrow::comp_allowance(allowance), + fee(finishFee + 1), + ter(tesSUCCESS)); + + auto const txMeta = env.meta(); + if (BEAST_EXPECT(txMeta->isFieldPresent(sfGasUsed))) + BEAST_EXPECT(txMeta->getFieldU32(sfGasUsed) == allowance); + if (BEAST_EXPECT(txMeta->isFieldPresent(sfWasmReturnCode))) + BEAST_EXPECTS( + txMeta->getFieldI32(sfWasmReturnCode) == 13, + std::to_string(txMeta->getFieldI32(sfWasmReturnCode))); + + BEAST_EXPECT(env.ownerCount(alice) == 0); + } + } + + { + // FinishFunction + FinishAfter #2 + Env env(*this, features); + // create escrow + env.fund(XRP(5000), alice, carol); + auto const seq = env.seq(alice); + BEAST_EXPECT(env.ownerCount(alice) == 0); + env(escrowCreate, + escrow::finish_function(wasmHex), + escrow::finish_time(env.now() + 2s), + escrow::cancel_time(env.now() + 100s), + fee(createFee)); + // Don't close the ledger here + + if (BEAST_EXPECT(env.ownerCount(alice) == 2)) + { + env.require(balance(alice, XRP(4000) - createFee)); + env.require(balance(carol, XRP(5000))); + + // finish time hasn't passed, function fails + env(escrow::finish(carol, alice, seq), + escrow::comp_allowance(allowance), + fee(finishFee), + ter(tecNO_PERMISSION)); + env.close(); + + // finish time has passed, function fails + env(escrow::finish(carol, alice, seq), + escrow::comp_allowance(allowance), + fee(finishFee), + ter(tecWASM_REJECTED)); + if (BEAST_EXPECT(env.meta()->isFieldPresent(sfGasUsed))) + BEAST_EXPECTS( + env.meta()->getFieldU32(sfGasUsed) == allowance, + std::to_string(env.meta()->getFieldU32(sfGasUsed))); + env.close(); + // finish time has passed, function succeeds, tx succeeds + env(escrow::finish(carol, alice, seq), + escrow::comp_allowance(allowance), + fee(finishFee), + ter(tesSUCCESS)); + + auto const txMeta = env.meta(); + if (BEAST_EXPECT(txMeta->isFieldPresent(sfGasUsed))) + BEAST_EXPECT(txMeta->getFieldU32(sfGasUsed) == allowance); + if (BEAST_EXPECT(txMeta->isFieldPresent(sfWasmReturnCode))) + BEAST_EXPECTS( + txMeta->getFieldI32(sfWasmReturnCode) == 6, + std::to_string(txMeta->getFieldI32(sfWasmReturnCode))); + + env.close(); + BEAST_EXPECT(env.ownerCount(alice) == 0); + } + } + } + + void + testUpdateDataOnFailure(FeatureBitset features) + { + testcase("Update escrow data on failure"); + + using namespace jtx; + using namespace std::chrono; + + // wasm that always fails + static auto const wasmHex = updateDataWasmHex; + + Account const alice{"alice"}; + Account const carol{"carol"}; + + Env env(*this, features); + // create escrow + env.fund(XRP(5000), alice); + auto const seq = env.seq(alice); + BEAST_EXPECT(env.ownerCount(alice) == 0); + auto escrowCreate = escrow::create(alice, alice, XRP(1000)); + XRPAmount txnFees = + env.current()->fees().base * 10 + wasmHex.size() / 2 * 5; + env(escrowCreate, + escrow::finish_function(wasmHex), + escrow::finish_time(env.now() + 2s), + escrow::cancel_time(env.now() + 100s), + fee(txnFees)); + env.close(); + env.close(); + env.close(); + + if (BEAST_EXPECT( + env.ownerCount(alice) == (1 + wasmHex.size() / 2 / 500))) + { + env.require(balance(alice, XRP(4000) - txnFees)); + + auto const allowance = 14; + XRPAmount const finishFee = env.current()->fees().base + + (allowance * env.current()->fees().gasPrice) / + MICRO_DROPS_PER_DROP + + 1; + + // FinishAfter time hasn't passed + env(escrow::finish(alice, alice, seq), + escrow::comp_allowance(allowance), + fee(finishFee), + ter(tecWASM_REJECTED)); + + auto const txMeta = env.meta(); + if (BEAST_EXPECT(txMeta && txMeta->isFieldPresent(sfGasUsed))) + BEAST_EXPECTS( + txMeta->getFieldU32(sfGasUsed) == allowance, + std::to_string(txMeta->getFieldU32(sfGasUsed))); + if (BEAST_EXPECT(txMeta->isFieldPresent(sfWasmReturnCode))) + BEAST_EXPECTS( + txMeta->getFieldI32(sfWasmReturnCode) == -256, + std::to_string(txMeta->getFieldI32(sfWasmReturnCode))); + + auto const sle = env.le(keylet::escrow(alice, seq)); + if (BEAST_EXPECT(sle && sle->isFieldPresent(sfData))) + BEAST_EXPECTS( + checkVL(sle, sfData, "Data"), + strHex(sle->getFieldVL(sfData))); + } + } + + void + testAllHostFunctions(FeatureBitset features) + { + testcase("Test all host functions"); + + using namespace jtx; + using namespace std::chrono; + + // TODO: create wasm module for all host functions + static auto wasmHex = allHostFunctionsWasmHex; + + Account const alice{"alice"}; + Account const carol{"carol"}; + + { + Env env(*this, features); + // create escrow + env.fund(XRP(5000), alice, carol); + auto const seq = env.seq(alice); + BEAST_EXPECT(env.ownerCount(alice) == 0); + auto escrowCreate = escrow::create(alice, carol, XRP(1000)); + XRPAmount txnFees = + env.current()->fees().base * 10 + wasmHex.size() / 2 * 5; + env(escrowCreate, + escrow::finish_function(wasmHex), + escrow::finish_time(env.now() + 11s), + escrow::cancel_time(env.now() + 100s), + escrow::data("1000000000"), // 1000 XRP in drops + fee(txnFees)); + env.close(); + + if (BEAST_EXPECT( + env.ownerCount(alice) == (1 + wasmHex.size() / 2 / 500))) + { + env.require(balance(alice, XRP(4000) - txnFees)); + env.require(balance(carol, XRP(5000))); + + auto const allowance = 1'000'000; + XRPAmount const finishFee = env.current()->fees().base + + (allowance * env.current()->fees().gasPrice) / + MICRO_DROPS_PER_DROP + + 1; + + // FinishAfter time hasn't passed + env(escrow::finish(carol, alice, seq), + escrow::comp_allowance(allowance), + fee(finishFee), + ter(tecNO_PERMISSION)); + env.close(); + env.close(); + env.close(); + + // reduce the destination balance + env(pay(carol, alice, XRP(4500))); + env.close(); + env.close(); + + env(escrow::finish(alice, alice, seq), + escrow::comp_allowance(allowance), + fee(finishFee), + ter(tesSUCCESS)); + + auto const txMeta = env.meta(); + if (BEAST_EXPECT(txMeta && txMeta->isFieldPresent(sfGasUsed))) + BEAST_EXPECTS( + txMeta->getFieldU32(sfGasUsed) == 794, + std::to_string(txMeta->getFieldU32(sfGasUsed))); + if (BEAST_EXPECT(txMeta->isFieldPresent(sfWasmReturnCode))) + BEAST_EXPECT(txMeta->getFieldI32(sfWasmReturnCode) == 1); + + env.close(); + BEAST_EXPECT(env.ownerCount(alice) == 0); + } + } + } + + void + testKeyletHostFunctions(FeatureBitset features) + { + testcase("Test all keylet host functions"); + + using namespace jtx; + using namespace std::chrono; + + // TODO: create wasm module for all host functions + static auto wasmHex = allKeyletsWasmHex; + + Account const alice{"alice"}; + Account const carol{"carol"}; + + { + Env env{*this}; + env.fund(XRP(10000), alice, carol); + + BEAST_EXPECT(env.seq(alice) == 4); + BEAST_EXPECT(env.ownerCount(alice) == 0); + + // base objects that need to be created first + auto const tokenId = + token::getNextID(env, alice, 0, tfTransferable); + env(token::mint(alice, 0u), txflags(tfTransferable)); + env(trust(alice, carol["USD"](1'000'000))); + env.close(); + BEAST_EXPECT(env.seq(alice) == 6); + BEAST_EXPECT(env.ownerCount(alice) == 2); + + // set up a bunch of objects to check their keylets + AMM amm(env, carol, XRP(10), carol["USD"](1000)); + env(check::create(alice, carol, XRP(100))); + env(credentials::create(alice, alice, "termsandconditions")); + env(delegate::set(alice, carol, {"TrustSet"})); + env(deposit::auth(alice, carol)); + env(did::set(alice), did::data("alice_did")); + env(escrow::create(alice, carol, XRP(100)), + escrow::finish_time(env.now() + 100s)); + MPTTester mptTester{env, alice, {.fund = false}}; + mptTester.create(); + mptTester.authorize({.account = carol}); + env(token::createOffer(carol, tokenId, XRP(100)), + token::owner(alice)); + env(offer(alice, carol["GBP"](0.1), XRP(100))); + env(paychan::create(alice, carol, XRP(1000), 100s, alice.pk())); + pdomain::Credentials credentials{{alice, "first credential"}}; + env(pdomain::setTx(alice, credentials)); + env(signers(alice, 1, {{carol, 1}})); + env(ticket::create(alice, 1)); + Vault vault{env}; + auto [tx, _keylet] = + vault.create({.owner = alice, .asset = xrpIssue()}); + env(tx); + env.close(); + + BEAST_EXPECTS( + env.ownerCount(alice) == 17, + std::to_string(env.ownerCount(alice))); + if (BEAST_EXPECTS( + env.seq(alice) == 20, std::to_string(env.seq(alice)))) + { + auto const seq = env.seq(alice); + XRPAmount txnFees = + env.current()->fees().base * 10 + wasmHex.size() / 2 * 5; + env(escrow::create(alice, carol, XRP(1000)), + escrow::finish_function(wasmHex), + escrow::finish_time(env.now() + 2s), + escrow::cancel_time(env.now() + 100s), + fee(txnFees)); + env.close(); + env.close(); + env.close(); + + auto const allowance = 2'985; + auto const finishFee = env.current()->fees().base + + (allowance * env.current()->fees().gasPrice) / + MICRO_DROPS_PER_DROP + + 1; + env(escrow::finish(carol, alice, seq), + escrow::comp_allowance(allowance), + fee(finishFee)); + env.close(); + + auto const txMeta = env.meta(); + if (BEAST_EXPECT(txMeta && txMeta->isFieldPresent(sfGasUsed))) + { + auto const gasUsed = txMeta->getFieldU32(sfGasUsed); + BEAST_EXPECTS( + gasUsed == allowance, std::to_string(gasUsed)); + } + BEAST_EXPECTS( + env.ownerCount(alice) == 17, + std::to_string(env.ownerCount(alice))); + } + } + } + + void + testWithFeats(FeatureBitset features) + { + testCreateFinishFunctionPreflight(features); + testFinishWasmFailures(features); + testFinishFunction(features); + testUpdateDataOnFailure(features); + + // TODO: Update module with new host functions + testAllHostFunctions(features); + testKeyletHostFunctions(features); + } + +public: + void + run() override + { + using namespace test::jtx; + FeatureBitset const all{testable_amendments()}; + testWithFeats(all); + } +}; + +BEAST_DEFINE_TESTSUITE(EscrowSmart, app, ripple); + +} // namespace test +} // namespace ripple diff --git a/src/test/app/Escrow_test.cpp b/src/test/app/Escrow_test.cpp index e3b2340022..67bcc61bc2 100644 --- a/src/test/app/Escrow_test.cpp +++ b/src/test/app/Escrow_test.cpp @@ -1507,7 +1507,7 @@ struct Escrow_test : public beast::unit_test::suite Account const alice{"alice"}; Account const bob{"bob"}; Account const carol{"carol"}; - Account const dillon{"dillon "}; + Account const dillon{"dillon"}; Account const zelda{"zelda"}; char const credType[] = "abcde"; @@ -1674,6 +1674,8 @@ public: FeatureBitset const all{testable_amendments()}; testWithFeats(all); testWithFeats(all - featureTokenEscrow); + testWithFeats(all - featureSmartEscrow); + testWithFeats(all - featureTokenEscrow - featureSmartEscrow); testTags(all - fixIncludeKeyletFields); } }; diff --git a/src/test/app/HostFuncImpl_test.cpp b/src/test/app/HostFuncImpl_test.cpp index d9f8d56025..c6402048a7 100644 --- a/src/test/app/HostFuncImpl_test.cpp +++ b/src/test/app/HostFuncImpl_test.cpp @@ -1,22 +1,3 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2025 Ripple Labs Inc. - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - #include #include diff --git a/src/test/app/NetworkOPs_test.cpp b/src/test/app/NetworkOPs_test.cpp index 3965778221..d83a2a3054 100644 --- a/src/test/app/NetworkOPs_test.cpp +++ b/src/test/app/NetworkOPs_test.cpp @@ -38,14 +38,14 @@ public: auto const jtx = env.jt(ticket::create(alice, 1), seq(1), fee(10)); - auto transacionId = jtx.stx->getTransactionID(); + auto transactionId = jtx.stx->getTransactionID(); env.app().getHashRouter().setFlags( - transacionId, HashRouterFlags::HELD); + transactionId, HashRouterFlags::HELD); env(jtx, json(jss::Sequence, 1), ter(terNO_ACCOUNT)); env.app().getHashRouter().setFlags( - transacionId, HashRouterFlags::BAD); + transactionId, HashRouterFlags::BAD); env.close(); } diff --git a/src/test/app/PseudoTx_test.cpp b/src/test/app/PseudoTx_test.cpp index ea53b0bee5..635e8e5739 100644 --- a/src/test/app/PseudoTx_test.cpp +++ b/src/test/app/PseudoTx_test.cpp @@ -33,6 +33,12 @@ struct PseudoTx_test : public beast::unit_test::suite obj[sfReserveIncrement] = 0; obj[sfReferenceFeeUnits] = 0; } + if (rules.enabled(featureSmartEscrow)) + { + obj[sfExtensionComputeLimit] = 0; + obj[sfExtensionSizeLimit] = 0; + obj[sfGasPrice] = 0; + } })); res.emplace_back(STTx(ttAMENDMENT, [&](auto& obj) { @@ -101,7 +107,9 @@ struct PseudoTx_test : public beast::unit_test::suite FeatureBitset const all{testable_amendments()}; FeatureBitset const xrpFees{featureXRPFees}; + testPrevented(all - featureXRPFees - featureSmartEscrow); testPrevented(all - featureXRPFees); + testPrevented(all - featureSmartEscrow); testPrevented(all); testAllowed(); } diff --git a/src/test/app/TestHostFunctions.h b/src/test/app/TestHostFunctions.h index d683bd7d59..fd59126943 100644 --- a/src/test/app/TestHostFunctions.h +++ b/src/test/app/TestHostFunctions.h @@ -1,22 +1,3 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2025 Ripple Labs Inc. - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - #include #include @@ -27,6 +8,7 @@ #include #include +#include namespace ripple { diff --git a/src/test/jtx/escrow.h b/src/test/jtx/escrow.h index d0d066c2d4..02f3ac8a93 100644 --- a/src/test/jtx/escrow.h +++ b/src/test/jtx/escrow.h @@ -85,6 +85,76 @@ auto const condition = JTxFieldWrapper(sfCondition); auto const fulfillment = JTxFieldWrapper(sfFulfillment); +struct finish_function +{ +private: + std::string value_; + +public: + explicit finish_function(std::string func) : value_(func) + { + } + + explicit finish_function(Slice const& func) : value_(strHex(func)) + { + } + + template + explicit finish_function(std::array const& f) + : finish_function(makeSlice(f)) + { + } + + void + operator()(Env&, JTx& jt) const + { + jt.jv[sfFinishFunction.jsonName] = value_; + } +}; + +struct data +{ +private: + std::string value_; + +public: + explicit data(std::string func) : value_(func) + { + } + + explicit data(Slice const& func) : value_(strHex(func)) + { + } + + template + explicit data(std::array const& f) : data(makeSlice(f)) + { + } + + void + operator()(Env&, JTx& jt) const + { + jt.jv[sfData.jsonName] = value_; + } +}; + +struct comp_allowance +{ +private: + std::uint32_t value_; + +public: + explicit comp_allowance(std::uint32_t const& value) : value_(value) + { + } + + void + operator()(Env&, JTx& jt) const + { + jt.jv[sfComputationAllowance.jsonName] = value_; + } +}; + } // namespace escrow } // namespace jtx diff --git a/src/test/jtx/impl/envconfig.cpp b/src/test/jtx/impl/envconfig.cpp index 8cf416a4c5..19f709d9b4 100644 --- a/src/test/jtx/impl/envconfig.cpp +++ b/src/test/jtx/impl/envconfig.cpp @@ -14,9 +14,14 @@ setupConfigForUnitTests(Config& cfg) using namespace jtx; // Default fees to old values, so tests don't have to worry about changes in // Config.h + // NOTE: For new `FEES` fields, you need to wait for the first flag ledger + // to close for the values to be activated. cfg.FEES.reference_fee = UNIT_TEST_REFERENCE_FEE; cfg.FEES.account_reserve = XRP(200).value().xrp().drops(); cfg.FEES.owner_reserve = XRP(50).value().xrp().drops(); + cfg.FEES.extension_compute_limit = 1'000'000; + cfg.FEES.extension_size_limit = 100'000; + cfg.FEES.gas_price = 1'000'000; // 1 drop = 1,000,000 micro-drops // The Beta API (currently v2) is always available to tests cfg.BETA_RPC_API = true; diff --git a/src/test/rpc/Subscribe_test.cpp b/src/test/rpc/Subscribe_test.cpp index 98e86484f5..b1d31198a3 100644 --- a/src/test/rpc/Subscribe_test.cpp +++ b/src/test/rpc/Subscribe_test.cpp @@ -515,6 +515,22 @@ public: if (jv.isMember(jss::reserve_inc) != isFlagLedger) return false; + if (env.closed()->rules().enabled(featureSmartEscrow)) + { + if (jv.isMember(jss::extension_compute) != isFlagLedger) + return false; + + if (jv.isMember(jss::extension_size) != isFlagLedger) + return false; + } + else + { + if (jv.isMember(jss::extension_compute)) + return false; + + if (jv.isMember(jss::extension_size)) + return false; + } return true; }; @@ -1579,7 +1595,8 @@ public: testTransactions_APIv1(); testTransactions_APIv2(); testManifests(); - testValidations(all - xrpFees); + testValidations(all - featureXRPFees - featureSmartEscrow); + testValidations(all - featureSmartEscrow); testValidations(all); testSubErrors(true); testSubErrors(false); diff --git a/src/xrpld/app/ledger/Ledger.cpp b/src/xrpld/app/ledger/Ledger.cpp index 6bf3170f18..9cfaf95d74 100644 --- a/src/xrpld/app/ledger/Ledger.cpp +++ b/src/xrpld/app/ledger/Ledger.cpp @@ -202,6 +202,15 @@ Ledger::Ledger( sle->at(sfReserveIncrement) = *f; sle->at(sfReferenceFeeUnits) = Config::FEE_UNITS_DEPRECATED; } + if (std::find( + amendments.begin(), amendments.end(), featureSmartEscrow) != + amendments.end()) + { + sle->at(sfExtensionComputeLimit) = + config.FEES.extension_compute_limit; + sle->at(sfExtensionSizeLimit) = config.FEES.extension_size_limit; + sle->at(sfGasPrice) = config.FEES.gas_price; + } rawInsert(sle); } @@ -595,6 +604,7 @@ Ledger::setup() { bool oldFees = false; bool newFees = false; + bool extensionFees = false; { auto const baseFee = sle->at(~sfBaseFee); auto const reserveBase = sle->at(~sfReserveBase); @@ -612,6 +622,7 @@ Ledger::setup() auto const reserveBaseXRP = sle->at(~sfReserveBaseDrops); auto const reserveIncrementXRP = sle->at(~sfReserveIncrementDrops); + auto assign = [&ret]( XRPAmount& dest, std::optional const& src) { @@ -628,12 +639,35 @@ Ledger::setup() assign(fees_.increment, reserveIncrementXRP); newFees = baseFeeXRP || reserveBaseXRP || reserveIncrementXRP; } + { + auto const extensionComputeLimit = + sle->at(~sfExtensionComputeLimit); + auto const extensionSizeLimit = sle->at(~sfExtensionSizeLimit); + auto const gasPrice = sle->at(~sfGasPrice); + + auto assign = [](std::uint32_t& dest, + std::optional const& src) { + if (src) + { + dest = src.value(); + } + }; + assign(fees_.extensionComputeLimit, extensionComputeLimit); + assign(fees_.extensionSizeLimit, extensionSizeLimit); + assign(fees_.gasPrice, gasPrice); + extensionFees = + extensionComputeLimit || extensionSizeLimit || gasPrice; + } if (oldFees && newFees) // Should be all of one or the other, but not both ret = false; if (!rules_.enabled(featureXRPFees) && newFees) // Can't populate the new fees before the amendment is enabled ret = false; + if (!rules_.enabled(featureSmartEscrow) && extensionFees) + // Can't populate the extension fees before the amendment is + // enabled + ret = false; } } catch (SHAMapMissingNode const&) @@ -652,15 +686,23 @@ Ledger::setup() void Ledger::defaultFees(Config const& config) { - XRPL_ASSERT( - fees_.base == 0 && fees_.reserve == 0 && fees_.increment == 0, - "ripple::Ledger::defaultFees : zero fees"); + assert( + fees_.base == 0 && fees_.reserve == 0 && fees_.increment == 0 && + fees_.extensionComputeLimit == 0 && fees_.extensionSizeLimit == 0 && + fees_.gasPrice == 0); if (fees_.base == 0) fees_.base = config.FEES.reference_fee; if (fees_.reserve == 0) fees_.reserve = config.FEES.account_reserve; if (fees_.increment == 0) fees_.increment = config.FEES.owner_reserve; + + if (fees_.extensionComputeLimit == 0) + fees_.extensionComputeLimit = config.FEES.extension_compute_limit; + if (fees_.extensionSizeLimit == 0) + fees_.extensionSizeLimit = config.FEES.extension_size_limit; + if (fees_.gasPrice == 0) + fees_.gasPrice = config.FEES.gas_price; } std::shared_ptr diff --git a/src/xrpld/app/misc/NetworkOPs.cpp b/src/xrpld/app/misc/NetworkOPs.cpp index 157de028e5..c9a32b51ac 100644 --- a/src/xrpld/app/misc/NetworkOPs.cpp +++ b/src/xrpld/app/misc/NetworkOPs.cpp @@ -2465,6 +2465,18 @@ NetworkOPsImp::pubValidation(std::shared_ptr const& val) reserveIncXRP && reserveIncXRP->native()) jvObj[jss::reserve_inc] = reserveIncXRP->xrp().jsonClipped(); + if (auto const extensionComputeLimit = + ~val->at(~sfExtensionComputeLimit); + extensionComputeLimit) + jvObj[jss::extension_compute] = *extensionComputeLimit; + + if (auto const extensionSizeLimit = ~val->at(~sfExtensionSizeLimit); + extensionSizeLimit) + jvObj[jss::extension_size] = *extensionSizeLimit; + + if (auto const gasPrice = ~val->at(~sfGasPrice); gasPrice) + jvObj[jss::gas_price] = *gasPrice; + // NOTE Use MultiApiJson to publish two slightly different JSON objects // for consumers supporting different API versions MultiApiJson multiObj{jvObj}; @@ -2914,11 +2926,20 @@ NetworkOPsImp::getServerInfo(bool human, bool admin, bool counters) l[jss::seq] = Json::UInt(lpClosed->info().seq); l[jss::hash] = to_string(lpClosed->info().hash); + bool const smartEscrowEnabled = + lpClosed->rules().enabled(featureSmartEscrow); if (!human) { l[jss::base_fee] = baseFee.jsonClipped(); l[jss::reserve_base] = lpClosed->fees().reserve.jsonClipped(); l[jss::reserve_inc] = lpClosed->fees().increment.jsonClipped(); + if (smartEscrowEnabled) + { + l[jss::extension_compute] = + lpClosed->fees().extensionComputeLimit; + l[jss::extension_size] = lpClosed->fees().extensionSizeLimit; + l[jss::gas_price] = lpClosed->fees().gasPrice; + } l[jss::close_time] = Json::Value::UInt( lpClosed->info().closeTime.time_since_epoch().count()); } @@ -2927,6 +2948,13 @@ NetworkOPsImp::getServerInfo(bool human, bool admin, bool counters) l[jss::base_fee_xrp] = baseFee.decimalXRP(); l[jss::reserve_base_xrp] = lpClosed->fees().reserve.decimalXRP(); l[jss::reserve_inc_xrp] = lpClosed->fees().increment.decimalXRP(); + if (smartEscrowEnabled) + { + l[jss::extension_compute] = + lpClosed->fees().extensionComputeLimit; + l[jss::extension_size] = lpClosed->fees().extensionSizeLimit; + l[jss::gas_price] = lpClosed->fees().gasPrice; + } if (auto const closeOffset = app_.timeKeeper().closeOffset(); std::abs(closeOffset.count()) >= 60) @@ -3120,6 +3148,14 @@ NetworkOPsImp::pubLedger(std::shared_ptr const& lpAccepted) jvObj[jss::reserve_base] = lpAccepted->fees().reserve.jsonClipped(); jvObj[jss::reserve_inc] = lpAccepted->fees().increment.jsonClipped(); + if (lpAccepted->rules().enabled(featureSmartEscrow)) + { + jvObj[jss::extension_compute] = + lpAccepted->fees().extensionComputeLimit; + jvObj[jss::extension_size] = + lpAccepted->fees().extensionSizeLimit; + jvObj[jss::gas_price] = lpAccepted->fees().gasPrice; + } jvObj[jss::txn_count] = Json::UInt(alpAccepted->size()); @@ -3490,8 +3526,8 @@ NetworkOPsImp::pubAccountTransaction( } JLOG(m_journal.trace()) - << "pubAccountTransaction: " - << "proposed=" << iProposed << ", accepted=" << iAccepted; + << "pubAccountTransaction: " << "proposed=" << iProposed + << ", accepted=" << iAccepted; if (!notify.empty() || !accountHistoryNotify.empty()) { @@ -4192,12 +4228,19 @@ NetworkOPsImp::subLedger(InfoSub::ref isrListener, Json::Value& jvResult) jvResult[jss::ledger_hash] = to_string(lpClosed->info().hash); jvResult[jss::ledger_time] = Json::Value::UInt( lpClosed->info().closeTime.time_since_epoch().count()); + jvResult[jss::network_id] = app_.config().NETWORK_ID; if (!lpClosed->rules().enabled(featureXRPFees)) jvResult[jss::fee_ref] = Config::FEE_UNITS_DEPRECATED; jvResult[jss::fee_base] = lpClosed->fees().base.jsonClipped(); jvResult[jss::reserve_base] = lpClosed->fees().reserve.jsonClipped(); jvResult[jss::reserve_inc] = lpClosed->fees().increment.jsonClipped(); - jvResult[jss::network_id] = app_.config().NETWORK_ID; + if (lpClosed->rules().enabled(featureSmartEscrow)) + { + jvResult[jss::extension_compute] = + lpClosed->fees().extensionComputeLimit; + jvResult[jss::extension_size] = lpClosed->fees().extensionSizeLimit; + jvResult[jss::gas_price] = lpClosed->fees().gasPrice; + } } if ((mMode >= OperatingMode::SYNCING) && !isNeedNetworkLedger()) diff --git a/src/xrpld/app/tx/detail/ApplyContext.cpp b/src/xrpld/app/tx/detail/ApplyContext.cpp index 4a7f72e2e3..cc33ae93b8 100644 --- a/src/xrpld/app/tx/detail/ApplyContext.cpp +++ b/src/xrpld/app/tx/detail/ApplyContext.cpp @@ -40,6 +40,11 @@ ApplyContext::discard() std::optional ApplyContext::apply(TER ter) { + if (wasmReturnCode_.has_value()) + { + view_->setWasmReturnCode(*wasmReturnCode_); + } + view_->setGasUsed(gasUsed_); return view_->apply( base_, tx, ter, parentBatchId_, flags_ & tapDRY_RUN, journal); } diff --git a/src/xrpld/app/tx/detail/ApplyContext.h b/src/xrpld/app/tx/detail/ApplyContext.h index e045189146..e52a02a73c 100644 --- a/src/xrpld/app/tx/detail/ApplyContext.h +++ b/src/xrpld/app/tx/detail/ApplyContext.h @@ -87,6 +87,20 @@ public: view_->deliver(amount); } + /** Sets the gas used in the metadata */ + void + setGasUsed(std::uint32_t const gasUsed) + { + gasUsed_ = gasUsed; + } + + /** Sets the gas used in the metadata */ + void + setWasmReturnCode(std::int32_t const wasmReturnCode) + { + wasmReturnCode_ = wasmReturnCode; + } + /** Discard changes and start fresh. */ void discard(); @@ -138,6 +152,8 @@ private: // The ID of the batch transaction we are executing under, if seated. std::optional parentBatchId_; + std::optional gasUsed_; + std::optional wasmReturnCode_; }; } // namespace ripple diff --git a/src/xrpld/app/tx/detail/Escrow.cpp b/src/xrpld/app/tx/detail/Escrow.cpp index c22b8145c6..5dd7b486c3 100644 --- a/src/xrpld/app/tx/detail/Escrow.cpp +++ b/src/xrpld/app/tx/detail/Escrow.cpp @@ -1,6 +1,8 @@ #include #include #include +#include +#include #include #include @@ -99,6 +101,30 @@ escrowCreatePreflightHelper(PreflightContext const& ctx) return tesSUCCESS; } +XRPAmount +EscrowCreate::calculateBaseFee(ReadView const& view, STTx const& tx) +{ + XRPAmount txnFees{Transactor::calculateBaseFee(view, tx)}; + if (tx.isFieldPresent(sfFinishFunction)) + { + // 10 base fees for the transaction (1 is in + // `Transactor::calculateBaseFee`), plus 5 drops per byte + txnFees += 9 * view.fees().base + 5 * tx[sfFinishFunction].size(); + } + return txnFees; +} + +bool +EscrowCreate::checkExtraFeatures(PreflightContext const& ctx) +{ + if ((ctx.tx.isFieldPresent(sfFinishFunction) || + ctx.tx.isFieldPresent(sfData)) && + !ctx.rules.enabled(featureSmartEscrow)) + return false; + + return true; +} + NotTEC EscrowCreate::preflight(PreflightContext const& ctx) { @@ -132,12 +158,21 @@ EscrowCreate::preflight(PreflightContext const& ctx) ctx.tx[sfCancelAfter] <= ctx.tx[sfFinishAfter]) return temBAD_EXPIRATION; + if (ctx.tx.isFieldPresent(sfFinishFunction) && + !ctx.tx.isFieldPresent(sfCancelAfter)) + return temBAD_EXPIRATION; + // In the absence of a FinishAfter, the escrow can be finished // immediately, which can be confusing. When creating an escrow, // we want to ensure that either a FinishAfter time is explicitly // specified or a completion condition is attached. - if (!ctx.tx[~sfFinishAfter] && !ctx.tx[~sfCondition]) + if (!ctx.tx[~sfFinishAfter] && !ctx.tx[~sfCondition] && + !ctx.tx[~sfFinishFunction]) + { + JLOG(ctx.j.debug()) << "Must have at least one of FinishAfter, " + "Condition, or FinishFunction."; return temMALFORMED; + } if (auto const cb = ctx.tx[~sfCondition]) { @@ -155,6 +190,43 @@ EscrowCreate::preflight(PreflightContext const& ctx) } } + if (ctx.tx.isFieldPresent(sfData)) + { + if (!ctx.tx.isFieldPresent(sfFinishFunction)) + { + JLOG(ctx.j.debug()) + << "EscrowCreate with Data requires FinishFunction"; + return temMALFORMED; + } + auto const data = ctx.tx.getFieldVL(sfData); + if (data.size() > maxWasmDataLength) + { + JLOG(ctx.j.debug()) << "EscrowCreate.Data bad size " << data.size(); + return temMALFORMED; + } + } + + if (ctx.tx.isFieldPresent(sfFinishFunction)) + { + auto const code = ctx.tx.getFieldVL(sfFinishFunction); + if (code.size() == 0 || + code.size() > ctx.app.config().FEES.extension_size_limit) + { + JLOG(ctx.j.debug()) + << "EscrowCreate.FinishFunction bad size " << code.size(); + return temMALFORMED; + } + + HostFunctions mock; + auto const re = + preflightEscrowWasm(code, ESCROW_FUNCTION_NAME, {}, &mock, ctx.j); + if (!isTesSuccess(re)) + { + JLOG(ctx.j.debug()) << "EscrowCreate.FinishFunction bad WASM"; + return re; + } + } + return tesSUCCESS; } @@ -413,6 +485,17 @@ escrowLockApplyHelper( return tesSUCCESS; } +template +static uint32_t +calculateAdditionalReserve(T const& finishFunction) +{ + if (!finishFunction) + return 1; + // First 500 bytes included in the normal reserve + // Each additional 500 bytes requires an additional reserve + return 1 + (finishFunction->size() / 500); +} + TER EscrowCreate::doApply() { @@ -430,9 +513,11 @@ EscrowCreate::doApply() // Check reserve and funds availability STAmount const amount{ctx_.tx[sfAmount]}; + auto const reserveToAdd = + calculateAdditionalReserve(ctx_.tx[~sfFinishFunction]); auto const reserve = - ctx_.view().fees().accountReserve((*sle)[sfOwnerCount] + 1); + ctx_.view().fees().accountReserve((*sle)[sfOwnerCount] + reserveToAdd); if (mSourceBalance < reserve) return tecINSUFFICIENT_RESERVE; @@ -467,6 +552,8 @@ EscrowCreate::doApply() (*slep)[~sfCancelAfter] = ctx_.tx[~sfCancelAfter]; (*slep)[~sfFinishAfter] = ctx_.tx[~sfFinishAfter]; (*slep)[~sfDestinationTag] = ctx_.tx[~sfDestinationTag]; + (*slep)[~sfFinishFunction] = ctx_.tx[~sfFinishFunction]; + (*slep)[~sfData] = ctx_.tx[~sfData]; if (ctx_.view().rules().enabled(fixIncludeKeyletFields)) { @@ -536,7 +623,7 @@ EscrowCreate::doApply() } // increment owner count - adjustOwnerCount(ctx_.view(), sle, 1, ctx_.journal); + adjustOwnerCount(ctx_.view(), sle, reserveToAdd, ctx_.journal); ctx_.view().update(sle); return tesSUCCESS; } @@ -564,8 +651,16 @@ checkCondition(Slice f, Slice c) bool EscrowFinish::checkExtraFeatures(PreflightContext const& ctx) { - return !ctx.tx.isFieldPresent(sfCredentialIDs) || - ctx.rules.enabled(featureCredentials); + if (ctx.tx.isFieldPresent(sfCredentialIDs) && + !ctx.rules.enabled(featureCredentials)) + return false; + + if (ctx.tx.isFieldPresent(sfComputationAllowance) && + !ctx.rules.enabled(featureSmartEscrow)) + { + return false; + } + return true; } NotTEC @@ -577,7 +672,10 @@ EscrowFinish::preflight(PreflightContext const& ctx) // If you specify a condition, then you must also specify // a fulfillment. if (static_cast(cb) != static_cast(fb)) + { + JLOG(ctx.j.debug()) << "Condition != Fulfillment"; return temMALFORMED; + } return tesSUCCESS; } @@ -607,6 +705,20 @@ EscrowFinish::preflightSigValidated(PreflightContext const& ctx) } } + if (auto const allowance = ctx.tx[~sfComputationAllowance]; allowance) + { + if (*allowance == 0) + { + return temBAD_LIMIT; + } + if (*allowance > ctx.app.config().FEES.extension_compute_limit) + { + JLOG(ctx.j.debug()) + << "ComputationAllowance too large: " << *allowance; + return temBAD_LIMIT; + } + } + if (auto const err = credentials::checkFields(ctx.tx, ctx.j); !isTesSuccess(err)) return err; @@ -623,7 +735,14 @@ EscrowFinish::calculateBaseFee(ReadView const& view, STTx const& tx) { extraFee += view.fees().base * (32 + (fb->size() / 16)); } - + if (auto const allowance = tx[~sfComputationAllowance]; allowance) + { + // The extra fee is the allowance in drops, rounded up to the nearest + // whole drop. + // Integer math rounds down by default, so we add 1 to round up. + extraFee += + ((*allowance) * view.fees().gasPrice) / MICRO_DROPS_PER_DROP + 1; + } return Transactor::calculateBaseFee(view, tx) + extraFee; } @@ -703,25 +822,52 @@ EscrowFinish::preclaim(PreclaimContext const& ctx) return err; } - if (ctx.view.rules().enabled(featureTokenEscrow)) + if (ctx.view.rules().enabled(featureTokenEscrow) || + ctx.view.rules().enabled(featureSmartEscrow)) { + // this check is done in doApply before this amendment is enabled auto const k = keylet::escrow(ctx.tx[sfOwner], ctx.tx[sfOfferSequence]); auto const slep = ctx.view.read(k); if (!slep) return tecNO_TARGET; - AccountID const dest = (*slep)[sfDestination]; - STAmount const amount = (*slep)[sfAmount]; - - if (!isXRP(amount)) + if (ctx.view.rules().enabled(featureSmartEscrow)) { - if (auto const ret = std::visit( - [&](T const&) { - return escrowFinishPreclaimHelper(ctx, dest, amount); - }, - amount.asset().value()); - !isTesSuccess(ret)) - return ret; + if (slep->isFieldPresent(sfFinishFunction)) + { + if (!ctx.tx.isFieldPresent(sfComputationAllowance)) + { + JLOG(ctx.j.debug()) + << "FinishFunction requires ComputationAllowance"; + return tefWASM_FIELD_NOT_INCLUDED; + } + } + else + { + if (ctx.tx.isFieldPresent(sfComputationAllowance)) + { + JLOG(ctx.j.debug()) << "FinishFunction not present, " + "ComputationAllowance present"; + return tefNO_WASM; + } + } + } + if (ctx.view.rules().enabled(featureTokenEscrow)) + { + AccountID const dest = (*slep)[sfDestination]; + STAmount const amount = (*slep)[sfAmount]; + + if (!isXRP(amount)) + { + if (auto const ret = std::visit( + [&](T const&) { + return escrowFinishPreclaimHelper( + ctx, dest, amount); + }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + } } } return tesSUCCESS; @@ -956,7 +1102,8 @@ EscrowFinish::doApply() auto const slep = ctx_.view().peek(k); if (!slep) { - if (ctx_.view().rules().enabled(featureTokenEscrow)) + if (ctx_.view().rules().enabled(featureTokenEscrow) || + ctx_.view().rules().enabled(featureSmartEscrow)) return tecINTERNAL; // LCOV_EXCL_LINE return tecNO_TARGET; @@ -974,6 +1121,20 @@ EscrowFinish::doApply() if ((*slep)[~sfCancelAfter] && after(now, (*slep)[sfCancelAfter])) return tecNO_PERMISSION; + AccountID const destID = (*slep)[sfDestination]; + auto const sled = ctx_.view().peek(keylet::account(destID)); + if (ctx_.view().rules().enabled(featureSmartEscrow)) + { + // NOTE: Escrow payments cannot be used to fund accounts. + if (!sled) + return tecNO_DST; + + if (auto err = verifyDepositPreauth( + ctx_.tx, ctx_.view(), account_, destID, sled, ctx_.journal); + !isTesSuccess(err)) + return err; + } + // Check cryptocondition fulfillment { auto const id = ctx_.tx.getTransactionID(); @@ -1023,16 +1184,69 @@ EscrowFinish::doApply() return tecCRYPTOCONDITION_ERROR; } - // NOTE: Escrow payments cannot be used to fund accounts. - AccountID const destID = (*slep)[sfDestination]; - auto const sled = ctx_.view().peek(keylet::account(destID)); - if (!sled) - return tecNO_DST; + if (!ctx_.view().rules().enabled(featureSmartEscrow)) + { + // NOTE: Escrow payments cannot be used to fund accounts. + if (!sled) + return tecNO_DST; - if (auto err = verifyDepositPreauth( - ctx_.tx, ctx_.view(), account_, destID, sled, ctx_.journal); - !isTesSuccess(err)) - return err; + if (auto err = verifyDepositPreauth( + ctx_.tx, ctx_.view(), account_, destID, sled, ctx_.journal); + !isTesSuccess(err)) + return err; + } + + // Execute custom release function + if ((*slep)[~sfFinishFunction]) + { + JLOG(j_.trace()) + << "The escrow has a finish function, running WASM code..."; + // WASM execution + auto const wasmStr = slep->getFieldVL(sfFinishFunction); + std::vector wasm(wasmStr.begin(), wasmStr.end()); + + WasmHostFunctionsImpl ledgerDataProvider(ctx_, k); + + if (!ctx_.tx.isFieldPresent(sfComputationAllowance)) + { + // already checked above, this check is just in case + return tecINTERNAL; + } + std::uint32_t allowance = ctx_.tx[sfComputationAllowance]; + auto re = runEscrowWasm( + wasm, ESCROW_FUNCTION_NAME, {}, &ledgerDataProvider, allowance); + JLOG(j_.trace()) << "Escrow WASM ran"; + + if (auto const& data = ledgerDataProvider.getData(); data.has_value()) + { + slep->setFieldVL(sfData, makeSlice(*data)); + ctx_.view().update(slep); + } + + if (re.has_value()) + { + auto reValue = re.value().result; + auto reCost = re.value().cost; + JLOG(j_.debug()) << "WASM Success: " + std::to_string(reValue) + << ", cost: " << reCost; + + ctx_.setWasmReturnCode(reValue); + + if (reCost < 0 || reCost > std::numeric_limits::max()) + return tecINTERNAL; // LCOV_EXCL_LINE + ctx_.setGasUsed(static_cast(reCost)); + + if (reValue <= 0) + { + return tecWASM_REJECTED; + } + } + else + { + JLOG(j_.debug()) << "WASM Failure: " + transHuman(re.error()); + return re.error(); + } + } AccountID const account = (*slep)[sfAccount]; @@ -1110,9 +1324,12 @@ EscrowFinish::doApply() ctx_.view().update(sled); + auto const reserveToSubtract = + calculateAdditionalReserve((*slep)[~sfFinishFunction]); + // Adjust source owner count auto const sle = ctx_.view().peek(keylet::account(account)); - adjustOwnerCount(ctx_.view(), sle, -1, ctx_.journal); + adjustOwnerCount(ctx_.view(), sle, -1 * reserveToSubtract, ctx_.journal); ctx_.view().update(sle); // Remove escrow from ledger @@ -1312,7 +1529,9 @@ EscrowCancel::doApply() } } - adjustOwnerCount(ctx_.view(), sle, -1, ctx_.journal); + auto const reserveToSubtract = + calculateAdditionalReserve((*slep)[~sfFinishFunction]); + adjustOwnerCount(ctx_.view(), sle, -1 * reserveToSubtract, ctx_.journal); ctx_.view().update(sle); // Remove escrow from ledger diff --git a/src/xrpld/app/tx/detail/Escrow.h b/src/xrpld/app/tx/detail/Escrow.h index d2821bc45d..1da745c165 100644 --- a/src/xrpld/app/tx/detail/Escrow.h +++ b/src/xrpld/app/tx/detail/Escrow.h @@ -14,9 +14,15 @@ public: { } + static bool + checkExtraFeatures(PreflightContext const& ctx); + static TxConsequences makeTxConsequences(PreflightContext const& ctx); + static XRPAmount + calculateBaseFee(ReadView const& view, STTx const& tx); + static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index 2ddef72c39..6e0abe988a 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -1032,6 +1032,22 @@ removeExpiredCredentials( } } +static void +modifyWasmDataFields( + ApplyView& view, + std::vector> const& wasmObjects, + beast::Journal viewJ) +{ + for (auto const& [index, data] : wasmObjects) + { + if (auto const sle = view.peek(keylet::escrow(index))) + { + sle->setFieldVL(sfData, data); + view.update(sle); + } + } +} + static void removeDeletedTrustLines( ApplyView& view, @@ -1190,6 +1206,7 @@ Transactor::operator()() else if ( (result == tecOVERSIZE) || (result == tecKILLED) || (result == tecINCOMPLETE) || (result == tecEXPIRED) || + (result == tecWASM_REJECTED) || (isTecClaimHardFail(result, view().flags()))) { JLOG(j_.trace()) << "reapplying because of " << transToken(result); @@ -1202,13 +1219,16 @@ Transactor::operator()() std::vector removedTrustLines; std::vector expiredNFTokenOffers; std::vector expiredCredentials; + std::vector> modifiedWasmObjects; bool const doOffers = ((result == tecOVERSIZE) || (result == tecKILLED)); bool const doLines = (result == tecINCOMPLETE); bool const doNFTokenOffers = (result == tecEXPIRED); bool const doCredentials = (result == tecEXPIRED); - if (doOffers || doLines || doNFTokenOffers || doCredentials) + bool const doWasmData = (result == tecWASM_REJECTED); + if (doOffers || doLines || doNFTokenOffers || doCredentials || + doWasmData) { ctx_.visit([doOffers, &removedOffers, @@ -1217,7 +1237,9 @@ Transactor::operator()() doNFTokenOffers, &expiredNFTokenOffers, doCredentials, - &expiredCredentials]( + &expiredCredentials, + doWasmData, + &modifiedWasmObjects]( uint256 const& index, bool isDelete, std::shared_ptr const& before, @@ -1252,6 +1274,13 @@ Transactor::operator()() (before->getType() == ltCREDENTIAL)) expiredCredentials.push_back(index); } + + if (doWasmData && before && after && + (before->getType() == ltESCROW)) + { + modifiedWasmObjects.push_back( + std::make_pair(index, after->getFieldVL(sfData))); + } }); } @@ -1281,6 +1310,10 @@ Transactor::operator()() removeExpiredCredentials( view(), expiredCredentials, ctx_.app.journal("View")); + if (result == tecWASM_REJECTED) + modifyWasmDataFields( + view(), modifiedWasmObjects, ctx_.app.journal("View")); + applied = isTecClaim(result); } diff --git a/src/xrpld/core/Config.h b/src/xrpld/core/Config.h index f48f2765e3..08c0425f72 100644 --- a/src/xrpld/core/Config.h +++ b/src/xrpld/core/Config.h @@ -54,6 +54,15 @@ struct FeeSetup /** The per-owned item reserve requirement in drops. */ XRPAmount owner_reserve{2 * DROPS_PER_XRP}; + /** The compute limit for Feature Extensions. */ + std::uint32_t extension_compute_limit{1'000'000}; + + /** The WASM size limit for Feature Extensions. */ + std::uint32_t extension_size_limit{100'000}; + + /** The price of 1 WASM gas, in micro-drops. */ + std::uint32_t gas_price{1'000'000}; + /* (Remember to update the example cfg files when changing any of these * values.) */ }; diff --git a/src/xrpld/core/detail/Config.cpp b/src/xrpld/core/detail/Config.cpp index 6a49416a45..11b73bce11 100644 --- a/src/xrpld/core/detail/Config.cpp +++ b/src/xrpld/core/detail/Config.cpp @@ -1122,6 +1122,12 @@ setup_FeeVote(Section const& section) setup.account_reserve = temp; if (set(temp, "owner_reserve", section)) setup.owner_reserve = temp; + if (set(temp, "extension_compute_limit", section)) + setup.extension_compute_limit = temp; + if (set(temp, "extension_size_limit", section)) + setup.extension_size_limit = temp; + if (set(temp, "gas_price", section)) + setup.gas_price = temp; } return setup; }