Skip to content

Commit

Permalink
zkSync L1 gas Price calculation oracle (#13498) (#1050) (#1056)
Browse files Browse the repository at this point in the history
* zkSync L1 Oracle gas Price

* fix tests & issues

* addressed comments

* added changeset

* Addressed comments

Merging the commit

smartcontractkit/chainlink@c6f1b30

## Motivation
Adding a Rollup GasPrice calculation oracle for zksync DA gasPrices
Ticket - https://smartcontract-it.atlassian.net/browse/SHIP-1988 

## Solution

---------

Co-authored-by: Rens Rooimans <[email protected]>
  • Loading branch information
simsonraj and RensR authored Jun 19, 2024
1 parent b399007 commit 64352e9
Show file tree
Hide file tree
Showing 3 changed files with 288 additions and 1 deletion.
4 changes: 3 additions & 1 deletion core/chains/evm/gas/rollups/l1_oracle.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const (
PollPeriod = 6 * time.Second
)

var supportedChainTypes = []config.ChainType{config.ChainArbitrum, config.ChainOptimismBedrock, config.ChainKroma, config.ChainScroll}
var supportedChainTypes = []config.ChainType{config.ChainArbitrum, config.ChainOptimismBedrock, config.ChainKroma, config.ChainScroll, config.ChainZkSync}

func IsRollupWithL1Support(chainType config.ChainType) bool {
return slices.Contains(supportedChainTypes, chainType)
Expand All @@ -62,6 +62,8 @@ func NewL1GasOracle(lggr logger.Logger, ethClient l1OracleClient, chainType conf
l1Oracle = NewOpStackL1GasOracle(lggr, ethClient, chainType)
case config.ChainArbitrum:
l1Oracle = NewArbitrumL1GasOracle(lggr, ethClient)
case config.ChainZkSync:
l1Oracle = NewZkSyncL1GasOracle(lggr, ethClient)
default:
panic(fmt.Sprintf("Received unspported chaintype %s", chainType))
}
Expand Down
38 changes: 38 additions & 0 deletions core/chains/evm/gas/rollups/l1_oracle_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package rollups

import (
"encoding/hex"
"errors"
"math/big"
"strings"
Expand Down Expand Up @@ -187,6 +188,43 @@ func TestL1Oracle_GasPrice(t *testing.T) {

assert.Equal(t, assets.NewWei(l1BaseFee), gasPrice)
})

t.Run("Calling GasPrice on started zkSync L1Oracle returns ZkSync l1GasPrice", func(t *testing.T) {
gasPerPubByteL2 := big.NewInt(1100)
gasPriceL2 := big.NewInt(25000000)
ZksyncGasInfo_getGasPriceL2 := "0xfe173b97"
ZksyncGasInfo_getGasPerPubdataByteL2 := "0x7cb9357e"
ethClient := mocks.NewL1OracleClient(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 := hex.DecodeString(ZksyncGasInfo_getGasPriceL2[2:])
require.NoError(t, err)
require.Equal(t, payload, callMsg.Data)
assert.Nil(t, blockNumber)
}).Return(common.BigToHash(gasPriceL2).Bytes(), nil).Once()

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 := hex.DecodeString(ZksyncGasInfo_getGasPerPubdataByteL2[2:])
require.NoError(t, err)
require.Equal(t, payload, callMsg.Data)
assert.Nil(t, blockNumber)
}).Return(common.BigToHash(gasPerPubByteL2).Bytes(), nil)

oracle := NewL1GasOracle(logger.Test(t), ethClient, config.ChainZkSync)
require.NoError(t, oracle.Start(testutils.Context(t)))
t.Cleanup(func() { assert.NoError(t, oracle.Close()) })

gasPrice, err := oracle.GasPrice(testutils.Context(t))
require.NoError(t, err)

assert.Equal(t, assets.NewWei(new(big.Int).Mul(gasPriceL2, gasPerPubByteL2)), gasPrice)
})
}

func TestL1Oracle_GetGasCost(t *testing.T) {
Expand Down
247 changes: 247 additions & 0 deletions core/chains/evm/gas/rollups/zkSync_l1_oracle.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
package rollups

import (
"context"
"encoding/hex"
"fmt"
"math/big"
"sync"
"time"

"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"

"github.com/smartcontractkit/chainlink-common/pkg/logger"
"github.com/smartcontractkit/chainlink-common/pkg/services"
"github.com/smartcontractkit/chainlink-common/pkg/utils"

gethtypes "github.com/ethereum/go-ethereum/core/types"

"github.com/smartcontractkit/chainlink/v2/common/config"
"github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets"
evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client"
)

// Reads L2-specific precompiles and caches the l1GasPrice set by the L2.
type zkSyncL1Oracle struct {
services.StateMachine
client l1OracleClient
pollPeriod time.Duration
logger logger.SugaredLogger
chainType config.ChainType

systemContextAddress string
gasPerPubdataMethod string
gasPerPubdataSelector string
l2GasPriceMethod string
l2GasPriceSelector string

l1GasPriceMu sync.RWMutex
l1GasPrice priceEntry

chInitialised chan struct{}
chStop services.StopChan
chDone chan struct{}
}

const (
// SystemContextAddress is the address of the "Precompiled contract that calls that holds the current gas per pubdata byte"
// https://sepolia.explorer.zksync.io/address/0x000000000000000000000000000000000000800b#contract
SystemContextAddress = "0x000000000000000000000000000000000000800B"

// ZksyncGasInfo_GetL2GasPerPubDataBytes is the a hex encoded call to:
// function gasPerPubdataByte() external view returns (uint256 gasPerPubdataByte);
SystemContext_gasPerPubdataByteMethod = "gasPerPubdataByte"
ZksyncGasInfo_getGasPerPubdataByteL2 = "0x7cb9357e"

// ZksyncGasInfo_GetL2GasPrice is the a hex encoded call to:
// `function gasPrice() external view returns (uint256);`
SystemContext_gasPriceMethod = "gasPrice"
ZksyncGasInfo_getGasPriceL2 = "0xfe173b97"
)

func NewZkSyncL1GasOracle(lggr logger.Logger, ethClient l1OracleClient) *zkSyncL1Oracle {
return &zkSyncL1Oracle{
client: ethClient,
pollPeriod: PollPeriod,
logger: logger.Sugared(logger.Named(lggr, "L1GasOracle(zkSync)")),
chainType: config.ChainZkSync,

systemContextAddress: SystemContextAddress,
gasPerPubdataMethod: SystemContext_gasPerPubdataByteMethod,
gasPerPubdataSelector: ZksyncGasInfo_getGasPerPubdataByteL2,
l2GasPriceMethod: SystemContext_gasPriceMethod,
l2GasPriceSelector: ZksyncGasInfo_getGasPriceL2,

chInitialised: make(chan struct{}),
chStop: make(chan struct{}),
chDone: make(chan struct{}),
}
}

func (o *zkSyncL1Oracle) Name() string {
return o.logger.Name()
}

func (o *zkSyncL1Oracle) Start(ctx context.Context) error {
return o.StartOnce(o.Name(), func() error {
go o.run()
<-o.chInitialised
return nil
})
}
func (o *zkSyncL1Oracle) Close() error {
return o.StopOnce(o.Name(), func() error {
close(o.chStop)
<-o.chDone
return nil
})
}

func (o *zkSyncL1Oracle) HealthReport() map[string]error {
return map[string]error{o.Name(): o.Healthy()}
}

func (o *zkSyncL1Oracle) run() {
defer close(o.chDone)

t := o.refresh()
close(o.chInitialised)

for {
select {
case <-o.chStop:
return
case <-t.C:
t = o.refresh()
}
}
}
func (o *zkSyncL1Oracle) refresh() (t *time.Timer) {
t, err := o.refreshWithError()
if err != nil {
o.SvcErrBuffer.Append(err)
}
return
}

func (o *zkSyncL1Oracle) refreshWithError() (t *time.Timer, err error) {
t = time.NewTimer(utils.WithJitter(o.pollPeriod))

ctx, cancel := o.chStop.CtxCancel(evmclient.ContextWithDefaultTimeout())
defer cancel()

price, err := o.CalculateL1GasPrice(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
}

// For zkSync l2_gas_PerPubdataByte = (blob_byte_price_on_l1 + part_of_l1_verification_cost) / (gas_price_on_l2)
// l2_gas_PerPubdataByte = blob_gas_price_on_l1 * gas_per_byte / gas_price_on_l2
// blob_gas_price_on_l1 * gas_per_byte ~= gas_price_on_l2 * l2_gas_PerPubdataByte
func (o *zkSyncL1Oracle) CalculateL1GasPrice(ctx context.Context) (price *big.Int, err error) {
l2GasPrice, err := o.GetL2GasPrice(ctx)
if err != nil {
return nil, err
}
l2GasPerPubDataByte, err := o.GetL2GasPerPubDataBytes(ctx)
if err != nil {
return nil, err
}
price = new(big.Int).Mul(l2GasPrice, l2GasPerPubDataByte)
return
}

func (o *zkSyncL1Oracle) GasPrice(_ context.Context) (l1GasPrice *assets.Wei, err error) {
var timestamp time.Time
ok := o.IfStarted(func() {
o.l1GasPriceMu.RLock()
l1GasPrice = o.l1GasPrice.price
timestamp = o.l1GasPrice.timestamp
o.l1GasPriceMu.RUnlock()
})
if !ok {
return l1GasPrice, fmt.Errorf("L1GasOracle is not started; cannot estimate gas")
}
if l1GasPrice == nil {
return l1GasPrice, fmt.Errorf("failed to get l1 gas price; gas price not set")
}
// Validate the price has been updated within the pollPeriod * 2
// Allowing double the poll period before declaring the price stale to give ample time for the refresh to process
if time.Since(timestamp) > o.pollPeriod*2 {
return l1GasPrice, fmt.Errorf("gas price is stale")
}
return
}

// Gets the L1 gas cost for the provided transaction at the specified block num
// If block num is not provided, the value on the latest block num is used
func (o *zkSyncL1Oracle) GetGasCost(ctx context.Context, tx *gethtypes.Transaction, blockNum *big.Int) (*assets.Wei, error) {
//Unused method, so not implemented
// And its not possible to know gas consumption of a transaction before its executed, since zkSync only posts the state difference
panic("unimplemented")
}

// GetL2GasPrice calls SystemContract.gasPrice() on the zksync system precompile contract.
//
// @return (The current gasPrice on L2: same as tx.gasPrice)
// function gasPrice() external view returns (uint256);
//
// https://github.com/matter-labs/era-contracts/blob/12a7d3bc1777ae5663e7525b2628061502755cbd/system-contracts/contracts/interfaces/ISystemContext.sol#L34C4-L34C57

func (o *zkSyncL1Oracle) GetL2GasPrice(ctx context.Context) (gasPriceL2 *big.Int, err error) {
precompile := common.HexToAddress(o.systemContextAddress)
method, err := hex.DecodeString(ZksyncGasInfo_getGasPriceL2[2:])
if err != nil {
return common.Big0, fmt.Errorf("cannot decode method: %w", err)
}
b, err := o.client.CallContract(ctx, ethereum.CallMsg{
To: &precompile,
Data: method,
}, nil)
if err != nil {
return common.Big0, fmt.Errorf("cannot fetch l2GasPrice from zkSync SystemContract: %w", err)
}

if len(b) != 1*32 { // uint256 gasPrice;
err = fmt.Errorf("return gasPrice (%d) different than expected (%d)", len(b), 3*32)
return
}
gasPriceL2 = new(big.Int).SetBytes(b)
return
}

// GetL2GasPerPubDataBytes calls SystemContract.gasPerPubdataByte() on the zksync system precompile contract.
//
// @return (The current gas per pubdata byte on L2)
// function gasPerPubdataByte() external view returns (uint256 gasPerPubdataByte);
//
// https://github.com/matter-labs/era-contracts/blob/12a7d3bc1777ae5663e7525b2628061502755cbd/system-contracts/contracts/interfaces/ISystemContext.sol#L58C14-L58C31

func (o *zkSyncL1Oracle) GetL2GasPerPubDataBytes(ctx context.Context) (gasPerPubByteL2 *big.Int, err error) {
precompile := common.HexToAddress(o.systemContextAddress)
method, err := hex.DecodeString(ZksyncGasInfo_getGasPerPubdataByteL2[2:])
if err != nil {
return common.Big0, fmt.Errorf("cannot decode method: %w", err)
}
b, err := o.client.CallContract(ctx, ethereum.CallMsg{
To: &precompile,
Data: method,
}, nil)
if err != nil {
return common.Big0, fmt.Errorf("cannot fetch gasPerPubdataByte from zkSync SystemContract: %w", err)
}

if len(b) != 1*32 { // uint256 gasPerPubdataByte;
err = fmt.Errorf("return data length (%d) different than expected (%d)", len(b), 3*32)
return
}
gasPerPubByteL2 = new(big.Int).SetBytes(b)
return
}

0 comments on commit 64352e9

Please sign in to comment.