From be81fa47deffb246df6c9f06cf01af69a2269492 Mon Sep 17 00:00:00 2001 From: Roberto Bayardo Date: Thu, 21 Dec 2023 09:41:22 -0800 Subject: [PATCH 1/2] Ecotone l1 cost function Move all ecotone switching logic into rollup_cost --- core/types/receipt.go | 2 +- core/types/receipt_test.go | 105 ++++++++++---- core/types/rollup_cost.go | 213 +++++++++++++++++++++++------ core/types/rollup_cost_test.go | 243 ++++++++++++++++++++++++++++----- 4 files changed, 459 insertions(+), 104 deletions(-) diff --git a/core/types/receipt.go b/core/types/receipt.go index a7562e7399..b85df05a3a 100644 --- a/core/types/receipt.go +++ b/core/types/receipt.go @@ -88,7 +88,7 @@ type Receipt struct { L1GasPrice *big.Int `json:"l1GasPrice,omitempty"` L1GasUsed *big.Int `json:"l1GasUsed,omitempty"` L1Fee *big.Int `json:"l1Fee,omitempty"` - FeeScalar *big.Float `json:"l1FeeScalar,omitempty"` + FeeScalar *big.Float `json:"l1FeeScalar,omitempty"` // always nil after Ecotone hardfork } type receiptMarshaling struct { diff --git a/core/types/receipt_test.go b/core/types/receipt_test.go index 766640129c..f6a3598ca9 100644 --- a/core/types/receipt_test.go +++ b/core/types/receipt_test.go @@ -34,6 +34,13 @@ import ( ) var ( + ecotoneTestConfig = func() *params.ChainConfig { + conf := *params.OptimismTestConfig // copy the config + time := uint64(0) + conf.EcotoneTime = &time + return &conf + }() + legacyReceipt = &Receipt{ Status: ReceiptStatusFailed, CumulativeGasUsed: 1, @@ -683,30 +690,20 @@ func clearComputedFieldsOnLogs(logs []*Log) []*Log { return l } -func TestDeriveOptimismTxReceipt(t *testing.T) { - to4 := common.HexToAddress("0x4") +func getOptimismTxReceipts( + t *testing.T, l1AttributesPayload []byte, + l1GasPrice, l1GasUsed *big.Int, feeScalar *big.Float, l1Fee *big.Int) ([]*Transaction, []*Receipt) { + //to4 := common.HexToAddress("0x4") // Create a few transactions to have receipts for txs := Transactions{ NewTx(&DepositTx{ To: nil, // contract creation Value: big.NewInt(6), Gas: 50, - // System config with L1Scalar=2_000_000 (becomes 2 after division), L1Overhead=2500, L1BaseFee=5000 - Data: common.Hex2Bytes("015d8eb900000000000000000000000000000000000000000000000026b39534042076f70000000000000000000000000000000000000000000000007e33b7c4995967580000000000000000000000000000000000000000000000000000000000001388547dea8ff339566349ed0ef6384876655d1b9b955e36ac165c6b8ab69b9af5cd0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000123400000000000000000000000000000000000000000000000000000000000009c400000000000000000000000000000000000000000000000000000000001e8480"), - }), - NewTx(&DynamicFeeTx{ - To: &to4, - Nonce: 4, - Value: big.NewInt(4), - Gas: 4, - GasTipCap: big.NewInt(44), - GasFeeCap: big.NewInt(1045), - Data: []byte{0, 1, 255, 0}, + Data: l1AttributesPayload, }), + emptyTx, } - depNonce := uint64(7) - blockNumber := big.NewInt(1) - blockHash := common.BytesToHash([]byte{0x03, 0x14}) // Create the corresponding receipts receipts := Receipts{ @@ -741,35 +738,83 @@ func TestDeriveOptimismTxReceipt(t *testing.T) { BlockHash: blockHash, BlockNumber: blockNumber, TransactionIndex: 0, - DepositNonce: &depNonce, + DepositNonce: &depNonce1, }, &Receipt{ - Type: DynamicFeeTxType, + Type: LegacyTxType, + EffectiveGasPrice: big.NewInt(0), PostState: common.Hash{4}.Bytes(), CumulativeGasUsed: 10, Logs: []*Log{}, // derived fields: - TxHash: txs[1].Hash(), - GasUsed: 18446744073709551561, - EffectiveGasPrice: big.NewInt(1044), - BlockHash: blockHash, - BlockNumber: blockNumber, - TransactionIndex: 1, - L1GasPrice: big.NewInt(5000), - L1GasUsed: big.NewInt(3976), - L1Fee: big.NewInt(39760000), - FeeScalar: big.NewFloat(2), + TxHash: txs[1].Hash(), + GasUsed: 18446744073709551561, + BlockHash: blockHash, + BlockNumber: blockNumber, + TransactionIndex: 1, + L1GasPrice: l1GasPrice, + L1GasUsed: l1GasUsed, + L1Fee: l1Fee, + FeeScalar: feeScalar, }, } + return txs, receipts +} + +func TestDeriveOptimismBedrockTxReceipts(t *testing.T) { + // Bedrock style l1 attributes with L1Scalar=7_000_000 (becomes 7 after division), L1Overhead=50, L1BaseFee=1000*1e6 + payload := common.Hex2Bytes("015d8eb900000000000000000000000000000000000000000000000000000000000004d200000000000000000000000000000000000000000000000000000000000004d2000000000000000000000000000000000000000000000000000000003b9aca0000000000000000000000000000000000000000000000000000000000000004d200000000000000000000000000000000000000000000000000000000000004d200000000000000000000000000000000000000000000000000000000000004d2000000000000000000000000000000000000000000000000000000000000003200000000000000000000000000000000000000000000000000000000006acfc0015d8eb900000000000000000000000000000000000000000000000000000000000004d200000000000000000000000000000000000000000000000000000000000004d2000000000000000000000000000000000000000000000000000000003b9aca0000000000000000000000000000000000000000000000000000000000000004d200000000000000000000000000000000000000000000000000000000000004d200000000000000000000000000000000000000000000000000000000000004d2000000000000000000000000000000000000000000000000000000000000003200000000000000000000000000000000000000000000000000000000006acfc0") + // the parameters we use below are defined in rollup_test.go + l1GasPrice := basefee + l1GasUsed := bedrockGas + feeScalar := big.NewFloat(float64(scalar.Uint64() / 1e6)) + l1Fee := bedrockFee + txs, receipts := getOptimismTxReceipts(t, payload, l1GasPrice, l1GasUsed, feeScalar, l1Fee) + + // Re-derive receipts. + basefee := big.NewInt(1000) + derivedReceipts := clearComputedFieldsOnReceipts(receipts) + err := Receipts(derivedReceipts).DeriveFields(params.OptimismTestConfig, blockHash, blockNumber.Uint64(), 0, basefee, nil, txs) + if err != nil { + t.Fatalf("DeriveFields(...) = %v, want ", err) + } + checkBedrockReceipts(t, receipts, derivedReceipts) + + // Should get same result with the Ecotone config because it will assume this is "first ecotone block" + // if it sees the bedrock style L1 attributes. + err = Receipts(derivedReceipts).DeriveFields(ecotoneTestConfig, blockHash, blockNumber.Uint64(), 0, basefee, nil, txs) + if err != nil { + t.Fatalf("DeriveFields(...) = %v, want ", err) + } + checkBedrockReceipts(t, receipts, derivedReceipts) +} + +func TestDeriveOptimismEcotoneTxReceipts(t *testing.T) { + // Ecotone style l1 attributes with basefeeScalar=2, blobBasfeeScalar=3, baseFee=1000*1e6, blobBasefee=10*1e6 + payload := common.Hex2Bytes("440a5e20000000020000000300000000000004d200000000000004d200000000000004d2000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000000000000000000000000000000000000098968000000000000000000000000000000000000000000000000000000000000004d200000000000000000000000000000000000000000000000000000000000004d2") + // the parameters we use below are defined in rollup_test.go + l1GasPrice := basefee + l1GasUsed := ecotoneGas + l1Fee := ecotoneFee + txs, receipts := getOptimismTxReceipts(t, payload, l1GasPrice, l1GasUsed, nil /*feeScalar*/, l1Fee) // Re-derive receipts. basefee := big.NewInt(1000) derivedReceipts := clearComputedFieldsOnReceipts(receipts) + // Should error out if we try to process this with a pre-Ecotone config err := Receipts(derivedReceipts).DeriveFields(params.OptimismTestConfig, blockHash, blockNumber.Uint64(), 0, basefee, nil, txs) + if err == nil { + t.Fatalf("expected error from deriving ecotone receipts with pre-ecotone config, got none") + } + + err = Receipts(derivedReceipts).DeriveFields(ecotoneTestConfig, 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, "", " ") if err != nil { @@ -783,6 +828,10 @@ func TestDeriveOptimismTxReceipt(t *testing.T) { if d != "" { t.Fatal("receipts differ:", d) } +} + +func checkBedrockReceipts(t *testing.T, receipts, derivedReceipts []*Receipt) { + diffReceipts(t, receipts, derivedReceipts) // Check that we preserved the invariant: l1Fee = l1GasPrice * l1GasUsed * l1FeeScalar // but with more difficult int math... diff --git a/core/types/rollup_cost.go b/core/types/rollup_cost.go index fcac3c1c6f..b1d4ccd0a6 100644 --- a/core/types/rollup_cost.go +++ b/core/types/rollup_cost.go @@ -17,6 +17,7 @@ package types import ( + "bytes" "fmt" "math/big" @@ -25,44 +26,87 @@ import ( "github.com/ethereum/go-ethereum/params" ) -type RollupCostData struct { - zeroes, ones uint64 -} +const ( + // The two 4-byte Ecotone fee scalar values are packed into the same storage slot as the 8-byte + // sequence number and have the following Solidity offsets within the slot. Note that Solidity + // offsets correspond to the last byte of the value in the slot, counting backwards from the + // end of the slot. For example, The 8-byte sequence number has offset 0, and is therefore + // stored as big-endian format in bytes [24:32] of the slot. + BasefeeScalarSlotOffset = 12 // bytes [16:20] of the slot + BlobBasefeeScalarSlotOffset = 8 // bytes [20:24] of the slot -func NewRollupCostData(data []byte) (out RollupCostData) { - for _, b := range data { - if b == 0 { - out.zeroes++ - } else { - out.ones++ - } + // scalarSectionStart 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. + scalarSectionStart = 32 - BasefeeScalarSlotOffset - 4 +) + +func init() { + if BlobBasefeeScalarSlotOffset != BasefeeScalarSlotOffset-4 { + panic("this code assumes the scalars are at adjacent positions in the scalars slot") } - return out +} + +var ( + // BedrockL1AttributesSelector is the function selector indicating Bedrock style L1 gas + // attributes. + BedrockL1AttributesSelector = []byte{0x01, 0x5d, 0x8e, 0xb9} + // EcotoneL1AttributesSelector is the selector indicating Ecotone style L1 gas attributes. + EcotoneL1AttributesSelector = []byte{0x44, 0x0a, 0x5e, 0x20} + + // L1BlockAddr is the address of the L1Block contract which stores the L1 gas attributes. + L1BlockAddr = common.HexToAddress("0x4200000000000000000000000000000000000015") + + L1BasefeeSlot = common.BigToHash(big.NewInt(1)) + OverheadSlot = common.BigToHash(big.NewInt(5)) + ScalarSlot = common.BigToHash(big.NewInt(6)) + + // L2BlobBasefeeSlot was added with the Ecotone upgrade and stores the blobBasefee L1 gas + // attribute. + L1BlobBasefeeSlot = common.BigToHash(big.NewInt(7)) + // L1FeeScalarsSlot as of the Ecotone upgrade stores the 32-bit basefeeScalar and + // blobBasefeeScalar L1 gas attributes at offsets `BasefeeScalarSlotOffset` and + // `BlobBasefeeScalarSlotOffset` respectively. + L1FeeScalarsSlot = common.BigToHash(big.NewInt(3)) + + oneMillion = big.NewInt(1_000_000) + ecotoneDivisor = big.NewInt(1_000_000 * 16) + sixteen = big.NewInt(16) + + emptyScalars = make([]byte, 8) +) + +// RollupCostData is a transaction structure that caches data for quickly computing the data +// availablility costs for the transaction. +type RollupCostData struct { + zeroes, ones uint64 } 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. +// L1CostFunc is used in the state transition to determine the data availability fee charged to the +// sender of non-Deposit transactions. It returns nil if no data availability 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") +func NewRollupCostData(data []byte) (out RollupCostData) { + for _, b := range data { + if b == 0 { + out.zeroes++ + } else { + out.ones++ + } + } + return out +} -// NewL1CostFunc returns a function used for calculating L1 fee cost, or nil if this is not an -// op-stack chain. +// NewL1CostFunc returns a function used for calculating data availability fees, or nil if this is +// not an op-stack chain. func NewL1CostFunc(config *params.ChainConfig, statedb StateGetter) L1CostFunc { if config.Optimism == nil { return nil @@ -74,31 +118,56 @@ func NewL1CostFunc(config *params.ChainConfig, statedb StateGetter) L1CostFunc { 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 + // Note: the various state variables below are not initialized from the DB until this + // point to allow deposit transactions from the block to be processed first by state + // transition. This behavior is consensus critical! + if !config.IsOptimismEcotone(blockTime) { + cachedFunc = newL1CostFuncBedrock(config, statedb, blockTime) + } else { + l1BlobBasefee := statedb.GetState(L1BlockAddr, L1BlobBasefeeSlot).Big() + l1FeeScalars := statedb.GetState(L1BlockAddr, L1FeeScalarsSlot).Bytes() + + // Edge case: the very first Ecotone block requires we use the Bedrock cost + // function. We detect this scenario by checking if the Ecotone parameters are + // unset. Not here we rely on assumption that the scalar parameters are adjacent + // in the buffer and basefeeScalar comes first. + if l1BlobBasefee.BitLen() == 0 && + bytes.Equal(emptyScalars, l1FeeScalars[scalarSectionStart:scalarSectionStart+8]) { + log.Info("using bedrock l1 cost func for first Ecotone block", "time", blockTime) + cachedFunc = newL1CostFuncBedrock(config, statedb, blockTime) + } else { + l1Basefee := statedb.GetState(L1BlockAddr, L1BasefeeSlot).Big() + offset := scalarSectionStart + l1BasefeeScalar := new(big.Int).SetBytes(l1FeeScalars[offset : offset+4]) + l1BlobBasefeeScalar := new(big.Int).SetBytes(l1FeeScalars[offset+4 : offset+8]) + cachedFunc = newL1CostFuncEcotone(l1Basefee, l1BlobBasefee, l1BasefeeScalar, l1BlobBasefeeScalar) + } + } } fee, _ := cachedFunc(rollupCostData) return fee } } -var ( - oneMillion = big.NewInt(1_000_000) -) +// 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 { + l1Basefee := statedb.GetState(L1BlockAddr, L1BasefeeSlot).Big() + overhead := statedb.GetState(L1BlockAddr, OverheadSlot).Big() + scalar := statedb.GetState(L1BlockAddr, ScalarSlot).Big() + isRegolith := config.IsRegolith(blockTime) + return newL1CostFuncBedrockHelper(l1Basefee, overhead, scalar, isRegolith) +} -func newL1CostFunc(l1Basefee, overhead, scalar *big.Int, isRegolith bool) l1CostFunc { +// newL1CostFuncBedrockHelper is lower level version of newL1CostFuncBedrock that expects already +// extracted parameters +func newL1CostFuncBedrockHelper(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) @@ -116,9 +185,50 @@ func newL1CostFunc(l1Basefee, overhead, scalar *big.Int, isRegolith bool) l1Cost } } +// newL1CostFuncEcotone returns an l1 cost function suitable for the Ecotone upgrade except for the +// very first block of the upgrade. +func newL1CostFuncEcotone(l1Basefee, l1BlobBasefee, l1BasefeeScalar, l1BlobBasefeeScalar *big.Int) l1CostFunc { + return func(costData RollupCostData) (fee, calldataGasUsed *big.Int) { + calldataGas := (costData.zeroes * params.TxDataZeroGas) + (costData.ones * params.TxDataNonZeroGasEIP2028) + calldataGasUsed = new(big.Int).SetUint64(calldataGas) + + // Ecotone L1 cost function: + // + // (gas/16)*(l1Basefee*16*l1BasefeeScalar + l1BlobBasefee*l1BlobBasefeeScalar)/1e6 + // + // We divide "gas" by 16 to change from units of calldata gas to "estimated # of bytes when + // compressed". + // + // Function is actually computed as follows for better precision under integer arithmetic: + // + // gas*(l1Basefee*16*l1BasefeeScalar + l1BlobBasefee*l1BlobBasefeeScalar)/16e6 + + calldataCostPerByte := new(big.Int).Set(l1Basefee) + calldataCostPerByte = calldataCostPerByte.Mul(calldataCostPerByte, sixteen) + calldataCostPerByte = calldataCostPerByte.Mul(calldataCostPerByte, l1BasefeeScalar) + + blobCostPerByte := new(big.Int).Set(l1BlobBasefee) + blobCostPerByte = blobCostPerByte.Mul(blobCostPerByte, l1BlobBasefeeScalar) + + fee = new(big.Int).Add(calldataCostPerByte, blobCostPerByte) + fee = fee.Mul(fee, calldataGasUsed) + fee = fee.Div(fee, ecotoneDivisor) + + return fee, calldataGasUsed + } +} + // 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) { + if config.IsEcotone(time) { + // 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 len(data) >= 4 && !bytes.Equal(data[0:4], BedrockL1AttributesSelector) { + l1Basefee, costFunc, err = extractL1GasParamsEcotone(data) + return + } + } + // 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)) @@ -130,11 +240,38 @@ func extractL1GasParams(config *params.ChainConfig, time uint64, data []byte) (l 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)) + costFunc = newL1CostFuncBedrockHelper(l1Basefee, overhead, scalar, config.IsRegolith(time)) + return +} + +// extractEcotoneL1GasParams extracts the gas parameters necessary to compute gas from L1 attribute +// info calldata after the Ecotone upgrade, but not for the very first Ecotone block. +func extractL1GasParamsEcotone(data []byte) (l1Basefee *big.Int, costFunc l1CostFunc, err error) { + if len(data) != 164 { + return nil, nil, fmt.Errorf("expected 164 L1 info bytes, got %d", len(data)) + } + // data layout assumed for Ecotone: + // 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, + l1Basefee = new(big.Int).SetBytes(data[36:68]) + l1BlobBasefee := new(big.Int).SetBytes(data[68:100]) + l1BasefeeScalar := new(big.Int).SetBytes(data[4:8]) + l1BlobBasefeeScalar := new(big.Int).SetBytes(data[8:12]) + costFunc = newL1CostFuncEcotone(l1Basefee, l1BlobBasefee, l1BasefeeScalar, l1BlobBasefeeScalar) return } -// L1Cost computes the the L1 data fee. It is used by e2e tests so must remain exported. +// 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 { l1GasUsed := new(big.Int).SetUint64(rollupDataGas) l1GasUsed.Add(l1GasUsed, overhead) diff --git a/core/types/rollup_cost_test.go b/core/types/rollup_cost_test.go index 57640d3015..a90a2d07cd 100644 --- a/core/types/rollup_cost_test.go +++ b/core/types/rollup_cost_test.go @@ -1,47 +1,139 @@ package types import ( + "encoding/binary" "math/big" "testing" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/params" "github.com/stretchr/testify/require" ) -func TestL1CostFunc(t *testing.T) { - basefee := big.NewInt(1) - overhead := big.NewInt(1) - scalar := big.NewInt(1_000_000) +var ( + basefee = big.NewInt(1000 * 1e6) + overhead = big.NewInt(50) + scalar = big.NewInt(7 * 1e6) - costFunc0 := newL1CostFunc(basefee, overhead, scalar, false /*isRegolith*/) - costFunc1 := newL1CostFunc(basefee, overhead, scalar, true) + blobBasefee = big.NewInt(10 * 1e6) + basefeeScalar = big.NewInt(2) + blobBasefeeScalar = big.NewInt(3) + + // below are the expected cost func outcomes for the above parameter settings on the emptyTx + // which is defined in transaction_test.go + bedrockFee = big.NewInt(11326000000000) + regolithFee = big.NewInt(3710000000000) + ecotoneFee = big.NewInt(960900) // (480/16)*(2*16*1000 + 3*10) == 960900 + + bedrockGas = big.NewInt(1618) + regolithGas = big.NewInt(530) // 530 = 1618 - (16*68) + ecotoneGas = big.NewInt(480) +) + +func TestBedrockL1CostFunc(t *testing.T) { + costFunc0 := newL1CostFuncBedrockHelper(basefee, overhead, scalar, false /*isRegolith*/) + costFunc1 := newL1CostFuncBedrockHelper(basefee, overhead, scalar, true) - // emptyTx is a test tx defined in transaction_test.go c0, g0 := costFunc0(emptyTx.RollupCostData()) // pre-Regolith c1, g1 := costFunc1(emptyTx.RollupCostData()) - require.Equal(t, big.NewInt(1569), c0) - require.Equal(t, big.NewInt(1569), g0) // gas-used == fee since scalars are all 1 - require.Equal(t, big.NewInt(481), c1) - require.Equal(t, big.NewInt(481), g1) + + require.Equal(t, bedrockFee, c0) + require.Equal(t, bedrockGas, g0) // gas-used + + require.Equal(t, regolithFee, c1) + require.Equal(t, regolithGas, g1) +} + +func TestEcotoneL1CostFunc(t *testing.T) { + costFunc := newL1CostFuncEcotone(basefee, blobBasefee, basefeeScalar, blobBasefeeScalar) + c, g := costFunc(emptyTx.RollupCostData()) + require.Equal(t, ecotoneGas, g) + require.Equal(t, ecotoneFee, c) } -func TestExtractGasParams(t *testing.T) { +func TestExtractBedrockGasParams(t *testing.T) { regolithTime := uint64(1) config := ¶ms.ChainConfig{ Optimism: params.OptimismTestConfig.Optimism, RegolithTime: ®olithTime, } - selector := []byte{0x01, 0x5d, 0x8e, 0xb9} - uint256 := make([]byte, 32) + data := getBedrockL1Attributes(basefee, overhead, scalar) - ignored := big.NewInt(1234) - basefee := big.NewInt(1) - overhead := big.NewInt(1) - scalar := big.NewInt(1_000_000) + _, costFuncPreRegolith, _, err := extractL1GasParams(config, regolithTime-1, data) + require.NoError(t, err) + + // Function should continue to succeed even with extra data (that just gets ignored) since we + // have been testing the data size is at least the expected number of bytes instead of exactly + // the expected number of bytes. It's unclear if this flexibility was intentional, but since + // it's been in production we shouldn't change this behavior. + data = append(data, []byte{0xBE, 0xEE, 0xEE, 0xFF}...) // tack on garbage data + _, costFuncRegolith, _, err := extractL1GasParams(config, regolithTime, data) + require.NoError(t, err) + + c, _ := costFuncPreRegolith(emptyTx.RollupCostData()) + require.Equal(t, bedrockFee, c) + + c, _ = costFuncRegolith(emptyTx.RollupCostData()) + require.Equal(t, regolithFee, c) + + // try to extract from data which has not enough params, should get error. + data = data[:len(data)-4-32] + _, _, _, err = extractL1GasParams(config, regolithTime, data) + require.Error(t, err) +} + +func TestExtractEcotoneGasParams(t *testing.T) { + zeroTime := uint64(0) + // create a config where ecotone upgrade is active + config := ¶ms.ChainConfig{ + Optimism: params.OptimismTestConfig.Optimism, + RegolithTime: &zeroTime, + EcotoneTime: &zeroTime, + } + require.True(t, config.IsOptimismEcotone(0)) + + data := getEcotoneL1Attributes(basefee, blobBasefee, basefeeScalar, blobBasefeeScalar) + + _, costFunc, _, err := extractL1GasParams(config, 0, data) + require.NoError(t, err) + + c, g := costFunc(emptyTx.RollupCostData()) + + require.Equal(t, ecotoneGas, g) + require.Equal(t, ecotoneFee, c) + + // make sure wrong amont of data results in error + data = append(data, 0x00) // tack on garbage byte + _, _, err = extractL1GasParamsEcotone(data) + require.Error(t, err) +} + +// make sure the first block of the ecotone upgrade is properly detected, and invokes the bedrock +// cost function appropriately +func TestFirstBlockEcotoneGasParams(t *testing.T) { + zeroTime := uint64(0) + // create a config where ecotone upgrade is active + config := ¶ms.ChainConfig{ + Optimism: params.OptimismTestConfig.Optimism, + RegolithTime: &zeroTime, + EcotoneTime: &zeroTime, + } + require.True(t, config.IsOptimismEcotone(0)) + data := getBedrockL1Attributes(basefee, overhead, scalar) + + _, oldCostFunc, _, err := extractL1GasParams(config, 0, data) + require.NoError(t, err) + c, _ := oldCostFunc(emptyTx.RollupCostData()) + require.Equal(t, regolithFee, c) +} + +func getBedrockL1Attributes(basefee, overhead, scalar *big.Int) []byte { + uint256 := make([]byte, 32) + ignored := big.NewInt(1234) data := []byte{} - data = append(data, selector...) // selector + data = append(data, BedrockL1AttributesSelector...) data = append(data, ignored.FillBytes(uint256)...) // arg 0 data = append(data, ignored.FillBytes(uint256)...) // arg 1 data = append(data, basefee.FillBytes(uint256)...) // arg 2 @@ -49,28 +141,105 @@ func TestExtractGasParams(t *testing.T) { data = append(data, ignored.FillBytes(uint256)...) // arg 4 data = append(data, ignored.FillBytes(uint256)...) // arg 5 data = append(data, overhead.FillBytes(uint256)...) // arg 6 + data = append(data, scalar.FillBytes(uint256)...) // arg 7 + return data +} - // try to extract from data which has not enough params, should get error. - _, _, _, err := extractL1GasParams(config, regolithTime, data) - require.Error(t, err) +func getEcotoneL1Attributes(basefee, blobBasefee, basefeeScalar, blobBasefeeScalar *big.Int) []byte { + ignored := big.NewInt(1234) + data := []byte{} + uint256 := make([]byte, 32) + uint64 := make([]byte, 8) + uint32 := make([]byte, 4) + data = append(data, EcotoneL1AttributesSelector...) + data = append(data, basefeeScalar.FillBytes(uint32)...) + data = append(data, blobBasefeeScalar.FillBytes(uint32)...) + data = append(data, ignored.FillBytes(uint64)...) + data = append(data, ignored.FillBytes(uint64)...) + data = append(data, ignored.FillBytes(uint64)...) + data = append(data, basefee.FillBytes(uint256)...) + data = append(data, blobBasefee.FillBytes(uint256)...) + data = append(data, ignored.FillBytes(uint256)...) + data = append(data, ignored.FillBytes(uint256)...) + return data +} + +type testStateGetter struct { + basefee, blobBasefee, overhead, scalar *big.Int + basefeeScalar, blobBasefeeScalar uint32 +} - data = append(data, scalar.FillBytes(uint256)...) // arg 7 +func (sg *testStateGetter) GetState(addr common.Address, slot common.Hash) common.Hash { + buf := common.Hash{} + switch slot { + case L1BasefeeSlot: + sg.basefee.FillBytes(buf[:]) + case OverheadSlot: + sg.overhead.FillBytes(buf[:]) + case ScalarSlot: + sg.scalar.FillBytes(buf[:]) + case L1BlobBasefeeSlot: + sg.blobBasefee.FillBytes(buf[:]) + case L1FeeScalarsSlot: + offset := scalarSectionStart + binary.BigEndian.PutUint32(buf[offset:offset+4], sg.basefeeScalar) + binary.BigEndian.PutUint32(buf[offset+4:offset+8], sg.blobBasefeeScalar) + default: + panic("unknown slot") + } + return buf +} - // now it should succeed - _, costFuncPreRegolith, _, err := extractL1GasParams(config, regolithTime-1, data) - require.NoError(t, err) +// TestNewL1CostFunc tests that the appropriate cost function is selected based on the +// configuration and statedb values. +func TestNewL1CostFunc(t *testing.T) { + time := uint64(1) + config := ¶ms.ChainConfig{ + Optimism: params.OptimismTestConfig.Optimism, + } + statedb := &testStateGetter{ + basefee: basefee, + overhead: overhead, + scalar: scalar, + blobBasefee: blobBasefee, + basefeeScalar: uint32(basefeeScalar.Uint64()), + blobBasefeeScalar: uint32(blobBasefeeScalar.Uint64()), + } - // Function should continue to succeed even with extra data (that just gets ignored) since we - // have been testing the data size is at least the expected number of bytes instead of exactly - // the expected number of bytes. It's unclear if this flexibility was intentional, but since - // it's been in production we shouldn't change this behavior. - data = append(data, ignored.FillBytes(uint256)...) // extra ignored arg - _, costFuncRegolith, _, err := extractL1GasParams(config, regolithTime, data) - require.NoError(t, err) + costFunc := NewL1CostFunc(config, statedb) + require.NotNil(t, costFunc) - c, _ := costFuncPreRegolith(emptyTx.RollupCostData()) - require.Equal(t, big.NewInt(1569), c) + // empty cost data should result in nil fee + fee := costFunc(RollupCostData{}, time) + require.Nil(t, fee) - c, _ = costFuncRegolith(emptyTx.RollupCostData()) - require.Equal(t, big.NewInt(481), c) + // emptyTx fee w/ bedrock config should be the bedrock fee + fee = costFunc(emptyTx.RollupCostData(), time) + require.NotNil(t, fee) + require.Equal(t, bedrockFee, fee) + + // emptyTx fee w/ regolith config should be the regolith fee + config.RegolithTime = &time + costFunc = NewL1CostFunc(config, statedb) + require.NotNil(t, costFunc) + fee = costFunc(emptyTx.RollupCostData(), time) + require.NotNil(t, fee) + require.Equal(t, regolithFee, fee) + + // emptyTx fee w/ ecotone config should be the ecotone fee + config.EcotoneTime = &time + costFunc = NewL1CostFunc(config, statedb) + fee = costFunc(emptyTx.RollupCostData(), time) + require.NotNil(t, fee) + require.Equal(t, ecotoneFee, fee) + + // emptyTx fee w/ ecotone config, but simulate first ecotone block by blowing away the ecotone + // params. Should result in regolith fee. + statedb.basefeeScalar = 0 + statedb.blobBasefeeScalar = 0 + statedb.blobBasefee = new(big.Int) + costFunc = NewL1CostFunc(config, statedb) + fee = costFunc(emptyTx.RollupCostData(), time) + require.NotNil(t, fee) + require.Equal(t, regolithFee, fee) } From c887e59de526b612a616ac56f5843da30d70ecb1 Mon Sep 17 00:00:00 2001 From: Roberto Bayardo Date: Tue, 9 Jan 2024 09:26:03 -0800 Subject: [PATCH 2/2] Update core/types/rollup_cost.go Co-authored-by: protolambda --- core/types/rollup_cost.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/types/rollup_cost.go b/core/types/rollup_cost.go index b1d4ccd0a6..9718560b76 100644 --- a/core/types/rollup_cost.go +++ b/core/types/rollup_cost.go @@ -194,14 +194,14 @@ func newL1CostFuncEcotone(l1Basefee, l1BlobBasefee, l1BasefeeScalar, l1BlobBasef // Ecotone L1 cost function: // - // (gas/16)*(l1Basefee*16*l1BasefeeScalar + l1BlobBasefee*l1BlobBasefeeScalar)/1e6 + // (calldataGas/16)*(l1Basefee*16*l1BasefeeScalar + l1BlobBasefee*l1BlobBasefeeScalar)/1e6 // - // We divide "gas" by 16 to change from units of calldata gas to "estimated # of bytes when - // compressed". + // We divide "calldataGas" by 16 to change from units of calldata gas to "estimated # of bytes when + // compressed". Known as "compressedTxSize" in the spec. // // Function is actually computed as follows for better precision under integer arithmetic: // - // gas*(l1Basefee*16*l1BasefeeScalar + l1BlobBasefee*l1BlobBasefeeScalar)/16e6 + // calldataGas*(l1Basefee*16*l1BasefeeScalar + l1BlobBasefee*l1BlobBasefeeScalar)/16e6 calldataCostPerByte := new(big.Int).Set(l1Basefee) calldataCostPerByte = calldataCostPerByte.Mul(calldataCostPerByte, sixteen)