diff --git a/execute/exectypes/costly_messages.go b/execute/exectypes/costly_messages.go index 3bce58a5c..498b8223d 100644 --- a/execute/exectypes/costly_messages.go +++ b/execute/exectypes/costly_messages.go @@ -15,6 +15,13 @@ import ( cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" ) +const ( + EVMWordBytes = 32 + MessageFixedBytesPerToken = 32 * ((2 * 3) + 3) + ConstantMessagePartBytes = 32 * 14 // A message consists of 14 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. @@ -340,11 +347,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 } @@ -364,4 +377,49 @@ 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 += MessageFixedBytesPerToken + + len(tokenAmount.ExtraData) + + len(tokenAmount.DestExecData) + } + + // Calculate total message data length + dataLen := ConstantMessagePartBytes + + len(msg.Data) + + len(msg.Sender) + + 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{} diff --git a/execute/exectypes/costly_messages_test.go b/execute/exectypes/costly_messages_test.go index ce801c253..86bd9cd14 100644 --- a/execute/exectypes/costly_messages_test.go +++ b/execute/exectypes/costly_messages_test.go @@ -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}, @@ -313,9 +315,15 @@ 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), @@ -323,6 +331,132 @@ func TestCCIPMessageExecCostUSD18Calculator_MessageExecCostUSD18(t *testing.T) { }, 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(55200), // 10_000 (exec) + 45_200 (da) + b2: plugintypes.NewUSD18(65200), // 20_000 (exec) + 45_200 (da) + b3: plugintypes.NewUSD18(75200), // 30_000 (exec) + 45_200 (da) + }, + wantErr: false, + }, + { + name: "message with token amounts affects DA gas calculation", + messages: []ccipocr3.Message{ + { + Header: ccipocr3.RampMessageHeader{MessageID: b1}, + TokenAmounts: []ccipocr3.RampTokenAmount{ + { + SourcePoolAddress: []byte("source_pool"), + DestTokenAddress: []byte("dest_token"), + ExtraData: []byte("extra"), + DestExecData: []byte("exec_data"), + Amount: ccipocr3.NewBigInt(big.NewInt(1)), + }, + }, + Data: []byte("some_data"), + Sender: []byte("sender"), + Receiver: []byte("receiver"), + ExtraArgs: []byte("extra_args"), + }, + }, + messageGases: []uint64{100}, + executionFee: big.NewInt(100), + dataAvailabilityFee: big.NewInt(400), + feeComponentsError: nil, + daGasConfig: ccipocr3.DataAvailabilityGasConfig{ + DestDataAvailabilityOverheadGas: 1000, + DestGasPerDataAvailabilityByte: 10, + DestDataAvailabilityMultiplierBps: 200, + }, + want: map[ccipocr3.Bytes32]plugintypes.USD18{ + b1: plugintypes.NewUSD18(79200), // 10_000 (exec) + 69_200 (da) + }, + wantErr: false, + }, + { + name: "zero DA multiplier results in only overhead gas", + messages: []ccipocr3.Message{ + { + Header: ccipocr3.RampMessageHeader{MessageID: b1}, + Data: []byte("some_data"), + }, + }, + messageGases: []uint64{100}, + executionFee: big.NewInt(100), + dataAvailabilityFee: big.NewInt(400), + feeComponentsError: nil, + daGasConfig: ccipocr3.DataAvailabilityGasConfig{ + DestDataAvailabilityOverheadGas: 1000, + DestGasPerDataAvailabilityByte: 10, + DestDataAvailabilityMultiplierBps: 0, // Zero multiplier + }, + want: map[ccipocr3.Bytes32]plugintypes.USD18{ + b1: plugintypes.NewUSD18(10000), // Only exec cost, DA cost is 0 + }, + wantErr: false, + }, + { + name: "large message with multiple tokens", + messages: []ccipocr3.Message{ + { + Header: ccipocr3.RampMessageHeader{MessageID: b1}, + TokenAmounts: []ccipocr3.RampTokenAmount{ + { + SourcePoolAddress: make([]byte, 100), // Large token data + DestTokenAddress: make([]byte, 100), + ExtraData: make([]byte, 100), + DestExecData: make([]byte, 100), + Amount: ccipocr3.NewBigInt(big.NewInt(1)), + }, + { + SourcePoolAddress: make([]byte, 100), // Second token + DestTokenAddress: make([]byte, 100), + ExtraData: make([]byte, 100), + DestExecData: make([]byte, 100), + Amount: ccipocr3.NewBigInt(big.NewInt(1)), + }, + }, + Data: make([]byte, 1000), // Large message data + Sender: make([]byte, 100), + Receiver: make([]byte, 100), + ExtraArgs: make([]byte, 100), + }, + }, + messageGases: []uint64{100}, + executionFee: big.NewInt(100), + dataAvailabilityFee: big.NewInt(400), + feeComponentsError: nil, + daGasConfig: ccipocr3.DataAvailabilityGasConfig{ + DestDataAvailabilityOverheadGas: 1000, + DestGasPerDataAvailabilityByte: 10, + DestDataAvailabilityMultiplierBps: 200, + }, + want: map[ccipocr3.Bytes32]plugintypes.USD18{ + b1: plugintypes.NewUSD18(219600), // 10_000 (exec) + 218_600 (da) + }, + wantErr: false, + }, { name: "fee components error", messages: []ccipocr3.Message{ @@ -336,11 +470,38 @@ 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: nil, + wantErr: true, + }, + { + name: "minimal message - only constant parts", + messages: []ccipocr3.Message{ + { + Header: ccipocr3.RampMessageHeader{MessageID: b1}, + }, + }, + messageGases: []uint64{100}, + executionFee: big.NewInt(100), + dataAvailabilityFee: big.NewInt(400), + feeComponentsError: nil, + daGasConfig: ccipocr3.DataAvailabilityGasConfig{ + DestDataAvailabilityOverheadGas: 1000, + DestGasPerDataAvailabilityByte: 10, + DestDataAvailabilityMultiplierBps: 200, + }, + want: map[ccipocr3.Bytes32]plugintypes.USD18{ + b1: plugintypes.NewUSD18(53600), // 10_000 (exec) + 43_600 (da) + }, + wantErr: false, }, } @@ -352,9 +513,12 @@ 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) + if !tt.wantErr { + mockReader.EXPECT().GetMedianDataAvailabilityGasConfig(ctx).Return(tt.daGasConfig, nil) + } ep := gasmock.NewMockEstimateProvider(t) if !tt.wantErr { diff --git a/internal/mocks/inmem/ccipreader_inmem.go b/internal/mocks/inmem/ccipreader_inmem.go index 5361ce098..c164e6449 100644 --- a/internal/mocks/inmem/ccipreader_inmem.go +++ b/internal/mocks/inmem/ccipreader_inmem.go @@ -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{} diff --git a/mocks/pkg/reader/ccip_reader.go b/mocks/pkg/reader/ccip_reader.go index 322a127fa..13dfdfbfa 100644 --- a/mocks/pkg/reader/ccip_reader.go +++ b/mocks/pkg/reader/ccip_reader.go @@ -486,6 +486,62 @@ func (_c *MockCCIPReader_GetExpectedNextSequenceNumber_Call) RunAndReturn(run fu return _c } +// GetMedianDataAvailabilityGasConfig provides a mock function with given fields: ctx +func (_m *MockCCIPReader) GetMedianDataAvailabilityGasConfig(ctx context.Context) (ccipocr3.DataAvailabilityGasConfig, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetMedianDataAvailabilityGasConfig") + } + + var r0 ccipocr3.DataAvailabilityGasConfig + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (ccipocr3.DataAvailabilityGasConfig, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) ccipocr3.DataAvailabilityGasConfig); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(ccipocr3.DataAvailabilityGasConfig) + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockCCIPReader_GetMedianDataAvailabilityGasConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetMedianDataAvailabilityGasConfig' +type MockCCIPReader_GetMedianDataAvailabilityGasConfig_Call struct { + *mock.Call +} + +// GetMedianDataAvailabilityGasConfig is a helper method to define mock.On call +// - ctx context.Context +func (_e *MockCCIPReader_Expecter) GetMedianDataAvailabilityGasConfig(ctx interface{}) *MockCCIPReader_GetMedianDataAvailabilityGasConfig_Call { + return &MockCCIPReader_GetMedianDataAvailabilityGasConfig_Call{Call: _e.mock.On("GetMedianDataAvailabilityGasConfig", ctx)} +} + +func (_c *MockCCIPReader_GetMedianDataAvailabilityGasConfig_Call) Run(run func(ctx context.Context)) *MockCCIPReader_GetMedianDataAvailabilityGasConfig_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *MockCCIPReader_GetMedianDataAvailabilityGasConfig_Call) Return(_a0 ccipocr3.DataAvailabilityGasConfig, _a1 error) *MockCCIPReader_GetMedianDataAvailabilityGasConfig_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockCCIPReader_GetMedianDataAvailabilityGasConfig_Call) RunAndReturn(run func(context.Context) (ccipocr3.DataAvailabilityGasConfig, error)) *MockCCIPReader_GetMedianDataAvailabilityGasConfig_Call { + _c.Call.Return(run) + return _c +} + // GetRMNRemoteConfig provides a mock function with given fields: ctx, destChainSelector func (_m *MockCCIPReader) GetRMNRemoteConfig(ctx context.Context, destChainSelector ccipocr3.ChainSelector) (rmntypes.RemoteConfig, error) { ret := _m.Called(ctx, destChainSelector) diff --git a/pkg/reader/ccip.go b/pkg/reader/ccip.go index b0acdcdf1..fb3eddbc1 100644 --- a/pkg/reader/ccip.go +++ b/pkg/reader/ccip.go @@ -18,6 +18,8 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/types/query" "github.com/smartcontractkit/chainlink-common/pkg/types/query/primitives" + "github.com/smartcontractkit/chainlink-ccip/internal/plugincommon/consensus" + rmntypes "github.com/smartcontractkit/chainlink-ccip/commit/merkleroot/rmn/types" typeconv "github.com/smartcontractkit/chainlink-ccip/internal/libs/typeconv" "github.com/smartcontractkit/chainlink-ccip/internal/plugintypes" @@ -1342,5 +1344,76 @@ 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 +// DA data lives in the FeeQuoter contract on the source chain. To get the config of the destination chain, we need to +// read the FeeQuoter contract on the source chain. As nodes are not required to have all chains configured, we need to +// read all FeeQuoter contracts to get the median. +func (r *ccipChainReader) GetMedianDataAvailabilityGasConfig( + ctx context.Context, +) (cciptypes.DataAvailabilityGasConfig, error) { + overheadGasValues := make([]uint32, 0) + gasPerByteValues := make([]uint16, 0) + multiplierBpsValues := make([]uint16, 0) + + // TODO: pay attention to performance here, as we are looping through all chains + 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) diff --git a/pkg/reader/ccip_interface.go b/pkg/reader/ccip_interface.go index b612cd95a..c575aba79 100644 --- a/pkg/reader/ccip_interface.go +++ b/pkg/reader/ccip_interface.go @@ -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) } diff --git a/pkg/reader/ccip_test.go b/pkg/reader/ccip_test.go index e62ecb420..e7e50ce2c 100644 --- a/pkg/reader/ccip_test.go +++ b/pkg/reader/ccip_test.go @@ -973,3 +973,146 @@ func TestCCIPChainReader_LinkPriceUSD(t *testing.T) { assert.NoError(t, err) assert.Equal(t, cciptypes.NewBigIntFromInt64(145), price) } + +func TestCCIPChainReader_GetMedianDataAvailabilityGasConfig(t *testing.T) { + type mockValue struct { + overhead uint32 + perByte uint16 + multiplier uint16 + enabled bool + } + + setupConfigMocks := func( + readers map[cciptypes.ChainSelector]*reader_mocks.MockExtended, + chains []cciptypes.ChainSelector, + values []mockValue) { + for i, chain := range chains { + readers[chain].EXPECT(). + ExtendedGetLatestValue( + mock.Anything, + consts.ContractNameFeeQuoter, + consts.MethodNameGetDestChainConfig, + primitives.Unconfirmed, + mock.Anything, + mock.Anything, + ). + Return(nil). + Run(withReturnValueOverridden(func(returnVal interface{}) { + cfg := returnVal.(*cciptypes.FeeQuoterDestChainConfig) + cfg.DestDataAvailabilityOverheadGas = values[i].overhead + cfg.DestGasPerDataAvailabilityByte = values[i].perByte + cfg.DestDataAvailabilityMultiplierBps = values[i].multiplier + cfg.IsEnabled = values[i].enabled + })).Once() + } + } + + setupErrorMocks := func( + readers map[cciptypes.ChainSelector]*reader_mocks.MockExtended, + chains []cciptypes.ChainSelector, err error) { + for _, chain := range chains { + readers[chain].EXPECT(). + ExtendedGetLatestValue( + mock.Anything, + consts.ContractNameFeeQuoter, + consts.MethodNameGetDestChainConfig, + primitives.Unconfirmed, + mock.Anything, + mock.Anything, + ). + Return(err).Once() + } + } + + tests := []struct { + name string + expectedConfig cciptypes.DataAvailabilityGasConfig + expectError bool + chains []cciptypes.ChainSelector + setupMocks func(readers map[cciptypes.ChainSelector]*reader_mocks.MockExtended) + }{ + { + name: "success - returns median values from multiple configs", + expectedConfig: cciptypes.DataAvailabilityGasConfig{ + DestDataAvailabilityOverheadGas: 200, + DestGasPerDataAvailabilityByte: 20, + DestDataAvailabilityMultiplierBps: 2000, + }, + chains: []cciptypes.ChainSelector{chainA, chainB, chainC}, + setupMocks: func(readers map[cciptypes.ChainSelector]*reader_mocks.MockExtended) { + values := []mockValue{ + {100, 10, 1000, true}, + {200, 20, 2000, true}, + {300, 30, 3000, true}, + } + setupConfigMocks(readers, []cciptypes.ChainSelector{chainA, chainB, chainC}, values) + }, + }, + { + name: "success - skips disabled configs", + expectedConfig: cciptypes.DataAvailabilityGasConfig{ + DestDataAvailabilityOverheadGas: 300, + DestGasPerDataAvailabilityByte: 30, + DestDataAvailabilityMultiplierBps: 3000, + }, + chains: []cciptypes.ChainSelector{chainA, chainB, chainC}, + setupMocks: func(readers map[cciptypes.ChainSelector]*reader_mocks.MockExtended) { + values := []mockValue{ + {100, 10, 1000, true}, + {200, 20, 2000, false}, + {300, 30, 3000, true}, + } + setupConfigMocks(readers, []cciptypes.ChainSelector{chainA, chainB, chainC}, values) + }, + }, + { + name: "error - no valid configs found", + expectError: true, + chains: []cciptypes.ChainSelector{chainA, chainB, chainC}, + setupMocks: func(readers map[cciptypes.ChainSelector]*reader_mocks.MockExtended) { + setupErrorMocks(readers, []cciptypes.ChainSelector{chainA, chainB, chainC}, errors.New("mock error")) + }, + }, + { + name: "error - all configs disabled", + expectError: true, + chains: []cciptypes.ChainSelector{chainA, chainB}, + setupMocks: func(readers map[cciptypes.ChainSelector]*reader_mocks.MockExtended) { + values := []mockValue{ + {100, 10, 1000, false}, + {200, 20, 2000, false}, + } + setupConfigMocks(readers, []cciptypes.ChainSelector{chainA, chainB}, values) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockReaders := make(map[cciptypes.ChainSelector]*reader_mocks.MockExtended) + contractReaders := make(map[cciptypes.ChainSelector]contractreader.Extended) + + // Initialize mocks + for _, chain := range tt.chains { + mockReaders[chain] = reader_mocks.NewMockExtended(t) + contractReaders[chain] = mockReaders[chain] + } + + tt.setupMocks(mockReaders) + + reader := &ccipChainReader{ + lggr: logger.Test(t), + contractReaders: contractReaders, + destChain: chainC, + } + config, err := reader.GetMedianDataAvailabilityGasConfig(context.Background()) + + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedConfig, config) + } + }) + } +} diff --git a/pkg/types/ccipocr3/fee_quoter.go b/pkg/types/ccipocr3/fee_quoter.go new file mode 100644 index 000000000..0a191af9b --- /dev/null +++ b/pkg/types/ccipocr3/fee_quoter.go @@ -0,0 +1,34 @@ +package ccipocr3 + +// FeeQuoterDestChainConfig represents the configuration of a destination chain in the FeeQuoter contract +type FeeQuoterDestChainConfig struct { + IsEnabled bool // Whether this destination chain is enabled + MaxNumberOfTokensPerMsg uint16 // Maximum number of distinct ERC20 token transferred per message + MaxDataBytes uint32 // Maximum payload data size in bytes + MaxPerMsgGasLimit uint32 // Maximum gas limit for messages targeting EVMs + DestGasOverhead uint32 // Gas charged on top of the gasLimit to cover destination chain costs + DestGasPerPayloadByte uint16 // Destination chain gas charged per byte of `data` payload to receiver + DestDataAvailabilityOverheadGas uint32 // Extra data availability gas charged, e.g., for OCR + DestGasPerDataAvailabilityByte uint16 // Gas charged per byte of message data needing availability + DestDataAvailabilityMultiplierBps uint16 // Multiplier for data availability gas in bps + DefaultTokenFeeUSDCents uint16 // Default token fee charged per token transfer + DefaultTokenDestGasOverhead uint32 // Default gas charged to execute token transfer on destination + DefaultTxGasLimit uint32 // Default gas limit for a transaction + GasMultiplierWeiPerEth uint64 // Multiplier for gas costs, 1e18 based (11e17 = 10% extra cost) + NetworkFeeUSDCents uint32 // Flat network fee for messages, in multiples of 0.01 USD + GasPriceStalenessThreshold uint32 // Maximum time for gas price to be valid (0 means disabled) + EnforceOutOfOrder bool // Enforce the allowOutOfOrderExecution extraArg to be true + ChainFamilySelector [4]byte // Selector identifying the destination chain's family +} + +// HasNonEmptyDAGasParams returns true if the destination chain has non-empty data availability gas parameters +func (c FeeQuoterDestChainConfig) HasNonEmptyDAGasParams() bool { + return c.DestDataAvailabilityOverheadGas != 0 && c.DestGasPerDataAvailabilityByte != 0 && + c.DestDataAvailabilityMultiplierBps != 0 +} + +type DataAvailabilityGasConfig struct { + DestDataAvailabilityOverheadGas uint32 // Extra data availability gas charged, e.g., for OCR + DestGasPerDataAvailabilityByte uint16 // Gas charged per byte of message data needing availability + DestDataAvailabilityMultiplierBps uint16 // Multiplier for data availability gas in bps +}