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/state_transition.go b/core/state_transition.go index af53eae272..c38976d8a1 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -253,6 +253,14 @@ func (st *StateTransition) buyGas() error { mgval = mgval.Add(mgval, l1Cost) } } + var operatorCost *big.Int + if st.evm.Context.OperatorCostFunc != nil && !st.msg.SkipAccountChecks { + operatorCost = st.evm.Context.OperatorCostFunc(new(big.Int).SetUint64(st.msg.GasLimit), true, 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) @@ -262,6 +270,9 @@ func (st *StateTransition) buyGas() error { if l1Cost != nil { balanceCheck.Add(balanceCheck, l1Cost) } + if operatorCost != nil { + balanceCheck.Add(balanceCheck, operatorCost) + } if st.evm.ChainConfig().IsCancun(st.evm.Context.BlockNumber, st.evm.Context.Time) { if blobGas := st.blobGasUsed(); blobGas > 0 { @@ -595,6 +606,15 @@ func (st *StateTransition) innerTransitionDb() (*ExecutionResult, error) { } st.state.AddBalance(params.OptimismL1FeeRecipient, amtU256, tracing.BalanceIncreaseRewardTransactionFee) } + + // Additionally pay the coinbase according for the operator fee. + if operatorCost := st.evm.Context.OperatorCostFunc(new(big.Int).SetUint64(st.gasUsed()), true, 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) + } } return &ExecutionResult{ @@ -623,6 +643,16 @@ func (st *StateTransition) refundGas(refundQuotient uint64) uint64 { remaining.Mul(remaining, uint256.MustFromBig(st.msg.GasPrice)) st.state.AddBalance(st.msg.From, remaining, tracing.BalanceIncreaseGasReturn) + 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), false, st.evm.Context.Time); operatorCost != nil { + amtU256, overflow := uint256.FromBig(operatorCost) + if !overflow { + st.state.AddBalance(st.msg.From, amtU256, tracing.BalanceIncreaseRewardTransactionFee) + } + } + } + if st.evm.Config.Tracer != nil && st.evm.Config.Tracer.OnGasChange != nil && st.gasRemaining > 0 { st.evm.Config.Tracer.OnGasChange(st.gasRemaining, 0, tracing.GasChangeTxLeftOverReturned) } 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..990334aace 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 Holocene hardfork + OperatorFeeConstant *uint64 `json:"operatorFeeConstant,omitempty"` // Always nil prior to the Holocene 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..cd194044f5 100644 --- a/core/types/receipt_test.go +++ b/core/types/receipt_test.go @@ -48,6 +48,12 @@ var ( conf.EcotoneTime = &time return &conf }() + holoceneTestConfig = func() *params.ChainConfig { + conf := *bedrockGenesisTestConfig // copy the config + time := uint64(0) + conf.HoloceneTime = &time + return &conf + }() legacyReceipt = &Receipt{ Status: ReceiptStatusFailed, @@ -768,6 +774,78 @@ func getOptimismEcotoneTxReceipts(l1AttributesPayload []byte, l1GasPrice, l1Blob return txs, receipts } +func getOptimismHoloceneTxReceipts(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 TestDeriveOptimismHoloceneTxReceipts(t *testing.T) { + // Holocene style l1 attributes with baseFeeScalar=2, blobBaseFeeScalar=3, baseFee=1000*1e6, blobBaseFee=10*1e6, operatorFeeScalar=7, operatorFeeConstant=9 + payload := common.Hex2Bytes("d1fbe15b000000020000000300000000000004d200000000000004d200000000000004d2000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000000000000000000000000000000000000098968000000000000000000000000000000000000000000000000000000000000004d200000000000000000000000000000000000000000000000000000000000004d2000000070000000000000009") + // 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 := getOptimismHoloceneTxReceipts(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-Holocene config + err := Receipts(derivedReceipts).DeriveFields(bedrockGenesisTestConfig, blockHash, blockNumber.Uint64(), 0, baseFee, nil, txs) + if err == nil { + t.Fatalf("expected error from deriving holocene receipts with pre-holocene config, got none") + } + + err = Receipts(derivedReceipts).DeriveFields(holoceneTestConfig, 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 425d4f1f39..2e396cdbb8 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 ) @@ -72,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) @@ -101,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 `includeConstant` parameter is usually true, unless calculating a refund. +type OperatorCostFunc func(gasUsed *big.Int, includeConstant 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) @@ -181,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, includeConstant bool, blockTime uint64) *big.Int { + if !config.IsOptimismHolocene(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 !includeConstant { + 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 { @@ -250,6 +282,8 @@ type gasParams struct { feeScalar *big.Float // pre-ecotone l1BaseFeeScalar *uint32 // post-ecotone l1BlobBaseFeeScalar *uint32 // post-ecotone + operatorFeeScalar *uint32 // post-holocene + operatorFeeConstant *uint64 // post-holocene } // intToScaledFloat returns scalar/10e6 as a float @@ -261,12 +295,24 @@ 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) { - p, err := extractL1GasParamsPostEcotone(config.IsHolocene(time), data) + if config.IsHolocene(time) { + p, err := extractL1GasParamsPostHolocene(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 } @@ -312,13 +358,8 @@ func extractL1GasParamsPreEcotone(config *params.ChainConfig, time uint64, data // extractL1GasParamsPostEcotone extracts the gas parameters necessary to compute gas from L1 attribute // info calldata after the Ecotone upgrade, other than the very first Ecotone block. -func extractL1GasParamsPostEcotone(isHolocene bool, data []byte) (gasParams, error) { +func extractL1GasParamsPostEcotone(data []byte) (gasParams, error) { expectedLen := 164 - if isHolocene && (len(data) < 4 || bytes.Equal(data[0:4], HoloceneL1AttributesSelector)) { - // We check that the Holocene selector is present to exclude the very first block after the - // Holocene upgrade, which should still have Ecotone style (len=164) attributes. - expectedLen = 180 - } if len(data) != expectedLen { return gasParams{}, fmt.Errorf("expected %d L1 info bytes, got %d", expectedLen, len(data)) } @@ -333,10 +374,6 @@ func extractL1GasParamsPostEcotone(isHolocene bool, data []byte) (gasParams, err // 68 uint256 _blobBaseFee, // 100 bytes32 _hash, // 132 bytes32 _batcherHash, - // - // added by Holocene: - // 164 uint64 _eip1559Denominator, - // 172 uint64 _eip1559Elasticity, l1BaseFee := new(big.Int).SetBytes(data[36:68]) l1BlobBaseFee := new(big.Int).SetBytes(data[68:100]) l1BaseFeeScalar := binary.BigEndian.Uint32(data[4:8]) @@ -349,6 +386,43 @@ func extractL1GasParamsPostEcotone(isHolocene bool, data []byte) (gasParams, err }, nil } +// extractL1GasParamsPostHolocene extracts the gas parameters necessary to compute gas from L1 attribute +// info calldata after the Holocene upgrade, but not for the very first Holocene block. +func extractL1GasParamsPostHolocene(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 Holocene: + // 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 { @@ -400,6 +474,12 @@ func extractEcotoneFeeParams(l1FeeParams []byte) (l1BaseFeeScalar, l1BlobBaseFee return } +func extractOperatorFeeParams(operatorFeeParams []byte) (operatorFeeScalar, operatorFeeConstant *big.Int) { + operatorFeeScalar = new(big.Int).SetBytes(operatorFeeParams[0:4]) + operatorFeeConstant = new(big.Int).SetBytes(operatorFeeParams[4:12]) + 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 a024d0c871..943d07250d 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 @@ -174,88 +176,57 @@ func TestExtractEcotoneGasParams(t *testing.T) { // make sure wrong amont of data results in error data = append(data, 0x00) // tack on garbage byte - _, err = extractL1GasParamsPostEcotone(false, data) - require.Error(t, err) - - // make sure holocene attributes result in error prior to Holocene activation - data = getHoloceneL1Attributes( - baseFee, - blobBaseFee, - baseFeeScalar, - blobBaseFeeScalar, - ) - gasparams, err = extractL1GasParams(config, zeroTime, data) + _, err = extractL1GasParamsPostEcotone(data) require.Error(t, err) } -func TestExtractHoloceneGasParams(t *testing.T) { +func TestExtractFjordGasParams(t *testing.T) { zeroTime := uint64(0) - // create a config where holocene upgrade is active + // create a config where fjord is active config := ¶ms.ChainConfig{ Optimism: params.OptimismTestConfig.Optimism, RegolithTime: &zeroTime, EcotoneTime: &zeroTime, - HoloceneTime: &zeroTime, + FjordTime: &zeroTime, } - require.True(t, config.IsOptimismEcotone(zeroTime)) - require.True(t, config.IsOptimismHolocene(zeroTime)) - - // make sure empty attributes returns error - data := []byte{} - _, err := extractL1GasParamsPostEcotone(true, data) - require.Error(t, err) + require.True(t, config.IsOptimismFjord(zeroTime)) - // Check that we still allow Ecotone-style L1 attributes post-Holocene, since the very first Holocene block will - // have Ecotone attributes. - data = getEcotoneL1Attributes( + data := getEcotoneL1Attributes( baseFee, blobBaseFee, baseFeeScalar, blobBaseFeeScalar, ) + gasparams, err := extractL1GasParams(config, zeroTime, data) require.NoError(t, err) costFunc := gasparams.costFunc - c, g := costFunc(emptyTx.RollupCostData()) - require.Equal(t, ecotoneGas, g) - require.Equal(t, ecotoneFee, c) - // Now confirm Holocene-style L1 attributes work. - data = getHoloceneL1Attributes( - baseFee, - blobBaseFee, - baseFeeScalar, - blobBaseFeeScalar, - ) - gasparams, err = extractL1GasParams(config, zeroTime, data) - require.NoError(t, err) - costFunc = gasparams.costFunc - c, g = costFunc(emptyTx.RollupCostData()) - require.Equal(t, ecotoneGas, g) - require.Equal(t, ecotoneFee, c) + c, g := costFunc(emptyTx.RollupCostData()) - // make sure wrong amont of data results in error - data = append(data, 0x00) // tack on garbage byte - _, err = extractL1GasParamsPostEcotone(true, data) - require.Error(t, err) + require.Equal(t, minimumFjordGas, g) + require.Equal(t, fjordFee, c) } -func TestExtractFjordGasParams(t *testing.T) { +func TestExtractHoloceneGasParams(t *testing.T) { zeroTime := uint64(0) - // create a config where fjord is active + // create a config where holocene is active config := ¶ms.ChainConfig{ Optimism: params.OptimismTestConfig.Optimism, RegolithTime: &zeroTime, EcotoneTime: &zeroTime, FjordTime: &zeroTime, + HoloceneTime: &zeroTime, } - require.True(t, config.IsOptimismFjord(zeroTime)) + require.True(t, config.IsOptimismHolocene(zeroTime)) - data := getEcotoneL1Attributes( + data := getHoloceneL1Attributes( baseFee, blobBaseFee, baseFeeScalar, blobBaseFeeScalar, + operatorFeeScalar, + operatorFeeConstant, ) gasparams, err := extractL1GasParams(config, zeroTime, data) @@ -266,6 +237,8 @@ func TestExtractFjordGasParams(t *testing.T) { 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 @@ -325,16 +298,32 @@ func getEcotoneL1Attributes(baseFee, blobBaseFee, baseFeeScalar, blobBaseFeeScal return data } -func getHoloceneL1Attributes(baseFee, blobBaseFee, baseFeeScalar, blobBaseFeeScalar *big.Int) []byte { - data := getEcotoneL1Attributes(baseFee, blobBaseFee, baseFeeScalar, blobBaseFeeScalar) - copy(data, HoloceneL1AttributesSelector) - data = append(data, make([]byte, 16)...) // add 0 values for the two new attributes +func getHoloceneL1Attributes(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, HoloceneL1AttributesSelector...) + 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 { @@ -349,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 248dd85925..619de999d0 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -82,6 +82,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/internal/ethapi/api.go b/internal/ethapi/api.go index fa652d0cdd..0c8e29a113 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -1947,6 +1947,13 @@ func marshalReceipt(receipt *types.Receipt, blockHash common.Hash, blockNumber u if receipt.L1BlobBaseFeeScalar != nil { fields["l1BlobBaseFeeScalar"] = hexutil.Uint64(*receipt.L1BlobBaseFeeScalar) } + // Fields added in Holocene + 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 b07ab17dd9..cba1028c30 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -27,6 +27,8 @@ var ( OptimismBaseFeeRecipient = common.HexToAddress("0x4200000000000000000000000000000000000019") // The L1 portion of the transaction fee accumulates at this predeploy OptimismL1FeeRecipient = common.HexToAddress("0x420000000000000000000000000000000000001A") + // 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