Skip to content

Commit

Permalink
4844 prep: Add Ecotone fork check & refactor l1 / data availability c…
Browse files Browse the repository at this point in the history
…ost functions (#203)

* add Ecotone upgrade getters & refactor L1CostFunc to make it easier to swap in new ones

* core: nil instead of zero, always check empty rollup data-cost, check op-stack config, fix tx-pool l1-cost-func

---------

Co-authored-by: protolambda <proto@protolambda.com>
  • Loading branch information
roberto-bayardo and protolambda authored Jan 3, 2024
1 parent 8e15470 commit e417703
Show file tree
Hide file tree
Showing 16 changed files with 296 additions and 184 deletions.
7 changes: 6 additions & 1 deletion cmd/utils/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,12 @@ var (
}
OverrideOptimismCanyon = &flags.BigFlag{
Name: "override.canyon",
Usage: "Manually specify the Optimsim Canyon fork timestamp, overriding the bundled setting",
Usage: "Manually specify the Optimism Canyon fork timestamp, overriding the bundled setting",
Category: flags.EthCategory,
}
OverrideOptimismEcotone = &flags.BigFlag{
Name: "override.ecotone",
Usage: "Manually specify the Optimism Ecotone fork timestamp, overriding the bundled setting",
Category: flags.EthCategory,
}
OverrideOptimismInterop = &cli.Uint64Flag{
Expand Down
46 changes: 23 additions & 23 deletions core/state_transition.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,28 +144,28 @@ type Message struct {
// This field will be set to true for operations like RPC eth_call.
SkipAccountChecks bool

IsSystemTx bool // IsSystemTx indicates the message, if also a deposit, does not emit gas usage.
IsDepositTx bool // IsDepositTx indicates the message is force-included and can persist a mint.
Mint *big.Int // Mint is the amount to mint before EVM processing, or nil if there is no minting.
RollupDataGas types.RollupGasData // RollupDataGas indicates the rollup cost of the message, 0 if not a rollup or no cost.
IsSystemTx bool // IsSystemTx indicates the message, if also a deposit, does not emit gas usage.
IsDepositTx bool // IsDepositTx indicates the message is force-included and can persist a mint.
Mint *big.Int // Mint is the amount to mint before EVM processing, or nil if there is no minting.
RollupCostData types.RollupCostData // RollupCostData caches data to compute the fee we charge for data availability
}

// TransactionToMessage converts a transaction into a Message.
func TransactionToMessage(tx *types.Transaction, s types.Signer, baseFee *big.Int) (*Message, error) {
msg := &Message{
Nonce: tx.Nonce(),
GasLimit: tx.Gas(),
GasPrice: new(big.Int).Set(tx.GasPrice()),
GasFeeCap: new(big.Int).Set(tx.GasFeeCap()),
GasTipCap: new(big.Int).Set(tx.GasTipCap()),
To: tx.To(),
Value: tx.Value(),
Data: tx.Data(),
AccessList: tx.AccessList(),
IsSystemTx: tx.IsSystemTx(),
IsDepositTx: tx.IsDepositTx(),
Mint: tx.Mint(),
RollupDataGas: tx.RollupDataGas(),
Nonce: tx.Nonce(),
GasLimit: tx.Gas(),
GasPrice: new(big.Int).Set(tx.GasPrice()),
GasFeeCap: new(big.Int).Set(tx.GasFeeCap()),
GasTipCap: new(big.Int).Set(tx.GasTipCap()),
To: tx.To(),
Value: tx.Value(),
Data: tx.Data(),
AccessList: tx.AccessList(),
IsSystemTx: tx.IsSystemTx(),
IsDepositTx: tx.IsDepositTx(),
Mint: tx.Mint(),
RollupCostData: tx.RollupCostData(),

SkipAccountChecks: false,
BlobHashes: tx.BlobHashes(),
Expand Down Expand Up @@ -245,10 +245,10 @@ func (st *StateTransition) buyGas() error {
mgval = mgval.Mul(mgval, st.msg.GasPrice)
var l1Cost *big.Int
if st.evm.Context.L1CostFunc != nil && !st.msg.SkipAccountChecks {
l1Cost = st.evm.Context.L1CostFunc(st.evm.Context.BlockNumber.Uint64(), st.evm.Context.Time, st.msg.RollupDataGas, st.msg.IsDepositTx)
}
if l1Cost != nil {
mgval = mgval.Add(mgval, l1Cost)
l1Cost = st.evm.Context.L1CostFunc(st.msg.RollupCostData, st.evm.Context.Time)
if l1Cost != nil {
mgval = mgval.Add(mgval, l1Cost)
}
}
balanceCheck := new(big.Int).Set(mgval)
if st.msg.GasFeeCap != nil {
Expand Down Expand Up @@ -535,9 +535,9 @@ func (st *StateTransition) innerTransitionDb() (*ExecutionResult, error) {

// Check that we are post bedrock to enable op-geth to be able to create pseudo pre-bedrock blocks (these are pre-bedrock, but don't follow l2 geth rules)
// Note optimismConfig will not be nil if rules.IsOptimismBedrock is true
if optimismConfig := st.evm.ChainConfig().Optimism; optimismConfig != nil && rules.IsOptimismBedrock {
if optimismConfig := st.evm.ChainConfig().Optimism; optimismConfig != nil && rules.IsOptimismBedrock && !st.msg.IsDepositTx {
st.state.AddBalance(params.OptimismBaseFeeRecipient, new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), st.evm.Context.BaseFee))
if cost := st.evm.Context.L1CostFunc(st.evm.Context.BlockNumber.Uint64(), st.evm.Context.Time, st.msg.RollupDataGas, st.msg.IsDepositTx); cost != nil {
if cost := st.evm.Context.L1CostFunc(st.msg.RollupCostData, st.evm.Context.Time); cost != nil {
st.state.AddBalance(params.OptimismL1FeeRecipient, cost)
}
}
Expand Down
13 changes: 7 additions & 6 deletions core/txpool/legacypool/legacypool.go
Original file line number Diff line number Diff line change
Expand Up @@ -660,7 +660,7 @@ func (pool *LegacyPool) validateTx(tx *types.Transaction, local bool) error {
if tx := list.txs.Get(nonce); tx != nil {
cost := tx.Cost()
if pool.l1CostFn != nil {
if l1Cost := pool.l1CostFn(tx.RollupDataGas()); l1Cost != nil { // add rollup cost
if l1Cost := pool.l1CostFn(tx.RollupCostData()); l1Cost != nil { // add rollup cost
cost = cost.Add(cost, l1Cost)
}
}
Expand Down Expand Up @@ -1445,9 +1445,10 @@ func (pool *LegacyPool) reset(oldHead, newHead *types.Header) {
pool.currentState = statedb
pool.pendingNonces = newNoncer(statedb)

costFn := types.NewL1CostFunc(pool.chainconfig, statedb)
pool.l1CostFn = func(dataGas types.RollupGasData) *big.Int {
return costFn(newHead.Number.Uint64(), newHead.Time, dataGas, false)
if costFn := types.NewL1CostFunc(pool.chainconfig, statedb); costFn != nil {
pool.l1CostFn = func(rollupCostData types.RollupCostData) *big.Int {
return costFn(rollupCostData, newHead.Time)
}
}

// Inject any transactions discarded due to reorgs
Expand Down Expand Up @@ -1481,7 +1482,7 @@ func (pool *LegacyPool) promoteExecutables(accounts []common.Address) []*types.T
if !list.Empty() && pool.l1CostFn != nil {
// Reduce the cost-cap by L1 rollup cost of the first tx if necessary. Other txs will get filtered out afterwards.
el := list.txs.FirstElement()
if l1Cost := pool.l1CostFn(el.RollupDataGas()); l1Cost != nil {
if l1Cost := pool.l1CostFn(el.RollupCostData()); l1Cost != nil {
balance = new(big.Int).Sub(balance, l1Cost) // negative big int is fine
}
}
Expand Down Expand Up @@ -1690,7 +1691,7 @@ func (pool *LegacyPool) demoteUnexecutables() {
if !list.Empty() && pool.l1CostFn != nil {
// Reduce the cost-cap by L1 rollup cost of the first tx if necessary. Other txs will get filtered out afterwards.
el := list.txs.FirstElement()
if l1Cost := pool.l1CostFn(el.RollupDataGas()); l1Cost != nil {
if l1Cost := pool.l1CostFn(el.RollupCostData()); l1Cost != nil {
balance = new(big.Int).Sub(balance, l1Cost) // negative big int is fine
}
}
Expand Down
2 changes: 1 addition & 1 deletion core/txpool/legacypool/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ func (l *list) Add(tx *types.Transaction, priceBump uint64, l1CostFn txpool.L1Co
// Add new tx cost to totalcost
l.totalcost.Add(l.totalcost, tx.Cost())
if l1CostFn != nil {
if l1Cost := l1CostFn(tx.RollupDataGas()); l1Cost != nil { // add rollup cost
if l1Cost := l1CostFn(tx.RollupCostData()); l1Cost != nil { // add rollup cost
l.totalcost.Add(l.totalcost, l1Cost)
}
}
Expand Down
2 changes: 1 addition & 1 deletion core/txpool/txpool.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import (
"github.com/ethereum/go-ethereum/metrics"
)

type L1CostFunc func(dataGas types.RollupGasData) *big.Int
type L1CostFunc func(dataGas types.RollupCostData) *big.Int

// TxStatus is the current status of a transaction as seen by the pool.
type TxStatus uint
Expand Down
2 changes: 1 addition & 1 deletion core/txpool/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ func ValidateTransactionWithState(tx *types.Transaction, signer types.Signer, op
cost = tx.Cost()
)
if opts.L1CostFn != nil {
if l1Cost := opts.L1CostFn(tx.RollupDataGas()); l1Cost != nil { // add rollup cost
if l1Cost := opts.L1CostFn(tx.RollupCostData()); l1Cost != nil { // add rollup cost
cost = cost.Add(cost, l1Cost)
}
}
Expand Down
29 changes: 10 additions & 19 deletions core/types/receipt.go
Original file line number Diff line number Diff line change
Expand Up @@ -570,27 +570,18 @@ func (rs Receipts) DeriveFields(config *params.ChainConfig, hash common.Hash, nu
}
}
if config.Optimism != nil && len(txs) >= 2 { // need at least an info tx and a non-info tx
if data := txs[0].Data(); len(data) >= 4+32*8 { // function selector + 8 arguments to setL1BlockValues
l1Basefee := new(big.Int).SetBytes(data[4+32*2 : 4+32*3]) // arg index 2
overhead := new(big.Int).SetBytes(data[4+32*6 : 4+32*7]) // arg index 6
scalar := new(big.Int).SetBytes(data[4+32*7 : 4+32*8]) // arg index 7
fscalar := new(big.Float).SetInt(scalar) // legacy: format fee scalar as big Float
fdivisor := new(big.Float).SetUint64(1_000_000) // 10**6, i.e. 6 decimals
feeScalar := new(big.Float).Quo(fscalar, fdivisor)
for i := 0; i < len(rs); i++ {
if !txs[i].IsDepositTx() {
gas := txs[i].RollupDataGas().DataGas(time, config)
rs[i].L1GasPrice = l1Basefee
// GasUsed reported in receipt should include the overhead
rs[i].L1GasUsed = new(big.Int).Add(new(big.Int).SetUint64(gas), overhead)
rs[i].L1Fee = L1Cost(gas, l1Basefee, overhead, scalar)
rs[i].FeeScalar = feeScalar
}
l1Basefee, costFunc, feeScalar, err := extractL1GasParams(config, time, txs[0].Data())
if err != nil {
return err
}
for i := 0; i < len(rs); i++ {
if txs[i].IsDepositTx() {
continue
}
} else {
return fmt.Errorf("L1 info tx only has %d bytes, cannot read gas price parameters", len(data))
rs[i].L1GasPrice = l1Basefee
rs[i].L1Fee, rs[i].L1GasUsed = costFunc(txs[i].RollupCostData())
rs[i].FeeScalar = feeScalar
}
}

return nil
}
148 changes: 148 additions & 0 deletions core/types/rollup_cost.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Copyright 2022 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.

package types

import (
"fmt"
"math/big"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
)

type RollupCostData struct {
zeroes, ones uint64
}

func NewRollupCostData(data []byte) (out RollupCostData) {
for _, b := range data {
if b == 0 {
out.zeroes++
} else {
out.ones++
}
}
return out
}

type StateGetter interface {
GetState(common.Address, common.Hash) common.Hash
}

// L1CostFunc is used in the state transition to determine the L1 data fee charged to the sender of
// non-Deposit transactions.
// It returns nil if no L1 data fee is charged.
type L1CostFunc func(rcd RollupCostData, blockTime uint64) *big.Int

// l1CostFunc is an internal version of L1CostFunc that also returns the gasUsed for use in
// receipts.
type l1CostFunc func(rcd RollupCostData) (fee, gasUsed *big.Int)

var (
L1BasefeeSlot = common.BigToHash(big.NewInt(1))
OverheadSlot = common.BigToHash(big.NewInt(5))
ScalarSlot = common.BigToHash(big.NewInt(6))
)

var L1BlockAddr = common.HexToAddress("0x4200000000000000000000000000000000000015")

// NewL1CostFunc returns a function used for calculating L1 fee cost, or nil if this is not an
// op-stack chain.
func NewL1CostFunc(config *params.ChainConfig, statedb StateGetter) L1CostFunc {
if config.Optimism == nil {
return nil
}
forBlock := ^uint64(0)
var cachedFunc l1CostFunc
return func(rollupCostData RollupCostData, blockTime uint64) *big.Int {
if rollupCostData == (RollupCostData{}) {
return nil // Do not charge if there is no rollup cost-data (e.g. RPC call or deposit).
}
if forBlock != blockTime {
// Note: The following variables are not initialized from the state DB until this point
// to allow deposit transactions from the block to be processed first by state
// transition. This behavior is consensus critical!
l1Basefee := statedb.GetState(L1BlockAddr, L1BasefeeSlot).Big()
overhead := statedb.GetState(L1BlockAddr, OverheadSlot).Big()
scalar := statedb.GetState(L1BlockAddr, ScalarSlot).Big()
isRegolith := config.IsRegolith(blockTime)
cachedFunc = newL1CostFunc(l1Basefee, overhead, scalar, isRegolith)
if forBlock != ^uint64(0) {
// best practice is not to re-use l1 cost funcs across different blocks, but we
// make it work just in case.
log.Info("l1 cost func re-used for different L1 block", "oldTime", forBlock, "newTime", blockTime)
}
forBlock = blockTime
}
fee, _ := cachedFunc(rollupCostData)
return fee
}
}

var (
oneMillion = big.NewInt(1_000_000)
)

func newL1CostFunc(l1Basefee, overhead, scalar *big.Int, isRegolith bool) l1CostFunc {
return func(rollupCostData RollupCostData) (fee, gasUsed *big.Int) {
if rollupCostData == (RollupCostData{}) {
return nil, nil // Do not charge if there is no rollup cost-data (e.g. RPC call or deposit)
}
gas := rollupCostData.zeroes * params.TxDataZeroGas
if isRegolith {
gas += rollupCostData.ones * params.TxDataNonZeroGasEIP2028
} else {
gas += (rollupCostData.ones + 68) * params.TxDataNonZeroGasEIP2028
}
gasWithOverhead := new(big.Int).SetUint64(gas)
gasWithOverhead.Add(gasWithOverhead, overhead)
l1Cost := l1CostHelper(gasWithOverhead, l1Basefee, scalar)
return l1Cost, gasWithOverhead
}
}

// extractL1GasParams extracts the gas parameters necessary to compute gas costs from L1 block info
// calldata.
func extractL1GasParams(config *params.ChainConfig, time uint64, data []byte) (l1Basefee *big.Int, costFunc l1CostFunc, feeScalar *big.Float, err error) {
// data consists of func selector followed by 7 ABI-encoded parameters (32 bytes each)
if len(data) < 4+32*8 {
return nil, nil, nil, fmt.Errorf("expected at least %d L1 info bytes, got %d", 4+32*8, len(data))
}
data = data[4:] // trim function selector
l1Basefee = new(big.Int).SetBytes(data[32*2 : 32*3]) // arg index 2
overhead := new(big.Int).SetBytes(data[32*6 : 32*7]) // arg index 6
scalar := new(big.Int).SetBytes(data[32*7 : 32*8]) // arg index 7
fscalar := new(big.Float).SetInt(scalar) // legacy: format fee scalar as big Float
fdivisor := new(big.Float).SetUint64(1_000_000) // 10**6, i.e. 6 decimals
feeScalar = new(big.Float).Quo(fscalar, fdivisor)
costFunc = newL1CostFunc(l1Basefee, overhead, scalar, config.IsRegolith(time))
return
}

// L1Cost computes the the L1 data fee. It is used by e2e tests so must remain exported.
func L1Cost(rollupDataGas uint64, l1Basefee, overhead, scalar *big.Int) *big.Int {
l1GasUsed := new(big.Int).SetUint64(rollupDataGas)
l1GasUsed.Add(l1GasUsed, overhead)
return l1CostHelper(l1GasUsed, l1Basefee, scalar)
}

func l1CostHelper(gasWithOverhead, l1Basefee, scalar *big.Int) *big.Int {
fee := new(big.Int).Set(gasWithOverhead)
fee.Mul(fee, l1Basefee).Mul(fee, scalar).Div(fee, oneMillion)
return fee
}
Loading

0 comments on commit e417703

Please sign in to comment.