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.
This commit is contained in:
Mayukha Vadari
2026-09-10 18:03:20 -04:00
parent 62d8d84a49
commit e4476e5821
3 changed files with 69 additions and 1 deletions

View File

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

View File

@@ -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<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
testDoVotingSmartEscrow()
{
@@ -1098,6 +1151,7 @@ class FeeVote_test : public beast::unit_test::Suite
testSingleInvalidTransaction();
testDoValidation();
testDoVoting();
testGenesisFeeSettings();
testDoVotingSmartEscrow();
}
};

View File

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