diff --git a/commit/chainfee/observation.go b/commit/chainfee/observation.go index a0e0df29e..1c146e578 100644 --- a/commit/chainfee/observation.go +++ b/commit/chainfee/observation.go @@ -7,9 +7,13 @@ import ( "sort" "time" - "github.com/smartcontractkit/chainlink-common/pkg/types" + mapset "github.com/deckarep/golang-set/v2" + "golang.org/x/exp/maps" + "golang.org/x/sync/errgroup" + "github.com/smartcontractkit/chainlink-common/pkg/types" + "github.com/smartcontractkit/chainlink-ccip/internal/plugintypes" cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" ) @@ -85,10 +89,18 @@ func (p *processor) Observation( "timestampNow", now, ) + uniqueChains := mapset.NewSet[cciptypes.ChainSelector](maps.Keys(feeComponents)...) + uniqueChains = uniqueChains.Intersect(mapset.NewSet(maps.Keys(nativeTokenPrices)...)) + + if len(uniqueChains.ToSlice()) == 0 { + p.lggr.Info("observations don't have any unique chains") + return Observation{}, nil + } + obs := Observation{ FChain: fChain, - FeeComponents: feeComponents, - NativeTokenPrices: nativeTokenPrices, + FeeComponents: filterMapByUniqueChains(feeComponents, uniqueChains), + NativeTokenPrices: filterMapByUniqueChains(nativeTokenPrices, uniqueChains), ChainFeeUpdates: chainFeeUpdates, TimestampNow: now, } @@ -97,6 +109,20 @@ func (p *processor) Observation( return obs, nil } +// filterMapBySet filters a map based on the keys present in the set. +func filterMapByUniqueChains[T comparable]( + m map[cciptypes.ChainSelector]T, + s mapset.Set[cciptypes.ChainSelector], +) map[cciptypes.ChainSelector]T { + filtered := make(map[cciptypes.ChainSelector]T) + for k, v := range m { + if s.Contains(k) { + filtered[k] = v + } + } + return filtered +} + func (p *processor) observeFChain() map[cciptypes.ChainSelector]int { fChain, err := p.homeChain.GetFChain() if err != nil { diff --git a/commit/chainfee/observation_test.go b/commit/chainfee/observation_test.go index 8fa3bf4c8..3ebbf2815 100644 --- a/commit/chainfee/observation_test.go +++ b/commit/chainfee/observation_test.go @@ -10,11 +10,12 @@ import ( "golang.org/x/exp/maps" mapset "github.com/deckarep/golang-set/v2" + "github.com/smartcontractkit/libocr/commontypes" + "github.com/stretchr/testify/require" + "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/types" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" - "github.com/smartcontractkit/libocr/commontypes" - "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink-ccip/internal/plugintypes" "github.com/smartcontractkit/chainlink-ccip/mocks/internal_/plugincommon" @@ -169,3 +170,185 @@ func Test_processor_Observation(t *testing.T) { }) } } + +func Test_unique_chain_filter_in_Observation(t *testing.T) { + fourHoursAgo := time.Now().Add(-4 * time.Hour).UTC().Truncate(time.Hour) + + testCases := []struct { + name string + supportedChains []ccipocr3.ChainSelector + chainFeeComponents map[ccipocr3.ChainSelector]types.ChainFeeComponents + nativeTokenPrices map[ccipocr3.ChainSelector]ccipocr3.BigInt + existingChainFeePriceUpdates map[ccipocr3.ChainSelector]plugintypes.TimestampedBig + fChain map[ccipocr3.ChainSelector]int + dstChain ccipocr3.ChainSelector + expUniqueChains int + }{ + { + name: "unique chains intersection", + supportedChains: []ccipocr3.ChainSelector{1, 2, 3}, + dstChain: 3, + chainFeeComponents: map[ccipocr3.ChainSelector]types.ChainFeeComponents{ + 1: { + ExecutionFee: big.NewInt(10), + DataAvailabilityFee: big.NewInt(20), + }, + 2: { + ExecutionFee: big.NewInt(100), + DataAvailabilityFee: big.NewInt(200), + }, + }, + nativeTokenPrices: map[ccipocr3.ChainSelector]ccipocr3.BigInt{ + 1: ccipocr3.NewBigIntFromInt64(1000), + 2: ccipocr3.NewBigIntFromInt64(2000), + }, + existingChainFeePriceUpdates: map[ccipocr3.ChainSelector]plugintypes.TimestampedBig{ + 1: { + Timestamp: fourHoursAgo, + Value: ccipocr3.NewBigInt(FeeComponentsToPackedFee(ComponentsUSDPrices{ + ExecutionFeePriceUSD: big.NewInt(1234), + DataAvFeePriceUSD: big.NewInt(4321), + })), + }, + 2: { + Timestamp: fourHoursAgo, + Value: ccipocr3.NewBigInt(FeeComponentsToPackedFee(ComponentsUSDPrices{ + ExecutionFeePriceUSD: big.NewInt(12340), + DataAvFeePriceUSD: big.NewInt(43210), + })), + }, + }, + fChain: map[ccipocr3.ChainSelector]int{ + 1: 1, + 2: 2, + 3: 1, + }, + expUniqueChains: 2, + }, + { + name: "only one unique chain between fee components and native token prices", + supportedChains: []ccipocr3.ChainSelector{1, 2, 3}, + dstChain: 3, + chainFeeComponents: map[ccipocr3.ChainSelector]types.ChainFeeComponents{ + 1: { + ExecutionFee: big.NewInt(10), + DataAvailabilityFee: big.NewInt(20), + }, + 2: { + ExecutionFee: big.NewInt(100), + DataAvailabilityFee: big.NewInt(200), + }, + }, + nativeTokenPrices: map[ccipocr3.ChainSelector]ccipocr3.BigInt{ + 1: ccipocr3.NewBigIntFromInt64(1000), + }, + existingChainFeePriceUpdates: map[ccipocr3.ChainSelector]plugintypes.TimestampedBig{ + 1: { + Timestamp: fourHoursAgo, + Value: ccipocr3.NewBigInt(FeeComponentsToPackedFee(ComponentsUSDPrices{ + ExecutionFeePriceUSD: big.NewInt(1234), + DataAvFeePriceUSD: big.NewInt(4321), + })), + }, + 3: { + Timestamp: fourHoursAgo, + Value: ccipocr3.NewBigInt(FeeComponentsToPackedFee(ComponentsUSDPrices{ + ExecutionFeePriceUSD: big.NewInt(1234), + DataAvFeePriceUSD: big.NewInt(4321), + })), + }, + }, + fChain: map[ccipocr3.ChainSelector]int{ + 1: 1, + 2: 2, + 3: 1, + }, + expUniqueChains: 1, + }, + { + name: "zero unique chains between fee components and native token prices", + supportedChains: []ccipocr3.ChainSelector{1, 2, 3}, + dstChain: 3, + chainFeeComponents: map[ccipocr3.ChainSelector]types.ChainFeeComponents{ + 1: { + ExecutionFee: big.NewInt(10), + DataAvailabilityFee: big.NewInt(20), + }, + 2: { + ExecutionFee: big.NewInt(100), + DataAvailabilityFee: big.NewInt(200), + }, + }, + nativeTokenPrices: map[ccipocr3.ChainSelector]ccipocr3.BigInt{ + 3: ccipocr3.NewBigIntFromInt64(1000), + }, + existingChainFeePriceUpdates: map[ccipocr3.ChainSelector]plugintypes.TimestampedBig{ + 3: { + Timestamp: fourHoursAgo, + Value: ccipocr3.NewBigInt(FeeComponentsToPackedFee(ComponentsUSDPrices{ + ExecutionFeePriceUSD: big.NewInt(1234), + DataAvFeePriceUSD: big.NewInt(4321), + })), + }, + }, + fChain: map[ccipocr3.ChainSelector]int{ + 1: 1, + 2: 2, + 3: 1, + }, + expUniqueChains: 0, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + cs := plugincommon.NewMockChainSupport(t) + ccipReader := reader.NewMockCCIPReader(t) + homeChain := reader2.NewMockHomeChain(t) + oracleID := commontypes.OracleID(rand.Int() % 255) + lggr := logger.Test(t) + ctx := tests.Context(t) + + p := &processor{ + lggr: lggr, + chainSupport: cs, + destChain: tc.dstChain, + ccipReader: ccipReader, + oracleID: oracleID, + homeChain: homeChain, + metricsReporter: NoopMetrics{}, + } + + supportedSet := mapset.NewSet(tc.supportedChains...) + cs.EXPECT().DestChain().Return(tc.dstChain).Maybe() + cs.EXPECT().SupportedChains(oracleID). + Return(supportedSet, nil).Maybe() + + supportedSet.Remove(tc.dstChain) + slicesWithoutDst := supportedSet.ToSlice() + sort.Slice(slicesWithoutDst, func(i, j int) bool { return slicesWithoutDst[i] < slicesWithoutDst[j] }) + + ccipReader.EXPECT().GetChainsFeeComponents(ctx, slicesWithoutDst). + Return(tc.chainFeeComponents).Maybe() + + ccipReader.EXPECT().GetWrappedNativeTokenPriceUSD(ctx, slicesWithoutDst). + Return(tc.nativeTokenPrices).Maybe() + + ccipReader.EXPECT().GetChainFeePriceUpdate(ctx, slicesWithoutDst). + Return(tc.existingChainFeePriceUpdates).Maybe() + + homeChain.EXPECT().GetFChain().Return(tc.fChain, nil).Maybe() + + obs, err := p.Observation(ctx, Outcome{}, Query{}) + require.NoError(t, err) + if tc.expUniqueChains == 0 { + require.Empty(t, obs) + return + } + + require.True(t, tc.expUniqueChains == len(maps.Keys(obs.FeeComponents))) + require.True(t, tc.expUniqueChains == len(maps.Keys(obs.NativeTokenPrices))) + require.ElementsMatch(t, maps.Keys(obs.FeeComponents), maps.Keys(obs.NativeTokenPrices)) + }) + } +} diff --git a/commit/chainfee/outcome.go b/commit/chainfee/outcome.go index a08c0124c..30bb7dd27 100644 --- a/commit/chainfee/outcome.go +++ b/commit/chainfee/outcome.go @@ -216,9 +216,17 @@ func (p *processor) getGasPricesToUpdate( for chain, currentChainFee := range currentChainUSDFees { packedFee := cciptypes.NewBigInt(FeeComponentsToPackedFee(currentChainFee)) lastUpdate, exists := latestUpdates[chain] - nextUpdateTime := lastUpdate.Timestamp.Add(p.cfg.RemoteGasPriceBatchWriteFrequency.Duration()) // If the chain is not in the fee quoter updates or is stale, then we should update it - if !exists || obsTimestamp.After(nextUpdateTime) { + if !exists { + gasPrices = append(gasPrices, cciptypes.GasPriceChain{ + ChainSel: chain, + GasPrice: packedFee, + }) + continue + } + + nextUpdateTime := lastUpdate.Timestamp.Add(p.cfg.RemoteGasPriceBatchWriteFrequency.Duration()) + if obsTimestamp.After(nextUpdateTime) { gasPrices = append(gasPrices, cciptypes.GasPriceChain{ ChainSel: chain, GasPrice: packedFee, diff --git a/commit/chainfee/types.go b/commit/chainfee/types.go index 91ffef1b8..fb299694e 100644 --- a/commit/chainfee/types.go +++ b/commit/chainfee/types.go @@ -58,3 +58,8 @@ type NoopMetrics struct{} func (n NoopMetrics) TrackChainFeeObservation(Observation) {} func (n NoopMetrics) TrackChainFeeOutcome(Outcome) {} + +func (o Observation) IsEmpty() bool { + return len(o.FeeComponents) == 0 && len(o.NativeTokenPrices) == 0 && len(o.ChainFeeUpdates) == 0 && + len(o.FChain) == 0 && o.TimestampNow.IsZero() +} diff --git a/commit/chainfee/validate_observation.go b/commit/chainfee/validate_observation.go index f498a94a5..a2bc5a393 100644 --- a/commit/chainfee/validate_observation.go +++ b/commit/chainfee/validate_observation.go @@ -3,8 +3,12 @@ package chainfee import ( "fmt" "math/big" + "time" + + mapset "github.com/deckarep/golang-set/v2" "github.com/smartcontractkit/chainlink-ccip/internal/plugincommon" + "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" "golang.org/x/exp/maps" ) @@ -17,38 +21,104 @@ func (p *processor) ValidateObservation( obs := ao.Observation zero := big.NewInt(0) + if obs.IsEmpty() { + return nil + } + if err := plugincommon.ValidateFChain(obs.FChain); err != nil { return fmt.Errorf("failed to validate FChain: %w", err) } - observerSupportedChains, err := p.chainSupport.SupportedChains(ao.OracleID) if err != nil { return fmt.Errorf("failed to get supported chains: %w", err) } + if err := validateObservedChains(ao, observerSupportedChains); err != nil { + return fmt.Errorf("failed to validate observed chains: %w", err) + } - observedChains := append(maps.Keys(obs.FeeComponents), maps.Keys(obs.NativeTokenPrices)...) + if err := validateFeeComponents(ao); err != nil { + return fmt.Errorf("failed to validate fee components: %w", err) + } - for _, chain := range observedChains { - if !observerSupportedChains.Contains(chain) { - return fmt.Errorf("chain %d is not supported by observer", chain) + if err := validateChainFeeUpdates(ao); err != nil { + return fmt.Errorf("failed to validate chain fee updates: %w", err) + } + + for _, token := range obs.NativeTokenPrices { + if token.Int == nil || token.Int.Cmp(zero) <= 0 { + return fmt.Errorf("nil or non-positive %s", "execution fee") + } + } + + if obs.TimestampNow.IsZero() || obs.TimestampNow.After(time.Now().UTC()) { + return fmt.Errorf("invalid timestamp now value %s", obs.TimestampNow.String()) + } + return nil +} + +func validateChainFeeUpdates( + ao plugincommon.AttributedObservation[Observation], +) error { + for _, update := range ao.Observation.ChainFeeUpdates { + if update.ChainFee.ExecutionFeePriceUSD == nil || update.ChainFee.ExecutionFeePriceUSD.Cmp(big.NewInt(0)) <= 0 { + return fmt.Errorf("nil or non-positive %s", "execution fee price") + } + + if update.ChainFee.DataAvFeePriceUSD == nil || update.ChainFee.DataAvFeePriceUSD.Cmp(big.NewInt(0)) < 0 { + return fmt.Errorf("nil or negative %s", "data availability fee price") + } + if update.Timestamp.IsZero() { + return fmt.Errorf("zero timestamp") } } + return nil +} - for _, feeComponent := range obs.FeeComponents { - if feeComponent.ExecutionFee == nil || feeComponent.ExecutionFee.Cmp(zero) <= 0 { +func validateFeeComponents( + ao plugincommon.AttributedObservation[Observation], +) error { + for _, feeComponent := range ao.Observation.FeeComponents { + if feeComponent.ExecutionFee == nil || feeComponent.ExecutionFee.Cmp(big.NewInt(0)) <= 0 { return fmt.Errorf("nil or non-positive %s", "execution fee") } - if feeComponent.DataAvailabilityFee == nil || feeComponent.DataAvailabilityFee.Cmp(zero) < 0 { + if feeComponent.DataAvailabilityFee == nil || feeComponent.DataAvailabilityFee.Cmp(big.NewInt(0)) < 0 { return fmt.Errorf("nil or negative %s", "data availability fee") } } + return nil +} - for _, token := range obs.NativeTokenPrices { - if token.Int == nil || token.Int.Cmp(zero) <= 0 { - return fmt.Errorf("nil or non-positive %s", "execution fee") +func validateObservedChains( + ao plugincommon.AttributedObservation[Observation], + observerSupportedChains mapset.Set[ccipocr3.ChainSelector], +) error { + obs := ao.Observation + if !areMapKeysEqual(obs.FeeComponents, obs.NativeTokenPrices) { + return fmt.Errorf("fee components and native token prices have different observed chains") + } + + observedChains := append(maps.Keys(obs.FeeComponents), maps.Keys(obs.NativeTokenPrices)...) + observedChains = append(observedChains, maps.Keys(obs.ChainFeeUpdates)...) + for _, chain := range observedChains { + if !observerSupportedChains.Contains(chain) { + return fmt.Errorf("chain %d is not supported by observer", chain) } } return nil } + +func areMapKeysEqual[T, T1 comparable](map1 map[ccipocr3.ChainSelector]T, map2 map[ccipocr3.ChainSelector]T1) bool { + if len(map1) != len(map2) { + return false + } + + for key := range map1 { + if _, exists := map2[key]; !exists { + return false + } + } + + return true +} diff --git a/commit/chainfee/validate_observation_test.go b/commit/chainfee/validate_observation_test.go new file mode 100644 index 000000000..65ef1bd3a --- /dev/null +++ b/commit/chainfee/validate_observation_test.go @@ -0,0 +1,290 @@ +package chainfee + +import ( + "math/big" + "testing" + "time" + + mapset "github.com/deckarep/golang-set/v2" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/types" + + "github.com/smartcontractkit/chainlink-ccip/internal/plugincommon" + "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" +) + +func Test_validateFeeComponentsAndChainFeeUpdates(t *testing.T) { + fourHoursAgo := time.Now().Add(-4 * time.Hour).UTC().Truncate(time.Hour) + tests := []struct { + name string + feeComponents map[ccipocr3.ChainSelector]types.ChainFeeComponents + chainFeeUpdates map[ccipocr3.ChainSelector]Update + expectedFeeComponentError string + expectedChainFeeUpdatesError string + }{ + { + name: "valid fee components", + feeComponents: map[ccipocr3.ChainSelector]types.ChainFeeComponents{ + 1: { + ExecutionFee: big.NewInt(10), + DataAvailabilityFee: big.NewInt(20), + }, + }, + expectedFeeComponentError: "", + }, + { + name: "nil execution fee", + feeComponents: map[ccipocr3.ChainSelector]types.ChainFeeComponents{ + 1: { + ExecutionFee: nil, + DataAvailabilityFee: big.NewInt(20), + }, + }, + expectedFeeComponentError: "nil or non-positive execution fee", + }, + { + name: "non-positive execution fee", + feeComponents: map[ccipocr3.ChainSelector]types.ChainFeeComponents{ + 1: { + ExecutionFee: big.NewInt(0), + DataAvailabilityFee: big.NewInt(20), + }, + }, + expectedFeeComponentError: "nil or non-positive execution fee", + }, + { + name: "nil data availability fee", + feeComponents: map[ccipocr3.ChainSelector]types.ChainFeeComponents{ + 1: { + ExecutionFee: big.NewInt(10), + DataAvailabilityFee: nil, + }, + }, + expectedFeeComponentError: "nil or negative data availability fee", + }, + { + name: "negative data availability fee", + feeComponents: map[ccipocr3.ChainSelector]types.ChainFeeComponents{ + 1: { + ExecutionFee: big.NewInt(10), + DataAvailabilityFee: big.NewInt(-1), + }, + }, + expectedFeeComponentError: "nil or negative data availability fee", + }, + { + name: "valid chain fee updates", + chainFeeUpdates: map[ccipocr3.ChainSelector]Update{ + 1: { + ChainFee: ComponentsUSDPrices{ + ExecutionFeePriceUSD: big.NewInt(10), + DataAvFeePriceUSD: big.NewInt(20), + }, + Timestamp: fourHoursAgo, + }, + }, + expectedChainFeeUpdatesError: "", + }, + { + name: "nil execution fee price - chain fee updates", + chainFeeUpdates: map[ccipocr3.ChainSelector]Update{ + 1: { + ChainFee: ComponentsUSDPrices{ + ExecutionFeePriceUSD: nil, + DataAvFeePriceUSD: big.NewInt(20), + }, + Timestamp: fourHoursAgo, + }, + }, + expectedChainFeeUpdatesError: "nil or non-positive execution fee price", + }, + { + name: "non-positive execution fee price - chain fee updates", + chainFeeUpdates: map[ccipocr3.ChainSelector]Update{ + 1: { + ChainFee: ComponentsUSDPrices{ + ExecutionFeePriceUSD: big.NewInt(0), + DataAvFeePriceUSD: big.NewInt(20), + }, + Timestamp: fourHoursAgo, + }, + }, + expectedChainFeeUpdatesError: "nil or non-positive execution fee price", + }, + { + name: "nil data availability fee price - chain fee updates", + chainFeeUpdates: map[ccipocr3.ChainSelector]Update{ + 1: { + ChainFee: ComponentsUSDPrices{ + ExecutionFeePriceUSD: big.NewInt(10), + DataAvFeePriceUSD: nil, + }, + Timestamp: fourHoursAgo, + }, + }, + expectedChainFeeUpdatesError: "nil or negative data availability fee price", + }, + { + name: "negative data availability fee price - chain fee updates", + chainFeeUpdates: map[ccipocr3.ChainSelector]Update{ + 1: { + ChainFee: ComponentsUSDPrices{ + ExecutionFeePriceUSD: big.NewInt(10), + DataAvFeePriceUSD: big.NewInt(-1), + }, + Timestamp: fourHoursAgo, + }, + }, + expectedChainFeeUpdatesError: "nil or negative data availability fee price", + }, + { + name: "zero timestamp - chain fee updates", + chainFeeUpdates: map[ccipocr3.ChainSelector]Update{ + 1: { + ChainFee: ComponentsUSDPrices{ + ExecutionFeePriceUSD: big.NewInt(10), + DataAvFeePriceUSD: big.NewInt(20), + }, + }, + }, + expectedChainFeeUpdatesError: "zero timestamp", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ao := plugincommon.AttributedObservation[Observation]{ + Observation: Observation{ + FeeComponents: tt.feeComponents, + ChainFeeUpdates: tt.chainFeeUpdates, + }, + } + err := validateFeeComponents(ao) + if tt.expectedFeeComponentError == "" { + require.NoError(t, err) + } else { + require.Error(t, err) + require.Contains(t, err.Error(), tt.expectedFeeComponentError) + } + err = validateChainFeeUpdates(ao) + if tt.expectedChainFeeUpdatesError == "" { + require.NoError(t, err) + } else { + require.Error(t, err) + require.Contains(t, err.Error(), tt.expectedChainFeeUpdatesError) + } + }) + } +} + +func Test_validateObservedChains(t *testing.T) { + fourHoursAgo := time.Now().Add(-4 * time.Hour).UTC().Truncate(time.Hour) + tests := []struct { + name string + ao plugincommon.AttributedObservation[Observation] + observerSupportedChains mapset.Set[ccipocr3.ChainSelector] + expectedError string + }{ + { + name: "valid observed chains", + ao: plugincommon.AttributedObservation[Observation]{ + Observation: Observation{ + FeeComponents: map[ccipocr3.ChainSelector]types.ChainFeeComponents{ + 1: { + ExecutionFee: big.NewInt(10), + DataAvailabilityFee: big.NewInt(20), + }, + }, + NativeTokenPrices: map[ccipocr3.ChainSelector]ccipocr3.BigInt{ + 1: { + Int: big.NewInt(100), + }, + }, + ChainFeeUpdates: map[ccipocr3.ChainSelector]Update{ + 1: { + ChainFee: ComponentsUSDPrices{ + ExecutionFeePriceUSD: big.NewInt(10), + DataAvFeePriceUSD: big.NewInt(20), + }, + Timestamp: fourHoursAgo, + }, + }, + }, + }, + observerSupportedChains: mapset.NewSet[ccipocr3.ChainSelector](1), + expectedError: "", + }, + { + name: "unsupported chain", + ao: plugincommon.AttributedObservation[Observation]{ + Observation: Observation{ + FeeComponents: map[ccipocr3.ChainSelector]types.ChainFeeComponents{ + 1: { + ExecutionFee: big.NewInt(10), + DataAvailabilityFee: big.NewInt(20), + }, + }, + NativeTokenPrices: map[ccipocr3.ChainSelector]ccipocr3.BigInt{ + 1: { + Int: big.NewInt(100), + }, + }, + ChainFeeUpdates: map[ccipocr3.ChainSelector]Update{ + 1: { + ChainFee: ComponentsUSDPrices{ + ExecutionFeePriceUSD: big.NewInt(10), + DataAvFeePriceUSD: big.NewInt(20), + }, + Timestamp: fourHoursAgo, + }, + }, + }, + }, + observerSupportedChains: mapset.NewSet[ccipocr3.ChainSelector](2), + expectedError: "chain 1 is not supported by observer", + }, + { + name: "different observed chains in fee components and native token prices", + ao: plugincommon.AttributedObservation[Observation]{ + Observation: Observation{ + FeeComponents: map[ccipocr3.ChainSelector]types.ChainFeeComponents{ + 1: { + ExecutionFee: big.NewInt(10), + DataAvailabilityFee: big.NewInt(20), + }, + }, + NativeTokenPrices: map[ccipocr3.ChainSelector]ccipocr3.BigInt{ + 2: { + Int: big.NewInt(100), + }, + }, + ChainFeeUpdates: map[ccipocr3.ChainSelector]Update{ + 1: { + ChainFee: ComponentsUSDPrices{ + ExecutionFeePriceUSD: big.NewInt(10), + DataAvFeePriceUSD: big.NewInt(20), + }, + Timestamp: fourHoursAgo, + }, + }, + }, + }, + observerSupportedChains: mapset.NewSet[ccipocr3.ChainSelector](1, 2), + expectedError: "fee components and native token prices have different observed chains", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateObservedChains(tt.ao, tt.observerSupportedChains) + if tt.expectedError == "" { + require.NoError(t, err) + } else { + require.Error(t, err) + require.Contains(t, err.Error(), tt.expectedError) + } + }) + } +}