Skip to content

Commit

Permalink
Introduce gas.EstimateProvider used by verifyReport for gas limiting. (
Browse files Browse the repository at this point in the history
  • Loading branch information
winder authored Aug 9, 2024
1 parent 04ac101 commit a5af749
Show file tree
Hide file tree
Showing 10 changed files with 368 additions and 86 deletions.
33 changes: 19 additions & 14 deletions execute/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/smartcontractkit/libocr/commontypes"
ragep2ptypes "github.com/smartcontractkit/libocr/ragep2p/types"

"github.com/smartcontractkit/chainlink-ccip/execute/internal/gas"
"github.com/smartcontractkit/chainlink-ccip/internal/reader"
"github.com/smartcontractkit/chainlink-ccip/pluginconfig"
)
Expand Down Expand Up @@ -47,13 +48,14 @@ func (p PluginFactoryConstructor) NewValidationService(ctx context.Context) (cor

// PluginFactory implements common ReportingPluginFactory and is used for (re-)initializing commit plugin instances.
type PluginFactory struct {
lggr logger.Logger
ocrConfig reader.OCR3ConfigWithMeta
execCodec cciptypes.ExecutePluginCodec
msgHasher cciptypes.MessageHasher
homeChainReader reader.HomeChain
contractReaders map[cciptypes.ChainSelector]types.ContractReader
chainWriters map[cciptypes.ChainSelector]types.ChainWriter
lggr logger.Logger
ocrConfig reader.OCR3ConfigWithMeta
execCodec cciptypes.ExecutePluginCodec
msgHasher cciptypes.MessageHasher
homeChainReader reader.HomeChain
estimateProvider gas.EstimateProvider
contractReaders map[cciptypes.ChainSelector]types.ContractReader
chainWriters map[cciptypes.ChainSelector]types.ChainWriter
}

func NewPluginFactory(
Expand All @@ -62,17 +64,19 @@ func NewPluginFactory(
execCodec cciptypes.ExecutePluginCodec,
msgHasher cciptypes.MessageHasher,
homeChainReader reader.HomeChain,
estimateProvider gas.EstimateProvider,
contractReaders map[cciptypes.ChainSelector]types.ContractReader,
chainWriters map[cciptypes.ChainSelector]types.ChainWriter,
) *PluginFactory {
return &PluginFactory{
lggr: lggr,
ocrConfig: ocrConfig,
execCodec: execCodec,
msgHasher: msgHasher,
homeChainReader: homeChainReader,
contractReaders: contractReaders,
chainWriters: chainWriters,
lggr: lggr,
ocrConfig: ocrConfig,
execCodec: execCodec,
msgHasher: msgHasher,
homeChainReader: homeChainReader,
estimateProvider: estimateProvider,
contractReaders: contractReaders,
chainWriters: chainWriters,
}
}

Expand Down Expand Up @@ -112,6 +116,7 @@ func (p PluginFactory) NewReportingPlugin(
p.msgHasher,
p.homeChainReader,
nil, // TODO: token data reader
p.estimateProvider,
p.lggr,
), ocr3types.ReportingPluginInfo{
Name: "CCIPRoleExecute",
Expand Down
81 changes: 81 additions & 0 deletions execute/internal/gas/evm/gas_helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Package evm provides an EVM implementation to the gas.EstimateProvider interface.
// TODO: Move this package into the EVM repo, chainlink-ccip should be chain agnostic.
package evm

import (
"math"

"github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3"
)

const (
EvmAddressLengthBytes = 20
EvmWordBytes = 32
CalldataGasPerByte = 16
TokenAdminRegistryWarmupCost = 2_500
TokenAdminRegistryPoolLookupGas = 100 + // WARM_ACCESS_COST TokenAdminRegistry
700 + // CALL cost for TokenAdminRegistry
2_100 // COLD_SLOAD_COST loading the pool address
SupportsInterfaceCheck = 2600 + // because the receiver will be untouched initially
30_000*3 // supportsInterface of ERC165Checker library performs 3 static-calls of 30k gas each
PerTokenOverheadGas = TokenAdminRegistryPoolLookupGas +
SupportsInterfaceCheck +
200_000 + // releaseOrMint using callWithExactGas
50_000 // transfer using callWithExactGas
RateLimiterOverheadGas = 2_100 + // COLD_SLOAD_COST for accessing token bucket
5_000 // SSTORE_RESET_GAS for updating & decreasing token bucket
ConstantMessagePartBytes = 10 * 32 // A message consists of 10 abi encoded fields 32B each (after encoding)
ExecutionStateProcessingOverheadGas = 2_100 + // COLD_SLOAD_COST for first reading the state
20_000 + // SSTORE_SET_GAS for writing from 0 (untouched) to non-zero (in-progress)
100 //# SLOAD_GAS = WARM_STORAGE_READ_COST for rewriting from non-zero (in-progress) to non-zero (success/failure)
)

type EstimateProvider struct {
}

// CalculateMerkleTreeGas estimates the merkle tree gas based on number of requests
func (gp EstimateProvider) CalculateMerkleTreeGas(numRequests int) uint64 {
if numRequests == 0 {
return 0
}
merkleProofBytes := (math.Ceil(math.Log2(float64(numRequests))))*32 + (1+2)*32 // only ever one outer root hash
return uint64(merkleProofBytes * CalldataGasPerByte)
}

// return the size of bytes for msg tokens
func bytesForMsgTokens(numTokens int) int {
// token address (address) + token amount (uint256)
return (EvmAddressLengthBytes + EvmWordBytes) * numTokens
}

// CalculateMessageMaxGas computes the maximum gas overhead for a message.
func (gp EstimateProvider) CalculateMessageMaxGas(msg ccipocr3.Message) uint64 {
numTokens := len(msg.TokenAmounts)
var data []byte = msg.Data
dataLength := len(data)

// TODO: parse max gas from ExtraArgs.

messageBytes := ConstantMessagePartBytes +
bytesForMsgTokens(numTokens) +
dataLength

messageCallDataGas := uint64(messageBytes * CalldataGasPerByte)

// Rate limiter only limits value in tokens. It's not called if there are no
// tokens in the message. The same goes for the admin registry, it's only loaded
// if there are tokens, and it's only loaded once.
rateLimiterOverhead := uint64(0)
adminRegistryOverhead := uint64(0)
if numTokens >= 1 {
rateLimiterOverhead = RateLimiterOverheadGas
adminRegistryOverhead = TokenAdminRegistryWarmupCost
}

return messageCallDataGas +
ExecutionStateProcessingOverheadGas +
SupportsInterfaceCheck +
adminRegistryOverhead +
rateLimiterOverhead +
PerTokenOverheadGas*uint64(numTokens)
}
98 changes: 98 additions & 0 deletions execute/internal/gas/evm/gas_helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package evm

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"

"github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3"
)

func Test_calculateMessageMaxGas(t *testing.T) {
type args struct {
dataLen int
numTokens int
}
tests := []struct {
name string
args args
want uint64
}{
{
name: "base",
args: args{dataLen: 5, numTokens: 2},
want: 822_264,
},
{
name: "large",
args: args{dataLen: 1000, numTokens: 1000},
want: 346_477_520,
},
{
name: "overheadGas test 1",
args: args{dataLen: 0, numTokens: 0},
want: 119_920,
},
{
name: "overheadGas test 2",
args: args{dataLen: len([]byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0}), numTokens: 1},
want: 475_948,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
msg := ccipocr3.Message{
Data: make([]byte, tt.args.dataLen),
TokenAmounts: make([]ccipocr3.RampTokenAmount, tt.args.numTokens),
}
ep := EstimateProvider{}
got := ep.CalculateMessageMaxGas(msg)
fmt.Println(got)
assert.Equalf(t, tt.want, got, "calculateMessageMaxGas(%v, %v)", tt.args.dataLen, tt.args.numTokens)
})
}
}

// TestCalculateMaxGas is taken from the ccip repo where the CalculateMerkleTreeGas and CalculateMessageMaxGas values
// are combined to one function.
func TestCalculateMaxGas(t *testing.T) {
tests := []struct {
name string
numRequests int
dataLength int
numberOfTokens int
want uint64
}{
{
name: "maxGasOverheadGas 1",
numRequests: 6,
dataLength: 0,
numberOfTokens: 0,
want: 122992,
},
{
name: "maxGasOverheadGas 2",
numRequests: 3,
dataLength: len([]byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0}),
numberOfTokens: 1,
want: 478508,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
msg := ccipocr3.Message{
Data: make([]byte, tt.dataLength),
TokenAmounts: make([]ccipocr3.RampTokenAmount, tt.numberOfTokens),
}
ep := EstimateProvider{}

gotTree := ep.CalculateMerkleTreeGas(tt.numRequests)
gotMsg := ep.CalculateMessageMaxGas(msg)
fmt.Println("want", tt.want, "got", gotTree+gotMsg)
assert.Equal(t, tt.want, gotTree+gotMsg)
})
}
}
8 changes: 8 additions & 0 deletions execute/internal/gas/gas_estimate_provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package gas

import "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3"

type EstimateProvider interface {
CalculateMerkleTreeGas(numRequests int) uint64
CalculateMessageMaxGas(msg ccipocr3.Message) uint64
}
58 changes: 38 additions & 20 deletions execute/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ import (
"github.com/smartcontractkit/libocr/offchainreporting2plus/types"
libocrtypes "github.com/smartcontractkit/libocr/ragep2p/types"

"github.com/smartcontractkit/chainlink-ccip/internal/plugincommon"

"github.com/smartcontractkit/chainlink-common/pkg/logger"
cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3"

"github.com/smartcontractkit/chainlink-ccip/execute/internal/gas"
"github.com/smartcontractkit/chainlink-ccip/execute/report"
types2 "github.com/smartcontractkit/chainlink-ccip/execute/types"
"github.com/smartcontractkit/chainlink-ccip/internal/plugincommon"
"github.com/smartcontractkit/chainlink-ccip/internal/reader"
"github.com/smartcontractkit/chainlink-ccip/pluginconfig"
"github.com/smartcontractkit/chainlink-ccip/plugintypes"
Expand All @@ -40,9 +40,10 @@ type Plugin struct {
msgHasher cciptypes.MessageHasher
homeChain reader.HomeChain

oracleIDToP2pID map[commontypes.OracleID]libocrtypes.PeerID
tokenDataReader types2.TokenDataReader
lggr logger.Logger
oracleIDToP2pID map[commontypes.OracleID]libocrtypes.PeerID
tokenDataReader types2.TokenDataReader
estimateProvider gas.EstimateProvider
lggr logger.Logger
}

func NewPlugin(
Expand All @@ -54,10 +55,9 @@ func NewPlugin(
msgHasher cciptypes.MessageHasher,
homeChain reader.HomeChain,
tokenDataReader types2.TokenDataReader,
estimateProvider gas.EstimateProvider,
lggr logger.Logger,
) *Plugin {
// TODO: initialize tokenDataReader.

readerSyncer := plugincommon.NewBackgroundReaderSyncer(
lggr,
ccipReader,
Expand All @@ -69,16 +69,17 @@ func NewPlugin(
}

return &Plugin{
reportingCfg: reportingCfg,
cfg: cfg,
oracleIDToP2pID: oracleIDToP2pID,
ccipReader: ccipReader,
readerSyncer: readerSyncer,
reportCodec: reportCodec,
msgHasher: msgHasher,
homeChain: homeChain,
tokenDataReader: tokenDataReader,
lggr: lggr,
reportingCfg: reportingCfg,
cfg: cfg,
oracleIDToP2pID: oracleIDToP2pID,
ccipReader: ccipReader,
readerSyncer: readerSyncer,
reportCodec: reportCodec,
msgHasher: msgHasher,
homeChain: homeChain,
tokenDataReader: tokenDataReader,
estimateProvider: estimateProvider,
lggr: lggr,
}
}

Expand Down Expand Up @@ -270,13 +271,23 @@ func selectReport(
hasher cciptypes.MessageHasher,
encoder cciptypes.ExecutePluginCodec,
tokenDataReader types2.TokenDataReader,
estimateProvider gas.EstimateProvider,
commitReports []plugintypes.ExecutePluginCommitData,
maxReportSizeBytes int,
maxGas uint64,
) ([]cciptypes.ExecutePluginReportSingleChain, []plugintypes.ExecutePluginCommitData, error) {
// TODO: It may be desirable for this entire function to be an interface so that
// different selection algorithms can be used.

builder := report.NewBuilder(ctx, lggr, hasher, tokenDataReader, encoder, uint64(maxReportSizeBytes), 99)
builder := report.NewBuilder(
ctx,
lggr,
hasher,
tokenDataReader,
encoder,
estimateProvider,
uint64(maxReportSizeBytes),
maxGas)
var stillPendingReports []plugintypes.ExecutePluginCommitData
for i, report := range commitReports {
// Reports at the end may not have messages yet.
Expand Down Expand Up @@ -375,8 +386,15 @@ func (p *Plugin) Outcome(

// TODO: this function should be pure, a context should not be needed.
outcomeReports, commitReports, err :=
selectReport(context.Background(), p.lggr, p.msgHasher, p.reportCodec, p.tokenDataReader,
commitReports, maxReportSizeBytes)
selectReport(
context.Background(),
p.lggr, p.msgHasher,
p.reportCodec,
p.tokenDataReader,
p.estimateProvider,
commitReports,
maxReportSizeBytes,
p.cfg.OffchainConfig.BatchGasLimit)
if err != nil {
return ocr3types.Outcome{}, fmt.Errorf("unable to extract proofs: %w", err)
}
Expand Down
3 changes: 3 additions & 0 deletions execute/plugin_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3"

"github.com/smartcontractkit/chainlink-ccip/chainconfig"
"github.com/smartcontractkit/chainlink-ccip/execute/internal/gas/evm"
"github.com/smartcontractkit/chainlink-ccip/execute/report"
"github.com/smartcontractkit/chainlink-ccip/execute/types"
"github.com/smartcontractkit/chainlink-ccip/internal/libs/slicelib"
Expand Down Expand Up @@ -176,6 +177,7 @@ func setupSimpleTest(
cfg := pluginconfig.ExecutePluginConfig{
OffchainConfig: pluginconfig.ExecuteOffchainConfig{
MessageVisibilityInterval: *commonconfig.MustNewDuration(8 * time.Hour),
BatchGasLimit: 100000000,
},
DestChain: dstSelector,
}
Expand Down Expand Up @@ -255,6 +257,7 @@ func newNode(
msgHasher,
homeChain,
tokenDataReader,
evm.EstimateProvider{},
lggr)

return nodeSetup{
Expand Down
Loading

0 comments on commit a5af749

Please sign in to comment.