diff --git a/include/xrpl/ledger/ApplyViewImpl.h b/include/xrpl/ledger/ApplyViewImpl.h index c1e9ccd3598..8a8b9b4e866 100644 --- a/include/xrpl/ledger/ApplyViewImpl.h +++ b/include/xrpl/ledger/ApplyViewImpl.h @@ -55,6 +55,18 @@ class ApplyViewImpl final : public detail::ApplyViewBase deliver_ = amount; } + void + setGasUsed(std::optional const gasUsed) + { + gasUsed_ = gasUsed; + } + + void + setWasmReturnCode(std::int32_t const wasmReturnCode) + { + wasmReturnCode_ = wasmReturnCode; + } + /** Get the number of modified entries */ std::size_t @@ -73,6 +85,8 @@ class ApplyViewImpl final : public detail::ApplyViewBase 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 887e2e77701..a5b1e549f05 100644 --- a/include/xrpl/ledger/detail/ApplyStateTable.h +++ b/include/xrpl/ledger/detail/ApplyStateTable.h @@ -53,6 +53,8 @@ class ApplyStateTable TER ter, std::optional const& deliver, std::optional const& parentBatchId, + std::optional const& gasUsed, + std::optional const& wasmReturnCode, bool isDryRun, beast::Journal j); diff --git a/include/xrpl/protocol/Indexes.h b/include/xrpl/protocol/Indexes.h index b48ed64d713..d88621b697f 100644 --- a/include/xrpl/protocol/Indexes.h +++ b/include/xrpl/protocol/Indexes.h @@ -212,6 +212,12 @@ page(Keylet const& root, std::uint64_t index = 0) noexcept Keylet escrow(AccountID const& src, std::uint32_t seq) noexcept; +inline Keylet +escrow(uint256 const& key) noexcept +{ + return {ltESCROW, key}; +} + /** A PaymentChannel */ Keylet payChan(AccountID const& src, AccountID const& dst, std::uint32_t seq) noexcept; diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index f31122db2b5..829a76ce558 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -168,6 +168,8 @@ enum TEFcodes : TERUnderlyingType { tefNO_TICKET, tefNFTOKEN_IS_NOT_TRANSFERABLE, tefINVALID_LEDGER_FIX_TYPE, + tefNO_WASM, + tefWASM_FIELD_NOT_INCLUDED, }; //------------------------------------------------------------------------------ @@ -349,6 +351,7 @@ enum TECcodes : TERUnderlyingType { // backward compatibility with historical data on non-prod networks, can be // reclaimed after those networks reset. tecNO_DELEGATE_PERMISSION = 198, + tecWASM_REJECTED = 199, }; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/TxMeta.h b/include/xrpl/protocol/TxMeta.h index 3ab58c9d0a5..0741caee388 100644 --- a/include/xrpl/protocol/TxMeta.h +++ b/include/xrpl/protocol/TxMeta.h @@ -85,6 +85,12 @@ class TxMeta if (obj.isFieldPresent(sfParentBatchID)) parentBatchID_ = obj.getFieldH256(sfParentBatchID); + + if (obj.isFieldPresent(sfGasUsed)) + gasUsed_ = obj.getFieldU32(sfGasUsed); + + if (obj.isFieldPresent(sfWasmReturnCode)) + wasmReturnCode_ = obj.getFieldI32(sfWasmReturnCode); } std::optional const& @@ -105,6 +111,30 @@ class TxMeta parentBatchID_ = id; } + void + setGasUsed(std::optional const gasUsed) + { + gasUsed_ = gasUsed; + } + + std::optional const& + getGasUsed() const + { + return gasUsed_; + } + + void + setWasmReturnCode(std::optional const wasmReturnCode) + { + wasmReturnCode_ = wasmReturnCode; + } + + std::optional const& + getWasmReturnCode() const + { + return wasmReturnCode_; + } + private: uint256 transactionID_; std::uint32_t ledgerSeq_; @@ -113,6 +143,8 @@ class TxMeta std::optional deliveredAmount_; std::optional parentBatchID_; + std::optional gasUsed_; + std::optional wasmReturnCode_; STArray nodes_; }; diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index af0ce527235..f6685241283 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -336,6 +336,8 @@ LEDGER_ENTRY(ltESCROW, 0x0075, Escrow, escrow, ({ {sfCondition, soeOPTIONAL}, {sfCancelAfter, soeOPTIONAL}, {sfFinishAfter, soeOPTIONAL}, + {sfFinishFunction, soeOPTIONAL}, + {sfData, soeOPTIONAL}, {sfSourceTag, soeOPTIONAL}, {sfDestinationTag, soeOPTIONAL}, {sfOwnerNode, soeREQUIRED}, diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 7fb9cc3b8f2..2a0f37490ff 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -99,6 +99,8 @@ TYPED_SFIELD(sfMutableFlags, UINT32, 53) TYPED_SFIELD(sfExtensionComputeLimit, UINT32, 54) TYPED_SFIELD(sfExtensionSizeLimit, UINT32, 55) TYPED_SFIELD(sfGasPrice, UINT32, 56) +TYPED_SFIELD(sfComputationAllowance, UINT32, 57) +TYPED_SFIELD(sfGasUsed, UINT32, 58) // 64-bit integers (common) TYPED_SFIELD(sfIndexNext, UINT64, 1) @@ -192,11 +194,8 @@ TYPED_SFIELD(sfAssetsMaximum, NUMBER, 3) TYPED_SFIELD(sfAssetsTotal, NUMBER, 4) TYPED_SFIELD(sfLossUnrealized, NUMBER, 5) -// int32 -// NOTE: Do not use `sfDummyInt32`. It's so far the only use of INT32 -// in this file and has been defined here for test only. -// TODO: Replace `sfDummyInt32` with actually useful field. -TYPED_SFIELD(sfDummyInt32, INT32, 1) // for tests only +// 32-bit signed (common) +TYPED_SFIELD(sfWasmReturnCode, INT32, 1) // currency amount (common) TYPED_SFIELD(sfAmount, AMOUNT, 1) @@ -226,7 +225,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) @@ -268,6 +267,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 eb2ca0d6ad6..63ee860010e 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -50,11 +50,13 @@ TRANSACTION(ttESCROW_CREATE, 1, EscrowCreate, noPriv, ({ {sfDestination, soeREQUIRED}, + {sfDestinationTag, soeOPTIONAL}, {sfAmount, soeREQUIRED, soeMPTSupported}, {sfCondition, soeOPTIONAL}, {sfCancelAfter, soeOPTIONAL}, {sfFinishAfter, soeOPTIONAL}, - {sfDestinationTag, soeOPTIONAL}, + {sfFinishFunction, soeOPTIONAL}, + {sfData, soeOPTIONAL}, })) /** This transaction type completes an existing escrow. */ @@ -68,6 +70,7 @@ TRANSACTION(ttESCROW_FINISH, 2, EscrowFinish, {sfFulfillment, soeOPTIONAL}, {sfCondition, soeOPTIONAL}, {sfCredentialIDs, soeOPTIONAL}, + {sfComputationAllowance, soeOPTIONAL}, })) diff --git a/src/libxrpl/ledger/ApplyStateTable.cpp b/src/libxrpl/ledger/ApplyStateTable.cpp index c236f0d1b56..a992d72b5f7 100644 --- a/src/libxrpl/ledger/ApplyStateTable.cpp +++ b/src/libxrpl/ledger/ApplyStateTable.cpp @@ -97,6 +97,8 @@ ApplyStateTable::apply( TER ter, std::optional const& deliver, std::optional const& parentBatchId, + std::optional const& gasUsed, + std::optional const& wasmReturnCode, bool isDryRun, beast::Journal j) { @@ -111,6 +113,8 @@ ApplyStateTable::apply( meta.setDeliveredAmount(deliver); meta.setParentBatchID(parentBatchId); + meta.setGasUsed(gasUsed); + meta.setWasmReturnCode(wasmReturnCode); Mods newMod; for (auto& item : items_) diff --git a/src/libxrpl/ledger/ApplyViewImpl.cpp b/src/libxrpl/ledger/ApplyViewImpl.cpp index 90652dd45b2..f204bc9c6a9 100644 --- a/src/libxrpl/ledger/ApplyViewImpl.cpp +++ b/src/libxrpl/ledger/ApplyViewImpl.cpp @@ -16,7 +16,16 @@ ApplyViewImpl::apply( bool isDryRun, beast::Journal j) { - return items_.apply(to, tx, ter, deliver_, parentBatchId, isDryRun, j); + return items_.apply( + to, + tx, + ter, + deliver_, + parentBatchId, + gasUsed_, + wasmReturnCode_, + isDryRun, + j); } std::size_t diff --git a/src/libxrpl/protocol/TER.cpp b/src/libxrpl/protocol/TER.cpp index 71b70363628..401ef872552 100644 --- a/src/libxrpl/protocol/TER.cpp +++ b/src/libxrpl/protocol/TER.cpp @@ -108,6 +108,7 @@ transResults() MAKE_ERROR(tecLIMIT_EXCEEDED, "Limit exceeded."), 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(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."), @@ -131,6 +132,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 ebc1d87b14e..19d4a56ef3d 100644 --- a/src/libxrpl/protocol/TxMeta.cpp +++ b/src/libxrpl/protocol/TxMeta.cpp @@ -211,6 +211,12 @@ TxMeta::getAsObject() const if (parentBatchID_.has_value()) metaData.setFieldH256(sfParentBatchID, *parentBatchID_); + if (gasUsed_.has_value()) + metaData.setFieldU32(sfGasUsed, *gasUsed_); + + if (wasmReturnCode_.has_value()) + metaData.setFieldI32(sfWasmReturnCode, *wasmReturnCode_); + return metaData; } diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index 5a1816ebae4..27c6c1ff3a3 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -7466,7 +7466,7 @@ struct AMM_test : public jtx::AMMTest using namespace test::jtx; auto const testCase = [&](std::string suffix, FeatureBitset features) { - testcase("Fail pseudo-account allocation " + suffix); + testcase("Pseudo-account allocation failure " + suffix); std::string logs; Env env{*this, features, std::make_unique(&logs)}; env.fund(XRP(30'000), gw, alice); diff --git a/src/test/app/EscrowSmart_test.cpp b/src/test/app/EscrowSmart_test.cpp new file mode 100644 index 00000000000..3bff14f9e1f --- /dev/null +++ b/src/test/app/EscrowSmart_test.cpp @@ -0,0 +1,916 @@ +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +namespace ripple { +namespace test { + +struct EscrowSmart_test : public beast::unit_test::suite +{ + void + testCreateFinishFunctionPreflight(FeatureBitset features) + { + testcase("Test preflight checks involving FinishFunction"); + + using namespace jtx; + using namespace std::chrono; + + Account const alice{"alice"}; + Account const carol{"carol"}; + + // Tests whether the ledger index is >= 5 + // getLedgerSqn() >= 5} + static auto wasmHex = ledgerSqnWasmHex; + + { + // featureSmartEscrow disabled + Env env(*this, features - featureSmartEscrow); + env.fund(XRP(5000), alice, carol); + XRPAmount const txnFees = env.current()->fees().base + 1000; + auto escrowCreate = escrow::create(alice, carol, XRP(1000)); + env(escrowCreate, + escrow::finish_function(wasmHex), + escrow::cancel_time(env.now() + 100s), + fee(txnFees), + ter(temDISABLED)); + env.close(); + + env(escrowCreate, + escrow::finish_function(wasmHex), + escrow::cancel_time(env.now() + 100s), + escrow::data("00112233"), + fee(txnFees), + ter(temDISABLED)); + env.close(); + } + + { + // FinishFunction > max length + Env env( + *this, + envconfig([](std::unique_ptr cfg) { + cfg->FEES.extension_size_limit = 10; // 10 bytes + return cfg; + }), + features); + XRPAmount const txnFees = env.current()->fees().base + 1000; + // create escrow + env.fund(XRP(5000), alice, carol); + + auto escrowCreate = escrow::create(alice, carol, XRP(500)); + + // 11-byte string + std::string longWasmHex = "00112233445566778899AA"; + env(escrowCreate, + escrow::finish_function(longWasmHex), + escrow::cancel_time(env.now() + 100s), + fee(txnFees), + ter(temMALFORMED)); + env.close(); + } + + { + // Data without FinishFunction + Env env(*this, features); + XRPAmount const txnFees = env.current()->fees().base + 100000; + // create escrow + env.fund(XRP(5000), alice, carol); + + auto escrowCreate = escrow::create(alice, carol, XRP(500)); + + std::string longData(4, 'A'); + env(escrowCreate, + escrow::data(longData), + escrow::finish_time(env.now() + 100s), + fee(txnFees), + ter(temMALFORMED)); + env.close(); + } + + { + // Data > max length + Env env(*this, features); + XRPAmount const txnFees = env.current()->fees().base + 100000; + // create escrow + env.fund(XRP(5000), alice, carol); + + auto escrowCreate = escrow::create(alice, carol, XRP(500)); + + // string of length maxWasmDataLength * 2 + 2 + std::string longData(maxWasmDataLength * 2 + 2, 'B'); + env(escrowCreate, + escrow::data(longData), + escrow::finish_function(wasmHex), + escrow::cancel_time(env.now() + 100s), + fee(txnFees), + ter(temMALFORMED)); + env.close(); + } + + Env env( + *this, + envconfig([](std::unique_ptr cfg) { + cfg->START_UP = Config::FRESH; + return cfg; + }), + features); + XRPAmount const txnFees = + env.current()->fees().base * 10 + wasmHex.size() / 2 * 5; + // create escrow + env.fund(XRP(5000), alice, carol); + + auto escrowCreate = escrow::create(alice, carol, XRP(500)); + + // Success situations + { + // FinishFunction + CancelAfter + env(escrowCreate, + escrow::finish_function(wasmHex), + escrow::cancel_time(env.now() + 20s), + fee(txnFees)); + env.close(); + } + { + // FinishFunction + Condition + CancelAfter + env(escrowCreate, + escrow::finish_function(wasmHex), + escrow::cancel_time(env.now() + 30s), + escrow::condition(escrow::cb1), + fee(txnFees)); + env.close(); + } + { + // FinishFunction + FinishAfter + CancelAfter + env(escrowCreate, + escrow::finish_function(wasmHex), + escrow::cancel_time(env.now() + 40s), + escrow::finish_time(env.now() + 2s), + fee(txnFees)); + env.close(); + } + { + // FinishFunction + FinishAfter + Condition + CancelAfter + env(escrowCreate, + escrow::finish_function(wasmHex), + escrow::cancel_time(env.now() + 50s), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 2s), + fee(txnFees)); + env.close(); + } + + // Failure situations (i.e. all other combinations) + { + // only FinishFunction + env(escrowCreate, + escrow::finish_function(wasmHex), + fee(txnFees), + ter(temBAD_EXPIRATION)); + env.close(); + } + { + // FinishFunction + FinishAfter + env(escrowCreate, + escrow::finish_function(wasmHex), + escrow::finish_time(env.now() + 2s), + fee(txnFees), + ter(temBAD_EXPIRATION)); + env.close(); + } + { + // FinishFunction + Condition + env(escrowCreate, + escrow::finish_function(wasmHex), + escrow::condition(escrow::cb1), + fee(txnFees), + ter(temBAD_EXPIRATION)); + env.close(); + } + { + // FinishFunction + FinishAfter + Condition + env(escrowCreate, + escrow::finish_function(wasmHex), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 2s), + fee(txnFees), + ter(temBAD_EXPIRATION)); + env.close(); + } + { + // FinishFunction 0 length + env(escrowCreate, + escrow::finish_function(""), + escrow::cancel_time(env.now() + 60s), + fee(txnFees), + ter(temMALFORMED)); + env.close(); + } + { + // Not enough fees + env(escrowCreate, + escrow::finish_function(wasmHex), + escrow::cancel_time(env.now() + 70s), + fee(txnFees - 1), + ter(telINSUF_FEE_P)); + env.close(); + } + + { + // FinishFunction nonexistent host function + // pub fn finish() -> bool { + // unsafe { host_lib::bad() >= 5 } + // } + auto const badWasmHex = + "0061736d010000000105016000017f02100108686f73745f6c696203626164" + "00000302010005030100100611027f00418080c0000b7f00418080c0000b07" + "2e04066d656d6f727902000666696e69736800010a5f5f646174615f656e64" + "03000b5f5f686561705f6261736503010a09010700100041044a0b004d0970" + "726f64756365727302086c616e6775616765010452757374000c70726f6365" + "737365642d6279010572757374631d312e38352e3120283465623136313235" + "3020323032352d30332d31352900490f7461726765745f6665617475726573" + "042b0f6d757461626c652d676c6f62616c732b087369676e2d6578742b0f72" + "65666572656e63652d74797065732b0a6d756c746976616c7565"; + env(escrowCreate, + escrow::finish_function(badWasmHex), + escrow::cancel_time(env.now() + 100s), + fee(txnFees), + ter(temBAD_WASM)); + env.close(); + } + } + + void + testFinishWasmFailures(FeatureBitset features) + { + testcase("EscrowFinish Smart Escrow failures"); + + using namespace jtx; + using namespace std::chrono; + + Account const alice{"alice"}; + Account const carol{"carol"}; + + // Tests whether the ledger index is >= 5 + // getLedgerSqn() >= 5} + static auto wasmHex = ledgerSqnWasmHex; + + { + // featureSmartEscrow disabled + Env env(*this, features - featureSmartEscrow); + env.fund(XRP(5000), alice, carol); + XRPAmount const txnFees = + env.current()->fees().base * 10 + wasmHex.size() / 2 * 5; + env(escrow::finish(carol, alice, 1), + fee(txnFees), + escrow::comp_allowance(4), + ter(temDISABLED)); + env.close(); + } + + { + // ComputationAllowance > max compute limit + Env env( + *this, + envconfig([](std::unique_ptr cfg) { + cfg->FEES.extension_compute_limit = 1'000; // in gas + return cfg; + }), + features); + env.fund(XRP(5000), alice, carol); + // Run past the flag ledger so that a Fee change vote occurs and + // updates FeeSettings. (It also activates all supported + // amendments.) + for (auto i = env.current()->seq(); i <= 257; ++i) + env.close(); + + auto const allowance = 1'001; + env(escrow::finish(carol, alice, 1), + fee(env.current()->fees().base + allowance), + escrow::comp_allowance(allowance), + ter(temBAD_LIMIT)); + } + + Env env(*this, features); + + // Run past the flag ledger so that a Fee change vote occurs and + // updates FeeSettings. (It also activates all supported + // amendments.) + for (auto i = env.current()->seq(); i <= 257; ++i) + env.close(); + + XRPAmount const txnFees = + env.current()->fees().base * 10 + wasmHex.size() / 2 * 5; + env.fund(XRP(5000), alice, carol); + + // create escrow + auto const seq = env.seq(alice); + env(escrow::create(alice, carol, XRP(500)), + escrow::finish_function(wasmHex), + escrow::cancel_time(env.now() + 100s), + fee(txnFees)); + env.close(); + + { + // no ComputationAllowance field + env(escrow::finish(carol, alice, seq), + ter(tefWASM_FIELD_NOT_INCLUDED)); + } + + { + // ComputationAllowance value of 0 + env(escrow::finish(carol, alice, seq), + escrow::comp_allowance(0), + ter(temBAD_LIMIT)); + } + + { + // not enough fees + // This function takes 4 gas + // In testing, 1 gas costs 1 drop + auto const finishFee = env.current()->fees().base + 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 + testUpdateDataOnFailure(FeatureBitset features) + { + testcase("Update escrow data on failure"); + + using namespace jtx; + using namespace std::chrono; + + // wasm that always fails + static auto const wasmHex = updateDataWasmHex; + + Account const alice{"alice"}; + Account const carol{"carol"}; + + Env env(*this, features); + // create escrow + env.fund(XRP(5000), alice); + auto const seq = env.seq(alice); + BEAST_EXPECT(env.ownerCount(alice) == 0); + auto escrowCreate = escrow::create(alice, alice, XRP(1000)); + XRPAmount txnFees = + env.current()->fees().base * 10 + wasmHex.size() / 2 * 5; + env(escrowCreate, + escrow::finish_function(wasmHex), + escrow::finish_time(env.now() + 2s), + escrow::cancel_time(env.now() + 100s), + fee(txnFees)); + env.close(); + env.close(); + env.close(); + + if (BEAST_EXPECT( + env.ownerCount(alice) == (1 + wasmHex.size() / 2 / 500))) + { + env.require(balance(alice, XRP(4000) - txnFees)); + + auto const allowance = 1'015; + XRPAmount const finishFee = env.current()->fees().base + + (allowance * env.current()->fees().gasPrice) / + MICRO_DROPS_PER_DROP + + 1; + + // FinishAfter time hasn't passed + env(escrow::finish(alice, alice, seq), + escrow::comp_allowance(allowance), + fee(finishFee), + ter(tecWASM_REJECTED)); + + auto const txMeta = env.meta(); + if (BEAST_EXPECT(txMeta && txMeta->isFieldPresent(sfGasUsed))) + BEAST_EXPECTS( + txMeta->getFieldU32(sfGasUsed) == allowance, + std::to_string(txMeta->getFieldU32(sfGasUsed))); + if (BEAST_EXPECT(txMeta->isFieldPresent(sfWasmReturnCode))) + BEAST_EXPECTS( + txMeta->getFieldI32(sfWasmReturnCode) == -256, + std::to_string(txMeta->getFieldI32(sfWasmReturnCode))); + + auto const sle = env.le(keylet::escrow(alice, seq)); + if (BEAST_EXPECT(sle && sle->isFieldPresent(sfData))) + BEAST_EXPECTS( + checkVL(sle, sfData, "Data"), + strHex(sle->getFieldVL(sfData))); + } + } + + void + testAllHostFunctions(FeatureBitset features) + { + testcase("Test all host functions"); + + using namespace jtx; + using namespace std::chrono; + + // TODO: create wasm module for all host functions + static auto wasmHex = allHostFunctionsWasmHex; + + Account const alice{"alice"}; + Account const carol{"carol"}; + + { + Env env(*this, features); + // create escrow + env.fund(XRP(5000), alice, carol); + auto const seq = env.seq(alice); + BEAST_EXPECT(env.ownerCount(alice) == 0); + auto escrowCreate = escrow::create(alice, carol, XRP(1000)); + XRPAmount txnFees = + env.current()->fees().base * 10 + wasmHex.size() / 2 * 5; + env(escrowCreate, + escrow::finish_function(wasmHex), + escrow::finish_time(env.now() + 11s), + escrow::cancel_time(env.now() + 100s), + escrow::data("1000000000"), // 1000 XRP in drops + fee(txnFees)); + env.close(); + + if (BEAST_EXPECT( + env.ownerCount(alice) == (1 + wasmHex.size() / 2 / 500))) + { + env.require(balance(alice, XRP(4000) - txnFees)); + env.require(balance(carol, XRP(5000))); + + auto const allowance = 1'000'000; + XRPAmount const finishFee = env.current()->fees().base + + (allowance * env.current()->fees().gasPrice) / + MICRO_DROPS_PER_DROP + + 1; + + // FinishAfter time hasn't passed + env(escrow::finish(carol, alice, seq), + escrow::comp_allowance(allowance), + fee(finishFee), + ter(tecNO_PERMISSION)); + env.close(); + env.close(); + env.close(); + + // reduce the destination balance + env(pay(carol, alice, XRP(4500))); + env.close(); + env.close(); + + env(escrow::finish(alice, alice, seq), + escrow::comp_allowance(allowance), + fee(finishFee), + ter(tesSUCCESS)); + + auto const txMeta = env.meta(); + if (BEAST_EXPECT(txMeta && txMeta->isFieldPresent(sfGasUsed))) + BEAST_EXPECTS( + txMeta->getFieldU32(sfGasUsed) == 38'562, + std::to_string(txMeta->getFieldU32(sfGasUsed))); + if (BEAST_EXPECT(txMeta->isFieldPresent(sfWasmReturnCode))) + BEAST_EXPECT(txMeta->getFieldI32(sfWasmReturnCode) == 1); + + env.close(); + BEAST_EXPECT(env.ownerCount(alice) == 0); + } + } + } + + void + testKeyletHostFunctions(FeatureBitset features) + { + testcase("Test all keylet host functions"); + + using namespace jtx; + using namespace std::chrono; + + // TODO: create wasm module for all host functions + static auto wasmHex = allKeyletsWasmHex; + + Account const alice{"alice"}; + Account const carol{"carol"}; + + { + Env env{*this}; + env.fund(XRP(10000), alice, carol); + + BEAST_EXPECT(env.seq(alice) == 4); + BEAST_EXPECT(env.ownerCount(alice) == 0); + + // base objects that need to be created first + auto const tokenId = + token::getNextID(env, alice, 0, tfTransferable); + env(token::mint(alice, 0u), txflags(tfTransferable)); + env(trust(alice, carol["USD"](1'000'000))); + env.close(); + BEAST_EXPECT(env.seq(alice) == 6); + BEAST_EXPECT(env.ownerCount(alice) == 2); + + // set up a bunch of objects to check their keylets + AMM amm(env, carol, XRP(10), carol["USD"](1000)); + env(check::create(alice, carol, XRP(100))); + env(credentials::create(alice, alice, "termsandconditions")); + env(delegate::set(alice, carol, {"TrustSet"})); + env(deposit::auth(alice, carol)); + env(did::set(alice), did::data("alice_did")); + env(escrow::create(alice, carol, XRP(100)), + escrow::finish_time(env.now() + 100s)); + MPTTester mptTester{env, alice, {.fund = false}}; + mptTester.create(); + mptTester.authorize({.account = carol}); + env(token::createOffer(carol, tokenId, XRP(100)), + token::owner(alice)); + env(offer(alice, carol["GBP"](0.1), XRP(100))); + env(paychan::create(alice, carol, XRP(1000), 100s, alice.pk())); + pdomain::Credentials credentials{{alice, "first credential"}}; + env(pdomain::setTx(alice, credentials)); + env(signers(alice, 1, {{carol, 1}})); + env(ticket::create(alice, 1)); + Vault vault{env}; + auto [tx, _keylet] = + vault.create({.owner = alice, .asset = xrpIssue()}); + env(tx); + env.close(); + + BEAST_EXPECTS( + env.ownerCount(alice) == 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'704; + 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) + { + testCreateFinishFunctionPreflight(features); + testFinishWasmFailures(features); + testFinishFunction(features); + testUpdateDataOnFailure(features); + + // TODO: Update module with new host functions + testAllHostFunctions(features); + testKeyletHostFunctions(features); + } + +public: + void + run() override + { + using namespace test::jtx; + FeatureBitset const all{testable_amendments()}; + testWithFeats(all); + } +}; + +BEAST_DEFINE_TESTSUITE(EscrowSmart, app, ripple); + +} // namespace test +} // namespace ripple diff --git a/src/test/app/Escrow_test.cpp b/src/test/app/Escrow_test.cpp index 22e81ccca44..b75590aab5e 100644 --- a/src/test/app/Escrow_test.cpp +++ b/src/test/app/Escrow_test.cpp @@ -1517,7 +1517,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"; @@ -1684,6 +1684,8 @@ struct Escrow_test : public beast::unit_test::suite FeatureBitset const all{testable_amendments()}; testWithFeats(all); testWithFeats(all - featureTokenEscrow); + testWithFeats(all - featureSmartEscrow); + testWithFeats(all - featureTokenEscrow - featureSmartEscrow); testTags(all - fixIncludeKeyletFields); } }; diff --git a/src/test/app/HostFuncImpl_test.cpp b/src/test/app/HostFuncImpl_test.cpp index 6ada72b7654..c483a0ac916 100644 --- a/src/test/app/HostFuncImpl_test.cpp +++ b/src/test/app/HostFuncImpl_test.cpp @@ -1301,7 +1301,8 @@ struct HostFuncImpl_test : public beast::unit_test::suite BEAST_EXPECT(result.has_value() && result.value() == data.size()); // Should fail for too large data - std::vector bigData(maxWasmDataLength + 1, 0x42); + std::vector bigData( + 1024 * 1024 + 1, 0x42); // > maxWasmDataLength auto const tooBig = hfs.updateData(Slice(bigData.data(), bigData.size())); if (BEAST_EXPECT(!tooBig.has_value())) diff --git a/src/test/app/wasm_fixtures/fixtures.cpp b/src/test/app/wasm_fixtures/fixtures.cpp index e4c65c3d629..dbeee60587f 100644 --- a/src/test/app/wasm_fixtures/fixtures.cpp +++ b/src/test/app/wasm_fixtures/fixtures.cpp @@ -10276,3 +10276,21 @@ extern std::string const disabledFloatHex = "6503050b5f5f686561705f6261736503060a5f5f686561705f656e640307" "0d5f5f6d656d6f72795f6261736503080c5f5f7461626c655f6261736503" "090a150202000b100043000000c54300200045921a41010b"; + +extern std::string const updateDataWasmHex = + "0061736d01000000010e0360027f7f017f6000006000017f02130103656e760b7570646174" + "655f64617461000003030201020503010002063f0a7f01419088040b7f004180080b7f0041" + "85080b7f004190080b7f00419088040b7f004180080b7f00419088040b7f00418080080b7f" + "0041000b7f0041010b07aa010c066d656d6f72790200115f5f7761736d5f63616c6c5f6374" + "6f727300010666696e69736800020c5f5f64736f5f68616e646c6503010a5f5f646174615f" + "656e6403020b5f5f737461636b5f6c6f7703030c5f5f737461636b5f6869676803040d5f5f" + "676c6f62616c5f6261736503050b5f5f686561705f6261736503060a5f5f686561705f656e" + "6403070d5f5f6d656d6f72795f6261736503080c5f5f7461626c655f6261736503090a3f02" + "02000b3a01017f230041106b220024002000410c6a4184082d00003a000020004180082800" + "00360208200041086a410410001a200041106a240041807e0b0b0b01004180080b04446174" + "61007f0970726f647563657273010c70726f6365737365642d62790105636c616e675f3139" + "2e312e352d776173692d73646b202868747470733a2f2f6769746875622e636f6d2f6c6c76" + "6d2f6c6c766d2d70726f6a6563742061623462356132646235383239353861663165653330" + "3861373930636664623432626432343732302900490f7461726765745f6665617475726573" + "042b0f6d757461626c652d676c6f62616c732b087369676e2d6578742b0f7265666572656e" + "63652d74797065732b0a6d756c746976616c7565"; diff --git a/src/test/app/wasm_fixtures/fixtures.h b/src/test/app/wasm_fixtures/fixtures.h index f7c258326fd..5ca691786aa 100644 --- a/src/test/app/wasm_fixtures/fixtures.h +++ b/src/test/app/wasm_fixtures/fixtures.h @@ -31,3 +31,5 @@ extern std::string const floatTestsWasmHex; extern std::string const float0Hex; extern std::string const disabledFloatHex; + +extern std::string const updateDataWasmHex; diff --git a/src/test/app/wasm_fixtures/updateData.c b/src/test/app/wasm_fixtures/updateData.c new file mode 100644 index 00000000000..8436f1c3905 --- /dev/null +++ b/src/test/app/wasm_fixtures/updateData.c @@ -0,0 +1,13 @@ +#include + +int32_t +update_data(uint8_t const*, int32_t); + +int +finish() +{ + uint8_t buf[] = "Data"; + update_data(buf, sizeof(buf) - 1); + + return -256; +} diff --git a/src/test/jtx/escrow.h b/src/test/jtx/escrow.h index d0d066c2d46..02f3ac8a937 100644 --- a/src/test/jtx/escrow.h +++ b/src/test/jtx/escrow.h @@ -85,6 +85,76 @@ auto const condition = JTxFieldWrapper(sfCondition); auto const fulfillment = JTxFieldWrapper(sfFulfillment); +struct finish_function +{ +private: + std::string value_; + +public: + explicit finish_function(std::string func) : value_(func) + { + } + + explicit finish_function(Slice const& func) : value_(strHex(func)) + { + } + + template + explicit finish_function(std::array const& f) + : finish_function(makeSlice(f)) + { + } + + void + operator()(Env&, JTx& jt) const + { + jt.jv[sfFinishFunction.jsonName] = value_; + } +}; + +struct data +{ +private: + std::string value_; + +public: + explicit data(std::string func) : value_(func) + { + } + + explicit data(Slice const& func) : value_(strHex(func)) + { + } + + template + explicit data(std::array const& f) : data(makeSlice(f)) + { + } + + void + operator()(Env&, JTx& jt) const + { + jt.jv[sfData.jsonName] = value_; + } +}; + +struct comp_allowance +{ +private: + std::uint32_t value_; + +public: + explicit comp_allowance(std::uint32_t const& value) : value_(value) + { + } + + void + operator()(Env&, JTx& jt) const + { + jt.jv[sfComputationAllowance.jsonName] = value_; + } +}; + } // namespace escrow } // namespace jtx diff --git a/src/test/jtx/impl/envconfig.cpp b/src/test/jtx/impl/envconfig.cpp index 8cf416a4c5d..19f709d9b49 100644 --- a/src/test/jtx/impl/envconfig.cpp +++ b/src/test/jtx/impl/envconfig.cpp @@ -14,9 +14,14 @@ setupConfigForUnitTests(Config& cfg) using namespace jtx; // Default fees to old values, so tests don't have to worry about changes in // Config.h + // NOTE: For new `FEES` fields, you need to wait for the first flag ledger + // to close for the values to be activated. cfg.FEES.reference_fee = UNIT_TEST_REFERENCE_FEE; cfg.FEES.account_reserve = XRP(200).value().xrp().drops(); cfg.FEES.owner_reserve = XRP(50).value().xrp().drops(); + cfg.FEES.extension_compute_limit = 1'000'000; + cfg.FEES.extension_size_limit = 100'000; + cfg.FEES.gas_price = 1'000'000; // 1 drop = 1,000,000 micro-drops // The Beta API (currently v2) is always available to tests cfg.BETA_RPC_API = true; diff --git a/src/test/protocol/STParsedJSON_test.cpp b/src/test/protocol/STParsedJSON_test.cpp index 7ad7e360971..2f1a8f9af1e 100644 --- a/src/test/protocol/STParsedJSON_test.cpp +++ b/src/test/protocol/STParsedJSON_test.cpp @@ -724,63 +724,66 @@ class STParsedJSON_test : public beast::unit_test::suite { Json::Value j; int const minInt32 = -2147483648; - j[sfDummyInt32] = minInt32; + j[sfWasmReturnCode] = minInt32; STParsedJSONObject obj("Test", j); BEAST_EXPECT(obj.object.has_value()); - if (BEAST_EXPECT(obj.object->isFieldPresent(sfDummyInt32))) - BEAST_EXPECT(obj.object->getFieldI32(sfDummyInt32) == minInt32); + if (BEAST_EXPECT(obj.object->isFieldPresent(sfWasmReturnCode))) + BEAST_EXPECT( + obj.object->getFieldI32(sfWasmReturnCode) == minInt32); } // max value { Json::Value j; int const maxInt32 = 2147483647; - j[sfDummyInt32] = maxInt32; + j[sfWasmReturnCode] = maxInt32; STParsedJSONObject obj("Test", j); BEAST_EXPECT(obj.object.has_value()); - if (BEAST_EXPECT(obj.object->isFieldPresent(sfDummyInt32))) - BEAST_EXPECT(obj.object->getFieldI32(sfDummyInt32) == maxInt32); + if (BEAST_EXPECT(obj.object->isFieldPresent(sfWasmReturnCode))) + BEAST_EXPECT( + obj.object->getFieldI32(sfWasmReturnCode) == maxInt32); } // max uint value { Json::Value j; unsigned int const maxUInt32 = 2147483647u; - j[sfDummyInt32] = maxUInt32; + j[sfWasmReturnCode] = maxUInt32; STParsedJSONObject obj("Test", j); BEAST_EXPECT(obj.object.has_value()); - if (BEAST_EXPECT(obj.object->isFieldPresent(sfDummyInt32))) + if (BEAST_EXPECT(obj.object->isFieldPresent(sfWasmReturnCode))) BEAST_EXPECT( - obj.object->getFieldI32(sfDummyInt32) == + obj.object->getFieldI32(sfWasmReturnCode) == static_cast(maxUInt32)); } // Test with string value { Json::Value j; - j[sfDummyInt32] = "2147483647"; + j[sfWasmReturnCode] = "2147483647"; STParsedJSONObject obj("Test", j); BEAST_EXPECT(obj.object.has_value()); - if (BEAST_EXPECT(obj.object->isFieldPresent(sfDummyInt32))) + if (BEAST_EXPECT(obj.object->isFieldPresent(sfWasmReturnCode))) BEAST_EXPECT( - obj.object->getFieldI32(sfDummyInt32) == 2147483647u); + obj.object->getFieldI32(sfWasmReturnCode) == 2147483647u); } // Test with string negative value { Json::Value j; int value = -2147483648; - j[sfDummyInt32] = std::to_string(value); + j[sfWasmReturnCode] = std::to_string(value); STParsedJSONObject obj("Test", j); BEAST_EXPECT(obj.object.has_value()); - if (BEAST_EXPECT(obj.object->isFieldPresent(sfDummyInt32))) - BEAST_EXPECT(obj.object->getFieldI32(sfDummyInt32) == value); + if (BEAST_EXPECT(obj.object->isFieldPresent(sfWasmReturnCode))) + BEAST_EXPECT( + obj.object->getFieldI32(sfWasmReturnCode) == value); } // Test out of range value for int32 (negative) { Json::Value j; - j[sfDummyInt32] = "-2147483649"; + j[sfWasmReturnCode] = "-2147483649"; STParsedJSONObject obj("Test", j); BEAST_EXPECT(!obj.object.has_value()); } @@ -788,7 +791,7 @@ class STParsedJSON_test : public beast::unit_test::suite // Test out of range value for int32 (positive) { Json::Value j; - j[sfDummyInt32] = 2147483648u; + j[sfWasmReturnCode] = 2147483648u; STParsedJSONObject obj("Test", j); BEAST_EXPECT(!obj.object.has_value()); } @@ -796,7 +799,7 @@ class STParsedJSON_test : public beast::unit_test::suite // Test string value out of range { Json::Value j; - j[sfDummyInt32] = "2147483648"; + j[sfWasmReturnCode] = "2147483648"; STParsedJSONObject obj("Test", j); BEAST_EXPECT(!obj.object.has_value()); } @@ -804,7 +807,7 @@ class STParsedJSON_test : public beast::unit_test::suite // Test bad_type (arrayValue) { Json::Value j; - j[sfDummyInt32] = Json::Value(Json::arrayValue); + j[sfWasmReturnCode] = Json::Value(Json::arrayValue); STParsedJSONObject obj("Test", j); BEAST_EXPECT(!obj.object.has_value()); } @@ -812,7 +815,7 @@ class STParsedJSON_test : public beast::unit_test::suite // Test bad_type (objectValue) { Json::Value j; - j[sfDummyInt32] = Json::Value(Json::objectValue); + j[sfWasmReturnCode] = Json::Value(Json::objectValue); STParsedJSONObject obj("Test", j); BEAST_EXPECT(!obj.object.has_value()); } diff --git a/src/xrpld/app/tx/detail/ApplyContext.cpp b/src/xrpld/app/tx/detail/ApplyContext.cpp index 4a7f72e2e35..cc33ae93b87 100644 --- a/src/xrpld/app/tx/detail/ApplyContext.cpp +++ b/src/xrpld/app/tx/detail/ApplyContext.cpp @@ -40,6 +40,11 @@ ApplyContext::discard() std::optional ApplyContext::apply(TER ter) { + if (wasmReturnCode_.has_value()) + { + view_->setWasmReturnCode(*wasmReturnCode_); + } + view_->setGasUsed(gasUsed_); return view_->apply( base_, tx, ter, parentBatchId_, flags_ & tapDRY_RUN, journal); } diff --git a/src/xrpld/app/tx/detail/ApplyContext.h b/src/xrpld/app/tx/detail/ApplyContext.h index e0451891467..e52a02a73c1 100644 --- a/src/xrpld/app/tx/detail/ApplyContext.h +++ b/src/xrpld/app/tx/detail/ApplyContext.h @@ -87,6 +87,20 @@ class ApplyContext view_->deliver(amount); } + /** Sets the gas used in the metadata */ + void + setGasUsed(std::uint32_t const gasUsed) + { + gasUsed_ = gasUsed; + } + + /** Sets the gas used in the metadata */ + void + setWasmReturnCode(std::int32_t const wasmReturnCode) + { + wasmReturnCode_ = wasmReturnCode; + } + /** Discard changes and start fresh. */ void discard(); @@ -138,6 +152,8 @@ class ApplyContext // 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 fb3e3c509e6..380165f6450 100644 --- a/src/xrpld/app/tx/detail/Escrow.cpp +++ b/src/xrpld/app/tx/detail/Escrow.cpp @@ -1,6 +1,8 @@ #include #include #include +#include +#include #include #include @@ -15,6 +17,7 @@ #include #include +#include namespace ripple { // During an EscrowFinish, the transaction must specify both @@ -99,6 +102,30 @@ escrowCreatePreflightHelper(PreflightContext const& ctx) return tesSUCCESS; } +XRPAmount +EscrowCreate::calculateBaseFee(ReadView const& view, STTx const& tx) +{ + XRPAmount txnFees{Transactor::calculateBaseFee(view, tx)}; + if (tx.isFieldPresent(sfFinishFunction)) + { + // 10 base fees for the transaction (1 is in + // `Transactor::calculateBaseFee`), plus 5 drops per byte + txnFees += 9 * view.fees().base + 5 * tx[sfFinishFunction].size(); + } + return txnFees; +} + +bool +EscrowCreate::checkExtraFeatures(PreflightContext const& ctx) +{ + if ((ctx.tx.isFieldPresent(sfFinishFunction) || + ctx.tx.isFieldPresent(sfData)) && + !ctx.rules.enabled(featureSmartEscrow)) + return false; + + return true; +} + NotTEC EscrowCreate::preflight(PreflightContext const& ctx) { @@ -132,12 +159,21 @@ EscrowCreate::preflight(PreflightContext const& ctx) ctx.tx[sfCancelAfter] <= ctx.tx[sfFinishAfter]) return temBAD_EXPIRATION; + if (ctx.tx.isFieldPresent(sfFinishFunction) && + !ctx.tx.isFieldPresent(sfCancelAfter)) + return temBAD_EXPIRATION; + // In the absence of a FinishAfter, the escrow can be finished // immediately, which can be confusing. When creating an escrow, // we want to ensure that either a FinishAfter time is explicitly // specified or a completion condition is attached. - if (!ctx.tx[~sfFinishAfter] && !ctx.tx[~sfCondition]) + if (!ctx.tx[~sfFinishAfter] && !ctx.tx[~sfCondition] && + !ctx.tx[~sfFinishFunction]) + { + JLOG(ctx.j.debug()) << "Must have at least one of FinishAfter, " + "Condition, or FinishFunction."; return temMALFORMED; + } if (auto const cb = ctx.tx[~sfCondition]) { @@ -161,6 +197,43 @@ EscrowCreate::preflight(PreflightContext const& ctx) return temDISABLED; } + if (ctx.tx.isFieldPresent(sfData)) + { + if (!ctx.tx.isFieldPresent(sfFinishFunction)) + { + JLOG(ctx.j.debug()) + << "EscrowCreate with Data requires FinishFunction"; + return temMALFORMED; + } + auto const data = ctx.tx.getFieldVL(sfData); + if (data.size() > maxWasmDataLength) + { + JLOG(ctx.j.debug()) << "EscrowCreate.Data bad size " << data.size(); + return temMALFORMED; + } + } + + if (ctx.tx.isFieldPresent(sfFinishFunction)) + { + auto const code = ctx.tx.getFieldVL(sfFinishFunction); + if (code.size() == 0 || + code.size() > ctx.app.config().FEES.extension_size_limit) + { + JLOG(ctx.j.debug()) + << "EscrowCreate.FinishFunction bad size " << code.size(); + return temMALFORMED; + } + + HostFunctions mock; + auto const re = + preflightEscrowWasm(code, ESCROW_FUNCTION_NAME, {}, &mock, ctx.j); + if (!isTesSuccess(re)) + { + JLOG(ctx.j.debug()) << "EscrowCreate.FinishFunction bad WASM"; + return re; + } + } + return tesSUCCESS; } @@ -419,6 +492,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() { @@ -436,9 +520,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; @@ -479,6 +565,8 @@ EscrowCreate::doApply() (*slep)[~sfCancelAfter] = ctx_.tx[~sfCancelAfter]; (*slep)[~sfFinishAfter] = ctx_.tx[~sfFinishAfter]; (*slep)[~sfDestinationTag] = ctx_.tx[~sfDestinationTag]; + (*slep)[~sfFinishFunction] = ctx_.tx[~sfFinishFunction]; + (*slep)[~sfData] = ctx_.tx[~sfData]; if (ctx_.view().rules().enabled(fixIncludeKeyletFields)) { @@ -548,7 +636,7 @@ EscrowCreate::doApply() } // increment owner count - adjustOwnerCount(ctx_.view(), sle, 1, ctx_.journal); + adjustOwnerCount(ctx_.view(), sle, reserveToAdd, ctx_.journal); ctx_.view().update(sle); return tesSUCCESS; } @@ -576,8 +664,16 @@ checkCondition(Slice f, Slice c) bool EscrowFinish::checkExtraFeatures(PreflightContext const& ctx) { - return !ctx.tx.isFieldPresent(sfCredentialIDs) || - ctx.rules.enabled(featureCredentials); + if (ctx.tx.isFieldPresent(sfCredentialIDs) && + !ctx.rules.enabled(featureCredentials)) + return false; + + if (ctx.tx.isFieldPresent(sfComputationAllowance) && + !ctx.rules.enabled(featureSmartEscrow)) + { + return false; + } + return true; } NotTEC @@ -589,7 +685,10 @@ EscrowFinish::preflight(PreflightContext const& ctx) // If you specify a condition, then you must also specify // a fulfillment. if (static_cast(cb) != static_cast(fb)) + { + JLOG(ctx.j.debug()) << "Condition != Fulfillment"; return temMALFORMED; + } return tesSUCCESS; } @@ -619,6 +718,20 @@ EscrowFinish::preflightSigValidated(PreflightContext const& ctx) } } + if (auto const allowance = ctx.tx[~sfComputationAllowance]; allowance) + { + if (*allowance == 0) + { + return temBAD_LIMIT; + } + if (*allowance > ctx.app.config().FEES.extension_compute_limit) + { + JLOG(ctx.j.debug()) + << "ComputationAllowance too large: " << *allowance; + return temBAD_LIMIT; + } + } + if (auto const err = credentials::checkFields(ctx.tx, ctx.j); !isTesSuccess(err)) return err; @@ -635,7 +748,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; } @@ -715,25 +835,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; @@ -968,7 +1115,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; @@ -986,6 +1134,23 @@ EscrowFinish::doApply() if ((*slep)[~sfCancelAfter] && after(now, (*slep)[sfCancelAfter])) return tecNO_PERMISSION; + AccountID const destID = (*slep)[sfDestination]; + auto const sled = ctx_.view().peek(keylet::account(destID)); + if (ctx_.view().rules().enabled(featureSmartEscrow)) + { + // NOTE: Escrow payments cannot be used to fund accounts. + if (!sled) + return tecNO_DST; + + if (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 { auto const id = ctx_.tx.getTransactionID(); @@ -1035,18 +1200,71 @@ EscrowFinish::doApply() return tecCRYPTOCONDITION_ERROR; } - // NOTE: Escrow payments cannot be used to fund accounts. - AccountID const destID = (*slep)[sfDestination]; - auto const sled = ctx_.view().peek(keylet::account(destID)); - if (!sled) - return tecNO_DST; + if (!ctx_.view().rules().enabled(featureSmartEscrow)) + { + // NOTE: Escrow payments cannot be used to fund accounts. + if (!sled) + return tecNO_DST; - if (ctx_.view().rules().enabled(featureDepositAuth)) + 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]) { - if (auto err = verifyDepositPreauth( - ctx_.tx, ctx_.view(), account_, destID, sled, ctx_.journal); - !isTesSuccess(err)) - return err; + JLOG(j_.trace()) + << "The escrow has a finish function, running WASM code..."; + // WASM execution + auto const wasmStr = slep->getFieldVL(sfFinishFunction); + std::vector wasm(wasmStr.begin(), wasmStr.end()); + + WasmHostFunctionsImpl ledgerDataProvider(ctx_, k); + + if (!ctx_.tx.isFieldPresent(sfComputationAllowance)) + { + // already checked above, this check is just in case + return tecINTERNAL; + } + std::uint32_t allowance = ctx_.tx[sfComputationAllowance]; + auto re = runEscrowWasm( + wasm, ESCROW_FUNCTION_NAME, {}, &ledgerDataProvider, allowance); + JLOG(j_.trace()) << "Escrow WASM ran"; + + if (auto const& data = ledgerDataProvider.getData(); data.has_value()) + { + slep->setFieldVL(sfData, makeSlice(*data)); + ctx_.view().update(slep); + } + + if (re.has_value()) + { + auto reValue = re.value().result; + auto reCost = re.value().cost; + JLOG(j_.debug()) << "WASM Success: " + std::to_string(reValue) + << ", cost: " << reCost; + + ctx_.setWasmReturnCode(reValue); + + if (reCost < 0 || reCost > std::numeric_limits::max()) + return tecINTERNAL; // LCOV_EXCL_LINE + ctx_.setGasUsed(static_cast(reCost)); + + if (reValue <= 0) + { + return tecWASM_REJECTED; + } + } + else + { + JLOG(j_.debug()) << "WASM Failure: " + transHuman(re.error()); + return re.error(); + } } AccountID const account = (*slep)[sfAccount]; @@ -1125,9 +1343,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 @@ -1327,7 +1548,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 d2821bc45d7..1da745c165a 100644 --- a/src/xrpld/app/tx/detail/Escrow.h +++ b/src/xrpld/app/tx/detail/Escrow.h @@ -14,9 +14,15 @@ class EscrowCreate : public Transactor { } + static bool + checkExtraFeatures(PreflightContext const& ctx); + static TxConsequences makeTxConsequences(PreflightContext const& ctx); + static XRPAmount + calculateBaseFee(ReadView const& view, STTx const& tx); + static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index 288ae4c85db..7c276c44872 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -1017,6 +1017,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, @@ -1175,6 +1191,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); @@ -1187,13 +1204,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, @@ -1202,7 +1222,9 @@ Transactor::operator()() doNFTokenOffers, &expiredNFTokenOffers, doCredentials, - &expiredCredentials]( + &expiredCredentials, + doWasmData, + &modifiedWasmObjects]( uint256 const& index, bool isDelete, std::shared_ptr const& before, @@ -1237,6 +1259,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))); + } }); } @@ -1266,6 +1295,10 @@ Transactor::operator()() removeExpiredCredentials( view(), expiredCredentials, ctx_.app.journal("View")); + if (result == tecWASM_REJECTED) + modifyWasmDataFields( + view(), modifiedWasmObjects, ctx_.app.journal("View")); + applied = isTecClaim(result); }