Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: compute base-fee on l1 #9986

Merged
merged 2 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
Maddiaa0 marked this conversation as resolved.
Show resolved Hide resolved
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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious, why don't we need muldiv here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a muldiv just not rounding up. We are rounding up on the others as they cover the "real" cost components and we want to make sure that they are covered. Should probably do it using the same muldiv function though, just for clarity


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
Loading