Skip to content

Commit

Permalink
Add DA cost estimation to CCIPMessageExecCostUSD18Calculator
Browse files Browse the repository at this point in the history
  • Loading branch information
rstout committed Oct 29, 2024
1 parent 09d6056 commit ad24a2b
Show file tree
Hide file tree
Showing 7 changed files with 299 additions and 21 deletions.
70 changes: 66 additions & 4 deletions execute/exectypes/costly_messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,20 @@ import (
"math/big"
"time"

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

"github.com/smartcontractkit/chainlink-ccip/execute/internal/gas"
"github.com/smartcontractkit/chainlink-common/pkg/logger"

"github.com/smartcontractkit/chainlink-ccip/internal/plugintypes"
readerpkg "github.com/smartcontractkit/chainlink-ccip/pkg/reader"
cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3"
)

const (
EvmWordBytes = 32
ConstantMessagePartBytes = 10 * 32 // A message consists of 10 abi encoded fields 32B each (after encoding)
daMultiplierBase = 10_000 // DA multiplier is in multiples of 0.0001, i.e. 1/daMultiplierBase
)

// CostlyMessageObserver observes messages that are too costly to execute.
type CostlyMessageObserver interface {
// Observe takes a set of messages and returns a slice of message IDs that are too costly to execute.
Expand Down Expand Up @@ -340,11 +345,17 @@ func (c *CCIPMessageExecCostUSD18Calculator) MessageExecCostUSD18(
if feeComponents.ExecutionFee == nil {
return nil, fmt.Errorf("missing execution fee")
}
if feeComponents.DataAvailabilityFee == nil {
return nil, fmt.Errorf("missing data availability fee")
}
daConfig, err := c.ccipReader.GetMedianDataAvailabilityGasConfig(ctx)
if err != nil {
return nil, fmt.Errorf("unable to get data availability gas config: %w", err)
}

for _, msg := range messages {
executionCostUSD18 := c.computeExecutionCostUSD18(feeComponents.ExecutionFee, msg)
// TODO: implement data availability cost
dataAvailabilityCostUSD18 := new(big.Int)
dataAvailabilityCostUSD18 := computeDataAvailabilityCostUSD18(feeComponents.DataAvailabilityFee, daConfig, msg)
totalCostUSD18 := new(big.Int).Add(executionCostUSD18, dataAvailabilityCostUSD18)
messageExecCosts[msg.Header.MessageID] = totalCostUSD18
}
Expand All @@ -364,4 +375,55 @@ func (c *CCIPMessageExecCostUSD18Calculator) computeExecutionCostUSD18(
return cost
}

// computeDataAvailabilityCostUSD18 computes the data availability cost of a message in USD18s.
func computeDataAvailabilityCostUSD18(
dataAvailabilityFee *big.Int,
daConfig cciptypes.DataAvailabilityGasConfig,
message cciptypes.Message,
) plugintypes.USD18 {
if dataAvailabilityFee == nil || dataAvailabilityFee.Cmp(big.NewInt(0)) == 0 {
return big.NewInt(0)
}

messageGas := CalculateMessageMaxDAGas(message, daConfig)
return big.NewInt(0).Mul(messageGas, dataAvailabilityFee)
}

// CalculateMessageMaxDAGas calculates the total DA gas needed for a CCIP message
func CalculateMessageMaxDAGas(
msg cciptypes.Message,
daConfig cciptypes.DataAvailabilityGasConfig,
) *big.Int {
// Calculate token data length
var totalTokenDataLen int
for _, tokenAmount := range msg.TokenAmounts {
totalTokenDataLen += len(tokenAmount.SourcePoolAddress) +
len(tokenAmount.DestTokenAddress) +
len(tokenAmount.ExtraData) +
EvmWordBytes +
len(tokenAmount.DestExecData)
}

// Calculate total message data length
dataLen := ConstantMessagePartBytes +
len(msg.Data) +
len(msg.Sender) +
len(msg.Receiver) +
len(msg.ExtraArgs) +
len(msg.FeeToken) +
EvmWordBytes*2 + // FeeTokenAmount and FeeValueJuels
totalTokenDataLen

// Calculate base gas cost
dataGas := big.NewInt(int64(dataLen))
dataGas = new(big.Int).Mul(dataGas, big.NewInt(int64(daConfig.DestGasPerDataAvailabilityByte)))
dataGas = new(big.Int).Add(dataGas, big.NewInt(int64(daConfig.DestDataAvailabilityOverheadGas)))

// Then apply the multiplier as: (dataGas * daMultiplier) / multiplierBase
dataGas = new(big.Int).Mul(dataGas, big.NewInt(int64(daConfig.DestDataAvailabilityMultiplierBps)))
dataGas = new(big.Int).Div(dataGas, big.NewInt(daMultiplierBase))

return dataGas
}

var _ MessageExecCostUSD18Calculator = &CCIPMessageExecCostUSD18Calculator{}
78 changes: 61 additions & 17 deletions execute/exectypes/costly_messages_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -292,16 +292,18 @@ func TestCCIPMessageFeeE18USDCalculator_MessageFeeE18USD(t *testing.T) {

func TestCCIPMessageExecCostUSD18Calculator_MessageExecCostUSD18(t *testing.T) {
tests := []struct {
name string
messages []ccipocr3.Message
messageGases []uint64
executionFee *big.Int
feeComponentsError error
want map[ccipocr3.Bytes32]plugintypes.USD18
wantErr bool
name string
messages []ccipocr3.Message
messageGases []uint64
executionFee *big.Int
dataAvailabilityFee *big.Int
feeComponentsError error
daGasConfig ccipocr3.DataAvailabilityGasConfig
want map[ccipocr3.Bytes32]plugintypes.USD18
wantErr bool
}{
{
name: "happy path",
name: "happy path, no DA cost",
messages: []ccipocr3.Message{
{
Header: ccipocr3.RampMessageHeader{MessageID: b1},
Expand All @@ -313,16 +315,51 @@ func TestCCIPMessageExecCostUSD18Calculator_MessageExecCostUSD18(t *testing.T) {
Header: ccipocr3.RampMessageHeader{MessageID: b3},
},
},
messageGases: []uint64{100, 200, 300},
executionFee: big.NewInt(100),
feeComponentsError: nil,
messageGases: []uint64{100, 200, 300},
executionFee: big.NewInt(100),
dataAvailabilityFee: big.NewInt(0),
feeComponentsError: nil,
daGasConfig: ccipocr3.DataAvailabilityGasConfig{
DestDataAvailabilityOverheadGas: 1,
DestGasPerDataAvailabilityByte: 1,
DestDataAvailabilityMultiplierBps: 1,
},
want: map[ccipocr3.Bytes32]plugintypes.USD18{
b1: plugintypes.NewUSD18(10000),
b2: plugintypes.NewUSD18(20000),
b3: plugintypes.NewUSD18(30000),
},
wantErr: false,
},
{
name: "happy path, with DA cost",
messages: []ccipocr3.Message{
{
Header: ccipocr3.RampMessageHeader{MessageID: b1},
},
{
Header: ccipocr3.RampMessageHeader{MessageID: b2},
},
{
Header: ccipocr3.RampMessageHeader{MessageID: b3},
},
},
messageGases: []uint64{100, 200, 300},
executionFee: big.NewInt(100),
dataAvailabilityFee: big.NewInt(400),
feeComponentsError: nil,
daGasConfig: ccipocr3.DataAvailabilityGasConfig{
DestDataAvailabilityOverheadGas: 1200,
DestGasPerDataAvailabilityByte: 10,
DestDataAvailabilityMultiplierBps: 200,
},
want: map[ccipocr3.Bytes32]plugintypes.USD18{
b1: plugintypes.NewUSD18(50000),
b2: plugintypes.NewUSD18(60000),
b3: plugintypes.NewUSD18(70000),
},
wantErr: false,
},
{
name: "fee components error",
messages: []ccipocr3.Message{
Expand All @@ -336,11 +373,17 @@ func TestCCIPMessageExecCostUSD18Calculator_MessageExecCostUSD18(t *testing.T) {
Header: ccipocr3.RampMessageHeader{MessageID: b3},
},
},
messageGases: []uint64{100, 200, 300},
executionFee: big.NewInt(100),
feeComponentsError: fmt.Errorf("error"),
want: map[ccipocr3.Bytes32]plugintypes.USD18{},
wantErr: true,
messageGases: []uint64{100, 200, 300},
executionFee: big.NewInt(100),
dataAvailabilityFee: big.NewInt(0),
feeComponentsError: fmt.Errorf("error"),
daGasConfig: ccipocr3.DataAvailabilityGasConfig{
DestDataAvailabilityOverheadGas: 1,
DestGasPerDataAvailabilityByte: 1,
DestDataAvailabilityMultiplierBps: 1,
},
want: map[ccipocr3.Bytes32]plugintypes.USD18{},
wantErr: true,
},
}

Expand All @@ -352,9 +395,10 @@ func TestCCIPMessageExecCostUSD18Calculator_MessageExecCostUSD18(t *testing.T) {
mockReader := readerpkg_mock.NewMockCCIPReader(t)
feeComponents := types.ChainFeeComponents{
ExecutionFee: tt.executionFee,
DataAvailabilityFee: big.NewInt(0),
DataAvailabilityFee: tt.dataAvailabilityFee,
}
mockReader.EXPECT().GetDestChainFeeComponents(ctx).Return(feeComponents, tt.feeComponentsError)
mockReader.EXPECT().GetMedianDataAvailabilityGasConfig(ctx).Return(tt.daGasConfig, nil).Maybe()

ep := gasmock.NewMockEstimateProvider(t)
if !tt.wantErr {
Expand Down
6 changes: 6 additions & 0 deletions internal/mocks/inmem/ccipreader_inmem.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,5 +170,11 @@ func (r InMemoryCCIPReader) Sync(_ context.Context, _ reader.ContractAddresses)
return nil
}

func (r InMemoryCCIPReader) GetMedianDataAvailabilityGasConfig(
ctx context.Context,
) (cciptypes.DataAvailabilityGasConfig, error) {
return cciptypes.DataAvailabilityGasConfig{}, nil
}

// Interface compatibility check.
var _ reader.CCIPReader = InMemoryCCIPReader{}
56 changes: 56 additions & 0 deletions mocks/pkg/reader/ccip_reader.go

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

67 changes: 67 additions & 0 deletions pkg/reader/ccip.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"golang.org/x/exp/maps"
"golang.org/x/sync/errgroup"

"github.com/smartcontractkit/chainlink-ccip/internal/plugincommon/consensus"
"github.com/smartcontractkit/chainlink-common/pkg/logger"
"github.com/smartcontractkit/chainlink-common/pkg/types"
"github.com/smartcontractkit/chainlink-common/pkg/types/query"
Expand Down Expand Up @@ -1292,5 +1293,71 @@ func (r *ccipChainReader) getRMNRemoteAddress(
return rmnRemoteAddress, nil
}

// Get the DestChainConfig from the FeeQuoter contract on the given chain.
func (r *ccipChainReader) getFeeQuoterDestChainConfig(
ctx context.Context,
chainSelector cciptypes.ChainSelector,
) (cciptypes.FeeQuoterDestChainConfig, error) {
if err := validateExtendedReaderExistence(r.contractReaders, chainSelector); err != nil {
return cciptypes.FeeQuoterDestChainConfig{}, err
}

var destChainConfig cciptypes.FeeQuoterDestChainConfig
srcReader := r.contractReaders[chainSelector]
err := srcReader.ExtendedGetLatestValue(
ctx,
consts.ContractNameFeeQuoter,
consts.MethodNameGetDestChainConfig,
primitives.Unconfirmed,
map[string]any{
"destChainSelector": r.destChain,
},
&destChainConfig,
)

if err != nil {
return cciptypes.FeeQuoterDestChainConfig{}, fmt.Errorf("failed to get dest chain config: %w", err)
}

return destChainConfig, nil
}

// GetMedianDataAvailabilityGasConfig returns the median of the DataAvailabilityGasConfig values from all FeeQuoters
func (r *ccipChainReader) GetMedianDataAvailabilityGasConfig(
ctx context.Context,
) (cciptypes.DataAvailabilityGasConfig, error) {
overheadGasValues := make([]uint32, 0)
gasPerByteValues := make([]uint16, 0)
multiplierBpsValues := make([]uint16, 0)
for chain := range r.contractReaders {
config, err := r.getFeeQuoterDestChainConfig(ctx, chain)
if err != nil {
continue
}
if config.IsEnabled && config.HasNonEmptyDAGasParams() {
overheadGasValues = append(overheadGasValues, config.DestDataAvailabilityOverheadGas)
gasPerByteValues = append(gasPerByteValues, config.DestGasPerDataAvailabilityByte)
multiplierBpsValues = append(multiplierBpsValues, config.DestDataAvailabilityMultiplierBps)
}
}

if len(overheadGasValues) == 0 {
return cciptypes.DataAvailabilityGasConfig{}, fmt.Errorf("no valid fee quoter destChainConfigs found")
}

// Calculate medians
medianOverheadGas := consensus.Median(overheadGasValues, func(a, b uint32) bool { return a < b })
medianGasPerByte := consensus.Median(gasPerByteValues, func(a, b uint16) bool { return a < b })
medianMultiplierBps := consensus.Median(multiplierBpsValues, func(a, b uint16) bool { return a < b })

daConfig := cciptypes.DataAvailabilityGasConfig{
DestDataAvailabilityOverheadGas: medianOverheadGas,
DestGasPerDataAvailabilityByte: medianGasPerByte,
DestDataAvailabilityMultiplierBps: medianMultiplierBps,
}

return daConfig, nil
}

// Interface compliance check
var _ CCIPReader = (*ccipChainReader)(nil)
3 changes: 3 additions & 0 deletions pkg/reader/ccip_interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,4 +158,7 @@ type CCIPReader interface {
// Sync can be used to perform frequent syncing operations inside the reader implementation.
// Returns a bool indicating whether something was updated.
Sync(ctx context.Context, contracts ContractAddresses) error

// GetMedianDataAvailabilityGasConfig returns the median of the DataAvailabilityGasConfig values from all FeeQuoters
GetMedianDataAvailabilityGasConfig(ctx context.Context) (cciptypes.DataAvailabilityGasConfig, error)
}
Loading

0 comments on commit ad24a2b

Please sign in to comment.