From e4476e58218a2b582676250caa0ce6dc380d2235 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Thu, 10 Sep 2026 18:03:20 -0400 Subject: [PATCH] fix: Carry the configured gas settings into the genesis ledger MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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. --- src/libxrpl/ledger/Ledger.cpp | 10 +++++++ src/test/app/FeeVote_test.cpp | 54 +++++++++++++++++++++++++++++++++++ src/xrpld/core/Config.h | 6 +++- 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/src/libxrpl/ledger/Ledger.cpp b/src/libxrpl/ledger/Ledger.cpp index 86a22eba60..9f70e89f02 100644 --- a/src/libxrpl/ledger/Ledger.cpp +++ b/src/libxrpl/ledger/Ledger.cpp @@ -204,6 +204,16 @@ Ledger::Ledger( 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); } diff --git a/src/test/app/FeeVote_test.cpp b/src/test/app/FeeVote_test.cpp index 5136ef0c3b..8fd91683a1 100644 --- a/src/test/app/FeeVote_test.cpp +++ b/src/test/app/FeeVote_test.cpp @@ -935,6 +935,59 @@ 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 const& amendments) { + return std::make_shared( + 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 testDoVotingSmartEscrow() { @@ -1098,6 +1151,7 @@ class FeeVote_test : public beast::unit_test::Suite testSingleInvalidTransaction(); testDoValidation(); testDoVoting(); + testGenesisFeeSettings(); testDoVotingSmartEscrow(); } }; diff --git a/src/xrpld/core/Config.h b/src/xrpld/core/Config.h index 852a9d9f4b..01d0514b0e 100644 --- a/src/xrpld/core/Config.h +++ b/src/xrpld/core/Config.h @@ -90,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; } };