diff --git a/cmd/geth/config.go b/cmd/geth/config.go index df12a831b6..718244f588 100644 --- a/cmd/geth/config.go +++ b/cmd/geth/config.go @@ -213,6 +213,11 @@ func makeFullNode(ctx *cli.Context) *node.Node { cfg.Eth.OverrideOptimismHolocene = &v } + if ctx.IsSet(utils.OverrideOptimismIsthmus.Name) { + v := ctx.Uint64(utils.OverrideOptimismIsthmus.Name) + cfg.Eth.OverrideOptimismIsthmus = &v + } + if ctx.IsSet(utils.OverrideOptimismInterop.Name) { v := ctx.Uint64(utils.OverrideOptimismInterop.Name) cfg.Eth.OverrideOptimismInterop = &v diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 30c7df3b84..063d54f96d 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -71,6 +71,7 @@ var ( utils.OverrideOptimismFjord, utils.OverrideOptimismGranite, utils.OverrideOptimismHolocene, + utils.OverrideOptimismIsthmus, utils.OverrideOptimismInterop, utils.EnablePersonal, utils.TxPoolLocalsFlag, diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 8eda534783..f944d24713 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -284,6 +284,11 @@ var ( Usage: "Manually specify the Optimism Holocene fork timestamp, overriding the bundled setting", Category: flags.EthCategory, } + OverrideOptimismIsthmus = &cli.Uint64Flag{ + Name: "override.isthmus", + Usage: "Manually specify the Optimism Isthmus fork timestamp, overriding the bundled setting", + Category: flags.EthCategory, + } OverrideOptimismInterop = &cli.Uint64Flag{ Name: "override.interop", Usage: "Manually specify the Optimsim Interop feature-set fork timestamp, overriding the bundled setting", diff --git a/core/evm.go b/core/evm.go index e8016fbb78..2eab83c030 100644 --- a/core/evm.go +++ b/core/evm.go @@ -64,18 +64,19 @@ func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common random = &header.MixDigest } return vm.BlockContext{ - CanTransfer: CanTransfer, - Transfer: Transfer, - GetHash: GetHashFn(header, chain), - Coinbase: beneficiary, - BlockNumber: new(big.Int).Set(header.Number), - Time: header.Time, - Difficulty: new(big.Int).Set(header.Difficulty), - BaseFee: baseFee, - BlobBaseFee: blobBaseFee, - GasLimit: header.GasLimit, - Random: random, - L1CostFunc: types.NewL1CostFunc(config, statedb), + CanTransfer: CanTransfer, + Transfer: Transfer, + GetHash: GetHashFn(header, chain), + Coinbase: beneficiary, + BlockNumber: new(big.Int).Set(header.Number), + Time: header.Time, + Difficulty: new(big.Int).Set(header.Difficulty), + BaseFee: baseFee, + BlobBaseFee: blobBaseFee, + GasLimit: header.GasLimit, + Random: random, + L1CostFunc: types.NewL1CostFunc(config, statedb), + OperatorCostFunc: types.NewOperatorCostFunc(config, statedb), } } diff --git a/core/genesis.go b/core/genesis.go index 278cde1068..6141a2ee82 100644 --- a/core/genesis.go +++ b/core/genesis.go @@ -276,6 +276,7 @@ type ChainOverrides struct { OverrideOptimismFjord *uint64 OverrideOptimismGranite *uint64 OverrideOptimismHolocene *uint64 + OverrideOptimismIsthmus *uint64 OverrideOptimismInterop *uint64 ApplySuperchainUpgrades bool } @@ -342,6 +343,9 @@ func SetupGenesisBlockWithOverride(db ethdb.Database, triedb *triedb.Database, g if overrides != nil && overrides.OverrideOptimismHolocene != nil { config.HoloceneTime = overrides.OverrideOptimismHolocene } + if overrides != nil && overrides.OverrideOptimismIsthmus != nil { + config.IsthmusTime = overrides.OverrideOptimismIsthmus + } if overrides != nil && overrides.OverrideOptimismInterop != nil { config.InteropTime = overrides.OverrideOptimismInterop } diff --git a/core/state_transition.go b/core/state_transition.go index a3b05705c9..7d5ded3b6d 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -261,6 +261,13 @@ func (st *StateTransition) buyGas() error { mgval = mgval.Add(mgval, l1Cost) } } + var operatorCost *big.Int + if st.evm.Context.OperatorCostFunc != nil && !st.msg.SkipNonceChecks && !st.msg.SkipFromEOACheck { + operatorCost = st.evm.Context.OperatorCostFunc(new(big.Int).SetUint64(st.msg.GasLimit), false, st.evm.Context.Time) + if operatorCost != nil { + mgval = mgval.Add(mgval, operatorCost) + } + } balanceCheck := new(big.Int).Set(mgval) if st.msg.GasFeeCap != nil { balanceCheck.SetUint64(st.msg.GasLimit) @@ -268,6 +275,9 @@ func (st *StateTransition) buyGas() error { if l1Cost != nil { balanceCheck.Add(balanceCheck, l1Cost) } + if operatorCost != nil { + balanceCheck.Add(balanceCheck, operatorCost) + } } balanceCheck.Add(balanceCheck, st.msg.Value) @@ -611,6 +621,13 @@ func (st *StateTransition) innerTransitionDb() (*ExecutionResult, error) { } st.state.AddBalance(params.OptimismL1FeeRecipient, amtU256, tracing.BalanceIncreaseRewardTransactionFee) } + if operatorCost := st.evm.Context.OperatorCostFunc(new(big.Int).SetUint64(st.msg.GasLimit), false, st.evm.Context.Time); operatorCost != nil { + amtU256, overflow = uint256.FromBig(operatorCost) + if overflow { + return nil, fmt.Errorf("optimism operator cost overflows U256: %d", operatorCost) + } + st.state.AddBalance(params.OptimismOperatorFeeRecipient, amtU256, tracing.BalanceIncreaseRewardTransactionFee) + } } } @@ -644,6 +661,16 @@ func (st *StateTransition) refundGas(refundQuotient uint64) uint64 { st.evm.Config.Tracer.OnGasChange(st.gasRemaining, 0, tracing.GasChangeTxLeftOverReturned) } + if optimismConfig := st.evm.ChainConfig().Optimism; optimismConfig != nil && !st.msg.IsDepositTx { + // Return ETH to transaction sender for operator cost overcharge. + if operatorCost := st.evm.Context.OperatorCostFunc(new(big.Int).SetUint64(st.gasRemaining), true, st.evm.Context.Time); operatorCost != nil { + amtU256, overflow := uint256.FromBig(operatorCost) + if !overflow { + st.state.AddBalance(st.msg.From, amtU256, tracing.BalanceIncreaseGasReturn) + } + } + } + // Also return remaining gas to the block gas counter so it is // available for the next transaction. st.gp.AddGas(st.gasRemaining) diff --git a/core/types/gen_receipt_json.go b/core/types/gen_receipt_json.go index 4e544a0b52..cd527d7d40 100644 --- a/core/types/gen_receipt_json.go +++ b/core/types/gen_receipt_json.go @@ -40,6 +40,8 @@ func (r Receipt) MarshalJSON() ([]byte, error) { FeeScalar *big.Float `json:"l1FeeScalar,omitempty"` L1BaseFeeScalar *hexutil.Uint64 `json:"l1BaseFeeScalar,omitempty"` L1BlobBaseFeeScalar *hexutil.Uint64 `json:"l1BlobBaseFeeScalar,omitempty"` + OperatorFeeScalar *hexutil.Uint64 `json:"operatorFeeScalar,omitempty"` + OperatorFeeConstant *hexutil.Uint64 `json:"operatorFeeConstant,omitempty"` } var enc Receipt enc.Type = hexutil.Uint64(r.Type) @@ -66,6 +68,8 @@ func (r Receipt) MarshalJSON() ([]byte, error) { enc.FeeScalar = r.FeeScalar enc.L1BaseFeeScalar = (*hexutil.Uint64)(r.L1BaseFeeScalar) enc.L1BlobBaseFeeScalar = (*hexutil.Uint64)(r.L1BlobBaseFeeScalar) + enc.OperatorFeeScalar = (*hexutil.Uint64)(r.OperatorFeeScalar) + enc.OperatorFeeConstant = (*hexutil.Uint64)(r.OperatorFeeConstant) return json.Marshal(&enc) } @@ -96,6 +100,8 @@ func (r *Receipt) UnmarshalJSON(input []byte) error { FeeScalar *big.Float `json:"l1FeeScalar,omitempty"` L1BaseFeeScalar *hexutil.Uint64 `json:"l1BaseFeeScalar,omitempty"` L1BlobBaseFeeScalar *hexutil.Uint64 `json:"l1BlobBaseFeeScalar,omitempty"` + OperatorFeeScalar *hexutil.Uint64 `json:"operatorFeeScalar,omitempty"` + OperatorFeeConstant *hexutil.Uint64 `json:"operatorFeeConstant,omitempty"` } var dec Receipt if err := json.Unmarshal(input, &dec); err != nil { @@ -178,5 +184,11 @@ func (r *Receipt) UnmarshalJSON(input []byte) error { if dec.L1BlobBaseFeeScalar != nil { r.L1BlobBaseFeeScalar = (*uint64)(dec.L1BlobBaseFeeScalar) } + if dec.OperatorFeeScalar != nil { + r.OperatorFeeScalar = (*uint64)(dec.OperatorFeeScalar) + } + if dec.OperatorFeeConstant != nil { + r.OperatorFeeConstant = (*uint64)(dec.OperatorFeeConstant) + } return nil } diff --git a/core/types/receipt.go b/core/types/receipt.go index 4bc83bf988..b81339cc88 100644 --- a/core/types/receipt.go +++ b/core/types/receipt.go @@ -84,7 +84,7 @@ type Receipt struct { BlockNumber *big.Int `json:"blockNumber,omitempty"` TransactionIndex uint `json:"transactionIndex"` - // Optimism: extend receipts with L1 fee info + // Optimism: extend receipts with L1 and operator fee info L1GasPrice *big.Int `json:"l1GasPrice,omitempty"` // Present from pre-bedrock. L1 Basefee after Bedrock L1BlobBaseFee *big.Int `json:"l1BlobBaseFee,omitempty"` // Always nil prior to the Ecotone hardfork L1GasUsed *big.Int `json:"l1GasUsed,omitempty"` // Present from pre-bedrock, deprecated as of Fjord @@ -92,6 +92,8 @@ type Receipt struct { FeeScalar *big.Float `json:"l1FeeScalar,omitempty"` // Present from pre-bedrock to Ecotone. Nil after Ecotone L1BaseFeeScalar *uint64 `json:"l1BaseFeeScalar,omitempty"` // Always nil prior to the Ecotone hardfork L1BlobBaseFeeScalar *uint64 `json:"l1BlobBaseFeeScalar,omitempty"` // Always nil prior to the Ecotone hardfork + OperatorFeeScalar *uint64 `json:"operatorFeeScalar,omitempty"` // Always nil prior to the Isthmus hardfork + OperatorFeeConstant *uint64 `json:"operatorFeeConstant,omitempty"` // Always nil prior to the Isthmus hardfork } type receiptMarshaling struct { @@ -116,6 +118,8 @@ type receiptMarshaling struct { L1BlobBaseFeeScalar *hexutil.Uint64 DepositNonce *hexutil.Uint64 DepositReceiptVersion *hexutil.Uint64 + OperatorFeeScalar *hexutil.Uint64 + OperatorFeeConstant *hexutil.Uint64 } // receiptRLP is the consensus encoding of a receipt. @@ -590,6 +594,8 @@ func (rs Receipts) DeriveFields(config *params.ChainConfig, hash common.Hash, nu rs[i].FeeScalar = gasParams.feeScalar rs[i].L1BaseFeeScalar = u32ptrTou64ptr(gasParams.l1BaseFeeScalar) rs[i].L1BlobBaseFeeScalar = u32ptrTou64ptr(gasParams.l1BlobBaseFeeScalar) + rs[i].OperatorFeeScalar = u32ptrTou64ptr(gasParams.operatorFeeScalar) + rs[i].OperatorFeeConstant = gasParams.operatorFeeConstant } } return nil diff --git a/core/types/receipt_test.go b/core/types/receipt_test.go index 76599fdd32..743be22b86 100644 --- a/core/types/receipt_test.go +++ b/core/types/receipt_test.go @@ -48,6 +48,12 @@ var ( conf.EcotoneTime = &time return &conf }() + isthmusTestConfig = func() *params.ChainConfig { + conf := *bedrockGenesisTestConfig // copy the config + time := uint64(0) + conf.IsthmusTime = &time + return &conf + }() legacyReceipt = &Receipt{ Status: ReceiptStatusFailed, @@ -768,6 +774,78 @@ func getOptimismEcotoneTxReceipts(l1AttributesPayload []byte, l1GasPrice, l1Blob return txs, receipts } +func getOptimismIsthmusTxReceipts(l1AttributesPayload []byte, l1GasPrice, l1BlobGasPrice, l1GasUsed, l1Fee *big.Int, baseFeeScalar, blobBaseFeeScalar, operatorFeeScalar, operatorFeeConstant *uint64) ([]*Transaction, []*Receipt) { + // Create a few transactions to have receipts for + txs := Transactions{ + NewTx(&DepositTx{ + To: nil, // contract creation + Value: big.NewInt(6), + Gas: 50, + Data: l1AttributesPayload, + }), + emptyTx, + } + + // Create the corresponding receipts + receipts := Receipts{ + &Receipt{ + Type: DepositTxType, + PostState: common.Hash{5}.Bytes(), + CumulativeGasUsed: 50 + 15, + Logs: []*Log{ + { + Address: common.BytesToAddress([]byte{0x33}), + // derived fields: + BlockNumber: blockNumber.Uint64(), + TxHash: txs[0].Hash(), + TxIndex: 0, + BlockHash: blockHash, + Index: 0, + }, + { + Address: common.BytesToAddress([]byte{0x03, 0x33}), + // derived fields: + BlockNumber: blockNumber.Uint64(), + TxHash: txs[0].Hash(), + TxIndex: 0, + BlockHash: blockHash, + Index: 1, + }, + }, + TxHash: txs[0].Hash(), + ContractAddress: common.HexToAddress("0x3bb898b4bbe24f68a4e9be46cfe72d1787fd74f4"), + GasUsed: 65, + EffectiveGasPrice: big.NewInt(0), + BlockHash: blockHash, + BlockNumber: blockNumber, + TransactionIndex: 0, + DepositNonce: &depNonce1, + }, + &Receipt{ + Type: LegacyTxType, + EffectiveGasPrice: big.NewInt(0), + PostState: common.Hash{4}.Bytes(), + CumulativeGasUsed: 10, + Logs: []*Log{}, + // derived fields: + TxHash: txs[1].Hash(), + GasUsed: 18446744073709551561, + BlockHash: blockHash, + BlockNumber: blockNumber, + TransactionIndex: 1, + L1GasPrice: l1GasPrice, + L1BlobBaseFee: l1BlobGasPrice, + L1GasUsed: l1GasUsed, + L1Fee: l1Fee, + L1BaseFeeScalar: baseFeeScalar, + L1BlobBaseFeeScalar: blobBaseFeeScalar, + OperatorFeeScalar: operatorFeeScalar, + OperatorFeeConstant: operatorFeeConstant, + }, + } + return txs, receipts +} + func getOptimismTxReceipts(l1AttributesPayload []byte, l1GasPrice, l1GasUsed, l1Fee *big.Int, feeScalar *big.Float) ([]*Transaction, []*Receipt) { // Create a few transactions to have receipts for txs := Transactions{ @@ -888,6 +966,32 @@ func TestDeriveOptimismEcotoneTxReceipts(t *testing.T) { diffReceipts(t, receipts, derivedReceipts) } +func TestDeriveOptimismIsthmusTxReceipts(t *testing.T) { + // Isthmus style l1 attributes with baseFeeScalar=2, blobBaseFeeScalar=3, baseFee=1000*1e6, blobBaseFee=10*1e6, operatorFeeScalar=7, operatorFeeConstant=9 + payload := common.Hex2Bytes("098999be000000020000000300000000000004d200000000000004d200000000000004d2000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000000000000000000000000000000000000098968000000000000000000000000000000000000000000000000000000000000004d200000000000000000000000000000000000000000000000000000000000004d2000000070000000000000009") + // the parameters we use below are defined in rollup_test.go + baseFeeScalarUint64 := baseFeeScalar.Uint64() + blobBaseFeeScalarUint64 := blobBaseFeeScalar.Uint64() + operatorFeeScalarUint64 := operatorFeeScalar.Uint64() + operatorFeeConstantUint64 := operatorFeeConstant.Uint64() + txs, receipts := getOptimismIsthmusTxReceipts(payload, baseFee, blobBaseFee, minimumFjordGas, fjordFee, &baseFeeScalarUint64, &blobBaseFeeScalarUint64, &operatorFeeScalarUint64, &operatorFeeConstantUint64) + + // Re-derive receipts. + baseFee := big.NewInt(1000) + derivedReceipts := clearComputedFieldsOnReceipts(receipts) + // Should error out if we try to process this with a pre-Isthmus config + err := Receipts(derivedReceipts).DeriveFields(bedrockGenesisTestConfig, blockHash, blockNumber.Uint64(), 0, baseFee, nil, txs) + if err == nil { + t.Fatalf("expected error from deriving isthmus receipts with pre-isthmus config, got none") + } + + err = Receipts(derivedReceipts).DeriveFields(isthmusTestConfig, blockHash, blockNumber.Uint64(), 0, baseFee, nil, txs) + if err != nil { + t.Fatalf("DeriveFields(...) = %v, want ", err) + } + diffReceipts(t, receipts, derivedReceipts) +} + func diffReceipts(t *testing.T, receipts, derivedReceipts []*Receipt) { // Check diff of receipts against derivedReceipts. r1, err := json.MarshalIndent(receipts, "", " ") diff --git a/core/types/rollup_cost.go b/core/types/rollup_cost.go index f398c43af1..2946f2c6e9 100644 --- a/core/types/rollup_cost.go +++ b/core/types/rollup_cost.go @@ -36,9 +36,9 @@ const ( BaseFeeScalarSlotOffset = 12 // bytes [16:20) of the slot BlobBaseFeeScalarSlotOffset = 8 // bytes [20:24) of the slot - // scalarSectionStart is the beginning of the scalar values segment in the slot + // ecotoneScalarSectionStart is the beginning of the scalar values segment in the slot // array. baseFeeScalar is in the first four bytes of the segment, blobBaseFeeScalar the next - // four. + // four, operatorFeeScalar the next four, and operatorFeeConstant the next eight. scalarSectionStart = 32 - BaseFeeScalarSlotOffset - 4 ) @@ -54,6 +54,8 @@ var ( BedrockL1AttributesSelector = []byte{0x01, 0x5d, 0x8e, 0xb9} // EcotoneL1AttributesSelector is the selector indicating Ecotone style L1 gas attributes. EcotoneL1AttributesSelector = []byte{0x44, 0x0a, 0x5e, 0x20} + // IsthmusL1AttributesSelector is the selector indicating Isthmus style L1 gas attributes. + IsthmusL1AttributesSelector = []byte{0x09, 0x89, 0x99, 0xbe} // L1BlockAddr is the address of the L1Block contract which stores the L1 gas attributes. L1BlockAddr = common.HexToAddress("0x4200000000000000000000000000000000000015") @@ -70,6 +72,10 @@ var ( // `BlobBaseFeeScalarSlotOffset` respectively. L1FeeScalarsSlot = common.BigToHash(big.NewInt(3)) + // OperatorFeeParamsSlot stores the operatorFeeScalar and operatorFeeConstant L1 gas + // attributes + OperatorFeeParamsSlot = common.BigToHash(big.NewInt(8)) + oneMillion = big.NewInt(1_000_000) ecotoneDivisor = big.NewInt(1_000_000 * 16) fjordDivisor = big.NewInt(1_000_000_000_000) @@ -99,6 +105,11 @@ type StateGetter interface { // sender of non-Deposit transactions. It returns nil if no data availability fee is charged. type L1CostFunc func(rcd RollupCostData, blockTime uint64) *big.Int +// OperatorCostFunc is used in the state transition to determine the operator fee charged to the +// sender of non-Deposit transactions. It returns nil if no data availability fee is charged. +// The `isRefund` parameter is true if calculating a refund. +type OperatorCostFunc func(gasUsed *big.Int, isRefund bool, 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) @@ -179,6 +190,29 @@ func NewL1CostFunc(config *params.ChainConfig, statedb StateGetter) L1CostFunc { } } +// NewOperatorCostFunc returns a function used for calculating operator fees, or nil if this is +// not an op-stack chain. +func NewOperatorCostFunc(config *params.ChainConfig, statedb StateGetter) OperatorCostFunc { + if config.Optimism == nil { + return nil + } + return func(gasUsed *big.Int, isRefund bool, blockTime uint64) *big.Int { + if !config.IsOptimismIsthmus(blockTime) { + return big.NewInt(0) + } + operatorFeeParams := statedb.GetState(L1BlockAddr, OperatorFeeParamsSlot).Bytes() + + operatorFeeScalar, operatorFeeConstant := extractOperatorFeeParams(operatorFeeParams) + product := operatorFeeScalar.Mul(gasUsed, operatorFeeScalar) + product = product.Div(product, oneMillion) + if isRefund { + return product + } else { + return product.Add(product, operatorFeeConstant) + } + } +} + // newL1CostFuncBedrock returns an L1 cost function suitable for Bedrock, Regolith, and the first // block only of the Ecotone upgrade. func newL1CostFuncBedrock(config *params.ChainConfig, statedb StateGetter, blockTime uint64) l1CostFunc { @@ -248,6 +282,8 @@ type gasParams struct { feeScalar *big.Float // pre-ecotone l1BaseFeeScalar *uint32 // post-ecotone l1BlobBaseFeeScalar *uint32 // post-ecotone + operatorFeeScalar *uint32 // post-Isthmus + operatorFeeConstant *uint64 // post-Isthmus } // intToScaledFloat returns scalar/10e6 as a float @@ -259,11 +295,23 @@ func intToScaledFloat(scalar *big.Int) *big.Float { // extractL1GasParams extracts the gas parameters necessary to compute gas costs from L1 block info func extractL1GasParams(config *params.ChainConfig, time uint64, data []byte) (gasParams, error) { - // edge case: for the very first Ecotone block we still need to use the Bedrock - // function. We detect this edge case by seeing if the function selector is the old one - // If so, fall through to the pre-ecotone format - // Both Ecotone and Fjord use the same function selector - if config.IsEcotone(time) && len(data) >= 4 && !bytes.Equal(data[0:4], BedrockL1AttributesSelector) { + if config.IsIsthmus(time) { + p, err := extractL1GasParamsPostIsthmus(data) + if err != nil { + return gasParams{}, err + } + p.costFunc = NewL1CostFuncFjord( + p.l1BaseFee, + p.l1BlobBaseFee, + big.NewInt(int64(*p.l1BaseFeeScalar)), + big.NewInt(int64(*p.l1BlobBaseFeeScalar)), + ) + return p, nil + } else if config.IsEcotone(time) && len(data) >= 4 && !bytes.Equal(data[0:4], BedrockL1AttributesSelector) { + // edge case: for the very first Ecotone block we still need to use the Bedrock + // function. We detect this edge case by seeing if the function selector is the old one + // If so, fall through to the pre-ecotone format + // Both Ecotone and Fjord use the same function selector p, err := extractL1GasParamsPostEcotone(data) if err != nil { return gasParams{}, err @@ -338,6 +386,43 @@ func extractL1GasParamsPostEcotone(data []byte) (gasParams, error) { }, nil } +// extractL1GasParamsPostIsthmus extracts the gas parameters necessary to compute gas from L1 attribute +// info calldata after the Isthmus upgrade, but not for the very first Isthmus block. +func extractL1GasParamsPostIsthmus(data []byte) (gasParams, error) { + if len(data) != 176 { + return gasParams{}, fmt.Errorf("expected 176 L1 info bytes, got %d", len(data)) + } + // data layout assumed for Isthmus: + // offset type varname + // 0 + // 4 uint32 _basefeeScalar + // 8 uint32 _blobBaseFeeScalar + // 12 uint64 _sequenceNumber, + // 20 uint64 _timestamp, + // 28 uint64 _l1BlockNumber + // 36 uint256 _basefee, + // 68 uint256 _blobBaseFee, + // 100 bytes32 _hash, + // 132 bytes32 _batcherHash, + // 164 uint32 _operatorFeeScalar + // 168 uint64 _operatorFeeConstant + l1BaseFee := new(big.Int).SetBytes(data[36:68]) + l1BlobBaseFee := new(big.Int).SetBytes(data[68:100]) + l1BaseFeeScalar := binary.BigEndian.Uint32(data[4:8]) + l1BlobBaseFeeScalar := binary.BigEndian.Uint32(data[8:12]) + operatorFeeScalar := binary.BigEndian.Uint32(data[164:168]) + operatorFeeConstant := binary.BigEndian.Uint64(data[168:176]) + + return gasParams{ + l1BaseFee: l1BaseFee, + l1BlobBaseFee: l1BlobBaseFee, + l1BaseFeeScalar: &l1BaseFeeScalar, + l1BlobBaseFeeScalar: &l1BlobBaseFeeScalar, + operatorFeeScalar: &operatorFeeScalar, + operatorFeeConstant: &operatorFeeConstant, + }, nil +} + // L1Cost computes the the data availability fee for transactions in blocks prior to the Ecotone // upgrade. It is used by e2e tests so must remain exported. func L1Cost(rollupDataGas uint64, l1BaseFee, overhead, scalar *big.Int) *big.Int { @@ -401,6 +486,12 @@ func extractEcotoneFeeParams(l1FeeParams []byte) (l1BaseFeeScalar, l1BlobBaseFee return } +func extractOperatorFeeParams(operatorFeeParams []byte) (operatorFeeScalar, operatorFeeConstant *big.Int) { + operatorFeeScalar = new(big.Int).SetBytes(operatorFeeParams[20:24]) + operatorFeeConstant = new(big.Int).SetBytes(operatorFeeParams[24:32]) + return +} + func bedrockCalldataGasUsed(costData RollupCostData) (calldataGasUsed *big.Int) { calldataGas := (costData.Zeroes * params.TxDataZeroGas) + (costData.Ones * params.TxDataNonZeroGasEIP2028) return new(big.Int).SetUint64(calldataGas) diff --git a/core/types/rollup_cost_test.go b/core/types/rollup_cost_test.go index 8d0f1e9218..4fab7bee75 100644 --- a/core/types/rollup_cost_test.go +++ b/core/types/rollup_cost_test.go @@ -18,9 +18,11 @@ var ( overhead = big.NewInt(50) scalar = big.NewInt(7 * 1e6) - blobBaseFee = big.NewInt(10 * 1e6) - baseFeeScalar = big.NewInt(2) - blobBaseFeeScalar = big.NewInt(3) + blobBaseFee = big.NewInt(10 * 1e6) + baseFeeScalar = big.NewInt(2) + blobBaseFeeScalar = big.NewInt(3) + operatorFeeScalar = big.NewInt(7) + operatorFeeConstant = big.NewInt(9) // below are the expected cost func outcomes for the above parameter settings on the emptyTx // which is defined in transaction_test.go @@ -206,6 +208,39 @@ func TestExtractFjordGasParams(t *testing.T) { require.Equal(t, fjordFee, c) } +func TestExtractIsthmusGasParams(t *testing.T) { + zeroTime := uint64(0) + // create a config where Isthmus is active + config := ¶ms.ChainConfig{ + Optimism: params.OptimismTestConfig.Optimism, + RegolithTime: &zeroTime, + EcotoneTime: &zeroTime, + FjordTime: &zeroTime, + IsthmusTime: &zeroTime, + } + require.True(t, config.IsOptimismIsthmus(zeroTime)) + + data := getIsthmusL1Attributes( + baseFee, + blobBaseFee, + baseFeeScalar, + blobBaseFeeScalar, + operatorFeeScalar, + operatorFeeConstant, + ) + + gasparams, err := extractL1GasParams(config, zeroTime, data) + require.NoError(t, err) + costFunc := gasparams.costFunc + + c, g := costFunc(emptyTx.RollupCostData()) + + require.Equal(t, minimumFjordGas, g) + require.Equal(t, fjordFee, c) + require.Equal(t, operatorFeeScalar.Uint64(), uint64(*gasparams.operatorFeeScalar)) + require.Equal(t, operatorFeeConstant.Uint64(), *gasparams.operatorFeeConstant) +} + // make sure the first block of the ecotone upgrade is properly detected, and invokes the bedrock // cost function appropriately func TestFirstBlockEcotoneGasParams(t *testing.T) { @@ -263,9 +298,32 @@ func getEcotoneL1Attributes(baseFee, blobBaseFee, baseFeeScalar, blobBaseFeeScal return data } +func getIsthmusL1Attributes(baseFee, blobBaseFee, baseFeeScalar, blobBaseFeeScalar, operatorFeeScalar, operatorFeeConstant *big.Int) []byte { + ignored := big.NewInt(1234) + data := []byte{} + uint256Slice := make([]byte, 32) + uint64Slice := make([]byte, 8) + uint32Slice := make([]byte, 4) + data = append(data, IsthmusL1AttributesSelector...) + data = append(data, baseFeeScalar.FillBytes(uint32Slice)...) + data = append(data, blobBaseFeeScalar.FillBytes(uint32Slice)...) + data = append(data, ignored.FillBytes(uint64Slice)...) + data = append(data, ignored.FillBytes(uint64Slice)...) + data = append(data, ignored.FillBytes(uint64Slice)...) + data = append(data, baseFee.FillBytes(uint256Slice)...) + data = append(data, blobBaseFee.FillBytes(uint256Slice)...) + data = append(data, ignored.FillBytes(uint256Slice)...) + data = append(data, ignored.FillBytes(uint256Slice)...) + data = append(data, operatorFeeScalar.FillBytes(uint32Slice)...) + data = append(data, operatorFeeConstant.FillBytes(uint64Slice)...) + return data +} + type testStateGetter struct { baseFee, blobBaseFee, overhead, scalar *big.Int baseFeeScalar, blobBaseFeeScalar uint32 + operatorFeeScalar uint32 + operatorFeeConstant uint64 } func (sg *testStateGetter) GetState(addr common.Address, slot common.Hash) common.Hash { @@ -280,10 +338,14 @@ func (sg *testStateGetter) GetState(addr common.Address, slot common.Hash) commo case L1BlobBaseFeeSlot: sg.blobBaseFee.FillBytes(buf[:]) case L1FeeScalarsSlot: - // fetch Ecotone fee sclars + // fetch Ecotone fee scalars offset := scalarSectionStart binary.BigEndian.PutUint32(buf[offset:offset+4], sg.baseFeeScalar) binary.BigEndian.PutUint32(buf[offset+4:offset+8], sg.blobBaseFeeScalar) + case OperatorFeeParamsSlot: + // fetch operator fee scalars + binary.BigEndian.PutUint32(buf[0:4], sg.operatorFeeScalar) + binary.BigEndian.PutUint64(buf[4:12], sg.operatorFeeConstant) default: panic("unknown slot") } diff --git a/core/vm/evm.go b/core/vm/evm.go index 1945a3b4db..ecb40fd703 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -61,6 +61,8 @@ type BlockContext struct { GetHash GetHashFunc // L1CostFunc returns the L1 cost of the rollup message, the function may be nil, or return nil L1CostFunc types.L1CostFunc + // OperatorCostFunc returns the operator cost. The function may be nil, or return nil + OperatorCostFunc types.OperatorCostFunc // Block information Coinbase common.Address // Provides information for COINBASE diff --git a/eth/backend.go b/eth/backend.go index 79453719c6..0976280254 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -242,6 +242,9 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { if config.OverrideOptimismHolocene != nil { overrides.OverrideOptimismHolocene = config.OverrideOptimismHolocene } + if config.OverrideOptimismIsthmus != nil { + overrides.OverrideOptimismIsthmus = config.OverrideOptimismIsthmus + } if config.OverrideOptimismInterop != nil { overrides.OverrideOptimismInterop = config.OverrideOptimismInterop } diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index 29eca7bb28..4bd822c83b 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -168,6 +168,8 @@ type Config struct { OverrideOptimismHolocene *uint64 `toml:",omitempty"` + OverrideOptimismIsthmus *uint64 `toml:",omitempty"` + OverrideOptimismInterop *uint64 `toml:",omitempty"` // ApplySuperchainUpgrades requests the node to load chain-configuration from the superchain-registry. diff --git a/eth/ethconfig/gen_config.go b/eth/ethconfig/gen_config.go index 72f0e1dc73..7a6f1e234e 100644 --- a/eth/ethconfig/gen_config.go +++ b/eth/ethconfig/gen_config.go @@ -57,6 +57,7 @@ func (c Config) MarshalTOML() (interface{}, error) { OverrideOptimismFjord *uint64 `toml:",omitempty"` OverrideOptimismGranite *uint64 `toml:",omitempty"` OverrideOptimismHolocene *uint64 `toml:",omitempty"` + OverrideOptimismIsthmus *uint64 `toml:",omitempty"` OverrideOptimismInterop *uint64 `toml:",omitempty"` ApplySuperchainUpgrades bool `toml:",omitempty"` RollupSequencerHTTP string @@ -111,6 +112,7 @@ func (c Config) MarshalTOML() (interface{}, error) { enc.OverrideOptimismFjord = c.OverrideOptimismFjord enc.OverrideOptimismGranite = c.OverrideOptimismGranite enc.OverrideOptimismHolocene = c.OverrideOptimismHolocene + enc.OverrideOptimismIsthmus = c.OverrideOptimismIsthmus enc.OverrideOptimismInterop = c.OverrideOptimismInterop enc.ApplySuperchainUpgrades = c.ApplySuperchainUpgrades enc.RollupSequencerHTTP = c.RollupSequencerHTTP @@ -169,6 +171,7 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error { OverrideOptimismFjord *uint64 `toml:",omitempty"` OverrideOptimismGranite *uint64 `toml:",omitempty"` OverrideOptimismHolocene *uint64 `toml:",omitempty"` + OverrideOptimismIsthmus *uint64 `toml:",omitempty"` OverrideOptimismInterop *uint64 `toml:",omitempty"` ApplySuperchainUpgrades *bool `toml:",omitempty"` RollupSequencerHTTP *string @@ -306,6 +309,9 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error { if dec.OverrideOptimismHolocene != nil { c.OverrideOptimismHolocene = dec.OverrideOptimismHolocene } + if dec.OverrideOptimismIsthmus != nil { + c.OverrideOptimismIsthmus = dec.OverrideOptimismIsthmus + } if dec.OverrideOptimismInterop != nil { c.OverrideOptimismInterop = dec.OverrideOptimismInterop } diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 7712d71273..d5a113e02f 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -2070,6 +2070,13 @@ func marshalReceipt(receipt *types.Receipt, blockHash common.Hash, blockNumber u if receipt.L1BlobBaseFeeScalar != nil { fields["l1BlobBaseFeeScalar"] = hexutil.Uint64(*receipt.L1BlobBaseFeeScalar) } + // Fields added in Isthmus + if receipt.OperatorFeeScalar != nil { + fields["operatorFeeScalar"] = hexutil.Uint64(*receipt.OperatorFeeScalar) + } + if receipt.OperatorFeeConstant != nil { + fields["operatorFeeConstant"] = hexutil.Uint64(*receipt.OperatorFeeConstant) + } } if chainConfig.Optimism != nil && tx.IsDepositTx() && receipt.DepositNonce != nil { fields["depositNonce"] = hexutil.Uint64(*receipt.DepositNonce) diff --git a/params/protocol_params.go b/params/protocol_params.go index 3c5ae0a8b4..3efa9a73dd 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -29,6 +29,8 @@ var ( OptimismL1FeeRecipient = common.HexToAddress("0x420000000000000000000000000000000000001A") // The L2 withdrawals contract predeploy address OptimismL2ToL1MessagePasser = common.HexToAddress("0x4200000000000000000000000000000000000016") + // The operator fee portion of the transaction fee accumulates at this predeploy + OptimismOperatorFeeRecipient = common.HexToAddress("0x420000000000000000000000000000000000001B") ) const ( diff --git a/tests/testdata b/tests/testdata index faf33b4714..9201075490 160000 --- a/tests/testdata +++ b/tests/testdata @@ -1 +1 @@ -Subproject commit faf33b471465d3c6cdc3d04fbd690895f78d33f2 +Subproject commit 9201075490807f58811078e9bb5ec895b4ac01a5