Skip to content

Commit

Permalink
feat: compute base-fee on l1 (#9986)
Browse files Browse the repository at this point in the history
  • Loading branch information
LHerskind authored Nov 20, 2024
1 parent 368ac8b commit 4ab46fe
Show file tree
Hide file tree
Showing 5 changed files with 244 additions and 53 deletions.
11 changes: 11 additions & 0 deletions l1-contracts/src/core/libraries/FeeMath.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
*
Expand Down
16 changes: 16 additions & 0 deletions l1-contracts/test/base/Base.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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]");
Expand All @@ -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);
Expand Down
45 changes: 45 additions & 0 deletions l1-contracts/test/fees/FeeModelTestPoints.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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");
}
}
131 changes: 93 additions & 38 deletions l1-contracts/test/fees/MinimalFeeModel.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
}

Expand All @@ -72,29 +127,29 @@ 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;
}

// 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;
Expand Down
Loading

0 comments on commit 4ab46fe

Please sign in to comment.