diff --git a/l1-contracts/src/core/libraries/FeeMath.sol b/l1-contracts/src/core/libraries/FeeMath.sol index 6b3b08661b8..6c63cc0e08f 100644 --- a/l1-contracts/src/core/libraries/FeeMath.sol +++ b/l1-contracts/src/core/libraries/FeeMath.sol @@ -28,6 +28,13 @@ library FeeMath { uint256 internal constant MAX_FEE_ASSET_PRICE_MODIFIER = 1000000000; uint256 internal constant FEE_ASSET_PRICE_UPDATE_FRACTION = 100000000000; + uint256 internal constant L1_GAS_PER_BLOCK_PROPOSED = 150000; + uint256 internal constant L1_GAS_PER_EPOCH_VERIFIED = 1000000; + + uint256 internal constant MINIMUM_CONGESTION_MULTIPLIER = 1000000000; + uint256 internal constant MANA_TARGET = 100000000; + uint256 internal constant CONGESTION_UPDATE_FRACTION = 854700854; + function assertValid(OracleInput memory _self) internal pure returns (bool) { require( SignedMath.abs(_self.provingCostModifier) <= MAX_PROVING_COST_MODIFIER, @@ -70,6 +77,10 @@ library FeeMath { return fakeExponential(MINIMUM_FEE_ASSET_PRICE, _numerator, FEE_ASSET_PRICE_UPDATE_FRACTION); } + function congestionMultiplier(uint256 _numerator) internal pure returns (uint256) { + return fakeExponential(MINIMUM_CONGESTION_MULTIPLIER, _numerator, CONGESTION_UPDATE_FRACTION); + } + /** * @notice An approximation of the exponential function: factor * e ** (numerator / denominator) * diff --git a/l1-contracts/test/base/Base.sol b/l1-contracts/test/base/Base.sol index f05cef62d5e..b23e785cd90 100644 --- a/l1-contracts/test/base/Base.sol +++ b/l1-contracts/test/base/Base.sol @@ -147,6 +147,15 @@ contract TestBase is Test { } } + function assertEq(uint256 a, Slot b) internal { + if (Slot.wrap(a) != b) { + emit log("Error: a == b not satisfied [Slot]"); + emit log_named_uint(" Left", a); + emit log_named_uint(" Right", b.unwrap()); + fail(); + } + } + function assertEq(Slot a, uint256 b) internal { if (a != Slot.wrap(b)) { emit log("Error: a == b not satisfied [Slot]"); @@ -163,6 +172,13 @@ contract TestBase is Test { } } + function assertEq(uint256 a, Slot b, string memory err) internal { + if (Slot.wrap(a) != b) { + emit log_named_string("Error", err); + assertEq(a, b); + } + } + function assertEq(Slot a, uint256 b, string memory err) internal { if (a != Slot.wrap(b)) { emit log_named_string("Error", err); diff --git a/l1-contracts/test/fees/FeeModelTestPoints.t.sol b/l1-contracts/test/fees/FeeModelTestPoints.t.sol index a27faa71428..3dd5b0de248 100644 --- a/l1-contracts/test/fees/FeeModelTestPoints.t.sol +++ b/l1-contracts/test/fees/FeeModelTestPoints.t.sol @@ -4,6 +4,7 @@ pragma solidity >=0.8.27; import {TestBase} from "../base/Base.sol"; +import {OracleInput as FeeMathOracleInput} from "@aztec/core/libraries/FeeMath.sol"; // Remember that foundry json parsing is alphabetically done, so you MUST // sort the struct fields alphabetically or prepare for a headache. @@ -96,4 +97,48 @@ contract FeeModelTestPoints is TestBase { points.push(data.points[i]); } } + + function assertEq(L1Fees memory a, L1Fees memory b) internal pure { + assertEq(a.base_fee, b.base_fee, "base_fee mismatch"); + assertEq(a.blob_fee, b.blob_fee, "blob_fee mismatch"); + } + + function assertEq(L1Fees memory a, L1Fees memory b, string memory _message) internal pure { + assertEq(a.base_fee, b.base_fee, string.concat(_message, "base_fee mismatch")); + assertEq(a.blob_fee, b.blob_fee, string.concat(_message, "blob_fee mismatch")); + } + + function assertEq(L1GasOracleValues memory a, L1GasOracleValues memory b) internal pure { + assertEq(a.post, b.post, "post "); + assertEq(a.pre, b.pre, "pre "); + assertEq(a.slot_of_change, b.slot_of_change, "slot_of_change mismatch"); + } + + function assertEq(OracleInput memory a, FeeMathOracleInput memory b) internal pure { + assertEq( + a.fee_asset_price_modifier, b.feeAssetPriceModifier, "fee_asset_price_modifier mismatch" + ); + assertEq(a.proving_cost_modifier, b.provingCostModifier, "proving_cost_modifier mismatch"); + } + + function assertEq(FeeHeader memory a, FeeHeader memory b) internal pure { + assertEq(a.excess_mana, b.excess_mana, "excess_mana mismatch"); + assertEq( + a.fee_asset_price_numerator, b.fee_asset_price_numerator, "fee_asset_price_numerator mismatch" + ); + assertEq(a.mana_used, b.mana_used, "mana_used mismatch"); + assertEq( + a.proving_cost_per_mana_numerator, + b.proving_cost_per_mana_numerator, + "proving_cost_per_mana_numerator mismatch" + ); + } + + function assertEq(ManaBaseFeeComponents memory a, ManaBaseFeeComponents memory b) internal pure { + assertEq(a.congestion_cost, b.congestion_cost, "congestion_cost mismatch"); + assertEq(a.congestion_multiplier, b.congestion_multiplier, "congestion_multiplier mismatch"); + assertEq(a.data_cost, b.data_cost, "data_cost mismatch"); + assertEq(a.gas_cost, b.gas_cost, "gas_cost mismatch"); + assertEq(a.proving_cost, b.proving_cost, "proving_cost mismatch"); + } } diff --git a/l1-contracts/test/fees/MinimalFeeModel.sol b/l1-contracts/test/fees/MinimalFeeModel.sol index 616fa3b4311..0e5284d0b43 100644 --- a/l1-contracts/test/fees/MinimalFeeModel.sol +++ b/l1-contracts/test/fees/MinimalFeeModel.sol @@ -3,63 +3,118 @@ pragma solidity >=0.8.27; import {FeeMath, OracleInput} from "@aztec/core/libraries/FeeMath.sol"; -import {Timestamp, TimeFns, Slot} from "@aztec/core/libraries/TimeMath.sol"; +import {Timestamp, TimeFns, Slot, SlotLib} from "@aztec/core/libraries/TimeMath.sol"; import {Vm} from "forge-std/Vm.sol"; +import { + ManaBaseFeeComponents, L1Fees, L1GasOracleValues, FeeHeader +} from "./FeeModelTestPoints.t.sol"; +import {Math} from "@oz/utils/math/Math.sol"; -struct BaseFees { - uint256 baseFee; - uint256 blobFee; -} - -// This actually behaves pretty close to the slow updates. -struct L1BaseFees { - BaseFees pre; - BaseFees post; - Slot slotOfChange; -} - -struct DataPoint { - uint256 provingCostNumerator; - uint256 feeAssetPriceNumerator; -} +// The data types are slightly messed up here, the reason is that +// we just want to use the same structs from the test points making +// is simpler to compare etc. contract MinimalFeeModel is TimeFns { using FeeMath for OracleInput; using FeeMath for uint256; + using SlotLib for Slot; // This is to allow us to use the cheatcodes for blobbasefee as foundry does not play nice // with the block.blobbasefee value if using cheatcodes to alter it. Vm internal constant VM = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); + uint256 internal constant BLOB_GAS_PER_BLOB = 2 ** 17; + uint256 internal constant GAS_PER_BLOB_POINT_EVALUATION = 50_000; + Slot public constant LIFETIME = Slot.wrap(5); Slot public constant LAG = Slot.wrap(2); Timestamp public immutable GENESIS_TIMESTAMP; uint256 public populatedThrough = 0; - mapping(uint256 _slotNumber => DataPoint _dataPoint) public dataPoints; + mapping(uint256 slotNumber => FeeHeader feeHeader) public feeHeaders; - L1BaseFees public l1BaseFees; + L1GasOracleValues public l1BaseFees; constructor(uint256 _slotDuration, uint256 _epochDuration) TimeFns(_slotDuration, _epochDuration) { GENESIS_TIMESTAMP = Timestamp.wrap(block.timestamp); - dataPoints[0] = DataPoint({provingCostNumerator: 0, feeAssetPriceNumerator: 0}); + feeHeaders[0] = FeeHeader({ + excess_mana: 0, + fee_asset_price_numerator: 0, + mana_used: 0, + proving_cost_per_mana_numerator: 0 + }); - l1BaseFees.pre = BaseFees({baseFee: 1 gwei, blobFee: 1}); - l1BaseFees.post = BaseFees({baseFee: block.basefee, blobFee: _getBlobBaseFee()}); - l1BaseFees.slotOfChange = LIFETIME; + l1BaseFees.pre = L1Fees({base_fee: 1 gwei, blob_fee: 1}); + l1BaseFees.post = L1Fees({base_fee: block.basefee, blob_fee: _getBlobBaseFee()}); + l1BaseFees.slot_of_change = LIFETIME.unwrap(); + } + + function getL1GasOracleValues() public view returns (L1GasOracleValues memory) { + return l1BaseFees; + } + + // For all of the estimations we have been using `3` blobs. + function manaBaseFeeComponents(uint256 _blobsUsed, bool _inFeeAsset) + public + view + returns (ManaBaseFeeComponents memory) + { + L1Fees memory fees = getCurrentL1Fees(); + uint256 dataCost = Math.mulDiv( + _blobsUsed * BLOB_GAS_PER_BLOB, fees.blob_fee, FeeMath.MANA_TARGET, Math.Rounding.Ceil + ); + uint256 gasUsed = FeeMath.L1_GAS_PER_BLOCK_PROPOSED + _blobsUsed * GAS_PER_BLOB_POINT_EVALUATION + + FeeMath.L1_GAS_PER_EPOCH_VERIFIED / EPOCH_DURATION; + uint256 gasCost = Math.mulDiv(gasUsed, fees.base_fee, FeeMath.MANA_TARGET, Math.Rounding.Ceil); + uint256 provingCost = getProvingCost(); + + uint256 congestionMultiplier = FeeMath.congestionMultiplier(calcExcessMana()); + + uint256 total = dataCost + gasCost + provingCost; + uint256 congestionCost = + (total * congestionMultiplier / FeeMath.MINIMUM_CONGESTION_MULTIPLIER) - total; + + uint256 feeAssetPrice = _inFeeAsset ? getFeeAssetPrice() : 1e9; + + return ManaBaseFeeComponents({ + data_cost: Math.mulDiv(dataCost, feeAssetPrice, 1e9, Math.Rounding.Ceil), + gas_cost: Math.mulDiv(gasCost, feeAssetPrice, 1e9, Math.Rounding.Ceil), + proving_cost: Math.mulDiv(provingCost, feeAssetPrice, 1e9, Math.Rounding.Ceil), + congestion_cost: Math.mulDiv(congestionCost, feeAssetPrice, 1e9, Math.Rounding.Ceil), + congestion_multiplier: congestionMultiplier + }); + } + + function getFeeHeader(uint256 _slotNumber) public view returns (FeeHeader memory) { + return feeHeaders[_slotNumber]; + } + + function calcExcessMana() internal view returns (uint256) { + FeeHeader storage parent = feeHeaders[populatedThrough]; + return (parent.excess_mana + parent.mana_used).clampedAdd(-int256(FeeMath.MANA_TARGET)); } - // See the `add_slot` function in the `fee-model.ipynb` notebook for more context. function addSlot(OracleInput memory _oracleInput) public { + addSlot(_oracleInput, 0); + } + + // The `_manaUsed` is all the data we needed to know to calculate the excess mana. + function addSlot(OracleInput memory _oracleInput, uint256 _manaUsed) public { _oracleInput.assertValid(); - DataPoint memory parent = dataPoints[populatedThrough]; + FeeHeader memory parent = feeHeaders[populatedThrough]; + + uint256 excessMana = calcExcessMana(); - dataPoints[++populatedThrough] = DataPoint({ - provingCostNumerator: parent.provingCostNumerator.clampedAdd(_oracleInput.provingCostModifier), - feeAssetPriceNumerator: parent.feeAssetPriceNumerator.clampedAdd( + feeHeaders[++populatedThrough] = FeeHeader({ + proving_cost_per_mana_numerator: parent.proving_cost_per_mana_numerator.clampedAdd( + _oracleInput.provingCostModifier + ), + fee_asset_price_numerator: parent.fee_asset_price_numerator.clampedAdd( _oracleInput.feeAssetPriceModifier - ) + ), + mana_used: _manaUsed, + excess_mana: excessMana }); } @@ -72,7 +127,7 @@ contract MinimalFeeModel is TimeFns { function photograph() public { Slot slot = getCurrentSlot(); // The slot where we find a new queued value acceptable - Slot acceptableSlot = l1BaseFees.slotOfChange + (LIFETIME - LAG); + Slot acceptableSlot = Slot.wrap(l1BaseFees.slot_of_change) + (LIFETIME - LAG); if (slot < acceptableSlot) { return; @@ -80,21 +135,21 @@ contract MinimalFeeModel is TimeFns { // If we are at or beyond the scheduled change, we need to update the "current" value l1BaseFees.pre = l1BaseFees.post; - l1BaseFees.post = BaseFees({baseFee: block.basefee, blobFee: _getBlobBaseFee()}); - l1BaseFees.slotOfChange = slot + LAG; + l1BaseFees.post = L1Fees({base_fee: block.basefee, blob_fee: _getBlobBaseFee()}); + l1BaseFees.slot_of_change = (slot + LAG).unwrap(); } - function getFeeAssetPrice(uint256 _slotNumber) public view returns (uint256) { - return FeeMath.feeAssetPriceModifier(dataPoints[_slotNumber].feeAssetPriceNumerator); + function getFeeAssetPrice() public view returns (uint256) { + return FeeMath.feeAssetPriceModifier(feeHeaders[populatedThrough].fee_asset_price_numerator); } - function getProvingCost(uint256 _slotNumber) public view returns (uint256) { - return FeeMath.provingCostPerMana(dataPoints[_slotNumber].provingCostNumerator); + function getProvingCost() public view returns (uint256) { + return FeeMath.provingCostPerMana(feeHeaders[populatedThrough].proving_cost_per_mana_numerator); } - function getCurrentL1Fees() public view returns (BaseFees memory) { + function getCurrentL1Fees() public view returns (L1Fees memory) { Slot slot = getCurrentSlot(); - if (slot < l1BaseFees.slotOfChange) { + if (slot < Slot.wrap(l1BaseFees.slot_of_change)) { return l1BaseFees.pre; } return l1BaseFees.post; diff --git a/l1-contracts/test/fees/MinimalFeeModel.t.sol b/l1-contracts/test/fees/MinimalFeeModel.t.sol index ad5cbe1bfac..cbc0149deff 100644 --- a/l1-contracts/test/fees/MinimalFeeModel.t.sol +++ b/l1-contracts/test/fees/MinimalFeeModel.t.sol @@ -2,8 +2,14 @@ pragma solidity >=0.8.27; import {OracleInput, FeeMath} from "@aztec/core/libraries/FeeMath.sol"; -import {FeeModelTestPoints, TestPoint} from "./FeeModelTestPoints.t.sol"; -import {MinimalFeeModel, BaseFees} from "./MinimalFeeModel.sol"; +import { + FeeModelTestPoints, + TestPoint, + ManaBaseFeeComponents, + L1Fees, + FeeHeader +} from "./FeeModelTestPoints.t.sol"; +import {MinimalFeeModel} from "./MinimalFeeModel.sol"; import {Errors} from "@aztec/core/libraries/Errors.sol"; import {SlotLib, Slot} from "@aztec/core/libraries/TimeMath.sol"; @@ -38,17 +44,17 @@ contract MinimalFeeModelTest is FeeModelTestPoints { // Then check that we get the same proving costs as the python model for (uint256 i = 0; i < points.length; i++) { + assertEq( + model.getProvingCost(), + points[i].outputs.mana_base_fee_components_in_wei.proving_cost, + "Computed proving cost does not match expected value" + ); model.addSlot( OracleInput({ provingCostModifier: points[i].oracle_input.proving_cost_modifier, feeAssetPriceModifier: points[i].oracle_input.fee_asset_price_modifier }) ); - assertEq( - model.getProvingCost(i), - points[i].outputs.mana_base_fee_components_in_wei.proving_cost, - "Computed proving cost does not match expected value" - ); } } @@ -57,17 +63,17 @@ contract MinimalFeeModelTest is FeeModelTestPoints { // Then check that we get the same fee asset price as the python model for (uint256 i = 0; i < points.length; i++) { + assertEq( + model.getFeeAssetPrice(), + points[i].outputs.fee_asset_price_at_execution, + "Computed fee asset price does not match expected value" + ); model.addSlot( OracleInput({ provingCostModifier: points[i].oracle_input.proving_cost_modifier, feeAssetPriceModifier: points[i].oracle_input.fee_asset_price_modifier }) ); - assertEq( - model.getFeeAssetPrice(i), - points[i].outputs.fee_asset_price_at_execution, - "Computed fee asset price does not match expected value" - ); } } @@ -111,13 +117,71 @@ contract MinimalFeeModelTest is FeeModelTestPoints { if (model.getCurrentSlot() == nextSlot) { TestPoint memory expected = points[nextSlot.unwrap() - 1]; - BaseFees memory fees = model.getCurrentL1Fees(); + L1Fees memory fees = model.getCurrentL1Fees(); assertEq(expected.block_header.l1_block_number, block.number, "invalid l1 block number"); assertEq(expected.block_header.block_number, nextSlot.unwrap(), "invalid l2 block number"); assertEq(expected.block_header.slot_number, nextSlot.unwrap(), "invalid l2 slot number"); - assertEq(expected.outputs.l1_fee_oracle_output.base_fee, fees.baseFee, "baseFee mismatch"); - assertEq(expected.outputs.l1_fee_oracle_output.blob_fee, fees.blobFee, "blobFee mismatch"); + assertEq(expected.outputs.l1_fee_oracle_output.base_fee, fees.base_fee, "baseFee mismatch"); + assertEq(expected.outputs.l1_fee_oracle_output.blob_fee, fees.blob_fee, "blobFee mismatch"); + nextSlot = nextSlot + Slot.wrap(1); + } + } + } + + function test_full() public { + // Because of the time jump at the deployment, the first l1 block metadata + // should be at slot 1. + Slot nextSlot = Slot.wrap(1); + + for (uint256 i = 0; i < l1Metadata.length; i++) { + _loadL1Metadata(i); + model.photograph(); + + if (model.getCurrentSlot() == nextSlot) { + TestPoint memory point = points[nextSlot.unwrap() - 1]; + + // Get a hold of the values that is used for the next block + L1Fees memory fees = model.getCurrentL1Fees(); + uint256 feeAssetPrice = model.getFeeAssetPrice(); + // We are assuming 3 blobs for all of these computations, as per the model. + // 3 blobs because that can fit ~360 txs, or 10 tps. + ManaBaseFeeComponents memory components = model.manaBaseFeeComponents(3, false); + ManaBaseFeeComponents memory componentsFeeAsset = model.manaBaseFeeComponents(3, true); + FeeHeader memory parentFeeHeader = model.getFeeHeader(point.block_header.slot_number - 1); + + model.addSlot( + OracleInput({ + provingCostModifier: point.oracle_input.proving_cost_modifier, + feeAssetPriceModifier: point.oracle_input.fee_asset_price_modifier + }), + point.block_header.mana_spent + ); + + // The fee header is the state that we are storing, so it is the value written at the block submission. + FeeHeader memory feeHeader = model.getFeeHeader(point.block_header.slot_number); + + // Ensure that we can reproduce the main parts of our test points. + // For now, most of the block header is not actually stored in the fee model + // but just needed to influence the other values and used for L1 state. + + assertEq(point.block_header.block_number, nextSlot, "invalid l2 block number"); + assertEq(point.block_header.l1_block_number, block.number, "invalid l1 block number"); + assertEq(point.block_header.slot_number, nextSlot, "invalid l2 slot number"); + assertEq(point.block_header.timestamp, block.timestamp, "invalid timestamp"); + + assertEq(point.fee_header, feeHeader); + + assertEq( + point.outputs.fee_asset_price_at_execution, feeAssetPrice, "feeAssetPrice mismatch" + ); + assertEq(point.outputs.l1_fee_oracle_output, fees, "l1 fee oracle output"); + assertEq(point.outputs.l1_gas_oracle_values, model.getL1GasOracleValues()); + assertEq(point.outputs.mana_base_fee_components_in_wei, components); + assertEq(point.outputs.mana_base_fee_components_in_fee_asset, componentsFeeAsset); + + assertEq(point.parent_fee_header, parentFeeHeader); + nextSlot = nextSlot + Slot.wrap(1); } }