From e7913cf8f081e54ae991ede0e5e7a9347552ed23 Mon Sep 17 00:00:00 2001 From: Ryan Stout Date: Tue, 29 Oct 2024 15:24:11 -0700 Subject: [PATCH 1/7] Add DA cost estimation to CCIPMessageExecCostUSD18Calculator --- execute/exectypes/costly_messages.go | 67 ++++++++++++++++++- execute/exectypes/costly_messages_test.go | 78 ++++++++++++++++++----- internal/mocks/inmem/ccipreader_inmem.go | 6 ++ mocks/pkg/reader/ccip_reader.go | 56 ++++++++++++++++ pkg/reader/ccip.go | 68 ++++++++++++++++++++ pkg/reader/ccip_interface.go | 3 + pkg/types/ccipocr3/fee_quoter.go | 34 ++++++++++ 7 files changed, 293 insertions(+), 19 deletions(-) create mode 100644 pkg/types/ccipocr3/fee_quoter.go diff --git a/execute/exectypes/costly_messages.go b/execute/exectypes/costly_messages.go index 3bce58a5..e392a53f 100644 --- a/execute/exectypes/costly_messages.go +++ b/execute/exectypes/costly_messages.go @@ -15,6 +15,12 @@ import ( 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. @@ -340,11 +346,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 +376,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{} diff --git a/execute/exectypes/costly_messages_test.go b/execute/exectypes/costly_messages_test.go index ce801c25..0621aed5 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,35 @@ 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(50000), + b2: plugintypes.NewUSD18(60000), + b3: plugintypes.NewUSD18(70000), + }, + wantErr: false, + }, { name: "fee components error", messages: []ccipocr3.Message{ @@ -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, }, } @@ -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 { diff --git a/internal/mocks/inmem/ccipreader_inmem.go b/internal/mocks/inmem/ccipreader_inmem.go index 5361ce09..c164e644 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 322a127f..13dfdfbf 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 3ede7f43..624e5f52 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" @@ -1292,5 +1294,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) diff --git a/pkg/reader/ccip_interface.go b/pkg/reader/ccip_interface.go index a5e967b7..7b22128b 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/types/ccipocr3/fee_quoter.go b/pkg/types/ccipocr3/fee_quoter.go new file mode 100644 index 00000000..0a191af9 --- /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 +} From ab38be9ef6f96183a0efe3e04909704115ba9e38 Mon Sep 17 00:00:00 2001 From: nogo <0xnogo@gmail.com> Date: Wed, 30 Oct 2024 14:33:35 +0400 Subject: [PATCH 2/7] GetMedianDataAvailabilityGasConfig tests --- pkg/reader/ccip_test.go | 138 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/pkg/reader/ccip_test.go b/pkg/reader/ccip_test.go index e62ecb42..c3ec87d1 100644 --- a/pkg/reader/ccip_test.go +++ b/pkg/reader/ccip_test.go @@ -973,3 +973,141 @@ 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(t *testing.T, 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(t *testing.T, 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(t *testing.T, 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(t *testing.T, readers map[cciptypes.ChainSelector]*reader_mocks.MockExtended) { + values := []mockValue{ + {100, 10, 1000, true}, + {200, 20, 2000, true}, + {300, 30, 3000, true}, + } + setupConfigMocks(t, 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(t *testing.T, readers map[cciptypes.ChainSelector]*reader_mocks.MockExtended) { + values := []mockValue{ + {100, 10, 1000, true}, + {200, 20, 2000, false}, + {300, 30, 3000, true}, + } + setupConfigMocks(t, readers, []cciptypes.ChainSelector{chainA, chainB, chainC}, values) + }, + }, + { + name: "error - no valid configs found", + expectError: true, + chains: []cciptypes.ChainSelector{chainA, chainB, chainC}, + setupMocks: func(t *testing.T, readers map[cciptypes.ChainSelector]*reader_mocks.MockExtended) { + setupErrorMocks(t, readers, []cciptypes.ChainSelector{chainA, chainB, chainC}, errors.New("mock error")) + }, + }, + { + name: "error - all configs disabled", + expectError: true, + chains: []cciptypes.ChainSelector{chainA, chainB}, + setupMocks: func(t *testing.T, readers map[cciptypes.ChainSelector]*reader_mocks.MockExtended) { + values := []mockValue{ + {100, 10, 1000, false}, + {200, 20, 2000, false}, + } + setupConfigMocks(t, 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(t, 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) + } + }) + } +} From f586cc905896a1667027920bb3cf3f8694573c3e Mon Sep 17 00:00:00 2001 From: nogo <0xnogo@gmail.com> Date: Wed, 30 Oct 2024 14:36:30 +0400 Subject: [PATCH 3/7] linting --- pkg/reader/ccip_test.go | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/pkg/reader/ccip_test.go b/pkg/reader/ccip_test.go index c3ec87d1..e7e50ce2 100644 --- a/pkg/reader/ccip_test.go +++ b/pkg/reader/ccip_test.go @@ -982,7 +982,10 @@ func TestCCIPChainReader_GetMedianDataAvailabilityGasConfig(t *testing.T) { enabled bool } - setupConfigMocks := func(t *testing.T, readers map[cciptypes.ChainSelector]*reader_mocks.MockExtended, chains []cciptypes.ChainSelector, values []mockValue) { + setupConfigMocks := func( + readers map[cciptypes.ChainSelector]*reader_mocks.MockExtended, + chains []cciptypes.ChainSelector, + values []mockValue) { for i, chain := range chains { readers[chain].EXPECT(). ExtendedGetLatestValue( @@ -1004,7 +1007,9 @@ func TestCCIPChainReader_GetMedianDataAvailabilityGasConfig(t *testing.T) { } } - setupErrorMocks := func(t *testing.T, readers map[cciptypes.ChainSelector]*reader_mocks.MockExtended, chains []cciptypes.ChainSelector, err error) { + setupErrorMocks := func( + readers map[cciptypes.ChainSelector]*reader_mocks.MockExtended, + chains []cciptypes.ChainSelector, err error) { for _, chain := range chains { readers[chain].EXPECT(). ExtendedGetLatestValue( @@ -1024,7 +1029,7 @@ func TestCCIPChainReader_GetMedianDataAvailabilityGasConfig(t *testing.T) { expectedConfig cciptypes.DataAvailabilityGasConfig expectError bool chains []cciptypes.ChainSelector - setupMocks func(t *testing.T, readers map[cciptypes.ChainSelector]*reader_mocks.MockExtended) + setupMocks func(readers map[cciptypes.ChainSelector]*reader_mocks.MockExtended) }{ { name: "success - returns median values from multiple configs", @@ -1034,13 +1039,13 @@ func TestCCIPChainReader_GetMedianDataAvailabilityGasConfig(t *testing.T) { DestDataAvailabilityMultiplierBps: 2000, }, chains: []cciptypes.ChainSelector{chainA, chainB, chainC}, - setupMocks: func(t *testing.T, readers map[cciptypes.ChainSelector]*reader_mocks.MockExtended) { + setupMocks: func(readers map[cciptypes.ChainSelector]*reader_mocks.MockExtended) { values := []mockValue{ {100, 10, 1000, true}, {200, 20, 2000, true}, {300, 30, 3000, true}, } - setupConfigMocks(t, readers, []cciptypes.ChainSelector{chainA, chainB, chainC}, values) + setupConfigMocks(readers, []cciptypes.ChainSelector{chainA, chainB, chainC}, values) }, }, { @@ -1051,33 +1056,33 @@ func TestCCIPChainReader_GetMedianDataAvailabilityGasConfig(t *testing.T) { DestDataAvailabilityMultiplierBps: 3000, }, chains: []cciptypes.ChainSelector{chainA, chainB, chainC}, - setupMocks: func(t *testing.T, readers map[cciptypes.ChainSelector]*reader_mocks.MockExtended) { + setupMocks: func(readers map[cciptypes.ChainSelector]*reader_mocks.MockExtended) { values := []mockValue{ {100, 10, 1000, true}, {200, 20, 2000, false}, {300, 30, 3000, true}, } - setupConfigMocks(t, readers, []cciptypes.ChainSelector{chainA, chainB, chainC}, values) + setupConfigMocks(readers, []cciptypes.ChainSelector{chainA, chainB, chainC}, values) }, }, { name: "error - no valid configs found", expectError: true, chains: []cciptypes.ChainSelector{chainA, chainB, chainC}, - setupMocks: func(t *testing.T, readers map[cciptypes.ChainSelector]*reader_mocks.MockExtended) { - setupErrorMocks(t, readers, []cciptypes.ChainSelector{chainA, chainB, chainC}, errors.New("mock error")) + 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(t *testing.T, readers map[cciptypes.ChainSelector]*reader_mocks.MockExtended) { + setupMocks: func(readers map[cciptypes.ChainSelector]*reader_mocks.MockExtended) { values := []mockValue{ {100, 10, 1000, false}, {200, 20, 2000, false}, } - setupConfigMocks(t, readers, []cciptypes.ChainSelector{chainA, chainB}, values) + setupConfigMocks(readers, []cciptypes.ChainSelector{chainA, chainB}, values) }, }, } @@ -1093,7 +1098,7 @@ func TestCCIPChainReader_GetMedianDataAvailabilityGasConfig(t *testing.T) { contractReaders[chain] = mockReaders[chain] } - tt.setupMocks(t, mockReaders) + tt.setupMocks(mockReaders) reader := &ccipChainReader{ lggr: logger.Test(t), From c9312f8256e27df7779c7070de8677604642c36a Mon Sep 17 00:00:00 2001 From: nogo <0xnogo@gmail.com> Date: Wed, 30 Oct 2024 14:59:53 +0400 Subject: [PATCH 4/7] addressing comments --- execute/exectypes/costly_messages.go | 6 +++--- pkg/reader/ccip.go | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/execute/exectypes/costly_messages.go b/execute/exectypes/costly_messages.go index e392a53f..e19ed0b3 100644 --- a/execute/exectypes/costly_messages.go +++ b/execute/exectypes/costly_messages.go @@ -386,12 +386,12 @@ func computeDataAvailabilityCostUSD18( return big.NewInt(0) } - messageGas := CalculateMessageMaxDAGas(message, daConfig) + messageGas := calculateMessageMaxDAGas(message, daConfig) return big.NewInt(0).Mul(messageGas, dataAvailabilityFee) } -// CalculateMessageMaxDAGas calculates the total DA gas needed for a CCIP message -func CalculateMessageMaxDAGas( +// calculateMessageMaxDAGas calculates the total DA gas needed for a CCIP message +func calculateMessageMaxDAGas( msg cciptypes.Message, daConfig cciptypes.DataAvailabilityGasConfig, ) *big.Int { diff --git a/pkg/reader/ccip.go b/pkg/reader/ccip.go index 624e5f52..46f8d648 100644 --- a/pkg/reader/ccip.go +++ b/pkg/reader/ccip.go @@ -1324,12 +1324,17 @@ func (r *ccipChainReader) getFeeQuoterDestChainConfig( } // 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 { From 4eba17b5cf9b37278fb2a09988b37c4609ef6ae4 Mon Sep 17 00:00:00 2001 From: nogo <0xnogo@gmail.com> Date: Wed, 30 Oct 2024 16:16:00 +0400 Subject: [PATCH 5/7] added DA unit test in costly messages --- execute/exectypes/costly_messages_test.go | 130 +++++++++++++++++++++- 1 file changed, 125 insertions(+), 5 deletions(-) diff --git a/execute/exectypes/costly_messages_test.go b/execute/exectypes/costly_messages_test.go index 0621aed5..a3cd8c63 100644 --- a/execute/exectypes/costly_messages_test.go +++ b/execute/exectypes/costly_messages_test.go @@ -354,9 +354,106 @@ func TestCCIPMessageExecCostUSD18Calculator_MessageExecCostUSD18(t *testing.T) { DestDataAvailabilityMultiplierBps: 200, }, want: map[ccipocr3.Bytes32]plugintypes.USD18{ - b1: plugintypes.NewUSD18(50000), - b2: plugintypes.NewUSD18(60000), - b3: plugintypes.NewUSD18(70000), + b1: plugintypes.NewUSD18(50000), // 10_000 (exec) + 40_000 (da) + b2: plugintypes.NewUSD18(60000), // 20_000 (exec) + 40_000 (da) + b3: plugintypes.NewUSD18(70000), // 30_000 (exec) + 40_000 (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(56400), // 10_000 (exec) + 46_400 (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(221600), // 10_000 (exec) + 211_600 (da) }, wantErr: false, }, @@ -382,9 +479,30 @@ func TestCCIPMessageExecCostUSD18Calculator_MessageExecCostUSD18(t *testing.T) { DestGasPerDataAvailabilityByte: 1, DestDataAvailabilityMultiplierBps: 1, }, - want: map[ccipocr3.Bytes32]plugintypes.USD18{}, + 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(48400), // 10_000 (exec) + 38_400 (da) + }, + wantErr: false, + }, } for _, tt := range tests { @@ -398,7 +516,9 @@ func TestCCIPMessageExecCostUSD18Calculator_MessageExecCostUSD18(t *testing.T) { DataAvailabilityFee: tt.dataAvailabilityFee, } mockReader.EXPECT().GetDestChainFeeComponents(ctx).Return(feeComponents, tt.feeComponentsError) - mockReader.EXPECT().GetMedianDataAvailabilityGasConfig(ctx).Return(tt.daGasConfig, nil).Maybe() + if !tt.wantErr { + mockReader.EXPECT().GetMedianDataAvailabilityGasConfig(ctx).Return(tt.daGasConfig, nil) + } ep := gasmock.NewMockEstimateProvider(t) if !tt.wantErr { From 515f11e2197df57bf0c938bb4bd2385f5e690a8b Mon Sep 17 00:00:00 2001 From: nogo <0xnogo@gmail.com> Date: Thu, 31 Oct 2024 16:49:40 +0400 Subject: [PATCH 6/7] addressing comments --- execute/exectypes/costly_messages.go | 15 +++++---------- execute/exectypes/costly_messages_test.go | 12 ++++++------ 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/execute/exectypes/costly_messages.go b/execute/exectypes/costly_messages.go index e19ed0b3..89986673 100644 --- a/execute/exectypes/costly_messages.go +++ b/execute/exectypes/costly_messages.go @@ -16,9 +16,10 @@ import ( ) 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 + 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. @@ -398,10 +399,8 @@ func calculateMessageMaxDAGas( // Calculate token data length var totalTokenDataLen int for _, tokenAmount := range msg.TokenAmounts { - totalTokenDataLen += len(tokenAmount.SourcePoolAddress) + - len(tokenAmount.DestTokenAddress) + + totalTokenDataLen += MessageFixedBytesPerToken + len(tokenAmount.ExtraData) + - EvmWordBytes + len(tokenAmount.DestExecData) } @@ -409,10 +408,6 @@ func calculateMessageMaxDAGas( 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 diff --git a/execute/exectypes/costly_messages_test.go b/execute/exectypes/costly_messages_test.go index a3cd8c63..86bd9cd1 100644 --- a/execute/exectypes/costly_messages_test.go +++ b/execute/exectypes/costly_messages_test.go @@ -354,9 +354,9 @@ func TestCCIPMessageExecCostUSD18Calculator_MessageExecCostUSD18(t *testing.T) { DestDataAvailabilityMultiplierBps: 200, }, want: map[ccipocr3.Bytes32]plugintypes.USD18{ - b1: plugintypes.NewUSD18(50000), // 10_000 (exec) + 40_000 (da) - b2: plugintypes.NewUSD18(60000), // 20_000 (exec) + 40_000 (da) - b3: plugintypes.NewUSD18(70000), // 30_000 (exec) + 40_000 (da) + 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, }, @@ -390,7 +390,7 @@ func TestCCIPMessageExecCostUSD18Calculator_MessageExecCostUSD18(t *testing.T) { DestDataAvailabilityMultiplierBps: 200, }, want: map[ccipocr3.Bytes32]plugintypes.USD18{ - b1: plugintypes.NewUSD18(56400), // 10_000 (exec) + 46_400 (da) + b1: plugintypes.NewUSD18(79200), // 10_000 (exec) + 69_200 (da) }, wantErr: false, }, @@ -453,7 +453,7 @@ func TestCCIPMessageExecCostUSD18Calculator_MessageExecCostUSD18(t *testing.T) { DestDataAvailabilityMultiplierBps: 200, }, want: map[ccipocr3.Bytes32]plugintypes.USD18{ - b1: plugintypes.NewUSD18(221600), // 10_000 (exec) + 211_600 (da) + b1: plugintypes.NewUSD18(219600), // 10_000 (exec) + 218_600 (da) }, wantErr: false, }, @@ -499,7 +499,7 @@ func TestCCIPMessageExecCostUSD18Calculator_MessageExecCostUSD18(t *testing.T) { DestDataAvailabilityMultiplierBps: 200, }, want: map[ccipocr3.Bytes32]plugintypes.USD18{ - b1: plugintypes.NewUSD18(48400), // 10_000 (exec) + 38_400 (da) + b1: plugintypes.NewUSD18(53600), // 10_000 (exec) + 43_600 (da) }, wantErr: false, }, From 16633223a1aa0bd71fa91a8eac827e730b30d959 Mon Sep 17 00:00:00 2001 From: nogo <0xnogo@gmail.com> Date: Fri, 1 Nov 2024 12:21:33 +0400 Subject: [PATCH 7/7] var name change --- execute/exectypes/costly_messages.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/execute/exectypes/costly_messages.go b/execute/exectypes/costly_messages.go index 89986673..498b8223 100644 --- a/execute/exectypes/costly_messages.go +++ b/execute/exectypes/costly_messages.go @@ -16,7 +16,7 @@ import ( ) const ( - EvmWordBytes = 32 + 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