Wasm SmartEscrow

This commit is contained in:
Oleksandr
2025-11-11 12:50:20 -05:00
parent 801110ee01
commit fe75c5d441
33 changed files with 1588 additions and 88 deletions

View File

@@ -1290,6 +1290,39 @@
# Example:
# owner_reserve = 2000000 # 2 XRP
#
# extension_compute_limit = <gas>
#
# The extension compute limit is the maximum amount of gas that can be
# consumed by a single transaction. The gas limit is used to prevent
# transactions from consuming too many resources.
#
# If this parameter is unspecified, rippled will use an internal
# default. Don't change this without understanding the consequences.
#
# Example:
# extension_compute_limit = 1000000 # 1 million gas
#
# extension_size_limit = <bytes>
#
# The extension size limit is the maximum size of a WASM extension in
# bytes. The size limit is used to prevent extensions from consuming
# too many resources.
#
# If this parameter is unspecified, rippled will use an internal
# default. Don't change this without understanding the consequences.
#
# Example:
# extension_size_limit = 100000 # 100 kb
#
# gas_price = <bytes>
#
# The gas price is the conversion between WASM gas and its price in drops.
#
# If this parameter is unspecified, rippled will use an internal
# default. Don't change this without understanding the consequences.
#
# Example:
# gas_price = 1000000 # 1 drop per gas
#-------------------------------------------------------------------------------
#
# 9. Misc Settings

View File

@@ -55,6 +55,18 @@ public:
deliver_ = amount;
}
void
setGasUsed(std::optional<std::uint32_t> const gasUsed)
{
gasUsed_ = gasUsed;
}
void
setWasmReturnCode(std::int32_t const wasmReturnCode)
{
wasmReturnCode_ = wasmReturnCode;
}
/** Get the number of modified entries
*/
std::size_t
@@ -73,6 +85,8 @@ public:
private:
std::optional<STAmount> deliver_;
std::optional<std::uint32_t> gasUsed_;
std::optional<std::int32_t> wasmReturnCode_;
};
} // namespace ripple

View File

@@ -53,6 +53,8 @@ public:
TER ter,
std::optional<STAmount> const& deliver,
std::optional<uint256 const> const& parentBatchId,
std::optional<std::uint32_t> const& gasUsed,
std::optional<std::int32_t> const& wasmReturnCode,
bool isDryRun,
beast::Journal j);

View File

@@ -5,6 +5,8 @@
namespace ripple {
constexpr std::uint32_t MICRO_DROPS_PER_DROP{1'000'000};
/** Reflects the fee settings for a particular ledger.
The fees are always the same for any transactions applied
@@ -15,6 +17,10 @@ struct Fees
XRPAmount base{0}; // Reference tx cost (drops)
XRPAmount reserve{0}; // Reserve base (drops)
XRPAmount increment{0}; // Reserve increment (drops)
std::uint32_t extensionComputeLimit{
0}; // Extension compute limit (instructions)
std::uint32_t extensionSizeLimit{0}; // Extension size limit (bytes)
std::uint32_t gasPrice{0}; // price of WASM gas (micro-drops)
explicit Fees() = default;
Fees(Fees const&) = default;

View File

@@ -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;

View File

@@ -85,6 +85,12 @@ public:
if (obj.isFieldPresent(sfParentBatchID))
parentBatchID_ = obj.getFieldH256(sfParentBatchID);
if (obj.isFieldPresent(sfGasUsed))
gasUsed_ = obj.getFieldU32(sfGasUsed);
if (obj.isFieldPresent(sfWasmReturnCode))
wasmReturnCode_ = obj.getFieldI32(sfWasmReturnCode);
}
std::optional<STAmount> const&
@@ -105,6 +111,30 @@ public:
parentBatchID_ = id;
}
void
setGasUsed(std::optional<std::uint32_t> const gasUsed)
{
gasUsed_ = gasUsed;
}
std::optional<std::uint32_t> const&
getGasUsed() const
{
return gasUsed_;
}
void
setWasmReturnCode(std::optional<std::int32_t> const wasmReturnCode)
{
wasmReturnCode_ = wasmReturnCode;
}
std::optional<std::int32_t> const&
getWasmReturnCode() const
{
return wasmReturnCode_;
}
private:
uint256 transactionID_;
std::uint32_t ledgerSeq_;
@@ -113,6 +143,8 @@ private:
std::optional<STAmount> deliveredAmount_;
std::optional<uint256> parentBatchID_;
std::optional<std::uint32_t> gasUsed_;
std::optional<std::int32_t> wasmReturnCode_;
STArray nodes_;
};

View File

@@ -16,6 +16,7 @@
// Add new amendments to the top of this list.
// Keep it sorted in reverse chronological order.
XRPL_FEATURE(SmartEscrow, Supported::no, VoteBehavior::DefaultNo)
XRPL_FEATURE(LendingProtocol, Supported::no, VoteBehavior::DefaultNo)
XRPL_FEATURE(PermissionDelegationV1_1, Supported::no, VoteBehavior::DefaultNo)
XRPL_FIX (DirectoryLimit, Supported::yes, VoteBehavior::DefaultNo)
@@ -32,7 +33,6 @@ XRPL_FEATURE(PermissionedDEX, Supported::yes, VoteBehavior::DefaultNo
XRPL_FEATURE(Batch, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(SingleAssetVault, Supported::no, VoteBehavior::DefaultNo)
XRPL_FIX (PayChanCancelAfter, Supported::yes, VoteBehavior::DefaultNo)
// Check flags in Credential transactions
XRPL_FIX (InvalidTxFlags, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (FrozenLPTokenTransfer, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(DeepFreeze, Supported::yes, VoteBehavior::DefaultNo)

View File

@@ -302,6 +302,11 @@ LEDGER_ENTRY(ltFEE_SETTINGS, 0x0073, FeeSettings, fee, ({
{sfBaseFeeDrops, soeOPTIONAL},
{sfReserveBaseDrops, soeOPTIONAL},
{sfReserveIncrementDrops, soeOPTIONAL},
// Smart Escrow fields
{sfExtensionComputeLimit, soeOPTIONAL},
{sfExtensionSizeLimit, soeOPTIONAL},
{sfGasPrice, soeOPTIONAL},
{sfPreviousTxnID, soeOPTIONAL},
{sfPreviousTxnLgrSeq, soeOPTIONAL},
}))
@@ -332,6 +337,8 @@ LEDGER_ENTRY(ltESCROW, 0x0075, Escrow, escrow, ({
{sfCondition, soeOPTIONAL},
{sfCancelAfter, soeOPTIONAL},
{sfFinishAfter, soeOPTIONAL},
{sfFinishFunction, soeOPTIONAL},
{sfData, soeOPTIONAL},
{sfSourceTag, soeOPTIONAL},
{sfDestinationTag, soeOPTIONAL},
{sfOwnerNode, soeREQUIRED},

View File

@@ -114,6 +114,11 @@ TYPED_SFIELD(sfInterestRate, UINT32, 65) // 1/10 basis points (bi
TYPED_SFIELD(sfLateInterestRate, UINT32, 66) // 1/10 basis points (bips)
TYPED_SFIELD(sfCloseInterestRate, UINT32, 67) // 1/10 basis points (bips)
TYPED_SFIELD(sfOverpaymentInterestRate, UINT32, 68) // 1/10 basis points (bips)
TYPED_SFIELD(sfExtensionComputeLimit, UINT32, 69)
TYPED_SFIELD(sfExtensionSizeLimit, UINT32, 70)
TYPED_SFIELD(sfGasPrice, UINT32, 71)
TYPED_SFIELD(sfComputationAllowance, UINT32, 72)
TYPED_SFIELD(sfGasUsed, UINT32, 73)
// 64-bit integers (common)
TYPED_SFIELD(sfIndexNext, UINT64, 1)
@@ -226,6 +231,7 @@ TYPED_SFIELD(sfManagementFeeOutstanding, NUMBER, 17)
// int32
TYPED_SFIELD(sfLoanScale, INT32, 1)
TYPED_SFIELD(sfWasmReturnCode, INT32, 2)
// currency amount (common)
TYPED_SFIELD(sfAmount, AMOUNT, 1)
@@ -255,7 +261,7 @@ TYPED_SFIELD(sfBaseFeeDrops, AMOUNT, 22)
TYPED_SFIELD(sfReserveBaseDrops, AMOUNT, 23)
TYPED_SFIELD(sfReserveIncrementDrops, AMOUNT, 24)
// currency amount (AMM)
// currency amount (more)
TYPED_SFIELD(sfLPTokenOut, AMOUNT, 25)
TYPED_SFIELD(sfLPTokenIn, AMOUNT, 26)
TYPED_SFIELD(sfEPrice, AMOUNT, 27)
@@ -297,6 +303,7 @@ TYPED_SFIELD(sfAssetClass, VL, 28)
TYPED_SFIELD(sfProvider, VL, 29)
TYPED_SFIELD(sfMPTokenMetadata, VL, 30)
TYPED_SFIELD(sfCredentialType, VL, 31)
TYPED_SFIELD(sfFinishFunction, VL, 32)
// account (common)
TYPED_SFIELD(sfAccount, ACCOUNT, 1)

View File

@@ -50,11 +50,13 @@ TRANSACTION(ttESCROW_CREATE, 1, EscrowCreate,
noPriv,
({
{sfDestination, soeREQUIRED},
{sfDestinationTag, soeOPTIONAL},
{sfAmount, soeREQUIRED, soeMPTSupported},
{sfCondition, soeOPTIONAL},
{sfCancelAfter, soeOPTIONAL},
{sfFinishAfter, soeOPTIONAL},
{sfDestinationTag, soeOPTIONAL},
{sfFinishFunction, soeOPTIONAL},
{sfData, soeOPTIONAL},
}))
/** This transaction type completes an existing escrow. */
@@ -68,6 +70,7 @@ TRANSACTION(ttESCROW_FINISH, 2, EscrowFinish,
{sfFulfillment, soeOPTIONAL},
{sfCondition, soeOPTIONAL},
{sfCredentialIDs, soeOPTIONAL},
{sfComputationAllowance, soeOPTIONAL},
}))
@@ -1092,6 +1095,10 @@ TRANSACTION(ttFEE, 101, SetFee,
{sfBaseFeeDrops, soeOPTIONAL},
{sfReserveBaseDrops, soeOPTIONAL},
{sfReserveIncrementDrops, soeOPTIONAL},
// Smart Escrow fields
{sfExtensionComputeLimit, soeOPTIONAL},
{sfExtensionSizeLimit, soeOPTIONAL},
{sfGasPrice, soeOPTIONAL},
}))
/** This system-generated transaction type is used to update the network's negative UNL

View File

@@ -255,6 +255,9 @@ JSS(expected_date_UTC); // out: any (warnings)
JSS(expected_ledger_size); // out: TxQ
JSS(expiration); // out: AccountOffers, AccountChannels,
// ValidatorList, amm_info
JSS(extension_compute); // out: NetworkOps
JSS(extension_size); // out: NetworkOps
JSS(gas_price); // out: NetworkOps
JSS(fail_hard); // in: Sign, Submit
JSS(failed); // out: InboundLedger
JSS(feature); // in: Feature
@@ -709,11 +712,11 @@ JSS(write_load); // out: GetCounts
#pragma push_macro("LEDGER_ENTRY_DUPLICATE")
#undef LEDGER_ENTRY_DUPLICATE
#define LEDGER_ENTRY(tag, value, name, rpcName, ...) \
JSS(name); \
#define LEDGER_ENTRY(tag, value, name, rpcName, fields) \
JSS(name); \
JSS(rpcName);
#define LEDGER_ENTRY_DUPLICATE(tag, value, name, rpcName, ...) JSS(rpcName);
#define LEDGER_ENTRY_DUPLICATE(tag, value, name, rpcName, fields) JSS(rpcName);
#include <xrpl/protocol/detail/ledger_entries.macro>

View File

@@ -97,6 +97,8 @@ ApplyStateTable::apply(
TER ter,
std::optional<STAmount> const& deliver,
std::optional<uint256 const> const& parentBatchId,
std::optional<std::uint32_t> const& gasUsed,
std::optional<std::int32_t> 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_)

View File

@@ -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

View File

@@ -58,6 +58,10 @@ STValidation::validationFormat()
{sfBaseFeeDrops, soeOPTIONAL},
{sfReserveBaseDrops, soeOPTIONAL},
{sfReserveIncrementDrops, soeOPTIONAL},
// featureSmartEscrow
{sfExtensionComputeLimit, soeOPTIONAL},
{sfExtensionSizeLimit, soeOPTIONAL},
{sfGasPrice, soeOPTIONAL},
};
// clang-format on

View File

@@ -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;
}

View File

@@ -0,0 +1,916 @@
#include <test/app/wasm_fixtures/fixtures.h>
#include <test/jtx.h>
#include <xrpld/app/tx/applySteps.h>
#include <xrpld/app/wasm/WasmVM.h>
#include <xrpl/ledger/Dir.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h>
#include <algorithm>
#include <iterator>
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<Config> 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<Config> 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<Config> cfg) {
cfg->FEES.extension_compute_limit = 1'000; // in gas
return cfg;
}),
features);
env.fund(XRP(5000), alice, carol);
// Run past the flag ledger so that a Fee change vote occurs and
// updates FeeSettings. (It also activates all supported
// amendments.)
for (auto i = env.current()->seq(); i <= 257; ++i)
env.close();
auto const allowance = 1'001;
env(escrow::finish(carol, alice, 1),
fee(env.current()->fees().base + allowance),
escrow::comp_allowance(allowance),
ter(temBAD_LIMIT));
}
Env env(*this, features);
// Run past the flag ledger so that a Fee change vote occurs and
// updates FeeSettings. (It also activates all supported
// amendments.)
for (auto i = env.current()->seq(); i <= 257; ++i)
env.close();
XRPAmount const txnFees =
env.current()->fees().base * 10 + wasmHex.size() / 2 * 5;
env.fund(XRP(5000), alice, carol);
// create escrow
auto const seq = env.seq(alice);
env(escrow::create(alice, carol, XRP(500)),
escrow::finish_function(wasmHex),
escrow::cancel_time(env.now() + 100s),
fee(txnFees));
env.close();
{
// no ComputationAllowance field
env(escrow::finish(carol, alice, seq),
ter(tefWASM_FIELD_NOT_INCLUDED));
}
{
// ComputationAllowance value of 0
env(escrow::finish(carol, alice, seq),
escrow::comp_allowance(0),
ter(temBAD_LIMIT));
}
{
// not enough fees
// This function takes 4 gas
// In testing, 1 gas costs 1 drop
auto const finishFee = env.current()->fees().base;
env(escrow::finish(carol, alice, seq),
fee(finishFee),
escrow::comp_allowance(4),
ter(telINSUF_FEE_P));
}
{
// not enough gas
// This function takes 4 gas
// In testing, 1 gas costs 1 drop
auto const finishFee = env.current()->fees().base + 4;
env(escrow::finish(carol, alice, seq),
fee(finishFee),
escrow::comp_allowance(2),
ter(tecFAILED_PROCESSING));
}
{
// ComputationAllowance field included w/no FinishFunction on
// escrow
auto const seq2 = env.seq(alice);
env(escrow::create(alice, carol, XRP(500)),
escrow::finish_time(env.now() + 10s),
escrow::cancel_time(env.now() + 100s));
env.close();
auto const allowance = 100;
env(escrow::finish(carol, alice, seq2),
fee(env.current()->fees().base +
(allowance * env.current()->fees().gasPrice) /
MICRO_DROPS_PER_DROP +
1),
escrow::comp_allowance(allowance),
ter(tefNO_WASM));
}
}
void
testFinishFunction(FeatureBitset features)
{
testcase("Example escrow function");
using namespace jtx;
using namespace std::chrono;
Account const alice{"alice"};
Account const carol{"carol"};
// Tests whether the ledger index is >= 5
// getLedgerSqn() >= 5}
auto const& wasmHex = ledgerSqnWasmHex;
std::uint32_t const allowance = 5;
auto escrowCreate = escrow::create(alice, carol, XRP(1000));
auto [createFee, finishFee] = [&]() {
Env env(*this, features);
auto createFee =
env.current()->fees().base * 10 + wasmHex.size() / 2 * 5;
auto finishFee = env.current()->fees().base +
(allowance * env.current()->fees().gasPrice) /
MICRO_DROPS_PER_DROP +
1;
return std::make_pair(createFee, finishFee);
}();
{
// basic FinishFunction situation
Env env(*this, features);
// create escrow
env.fund(XRP(5000), alice, carol);
auto const seq = env.seq(alice);
BEAST_EXPECT(env.ownerCount(alice) == 0);
env(escrowCreate,
escrow::finish_function(wasmHex),
escrow::cancel_time(env.now() + 100s),
fee(createFee));
env.close();
if (BEAST_EXPECT(env.ownerCount(alice) == 2))
{
env.require(balance(alice, XRP(4000) - createFee));
env.require(balance(carol, XRP(5000)));
env(escrow::finish(carol, alice, seq),
escrow::comp_allowance(allowance),
fee(finishFee),
ter(tecWASM_REJECTED));
env(escrow::finish(alice, alice, seq),
escrow::comp_allowance(allowance),
fee(finishFee),
ter(tecWASM_REJECTED));
env(escrow::finish(alice, alice, seq),
escrow::comp_allowance(allowance),
fee(finishFee),
ter(tecWASM_REJECTED));
env(escrow::finish(carol, alice, seq),
escrow::comp_allowance(allowance),
fee(finishFee),
ter(tecWASM_REJECTED));
env(escrow::finish(carol, alice, seq),
escrow::comp_allowance(allowance),
fee(finishFee),
ter(tecWASM_REJECTED));
env.close();
{
auto const txMeta = env.meta();
if (BEAST_EXPECT(txMeta->isFieldPresent(sfGasUsed)))
BEAST_EXPECTS(
env.meta()->getFieldU32(sfGasUsed) == allowance,
std::to_string(env.meta()->getFieldU32(sfGasUsed)));
}
env(escrow::finish(alice, alice, seq),
fee(finishFee),
escrow::comp_allowance(allowance),
ter(tesSUCCESS));
auto const txMeta = env.meta();
if (BEAST_EXPECT(txMeta->isFieldPresent(sfGasUsed)))
BEAST_EXPECTS(
txMeta->getFieldU32(sfGasUsed) == allowance,
std::to_string(txMeta->getFieldU32(sfGasUsed)));
if (BEAST_EXPECT(txMeta->isFieldPresent(sfWasmReturnCode)))
BEAST_EXPECTS(
txMeta->getFieldI32(sfWasmReturnCode) == 5,
std::to_string(txMeta->getFieldI32(sfWasmReturnCode)));
BEAST_EXPECT(env.ownerCount(alice) == 0);
}
}
{
// FinishFunction + Condition
Env env(*this, features);
env.fund(XRP(5000), alice, carol);
BEAST_EXPECT(env.ownerCount(alice) == 0);
auto const seq = env.seq(alice);
// create escrow
env(escrowCreate,
escrow::finish_function(wasmHex),
escrow::condition(escrow::cb1),
escrow::cancel_time(env.now() + 100s),
fee(createFee));
env.close();
auto const conditionFinishFee = finishFee +
env.current()->fees().base * (32 + (escrow::fb1.size() / 16));
if (BEAST_EXPECT(env.ownerCount(alice) == 2))
{
env.require(balance(alice, XRP(4000) - createFee));
env.require(balance(carol, XRP(5000)));
// no fulfillment provided, function fails
env(escrow::finish(carol, alice, seq),
escrow::comp_allowance(allowance),
fee(finishFee),
ter(tecCRYPTOCONDITION_ERROR));
// fulfillment provided, function fails
env(escrow::finish(carol, alice, seq),
escrow::condition(escrow::cb1),
escrow::fulfillment(escrow::fb1),
escrow::comp_allowance(allowance),
fee(conditionFinishFee),
ter(tecWASM_REJECTED));
if (BEAST_EXPECT(env.meta()->isFieldPresent(sfGasUsed)))
BEAST_EXPECTS(
env.meta()->getFieldU32(sfGasUsed) == allowance,
std::to_string(env.meta()->getFieldU32(sfGasUsed)));
env.close();
// no fulfillment provided, function succeeds
env(escrow::finish(alice, alice, seq),
escrow::comp_allowance(allowance),
fee(conditionFinishFee),
ter(tecCRYPTOCONDITION_ERROR));
// wrong fulfillment provided, function succeeds
env(escrow::finish(alice, alice, seq),
escrow::condition(escrow::cb1),
escrow::fulfillment(escrow::fb2),
escrow::comp_allowance(allowance),
fee(conditionFinishFee),
ter(tecCRYPTOCONDITION_ERROR));
// fulfillment provided, function succeeds, tx succeeds
env(escrow::finish(alice, alice, seq),
escrow::condition(escrow::cb1),
escrow::fulfillment(escrow::fb1),
escrow::comp_allowance(allowance),
fee(conditionFinishFee),
ter(tesSUCCESS));
auto const txMeta = env.meta();
if (BEAST_EXPECT(txMeta->isFieldPresent(sfGasUsed)))
BEAST_EXPECT(txMeta->getFieldU32(sfGasUsed) == allowance);
if (BEAST_EXPECT(txMeta->isFieldPresent(sfWasmReturnCode)))
BEAST_EXPECTS(
txMeta->getFieldI32(sfWasmReturnCode) == 6,
std::to_string(txMeta->getFieldI32(sfWasmReturnCode)));
env.close();
BEAST_EXPECT(env.ownerCount(alice) == 0);
}
}
{
// FinishFunction + FinishAfter
Env env(*this, features);
// create escrow
env.fund(XRP(5000), alice, carol);
auto const seq = env.seq(alice);
BEAST_EXPECT(env.ownerCount(alice) == 0);
auto const ts = env.now() + 97s;
env(escrowCreate,
escrow::finish_function(wasmHex),
escrow::finish_time(ts),
escrow::cancel_time(env.now() + 1000s),
fee(createFee));
env.close();
if (BEAST_EXPECT(env.ownerCount(alice) == 2))
{
env.require(balance(alice, XRP(4000) - createFee));
env.require(balance(carol, XRP(5000)));
// finish time hasn't passed, function fails
env(escrow::finish(carol, alice, seq),
escrow::comp_allowance(allowance),
fee(finishFee + 1),
ter(tecNO_PERMISSION));
env.close();
// finish time hasn't passed, function succeeds
for (; env.now() < ts; env.close())
env(escrow::finish(carol, alice, seq),
escrow::comp_allowance(allowance),
fee(finishFee + 2),
ter(tecNO_PERMISSION));
env(escrow::finish(carol, alice, seq),
escrow::comp_allowance(allowance),
fee(finishFee + 1),
ter(tesSUCCESS));
auto const txMeta = env.meta();
if (BEAST_EXPECT(txMeta->isFieldPresent(sfGasUsed)))
BEAST_EXPECT(txMeta->getFieldU32(sfGasUsed) == allowance);
if (BEAST_EXPECT(txMeta->isFieldPresent(sfWasmReturnCode)))
BEAST_EXPECTS(
txMeta->getFieldI32(sfWasmReturnCode) == 13,
std::to_string(txMeta->getFieldI32(sfWasmReturnCode)));
BEAST_EXPECT(env.ownerCount(alice) == 0);
}
}
{
// FinishFunction + FinishAfter #2
Env env(*this, features);
// create escrow
env.fund(XRP(5000), alice, carol);
auto const seq = env.seq(alice);
BEAST_EXPECT(env.ownerCount(alice) == 0);
env(escrowCreate,
escrow::finish_function(wasmHex),
escrow::finish_time(env.now() + 2s),
escrow::cancel_time(env.now() + 100s),
fee(createFee));
// Don't close the ledger here
if (BEAST_EXPECT(env.ownerCount(alice) == 2))
{
env.require(balance(alice, XRP(4000) - createFee));
env.require(balance(carol, XRP(5000)));
// finish time hasn't passed, function fails
env(escrow::finish(carol, alice, seq),
escrow::comp_allowance(allowance),
fee(finishFee),
ter(tecNO_PERMISSION));
env.close();
// finish time has passed, function fails
env(escrow::finish(carol, alice, seq),
escrow::comp_allowance(allowance),
fee(finishFee),
ter(tecWASM_REJECTED));
if (BEAST_EXPECT(env.meta()->isFieldPresent(sfGasUsed)))
BEAST_EXPECTS(
env.meta()->getFieldU32(sfGasUsed) == allowance,
std::to_string(env.meta()->getFieldU32(sfGasUsed)));
env.close();
// finish time has passed, function succeeds, tx succeeds
env(escrow::finish(carol, alice, seq),
escrow::comp_allowance(allowance),
fee(finishFee),
ter(tesSUCCESS));
auto const txMeta = env.meta();
if (BEAST_EXPECT(txMeta->isFieldPresent(sfGasUsed)))
BEAST_EXPECT(txMeta->getFieldU32(sfGasUsed) == allowance);
if (BEAST_EXPECT(txMeta->isFieldPresent(sfWasmReturnCode)))
BEAST_EXPECTS(
txMeta->getFieldI32(sfWasmReturnCode) == 6,
std::to_string(txMeta->getFieldI32(sfWasmReturnCode)));
env.close();
BEAST_EXPECT(env.ownerCount(alice) == 0);
}
}
}
void
testUpdateDataOnFailure(FeatureBitset features)
{
testcase("Update escrow data on failure");
using namespace jtx;
using namespace std::chrono;
// wasm that always fails
static auto const wasmHex = updateDataWasmHex;
Account const alice{"alice"};
Account const carol{"carol"};
Env env(*this, features);
// create escrow
env.fund(XRP(5000), alice);
auto const seq = env.seq(alice);
BEAST_EXPECT(env.ownerCount(alice) == 0);
auto escrowCreate = escrow::create(alice, alice, XRP(1000));
XRPAmount txnFees =
env.current()->fees().base * 10 + wasmHex.size() / 2 * 5;
env(escrowCreate,
escrow::finish_function(wasmHex),
escrow::finish_time(env.now() + 2s),
escrow::cancel_time(env.now() + 100s),
fee(txnFees));
env.close();
env.close();
env.close();
if (BEAST_EXPECT(
env.ownerCount(alice) == (1 + wasmHex.size() / 2 / 500)))
{
env.require(balance(alice, XRP(4000) - txnFees));
auto const allowance = 14;
XRPAmount const finishFee = env.current()->fees().base +
(allowance * env.current()->fees().gasPrice) /
MICRO_DROPS_PER_DROP +
1;
// FinishAfter time hasn't passed
env(escrow::finish(alice, alice, seq),
escrow::comp_allowance(allowance),
fee(finishFee),
ter(tecWASM_REJECTED));
auto const txMeta = env.meta();
if (BEAST_EXPECT(txMeta && txMeta->isFieldPresent(sfGasUsed)))
BEAST_EXPECTS(
txMeta->getFieldU32(sfGasUsed) == allowance,
std::to_string(txMeta->getFieldU32(sfGasUsed)));
if (BEAST_EXPECT(txMeta->isFieldPresent(sfWasmReturnCode)))
BEAST_EXPECTS(
txMeta->getFieldI32(sfWasmReturnCode) == -256,
std::to_string(txMeta->getFieldI32(sfWasmReturnCode)));
auto const sle = env.le(keylet::escrow(alice, seq));
if (BEAST_EXPECT(sle && sle->isFieldPresent(sfData)))
BEAST_EXPECTS(
checkVL(sle, sfData, "Data"),
strHex(sle->getFieldVL(sfData)));
}
}
void
testAllHostFunctions(FeatureBitset features)
{
testcase("Test all host functions");
using namespace jtx;
using namespace std::chrono;
// TODO: create wasm module for all host functions
static auto wasmHex = allHostFunctionsWasmHex;
Account const alice{"alice"};
Account const carol{"carol"};
{
Env env(*this, features);
// create escrow
env.fund(XRP(5000), alice, carol);
auto const seq = env.seq(alice);
BEAST_EXPECT(env.ownerCount(alice) == 0);
auto escrowCreate = escrow::create(alice, carol, XRP(1000));
XRPAmount txnFees =
env.current()->fees().base * 10 + wasmHex.size() / 2 * 5;
env(escrowCreate,
escrow::finish_function(wasmHex),
escrow::finish_time(env.now() + 11s),
escrow::cancel_time(env.now() + 100s),
escrow::data("1000000000"), // 1000 XRP in drops
fee(txnFees));
env.close();
if (BEAST_EXPECT(
env.ownerCount(alice) == (1 + wasmHex.size() / 2 / 500)))
{
env.require(balance(alice, XRP(4000) - txnFees));
env.require(balance(carol, XRP(5000)));
auto const allowance = 1'000'000;
XRPAmount const finishFee = env.current()->fees().base +
(allowance * env.current()->fees().gasPrice) /
MICRO_DROPS_PER_DROP +
1;
// FinishAfter time hasn't passed
env(escrow::finish(carol, alice, seq),
escrow::comp_allowance(allowance),
fee(finishFee),
ter(tecNO_PERMISSION));
env.close();
env.close();
env.close();
// reduce the destination balance
env(pay(carol, alice, XRP(4500)));
env.close();
env.close();
env(escrow::finish(alice, alice, seq),
escrow::comp_allowance(allowance),
fee(finishFee),
ter(tesSUCCESS));
auto const txMeta = env.meta();
if (BEAST_EXPECT(txMeta && txMeta->isFieldPresent(sfGasUsed)))
BEAST_EXPECTS(
txMeta->getFieldU32(sfGasUsed) == 794,
std::to_string(txMeta->getFieldU32(sfGasUsed)));
if (BEAST_EXPECT(txMeta->isFieldPresent(sfWasmReturnCode)))
BEAST_EXPECT(txMeta->getFieldI32(sfWasmReturnCode) == 1);
env.close();
BEAST_EXPECT(env.ownerCount(alice) == 0);
}
}
}
void
testKeyletHostFunctions(FeatureBitset features)
{
testcase("Test all keylet host functions");
using namespace jtx;
using namespace std::chrono;
// TODO: create wasm module for all host functions
static auto wasmHex = allKeyletsWasmHex;
Account const alice{"alice"};
Account const carol{"carol"};
{
Env env{*this};
env.fund(XRP(10000), alice, carol);
BEAST_EXPECT(env.seq(alice) == 4);
BEAST_EXPECT(env.ownerCount(alice) == 0);
// base objects that need to be created first
auto const tokenId =
token::getNextID(env, alice, 0, tfTransferable);
env(token::mint(alice, 0u), txflags(tfTransferable));
env(trust(alice, carol["USD"](1'000'000)));
env.close();
BEAST_EXPECT(env.seq(alice) == 6);
BEAST_EXPECT(env.ownerCount(alice) == 2);
// set up a bunch of objects to check their keylets
AMM amm(env, carol, XRP(10), carol["USD"](1000));
env(check::create(alice, carol, XRP(100)));
env(credentials::create(alice, alice, "termsandconditions"));
env(delegate::set(alice, carol, {"TrustSet"}));
env(deposit::auth(alice, carol));
env(did::set(alice), did::data("alice_did"));
env(escrow::create(alice, carol, XRP(100)),
escrow::finish_time(env.now() + 100s));
MPTTester mptTester{env, alice, {.fund = false}};
mptTester.create();
mptTester.authorize({.account = carol});
env(token::createOffer(carol, tokenId, XRP(100)),
token::owner(alice));
env(offer(alice, carol["GBP"](0.1), XRP(100)));
env(paychan::create(alice, carol, XRP(1000), 100s, alice.pk()));
pdomain::Credentials credentials{{alice, "first credential"}};
env(pdomain::setTx(alice, credentials));
env(signers(alice, 1, {{carol, 1}}));
env(ticket::create(alice, 1));
Vault vault{env};
auto [tx, _keylet] =
vault.create({.owner = alice, .asset = xrpIssue()});
env(tx);
env.close();
BEAST_EXPECTS(
env.ownerCount(alice) == 17,
std::to_string(env.ownerCount(alice)));
if (BEAST_EXPECTS(
env.seq(alice) == 20, std::to_string(env.seq(alice))))
{
auto const seq = env.seq(alice);
XRPAmount txnFees =
env.current()->fees().base * 10 + wasmHex.size() / 2 * 5;
env(escrow::create(alice, carol, XRP(1000)),
escrow::finish_function(wasmHex),
escrow::finish_time(env.now() + 2s),
escrow::cancel_time(env.now() + 100s),
fee(txnFees));
env.close();
env.close();
env.close();
auto const allowance = 2'985;
auto const finishFee = env.current()->fees().base +
(allowance * env.current()->fees().gasPrice) /
MICRO_DROPS_PER_DROP +
1;
env(escrow::finish(carol, alice, seq),
escrow::comp_allowance(allowance),
fee(finishFee));
env.close();
auto const txMeta = env.meta();
if (BEAST_EXPECT(txMeta && txMeta->isFieldPresent(sfGasUsed)))
{
auto const gasUsed = txMeta->getFieldU32(sfGasUsed);
BEAST_EXPECTS(
gasUsed == allowance, std::to_string(gasUsed));
}
BEAST_EXPECTS(
env.ownerCount(alice) == 17,
std::to_string(env.ownerCount(alice)));
}
}
}
void
testWithFeats(FeatureBitset features)
{
testCreateFinishFunctionPreflight(features);
testFinishWasmFailures(features);
testFinishFunction(features);
testUpdateDataOnFailure(features);
// TODO: Update module with new host functions
testAllHostFunctions(features);
testKeyletHostFunctions(features);
}
public:
void
run() override
{
using namespace test::jtx;
FeatureBitset const all{testable_amendments()};
testWithFeats(all);
}
};
BEAST_DEFINE_TESTSUITE(EscrowSmart, app, ripple);
} // namespace test
} // namespace ripple

View File

@@ -1507,7 +1507,7 @@ struct Escrow_test : public beast::unit_test::suite
Account const alice{"alice"};
Account const bob{"bob"};
Account const carol{"carol"};
Account const dillon{"dillon "};
Account const dillon{"dillon"};
Account const zelda{"zelda"};
char const credType[] = "abcde";
@@ -1674,6 +1674,8 @@ public:
FeatureBitset const all{testable_amendments()};
testWithFeats(all);
testWithFeats(all - featureTokenEscrow);
testWithFeats(all - featureSmartEscrow);
testWithFeats(all - featureTokenEscrow - featureSmartEscrow);
testTags(all - fixIncludeKeyletFields);
}
};

View File

@@ -1,22 +1,3 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <test/jtx.h>
#include <xrpld/app/wasm/HostFuncImpl.h>

View File

@@ -38,14 +38,14 @@ public:
auto const jtx = env.jt(ticket::create(alice, 1), seq(1), fee(10));
auto transacionId = jtx.stx->getTransactionID();
auto transactionId = jtx.stx->getTransactionID();
env.app().getHashRouter().setFlags(
transacionId, HashRouterFlags::HELD);
transactionId, HashRouterFlags::HELD);
env(jtx, json(jss::Sequence, 1), ter(terNO_ACCOUNT));
env.app().getHashRouter().setFlags(
transacionId, HashRouterFlags::BAD);
transactionId, HashRouterFlags::BAD);
env.close();
}

View File

@@ -33,6 +33,12 @@ struct PseudoTx_test : public beast::unit_test::suite
obj[sfReserveIncrement] = 0;
obj[sfReferenceFeeUnits] = 0;
}
if (rules.enabled(featureSmartEscrow))
{
obj[sfExtensionComputeLimit] = 0;
obj[sfExtensionSizeLimit] = 0;
obj[sfGasPrice] = 0;
}
}));
res.emplace_back(STTx(ttAMENDMENT, [&](auto& obj) {
@@ -101,7 +107,9 @@ struct PseudoTx_test : public beast::unit_test::suite
FeatureBitset const all{testable_amendments()};
FeatureBitset const xrpFees{featureXRPFees};
testPrevented(all - featureXRPFees - featureSmartEscrow);
testPrevented(all - featureXRPFees);
testPrevented(all - featureSmartEscrow);
testPrevented(all);
testAllowed();
}

View File

@@ -1,22 +1,3 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <test/app/wasm_fixtures/fixtures.h>
#include <test/jtx.h>
@@ -27,6 +8,7 @@
#include <xrpl/protocol/digest.h>
#include <xrpl/ledger/detail/ApplyViewBase.h>
#include <xrpl/protocol/digest.h>
namespace ripple {

View File

@@ -85,6 +85,76 @@ auto const condition = JTxFieldWrapper<blobField>(sfCondition);
auto const fulfillment = JTxFieldWrapper<blobField>(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 <size_t N>
explicit finish_function(std::array<std::uint8_t, N> 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 <size_t N>
explicit data(std::array<std::uint8_t, N> 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

View File

@@ -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;

View File

@@ -515,6 +515,22 @@ public:
if (jv.isMember(jss::reserve_inc) != isFlagLedger)
return false;
if (env.closed()->rules().enabled(featureSmartEscrow))
{
if (jv.isMember(jss::extension_compute) != isFlagLedger)
return false;
if (jv.isMember(jss::extension_size) != isFlagLedger)
return false;
}
else
{
if (jv.isMember(jss::extension_compute))
return false;
if (jv.isMember(jss::extension_size))
return false;
}
return true;
};
@@ -1579,7 +1595,8 @@ public:
testTransactions_APIv1();
testTransactions_APIv2();
testManifests();
testValidations(all - xrpFees);
testValidations(all - featureXRPFees - featureSmartEscrow);
testValidations(all - featureSmartEscrow);
testValidations(all);
testSubErrors(true);
testSubErrors(false);

View File

@@ -202,6 +202,15 @@ Ledger::Ledger(
sle->at(sfReserveIncrement) = *f;
sle->at(sfReferenceFeeUnits) = Config::FEE_UNITS_DEPRECATED;
}
if (std::find(
amendments.begin(), amendments.end(), featureSmartEscrow) !=
amendments.end())
{
sle->at(sfExtensionComputeLimit) =
config.FEES.extension_compute_limit;
sle->at(sfExtensionSizeLimit) = config.FEES.extension_size_limit;
sle->at(sfGasPrice) = config.FEES.gas_price;
}
rawInsert(sle);
}
@@ -595,6 +604,7 @@ Ledger::setup()
{
bool oldFees = false;
bool newFees = false;
bool extensionFees = false;
{
auto const baseFee = sle->at(~sfBaseFee);
auto const reserveBase = sle->at(~sfReserveBase);
@@ -612,6 +622,7 @@ Ledger::setup()
auto const reserveBaseXRP = sle->at(~sfReserveBaseDrops);
auto const reserveIncrementXRP =
sle->at(~sfReserveIncrementDrops);
auto assign = [&ret](
XRPAmount& dest,
std::optional<STAmount> const& src) {
@@ -628,12 +639,35 @@ Ledger::setup()
assign(fees_.increment, reserveIncrementXRP);
newFees = baseFeeXRP || reserveBaseXRP || reserveIncrementXRP;
}
{
auto const extensionComputeLimit =
sle->at(~sfExtensionComputeLimit);
auto const extensionSizeLimit = sle->at(~sfExtensionSizeLimit);
auto const gasPrice = sle->at(~sfGasPrice);
auto assign = [](std::uint32_t& dest,
std::optional<std::uint32_t> const& src) {
if (src)
{
dest = src.value();
}
};
assign(fees_.extensionComputeLimit, extensionComputeLimit);
assign(fees_.extensionSizeLimit, extensionSizeLimit);
assign(fees_.gasPrice, gasPrice);
extensionFees =
extensionComputeLimit || extensionSizeLimit || gasPrice;
}
if (oldFees && newFees)
// Should be all of one or the other, but not both
ret = false;
if (!rules_.enabled(featureXRPFees) && newFees)
// Can't populate the new fees before the amendment is enabled
ret = false;
if (!rules_.enabled(featureSmartEscrow) && extensionFees)
// Can't populate the extension fees before the amendment is
// enabled
ret = false;
}
}
catch (SHAMapMissingNode const&)
@@ -652,15 +686,23 @@ Ledger::setup()
void
Ledger::defaultFees(Config const& config)
{
XRPL_ASSERT(
fees_.base == 0 && fees_.reserve == 0 && fees_.increment == 0,
"ripple::Ledger::defaultFees : zero fees");
assert(
fees_.base == 0 && fees_.reserve == 0 && fees_.increment == 0 &&
fees_.extensionComputeLimit == 0 && fees_.extensionSizeLimit == 0 &&
fees_.gasPrice == 0);
if (fees_.base == 0)
fees_.base = config.FEES.reference_fee;
if (fees_.reserve == 0)
fees_.reserve = config.FEES.account_reserve;
if (fees_.increment == 0)
fees_.increment = config.FEES.owner_reserve;
if (fees_.extensionComputeLimit == 0)
fees_.extensionComputeLimit = config.FEES.extension_compute_limit;
if (fees_.extensionSizeLimit == 0)
fees_.extensionSizeLimit = config.FEES.extension_size_limit;
if (fees_.gasPrice == 0)
fees_.gasPrice = config.FEES.gas_price;
}
std::shared_ptr<SLE>

View File

@@ -2465,6 +2465,18 @@ NetworkOPsImp::pubValidation(std::shared_ptr<STValidation> const& val)
reserveIncXRP && reserveIncXRP->native())
jvObj[jss::reserve_inc] = reserveIncXRP->xrp().jsonClipped();
if (auto const extensionComputeLimit =
~val->at(~sfExtensionComputeLimit);
extensionComputeLimit)
jvObj[jss::extension_compute] = *extensionComputeLimit;
if (auto const extensionSizeLimit = ~val->at(~sfExtensionSizeLimit);
extensionSizeLimit)
jvObj[jss::extension_size] = *extensionSizeLimit;
if (auto const gasPrice = ~val->at(~sfGasPrice); gasPrice)
jvObj[jss::gas_price] = *gasPrice;
// NOTE Use MultiApiJson to publish two slightly different JSON objects
// for consumers supporting different API versions
MultiApiJson multiObj{jvObj};
@@ -2914,11 +2926,20 @@ NetworkOPsImp::getServerInfo(bool human, bool admin, bool counters)
l[jss::seq] = Json::UInt(lpClosed->info().seq);
l[jss::hash] = to_string(lpClosed->info().hash);
bool const smartEscrowEnabled =
lpClosed->rules().enabled(featureSmartEscrow);
if (!human)
{
l[jss::base_fee] = baseFee.jsonClipped();
l[jss::reserve_base] = lpClosed->fees().reserve.jsonClipped();
l[jss::reserve_inc] = lpClosed->fees().increment.jsonClipped();
if (smartEscrowEnabled)
{
l[jss::extension_compute] =
lpClosed->fees().extensionComputeLimit;
l[jss::extension_size] = lpClosed->fees().extensionSizeLimit;
l[jss::gas_price] = lpClosed->fees().gasPrice;
}
l[jss::close_time] = Json::Value::UInt(
lpClosed->info().closeTime.time_since_epoch().count());
}
@@ -2927,6 +2948,13 @@ NetworkOPsImp::getServerInfo(bool human, bool admin, bool counters)
l[jss::base_fee_xrp] = baseFee.decimalXRP();
l[jss::reserve_base_xrp] = lpClosed->fees().reserve.decimalXRP();
l[jss::reserve_inc_xrp] = lpClosed->fees().increment.decimalXRP();
if (smartEscrowEnabled)
{
l[jss::extension_compute] =
lpClosed->fees().extensionComputeLimit;
l[jss::extension_size] = lpClosed->fees().extensionSizeLimit;
l[jss::gas_price] = lpClosed->fees().gasPrice;
}
if (auto const closeOffset = app_.timeKeeper().closeOffset();
std::abs(closeOffset.count()) >= 60)
@@ -3120,6 +3148,14 @@ NetworkOPsImp::pubLedger(std::shared_ptr<ReadView const> const& lpAccepted)
jvObj[jss::reserve_base] = lpAccepted->fees().reserve.jsonClipped();
jvObj[jss::reserve_inc] =
lpAccepted->fees().increment.jsonClipped();
if (lpAccepted->rules().enabled(featureSmartEscrow))
{
jvObj[jss::extension_compute] =
lpAccepted->fees().extensionComputeLimit;
jvObj[jss::extension_size] =
lpAccepted->fees().extensionSizeLimit;
jvObj[jss::gas_price] = lpAccepted->fees().gasPrice;
}
jvObj[jss::txn_count] = Json::UInt(alpAccepted->size());
@@ -3490,8 +3526,8 @@ NetworkOPsImp::pubAccountTransaction(
}
JLOG(m_journal.trace())
<< "pubAccountTransaction: "
<< "proposed=" << iProposed << ", accepted=" << iAccepted;
<< "pubAccountTransaction: " << "proposed=" << iProposed
<< ", accepted=" << iAccepted;
if (!notify.empty() || !accountHistoryNotify.empty())
{
@@ -4192,12 +4228,19 @@ NetworkOPsImp::subLedger(InfoSub::ref isrListener, Json::Value& jvResult)
jvResult[jss::ledger_hash] = to_string(lpClosed->info().hash);
jvResult[jss::ledger_time] = Json::Value::UInt(
lpClosed->info().closeTime.time_since_epoch().count());
jvResult[jss::network_id] = app_.config().NETWORK_ID;
if (!lpClosed->rules().enabled(featureXRPFees))
jvResult[jss::fee_ref] = Config::FEE_UNITS_DEPRECATED;
jvResult[jss::fee_base] = lpClosed->fees().base.jsonClipped();
jvResult[jss::reserve_base] = lpClosed->fees().reserve.jsonClipped();
jvResult[jss::reserve_inc] = lpClosed->fees().increment.jsonClipped();
jvResult[jss::network_id] = app_.config().NETWORK_ID;
if (lpClosed->rules().enabled(featureSmartEscrow))
{
jvResult[jss::extension_compute] =
lpClosed->fees().extensionComputeLimit;
jvResult[jss::extension_size] = lpClosed->fees().extensionSizeLimit;
jvResult[jss::gas_price] = lpClosed->fees().gasPrice;
}
}
if ((mMode >= OperatingMode::SYNCING) && !isNeedNetworkLedger())

View File

@@ -40,6 +40,11 @@ ApplyContext::discard()
std::optional<TxMeta>
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);
}

View File

@@ -87,6 +87,20 @@ public:
view_->deliver(amount);
}
/** Sets the gas used in the metadata */
void
setGasUsed(std::uint32_t const gasUsed)
{
gasUsed_ = gasUsed;
}
/** Sets the gas used in the metadata */
void
setWasmReturnCode(std::int32_t const wasmReturnCode)
{
wasmReturnCode_ = wasmReturnCode;
}
/** Discard changes and start fresh. */
void
discard();
@@ -138,6 +152,8 @@ private:
// The ID of the batch transaction we are executing under, if seated.
std::optional<uint256 const> parentBatchId_;
std::optional<std::uint32_t> gasUsed_;
std::optional<std::int32_t> wasmReturnCode_;
};
} // namespace ripple

View File

@@ -1,6 +1,8 @@
#include <xrpld/app/misc/HashRouter.h>
#include <xrpld/app/tx/detail/Escrow.h>
#include <xrpld/app/tx/detail/MPTokenAuthorize.h>
#include <xrpld/app/wasm/HostFuncImpl.h>
#include <xrpld/app/wasm/WasmVM.h>
#include <xrpld/conditions/Condition.h>
#include <xrpld/conditions/Fulfillment.h>
@@ -99,6 +101,30 @@ escrowCreatePreflightHelper<MPTIssue>(PreflightContext const& ctx)
return tesSUCCESS;
}
XRPAmount
EscrowCreate::calculateBaseFee(ReadView const& view, STTx const& tx)
{
XRPAmount txnFees{Transactor::calculateBaseFee(view, tx)};
if (tx.isFieldPresent(sfFinishFunction))
{
// 10 base fees for the transaction (1 is in
// `Transactor::calculateBaseFee`), plus 5 drops per byte
txnFees += 9 * view.fees().base + 5 * tx[sfFinishFunction].size();
}
return txnFees;
}
bool
EscrowCreate::checkExtraFeatures(PreflightContext const& ctx)
{
if ((ctx.tx.isFieldPresent(sfFinishFunction) ||
ctx.tx.isFieldPresent(sfData)) &&
!ctx.rules.enabled(featureSmartEscrow))
return false;
return true;
}
NotTEC
EscrowCreate::preflight(PreflightContext const& ctx)
{
@@ -132,12 +158,21 @@ EscrowCreate::preflight(PreflightContext const& ctx)
ctx.tx[sfCancelAfter] <= ctx.tx[sfFinishAfter])
return temBAD_EXPIRATION;
if (ctx.tx.isFieldPresent(sfFinishFunction) &&
!ctx.tx.isFieldPresent(sfCancelAfter))
return temBAD_EXPIRATION;
// In the absence of a FinishAfter, the escrow can be finished
// immediately, which can be confusing. When creating an escrow,
// we want to ensure that either a FinishAfter time is explicitly
// specified or a completion condition is attached.
if (!ctx.tx[~sfFinishAfter] && !ctx.tx[~sfCondition])
if (!ctx.tx[~sfFinishAfter] && !ctx.tx[~sfCondition] &&
!ctx.tx[~sfFinishFunction])
{
JLOG(ctx.j.debug()) << "Must have at least one of FinishAfter, "
"Condition, or FinishFunction.";
return temMALFORMED;
}
if (auto const cb = ctx.tx[~sfCondition])
{
@@ -155,6 +190,43 @@ EscrowCreate::preflight(PreflightContext const& ctx)
}
}
if (ctx.tx.isFieldPresent(sfData))
{
if (!ctx.tx.isFieldPresent(sfFinishFunction))
{
JLOG(ctx.j.debug())
<< "EscrowCreate with Data requires FinishFunction";
return temMALFORMED;
}
auto const data = ctx.tx.getFieldVL(sfData);
if (data.size() > maxWasmDataLength)
{
JLOG(ctx.j.debug()) << "EscrowCreate.Data bad size " << data.size();
return temMALFORMED;
}
}
if (ctx.tx.isFieldPresent(sfFinishFunction))
{
auto const code = ctx.tx.getFieldVL(sfFinishFunction);
if (code.size() == 0 ||
code.size() > ctx.app.config().FEES.extension_size_limit)
{
JLOG(ctx.j.debug())
<< "EscrowCreate.FinishFunction bad size " << code.size();
return temMALFORMED;
}
HostFunctions mock;
auto const re =
preflightEscrowWasm(code, ESCROW_FUNCTION_NAME, {}, &mock, ctx.j);
if (!isTesSuccess(re))
{
JLOG(ctx.j.debug()) << "EscrowCreate.FinishFunction bad WASM";
return re;
}
}
return tesSUCCESS;
}
@@ -413,6 +485,17 @@ escrowLockApplyHelper<MPTIssue>(
return tesSUCCESS;
}
template <class T>
static uint32_t
calculateAdditionalReserve(T const& finishFunction)
{
if (!finishFunction)
return 1;
// First 500 bytes included in the normal reserve
// Each additional 500 bytes requires an additional reserve
return 1 + (finishFunction->size() / 500);
}
TER
EscrowCreate::doApply()
{
@@ -430,9 +513,11 @@ EscrowCreate::doApply()
// Check reserve and funds availability
STAmount const amount{ctx_.tx[sfAmount]};
auto const reserveToAdd =
calculateAdditionalReserve(ctx_.tx[~sfFinishFunction]);
auto const reserve =
ctx_.view().fees().accountReserve((*sle)[sfOwnerCount] + 1);
ctx_.view().fees().accountReserve((*sle)[sfOwnerCount] + reserveToAdd);
if (mSourceBalance < reserve)
return tecINSUFFICIENT_RESERVE;
@@ -467,6 +552,8 @@ EscrowCreate::doApply()
(*slep)[~sfCancelAfter] = ctx_.tx[~sfCancelAfter];
(*slep)[~sfFinishAfter] = ctx_.tx[~sfFinishAfter];
(*slep)[~sfDestinationTag] = ctx_.tx[~sfDestinationTag];
(*slep)[~sfFinishFunction] = ctx_.tx[~sfFinishFunction];
(*slep)[~sfData] = ctx_.tx[~sfData];
if (ctx_.view().rules().enabled(fixIncludeKeyletFields))
{
@@ -536,7 +623,7 @@ EscrowCreate::doApply()
}
// increment owner count
adjustOwnerCount(ctx_.view(), sle, 1, ctx_.journal);
adjustOwnerCount(ctx_.view(), sle, reserveToAdd, ctx_.journal);
ctx_.view().update(sle);
return tesSUCCESS;
}
@@ -564,8 +651,16 @@ checkCondition(Slice f, Slice c)
bool
EscrowFinish::checkExtraFeatures(PreflightContext const& ctx)
{
return !ctx.tx.isFieldPresent(sfCredentialIDs) ||
ctx.rules.enabled(featureCredentials);
if (ctx.tx.isFieldPresent(sfCredentialIDs) &&
!ctx.rules.enabled(featureCredentials))
return false;
if (ctx.tx.isFieldPresent(sfComputationAllowance) &&
!ctx.rules.enabled(featureSmartEscrow))
{
return false;
}
return true;
}
NotTEC
@@ -577,7 +672,10 @@ EscrowFinish::preflight(PreflightContext const& ctx)
// If you specify a condition, then you must also specify
// a fulfillment.
if (static_cast<bool>(cb) != static_cast<bool>(fb))
{
JLOG(ctx.j.debug()) << "Condition != Fulfillment";
return temMALFORMED;
}
return tesSUCCESS;
}
@@ -607,6 +705,20 @@ EscrowFinish::preflightSigValidated(PreflightContext const& ctx)
}
}
if (auto const allowance = ctx.tx[~sfComputationAllowance]; allowance)
{
if (*allowance == 0)
{
return temBAD_LIMIT;
}
if (*allowance > ctx.app.config().FEES.extension_compute_limit)
{
JLOG(ctx.j.debug())
<< "ComputationAllowance too large: " << *allowance;
return temBAD_LIMIT;
}
}
if (auto const err = credentials::checkFields(ctx.tx, ctx.j);
!isTesSuccess(err))
return err;
@@ -623,7 +735,14 @@ EscrowFinish::calculateBaseFee(ReadView const& view, STTx const& tx)
{
extraFee += view.fees().base * (32 + (fb->size() / 16));
}
if (auto const allowance = tx[~sfComputationAllowance]; allowance)
{
// The extra fee is the allowance in drops, rounded up to the nearest
// whole drop.
// Integer math rounds down by default, so we add 1 to round up.
extraFee +=
((*allowance) * view.fees().gasPrice) / MICRO_DROPS_PER_DROP + 1;
}
return Transactor::calculateBaseFee(view, tx) + extraFee;
}
@@ -703,25 +822,52 @@ EscrowFinish::preclaim(PreclaimContext const& ctx)
return err;
}
if (ctx.view.rules().enabled(featureTokenEscrow))
if (ctx.view.rules().enabled(featureTokenEscrow) ||
ctx.view.rules().enabled(featureSmartEscrow))
{
// this check is done in doApply before this amendment is enabled
auto const k = keylet::escrow(ctx.tx[sfOwner], ctx.tx[sfOfferSequence]);
auto const slep = ctx.view.read(k);
if (!slep)
return tecNO_TARGET;
AccountID const dest = (*slep)[sfDestination];
STAmount const amount = (*slep)[sfAmount];
if (!isXRP(amount))
if (ctx.view.rules().enabled(featureSmartEscrow))
{
if (auto const ret = std::visit(
[&]<typename T>(T const&) {
return escrowFinishPreclaimHelper<T>(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(
[&]<typename T>(T const&) {
return escrowFinishPreclaimHelper<T>(
ctx, dest, amount);
},
amount.asset().value());
!isTesSuccess(ret))
return ret;
}
}
}
return tesSUCCESS;
@@ -956,7 +1102,8 @@ EscrowFinish::doApply()
auto const slep = ctx_.view().peek(k);
if (!slep)
{
if (ctx_.view().rules().enabled(featureTokenEscrow))
if (ctx_.view().rules().enabled(featureTokenEscrow) ||
ctx_.view().rules().enabled(featureSmartEscrow))
return tecINTERNAL; // LCOV_EXCL_LINE
return tecNO_TARGET;
@@ -974,6 +1121,20 @@ EscrowFinish::doApply()
if ((*slep)[~sfCancelAfter] && after(now, (*slep)[sfCancelAfter]))
return tecNO_PERMISSION;
AccountID const destID = (*slep)[sfDestination];
auto const sled = ctx_.view().peek(keylet::account(destID));
if (ctx_.view().rules().enabled(featureSmartEscrow))
{
// NOTE: Escrow payments cannot be used to fund accounts.
if (!sled)
return tecNO_DST;
if (auto err = verifyDepositPreauth(
ctx_.tx, ctx_.view(), account_, destID, sled, ctx_.journal);
!isTesSuccess(err))
return err;
}
// Check cryptocondition fulfillment
{
auto const id = ctx_.tx.getTransactionID();
@@ -1023,16 +1184,69 @@ EscrowFinish::doApply()
return tecCRYPTOCONDITION_ERROR;
}
// NOTE: Escrow payments cannot be used to fund accounts.
AccountID const destID = (*slep)[sfDestination];
auto const sled = ctx_.view().peek(keylet::account(destID));
if (!sled)
return tecNO_DST;
if (!ctx_.view().rules().enabled(featureSmartEscrow))
{
// NOTE: Escrow payments cannot be used to fund accounts.
if (!sled)
return tecNO_DST;
if (auto err = verifyDepositPreauth(
ctx_.tx, ctx_.view(), account_, destID, sled, ctx_.journal);
!isTesSuccess(err))
return err;
if (auto err = verifyDepositPreauth(
ctx_.tx, ctx_.view(), account_, destID, sled, ctx_.journal);
!isTesSuccess(err))
return err;
}
// Execute custom release function
if ((*slep)[~sfFinishFunction])
{
JLOG(j_.trace())
<< "The escrow has a finish function, running WASM code...";
// WASM execution
auto const wasmStr = slep->getFieldVL(sfFinishFunction);
std::vector<uint8_t> 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<uint32_t>::max())
return tecINTERNAL; // LCOV_EXCL_LINE
ctx_.setGasUsed(static_cast<uint32_t>(reCost));
if (reValue <= 0)
{
return tecWASM_REJECTED;
}
}
else
{
JLOG(j_.debug()) << "WASM Failure: " + transHuman(re.error());
return re.error();
}
}
AccountID const account = (*slep)[sfAccount];
@@ -1110,9 +1324,12 @@ EscrowFinish::doApply()
ctx_.view().update(sled);
auto const reserveToSubtract =
calculateAdditionalReserve((*slep)[~sfFinishFunction]);
// Adjust source owner count
auto const sle = ctx_.view().peek(keylet::account(account));
adjustOwnerCount(ctx_.view(), sle, -1, ctx_.journal);
adjustOwnerCount(ctx_.view(), sle, -1 * reserveToSubtract, ctx_.journal);
ctx_.view().update(sle);
// Remove escrow from ledger
@@ -1312,7 +1529,9 @@ EscrowCancel::doApply()
}
}
adjustOwnerCount(ctx_.view(), sle, -1, ctx_.journal);
auto const reserveToSubtract =
calculateAdditionalReserve((*slep)[~sfFinishFunction]);
adjustOwnerCount(ctx_.view(), sle, -1 * reserveToSubtract, ctx_.journal);
ctx_.view().update(sle);
// Remove escrow from ledger

View File

@@ -14,9 +14,15 @@ public:
{
}
static bool
checkExtraFeatures(PreflightContext const& ctx);
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
static NotTEC
preflight(PreflightContext const& ctx);

View File

@@ -1032,6 +1032,22 @@ removeExpiredCredentials(
}
}
static void
modifyWasmDataFields(
ApplyView& view,
std::vector<std::pair<uint256, Blob>> const& wasmObjects,
beast::Journal viewJ)
{
for (auto const& [index, data] : wasmObjects)
{
if (auto const sle = view.peek(keylet::escrow(index)))
{
sle->setFieldVL(sfData, data);
view.update(sle);
}
}
}
static void
removeDeletedTrustLines(
ApplyView& view,
@@ -1190,6 +1206,7 @@ Transactor::operator()()
else if (
(result == tecOVERSIZE) || (result == tecKILLED) ||
(result == tecINCOMPLETE) || (result == tecEXPIRED) ||
(result == tecWASM_REJECTED) ||
(isTecClaimHardFail(result, view().flags())))
{
JLOG(j_.trace()) << "reapplying because of " << transToken(result);
@@ -1202,13 +1219,16 @@ Transactor::operator()()
std::vector<uint256> removedTrustLines;
std::vector<uint256> expiredNFTokenOffers;
std::vector<uint256> expiredCredentials;
std::vector<std::pair<uint256, Blob>> modifiedWasmObjects;
bool const doOffers =
((result == tecOVERSIZE) || (result == tecKILLED));
bool const doLines = (result == tecINCOMPLETE);
bool const doNFTokenOffers = (result == tecEXPIRED);
bool const doCredentials = (result == tecEXPIRED);
if (doOffers || doLines || doNFTokenOffers || doCredentials)
bool const doWasmData = (result == tecWASM_REJECTED);
if (doOffers || doLines || doNFTokenOffers || doCredentials ||
doWasmData)
{
ctx_.visit([doOffers,
&removedOffers,
@@ -1217,7 +1237,9 @@ Transactor::operator()()
doNFTokenOffers,
&expiredNFTokenOffers,
doCredentials,
&expiredCredentials](
&expiredCredentials,
doWasmData,
&modifiedWasmObjects](
uint256 const& index,
bool isDelete,
std::shared_ptr<SLE const> const& before,
@@ -1252,6 +1274,13 @@ Transactor::operator()()
(before->getType() == ltCREDENTIAL))
expiredCredentials.push_back(index);
}
if (doWasmData && before && after &&
(before->getType() == ltESCROW))
{
modifiedWasmObjects.push_back(
std::make_pair(index, after->getFieldVL(sfData)));
}
});
}
@@ -1281,6 +1310,10 @@ Transactor::operator()()
removeExpiredCredentials(
view(), expiredCredentials, ctx_.app.journal("View"));
if (result == tecWASM_REJECTED)
modifyWasmDataFields(
view(), modifiedWasmObjects, ctx_.app.journal("View"));
applied = isTecClaim(result);
}

View File

@@ -54,6 +54,15 @@ struct FeeSetup
/** The per-owned item reserve requirement in drops. */
XRPAmount owner_reserve{2 * DROPS_PER_XRP};
/** The compute limit for Feature Extensions. */
std::uint32_t extension_compute_limit{1'000'000};
/** The WASM size limit for Feature Extensions. */
std::uint32_t extension_size_limit{100'000};
/** The price of 1 WASM gas, in micro-drops. */
std::uint32_t gas_price{1'000'000};
/* (Remember to update the example cfg files when changing any of these
* values.) */
};

View File

@@ -1122,6 +1122,12 @@ setup_FeeVote(Section const& section)
setup.account_reserve = temp;
if (set(temp, "owner_reserve", section))
setup.owner_reserve = temp;
if (set(temp, "extension_compute_limit", section))
setup.extension_compute_limit = temp;
if (set(temp, "extension_size_limit", section))
setup.extension_size_limit = temp;
if (set(temp, "gas_price", section))
setup.gas_price = temp;
}
return setup;
}