Skip to content

Commit

Permalink
Ecotone gas price (#12584)
Browse files Browse the repository at this point in the history
* define OP stack da price reader

* fix existing l1 oracle test

* fetch v1 gas price test

* add isEcotone test case

* address lint

* add changeset

* go imports

* fix test name

* fix OP oracle address

* fix lint
  • Loading branch information
matYang authored Mar 26, 2024
1 parent 7499d7e commit c7cacd0
Show file tree
Hide file tree
Showing 8 changed files with 569 additions and 35 deletions.
5 changes: 5 additions & 0 deletions .changeset/rude-paws-cross.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"chainlink": patch
---

L1Oracle handles OP Stack Ecotone encoded l1 gas price
56 changes: 47 additions & 9 deletions core/chains/evm/gas/rollups/l1_oracle.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/rpc"

"github.com/smartcontractkit/chainlink-common/pkg/logger"
"github.com/smartcontractkit/chainlink-common/pkg/services"
Expand All @@ -28,6 +29,12 @@ import (
//go:generate mockery --quiet --name ethClient --output ./mocks/ --case=underscore --structname ETHClient
type ethClient interface {
CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) ([]byte, error)
BatchCallContext(ctx context.Context, b []rpc.BatchElem) error
}

//go:generate mockery --quiet --name daPriceReader --output ./mocks/ --case=underscore --structname DAPriceReader
type daPriceReader interface {
GetDAGasPrice(ctx context.Context) (*big.Int, error)
}

type priceEntry struct {
Expand All @@ -53,6 +60,8 @@ type l1Oracle struct {
gasCostMethod string
l1GasCostMethodAbi abi.ABI

priceReader daPriceReader

chInitialised chan struct{}
chStop services.StopChan
chDone chan struct{}
Expand Down Expand Up @@ -109,9 +118,23 @@ func IsRollupWithL1Support(chainType config.ChainType) bool {
}

func NewL1GasOracle(lggr logger.Logger, ethClient ethClient, chainType config.ChainType) L1Oracle {
var priceReader daPriceReader
switch chainType {
case config.ChainOptimismBedrock:
priceReader = newOPPriceReader(lggr, ethClient, chainType, OPGasOracleAddress)
case config.ChainKroma:
priceReader = newOPPriceReader(lggr, ethClient, chainType, KromaGasOracleAddress)
default:
priceReader = nil
}
return newL1GasOracle(lggr, ethClient, chainType, priceReader)
}

func newL1GasOracle(lggr logger.Logger, ethClient ethClient, chainType config.ChainType, priceReader daPriceReader) L1Oracle {
var l1GasPriceAddress, gasPriceMethod, l1GasCostAddress, gasCostMethod string
var l1GasPriceMethodAbi, l1GasCostMethodAbi abi.ABI
var gasPriceErr, gasCostErr error

switch chainType {
case config.ChainArbitrum:
l1GasPriceAddress = ArbGasInfoAddress
Expand Down Expand Up @@ -164,6 +187,8 @@ func NewL1GasOracle(lggr logger.Logger, ethClient ethClient, chainType config.Ch
gasCostMethod: gasCostMethod,
l1GasCostMethodAbi: l1GasCostMethodAbi,

priceReader: priceReader,

chInitialised: make(chan struct{}),
chStop: make(chan struct{}),
chDone: make(chan struct{}),
Expand Down Expand Up @@ -222,13 +247,30 @@ func (o *l1Oracle) refreshWithError() (t *time.Timer, err error) {
ctx, cancel := o.chStop.CtxCancel(evmclient.ContextWithDefaultTimeout())
defer cancel()

price, err := o.fetchL1GasPrice(ctx)
if err != nil {
return t, err
}

o.l1GasPriceMu.Lock()
defer o.l1GasPriceMu.Unlock()
o.l1GasPrice = priceEntry{price: assets.NewWei(price), timestamp: time.Now()}
return
}

func (o *l1Oracle) fetchL1GasPrice(ctx context.Context) (price *big.Int, err error) {
// if dedicated priceReader exists, use the reader
if o.priceReader != nil {
return o.priceReader.GetDAGasPrice(ctx)
}

var callData, b []byte
precompile := common.HexToAddress(o.l1GasPriceAddress)
callData, err = o.l1GasPriceMethodAbi.Pack(o.gasPriceMethod)
if err != nil {
errMsg := fmt.Sprintf("failed to pack calldata for %s L1 gas price method", o.chainType)
o.logger.Errorf(errMsg)
return t, fmt.Errorf("%s: %w", errMsg, err)
return nil, fmt.Errorf("%s: %w", errMsg, err)
}
b, err = o.client.CallContract(ctx, ethereum.CallMsg{
To: &precompile,
Expand All @@ -237,20 +279,16 @@ func (o *l1Oracle) refreshWithError() (t *time.Timer, err error) {
if err != nil {
errMsg := "gas oracle contract call failed"
o.logger.Errorf(errMsg)
return t, fmt.Errorf("%s: %w", errMsg, err)
return nil, fmt.Errorf("%s: %w", errMsg, err)
}

if len(b) != 32 { // returns uint256;
errMsg := fmt.Sprintf("return data length (%d) different than expected (%d)", len(b), 32)
o.logger.Criticalf(errMsg)
return t, fmt.Errorf(errMsg)
return nil, fmt.Errorf(errMsg)
}
price := new(big.Int).SetBytes(b)

o.l1GasPriceMu.Lock()
defer o.l1GasPriceMu.Unlock()
o.l1GasPrice = priceEntry{price: assets.NewWei(price), timestamp: time.Now()}
return
price = new(big.Int).SetBytes(b)
return price, nil
}

func (o *l1Oracle) GasPrice(_ context.Context) (l1GasPrice *assets.Wei, err error) {
Expand Down
4 changes: 4 additions & 0 deletions core/chains/evm/gas/rollups/l1_oracle_abi.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@ const GasEstimateL1ComponentAbiString = `[{"inputs":[{"internalType":"address","
// All ABIs found at https://optimistic.etherscan.io/address/0xc0d3c0d3c0d3c0d3c0d3c0d3c0d3c0d3c0d3000f#code
const L1BaseFeeAbiString = `[{"inputs":[],"name":"l1BaseFee","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]`
const GetL1FeeAbiString = `[{"inputs":[{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"getL1Fee","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]`

// ABIs for OP Stack Ecotone GasPriceOracle methods needed to calculated encoded gas price
const OPIsEcotoneAbiString = `[{"inputs":[],"name":"isEcotone","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"}]`
const OPGetL1GasUsedAbiString = `[{"inputs":[{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"getL1GasUsed","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]`
32 changes: 6 additions & 26 deletions core/chains/evm/gas/rollups/l1_oracle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,21 +72,11 @@ func TestL1Oracle_GasPrice(t *testing.T) {

t.Run("Calling GasPrice on started Kroma L1Oracle returns Kroma l1GasPrice", func(t *testing.T) {
l1BaseFee := big.NewInt(100)
l1GasPriceMethodAbi, err := abi.JSON(strings.NewReader(L1BaseFeeAbiString))
require.NoError(t, err)

ethClient := mocks.NewETHClient(t)
ethClient.On("CallContract", mock.Anything, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})).Run(func(args mock.Arguments) {
callMsg := args.Get(1).(ethereum.CallMsg)
blockNumber := args.Get(2).(*big.Int)
var payload []byte
payload, err = l1GasPriceMethodAbi.Pack("l1BaseFee")
require.NoError(t, err)
require.Equal(t, payload, callMsg.Data)
assert.Nil(t, blockNumber)
}).Return(common.BigToHash(l1BaseFee).Bytes(), nil)
priceReader := mocks.NewDAPriceReader(t)
priceReader.On("GetDAGasPrice", mock.Anything).Return(l1BaseFee, nil)

oracle := NewL1GasOracle(logger.Test(t), ethClient, config.ChainKroma)
oracle := newL1GasOracle(logger.Test(t), nil, config.ChainKroma, priceReader)
servicetest.RunHealthy(t, oracle)

gasPrice, err := oracle.GasPrice(testutils.Context(t))
Expand All @@ -97,21 +87,11 @@ func TestL1Oracle_GasPrice(t *testing.T) {

t.Run("Calling GasPrice on started OPStack L1Oracle returns OPStack l1GasPrice", func(t *testing.T) {
l1BaseFee := big.NewInt(100)
l1GasPriceMethodAbi, err := abi.JSON(strings.NewReader(L1BaseFeeAbiString))
require.NoError(t, err)

ethClient := mocks.NewETHClient(t)
ethClient.On("CallContract", mock.Anything, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})).Run(func(args mock.Arguments) {
callMsg := args.Get(1).(ethereum.CallMsg)
blockNumber := args.Get(2).(*big.Int)
var payload []byte
payload, err = l1GasPriceMethodAbi.Pack("l1BaseFee")
require.NoError(t, err)
require.Equal(t, payload, callMsg.Data)
assert.Nil(t, blockNumber)
}).Return(common.BigToHash(l1BaseFee).Bytes(), nil)
priceReader := mocks.NewDAPriceReader(t)
priceReader.On("GetDAGasPrice", mock.Anything).Return(l1BaseFee, nil)

oracle := NewL1GasOracle(logger.Test(t), ethClient, config.ChainOptimismBedrock)
oracle := newL1GasOracle(logger.Test(t), nil, config.ChainOptimismBedrock, priceReader)
servicetest.RunHealthy(t, oracle)

gasPrice, err := oracle.GasPrice(testutils.Context(t))
Expand Down
59 changes: 59 additions & 0 deletions core/chains/evm/gas/rollups/mocks/da_price_reader.go

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

20 changes: 20 additions & 0 deletions core/chains/evm/gas/rollups/mocks/eth_client.go

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

Loading

0 comments on commit c7cacd0

Please sign in to comment.