From 817f9c4f8c17b2736221e0dd3479d0148b3fbcbb Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Thu, 18 Sep 2025 15:59:05 -0400 Subject: [PATCH] move changes over --- include/xrpl/ledger/ApplyViewImpl.h | 19 +- include/xrpl/ledger/detail/ApplyStateTable.h | 173 ++-- include/xrpl/protocol/Fees.h | 6 + include/xrpl/protocol/Indexes.h | 8 +- include/xrpl/protocol/Protocol.h | 1 + include/xrpl/protocol/TER.h | 3 + include/xrpl/protocol/TxMeta.h | 57 +- include/xrpl/protocol/detail/features.macro | 5 +- .../xrpl/protocol/detail/ledger_entries.macro | 10 +- include/xrpl/protocol/detail/sfields.macro | 18 +- .../xrpl/protocol/detail/transactions.macro | 246 +----- src/libxrpl/ledger/ApplyStateTable.cpp | 739 ++--------------- src/libxrpl/ledger/ApplyViewImpl.cpp | 117 ++- src/libxrpl/protocol/TER.cpp | 3 + src/libxrpl/protocol/TxMeta.cpp | 22 +- src/test/app/Escrow_test.cpp | 783 +++++++++++++++++- src/test/app/TestHostFunctions.h | 3 +- src/test/app/Wasm_test.cpp | 4 +- src/test/jtx/escrow.h | 151 +++- src/test/jtx/impl/envconfig.cpp | 5 + src/xrpld/app/tx/detail/ApplyContext.cpp | 5 + src/xrpld/app/tx/detail/ApplyContext.h | 18 +- src/xrpld/app/tx/detail/Escrow.cpp | 293 +++++-- src/xrpld/app/tx/detail/Escrow.h | 3 + src/xrpld/app/tx/detail/Transactor.cpp | 46 +- 25 files changed, 1583 insertions(+), 1155 deletions(-) diff --git a/include/xrpl/ledger/ApplyViewImpl.h b/include/xrpl/ledger/ApplyViewImpl.h index eadcd8acb5..e6eb4a683d 100644 --- a/include/xrpl/ledger/ApplyViewImpl.h +++ b/include/xrpl/ledger/ApplyViewImpl.h @@ -20,8 +20,9 @@ #ifndef RIPPLE_LEDGER_APPLYVIEWIMPL_H_INCLUDED #define RIPPLE_LEDGER_APPLYVIEWIMPL_H_INCLUDED -#include -#include +#include +#include + #include #include @@ -74,6 +75,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 @@ -92,6 +105,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 01ab1e07ab..e6eb4a683d 100644 --- a/include/xrpl/ledger/detail/ApplyStateTable.h +++ b/include/xrpl/ledger/detail/ApplyStateTable.h @@ -17,147 +17,98 @@ */ //============================================================================== -#ifndef RIPPLE_LEDGER_APPLYSTATETABLE_H_INCLUDED -#define RIPPLE_LEDGER_APPLYSTATETABLE_H_INCLUDED +#ifndef RIPPLE_LEDGER_APPLYVIEWIMPL_H_INCLUDED +#define RIPPLE_LEDGER_APPLYVIEWIMPL_H_INCLUDED -#include -#include -#include -#include +#include +#include + +#include #include -#include -#include - -#include namespace ripple { -namespace detail { -// Helper class that buffers modifications -class ApplyStateTable +/** Editable, discardable view that can build metadata for one tx. + + Iteration of the tx map is delegated to the base. + + @note Presented as ApplyView to clients. +*/ +class ApplyViewImpl final : public detail::ApplyViewBase { public: - using key_type = ReadView::key_type; + ApplyViewImpl() = delete; + ApplyViewImpl(ApplyViewImpl const&) = delete; + ApplyViewImpl& + operator=(ApplyViewImpl&&) = delete; + ApplyViewImpl& + operator=(ApplyViewImpl const&) = delete; -private: - enum class Action { - cache, - erase, - insert, - modify, - }; + ApplyViewImpl(ApplyViewImpl&&) = default; + ApplyViewImpl(ReadView const* base, ApplyFlags flags); - using items_t = std::map>>; - - items_t items_; - XRPAmount dropsDestroyed_{0}; - -public: - ApplyStateTable() = default; - ApplyStateTable(ApplyStateTable&&) = default; - - ApplyStateTable(ApplyStateTable const&) = delete; - ApplyStateTable& - operator=(ApplyStateTable&&) = delete; - ApplyStateTable& - operator=(ApplyStateTable const&) = delete; - - void - apply(RawView& to) const; + /** Apply the transaction. + After a call to `apply`, the only valid + operation on this object is to call the + destructor. + */ std::optional apply( OpenView& to, STTx const& tx, TER ter, - std::optional const& deliver, - std::optional const& parentBatchId, + std::optional parentBatchId, bool isDryRun, beast::Journal j); - bool - exists(ReadView const& base, Keylet const& k) const; + /** Set the amount of currency delivered. - std::optional - succ( - ReadView const& base, - key_type const& key, - std::optional const& last) const; - - std::shared_ptr - read(ReadView const& base, Keylet const& k) const; - - std::shared_ptr - peek(ReadView const& base, Keylet const& k); - - std::size_t - size() const; + This value is used when generating metadata + for payments, to set the DeliveredAmount field. + If the amount is not specified, the field is + excluded from the resulting metadata. + */ + void + deliver(STAmount const& amount) + { + 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 + size(); + + /** Visit modified entries + */ void visit( - ReadView const& base, + OpenView& target, std::function const& before, - std::shared_ptr const& after)> const& func) const; - - void - erase(ReadView const& base, std::shared_ptr const& sle); - - void - rawErase(ReadView const& base, std::shared_ptr const& sle); - - void - insert(ReadView const& base, std::shared_ptr const& sle); - - void - update(ReadView const& base, std::shared_ptr const& sle); - - void - replace(ReadView const& base, std::shared_ptr const& sle); - - void - destroyXRP(XRPAmount const& fee); - - // For debugging - XRPAmount const& - dropsDestroyed() const - { - return dropsDestroyed_; - } + std::shared_ptr const& after)> const& func); private: - using Mods = hash_map>; - - static void - threadItem(TxMeta& meta, std::shared_ptr const& to); - - std::shared_ptr - getForMod( - ReadView const& base, - key_type const& key, - Mods& mods, - beast::Journal j); - - void - threadTx( - ReadView const& base, - TxMeta& meta, - AccountID const& to, - Mods& mods, - beast::Journal j); - - void - threadOwners( - ReadView const& base, - TxMeta& meta, - std::shared_ptr const& sle, - Mods& mods, - beast::Journal j); + std::optional deliver_; + std::optional gasUsed_; + std::optional wasmReturnCode_; }; -} // namespace detail } // namespace ripple #endif diff --git a/include/xrpl/protocol/Fees.h b/include/xrpl/protocol/Fees.h index 4393f1a1d9..1a1ecacb36 100644 --- a/include/xrpl/protocol/Fees.h +++ b/include/xrpl/protocol/Fees.h @@ -24,6 +24,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 @@ -34,6 +36,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 79be15d906..1e4bb921d7 100644 --- a/include/xrpl/protocol/Indexes.h +++ b/include/xrpl/protocol/Indexes.h @@ -231,6 +231,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; @@ -287,11 +293,9 @@ delegate(AccountID const& account, AccountID const& authorizedAccount) noexcept; Keylet bridge(STXChainBridge const& bridge, STXChainBridge::ChainType chainType); -// `seq` is stored as `sfXChainClaimID` in the object Keylet xChainClaimID(STXChainBridge const& bridge, std::uint64_t seq); -// `seq` is stored as `sfXChainAccountCreateCount` in the object Keylet xChainCreateAccountClaimID(STXChainBridge const& bridge, std::uint64_t seq); diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index 535fd615c1..d52ae2dfc1 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -22,6 +22,7 @@ #include #include +#include #include diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index 2df3e72575..9fd73520d5 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -187,6 +187,8 @@ enum TEFcodes : TERUnderlyingType { tefNO_TICKET, tefNFTOKEN_IS_NOT_TRANSFERABLE, tefINVALID_LEDGER_FIX_TYPE, + tefNO_WASM, + tefWASM_FIELD_NOT_INCLUDED, }; //------------------------------------------------------------------------------ @@ -364,6 +366,7 @@ enum TECcodes : TERUnderlyingType { tecPSEUDO_ACCOUNT = 196, tecPRECISION_LOSS = 197, tecNO_DELEGATE_PERMISSION = 198, + tecWASM_REJECTED = 199, }; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/TxMeta.h b/include/xrpl/protocol/TxMeta.h index 02fde2ffe5..6b1c24e9eb 100644 --- a/include/xrpl/protocol/TxMeta.h +++ b/include/xrpl/protocol/TxMeta.h @@ -46,10 +46,7 @@ private: CtorHelper); public: - TxMeta( - uint256 const& transactionID, - std::uint32_t ledger, - std::optional parentBatchId = std::nullopt); + TxMeta(uint256 const& transactionID, std::uint32_t ledger); TxMeta(uint256 const& txID, std::uint32_t ledger, Blob const&); TxMeta(uint256 const& txID, std::uint32_t ledger, std::string const&); TxMeta(uint256 const& txID, std::uint32_t ledger, STObject const&); @@ -136,7 +133,7 @@ public: void setParentBatchId(uint256 const& parentBatchId) { - mParentBatchId = parentBatchId; + parentBatchId_ = parentBatchId; } uint256 @@ -145,13 +142,55 @@ public: XRPL_ASSERT( hasParentBatchId(), "ripple::TxMeta::getParentBatchId : non-null batch id"); - return *mParentBatchId; + return *parentBatchId_; } bool hasParentBatchId() const { - return static_cast(mParentBatchId); + return static_cast(parentBatchId_); + } + + void + setGasUsed(std::uint32_t const& gasUsed) + { + gasUsed_ = gasUsed; + } + + std::uint32_t + getGasUsed() const + { + XRPL_ASSERT( + hasGasUsed(), + "ripple::TxMeta::getGasUsed : non-null gas used field"); + return *gasUsed_; + } + + bool + hasGasUsed() const + { + return static_cast(gasUsed_); + } + + void + setWasmReturnCode(std::int32_t const& wasmReturnCode) + { + wasmReturnCode_ = wasmReturnCode; + } + + std::int32_t + getWasmReturnCode() const + { + XRPL_ASSERT( + hasWasmReturnCode(), + "ripple::TxMeta::getWasmReturnCode : non-null wasm return code"); + return *wasmReturnCode_; + } + + bool + hasWasmReturnCode() const + { + return static_cast(wasmReturnCode_); } private: @@ -161,7 +200,9 @@ private: int mResult; std::optional mDelivered; - std::optional mParentBatchId; + std::optional parentBatchId_; + std::optional gasUsed_; + std::optional wasmReturnCode_; STArray mNodes; }; diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 9dc40dc8e5..3e581d5020 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -32,9 +32,7 @@ // If you add an amendment here, then do not forget to increment `numFeatures` // in include/xrpl/protocol/Feature.h. -XRPL_FIX (IncludeKeyletFields, Supported::no, VoteBehavior::DefaultNo) -XRPL_FEATURE(DynamicMPT, Supported::no, VoteBehavior::DefaultNo) -XRPL_FIX (TokenEscrowV1, Supported::yes, VoteBehavior::DefaultNo) +XRPL_FEATURE(SmartEscrow, Supported::no, VoteBehavior::DefaultNo) XRPL_FIX (DelegateV1_1, Supported::no, VoteBehavior::DefaultNo) XRPL_FIX (PriceOracleOrder, Supported::no, VoteBehavior::DefaultNo) XRPL_FIX (MPTDeliveredAmount, Supported::no, VoteBehavior::DefaultNo) @@ -47,7 +45,6 @@ XRPL_FEATURE(Batch, Supported::yes, VoteBehavior::DefaultNo XRPL_FEATURE(SingleAssetVault, Supported::no, VoteBehavior::DefaultNo) XRPL_FEATURE(PermissionDelegation, Supported::yes, 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 f76188095e..9c69b33cb8 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -120,7 +120,6 @@ LEDGER_ENTRY(ltNFTOKEN_PAGE, 0x0050, NFTokenPage, nft_page, ({ // All fields are soeREQUIRED because there is always a SignerEntries. // If there are no SignerEntries the node is deleted. LEDGER_ENTRY(ltSIGNER_LIST, 0x0053, SignerList, signer_list, ({ - {sfOwner, soeOPTIONAL}, {sfOwnerNode, soeREQUIRED}, {sfSignerQuorum, soeREQUIRED}, {sfSignerEntries, soeREQUIRED}, @@ -189,7 +188,7 @@ LEDGER_ENTRY(ltDIR_NODE, 0x0064, DirectoryNode, directory, ({ {sfNFTokenID, soeOPTIONAL}, {sfPreviousTxnID, soeOPTIONAL}, {sfPreviousTxnLgrSeq, soeOPTIONAL}, - {sfDomainID, soeOPTIONAL} // order book directories + {sfDomainID, soeOPTIONAL} })) /** The ledger object which lists details about amendments on the network. @@ -320,6 +319,7 @@ LEDGER_ENTRY(ltFEE_SETTINGS, 0x0073, FeeSettings, fee, ({ {sfBaseFeeDrops, soeOPTIONAL}, {sfReserveBaseDrops, soeOPTIONAL}, {sfReserveIncrementDrops, soeOPTIONAL}, + {sfPreviousTxnID, soeOPTIONAL}, {sfPreviousTxnLgrSeq, soeOPTIONAL}, })) @@ -344,12 +344,13 @@ LEDGER_ENTRY(ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID, 0x0074, XChainOwnedCreateAc */ LEDGER_ENTRY(ltESCROW, 0x0075, Escrow, escrow, ({ {sfAccount, soeREQUIRED}, - {sfSequence, soeOPTIONAL}, {sfDestination, soeREQUIRED}, {sfAmount, soeREQUIRED}, {sfCondition, soeOPTIONAL}, {sfCancelAfter, soeOPTIONAL}, {sfFinishAfter, soeOPTIONAL}, + {sfFinishFunction, soeOPTIONAL}, + {sfData, soeOPTIONAL}, {sfSourceTag, soeOPTIONAL}, {sfDestinationTag, soeOPTIONAL}, {sfOwnerNode, soeREQUIRED}, @@ -367,7 +368,6 @@ LEDGER_ENTRY(ltESCROW, 0x0075, Escrow, escrow, ({ LEDGER_ENTRY(ltPAYCHAN, 0x0078, PayChannel, payment_channel, ({ {sfAccount, soeREQUIRED}, {sfDestination, soeREQUIRED}, - {sfSequence, soeOPTIONAL}, {sfAmount, soeREQUIRED}, {sfBalance, soeREQUIRED}, {sfPublicKey, soeREQUIRED}, @@ -415,7 +415,6 @@ LEDGER_ENTRY(ltMPTOKEN_ISSUANCE, 0x007e, MPTokenIssuance, mpt_issuance, ({ {sfPreviousTxnID, soeREQUIRED}, {sfPreviousTxnLgrSeq, soeREQUIRED}, {sfDomainID, soeOPTIONAL}, - {sfMutableFlags, soeDEFAULT}, })) /** A ledger object which tracks MPToken @@ -436,7 +435,6 @@ LEDGER_ENTRY(ltMPTOKEN, 0x007f, MPToken, mptoken, ({ */ LEDGER_ENTRY(ltORACLE, 0x0080, Oracle, oracle, ({ {sfOwner, soeREQUIRED}, - {sfOracleDocumentID, soeOPTIONAL}, {sfProvider, soeREQUIRED}, {sfPriceDataSeries, soeREQUIRED}, {sfAssetClass, soeREQUIRED}, diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 10fe015dac..1fcf1da622 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -114,7 +114,11 @@ TYPED_SFIELD(sfVoteWeight, UINT32, 48) TYPED_SFIELD(sfFirstNFTokenSequence, UINT32, 50) TYPED_SFIELD(sfOracleDocumentID, UINT32, 51) TYPED_SFIELD(sfPermissionValue, UINT32, 52) -TYPED_SFIELD(sfMutableFlags, UINT32, 53) +TYPED_SFIELD(sfExtensionComputeLimit, UINT32, 53) +TYPED_SFIELD(sfExtensionSizeLimit, UINT32, 54) +TYPED_SFIELD(sfGasPrice, UINT32, 55) +TYPED_SFIELD(sfComputationAllowance, UINT32, 56) +TYPED_SFIELD(sfGasUsed, UINT32, 57) // 64-bit integers (common) TYPED_SFIELD(sfIndexNext, UINT64, 1) @@ -174,8 +178,7 @@ TYPED_SFIELD(sfNFTokenID, UINT256, 10) TYPED_SFIELD(sfEmitParentTxnID, UINT256, 11) TYPED_SFIELD(sfEmitNonce, UINT256, 12) TYPED_SFIELD(sfEmitHookHash, UINT256, 13) -TYPED_SFIELD(sfAMMID, UINT256, 14, - SField::sMD_PseudoAccount | SField::sMD_Default) +TYPED_SFIELD(sfAMMID, UINT256, 14) // 256-bit (uncommon) TYPED_SFIELD(sfBookDirectory, UINT256, 16) @@ -197,8 +200,7 @@ TYPED_SFIELD(sfHookHash, UINT256, 31) TYPED_SFIELD(sfHookNamespace, UINT256, 32) TYPED_SFIELD(sfHookSetTxnID, UINT256, 33) TYPED_SFIELD(sfDomainID, UINT256, 34) -TYPED_SFIELD(sfVaultID, UINT256, 35, - SField::sMD_PseudoAccount | SField::sMD_Default) +TYPED_SFIELD(sfVaultID, UINT256, 35) TYPED_SFIELD(sfParentBatchID, UINT256, 36) // number (common) @@ -208,6 +210,9 @@ TYPED_SFIELD(sfAssetsMaximum, NUMBER, 3) TYPED_SFIELD(sfAssetsTotal, NUMBER, 4) TYPED_SFIELD(sfLossUnrealized, NUMBER, 5) +// 32-bit signed (common) +TYPED_SFIELD(sfWasmReturnCode, INT32, 1) + // currency amount (common) TYPED_SFIELD(sfAmount, AMOUNT, 1) TYPED_SFIELD(sfBalance, AMOUNT, 2) @@ -236,7 +241,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) @@ -278,6 +283,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 3ea4a3bbec..cb0ff47b9f 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -22,31 +22,16 @@ #endif /** - * TRANSACTION(tag, value, name, delegatable, amendments, privileges, fields) - * - * To ease maintenance, you may replace any unneeded values with "..." - * e.g. #define TRANSACTION(tag, value, name, ...) + * TRANSACTION(tag, value, name, delegatable, amendments, fields) * * You must define a transactor class in the `ripple` namespace named `name`, - * and include its header alongside the TRANSACTOR definition using this - * format: - * #if TRANSACTION_INCLUDE - * # include - * #endif - * - * The `privileges` parameter of the TRANSACTION macro is a bitfield - * defining which operations the transaction can perform. - * The values are defined and used in InvariantCheck.cpp + * and include its header in `src/xrpld/app/tx/detail/applySteps.cpp`. */ /** This transaction type executes a payment. */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttPAYMENT, 0, Payment, Delegation::delegatable, uint256{}, - createAcct, ({ {sfDestination, soeREQUIRED}, {sfAmount, soeREQUIRED, soeMPTSupported}, @@ -60,44 +45,38 @@ TRANSACTION(ttPAYMENT, 0, Payment, })) /** This transaction type creates an escrow object. */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttESCROW_CREATE, 1, EscrowCreate, Delegation::delegatable, uint256{}, - 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. */ TRANSACTION(ttESCROW_FINISH, 2, EscrowFinish, Delegation::delegatable, uint256{}, - noPriv, ({ {sfOwner, soeREQUIRED}, {sfOfferSequence, soeREQUIRED}, {sfFulfillment, soeOPTIONAL}, {sfCondition, soeOPTIONAL}, {sfCredentialIDs, soeOPTIONAL}, + {sfComputationAllowance, soeOPTIONAL}, })) /** This transaction type adjusts various account settings. */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttACCOUNT_SET, 3, AccountSet, Delegation::notDelegatable, uint256{}, - noPriv, ({ {sfEmailHash, soeOPTIONAL}, {sfWalletLocator, soeOPTIONAL}, @@ -112,26 +91,18 @@ TRANSACTION(ttACCOUNT_SET, 3, AccountSet, })) /** This transaction type cancels an existing escrow. */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttESCROW_CANCEL, 4, EscrowCancel, Delegation::delegatable, uint256{}, - noPriv, ({ {sfOwner, soeREQUIRED}, {sfOfferSequence, soeREQUIRED}, })) /** This transaction type sets or clears an account's "regular key". */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttREGULAR_KEY_SET, 5, SetRegularKey, Delegation::notDelegatable, uint256{}, - noPriv, ({ {sfRegularKey, soeOPTIONAL}, })) @@ -139,13 +110,9 @@ TRANSACTION(ttREGULAR_KEY_SET, 5, SetRegularKey, // 6 deprecated /** This transaction type creates an offer to trade one asset for another. */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttOFFER_CREATE, 7, OfferCreate, Delegation::delegatable, uint256{}, - noPriv, ({ {sfTakerPays, soeREQUIRED}, {sfTakerGets, soeREQUIRED}, @@ -155,13 +122,9 @@ TRANSACTION(ttOFFER_CREATE, 7, OfferCreate, })) /** This transaction type cancels existing offers to trade one asset for another. */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttOFFER_CANCEL, 8, OfferCancel, Delegation::delegatable, uint256{}, - noPriv, ({ {sfOfferSequence, soeREQUIRED}, })) @@ -169,13 +132,9 @@ TRANSACTION(ttOFFER_CANCEL, 8, OfferCancel, // 9 deprecated /** This transaction type creates a new set of tickets. */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttTICKET_CREATE, 10, TicketCreate, Delegation::delegatable, featureTicketBatch, - noPriv, ({ {sfTicketCount, soeREQUIRED}, })) @@ -185,26 +144,18 @@ TRANSACTION(ttTICKET_CREATE, 10, TicketCreate, /** This transaction type modifies the signer list associated with an account. */ // The SignerEntries are optional because a SignerList is deleted by // setting the SignerQuorum to zero and omitting SignerEntries. -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttSIGNER_LIST_SET, 12, SignerListSet, Delegation::notDelegatable, uint256{}, - noPriv, ({ {sfSignerQuorum, soeREQUIRED}, {sfSignerEntries, soeOPTIONAL}, })) /** This transaction type creates a new unidirectional XRP payment channel. */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttPAYCHAN_CREATE, 13, PaymentChannelCreate, Delegation::delegatable, uint256{}, - noPriv, ({ {sfDestination, soeREQUIRED}, {sfAmount, soeREQUIRED}, @@ -218,7 +169,6 @@ TRANSACTION(ttPAYCHAN_CREATE, 13, PaymentChannelCreate, TRANSACTION(ttPAYCHAN_FUND, 14, PaymentChannelFund, Delegation::delegatable, uint256{}, - noPriv, ({ {sfChannel, soeREQUIRED}, {sfAmount, soeREQUIRED}, @@ -229,7 +179,6 @@ TRANSACTION(ttPAYCHAN_FUND, 14, PaymentChannelFund, TRANSACTION(ttPAYCHAN_CLAIM, 15, PaymentChannelClaim, Delegation::delegatable, uint256{}, - noPriv, ({ {sfChannel, soeREQUIRED}, {sfAmount, soeOPTIONAL}, @@ -240,13 +189,9 @@ TRANSACTION(ttPAYCHAN_CLAIM, 15, PaymentChannelClaim, })) /** This transaction type creates a new check. */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttCHECK_CREATE, 16, CheckCreate, Delegation::delegatable, featureChecks, - noPriv, ({ {sfDestination, soeREQUIRED}, {sfSendMax, soeREQUIRED}, @@ -256,13 +201,9 @@ TRANSACTION(ttCHECK_CREATE, 16, CheckCreate, })) /** This transaction type cashes an existing check. */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttCHECK_CASH, 17, CheckCash, Delegation::delegatable, featureChecks, - noPriv, ({ {sfCheckID, soeREQUIRED}, {sfAmount, soeOPTIONAL}, @@ -270,25 +211,17 @@ TRANSACTION(ttCHECK_CASH, 17, CheckCash, })) /** This transaction type cancels an existing check. */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttCHECK_CANCEL, 18, CheckCancel, Delegation::delegatable, featureChecks, - noPriv, ({ {sfCheckID, soeREQUIRED}, })) /** This transaction type grants or revokes authorization to transfer funds. */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttDEPOSIT_PREAUTH, 19, DepositPreauth, Delegation::delegatable, featureDepositPreauth, - noPriv, ({ {sfAuthorize, soeOPTIONAL}, {sfUnauthorize, soeOPTIONAL}, @@ -297,13 +230,9 @@ TRANSACTION(ttDEPOSIT_PREAUTH, 19, DepositPreauth, })) /** This transaction type modifies a trustline between two accounts. */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttTRUST_SET, 20, TrustSet, Delegation::delegatable, uint256{}, - noPriv, ({ {sfLimitAmount, soeOPTIONAL}, {sfQualityIn, soeOPTIONAL}, @@ -311,13 +240,9 @@ TRANSACTION(ttTRUST_SET, 20, TrustSet, })) /** This transaction type deletes an existing account. */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttACCOUNT_DELETE, 21, AccountDelete, Delegation::notDelegatable, uint256{}, - mustDeleteAcct, ({ {sfDestination, soeREQUIRED}, {sfDestinationTag, soeOPTIONAL}, @@ -327,13 +252,9 @@ TRANSACTION(ttACCOUNT_DELETE, 21, AccountDelete, // 22 reserved /** This transaction mints a new NFT. */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttNFTOKEN_MINT, 25, NFTokenMint, Delegation::delegatable, featureNonFungibleTokensV1, - changeNFTCounts, ({ {sfNFTokenTaxon, soeREQUIRED}, {sfTransferFee, soeOPTIONAL}, @@ -345,26 +266,18 @@ TRANSACTION(ttNFTOKEN_MINT, 25, NFTokenMint, })) /** This transaction burns (i.e. destroys) an existing NFT. */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttNFTOKEN_BURN, 26, NFTokenBurn, Delegation::delegatable, featureNonFungibleTokensV1, - changeNFTCounts, ({ {sfNFTokenID, soeREQUIRED}, {sfOwner, soeOPTIONAL}, })) /** This transaction creates a new offer to buy or sell an NFT. */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttNFTOKEN_CREATE_OFFER, 27, NFTokenCreateOffer, Delegation::delegatable, featureNonFungibleTokensV1, - noPriv, ({ {sfNFTokenID, soeREQUIRED}, {sfAmount, soeREQUIRED}, @@ -374,25 +287,17 @@ TRANSACTION(ttNFTOKEN_CREATE_OFFER, 27, NFTokenCreateOffer, })) /** This transaction cancels an existing offer to buy or sell an existing NFT. */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttNFTOKEN_CANCEL_OFFER, 28, NFTokenCancelOffer, Delegation::delegatable, featureNonFungibleTokensV1, - noPriv, ({ {sfNFTokenOffers, soeREQUIRED}, })) /** This transaction accepts an existing offer to buy or sell an existing NFT. */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttNFTOKEN_ACCEPT_OFFER, 29, NFTokenAcceptOffer, Delegation::delegatable, featureNonFungibleTokensV1, - noPriv, ({ {sfNFTokenBuyOffer, soeOPTIONAL}, {sfNFTokenSellOffer, soeOPTIONAL}, @@ -400,26 +305,18 @@ TRANSACTION(ttNFTOKEN_ACCEPT_OFFER, 29, NFTokenAcceptOffer, })) /** This transaction claws back issued tokens. */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttCLAWBACK, 30, Clawback, Delegation::delegatable, featureClawback, - noPriv, ({ {sfAmount, soeREQUIRED, soeMPTSupported}, {sfHolder, soeOPTIONAL}, })) /** This transaction claws back tokens from an AMM pool. */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttAMM_CLAWBACK, 31, AMMClawback, Delegation::delegatable, featureAMMClawback, - mayDeleteAcct | overrideFreeze, ({ {sfHolder, soeREQUIRED}, {sfAsset, soeREQUIRED}, @@ -428,13 +325,9 @@ TRANSACTION(ttAMM_CLAWBACK, 31, AMMClawback, })) /** This transaction type creates an AMM instance */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttAMM_CREATE, 35, AMMCreate, Delegation::delegatable, featureAMM, - createPseudoAcct, ({ {sfAmount, soeREQUIRED}, {sfAmount2, soeREQUIRED}, @@ -442,13 +335,9 @@ TRANSACTION(ttAMM_CREATE, 35, AMMCreate, })) /** This transaction type deposits into an AMM instance */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttAMM_DEPOSIT, 36, AMMDeposit, Delegation::delegatable, featureAMM, - noPriv, ({ {sfAsset, soeREQUIRED}, {sfAsset2, soeREQUIRED}, @@ -460,13 +349,9 @@ TRANSACTION(ttAMM_DEPOSIT, 36, AMMDeposit, })) /** This transaction type withdraws from an AMM instance */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttAMM_WITHDRAW, 37, AMMWithdraw, Delegation::delegatable, featureAMM, - mayDeleteAcct, ({ {sfAsset, soeREQUIRED}, {sfAsset2, soeREQUIRED}, @@ -477,13 +362,9 @@ TRANSACTION(ttAMM_WITHDRAW, 37, AMMWithdraw, })) /** This transaction type votes for the trading fee */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttAMM_VOTE, 38, AMMVote, Delegation::delegatable, featureAMM, - noPriv, ({ {sfAsset, soeREQUIRED}, {sfAsset2, soeREQUIRED}, @@ -491,13 +372,9 @@ TRANSACTION(ttAMM_VOTE, 38, AMMVote, })) /** This transaction type bids for the auction slot */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttAMM_BID, 39, AMMBid, Delegation::delegatable, featureAMM, - noPriv, ({ {sfAsset, soeREQUIRED}, {sfAsset2, soeREQUIRED}, @@ -507,26 +384,18 @@ TRANSACTION(ttAMM_BID, 39, AMMBid, })) /** This transaction type deletes AMM in the empty state */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttAMM_DELETE, 40, AMMDelete, Delegation::delegatable, featureAMM, - mustDeleteAcct, ({ {sfAsset, soeREQUIRED}, {sfAsset2, soeREQUIRED}, })) /** This transactions creates a crosschain sequence number */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttXCHAIN_CREATE_CLAIM_ID, 41, XChainCreateClaimID, Delegation::delegatable, featureXChainBridge, - noPriv, ({ {sfXChainBridge, soeREQUIRED}, {sfSignatureReward, soeREQUIRED}, @@ -537,7 +406,6 @@ TRANSACTION(ttXCHAIN_CREATE_CLAIM_ID, 41, XChainCreateClaimID, TRANSACTION(ttXCHAIN_COMMIT, 42, XChainCommit, Delegation::delegatable, featureXChainBridge, - noPriv, ({ {sfXChainBridge, soeREQUIRED}, {sfXChainClaimID, soeREQUIRED}, @@ -549,7 +417,6 @@ TRANSACTION(ttXCHAIN_COMMIT, 42, XChainCommit, TRANSACTION(ttXCHAIN_CLAIM, 43, XChainClaim, Delegation::delegatable, featureXChainBridge, - noPriv, ({ {sfXChainBridge, soeREQUIRED}, {sfXChainClaimID, soeREQUIRED}, @@ -562,7 +429,6 @@ TRANSACTION(ttXCHAIN_CLAIM, 43, XChainClaim, TRANSACTION(ttXCHAIN_ACCOUNT_CREATE_COMMIT, 44, XChainAccountCreateCommit, Delegation::delegatable, featureXChainBridge, - noPriv, ({ {sfXChainBridge, soeREQUIRED}, {sfDestination, soeREQUIRED}, @@ -574,7 +440,6 @@ TRANSACTION(ttXCHAIN_ACCOUNT_CREATE_COMMIT, 44, XChainAccountCreateCommit, TRANSACTION(ttXCHAIN_ADD_CLAIM_ATTESTATION, 45, XChainAddClaimAttestation, Delegation::delegatable, featureXChainBridge, - createAcct, ({ {sfXChainBridge, soeREQUIRED}, @@ -591,11 +456,9 @@ TRANSACTION(ttXCHAIN_ADD_CLAIM_ATTESTATION, 45, XChainAddClaimAttestation, })) /** This transaction adds an attestation to an account */ -TRANSACTION(ttXCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION, 46, - XChainAddAccountCreateAttestation, +TRANSACTION(ttXCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION, 46, XChainAddAccountCreateAttestation, Delegation::delegatable, featureXChainBridge, - createAcct, ({ {sfXChainBridge, soeREQUIRED}, @@ -616,7 +479,6 @@ TRANSACTION(ttXCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION, 46, TRANSACTION(ttXCHAIN_MODIFY_BRIDGE, 47, XChainModifyBridge, Delegation::delegatable, featureXChainBridge, - noPriv, ({ {sfXChainBridge, soeREQUIRED}, {sfSignatureReward, soeOPTIONAL}, @@ -627,7 +489,6 @@ TRANSACTION(ttXCHAIN_MODIFY_BRIDGE, 47, XChainModifyBridge, TRANSACTION(ttXCHAIN_CREATE_BRIDGE, 48, XChainCreateBridge, Delegation::delegatable, featureXChainBridge, - noPriv, ({ {sfXChainBridge, soeREQUIRED}, {sfSignatureReward, soeREQUIRED}, @@ -635,13 +496,9 @@ TRANSACTION(ttXCHAIN_CREATE_BRIDGE, 48, XChainCreateBridge, })) /** This transaction type creates or updates a DID */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttDID_SET, 49, DIDSet, Delegation::delegatable, featureDID, - noPriv, ({ {sfDIDDocument, soeOPTIONAL}, {sfURI, soeOPTIONAL}, @@ -652,17 +509,12 @@ TRANSACTION(ttDID_SET, 49, DIDSet, TRANSACTION(ttDID_DELETE, 50, DIDDelete, Delegation::delegatable, featureDID, - noPriv, ({})) /** This transaction type creates an Oracle instance */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttORACLE_SET, 51, OracleSet, Delegation::delegatable, featurePriceOracle, - noPriv, ({ {sfOracleDocumentID, soeREQUIRED}, {sfProvider, soeOPTIONAL}, @@ -673,97 +525,65 @@ TRANSACTION(ttORACLE_SET, 51, OracleSet, })) /** This transaction type deletes an Oracle instance */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttORACLE_DELETE, 52, OracleDelete, Delegation::delegatable, featurePriceOracle, - noPriv, ({ {sfOracleDocumentID, soeREQUIRED}, })) /** This transaction type fixes a problem in the ledger state */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttLEDGER_STATE_FIX, 53, LedgerStateFix, Delegation::delegatable, fixNFTokenPageLinks, - noPriv, ({ {sfLedgerFixType, soeREQUIRED}, {sfOwner, soeOPTIONAL}, })) /** This transaction type creates a MPTokensIssuance instance */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttMPTOKEN_ISSUANCE_CREATE, 54, MPTokenIssuanceCreate, Delegation::delegatable, featureMPTokensV1, - createMPTIssuance, ({ {sfAssetScale, soeOPTIONAL}, {sfTransferFee, soeOPTIONAL}, {sfMaximumAmount, soeOPTIONAL}, {sfMPTokenMetadata, soeOPTIONAL}, {sfDomainID, soeOPTIONAL}, - {sfMutableFlags, soeOPTIONAL}, })) /** This transaction type destroys a MPTokensIssuance instance */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttMPTOKEN_ISSUANCE_DESTROY, 55, MPTokenIssuanceDestroy, Delegation::delegatable, featureMPTokensV1, - destroyMPTIssuance, ({ {sfMPTokenIssuanceID, soeREQUIRED}, })) /** This transaction type sets flags on a MPTokensIssuance or MPToken instance */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttMPTOKEN_ISSUANCE_SET, 56, MPTokenIssuanceSet, Delegation::delegatable, featureMPTokensV1, - noPriv, ({ {sfMPTokenIssuanceID, soeREQUIRED}, {sfHolder, soeOPTIONAL}, {sfDomainID, soeOPTIONAL}, - {sfMPTokenMetadata, soeOPTIONAL}, - {sfTransferFee, soeOPTIONAL}, - {sfMutableFlags, soeOPTIONAL}, })) /** This transaction type authorizes a MPToken instance */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttMPTOKEN_AUTHORIZE, 57, MPTokenAuthorize, Delegation::delegatable, featureMPTokensV1, - mustAuthorizeMPT, ({ {sfMPTokenIssuanceID, soeREQUIRED}, {sfHolder, soeOPTIONAL}, })) /** This transaction type create an Credential instance */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttCREDENTIAL_CREATE, 58, CredentialCreate, Delegation::delegatable, featureCredentials, - noPriv, ({ {sfSubject, soeREQUIRED}, {sfCredentialType, soeREQUIRED}, @@ -775,7 +595,6 @@ TRANSACTION(ttCREDENTIAL_CREATE, 58, CredentialCreate, TRANSACTION(ttCREDENTIAL_ACCEPT, 59, CredentialAccept, Delegation::delegatable, featureCredentials, - noPriv, ({ {sfIssuer, soeREQUIRED}, {sfCredentialType, soeREQUIRED}, @@ -785,7 +604,6 @@ TRANSACTION(ttCREDENTIAL_ACCEPT, 59, CredentialAccept, TRANSACTION(ttCREDENTIAL_DELETE, 60, CredentialDelete, Delegation::delegatable, featureCredentials, - noPriv, ({ {sfSubject, soeOPTIONAL}, {sfIssuer, soeOPTIONAL}, @@ -793,13 +611,9 @@ TRANSACTION(ttCREDENTIAL_DELETE, 60, CredentialDelete, })) /** This transaction type modify a NFToken */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttNFTOKEN_MODIFY, 61, NFTokenModify, Delegation::delegatable, featureDynamicNFT, - noPriv, ({ {sfNFTokenID, soeREQUIRED}, {sfOwner, soeOPTIONAL}, @@ -807,51 +621,35 @@ TRANSACTION(ttNFTOKEN_MODIFY, 61, NFTokenModify, })) /** This transaction type creates or modifies a Permissioned Domain */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttPERMISSIONED_DOMAIN_SET, 62, PermissionedDomainSet, Delegation::delegatable, featurePermissionedDomains, - noPriv, ({ {sfDomainID, soeOPTIONAL}, {sfAcceptedCredentials, soeREQUIRED}, })) /** This transaction type deletes a Permissioned Domain */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttPERMISSIONED_DOMAIN_DELETE, 63, PermissionedDomainDelete, Delegation::delegatable, featurePermissionedDomains, - noPriv, ({ {sfDomainID, soeREQUIRED}, })) /** This transaction type delegates authorized account specified permissions */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttDELEGATE_SET, 64, DelegateSet, Delegation::notDelegatable, featurePermissionDelegation, - noPriv, ({ {sfAuthorize, soeREQUIRED}, {sfPermissions, soeREQUIRED}, })) /** This transaction creates a single asset vault. */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttVAULT_CREATE, 65, VaultCreate, Delegation::delegatable, featureSingleAssetVault, - createPseudoAcct | createMPTIssuance, ({ {sfAsset, soeREQUIRED, soeMPTSupported}, {sfAssetsMaximum, soeOPTIONAL}, @@ -863,13 +661,9 @@ TRANSACTION(ttVAULT_CREATE, 65, VaultCreate, })) /** This transaction updates a single asset vault. */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttVAULT_SET, 66, VaultSet, Delegation::delegatable, featureSingleAssetVault, - noPriv, ({ {sfVaultID, soeREQUIRED}, {sfAssetsMaximum, soeOPTIONAL}, @@ -878,38 +672,26 @@ TRANSACTION(ttVAULT_SET, 66, VaultSet, })) /** This transaction deletes a single asset vault. */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttVAULT_DELETE, 67, VaultDelete, Delegation::delegatable, featureSingleAssetVault, - mustDeleteAcct | destroyMPTIssuance, ({ {sfVaultID, soeREQUIRED}, })) /** This transaction trades assets for shares with a vault. */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttVAULT_DEPOSIT, 68, VaultDeposit, Delegation::delegatable, featureSingleAssetVault, - mayAuthorizeMPT, ({ {sfVaultID, soeREQUIRED}, {sfAmount, soeREQUIRED, soeMPTSupported}, })) /** This transaction trades shares for assets with a vault. */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttVAULT_WITHDRAW, 69, VaultWithdraw, Delegation::delegatable, featureSingleAssetVault, - mayDeleteMPT, ({ {sfVaultID, soeREQUIRED}, {sfAmount, soeREQUIRED, soeMPTSupported}, @@ -918,13 +700,9 @@ TRANSACTION(ttVAULT_WITHDRAW, 69, VaultWithdraw, })) /** This transaction claws back tokens from a vault. */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttVAULT_CLAWBACK, 70, VaultClawback, Delegation::delegatable, featureSingleAssetVault, - mayDeleteMPT, ({ {sfVaultID, soeREQUIRED}, {sfHolder, soeREQUIRED}, @@ -932,13 +710,9 @@ TRANSACTION(ttVAULT_CLAWBACK, 70, VaultClawback, })) /** This transaction type batches together transactions. */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttBATCH, 71, Batch, Delegation::notDelegatable, featureBatch, - noPriv, ({ {sfRawTransactions, soeREQUIRED}, {sfBatchSigners, soeOPTIONAL}, @@ -948,13 +722,9 @@ TRANSACTION(ttBATCH, 71, Batch, For details, see: https://xrpl.org/amendments.html */ -#if TRANSACTION_INCLUDE -# include -#endif TRANSACTION(ttAMENDMENT, 100, EnableAmendment, Delegation::notDelegatable, uint256{}, - noPriv, ({ {sfLedgerSequence, soeREQUIRED}, {sfAmendment, soeREQUIRED}, @@ -966,7 +736,6 @@ TRANSACTION(ttAMENDMENT, 100, EnableAmendment, TRANSACTION(ttFEE, 101, SetFee, Delegation::notDelegatable, uint256{}, - noPriv, ({ {sfLedgerSequence, soeOPTIONAL}, // Old version uses raw numbers @@ -987,7 +756,6 @@ TRANSACTION(ttFEE, 101, SetFee, TRANSACTION(ttUNL_MODIFY, 102, UNLModify, Delegation::notDelegatable, uint256{}, - noPriv, ({ {sfUNLModifyDisabling, soeREQUIRED}, {sfLedgerSequence, soeREQUIRED}, diff --git a/src/libxrpl/ledger/ApplyStateTable.cpp b/src/libxrpl/ledger/ApplyStateTable.cpp index 7b041939d4..e6eb4a683d 100644 --- a/src/libxrpl/ledger/ApplyStateTable.cpp +++ b/src/libxrpl/ledger/ApplyStateTable.cpp @@ -17,681 +17,98 @@ */ //============================================================================== -#include -#include -#include -#include -#include -#include +#ifndef RIPPLE_LEDGER_APPLYVIEWIMPL_H_INCLUDED +#define RIPPLE_LEDGER_APPLYVIEWIMPL_H_INCLUDED + +#include +#include + +#include +#include namespace ripple { -namespace detail { -void -ApplyStateTable::apply(RawView& to) const +/** Editable, discardable view that can build metadata for one tx. + + Iteration of the tx map is delegated to the base. + + @note Presented as ApplyView to clients. +*/ +class ApplyViewImpl final : public detail::ApplyViewBase { - to.rawDestroyXRP(dropsDestroyed_); - for (auto const& item : items_) +public: + ApplyViewImpl() = delete; + ApplyViewImpl(ApplyViewImpl const&) = delete; + ApplyViewImpl& + operator=(ApplyViewImpl&&) = delete; + ApplyViewImpl& + operator=(ApplyViewImpl const&) = delete; + + ApplyViewImpl(ApplyViewImpl&&) = default; + ApplyViewImpl(ReadView const* base, ApplyFlags flags); + + /** Apply the transaction. + + After a call to `apply`, the only valid + operation on this object is to call the + destructor. + */ + std::optional + apply( + OpenView& to, + STTx const& tx, + TER ter, + std::optional parentBatchId, + bool isDryRun, + beast::Journal j); + + /** Set the amount of currency delivered. + + This value is used when generating metadata + for payments, to set the DeliveredAmount field. + If the amount is not specified, the field is + excluded from the resulting metadata. + */ + void + deliver(STAmount const& amount) { - auto const& sle = item.second.second; - switch (item.second.first) - { - case Action::cache: - break; - case Action::erase: - to.rawErase(sle); - break; - case Action::insert: - to.rawInsert(sle); - break; - case Action::modify: - to.rawReplace(sle); - break; - }; - } -} - -std::size_t -ApplyStateTable::size() const -{ - std::size_t ret = 0; - for (auto& item : items_) - { - switch (item.second.first) - { - case Action::erase: - case Action::insert: - case Action::modify: - ++ret; - default: - break; - } - } - return ret; -} - -void -ApplyStateTable::visit( - ReadView const& to, - std::function const& before, - std::shared_ptr const& after)> const& func) const -{ - for (auto& item : items_) - { - switch (item.second.first) - { - case Action::erase: - func( - item.first, - true, - to.read(keylet::unchecked(item.first)), - item.second.second); - break; - - case Action::insert: - func(item.first, false, nullptr, item.second.second); - break; - - case Action::modify: - func( - item.first, - false, - to.read(keylet::unchecked(item.first)), - item.second.second); - break; - - default: - break; - } - } -} - -std::optional -ApplyStateTable::apply( - OpenView& to, - STTx const& tx, - TER ter, - std::optional const& deliver, - std::optional const& parentBatchId, - bool isDryRun, - beast::Journal j) -{ - // Build metadata and insert - auto const sTx = std::make_shared(); - tx.add(*sTx); - std::shared_ptr sMeta; - std::optional metadata; - if (!to.open() || isDryRun) - { - TxMeta meta(tx.getTransactionID(), to.seq(), parentBatchId); - - if (deliver) - meta.setDeliveredAmount(*deliver); - - Mods newMod; - for (auto& item : items_) - { - SField const* type; - switch (item.second.first) - { - default: - case Action::cache: - continue; - case Action::erase: - type = &sfDeletedNode; - break; - case Action::insert: - type = &sfCreatedNode; - break; - case Action::modify: - type = &sfModifiedNode; - break; - } - auto const origNode = to.read(keylet::unchecked(item.first)); - auto curNode = item.second.second; - if ((type == &sfModifiedNode) && (*curNode == *origNode)) - continue; - std::uint16_t nodeType = curNode - ? curNode->getFieldU16(sfLedgerEntryType) - : origNode->getFieldU16(sfLedgerEntryType); - meta.setAffectedNode(item.first, *type, nodeType); - if (type == &sfDeletedNode) - { - XRPL_ASSERT( - origNode && curNode, - "ripple::detail::ApplyStateTable::apply : valid nodes for " - "deletion"); - threadOwners(to, meta, origNode, newMod, j); - - STObject prevs(sfPreviousFields); - for (auto const& obj : *origNode) - { - // go through the original node for - // modified fields saved on modification - if (obj.getFName().shouldMeta(SField::sMD_ChangeOrig) && - !curNode->hasMatchingEntry(obj)) - prevs.emplace_back(obj); - } - - if (!prevs.empty()) - meta.getAffectedNode(item.first) - .emplace_back(std::move(prevs)); - - STObject finals(sfFinalFields); - for (auto const& obj : *curNode) - { - // go through the final node for final fields - if (obj.getFName().shouldMeta( - SField::sMD_Always | SField::sMD_DeleteFinal)) - finals.emplace_back(obj); - } - - if (!finals.empty()) - meta.getAffectedNode(item.first) - .emplace_back(std::move(finals)); - } - else if (type == &sfModifiedNode) - { - XRPL_ASSERT( - curNode && origNode, - "ripple::detail::ApplyStateTable::apply : valid nodes for " - "modification"); - - if (curNode->isThreadedType( - to.rules())) // thread transaction to node - // item modified - threadItem(meta, curNode); - - STObject prevs(sfPreviousFields); - for (auto const& obj : *origNode) - { - // search the original node for values saved on modify - if (obj.getFName().shouldMeta(SField::sMD_ChangeOrig) && - !curNode->hasMatchingEntry(obj)) - prevs.emplace_back(obj); - } - - if (!prevs.empty()) - meta.getAffectedNode(item.first) - .emplace_back(std::move(prevs)); - - STObject finals(sfFinalFields); - for (auto const& obj : *curNode) - { - // search the final node for values saved always - if (obj.getFName().shouldMeta( - SField::sMD_Always | SField::sMD_ChangeNew)) - finals.emplace_back(obj); - } - - if (!finals.empty()) - meta.getAffectedNode(item.first) - .emplace_back(std::move(finals)); - } - else if (type == &sfCreatedNode) // if created, thread to owner(s) - { - XRPL_ASSERT( - curNode && !origNode, - "ripple::detail::ApplyStateTable::apply : valid nodes for " - "creation"); - threadOwners(to, meta, curNode, newMod, j); - - if (curNode->isThreadedType( - to.rules())) // always thread to self - threadItem(meta, curNode); - - STObject news(sfNewFields); - for (auto const& obj : *curNode) - { - // save non-default values - if (!obj.isDefault() && - obj.getFName().shouldMeta( - SField::sMD_Create | SField::sMD_Always)) - news.emplace_back(obj); - } - - if (!news.empty()) - meta.getAffectedNode(item.first) - .emplace_back(std::move(news)); - } - else - { - UNREACHABLE( - "ripple::detail::ApplyStateTable::apply : unsupported " - "operation type"); - } - } - - if (!isDryRun) - { - // add any new modified nodes to the modification set - for (auto const& mod : newMod) - to.rawReplace(mod.second); - } - - sMeta = std::make_shared(); - meta.addRaw(*sMeta, ter, to.txCount()); - - // VFALCO For diagnostics do we want to show - // metadata even when the base view is open? - JLOG(j.trace()) << "metadata " << meta.getJson(JsonOptions::none); - - metadata = meta; + deliver_ = amount; } - if (!isDryRun) + void + setGasUsed(std::optional const gasUsed) { - to.rawTxInsert(tx.getTransactionID(), sTx, sMeta); - apply(to); + gasUsed_ = gasUsed; } - return metadata; -} -//--- - -bool -ApplyStateTable::exists(ReadView const& base, Keylet const& k) const -{ - auto const iter = items_.find(k.key); - if (iter == items_.end()) - return base.exists(k); - auto const& item = iter->second; - auto const& sle = item.second; - switch (item.first) + void + setWasmReturnCode(std::int32_t const wasmReturnCode) { - case Action::erase: - return false; - case Action::cache: - case Action::insert: - case Action::modify: - break; + wasmReturnCode_ = wasmReturnCode; } - if (!k.check(*sle)) - return false; - return true; -} -auto -ApplyStateTable::succ( - ReadView const& base, - key_type const& key, - std::optional const& last) const -> std::optional -{ - std::optional next = key; - items_t::const_iterator iter; - // Find base successor that is - // not also deleted in our list - do - { - next = base.succ(*next, last); - if (!next) - break; - iter = items_.find(*next); - } while (iter != items_.end() && iter->second.first == Action::erase); - // Find non-deleted successor in our list - for (iter = items_.upper_bound(key); iter != items_.end(); ++iter) - { - if (iter->second.first != Action::erase) - { - // Found both, return the lower key - if (!next || next > iter->first) - next = iter->first; - break; - } - } - // Nothing in our list, return - // what we got from the parent. - if (last && next >= last) - return std::nullopt; - return next; -} + /** Get the number of modified entries + */ + std::size_t + size(); -std::shared_ptr -ApplyStateTable::read(ReadView const& base, Keylet const& k) const -{ - auto const iter = items_.find(k.key); - if (iter == items_.end()) - return base.read(k); - auto const& item = iter->second; - auto const& sle = item.second; - switch (item.first) - { - case Action::erase: - return nullptr; - case Action::cache: - case Action::insert: - case Action::modify: - break; - }; - if (!k.check(*sle)) - return nullptr; - return sle; -} + /** Visit modified entries + */ + void + visit( + OpenView& target, + std::function const& before, + std::shared_ptr const& after)> const& func); -std::shared_ptr -ApplyStateTable::peek(ReadView const& base, Keylet const& k) -{ - auto iter = items_.lower_bound(k.key); - if (iter == items_.end() || iter->first != k.key) - { - auto const sle = base.read(k); - if (!sle) - return nullptr; - // Make our own copy - using namespace std; - iter = items_.emplace_hint( - iter, - piecewise_construct, - forward_as_tuple(sle->key()), - forward_as_tuple(Action::cache, make_shared(*sle))); - return iter->second.second; - } - auto const& item = iter->second; - auto const& sle = item.second; - switch (item.first) - { - case Action::erase: - return nullptr; - case Action::cache: - case Action::insert: - case Action::modify: - break; - }; - if (!k.check(*sle)) - return nullptr; - return sle; -} +private: + std::optional deliver_; + std::optional gasUsed_; + std::optional wasmReturnCode_; +}; -void -ApplyStateTable::erase(ReadView const& base, std::shared_ptr const& sle) -{ - auto const iter = items_.find(sle->key()); - if (iter == items_.end()) - LogicError("ApplyStateTable::erase: missing key"); - auto& item = iter->second; - if (item.second != sle) - LogicError("ApplyStateTable::erase: unknown SLE"); - switch (item.first) - { - case Action::erase: - LogicError("ApplyStateTable::erase: double erase"); - break; - case Action::insert: - items_.erase(iter); - break; - case Action::cache: - case Action::modify: - item.first = Action::erase; - break; - } -} - -void -ApplyStateTable::rawErase(ReadView const& base, std::shared_ptr const& sle) -{ - using namespace std; - auto const result = items_.emplace( - piecewise_construct, - forward_as_tuple(sle->key()), - forward_as_tuple(Action::erase, sle)); - if (result.second) - return; - auto& item = result.first->second; - switch (item.first) - { - case Action::erase: - LogicError("ApplyStateTable::rawErase: double erase"); - break; - case Action::insert: - items_.erase(result.first); - break; - case Action::cache: - case Action::modify: - item.first = Action::erase; - item.second = sle; - break; - } -} - -void -ApplyStateTable::insert(ReadView const& base, std::shared_ptr const& sle) -{ - auto const iter = items_.lower_bound(sle->key()); - if (iter == items_.end() || iter->first != sle->key()) - { - using namespace std; - items_.emplace_hint( - iter, - piecewise_construct, - forward_as_tuple(sle->key()), - forward_as_tuple(Action::insert, sle)); - return; - } - auto& item = iter->second; - switch (item.first) - { - case Action::cache: - LogicError("ApplyStateTable::insert: already cached"); - case Action::insert: - LogicError("ApplyStateTable::insert: already inserted"); - case Action::modify: - LogicError("ApplyStateTable::insert: already modified"); - case Action::erase: - break; - } - item.first = Action::modify; - item.second = sle; -} - -void -ApplyStateTable::replace(ReadView const& base, std::shared_ptr const& sle) -{ - auto const iter = items_.lower_bound(sle->key()); - if (iter == items_.end() || iter->first != sle->key()) - { - using namespace std; - items_.emplace_hint( - iter, - piecewise_construct, - forward_as_tuple(sle->key()), - forward_as_tuple(Action::modify, sle)); - return; - } - auto& item = iter->second; - switch (item.first) - { - case Action::erase: - LogicError("ApplyStateTable::replace: already erased"); - case Action::cache: - item.first = Action::modify; - break; - case Action::insert: - case Action::modify: - break; - } - item.second = sle; -} - -void -ApplyStateTable::update(ReadView const& base, std::shared_ptr const& sle) -{ - auto const iter = items_.find(sle->key()); - if (iter == items_.end()) - LogicError("ApplyStateTable::update: missing key"); - auto& item = iter->second; - if (item.second != sle) - LogicError("ApplyStateTable::update: unknown SLE"); - switch (item.first) - { - case Action::erase: - LogicError("ApplyStateTable::update: erased"); - break; - case Action::cache: - item.first = Action::modify; - break; - case Action::insert: - case Action::modify: - break; - }; -} - -void -ApplyStateTable::destroyXRP(XRPAmount const& fee) -{ - dropsDestroyed_ += fee; -} - -//------------------------------------------------------------------------------ - -// Insert this transaction to the SLE's threading list -void -ApplyStateTable::threadItem(TxMeta& meta, std::shared_ptr const& sle) -{ - key_type prevTxID; - LedgerIndex prevLgrID; - - if (!sle->thread(meta.getTxID(), meta.getLgrSeq(), prevTxID, prevLgrID)) - return; - - if (!prevTxID.isZero()) - { - auto& node = meta.getAffectedNode(sle, sfModifiedNode); - - if (node.getFieldIndex(sfPreviousTxnID) == -1) - { - XRPL_ASSERT( - node.getFieldIndex(sfPreviousTxnLgrSeq) == -1, - "ripple::ApplyStateTable::threadItem : previous ledger is not " - "set"); - node.setFieldH256(sfPreviousTxnID, prevTxID); - node.setFieldU32(sfPreviousTxnLgrSeq, prevLgrID); - } - - XRPL_ASSERT( - node.getFieldH256(sfPreviousTxnID) == prevTxID, - "ripple::ApplyStateTable::threadItem : previous transaction is a " - "match"); - XRPL_ASSERT( - node.getFieldU32(sfPreviousTxnLgrSeq) == prevLgrID, - "ripple::ApplyStateTable::threadItem : previous ledger is a match"); - } -} - -std::shared_ptr -ApplyStateTable::getForMod( - ReadView const& base, - key_type const& key, - Mods& mods, - beast::Journal j) -{ - { - auto miter = mods.find(key); - if (miter != mods.end()) - { - XRPL_ASSERT( - miter->second, - "ripple::ApplyStateTable::getForMod : non-null result"); - return miter->second; - } - } - { - auto iter = items_.find(key); - if (iter != items_.end()) - { - auto const& item = iter->second; - if (item.first == Action::erase) - { - // The Destination of an Escrow or a PayChannel may have been - // deleted. In that case the account we're threading to will - // not be found and it is appropriate to return a nullptr. - JLOG(j.warn()) << "Trying to thread to deleted node"; - return nullptr; - } - if (item.first != Action::cache) - return item.second; - - // If it's only cached, then the node is being modified only by - // metadata; fall through and track it in the mods table. - } - } - auto c = base.read(keylet::unchecked(key)); - if (!c) - { - // The Destination of an Escrow or a PayChannel may have been - // deleted. In that case the account we're threading to will - // not be found and it is appropriate to return a nullptr. - JLOG(j.warn()) << "ApplyStateTable::getForMod: key not found"; - return nullptr; - } - auto sle = std::make_shared(*c); - mods.emplace(key, sle); - return sle; -} - -void -ApplyStateTable::threadTx( - ReadView const& base, - TxMeta& meta, - AccountID const& to, - Mods& mods, - beast::Journal j) -{ - auto const sle = getForMod(base, keylet::account(to).key, mods, j); - if (!sle) - { - // The Destination of an Escrow or PayChannel may have been deleted. - // In that case the account we are threading to will not be found. - // So this logging is just a warning. - JLOG(j.warn()) << "Threading to non-existent account: " << toBase58(to); - return; - } - // threadItem only applied to AccountRoot - XRPL_ASSERT( - sle->isThreadedType(base.rules()), - "ripple::ApplyStateTable::threadTx : SLE is threaded"); - threadItem(meta, sle); -} - -void -ApplyStateTable::threadOwners( - ReadView const& base, - TxMeta& meta, - std::shared_ptr const& sle, - Mods& mods, - beast::Journal j) -{ - LedgerEntryType const ledgerType{sle->getType()}; - switch (ledgerType) - { - case ltACCOUNT_ROOT: { - // Nothing to do - break; - } - case ltRIPPLE_STATE: { - threadTx(base, meta, (*sle)[sfLowLimit].getIssuer(), mods, j); - threadTx(base, meta, (*sle)[sfHighLimit].getIssuer(), mods, j); - break; - } - default: { - // If sfAccount is present, thread to that account - if (auto const optSleAcct{(*sle)[~sfAccount]}) - threadTx(base, meta, *optSleAcct, mods, j); - - // Don't thread a check's sfDestination unless the amendment is - // enabled - if (ledgerType == ltCHECK && - !base.rules().enabled(fixCheckThreading)) - break; - - // If sfDestination is present, thread to that account - if (auto const optSleDest{(*sle)[~sfDestination]}) - threadTx(base, meta, *optSleDest, mods, j); - } - } -} - -} // namespace detail } // namespace ripple + +#endif diff --git a/src/libxrpl/ledger/ApplyViewImpl.cpp b/src/libxrpl/ledger/ApplyViewImpl.cpp index 9429bcee6e..e6eb4a683d 100644 --- a/src/libxrpl/ledger/ApplyViewImpl.cpp +++ b/src/libxrpl/ledger/ApplyViewImpl.cpp @@ -17,43 +17,98 @@ */ //============================================================================== -#include +#ifndef RIPPLE_LEDGER_APPLYVIEWIMPL_H_INCLUDED +#define RIPPLE_LEDGER_APPLYVIEWIMPL_H_INCLUDED + +#include +#include + +#include +#include namespace ripple { -ApplyViewImpl::ApplyViewImpl(ReadView const* base, ApplyFlags flags) - : ApplyViewBase(base, flags) -{ -} +/** Editable, discardable view that can build metadata for one tx. -std::optional -ApplyViewImpl::apply( - OpenView& to, - STTx const& tx, - TER ter, - std::optional parentBatchId, - bool isDryRun, - beast::Journal j) -{ - return items_.apply(to, tx, ter, deliver_, parentBatchId, isDryRun, j); -} + Iteration of the tx map is delegated to the base. -std::size_t -ApplyViewImpl::size() + @note Presented as ApplyView to clients. +*/ +class ApplyViewImpl final : public detail::ApplyViewBase { - return items_.size(); -} +public: + ApplyViewImpl() = delete; + ApplyViewImpl(ApplyViewImpl const&) = delete; + ApplyViewImpl& + operator=(ApplyViewImpl&&) = delete; + ApplyViewImpl& + operator=(ApplyViewImpl const&) = delete; -void -ApplyViewImpl::visit( - OpenView& to, - std::function const& before, - std::shared_ptr const& after)> const& func) -{ - items_.visit(to, func); -} + ApplyViewImpl(ApplyViewImpl&&) = default; + ApplyViewImpl(ReadView const* base, ApplyFlags flags); + + /** Apply the transaction. + + After a call to `apply`, the only valid + operation on this object is to call the + destructor. + */ + std::optional + apply( + OpenView& to, + STTx const& tx, + TER ter, + std::optional parentBatchId, + bool isDryRun, + beast::Journal j); + + /** Set the amount of currency delivered. + + This value is used when generating metadata + for payments, to set the DeliveredAmount field. + If the amount is not specified, the field is + excluded from the resulting metadata. + */ + void + deliver(STAmount const& amount) + { + 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 + size(); + + /** Visit modified entries + */ + void + visit( + OpenView& target, + std::function const& before, + std::shared_ptr const& after)> const& func); + +private: + std::optional deliver_; + std::optional gasUsed_; + std::optional wasmReturnCode_; +}; } // namespace ripple + +#endif diff --git a/src/libxrpl/protocol/TER.cpp b/src/libxrpl/protocol/TER.cpp index 5d62643a13..35cf23d50e 100644 --- a/src/libxrpl/protocol/TER.cpp +++ b/src/libxrpl/protocol/TER.cpp @@ -128,6 +128,7 @@ transResults() MAKE_ERROR(tecPSEUDO_ACCOUNT, "This operation is not allowed against a pseudo-account."), MAKE_ERROR(tecPRECISION_LOSS, "The amounts used by the transaction cannot interact."), MAKE_ERROR(tecNO_DELEGATE_PERMISSION, "Delegated account lacks permission to perform this transaction."), + MAKE_ERROR(tecWASM_REJECTED, "The custom WASM code that was run rejected your transaction."), MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."), MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."), @@ -151,6 +152,8 @@ transResults() MAKE_ERROR(tefNO_TICKET, "Ticket is not in ledger."), MAKE_ERROR(tefNFTOKEN_IS_NOT_TRANSFERABLE, "The specified NFToken is not transferable."), MAKE_ERROR(tefINVALID_LEDGER_FIX_TYPE, "The LedgerFixType field has an invalid value."), + MAKE_ERROR(tefNO_WASM, "There is no WASM code to run, but a WASM-specific field was included."), + MAKE_ERROR(tefWASM_FIELD_NOT_INCLUDED, "WASM code requires a field to be included that was not included."), MAKE_ERROR(telLOCAL_ERROR, "Local failure."), MAKE_ERROR(telBAD_DOMAIN, "Domain too long."), diff --git a/src/libxrpl/protocol/TxMeta.cpp b/src/libxrpl/protocol/TxMeta.cpp index 2343a6a794..923059e111 100644 --- a/src/libxrpl/protocol/TxMeta.cpp +++ b/src/libxrpl/protocol/TxMeta.cpp @@ -56,9 +56,12 @@ TxMeta::TxMeta( if (obj.isFieldPresent(sfDeliveredAmount)) setDeliveredAmount(obj.getFieldAmount(sfDeliveredAmount)); - if (obj.isFieldPresent(sfParentBatchID)) setParentBatchId(obj.getFieldH256(sfParentBatchID)); + if (obj.isFieldPresent(sfGasUsed)) + setGasUsed(obj.getFieldU32(sfGasUsed)); + if (obj.isFieldPresent(sfWasmReturnCode)) + setWasmReturnCode(obj.getFieldI32(sfWasmReturnCode)); } TxMeta::TxMeta(uint256 const& txid, std::uint32_t ledger, STObject const& obj) @@ -82,6 +85,12 @@ TxMeta::TxMeta(uint256 const& txid, std::uint32_t ledger, STObject const& obj) if (obj.isFieldPresent(sfParentBatchID)) setParentBatchId(obj.getFieldH256(sfParentBatchID)); + + if (obj.isFieldPresent(sfGasUsed)) + setGasUsed(obj.getFieldU32(sfGasUsed)); + + if (obj.isFieldPresent(sfWasmReturnCode)) + setWasmReturnCode(obj.getFieldI32(sfWasmReturnCode)); } TxMeta::TxMeta(uint256 const& txid, std::uint32_t ledger, Blob const& vec) @@ -97,15 +106,11 @@ TxMeta::TxMeta( { } -TxMeta::TxMeta( - uint256 const& transactionID, - std::uint32_t ledger, - std::optional parentBatchId) +TxMeta::TxMeta(uint256 const& transactionID, std::uint32_t ledger) : mTransactionID(transactionID) , mLedger(ledger) , mIndex(static_cast(-1)) , mResult(255) - , mParentBatchId(parentBatchId) , mNodes(sfAffectedNodes) { mNodes.reserve(32); @@ -253,9 +258,12 @@ TxMeta::getAsObject() const metaData.emplace_back(mNodes); if (hasDeliveredAmount()) metaData.setFieldAmount(sfDeliveredAmount, getDeliveredAmount()); - if (hasParentBatchId()) metaData.setFieldH256(sfParentBatchID, getParentBatchId()); + if (hasGasUsed()) + metaData.setFieldU32(sfGasUsed, getGasUsed()); + if (hasWasmReturnCode()) + metaData.setFieldI32(sfWasmReturnCode, getWasmReturnCode()); return metaData; } diff --git a/src/test/app/Escrow_test.cpp b/src/test/app/Escrow_test.cpp index cea3a835a6..74babf797c 100644 --- a/src/test/app/Escrow_test.cpp +++ b/src/test/app/Escrow_test.cpp @@ -17,11 +17,13 @@ */ //============================================================================== +#include #include #include +#include +#include -#include #include #include #include @@ -253,14 +255,6 @@ struct Escrow_test : public beast::unit_test::suite BEAST_EXPECT(sle); BEAST_EXPECT((*sle)[sfSourceTag] == 1); BEAST_EXPECT((*sle)[sfDestinationTag] == 2); - if (features[fixIncludeKeyletFields]) - { - BEAST_EXPECT((*sle)[sfSequence] == seq); - } - else - { - BEAST_EXPECT(!sle->isFieldPresent(sfSequence)); - } } void @@ -302,7 +296,7 @@ struct Escrow_test : public beast::unit_test::suite { testcase("Implied Finish Time (without fix1571)"); - Env env(*this, testable_amendments() - fix1571); + Env env(*this, features - fix1571); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob", "carol"); env.close(); @@ -1559,7 +1553,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"; @@ -1701,6 +1695,763 @@ struct Escrow_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(); + } + + { + // 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(); + } + + 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 + 3; + 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 = 66; + 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 + 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) == 38'571, + 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(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) == 16, + 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 = 137'596; + 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) == 16, + std::to_string(env.ownerCount(alice))); + } + } + } + void testWithFeats(FeatureBitset features) { @@ -1716,6 +2467,13 @@ struct Escrow_test : public beast::unit_test::suite testConsequences(features); testEscrowWithTickets(features); testCredentials(features); + testCreateFinishFunctionPreflight(features); + testFinishWasmFailures(features); + testFinishFunction(features); + + // TODO: Update module with new host functions + testAllHostFunctions(features); + testKeyletHostFunctions(features); } public: @@ -1726,8 +2484,7 @@ public: FeatureBitset const all{testable_amendments()}; testWithFeats(all); testWithFeats(all - featureTokenEscrow); - testTags(all - fixIncludeKeyletFields); - } + }; }; BEAST_DEFINE_TESTSUITE(Escrow, app, ripple); diff --git a/src/test/app/TestHostFunctions.h b/src/test/app/TestHostFunctions.h index ffa0bfb895..ae4db59b0d 100644 --- a/src/test/app/TestHostFunctions.h +++ b/src/test/app/TestHostFunctions.h @@ -24,8 +24,7 @@ #include #include #include - -#include +#include namespace ripple { diff --git a/src/test/app/Wasm_test.cpp b/src/test/app/Wasm_test.cpp index bd4f7d4380..a48e16f1eb 100644 --- a/src/test/app/Wasm_test.cpp +++ b/src/test/app/Wasm_test.cpp @@ -649,7 +649,7 @@ struct Wasm_test : public beast::unit_test::suite Bytes const wasm(wasmStr.begin(), wasmStr.end()); TestHostFunctions hfs(env, 0); - auto const allowance = 148'406; + auto const allowance = 121'895; auto re = runEscrowWasm( wasm, ESCROW_FUNCTION_NAME, {}, &hfs, allowance, env.journal); @@ -709,7 +709,7 @@ struct Wasm_test : public beast::unit_test::suite testWasmSha(); testWasmB58(); - // running too long + // runing too long // testWasmSP1Verifier(); testWasmBG16Verifier(); diff --git a/src/test/jtx/escrow.h b/src/test/jtx/escrow.h index 483db578b0..8e21890d98 100644 --- a/src/test/jtx/escrow.h +++ b/src/test/jtx/escrow.h @@ -22,7 +22,6 @@ #include #include -#include #include #include @@ -95,14 +94,156 @@ std::array const cb3 = { 0x57, 0x0D, 0x15, 0x85, 0x8B, 0xD4, 0x81, 0x01, 0x04}}; /** Set the "FinishAfter" time tag on a JTx */ -auto const finish_time = JTxFieldWrapper(sfFinishAfter); +struct finish_time +{ +private: + NetClock::time_point value_; + +public: + explicit finish_time(NetClock::time_point const& value) : value_(value) + { + } + + void + operator()(Env&, JTx& jt) const + { + jt.jv[sfFinishAfter.jsonName] = value_.time_since_epoch().count(); + } +}; /** Set the "CancelAfter" time tag on a JTx */ -auto const cancel_time = JTxFieldWrapper(sfCancelAfter); +struct cancel_time +{ +private: + NetClock::time_point value_; -auto const condition = JTxFieldWrapper(sfCondition); +public: + explicit cancel_time(NetClock::time_point const& value) : value_(value) + { + } -auto const fulfillment = JTxFieldWrapper(sfFulfillment); + void + operator()(jtx::Env&, jtx::JTx& jt) const + { + jt.jv[sfCancelAfter.jsonName] = value_.time_since_epoch().count(); + } +}; + +struct condition +{ +private: + std::string value_; + +public: + explicit condition(Slice const& cond) : value_(strHex(cond)) + { + } + + template + explicit condition(std::array const& c) + : condition(makeSlice(c)) + { + } + + void + operator()(Env&, JTx& jt) const + { + jt.jv[sfCondition.jsonName] = value_; + } +}; + +struct fulfillment +{ +private: + std::string value_; + +public: + explicit fulfillment(Slice condition) : value_(strHex(condition)) + { + } + + template + explicit fulfillment(std::array f) + : fulfillment(makeSlice(f)) + { + } + + void + operator()(Env&, JTx& jt) const + { + jt.jv[sfFulfillment.jsonName] = value_; + } +}; + +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 diff --git a/src/test/jtx/impl/envconfig.cpp b/src/test/jtx/impl/envconfig.cpp index 624036196d..9a59a2c2cd 100644 --- a/src/test/jtx/impl/envconfig.cpp +++ b/src/test/jtx/impl/envconfig.cpp @@ -33,9 +33,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/xrpld/app/tx/detail/ApplyContext.cpp b/src/xrpld/app/tx/detail/ApplyContext.cpp index 79cbb7f40d..42f6c7ecfa 100644 --- a/src/xrpld/app/tx/detail/ApplyContext.cpp +++ b/src/xrpld/app/tx/detail/ApplyContext.cpp @@ -59,6 +59,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 0344771a43..22b270b847 100644 --- a/src/xrpld/app/tx/detail/ApplyContext.h +++ b/src/xrpld/app/tx/detail/ApplyContext.h @@ -22,9 +22,9 @@ #include #include +#include #include -#include #include #include @@ -106,6 +106,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(); @@ -157,6 +171,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 f1d1db79a0..d0b42e1615 100644 --- a/src/xrpld/app/tx/detail/Escrow.cpp +++ b/src/xrpld/app/tx/detail/Escrow.cpp @@ -17,23 +17,26 @@ */ //============================================================================== +#include #include #include #include +#include +#include #include #include +#include +#include #include #include -#include -#include -#include #include #include #include #include #include +#include namespace ripple { // During an EscrowFinish, the transaction must specify both @@ -81,8 +84,8 @@ constexpr HashRouterFlags SF_CF_VALID = HashRouterFlags::PRIVATE6; TxConsequences EscrowCreate::makeTxConsequences(PreflightContext const& ctx) { - auto const amount = ctx.tx[sfAmount]; - return TxConsequences{ctx.tx, isXRP(amount) ? amount.xrp() : beast::zero}; + return TxConsequences{ + ctx.tx, isXRP(ctx.tx[sfAmount]) ? ctx.tx[sfAmount].xrp() : beast::zero}; } template @@ -118,9 +121,29 @@ 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; +} + NotTEC EscrowCreate::preflight(PreflightContext const& ctx) { + if (ctx.tx.isFieldPresent(sfFinishFunction) && + !ctx.rules.enabled(featureSmartEscrow)) + { + JLOG(ctx.j.debug()) << "SmartEscrow not enabled"; + return temDISABLED; + } + if (ctx.rules.enabled(fix1543) && ctx.tx.getFlags() & tfUniversalMask) return temINVALID_FLAG; @@ -157,14 +180,23 @@ 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; + if (ctx.rules.enabled(fix1571)) { // 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]) @@ -189,6 +221,27 @@ EscrowCreate::preflight(PreflightContext const& ctx) return temDISABLED; } + 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 preflight2(ctx); } @@ -451,6 +504,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() { @@ -494,9 +558,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; @@ -537,11 +603,8 @@ EscrowCreate::doApply() (*slep)[~sfCancelAfter] = ctx_.tx[~sfCancelAfter]; (*slep)[~sfFinishAfter] = ctx_.tx[~sfFinishAfter]; (*slep)[~sfDestinationTag] = ctx_.tx[~sfDestinationTag]; - - if (ctx_.view().rules().enabled(fixIncludeKeyletFields)) - { - (*slep)[sfSequence] = ctx_.tx.getSeqValue(); - } + (*slep)[~sfFinishFunction] = ctx_.tx[~sfFinishFunction]; + (*slep)[~sfData] = ctx_.tx[~sfData]; if (ctx_.view().rules().enabled(featureTokenEscrow) && !isXRP(amount)) { @@ -604,7 +667,8 @@ EscrowCreate::doApply() } // increment owner count - adjustOwnerCount(ctx_.view(), sle, 1, ctx_.journal); + // TODO: determine actual reserve based on FinishFunction size + adjustOwnerCount(ctx_.view(), sle, reserveToAdd, ctx_.journal); ctx_.view().update(sle); return tesSUCCESS; } @@ -639,6 +703,13 @@ EscrowFinish::preflight(PreflightContext const& ctx) !ctx.rules.enabled(featureCredentials)) return temDISABLED; + if (ctx.tx.isFieldPresent(sfComputationAllowance) && + !ctx.rules.enabled(featureSmartEscrow)) + { + JLOG(ctx.j.debug()) << "SmartEscrow not enabled"; + return temDISABLED; + } + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; @@ -648,7 +719,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; + } // Verify the transaction signature. If it doesn't work // then don't do any more work. @@ -677,6 +751,20 @@ EscrowFinish::preflight(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; @@ -693,7 +781,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; } @@ -773,25 +868,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; @@ -1012,13 +1134,8 @@ escrowUnlockApplyHelper( // compute balance to transfer finalAmt = amount.value() - xferFee; } - return rippleUnlockEscrowMPT( - view, - sender, - receiver, - finalAmt, - view.rules().enabled(fixTokenEscrowV1) ? amount : finalAmt, - journal); + + return rippleUnlockEscrowMPT(view, sender, receiver, finalAmt, journal); } TER @@ -1028,7 +1145,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; @@ -1043,11 +1161,17 @@ EscrowFinish::doApply() // Too soon: can't execute before the finish time if ((*slep)[~sfFinishAfter] && !after(now, (*slep)[sfFinishAfter])) + { + JLOG(j_.debug()) << "Too soon"; return tecNO_PERMISSION; + } // Too late: can't execute after the cancel time if ((*slep)[~sfCancelAfter] && after(now, (*slep)[sfCancelAfter])) + { + JLOG(j_.debug()) << "Too late"; return tecNO_PERMISSION; + } } else { @@ -1055,13 +1179,36 @@ EscrowFinish::doApply() if ((*slep)[~sfFinishAfter] && ctx_.view().info().parentCloseTime.time_since_epoch().count() <= (*slep)[sfFinishAfter]) + { + JLOG(j_.debug()) << "Too soon?"; return tecNO_PERMISSION; + } // Too late? if ((*slep)[~sfCancelAfter] && ctx_.view().info().parentCloseTime.time_since_epoch().count() <= (*slep)[sfCancelAfter]) + { + JLOG(j_.debug()) << "Too late?"; 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 (ctx_.view().rules().enabled(featureDepositAuth)) + { + if (auto err = verifyDepositPreauth( + ctx_.tx, ctx_.view(), account_, destID, sled, ctx_.journal); + !isTesSuccess(err)) + return err; + } } // Check cryptocondition fulfillment @@ -1111,18 +1258,65 @@ 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(featureDepositAuth)) + if (!ctx_.view().rules().enabled(featureSmartEscrow)) { - if (auto err = verifyDepositPreauth( - ctx_.tx, ctx_.view(), account_, destID, sled, ctx_.journal); - !isTesSuccess(err)) - return err; + // NOTE: Escrow payments cannot be used to fund accounts. + if (!sled) + return tecNO_DST; + + if (ctx_.view().rules().enabled(featureDepositAuth)) + { + 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)); + } + + if (re.has_value()) + { + auto reValue = re.value().result; + ctx_.setWasmReturnCode(reValue); + // TODO: better error handling for this conversion + ctx_.setGasUsed(static_cast(re.value().cost)); + JLOG(j_.debug()) << "WASM Success: " + std::to_string(reValue) + << ", cost: " << re.value().cost; + if (reValue <= 0) + { + return tecWASM_REJECTED; + } + } + else + { + JLOG(j_.debug()) << "WASM Failure: " + transHuman(re.error()); + return re.error(); + } } AccountID const account = (*slep)[sfAccount]; @@ -1195,9 +1389,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 @@ -1408,7 +1605,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 2225c94f16..1573dae40e 100644 --- a/src/xrpld/app/tx/detail/Escrow.h +++ b/src/xrpld/app/tx/detail/Escrow.h @@ -36,6 +36,9 @@ public: 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 fd396e4556..1104368b7c 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -18,6 +18,7 @@ //============================================================================== #include +#include #include #include #include @@ -25,12 +26,11 @@ #include #include #include +#include #include #include #include -#include -#include #include #include #include @@ -206,10 +206,7 @@ preflight2(PreflightContext const& ctx) //------------------------------------------------------------------------------ Transactor::Transactor(ApplyContext& ctx) - : ctx_(ctx) - , sink_(ctx.journal, to_short_string(ctx.tx.getTransactionID()) + " ") - , j_(sink_) - , account_(ctx.tx.getAccountID(sfAccount)) + : ctx_(ctx), j_(ctx.journal), account_(ctx.tx.getAccountID(sfAccount)) { } @@ -967,6 +964,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, @@ -1124,6 +1137,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); @@ -1136,13 +1150,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, @@ -1151,7 +1168,9 @@ Transactor::operator()() doNFTokenOffers, &expiredNFTokenOffers, doCredentials, - &expiredCredentials]( + &expiredCredentials, + doWasmData, + &modifiedWasmObjects]( uint256 const& index, bool isDelete, std::shared_ptr const& before, @@ -1186,6 +1205,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))); + } }); } @@ -1215,6 +1241,10 @@ Transactor::operator()() removeExpiredCredentials( view(), expiredCredentials, ctx_.app.journal("View")); + if (result == tecWASM_REJECTED) + modifyWasmDataFields( + view(), modifiedWasmObjects, ctx_.app.journal("View")); + applied = isTecClaim(result); }