Skip to content

Commit

Permalink
Merge pull request #278 from ethereum-optimism/jg/ecotone_json_receip…
Browse files Browse the repository at this point in the history
…t_data

Add Ecotone Fee Information to JSON Receipts
  • Loading branch information
sebastianst authored Apr 12, 2024
2 parents 184b7b3 + 900154b commit da6ea72
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 49 deletions.
18 changes: 18 additions & 0 deletions core/types/gen_receipt_json.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 15 additions & 9 deletions core/types/receipt.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,14 @@ type Receipt struct {
BlockNumber *big.Int `json:"blockNumber,omitempty"`
TransactionIndex uint `json:"transactionIndex"`

// OVM legacy: extend receipts with their L1 price (if a rollup tx)
L1GasPrice *big.Int `json:"l1GasPrice,omitempty"`
L1GasUsed *big.Int `json:"l1GasUsed,omitempty"`
L1Fee *big.Int `json:"l1Fee,omitempty"`
FeeScalar *big.Float `json:"l1FeeScalar,omitempty"` // always nil after Ecotone hardfork
// Optimism: extend receipts with L1 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
L1Fee *big.Int `json:"l1Fee,omitempty"` // Present from pre-bedrock
FeeScalar *big.Float `json:"l1FeeScalar,omitempty"` // Present from pre-bedrock to Ecotone. Nil after Ecotone
L1BaseFeeScalar *uint32 `json:"l1BaseFeeScalar,omitempty"` // Always nil prior to the Ecotone hardfork
L1BlobBaseFeeScalar *uint32 `json:"l1BlobBaseFeeScalar,omitempty"` // Always nil prior to the Ecotone hardfork
}

type receiptMarshaling struct {
Expand Down Expand Up @@ -570,17 +573,20 @@ func (rs Receipts) DeriveFields(config *params.ChainConfig, hash common.Hash, nu
}
}
if config.Optimism != nil && len(txs) >= 2 && config.IsBedrock(new(big.Int).SetUint64(number)) { // need at least an info tx and a non-info tx
l1BaseFee, costFunc, feeScalar, err := extractL1GasParams(config, time, txs[0].Data())
gasParams, err := extractL1GasParams(config, time, txs[0].Data())
if err != nil {
return err
}
for i := 0; i < len(rs); i++ {
if txs[i].IsDepositTx() {
continue
}
rs[i].L1GasPrice = l1BaseFee
rs[i].L1Fee, rs[i].L1GasUsed = costFunc(txs[i].RollupCostData())
rs[i].FeeScalar = feeScalar
rs[i].L1GasPrice = gasParams.l1BaseFee
rs[i].L1BlobBaseFee = gasParams.l1BlobBaseFee
rs[i].L1Fee, rs[i].L1GasUsed = gasParams.costFunc(txs[i].RollupCostData())
rs[i].FeeScalar = gasParams.feeScalar
rs[i].L1BaseFeeScalar = gasParams.l1BaseFeeScalar
rs[i].L1BlobBaseFeeScalar = gasParams.l1BlobBaseFeeScalar
}
}
return nil
Expand Down
84 changes: 75 additions & 9 deletions core/types/receipt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -698,10 +698,77 @@ func clearComputedFieldsOnLogs(logs []*Log) []*Log {
return l
}

func getOptimismTxReceipts(
t *testing.T, l1AttributesPayload []byte,
l1GasPrice, l1GasUsed *big.Int, feeScalar *big.Float, l1Fee *big.Int) ([]*Transaction, []*Receipt) {
//to4 := common.HexToAddress("0x4")
func getOptimismEcotoneTxReceipts(l1AttributesPayload []byte, l1GasPrice, l1BlobGasPrice, l1GasUsed, l1Fee *big.Int, baseFeeScalar, blobBaseFeeScalar *uint32) ([]*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,
},
}
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{
NewTx(&DepositTx{
Expand Down Expand Up @@ -777,7 +844,7 @@ func TestDeriveOptimismBedrockTxReceipts(t *testing.T) {
l1GasUsed := bedrockGas
feeScalar := big.NewFloat(float64(scalar.Uint64() / 1e6))
l1Fee := bedrockFee
txs, receipts := getOptimismTxReceipts(t, payload, l1GasPrice, l1GasUsed, feeScalar, l1Fee)
txs, receipts := getOptimismTxReceipts(payload, l1GasPrice, l1GasUsed, l1Fee, feeScalar)

// Re-derive receipts.
baseFee := big.NewInt(1000)
Expand All @@ -801,10 +868,9 @@ func TestDeriveOptimismEcotoneTxReceipts(t *testing.T) {
// Ecotone style l1 attributes with baseFeeScalar=2, blobBaseFeeScalar=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)
baseFeeScalarUint32 := uint32(baseFeeScalar.Uint64())
blobBaseFeeScalarUint32 := uint32(blobBaseFeeScalar.Uint64())
txs, receipts := getOptimismEcotoneTxReceipts(payload, baseFee, blobBaseFee, ecotoneGas, ecotoneFee, &baseFeeScalarUint32, &blobBaseFeeScalarUint32)

// Re-derive receipts.
baseFee := big.NewInt(1000)
Expand Down
78 changes: 53 additions & 25 deletions core/types/rollup_cost.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package types

import (
"bytes"
"encoding/binary"
"fmt"
"math/big"

Expand Down Expand Up @@ -218,37 +219,56 @@ func newL1CostFuncEcotone(l1BaseFee, l1BlobBaseFee, l1BaseFeeScalar, l1BlobBaseF
}
}

type gasParams struct {
l1BaseFee *big.Int
l1BlobBaseFee *big.Int
costFunc l1CostFunc
feeScalar *big.Float // pre-ecotone
l1BaseFeeScalar *uint32 // post-ecotone
l1BlobBaseFeeScalar *uint32 // post-ecotone
}

// intToScaledFloat returns scalar/10e6 as a float
func intToScaledFloat(scalar *big.Int) *big.Float {
fscalar := new(big.Float).SetInt(scalar)
fdivisor := new(big.Float).SetUint64(1_000_000) // 10**6, i.e. 6 decimals
return new(big.Float).Quo(fscalar, fdivisor)
}

// extractL1GasParams extracts the gas parameters necessary to compute gas costs from L1 block info
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
}
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
if config.IsEcotone(time) && len(data) >= 4 && !bytes.Equal(data[0:4], BedrockL1AttributesSelector) {
return extractL1GasParamsEcotone(data)
}
return extractL1GasParamsPreEcotone(config, time, data)
}

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

// 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) {
func extractL1GasParamsEcotone(data []byte) (gasParams, error) {
if len(data) != 164 {
return nil, nil, fmt.Errorf("expected 164 L1 info bytes, got %d", len(data))
return gasParams{}, fmt.Errorf("expected 164 L1 info bytes, got %d", len(data))
}
// data layout assumed for Ecotone:
// offset type varname
Expand All @@ -262,12 +282,20 @@ func extractL1GasParamsEcotone(data []byte) (l1BaseFee *big.Int, costFunc l1Cost
// 68 uint256 _blobBaseFee,
// 100 bytes32 _hash,
// 132 bytes32 _batcherHash,
l1BaseFee = new(big.Int).SetBytes(data[36:68])
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
l1BaseFeeScalar := binary.BigEndian.Uint32(data[4:8])
l1BaseFeeScalarBig := big.NewInt(int64(l1BaseFeeScalar))
l1BlobBaseFeeScalar := binary.BigEndian.Uint32(data[8:12])
l1BlobBaseFeeScalarBig := big.NewInt(int64(l1BlobBaseFeeScalar))
costFunc := newL1CostFuncEcotone(l1BaseFee, l1BlobBaseFee, l1BaseFeeScalarBig, l1BlobBaseFeeScalarBig)
return gasParams{
l1BaseFee: l1BaseFee,
l1BlobBaseFee: l1BlobBaseFee,
costFunc: costFunc,
l1BaseFeeScalar: &l1BaseFeeScalar,
l1BlobBaseFeeScalar: &l1BlobBaseFeeScalar,
}, nil
}

// L1Cost computes the the data availability fee for transactions in blocks prior to the Ecotone
Expand Down
16 changes: 10 additions & 6 deletions core/types/rollup_cost_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,17 @@ func TestExtractBedrockGasParams(t *testing.T) {

data := getBedrockL1Attributes(baseFee, overhead, scalar)

_, costFuncPreRegolith, _, err := extractL1GasParams(config, regolithTime-1, data)
gasparams, err := extractL1GasParams(config, regolithTime-1, data)
costFuncPreRegolith := gasparams.costFunc
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)
gasparams, err = extractL1GasParams(config, regolithTime, data)
costFuncRegolith := gasparams.costFunc
require.NoError(t, err)

c, _ := costFuncPreRegolith(emptyTx.RollupCostData())
Expand All @@ -79,7 +81,7 @@ func TestExtractBedrockGasParams(t *testing.T) {

// try to extract from data which has not enough params, should get error.
data = data[:len(data)-4-32]
_, _, _, err = extractL1GasParams(config, regolithTime, data)
_, err = extractL1GasParams(config, regolithTime, data)
require.Error(t, err)
}

Expand All @@ -95,7 +97,8 @@ func TestExtractEcotoneGasParams(t *testing.T) {

data := getEcotoneL1Attributes(baseFee, blobBaseFee, baseFeeScalar, blobBaseFeeScalar)

_, costFunc, _, err := extractL1GasParams(config, 0, data)
gasparams, err := extractL1GasParams(config, 0, data)
costFunc := gasparams.costFunc
require.NoError(t, err)

c, g := costFunc(emptyTx.RollupCostData())
Expand All @@ -105,7 +108,7 @@ func TestExtractEcotoneGasParams(t *testing.T) {

// make sure wrong amont of data results in error
data = append(data, 0x00) // tack on garbage byte
_, _, err = extractL1GasParamsEcotone(data)
_, err = extractL1GasParamsEcotone(data)
require.Error(t, err)
}

Expand All @@ -123,7 +126,8 @@ func TestFirstBlockEcotoneGasParams(t *testing.T) {

data := getBedrockL1Attributes(baseFee, overhead, scalar)

_, oldCostFunc, _, err := extractL1GasParams(config, 0, data)
gasparams, err := extractL1GasParams(config, 0, data)
oldCostFunc := gasparams.costFunc
require.NoError(t, err)
c, _ := oldCostFunc(emptyTx.RollupCostData())
require.Equal(t, regolithFee, c)
Expand Down

0 comments on commit da6ea72

Please sign in to comment.