From 62d8d84a49e4fc491fb0ec2e451a7dbfa91e2c67 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Thu, 10 Sep 2026 14:55:08 -0400 Subject: [PATCH] feat: Add Smart Escrow fee voting Add `GasLimit`, `BytecodeSizeLimit`, and `GasPrice` to the network's votable fee settings, gated on `featureSmartEscrow`. The `.macro` and autogen plumbing for these fields landed in #8157; this adds the behavior behind them: config parsing, fee voting, the `SetFee` pseudo- transaction, `FeeSettings` genesis and load, and the RPC surfaces that report fee settings. `detail::VotableValue` in FeeVoteImpl.cpp becomes a template, since the three new settings are `std::uint32_t` rather than `XRPAmount`. `ServiceRegistry::getFees()` exposes the configured settings to code that has no `ReadView`. It has no production caller yet; the Smart Escrow transactors call it from `preflight`. --- cfg/xrpld-example.cfg | 33 ++ include/xrpl/config/Constants.h | 3 + include/xrpl/core/ServiceRegistry.h | 4 + include/xrpl/protocol/Fees.h | 24 + include/xrpl/protocol/jss.h | 3 + src/libxrpl/ledger/Ledger.cpp | 30 ++ src/libxrpl/tx/transactors/system/Change.cpp | 29 +- src/test/app/FeeVote_test.cpp | 423 ++++++++++++++++-- src/test/app/PseudoTx_test.cpp | 8 + src/test/jtx/impl/envconfig.cpp | 5 + src/test/rpc/Subscribe_test.cpp | 19 +- .../libxrpl/helpers/TestServiceRegistry.h | 19 + src/xrpld/app/main/Application.cpp | 18 + src/xrpld/app/misc/FeeVoteImpl.cpp | 140 ++++-- src/xrpld/app/misc/NetworkOPs.cpp | 34 ++ src/xrpld/core/Config.h | 15 + src/xrpld/core/detail/Config.cpp | 7 + 17 files changed, 742 insertions(+), 72 deletions(-) diff --git a/cfg/xrpld-example.cfg b/cfg/xrpld-example.cfg index 8c4ae07fb1..c7df64884d 100644 --- a/cfg/xrpld-example.cfg +++ b/cfg/xrpld-example.cfg @@ -1360,6 +1360,39 @@ # Example: # owner_reserve = 200000 # 0.2 XRP # +# gas_limit = +# +# The gas 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, xrpld will use an internal +# default. Don't change this without understanding the consequences. +# +# Example: +# gas_limit = 1000000 # 1 million gas +# +# bytecode_size_limit = +# +# The bytecode 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, xrpld will use an internal +# default. Don't change this without understanding the consequences. +# +# Example: +# bytecode_size_limit = 100000 # 100 kb +# +# gas_price = +# +# The gas price is the conversion between WASM gas and its price in drops. +# +# If this parameter is unspecified, xrpld will use an internal +# default. Don't change this without understanding the consequences. +# +# Example: +# gas_price = 1000000 # 1 drop per gas #------------------------------------------------------------------------------- # # 9. Misc Settings diff --git a/include/xrpl/config/Constants.h b/include/xrpl/config/Constants.h index c78643d6c3..19f5dc7c5b 100644 --- a/include/xrpl/config/Constants.h +++ b/include/xrpl/config/Constants.h @@ -94,6 +94,7 @@ struct Keys static constexpr auto kBbtOptions = "bbt_options"; static constexpr auto kBgThreads = "bg_threads"; static constexpr auto kBlockSize = "block_size"; + static constexpr auto kBytecodeSizeLimit = "bytecode_size_limit"; static constexpr auto kCacheAge = "cache_age"; static constexpr auto kCacheMb = "cache_mb"; static constexpr auto kCacheSize = "cache_size"; @@ -108,6 +109,8 @@ struct Keys static constexpr auto kFileSizeMult = "file_size_mult"; static constexpr auto kFilterBits = "filter_bits"; static constexpr auto kFilterFull = "filter_full"; + static constexpr auto kGasLimit = "gas_limit"; + static constexpr auto kGasPrice = "gas_price"; static constexpr auto kHardSet = "hard_set"; static constexpr auto kHighThreads = "high_threads"; static constexpr auto kHoldTime = "hold_time"; diff --git a/include/xrpl/core/ServiceRegistry.h b/include/xrpl/core/ServiceRegistry.h index 000bdaa7fa..e020842ca7 100644 --- a/include/xrpl/core/ServiceRegistry.h +++ b/include/xrpl/core/ServiceRegistry.h @@ -6,6 +6,7 @@ #include #include #include +#include #include @@ -246,6 +247,9 @@ public: virtual DatabaseCon& getWalletDB() = 0; + [[nodiscard]] virtual Fees + getFees() const = 0; + // Temporary: Get the underlying Application for functions that haven't // been migrated yet. This should be removed once all code is migrated. virtual Application& diff --git a/include/xrpl/protocol/Fees.h b/include/xrpl/protocol/Fees.h index fdfadcd8fd..119e11a0a1 100644 --- a/include/xrpl/protocol/Fees.h +++ b/include/xrpl/protocol/Fees.h @@ -10,6 +10,15 @@ namespace xrpl { // This was the reference fee units used in the old fee calculation. inline constexpr std::uint32_t kFeeUnitsDeprecated = 10; +// Number of micro-drops in one drop. +constexpr std::uint32_t microDropsPerDrop{1'000'000}; + +/** + * Maximum Feature Extension fee settings. + */ +inline constexpr std::uint32_t kMaxGasLimit{2'000'000}; +inline constexpr std::uint32_t kMaxBytecodeSizeLimit{200'000}; + /** * Reflects the fee settings for a particular ledger. * @@ -33,6 +42,21 @@ struct Fees */ XRPAmount increment{0}; + /** + * @brief Gas limit for Feature Extensions (instructions). + */ + std::uint32_t gasLimit{0}; + + /** + * @brief Bytecode size limit for Feature Extensions (bytes). + */ + std::uint32_t bytecodeSizeLimit{0}; + + /** + * @brief Price of WASM gas (micro-drops). + */ + std::uint32_t gasPrice{0}; + explicit Fees() = default; Fees(Fees const&) = default; Fees& diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index 63e877ca31..5190bad8b2 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -252,6 +252,9 @@ JSS(expected_date); // out: any (warnings) JSS(expected_date_UTC); // out: any (warnings) JSS(expected_ledger_size); // out: TxQ JSS(expiration); // out: AccountOffers, AccountChannels, ValidatorList, amm_info +JSS(gas_limit); // out: NetworkOPs +JSS(bytecode_size_limit); // out: NetworkOPs +JSS(gas_price); // out: NetworkOPs JSS(fail_hard); // in: Sign, Submit JSS(failed); // out: InboundLedger JSS(feature); // in: Feature diff --git a/src/libxrpl/ledger/Ledger.cpp b/src/libxrpl/ledger/Ledger.cpp index 188fb3cd2c..86a22eba60 100644 --- a/src/libxrpl/ledger/Ledger.cpp +++ b/src/libxrpl/ledger/Ledger.cpp @@ -198,6 +198,12 @@ Ledger::Ledger( sle->at(sfReserveIncrement) = *f; sle->at(sfReferenceFeeUnits) = kFeeUnitsDeprecated; } + if (std::ranges::find(amendments, featureSmartEscrow) != amendments.end()) + { + sle->at(sfGasLimit) = fees.gasLimit; + sle->at(sfBytecodeSizeLimit) = fees.bytecodeSizeLimit; + sle->at(sfGasPrice) = fees.gasPrice; + } rawInsert(sle); } @@ -564,6 +570,7 @@ Ledger::setup() { bool oldFees = false; bool newFees = false; + bool extensionFees = false; { auto const baseFee = sle->at(~sfBaseFee); auto const reserveBase = sle->at(~sfReserveBase); @@ -580,6 +587,7 @@ Ledger::setup() auto const baseFeeXRP = sle->at(~sfBaseFeeDrops); auto const reserveBaseXRP = sle->at(~sfReserveBaseDrops); auto const reserveIncrementXRP = sle->at(~sfReserveIncrementDrops); + auto assign = [&ret](XRPAmount& dest, std::optional const& src) { if (src) { @@ -598,6 +606,22 @@ Ledger::setup() assign(fees_.increment, reserveIncrementXRP); newFees = baseFeeXRP || reserveBaseXRP || reserveIncrementXRP; } + { + auto const gasLimit = sle->at(~sfGasLimit); + auto const bytecodeSizeLimit = sle->at(~sfBytecodeSizeLimit); + auto const gasPrice = sle->at(~sfGasPrice); + + auto assign = [](std::uint32_t& dest, std::optional const& src) { + if (src) + { + dest = src.value(); + } + }; + assign(fees_.gasLimit, gasLimit); + assign(fees_.bytecodeSizeLimit, bytecodeSizeLimit); + assign(fees_.gasPrice, gasPrice); + extensionFees = gasLimit || bytecodeSizeLimit || gasPrice; + } if (oldFees && newFees) { // Should be all of one or the other, but not both @@ -608,6 +632,12 @@ Ledger::setup() // 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&) diff --git a/src/libxrpl/tx/transactors/system/Change.cpp b/src/libxrpl/tx/transactors/system/Change.cpp index 0d50b80af4..4016696d08 100644 --- a/src/libxrpl/tx/transactors/system/Change.cpp +++ b/src/libxrpl/tx/transactors/system/Change.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -123,12 +124,22 @@ Change::preclaim(PreclaimContext const& ctx) ctx.tx.isFieldPresent(sfReserveIncrementDrops)) return temDISABLED; } - // The ttFEE transaction format defines these fields as optional, - // but they are unconditionally forbidden until FeeVoteImpl is - // updated to populate them (SmartEscrow behavioral port). - if (ctx.tx.isFieldPresent(sfGasLimit) || ctx.tx.isFieldPresent(sfBytecodeSizeLimit) || - ctx.tx.isFieldPresent(sfGasPrice)) - return temDISABLED; + if (ctx.view.rules().enabled(featureSmartEscrow)) + { + if (!ctx.tx.isFieldPresent(sfGasLimit) || + !ctx.tx.isFieldPresent(sfBytecodeSizeLimit) || + !ctx.tx.isFieldPresent(sfGasPrice)) + return temMALFORMED; + if (ctx.tx[sfGasLimit] > kMaxGasLimit || + ctx.tx[sfBytecodeSizeLimit] > kMaxBytecodeSizeLimit) + return temBAD_FEE; + } + else + { + if (ctx.tx.isFieldPresent(sfGasLimit) || + ctx.tx.isFieldPresent(sfBytecodeSizeLimit) || ctx.tx.isFieldPresent(sfGasPrice)) + return temDISABLED; + } return tesSUCCESS; case ttAMENDMENT: case ttUNL_MODIFY: @@ -290,6 +301,12 @@ Change::applyFee() set(feeObject, ctx_.tx, sfReserveBase); set(feeObject, ctx_.tx, sfReserveIncrement); } + if (view().rules().enabled(featureSmartEscrow)) + { + set(feeObject, ctx_.tx, sfGasLimit); + set(feeObject, ctx_.tx, sfBytecodeSizeLimit); + set(feeObject, ctx_.tx, sfGasPrice); + } view().update(feeObject); diff --git a/src/test/app/FeeVote_test.cpp b/src/test/app/FeeVote_test.cpp index 666dec1249..5136ef0c3b 100644 --- a/src/test/app/FeeVote_test.cpp +++ b/src/test/app/FeeVote_test.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -31,7 +32,9 @@ #include #include #include +#include #include +#include #include namespace xrpl::test { @@ -45,10 +48,17 @@ struct FeeSettingsFields std::optional baseFeeDrops = std::nullopt; std::optional reserveBaseDrops = std::nullopt; std::optional reserveIncrementDrops = std::nullopt; + std::optional gasLimit = std::nullopt; + std::optional bytecodeSizeLimit = std::nullopt; + std::optional gasPrice = std::nullopt; }; STTx -createFeeTx(Rules const& rules, std::uint32_t seq, FeeSettingsFields const& fields) +createFeeTx( + Rules const& rules, + std::uint32_t seq, + FeeSettingsFields const& fields, + bool forceAllFields = false) { auto fill = [&](auto& obj) { obj.setAccountID(sfAccount, AccountID()); @@ -76,6 +86,13 @@ createFeeTx(Rules const& rules, std::uint32_t seq, FeeSettingsFields const& fiel obj.setFieldU32( sfReferenceFeeUnits, fields.referenceFeeUnits ? *fields.referenceFeeUnits : 0); } + if (rules.enabled(featureSmartEscrow) || forceAllFields) + { + obj.setFieldU32(sfGasLimit, fields.gasLimit ? *fields.gasLimit : 0); + obj.setFieldU32( + sfBytecodeSizeLimit, fields.bytecodeSizeLimit ? *fields.bytecodeSizeLimit : 0); + obj.setFieldU32(sfGasPrice, fields.gasPrice ? *fields.gasPrice : 0); + } }; return STTx(ttFEE, fill); } @@ -124,6 +141,12 @@ createInvalidFeeTx( obj.setFieldU32(sfReserveIncrement, 50000); obj.setFieldU32(sfReferenceFeeUnits, 10); } + if (rules.enabled(featureSmartEscrow)) + { + obj.setFieldU32(sfGasLimit, 100 + uniqueValue); + obj.setFieldU32(sfBytecodeSizeLimit, 200 + uniqueValue); + obj.setFieldU32(sfGasPrice, 300 + uniqueValue); + } } // If missingRequiredFields is true, we don't add the required fields // (default behavior) @@ -131,11 +154,11 @@ createInvalidFeeTx( return STTx(ttFEE, fill); } -bool +TER applyFeeAndTestResult(jtx::Env& env, OpenView& view, STTx const& tx) { auto const res = apply(env.app(), view, tx, ApplyFlags::TapNone, env.journal); - return isTesSuccess(res.ter); + return res.ter; } bool @@ -186,6 +209,21 @@ verifyFeeObject( if (!checkEquality(sfReferenceFeeUnits, expected.referenceFeeUnits)) return false; } + if (rules.enabled(featureSmartEscrow)) + { + if (!checkEquality(sfGasLimit, expected.gasLimit.value_or(0))) + return false; + if (!checkEquality(sfBytecodeSizeLimit, expected.bytecodeSizeLimit.value_or(0))) + return false; + if (!checkEquality(sfGasPrice, expected.gasPrice.value_or(0))) + return false; + } + else + { + if (feeObject->isFieldPresent(sfGasLimit) || + feeObject->isFieldPresent(sfBytecodeSizeLimit) || feeObject->isFieldPresent(sfGasPrice)) + return false; + } return true; } @@ -208,6 +246,7 @@ class FeeVote_test : public beast::unit_test::Suite void testSetup() { + testcase("FeeVote setup"); FeeSetup const defaultSetup; { // defaults @@ -216,35 +255,63 @@ class FeeVote_test : public beast::unit_test::Suite BEAST_EXPECT(setup.referenceFee == defaultSetup.referenceFee); BEAST_EXPECT(setup.accountReserve == defaultSetup.accountReserve); BEAST_EXPECT(setup.ownerReserve == defaultSetup.ownerReserve); + BEAST_EXPECT(setup.gasLimit == defaultSetup.gasLimit); + BEAST_EXPECT(setup.bytecodeSizeLimit == defaultSetup.bytecodeSizeLimit); + BEAST_EXPECT(setup.gasPrice == defaultSetup.gasPrice); } { Section config; config.append( - {"reference_fee = 50", "account_reserve = 1234567", "owner_reserve = 1234"}); + {"reference_fee = 50", + "account_reserve = 1234567", + "owner_reserve = 1234", + "gas_limit = 100", + "bytecode_size_limit = 200", + "gas_price = 300"}); auto setup = setupFeeVote(config); BEAST_EXPECT(setup.referenceFee == 50); BEAST_EXPECT(setup.accountReserve == 1234567); BEAST_EXPECT(setup.ownerReserve == 1234); + BEAST_EXPECT(setup.gasLimit == 100); + BEAST_EXPECT(setup.bytecodeSizeLimit == 200); + BEAST_EXPECT(setup.gasPrice == 300); } { Section config; config.append( - {"reference_fee = blah", "account_reserve = yada", "owner_reserve = foo"}); + {"reference_fee = blah", + "account_reserve = yada", + "owner_reserve = foo", + "gas_limit = bar", + "bytecode_size_limit = baz", + "gas_price = qux"}); // Illegal values are ignored, and the defaults left unchanged auto setup = setupFeeVote(config); BEAST_EXPECT(setup.referenceFee == defaultSetup.referenceFee); BEAST_EXPECT(setup.accountReserve == defaultSetup.accountReserve); BEAST_EXPECT(setup.ownerReserve == defaultSetup.ownerReserve); + BEAST_EXPECT(setup.gasLimit == defaultSetup.gasLimit); + BEAST_EXPECT(setup.bytecodeSizeLimit == defaultSetup.bytecodeSizeLimit); + BEAST_EXPECT(setup.gasPrice == defaultSetup.gasPrice); } { Section config; config.append( - {"reference_fee = -50", "account_reserve = -1234567", "owner_reserve = -1234"}); - // Illegal values are ignored, and the defaults left unchanged + {"reference_fee = -50", + "account_reserve = -1234567", + "owner_reserve = -1234", + "gas_limit = -100", + "bytecode_size_limit = -200", + "gas_price = -300"}); + // Negative gas/bytecode limit values wrap past their maximum and are + // ignored. Other uint32_t fields keep the existing behavior. auto setup = setupFeeVote(config); BEAST_EXPECT(setup.referenceFee == defaultSetup.referenceFee); BEAST_EXPECT(setup.accountReserve == static_cast(-1234567)); BEAST_EXPECT(setup.ownerReserve == static_cast(-1234)); + BEAST_EXPECT(setup.gasLimit == defaultSetup.gasLimit); + BEAST_EXPECT(setup.bytecodeSizeLimit == defaultSetup.bytecodeSizeLimit); + BEAST_EXPECT(setup.gasPrice == static_cast(-300)); } { auto const big64 = std::to_string( @@ -253,12 +320,36 @@ class FeeVote_test : public beast::unit_test::Suite config.append( {"reference_fee = " + big64, "account_reserve = " + big64, - "owner_reserve = " + big64}); + "owner_reserve = " + big64, + "gas_limit = " + big64, + "bytecode_size_limit = " + big64, + "gas_price = " + big64}); // Illegal values are ignored, and the defaults left unchanged auto setup = setupFeeVote(config); BEAST_EXPECT(setup.referenceFee == defaultSetup.referenceFee); BEAST_EXPECT(setup.accountReserve == defaultSetup.accountReserve); BEAST_EXPECT(setup.ownerReserve == defaultSetup.ownerReserve); + BEAST_EXPECT(setup.gasLimit == defaultSetup.gasLimit); + BEAST_EXPECT(setup.bytecodeSizeLimit == defaultSetup.bytecodeSizeLimit); + BEAST_EXPECT(setup.gasPrice == defaultSetup.gasPrice); + } + { + Section config; + config.append( + {"gas_limit = " + std::to_string(kMaxGasLimit + 1), + "bytecode_size_limit = " + std::to_string(kMaxBytecodeSizeLimit + 1)}); + auto const setup = setupFeeVote(config); + BEAST_EXPECT(setup.gasLimit == defaultSetup.gasLimit); + BEAST_EXPECT(setup.bytecodeSizeLimit == defaultSetup.bytecodeSizeLimit); + } + { + Section config; + config.append( + {"gas_limit = " + std::to_string(kMaxGasLimit), + "bytecode_size_limit = " + std::to_string(kMaxBytecodeSizeLimit)}); + auto const setup = setupFeeVote(config); + BEAST_EXPECT(setup.gasLimit == kMaxGasLimit); + BEAST_EXPECT(setup.bytecodeSizeLimit == kMaxBytecodeSizeLimit); } } @@ -269,7 +360,7 @@ class FeeVote_test : public beast::unit_test::Suite // Test with XRPFees disabled (legacy format) { - jtx::Env env(*this, jtx::testableAmendments() - featureXRPFees); + jtx::Env env(*this, jtx::testableAmendments() - featureXRPFees - featureSmartEscrow); auto ledger = std::make_shared( kCreateGenesis, Rules{env.app().config().features}, @@ -290,7 +381,7 @@ class FeeVote_test : public beast::unit_test::Suite auto feeTx = createFeeTx(ledger->rules(), ledger->seq(), fields); OpenView accum(ledger.get()); - BEAST_EXPECT(applyFeeAndTestResult(env, accum, feeTx)); + BEAST_EXPECT(isTesSuccess(applyFeeAndTestResult(env, accum, feeTx))); accum.apply(*ledger); // Verify fee object was created/updated correctly @@ -299,7 +390,7 @@ class FeeVote_test : public beast::unit_test::Suite // Test with XRPFees enabled (new format) { - jtx::Env env(*this, jtx::testableAmendments() | featureXRPFees); + jtx::Env env(*this, jtx::testableAmendments() - featureSmartEscrow); auto ledger = std::make_shared( kCreateGenesis, Rules{env.app().config().features}, @@ -318,12 +409,105 @@ class FeeVote_test : public beast::unit_test::Suite auto feeTx = createFeeTx(ledger->rules(), ledger->seq(), fields); OpenView accum(ledger.get()); - BEAST_EXPECT(applyFeeAndTestResult(env, accum, feeTx)); + BEAST_EXPECT(isTesSuccess(applyFeeAndTestResult(env, accum, feeTx))); accum.apply(*ledger); // Verify fee object was created/updated correctly BEAST_EXPECT(verifyFeeObject(ledger, ledger->rules(), fields)); } + + // Test with both XRPFees and SmartEscrow enabled + { + jtx::Env env(*this, jtx::testableAmendments()); + auto ledger = std::make_shared( + kCreateGenesis, + Rules{env.app().config().features}, + env.app().config().fees.toFees(), + std::vector{}, + env.app().getNodeFamily()); + + // Create the next ledger to apply transaction to + ledger = std::make_shared(*ledger, env.app().getTimeKeeper().closeTime()); + + FeeSettingsFields const fields{ + .baseFeeDrops = XRPAmount{10}, + .reserveBaseDrops = XRPAmount{200000}, + .reserveIncrementDrops = XRPAmount{50000}, + .gasLimit = 100, + .bytecodeSizeLimit = 200, + .gasPrice = 300}; + // Test successful fee transaction with new fields + auto feeTx = createFeeTx(ledger->rules(), ledger->seq(), fields); + + OpenView accum(ledger.get()); + BEAST_EXPECT(isTesSuccess(applyFeeAndTestResult(env, accum, feeTx))); + accum.apply(*ledger); + + // Verify fee object was created/updated correctly + BEAST_EXPECT(verifyFeeObject(ledger, ledger->rules(), fields)); + } + + // Test that Smart Escrow limits reject values above their maximums. + { + jtx::Env env(*this, jtx::testableAmendments()); + auto ledger = std::make_shared( + kCreateGenesis, + Rules{env.app().config().features}, + env.app().config().fees.toFees(), + std::vector{}, + env.app().getNodeFamily()); + + ledger = std::make_shared(*ledger, env.app().getTimeKeeper().closeTime()); + + auto testBadFields = [&](FeeSettingsFields const& fields) { + auto feeTx = createFeeTx(ledger->rules(), ledger->seq(), fields); + OpenView accum(ledger.get()); + BEAST_EXPECT(!isTesSuccess(applyFeeAndTestResult(env, accum, feeTx))); + }; + + testBadFields( + {.baseFeeDrops = XRPAmount{10}, + .reserveBaseDrops = XRPAmount{200000}, + .reserveIncrementDrops = XRPAmount{50000}, + .gasLimit = kMaxGasLimit + 1, + .bytecodeSizeLimit = kMaxBytecodeSizeLimit, + .gasPrice = 300}); + testBadFields( + {.baseFeeDrops = XRPAmount{10}, + .reserveBaseDrops = XRPAmount{200000}, + .reserveIncrementDrops = XRPAmount{50000}, + .gasLimit = kMaxGasLimit, + .bytecodeSizeLimit = kMaxBytecodeSizeLimit + 1, + .gasPrice = 300}); + } + + // Test that the Smart Escrow fields are rejected if the + // feature is disabled + { + jtx::Env env(*this, jtx::testableAmendments() - featureSmartEscrow); + auto ledger = std::make_shared( + kCreateGenesis, + Rules{env.app().config().features}, + env.app().config().fees.toFees(), + std::vector{}, + env.app().getNodeFamily()); + + // Create the next ledger to apply transaction to + ledger = std::make_shared(*ledger, env.app().getTimeKeeper().closeTime()); + + FeeSettingsFields const fields{ + .baseFeeDrops = XRPAmount{10}, + .reserveBaseDrops = XRPAmount{200000}, + .reserveIncrementDrops = XRPAmount{50000}, + .gasLimit = 100, + .bytecodeSizeLimit = 200, + .gasPrice = 300}; + // Test successful fee transaction with new fields + auto feeTx = createFeeTx(ledger->rules(), ledger->seq(), fields, true); + + OpenView accum(ledger.get()); + BEAST_EXPECT(!isTesSuccess(applyFeeAndTestResult(env, accum, feeTx))); + } } void @@ -332,7 +516,7 @@ class FeeVote_test : public beast::unit_test::Suite testcase("Fee Transaction Validation"); { - jtx::Env env(*this, jtx::testableAmendments() - featureXRPFees); + jtx::Env env(*this, jtx::testableAmendments() - featureXRPFees - featureSmartEscrow); auto ledger = std::make_shared( kCreateGenesis, Rules{env.app().config().features}, @@ -346,15 +530,15 @@ class FeeVote_test : public beast::unit_test::Suite // Test transaction with missing required legacy fields auto invalidTx = createInvalidFeeTx(ledger->rules(), ledger->seq(), true, false, 1); OpenView accum(ledger.get()); - BEAST_EXPECT(!applyFeeAndTestResult(env, accum, invalidTx)); + BEAST_EXPECT(!isTesSuccess(applyFeeAndTestResult(env, accum, invalidTx))); // Test transaction with new format fields when XRPFees is disabled auto disallowedTx = createInvalidFeeTx(ledger->rules(), ledger->seq(), false, true, 2); - BEAST_EXPECT(!applyFeeAndTestResult(env, accum, disallowedTx)); + BEAST_EXPECT(!isTesSuccess(applyFeeAndTestResult(env, accum, disallowedTx))); } { - jtx::Env env(*this, jtx::testableAmendments() | featureXRPFees); + jtx::Env env(*this, jtx::testableAmendments() - featureSmartEscrow); auto ledger = std::make_shared( kCreateGenesis, Rules{env.app().config().features}, @@ -368,11 +552,33 @@ class FeeVote_test : public beast::unit_test::Suite // Test transaction with missing required new fields auto invalidTx = createInvalidFeeTx(ledger->rules(), ledger->seq(), true, false, 3); OpenView accum(ledger.get()); - BEAST_EXPECT(!applyFeeAndTestResult(env, accum, invalidTx)); + BEAST_EXPECT(!isTesSuccess(applyFeeAndTestResult(env, accum, invalidTx))); // Test transaction with legacy fields when XRPFees is enabled auto disallowedTx = createInvalidFeeTx(ledger->rules(), ledger->seq(), false, true, 4); - BEAST_EXPECT(!applyFeeAndTestResult(env, accum, disallowedTx)); + BEAST_EXPECT(!isTesSuccess(applyFeeAndTestResult(env, accum, disallowedTx))); + } + + { + jtx::Env env(*this, jtx::testableAmendments() | featureXRPFees | featureSmartEscrow); + auto ledger = std::make_shared( + kCreateGenesis, + Rules{env.app().config().features}, + env.app().config().fees.toFees(), + std::vector{}, + env.app().getNodeFamily()); + + // Create the next ledger to apply transaction to + ledger = std::make_shared(*ledger, env.app().getTimeKeeper().closeTime()); + + // Test transaction with missing required new fields + auto invalidTx = createInvalidFeeTx(ledger->rules(), ledger->seq(), true, false, 5); + OpenView accum(ledger.get()); + BEAST_EXPECT(!isTesSuccess(applyFeeAndTestResult(env, accum, invalidTx))); + + // Test transaction with legacy fields when XRPFees is enabled + auto disallowedTx = createInvalidFeeTx(ledger->rules(), ledger->seq(), false, true, 6); + BEAST_EXPECT(!isTesSuccess(applyFeeAndTestResult(env, accum, disallowedTx))); } } @@ -381,7 +587,7 @@ class FeeVote_test : public beast::unit_test::Suite { testcase("Pseudo Transaction Properties"); - jtx::Env env(*this, jtx::testableAmendments()); + jtx::Env env(*this, jtx::testableAmendments() - featureSmartEscrow); auto ledger = std::make_shared( kCreateGenesis, Rules{env.app().config().features}, @@ -411,7 +617,7 @@ class FeeVote_test : public beast::unit_test::Suite // But can be applied to a closed ledger { OpenView closedAccum(ledger.get()); - BEAST_EXPECT(applyFeeAndTestResult(env, closedAccum, feeTx)); + BEAST_EXPECT(isTesSuccess(applyFeeAndTestResult(env, closedAccum, feeTx))); } } @@ -420,7 +626,7 @@ class FeeVote_test : public beast::unit_test::Suite { testcase("Multiple Fee Updates"); - jtx::Env env(*this, jtx::testableAmendments() | featureXRPFees); + jtx::Env env(*this, jtx::testableAmendments() - featureSmartEscrow); auto ledger = std::make_shared( kCreateGenesis, Rules{env.app().config().features}, @@ -438,7 +644,7 @@ class FeeVote_test : public beast::unit_test::Suite { OpenView accum(ledger.get()); - BEAST_EXPECT(applyFeeAndTestResult(env, accum, feeTx1)); + BEAST_EXPECT(isTesSuccess(applyFeeAndTestResult(env, accum, feeTx1))); accum.apply(*ledger); } @@ -455,7 +661,7 @@ class FeeVote_test : public beast::unit_test::Suite { OpenView accum(ledger.get()); - BEAST_EXPECT(applyFeeAndTestResult(env, accum, feeTx2)); + BEAST_EXPECT(isTesSuccess(applyFeeAndTestResult(env, accum, feeTx2))); accum.apply(*ledger); } @@ -468,7 +674,7 @@ class FeeVote_test : public beast::unit_test::Suite { testcase("Wrong Ledger Sequence"); - jtx::Env env(*this, jtx::testableAmendments() | featureXRPFees); + jtx::Env env(*this, jtx::testableAmendments() - featureSmartEscrow); auto ledger = std::make_shared( kCreateGenesis, Rules{env.app().config().features}, @@ -491,7 +697,7 @@ class FeeVote_test : public beast::unit_test::Suite // The transaction should still succeed as long as other fields are // valid // The ledger sequence field is only used for informational purposes - BEAST_EXPECT(applyFeeAndTestResult(env, accum, feeTx)); + BEAST_EXPECT(isTesSuccess(applyFeeAndTestResult(env, accum, feeTx))); } void @@ -499,7 +705,7 @@ class FeeVote_test : public beast::unit_test::Suite { testcase("Partial Field Updates"); - jtx::Env env(*this, jtx::testableAmendments() | featureXRPFees); + jtx::Env env(*this, jtx::testableAmendments() - featureSmartEscrow); auto ledger = std::make_shared( kCreateGenesis, Rules{env.app().config().features}, @@ -517,7 +723,7 @@ class FeeVote_test : public beast::unit_test::Suite { OpenView accum(ledger.get()); - BEAST_EXPECT(applyFeeAndTestResult(env, accum, feeTx1)); + BEAST_EXPECT(isTesSuccess(applyFeeAndTestResult(env, accum, feeTx1))); accum.apply(*ledger); } @@ -532,7 +738,7 @@ class FeeVote_test : public beast::unit_test::Suite { OpenView accum(ledger.get()); - BEAST_EXPECT(applyFeeAndTestResult(env, accum, feeTx2)); + BEAST_EXPECT(isTesSuccess(applyFeeAndTestResult(env, accum, feeTx2))); accum.apply(*ledger); } @@ -545,7 +751,7 @@ class FeeVote_test : public beast::unit_test::Suite { testcase("Single Invalid Transaction"); - jtx::Env env(*this, jtx::testableAmendments() | featureXRPFees); + jtx::Env env(*this, jtx::testableAmendments() - featureSmartEscrow); auto ledger = std::make_shared( kCreateGenesis, Rules{env.app().config().features}, @@ -567,7 +773,7 @@ class FeeVote_test : public beast::unit_test::Suite }); OpenView accum(ledger.get()); - BEAST_EXPECT(!applyFeeAndTestResult(env, accum, invalidTx)); + BEAST_EXPECT(!isTesSuccess(applyFeeAndTestResult(env, accum, invalidTx))); } void @@ -584,7 +790,7 @@ class FeeVote_test : public beast::unit_test::Suite // Test with XRPFees enabled { - Env env(*this, testableAmendments() | featureXRPFees); + Env env(*this, testableAmendments() - featureSmartEscrow); auto feeVote = makeFeeVote(setup, env.app().getJournal("FeeVote")); auto ledger = std::make_shared( @@ -614,7 +820,7 @@ class FeeVote_test : public beast::unit_test::Suite // Test with XRPFees disabled (legacy format) { - Env env(*this, testableAmendments() - featureXRPFees); + Env env(*this, testableAmendments() - featureXRPFees - featureSmartEscrow); auto feeVote = makeFeeVote(setup, env.app().getJournal("FeeVote")); auto ledger = std::make_shared( @@ -654,7 +860,7 @@ class FeeVote_test : public beast::unit_test::Suite setup.accountReserve = 1234567; setup.ownerReserve = 7654321; - Env env(*this, testableAmendments() | featureXRPFees); + Env env(*this, testableAmendments() - featureSmartEscrow); // establish what the current fees are BEAST_EXPECT(env.current()->fees().base == XRPAmount{UNIT_TEST_REFERENCE_FEE}); @@ -729,6 +935,156 @@ class FeeVote_test : public beast::unit_test::Suite feeTx.getFieldAmount(sfReserveIncrementDrops) == XRPAmount{setup.ownerReserve}); } + void + testDoVotingSmartEscrow() + { + testcase("doVoting with Smart Escrow"); + + using namespace jtx; + + Env env(*this, testableAmendments() | featureXRPFees | featureSmartEscrow); + + // establish what the current fees are + BEAST_EXPECT(env.current()->fees().base == XRPAmount{UNIT_TEST_REFERENCE_FEE}); + BEAST_EXPECT(env.current()->fees().reserve == XRPAmount{200'000'000}); + BEAST_EXPECT(env.current()->fees().increment == XRPAmount{50'000'000}); + BEAST_EXPECT(env.current()->fees().gasLimit == 0); + BEAST_EXPECT(env.current()->fees().bytecodeSizeLimit == 0); + BEAST_EXPECT(env.current()->fees().gasPrice == 0); + + auto const createFeeTxFromVoting = + [&](FeeSetup const& setup) -> std::pair> { + auto feeVote = makeFeeVote(setup, env.app().getJournal("FeeVote")); + auto ledger = std::make_shared( + kCreateGenesis, + Rules{env.app().config().features}, + env.app().config().fees.toFees(), + std::vector{}, + env.app().getNodeFamily()); + + // doVoting requires a flag ledger (every 256th ledger) + // We need to create a ledger at sequence 256 to make it a flag + // ledger + for (int i = 0; i < 256 - 1; ++i) + { + ledger = std::make_shared(*ledger, env.app().getTimeKeeper().closeTime()); + } + BEAST_EXPECT(ledger->isFlagLedger()); + + // Create some mock validations with fee votes + std::vector> validations; + + for (int i = 0; i < 5; i++) + { + auto sec = randomSecretKey(); + auto pub = derivePublicKey(KeyType::Secp256k1, sec); + + auto val = std::make_shared( + env.app().getTimeKeeper().now(), + pub, + sec, + calcNodeID(pub), + [&](STValidation& v) { + v.setFieldU32(sfLedgerSequence, ledger->seq()); + // Vote for different fees than current + v.setFieldAmount(sfBaseFeeDrops, XRPAmount{setup.referenceFee}); + v.setFieldAmount(sfReserveBaseDrops, XRPAmount{setup.accountReserve}); + v.setFieldAmount(sfReserveIncrementDrops, XRPAmount{setup.ownerReserve}); + v.setFieldU32(sfGasLimit, setup.gasLimit); + v.setFieldU32(sfBytecodeSizeLimit, setup.bytecodeSizeLimit); + v.setFieldU32(sfGasPrice, setup.gasPrice); + }); + if (i % 2) + val->setTrusted(); + validations.push_back(val); + } + + auto txSet = + std::make_shared(SHAMapType::TRANSACTION, env.app().getNodeFamily()); + + // This should not throw since we have a flag ledger + feeVote->doVoting(ledger, validations, txSet); + + auto const txs = getTxs(txSet); + BEAST_EXPECT(txs.size() == 1); + return {txs[0], ledger}; + }; + + auto checkFeeTx = [&](FeeSetup const& setup, + STTx const& feeTx, + std::shared_ptr const& ledger, + std::source_location const loc = std::source_location::current()) { + auto const line = " (" + std::to_string(loc.line()) + ")"; + BEAST_EXPECTS(feeTx.getTxnType() == ttFEE, line); + + BEAST_EXPECTS(feeTx.getAccountID(sfAccount) == AccountID(), line); + BEAST_EXPECTS(feeTx.getFieldU32(sfLedgerSequence) == ledger->seq() + 1, line); + + BEAST_EXPECTS(feeTx.isFieldPresent(sfBaseFeeDrops), line); + BEAST_EXPECTS(feeTx.isFieldPresent(sfReserveBaseDrops), line); + BEAST_EXPECTS(feeTx.isFieldPresent(sfReserveIncrementDrops), line); + + // The legacy fields should NOT be present + BEAST_EXPECTS(!feeTx.isFieldPresent(sfBaseFee), line); + BEAST_EXPECTS(!feeTx.isFieldPresent(sfReserveBase), line); + BEAST_EXPECTS(!feeTx.isFieldPresent(sfReserveIncrement), line); + BEAST_EXPECTS(!feeTx.isFieldPresent(sfReferenceFeeUnits), line); + + // Check the values + BEAST_EXPECTS( + feeTx.getFieldAmount(sfBaseFeeDrops) == XRPAmount{setup.referenceFee}, line); + BEAST_EXPECTS( + feeTx.getFieldAmount(sfReserveBaseDrops) == XRPAmount{setup.accountReserve}, line); + BEAST_EXPECTS( + feeTx.getFieldAmount(sfReserveIncrementDrops) == XRPAmount{setup.ownerReserve}, + line); + BEAST_EXPECTS(feeTx.getFieldU32(sfGasLimit) == setup.gasLimit, line); + BEAST_EXPECTS(feeTx.getFieldU32(sfBytecodeSizeLimit) == setup.bytecodeSizeLimit, line); + BEAST_EXPECTS(feeTx.getFieldU32(sfGasPrice) == setup.gasPrice, line); + }; + + { + FeeSetup setup; + setup.referenceFee = 42; + setup.accountReserve = 1234567; + setup.ownerReserve = 7654321; + setup.gasLimit = 100; + setup.bytecodeSizeLimit = 200; + setup.gasPrice = 300; + auto const [feeTx, ledger] = createFeeTxFromVoting(setup); + + checkFeeTx(setup, feeTx, ledger); + } + + { + FeeSetup setup; + setup.referenceFee = 42; + setup.accountReserve = 1234567; + setup.ownerReserve = 7654321; + setup.gasLimit = 0; + setup.bytecodeSizeLimit = 0; + setup.gasPrice = 300; + auto const [feeTx, ledger] = createFeeTxFromVoting(setup); + + checkFeeTx(setup, feeTx, ledger); + } + + { + FeeSetup setup; + setup.referenceFee = 42; + setup.accountReserve = 1234567; + setup.ownerReserve = 7654321; + setup.gasLimit = kMaxGasLimit + 1; + setup.bytecodeSizeLimit = kMaxBytecodeSizeLimit + 1; + setup.gasPrice = 300; + auto const [feeTx, ledger] = createFeeTxFromVoting(setup); + + setup.gasLimit = ledger->fees().gasLimit; + setup.bytecodeSizeLimit = ledger->fees().bytecodeSizeLimit; + checkFeeTx(setup, feeTx, ledger); + } + } + void run() override { @@ -742,6 +1098,7 @@ class FeeVote_test : public beast::unit_test::Suite testSingleInvalidTransaction(); testDoValidation(); testDoVoting(); + testDoVotingSmartEscrow(); } }; diff --git a/src/test/app/PseudoTx_test.cpp b/src/test/app/PseudoTx_test.cpp index 9f65daad18..d9ace0cf49 100644 --- a/src/test/app/PseudoTx_test.cpp +++ b/src/test/app/PseudoTx_test.cpp @@ -43,6 +43,12 @@ struct PseudoTx_test : public beast::unit_test::Suite obj[sfReserveIncrement] = 0; obj[sfReferenceFeeUnits] = 0; } + if (rules.enabled(featureSmartEscrow)) + { + obj[sfGasLimit] = 0; + obj[sfBytecodeSizeLimit] = 0; + obj[sfGasPrice] = 0; + } }); res.emplace_back(ttAMENDMENT, [&](auto& obj) { @@ -107,7 +113,9 @@ struct PseudoTx_test : public beast::unit_test::Suite FeatureBitset const all{testableAmendments()}; FeatureBitset const xrpFees{featureXRPFees}; + testPrevented(all - featureXRPFees - featureSmartEscrow); testPrevented(all - featureXRPFees); + testPrevented(all - featureSmartEscrow); testPrevented(all); testAllowed(); } diff --git a/src/test/jtx/impl/envconfig.cpp b/src/test/jtx/impl/envconfig.cpp index 14690058ec..9eab91459a 100644 --- a/src/test/jtx/impl/envconfig.cpp +++ b/src/test/jtx/impl/envconfig.cpp @@ -23,9 +23,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.referenceFee = UNIT_TEST_REFERENCE_FEE; cfg.fees.accountReserve = XRP(200).value().xrp().drops(); cfg.fees.ownerReserve = XRP(50).value().xrp().drops(); + cfg.fees.gasLimit = 1'000'000; + cfg.fees.bytecodeSizeLimit = 100'000; + cfg.fees.gasPrice = 1'000'000; // 1 drop = 1,000,000 micro-drops // The Beta API (currently v2) is always available to tests cfg.betaRpcApi = true; diff --git a/src/test/rpc/Subscribe_test.cpp b/src/test/rpc/Subscribe_test.cpp index 47c2245fa5..30e299b841 100644 --- a/src/test/rpc/Subscribe_test.cpp +++ b/src/test/rpc/Subscribe_test.cpp @@ -514,6 +514,22 @@ public: if (jv.isMember(jss::reserve_inc) != isFlagLedger) return false; + if (env.closed()->rules().enabled(featureSmartEscrow)) + { + if (jv.isMember(jss::gas_limit) != isFlagLedger) + return false; + + if (jv.isMember(jss::bytecode_size_limit) != isFlagLedger) + return false; + } + else + { + if (jv.isMember(jss::gas_limit)) + return false; + + if (jv.isMember(jss::bytecode_size_limit)) + return false; + } return true; }; @@ -1976,7 +1992,8 @@ public: testTransactionsAPIv1(); testTransactionsAPIv2(); testManifests(); - testValidations(all - xrpFees); + testValidations(all - featureXRPFees - featureSmartEscrow); + testValidations(all - featureSmartEscrow); testValidations(all); testSubErrors(true); testSubErrors(false); diff --git a/src/tests/libxrpl/helpers/TestServiceRegistry.h b/src/tests/libxrpl/helpers/TestServiceRegistry.h index e763c8bde4..4122d87e0d 100644 --- a/src/tests/libxrpl/helpers/TestServiceRegistry.h +++ b/src/tests/libxrpl/helpers/TestServiceRegistry.h @@ -8,6 +8,8 @@ #include #include #include +#include +#include #include #include @@ -71,11 +73,22 @@ private: */ class TestServiceRegistry : public ServiceRegistry { + static Fees + defaultFees() + { + Fees fees{XRPAmount{10}, XRPAmount{10 * kDropsPerXrp}, XRPAmount{2 * kDropsPerXrp}}; + fees.gasLimit = 1'000'000; + fees.bytecodeSizeLimit = 100'000; + fees.gasPrice = 1'000'000; + return fees; + } + TestLogs logs_{beast::Severity::Warning}; boost::asio::io_context ioContext_; TestFamily family_{logs_.journal("TestFamily")}; LoadFeeTrack feeTrack_{logs_.journal("LoadFeeTrack")}; TestNetworkIDService networkIDService_; + Fees fees_{defaultFees()}; HashRouter hashRouter_{HashRouter::Setup{}, stopwatch()}; NodeCache tempNodeCache_{ "TempNodeCache", @@ -374,6 +387,12 @@ public: throw std::logic_error("TestServiceRegistry::getWalletDB() not implemented"); } + Fees + getFees() const override + { + return fees_; + } + // Temporary: Get the underlying Application Application& getApp() override diff --git a/src/xrpld/app/main/Application.cpp b/src/xrpld/app/main/Application.cpp index d50637d98e..276ebcef96 100644 --- a/src/xrpld/app/main/Application.cpp +++ b/src/xrpld/app/main/Application.cpp @@ -826,6 +826,24 @@ public: return *walletDB_; } + Fees + getFees() const override + { + XRPL_ASSERT(config_, "xrpl::ApplicationImp::getFees : non-null config"); + + auto const& f1(config_->fees); + + Fees f2; + f2.base = f1.referenceFee; + f2.reserve = f1.accountReserve; + f2.increment = f1.ownerReserve; + f2.gasLimit = f1.gasLimit; + f2.bytecodeSizeLimit = f1.bytecodeSizeLimit; + f2.gasPrice = f1.gasPrice; + + return f2; + } + bool serverOkay(std::string& reason) override; diff --git a/src/xrpld/app/misc/FeeVoteImpl.cpp b/src/xrpld/app/misc/FeeVoteImpl.cpp index f1cb944a52..26bf6bd24b 100644 --- a/src/xrpld/app/misc/FeeVoteImpl.cpp +++ b/src/xrpld/app/misc/FeeVoteImpl.cpp @@ -33,23 +33,23 @@ namespace xrpl { namespace detail { +template class VotableValue { private: - using value_type = XRPAmount; - value_type const current_; // The current setting - value_type const target_; // The setting we want - std::map voteMap_; + ValueType const current_; // The current setting + ValueType const target_; // The setting we want + std::map voteMap_; public: - VotableValue(value_type current, value_type target) : current_(current), target_(target) + VotableValue(ValueType current, ValueType target) : current_(current), target_(target) { // Add our vote ++voteMap_[target_]; } void - addVote(value_type vote) + addVote(ValueType vote) { ++voteMap_[vote]; } @@ -60,20 +60,21 @@ public: addVote(current_); } - [[nodiscard]] value_type + [[nodiscard]] ValueType current() const { return current_; } - [[nodiscard]] std::pair + [[nodiscard]] std::pair getVotes() const; }; -auto -VotableValue::getVotes() const -> std::pair +template +std::pair +VotableValue::getVotes() const { - value_type ourVote = current_; + ValueType ourVote = current_; int weight = 0; for (auto const& [key, val] : voteMap_) { @@ -125,17 +126,16 @@ FeeVoteImpl::doValidation(Fees const& lastFees, Rules const& rules, STValidation // Values should always be in a valid range (because the voting process // will ignore out-of-range values) but if we detect such a case, we do // not send a value. + auto vote = [&v, this](auto const current, auto target, char const* name, auto const& sfield) { + if (current != target) + { + JLOG(journal_.info()) << "Voting for " << name << " of " << target; + + v[sfield] = target; + } + }; if (rules.enabled(featureXRPFees)) { - auto vote = - [&v, this](auto const current, XRPAmount target, char const* name, auto const& sfield) { - if (current != target) - { - JLOG(journal_.info()) << "Voting for " << name << " of " << target; - - v[sfield] = target; - } - }; vote(lastFees.base, target_.referenceFee, "base fee", sfBaseFeeDrops); vote(lastFees.reserve, target_.accountReserve, "base reserve", sfReserveBaseDrops); vote( @@ -145,12 +145,12 @@ FeeVoteImpl::doValidation(Fees const& lastFees, Rules const& rules, STValidation { auto to32 = [](XRPAmount target) { return target.dropsAs(); }; auto to64 = [](XRPAmount target) { return target.dropsAs(); }; - auto vote = [&v, this]( - auto const current, - XRPAmount target, - auto const& convertCallback, - char const* name, - auto const& sfield) { + auto voteAndConvert = [&v, this]( + auto const current, + XRPAmount target, + auto const& convertCallback, + char const* name, + auto const& sfield) { if (current != target) { JLOG(journal_.info()) << "Voting for " << name << " of " << target; @@ -160,15 +160,32 @@ FeeVoteImpl::doValidation(Fees const& lastFees, Rules const& rules, STValidation } }; - vote(lastFees.base, target_.referenceFee, to64, "base fee", sfBaseFee); - vote(lastFees.reserve, target_.accountReserve, to32, "base reserve", sfReserveBase); - vote( + voteAndConvert(lastFees.base, target_.referenceFee, to64, "base fee", sfBaseFee); + voteAndConvert( + lastFees.reserve, target_.accountReserve, to32, "base reserve", sfReserveBase); + voteAndConvert( lastFees.increment, target_.ownerReserve, to32, "reserve increment", sfReserveIncrement); } + if (rules.enabled(featureSmartEscrow)) + { + if (target_.gasLimit <= kMaxGasLimit) + { + vote(lastFees.gasLimit, target_.gasLimit, "gas limit", sfGasLimit); + } + if (target_.bytecodeSizeLimit <= kMaxBytecodeSizeLimit) + { + vote( + lastFees.bytecodeSizeLimit, + target_.bytecodeSizeLimit, + "bytecode size limit", + sfBytecodeSizeLimit); + } + vote(lastFees.gasPrice, target_.gasPrice, "gas price", sfGasPrice); + } } void @@ -188,11 +205,28 @@ FeeVoteImpl::doVoting( detail::VotableValue incReserveVote(lastClosedLedger->fees().increment, target_.ownerReserve); + auto validOrCurrent = [](std::uint32_t target, std::uint32_t max, std::uint32_t current) { + return target <= max ? target : current; + }; + + detail::VotableValue gasLimitVote( + lastClosedLedger->fees().gasLimit, + validOrCurrent(target_.gasLimit, kMaxGasLimit, lastClosedLedger->fees().gasLimit)); + + detail::VotableValue bytecodeSizeLimitVote( + lastClosedLedger->fees().bytecodeSizeLimit, + validOrCurrent( + target_.bytecodeSizeLimit, + kMaxBytecodeSizeLimit, + lastClosedLedger->fees().bytecodeSizeLimit)); + + detail::VotableValue gasPriceVote(lastClosedLedger->fees().gasPrice, target_.gasPrice); + auto const& rules = lastClosedLedger->rules(); if (rules.enabled(featureXRPFees)) { auto doVote = [](std::shared_ptr const& val, - detail::VotableValue& value, + detail::VotableValue& value, SF_AMOUNT const& xrpField) { if (auto const field = ~val->at(~xrpField); field && field->native()) { @@ -224,7 +258,7 @@ FeeVoteImpl::doVoting( else { auto doVote = [](std::shared_ptr const& val, - detail::VotableValue& value, + detail::VotableValue& value, auto const& valueField) { if (auto const field = val->at(~valueField)) { @@ -258,16 +292,52 @@ FeeVoteImpl::doVoting( doVote(val, incReserveVote, sfReserveIncrement); } } + if (rules.enabled(featureSmartEscrow)) + { + auto doVote = [](std::shared_ptr const& val, + detail::VotableValue& value, + SF_UINT32 const& sfield, + std::uint32_t maxValue) { + if (auto const field = ~val->at(~sfield); field) + { + if (field.value() <= maxValue) + { + value.addVote(field.value()); + } + else + { + value.noVote(); + } + } + else + { + value.noVote(); + } + }; + + for (auto const& val : set) + { + if (!val->isTrusted()) + continue; + doVote(val, gasLimitVote, sfGasLimit, kMaxGasLimit); + doVote(val, bytecodeSizeLimitVote, sfBytecodeSizeLimit, kMaxBytecodeSizeLimit); + doVote(val, gasPriceVote, sfGasPrice, std::numeric_limits::max()); + } + } // choose our positions auto const [baseFee, baseFeeChanged] = baseFeeVote.getVotes(); auto const [baseReserve, baseReserveChanged] = baseReserveVote.getVotes(); auto const [incReserve, incReserveChanged] = incReserveVote.getVotes(); + auto const [gasLimit, gasLimitChanged] = gasLimitVote.getVotes(); + auto const [bytecodeSizeLimit, bytecodeSizeLimitChanged] = bytecodeSizeLimitVote.getVotes(); + auto const [gasPrice, gasPriceChanged] = gasPriceVote.getVotes(); auto const seq = lastClosedLedger->header().seq + 1; // add transactions to our position - if (baseFeeChanged || baseReserveChanged || incReserveChanged) + if (baseFeeChanged || baseReserveChanged || incReserveChanged || gasLimitChanged || + bytecodeSizeLimitChanged || gasPriceChanged) { JLOG(journal_.warn()) << "We are voting for a fee change: " << baseFee << "/" << baseReserve << "/" << incReserve; @@ -291,6 +361,12 @@ FeeVoteImpl::doVoting( incReserve.dropsAs(incReserveVote.current()); obj[sfReferenceFeeUnits] = kFeeUnitsDeprecated; } + if (rules.enabled(featureSmartEscrow)) + { + obj[sfGasLimit] = gasLimit; + obj[sfBytecodeSizeLimit] = bytecodeSizeLimit; + obj[sfGasPrice] = gasPrice; + } }); uint256 const txID = feeTx.getTransactionID(); diff --git a/src/xrpld/app/misc/NetworkOPs.cpp b/src/xrpld/app/misc/NetworkOPs.cpp index a440c9ad1c..da900cbb06 100644 --- a/src/xrpld/app/misc/NetworkOPs.cpp +++ b/src/xrpld/app/misc/NetworkOPs.cpp @@ -2646,6 +2646,15 @@ NetworkOPsImp::pubValidation(std::shared_ptr const& val) reserveIncXRP && reserveIncXRP->native()) jvObj[jss::reserve_inc] = reserveIncXRP->xrp().jsonClipped(); + if (auto const gasLimit = ~val->at(~sfGasLimit); gasLimit) + jvObj[jss::gas_limit] = *gasLimit; + + if (auto const bytecodeSizeLimit = ~val->at(~sfBytecodeSizeLimit); bytecodeSizeLimit) + jvObj[jss::bytecode_size_limit] = *bytecodeSizeLimit; + + 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}; @@ -3101,11 +3110,18 @@ NetworkOPsImp::getServerInfo(bool human, bool admin, bool counters) l[jss::seq] = json::UInt(lpClosed->header().seq); l[jss::hash] = to_string(lpClosed->header().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::gas_limit] = lpClosed->fees().gasLimit; + l[jss::bytecode_size_limit] = lpClosed->fees().bytecodeSizeLimit; + l[jss::gas_price] = lpClosed->fees().gasPrice; + } l[jss::close_time] = json::Value::UInt(lpClosed->header().closeTime.time_since_epoch().count()); } @@ -3114,6 +3130,12 @@ 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::gas_limit] = lpClosed->fees().gasLimit; + l[jss::bytecode_size_limit] = lpClosed->fees().bytecodeSizeLimit; + l[jss::gas_price] = lpClosed->fees().gasPrice; + } if (auto const closeOffset = registry_.get().getTimeKeeper().closeOffset(); std::abs(closeOffset.count()) >= 60) @@ -3342,6 +3364,12 @@ NetworkOPsImp::publishLedgerStreams( jvObj[jss::fee_base] = lpAccepted->fees().base.jsonClipped(); jvObj[jss::reserve_base] = lpAccepted->fees().reserve.jsonClipped(); jvObj[jss::reserve_inc] = lpAccepted->fees().increment.jsonClipped(); + if (lpAccepted->rules().enabled(featureSmartEscrow)) + { + jvObj[jss::gas_limit] = lpAccepted->fees().gasLimit; + jvObj[jss::bytecode_size_limit] = lpAccepted->fees().bytecodeSizeLimit; + jvObj[jss::gas_price] = lpAccepted->fees().gasPrice; + } jvObj[jss::txn_count] = json::UInt(alpAccepted->size()); @@ -4540,6 +4568,12 @@ NetworkOPsImp::subLedger(InfoSub::ref isrListener, json::Value& jvResult) jvResult[jss::reserve_base] = lpClosed->fees().reserve.jsonClipped(); jvResult[jss::reserve_inc] = lpClosed->fees().increment.jsonClipped(); jvResult[jss::network_id] = registry_.get().getNetworkIDService().getNetworkID(); + if (lpClosed->rules().enabled(featureSmartEscrow)) + { + jvResult[jss::gas_limit] = lpClosed->fees().gasLimit; + jvResult[jss::bytecode_size_limit] = lpClosed->fees().bytecodeSizeLimit; + jvResult[jss::gas_price] = lpClosed->fees().gasPrice; + } } if ((mode_ >= OperatingMode::SYNCING) && !isNeedNetworkLedger()) diff --git a/src/xrpld/core/Config.h b/src/xrpld/core/Config.h index 2dea8f3597..852a9d9f4b 100644 --- a/src/xrpld/core/Config.h +++ b/src/xrpld/core/Config.h @@ -66,6 +66,21 @@ struct FeeSetup */ XRPAmount ownerReserve{2 * kDropsPerXrp}; + /** + * The gas limit for Feature Extensions. + */ + std::uint32_t gasLimit{1'000'000}; + + /** + * The bytecode size limit for Feature Extensions. + */ + std::uint32_t bytecodeSizeLimit{100'000}; + + /** + * The price of 1 WASM gas, in micro-drops. + */ + std::uint32_t gasPrice{1'000'000}; + /* (Remember to update the example cfg files when changing any of these * values.) */ diff --git a/src/xrpld/core/detail/Config.cpp b/src/xrpld/core/detail/Config.cpp index 3ff62c9b64..d8002f483c 100644 --- a/src/xrpld/core/detail/Config.cpp +++ b/src/xrpld/core/detail/Config.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -1253,6 +1254,12 @@ setupFeeVote(Section const& section) setup.accountReserve = temp; if (set(temp, Keys::kOwnerReserve, section)) setup.ownerReserve = temp; + if (set(temp, Keys::kGasLimit, section) && temp <= kMaxGasLimit) + setup.gasLimit = temp; + if (set(temp, Keys::kBytecodeSizeLimit, section) && temp <= kMaxBytecodeSizeLimit) + setup.bytecodeSizeLimit = temp; + if (set(temp, Keys::kGasPrice, section)) + setup.gasPrice = temp; } return setup; }