Compare commits

...

5 Commits

Author SHA1 Message Date
Mayukha Vadari
4cca58490e test: Assert gas_price on the validations stream
The stream assertions covered `gas_limit` and `bytecode_size_limit` but not
`gas_price`, which is published from the same place and voted by the same
helper. Assert it too, on both the enabled and the disabled path.

Reported by Copilot on #8214.
2026-09-10 18:03:55 -04:00
Mayukha Vadari
a6cc20aa04 fix: Drop an unused include from TestServiceRegistry
`kDropsPerXrp` comes from `XRPAmount.h`, which is already included, so
`Protocol.h` was never used. clang-tidy's misc-include-cleaner failed CI on
it.
2026-09-10 18:03:52 -04:00
Mayukha Vadari
052782a3d4 fix: Don't vote on gas settings before Smart Escrow is enabled
`doVoting` built the three gas votes unconditionally. Before the amendment,
the ledger reports zero for all three while the config targets are non-zero by
default, and the loop that collects validator votes is itself gated on the
amendment — so the vote map held only our own target and every one of the
three reported a change.

The result was a `SetFee` pseudo-transaction on every flag ledger, from every
node running this build, for as long as the amendment stayed disabled. The
transaction carried no gas fields, since that part was gated correctly, so it
proposed no change at all.

Gate the three flags on the amendment, matching `doValidation`, which already
had its gas votes inside the same check.

Reported by xrplf-ai-reviewer on #8214.
2026-09-10 18:03:41 -04:00
Mayukha Vadari
e4476e5821 fix: Carry the configured gas settings into the genesis ledger
`FeeSetup::toFees()` returned the three-argument `Fees`, leaving `gasLimit`,
`bytecodeSizeLimit` and `gasPrice` at zero. `startGenesisLedger` passes that
straight into the genesis `Ledger`, which is where those three are written to
the `FeeSettings` entry — so a network started fresh with `featureSmartEscrow`
in its initial amendment set got a gas limit of zero, i.e. Smart Escrow
switched off no matter what the operator configured.

Carrying them through `toFees()` alone is not enough. Every `Ledger` built
from stored data is seeded with the same `Fees`, and `Ledger::setup()` only
overwrites the fields the `FeeSettings` entry actually carries. A ledger from
before the amendment carries none, so the seed would survive and the node
would read its own configuration back as though the network had agreed to it —
and then never vote for the values it wants, because `doValidation` only votes
when the current setting differs from the target.

So the genesis constructor now clears the three whenever it does not write
them. A ledger reports what its `FeeSettings` entry holds, and nothing more.

Reported by depthfirst-app and Copilot on #8214.
2026-09-10 18:03:20 -04:00
Mayukha Vadari
62d8d84a49 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`.
2026-09-10 14:55:08 -04:00
17 changed files with 888 additions and 73 deletions

View File

@@ -1360,6 +1360,39 @@
# Example:
# owner_reserve = 200000 # 0.2 XRP
#
# gas_limit = <gas>
#
# 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 = <bytes>
#
# 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 = <micro-drops>
#
# 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

View File

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

View File

@@ -6,6 +6,7 @@
#include <xrpl/basics/TaggedCache.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/protocol/Fees.h>
#include <boost/asio.hpp>
@@ -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&

View File

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

View File

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

View File

@@ -198,6 +198,22 @@ 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;
}
else
{
// This ledger does not carry the gas settings, so it must not
// report them either. Otherwise a node reads its own config back
// as though the network had agreed to it, and never votes for the
// values it wants.
fees_.gasLimit = 0;
fees_.bytecodeSizeLimit = 0;
fees_.gasPrice = 0;
}
rawInsert(sle);
}
@@ -564,6 +580,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 +597,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<STAmount> const& src) {
if (src)
{
@@ -598,6 +616,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<std::uint32_t> 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 +642,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&)

View File

@@ -9,6 +9,7 @@
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/AmendmentTable.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Fees.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/PublicKey.h>
@@ -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);

View File

@@ -11,6 +11,7 @@
#include <xrpl/ledger/Ledger.h>
#include <xrpl/ledger/OpenView.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Fees.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/KeyType.h>
#include <xrpl/protocol/PublicKey.h>
@@ -31,7 +32,9 @@
#include <limits>
#include <memory>
#include <optional>
#include <source_location>
#include <string>
#include <utility>
#include <vector>
namespace xrpl::test {
@@ -45,10 +48,17 @@ struct FeeSettingsFields
std::optional<XRPAmount> baseFeeDrops = std::nullopt;
std::optional<XRPAmount> reserveBaseDrops = std::nullopt;
std::optional<XRPAmount> reserveIncrementDrops = std::nullopt;
std::optional<std::uint32_t> gasLimit = std::nullopt;
std::optional<std::uint32_t> bytecodeSizeLimit = std::nullopt;
std::optional<std::uint32_t> 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<std::uint32_t>(-1234567));
BEAST_EXPECT(setup.ownerReserve == static_cast<std::uint32_t>(-1234));
BEAST_EXPECT(setup.gasLimit == defaultSetup.gasLimit);
BEAST_EXPECT(setup.bytecodeSizeLimit == defaultSetup.bytecodeSizeLimit);
BEAST_EXPECT(setup.gasPrice == static_cast<std::uint32_t>(-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<Ledger>(
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<Ledger>(
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<Ledger>(
kCreateGenesis,
Rules{env.app().config().features},
env.app().config().fees.toFees(),
std::vector<uint256>{},
env.app().getNodeFamily());
// Create the next ledger to apply transaction to
ledger = std::make_shared<Ledger>(*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<Ledger>(
kCreateGenesis,
Rules{env.app().config().features},
env.app().config().fees.toFees(),
std::vector<uint256>{},
env.app().getNodeFamily());
ledger = std::make_shared<Ledger>(*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<Ledger>(
kCreateGenesis,
Rules{env.app().config().features},
env.app().config().fees.toFees(),
std::vector<uint256>{},
env.app().getNodeFamily());
// Create the next ledger to apply transaction to
ledger = std::make_shared<Ledger>(*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<Ledger>(
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<Ledger>(
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<Ledger>(
kCreateGenesis,
Rules{env.app().config().features},
env.app().config().fees.toFees(),
std::vector<uint256>{},
env.app().getNodeFamily());
// Create the next ledger to apply transaction to
ledger = std::make_shared<Ledger>(*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<Ledger>(
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<Ledger>(
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<Ledger>(
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<Ledger>(
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<Ledger>(
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<Ledger>(
@@ -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<Ledger>(
@@ -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,274 @@ class FeeVote_test : public beast::unit_test::Suite
feeTx.getFieldAmount(sfReserveIncrementDrops) == XRPAmount{setup.ownerReserve});
}
void
testGenesisFeeSettings()
{
testcase("genesis FeeSettings carries the configured gas settings");
using namespace jtx;
Env env(*this, testableAmendments());
auto const& cfg = env.app().config().fees;
BEAST_EXPECT(cfg.gasLimit != 0 && cfg.bytecodeSizeLimit != 0 && cfg.gasPrice != 0);
auto const genesisWith = [&](std::vector<uint256> const& amendments) {
return std::make_shared<Ledger>(
kCreateGenesis,
Rules{env.app().config().features},
cfg.toFees(),
amendments,
env.app().getNodeFamily());
};
{
// With the amendment, the operator's configuration becomes the
// network's starting point.
auto const ledger = genesisWith({featureSmartEscrow});
auto const sle = ledger->read(keylet::feeSettings());
if (BEAST_EXPECT(sle))
{
BEAST_EXPECT(sle->getFieldU32(sfGasLimit) == cfg.gasLimit);
BEAST_EXPECT(sle->getFieldU32(sfBytecodeSizeLimit) == cfg.bytecodeSizeLimit);
BEAST_EXPECT(sle->getFieldU32(sfGasPrice) == cfg.gasPrice);
}
BEAST_EXPECT(ledger->fees().gasLimit == cfg.gasLimit);
BEAST_EXPECT(ledger->fees().bytecodeSizeLimit == cfg.bytecodeSizeLimit);
BEAST_EXPECT(ledger->fees().gasPrice == cfg.gasPrice);
}
{
// Without it, the entry carries nothing and the ledger reports
// nothing, so the node still has something to vote for.
auto const ledger = genesisWith({});
auto const sle = ledger->read(keylet::feeSettings());
if (BEAST_EXPECT(sle))
{
BEAST_EXPECT(!sle->isFieldPresent(sfGasLimit));
BEAST_EXPECT(!sle->isFieldPresent(sfBytecodeSizeLimit));
BEAST_EXPECT(!sle->isFieldPresent(sfGasPrice));
}
BEAST_EXPECT(ledger->fees().gasLimit == 0);
BEAST_EXPECT(ledger->fees().bytecodeSizeLimit == 0);
BEAST_EXPECT(ledger->fees().gasPrice == 0);
}
}
void
testDoVotingNoChangePreSmartEscrow()
{
testcase("doVoting votes for nothing before Smart Escrow");
using namespace jtx;
// A ledger from before the amendment reports zero for all three gas
// settings, while the config targets are non-zero by default. Those
// three must not count as a change, or every node emits a SetFee on
// every flag ledger for as long as the amendment is off.
Env env(*this, testableAmendments() - featureSmartEscrow);
FeeSetup setup;
setup.referenceFee = UNIT_TEST_REFERENCE_FEE;
setup.accountReserve = 200'000'000;
setup.ownerReserve = 50'000'000;
BEAST_EXPECT(setup.gasLimit != 0);
BEAST_EXPECT(setup.bytecodeSizeLimit != 0);
BEAST_EXPECT(setup.gasPrice != 0);
// The three-argument Fees leaves the gas settings at zero, which is
// what Ledger::setup() reads back from a pre-amendment FeeSettings.
Fees const ledgerFees{setup.referenceFee, setup.accountReserve, setup.ownerReserve};
auto feeVote = makeFeeVote(setup, env.app().getJournal("FeeVote"));
auto ledger = std::make_shared<Ledger>(
kCreateGenesis,
Rules{env.app().config().features},
ledgerFees,
std::vector<uint256>{},
env.app().getNodeFamily());
for (int i = 0; i < 256 - 1; ++i)
{
ledger = std::make_shared<Ledger>(*ledger, env.app().getTimeKeeper().closeTime());
}
BEAST_EXPECT(ledger->isFlagLedger());
BEAST_EXPECT(ledger->fees().gasLimit == 0);
std::vector<std::shared_ptr<STValidation>> validations;
for (int i = 0; i < 5; i++)
{
auto sec = randomSecretKey();
auto pub = derivePublicKey(KeyType::Secp256k1, sec);
auto val = std::make_shared<STValidation>(
env.app().getTimeKeeper().now(), pub, sec, calcNodeID(pub), [&](STValidation& v) {
v.setFieldU32(sfLedgerSequence, ledger->seq());
// Everyone is content with the fees as they stand.
v.setFieldAmount(sfBaseFeeDrops, XRPAmount{setup.referenceFee});
v.setFieldAmount(sfReserveBaseDrops, XRPAmount{setup.accountReserve});
v.setFieldAmount(sfReserveIncrementDrops, XRPAmount{setup.ownerReserve});
});
if ((i % 2) != 0)
val->setTrusted();
validations.push_back(val);
}
auto txSet = std::make_shared<SHAMap>(SHAMapType::TRANSACTION, env.app().getNodeFamily());
feeVote->doVoting(ledger, validations, txSet);
BEAST_EXPECT(getTxs(txSet).empty());
}
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<STTx, std::shared_ptr<Ledger>> {
auto feeVote = makeFeeVote(setup, env.app().getJournal("FeeVote"));
auto ledger = std::make_shared<Ledger>(
kCreateGenesis,
Rules{env.app().config().features},
env.app().config().fees.toFees(),
std::vector<uint256>{},
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>(*ledger, env.app().getTimeKeeper().closeTime());
}
BEAST_EXPECT(ledger->isFlagLedger());
// Create some mock validations with fee votes
std::vector<std::shared_ptr<STValidation>> validations;
for (int i = 0; i < 5; i++)
{
auto sec = randomSecretKey();
auto pub = derivePublicKey(KeyType::Secp256k1, sec);
auto val = std::make_shared<STValidation>(
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<SHAMap>(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<Ledger> 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 +1216,9 @@ class FeeVote_test : public beast::unit_test::Suite
testSingleInvalidTransaction();
testDoValidation();
testDoVoting();
testGenesisFeeSettings();
testDoVotingNoChangePreSmartEscrow();
testDoVotingSmartEscrow();
}
};

View File

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

View File

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

View File

@@ -514,6 +514,28 @@ 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;
if (jv.isMember(jss::gas_price) != isFlagLedger)
return false;
}
else
{
if (jv.isMember(jss::gas_limit))
return false;
if (jv.isMember(jss::bytecode_size_limit))
return false;
if (jv.isMember(jss::gas_price))
return false;
}
return true;
};
@@ -1976,7 +1998,8 @@ public:
testTransactionsAPIv1();
testTransactionsAPIv2();
testManifests();
testValidations(all - xrpFees);
testValidations(all - featureXRPFees - featureSmartEscrow);
testValidations(all - featureSmartEscrow);
testValidations(all);
testSubErrors(true);
testSubErrors(false);

View File

@@ -8,6 +8,7 @@
#include <xrpl/core/NetworkIDService.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/PendingSaves.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/server/LoadFeeTrack.h>
#include <boost/asio/io_context.hpp>
@@ -71,11 +72,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 +386,12 @@ public:
throw std::logic_error("TestServiceRegistry::getWalletDB() not implemented");
}
Fees
getFees() const override
{
return fees_;
}
// Temporary: Get the underlying Application
Application&
getApp() override

View File

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

View File

@@ -33,23 +33,23 @@ namespace xrpl {
namespace detail {
template <typename ValueType>
class VotableValue
{
private:
using value_type = XRPAmount;
value_type const current_; // The current setting
value_type const target_; // The setting we want
std::map<value_type, int> voteMap_;
ValueType const current_; // The current setting
ValueType const target_; // The setting we want
std::map<ValueType, int> 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<value_type, bool>
[[nodiscard]] std::pair<ValueType, bool>
getVotes() const;
};
auto
VotableValue::getVotes() const -> std::pair<value_type, bool>
template <typename ValueType>
std::pair<ValueType, bool>
VotableValue<ValueType>::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<std::uint32_t>(); };
auto to64 = [](XRPAmount target) { return target.dropsAs<std::uint64_t>(); };
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<STValidation> const& val,
detail::VotableValue& value,
detail::VotableValue<XRPAmount>& 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<STValidation> const& val,
detail::VotableValue& value,
detail::VotableValue<XRPAmount>& value,
auto const& valueField) {
if (auto const field = val->at(~valueField))
{
@@ -258,16 +292,58 @@ FeeVoteImpl::doVoting(
doVote(val, incReserveVote, sfReserveIncrement);
}
}
if (rules.enabled(featureSmartEscrow))
{
auto doVote = [](std::shared_ptr<STValidation> const& val,
detail::VotableValue<std::uint32_t>& 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<std::uint32_t>::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)
//
// The gas votes only count once the amendment is on. Before it is, the
// ledger reports zero for all three while the config targets are
// non-zero, so they would report a change on every flag ledger and have
// us emit a SetFee that carries no gas fields and changes nothing.
if (baseFeeChanged || baseReserveChanged || incReserveChanged ||
(rules.enabled(featureSmartEscrow) &&
(gasLimitChanged || bytecodeSizeLimitChanged || gasPriceChanged)))
{
JLOG(journal_.warn()) << "We are voting for a fee change: " << baseFee << "/" << baseReserve
<< "/" << incReserve;
@@ -291,6 +367,12 @@ FeeVoteImpl::doVoting(
incReserve.dropsAs<std::uint32_t>(incReserveVote.current());
obj[sfReferenceFeeUnits] = kFeeUnitsDeprecated;
}
if (rules.enabled(featureSmartEscrow))
{
obj[sfGasLimit] = gasLimit;
obj[sfBytecodeSizeLimit] = bytecodeSizeLimit;
obj[sfGasPrice] = gasPrice;
}
});
uint256 const txID = feeTx.getTransactionID();

View File

@@ -2646,6 +2646,15 @@ NetworkOPsImp::pubValidation(std::shared_ptr<STValidation> 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())

View File

@@ -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.) */
@@ -75,7 +90,11 @@ struct FeeSetup
[[nodiscard]] Fees
toFees() const
{
return Fees{referenceFee, accountReserve, ownerReserve};
Fees fees{referenceFee, accountReserve, ownerReserve};
fees.gasLimit = gasLimit;
fees.bytecodeSizeLimit = bytecodeSizeLimit;
fees.gasPrice = gasPrice;
return fees;
}
};

View File

@@ -12,6 +12,7 @@
#include <xrpl/config/Constants.h>
#include <xrpl/net/HTTPClient.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Fees.h>
#include <xrpl/protocol/SystemParameters.h>
#include <xrpl/rdb/DBInit.h>
#include <xrpl/rdb/DatabaseCon.h>
@@ -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;
}