From dc8599f0d1252eec075f16aaf9964e38c1378e7e Mon Sep 17 00:00:00 2001 From: Chunkai Yang Date: Thu, 27 Jun 2024 09:55:59 -0400 Subject: [PATCH] Leader lane multi-chain gas price reports (#1071) Previously, leader lane ORM was added to enabled Commit plugin to read multi-chain gas prices during `Observation`. This PR updates `Report` phase so Commit plugin can now report multi-chain gas prices. Important changes are: 1. make use of the PriceReportingDisabled flag to skip price reports on non-leader lanes 2. update gas price logic during Report to handle multi-chain gas prices --- .../ocr2/plugins/ccip/ccipcommit/ocr2.go | 220 +++-- .../ocr2/plugins/ccip/ccipcommit/ocr2_test.go | 777 ++++++++++++------ .../internal/ccipdb/price_service_test.go | 27 +- .../ocr2/plugins/ccip/observations.go | 7 +- .../ocr2/plugins/ccip/observations_test.go | 54 +- 5 files changed, 702 insertions(+), 383 deletions(-) diff --git a/core/services/ocr2/plugins/ccip/ccipcommit/ocr2.go b/core/services/ocr2/plugins/ccip/ccipcommit/ocr2.go index f1e732ebbc..3c96940b8d 100644 --- a/core/services/ocr2/plugins/ccip/ccipcommit/ocr2.go +++ b/core/services/ocr2/plugins/ccip/ccipcommit/ocr2.go @@ -112,7 +112,8 @@ func (r *CommitReportingPlugin) Observation(ctx context.Context, epochAndRound t return nil, err } - sourceGasPriceUSD, tokenPricesUSD, err := r.observePriceUpdates(ctx) + // Fetches multi-lane gasPricesUSD and tokenPricesUSD for the same dest chain + gasPricesUSD, sourceGasPriceUSD, tokenPricesUSD, err := r.observePriceUpdates(ctx) if err != nil { return nil, err } @@ -120,7 +121,7 @@ func (r *CommitReportingPlugin) Observation(ctx context.Context, epochAndRound t lggr.Infow("Observation", "minSeqNr", minSeqNr, "maxSeqNr", maxSeqNr, - "sourceGasPriceUSD", sourceGasPriceUSD, + "gasPricesUSD", gasPricesUSD, "tokenPricesUSD", tokenPricesUSD, "epochAndRound", epochAndRound, "messageIDs", messageIDs, @@ -134,11 +135,43 @@ func (r *CommitReportingPlugin) Observation(ctx context.Context, epochAndRound t Min: minSeqNr, Max: maxSeqNr, }, - TokenPricesUSD: tokenPricesUSD, - SourceGasPriceUSD: sourceGasPriceUSD, + TokenPricesUSD: tokenPricesUSD, + SourceGasPriceUSD: sourceGasPriceUSD, + SourceGasPriceUSDPerChain: gasPricesUSD, }.Marshal() } +// observePriceUpdates fetches latest gas and token prices from DB as long as price reporting is not disabled. +// The prices are aggregated for all lanes for the same destination chain. +func (r *CommitReportingPlugin) observePriceUpdates( + ctx context.Context, +) (gasPricesUSD map[uint64]*big.Int, sourceGasPriceUSD *big.Int, tokenPricesUSD map[cciptypes.Address]*big.Int, err error) { + // Do not observe prices if price reporting is disabled. Price reporting will be disabled for lanes that are not leader lanes. + if r.offchainConfig.PriceReportingDisabled { + r.lggr.Infow("Price reporting disabled, skipping gas and token price reads") + return map[uint64]*big.Int{}, nil, map[cciptypes.Address]*big.Int{}, nil + } + + // Fetches multi-lane gas prices and token prices, for the given dest chain + gasPricesUSD, tokenPricesUSD, err = r.priceService.GetGasAndTokenPrices(ctx, r.destChainSelector) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to get prices from PriceService: %w", err) + } + + // Set prices to empty maps if nil to be friendlier to JSON encoding + if gasPricesUSD == nil { + gasPricesUSD = map[uint64]*big.Int{} + } + if tokenPricesUSD == nil { + tokenPricesUSD = map[cciptypes.Address]*big.Int{} + } + + // For backwards compatibility with the older release during phased rollout, set the default gas price on this lane + sourceGasPriceUSD = gasPricesUSD[r.sourceChainSelector] + + return gasPricesUSD, sourceGasPriceUSD, tokenPricesUSD, nil +} + func (r *CommitReportingPlugin) calculateMinMaxSequenceNumbers(ctx context.Context, lggr logger.Logger) (uint64, uint64, []cciptypes.Hash, error) { nextSeqNum, err := r.commitStoreReader.GetExpectedNextSequenceNumber(ctx) if err != nil { @@ -174,23 +207,6 @@ func (r *CommitReportingPlugin) calculateMinMaxSequenceNumbers(ctx context.Conte return minSeqNr, maxSeqNr, messageIDs, nil } -func (r *CommitReportingPlugin) observePriceUpdates( - ctx context.Context, -) (sourceGasPriceUSD *big.Int, tokenPricesUSD map[cciptypes.Address]*big.Int, err error) { - gasPricesUSD, tokenPricesUSD, err := r.priceService.GetGasAndTokenPrices(ctx, r.destChainSelector) - if err != nil { - return nil, nil, err - } - - // Reduce to single gas price for compatibility. In a followup PR, Commit plugin will make use of all source chain gas prices. - sourceGasPriceUSD = gasPricesUSD[r.sourceChainSelector] - if sourceGasPriceUSD == nil { - return nil, nil, fmt.Errorf("missing gas price for sourceChainSelector %d", r.sourceChainSelector) - } - - return sourceGasPriceUSD, tokenPricesUSD, nil -} - // Gets the latest token price updates based on logs within the heartbeat // The updates returned by this function are guaranteed to not contain nil values. func (r *CommitReportingPlugin) getLatestTokenPriceUpdates(ctx context.Context, now time.Time) (map[cciptypes.Address]update, error) { @@ -219,33 +235,34 @@ func (r *CommitReportingPlugin) getLatestTokenPriceUpdates(ctx context.Context, return latestUpdates, nil } -// getLatestGasPriceUpdate returns the latest gas price update based on logs within the heartbeat. -// If an update is found, it is not expected to contain a nil value. If no updates found, empty update with nil value is returned. -func (r *CommitReportingPlugin) getLatestGasPriceUpdate(ctx context.Context, now time.Time) (gasUpdate update, error error) { - // If there are no price updates inflight, check latest prices onchain - gasPriceUpdates, err := r.destPriceRegistryReader.GetGasPriceUpdatesCreatedAfter( +// getLatestGasPriceUpdate returns the latest gas price updates based on logs within the heartbeat. +// If an update is found, it is not expected to contain a nil value. +func (r *CommitReportingPlugin) getLatestGasPriceUpdate(ctx context.Context, now time.Time) (map[uint64]update, error) { + gasPriceUpdates, err := r.destPriceRegistryReader.GetAllGasPriceUpdatesCreatedAfter( ctx, - r.sourceChainSelector, now.Add(-r.offchainConfig.GasPriceHeartBeat), 0, ) + if err != nil { - return update{}, err + return nil, err } - for _, priceUpdate := range gasPriceUpdates { + latestUpdates := make(map[uint64]update) + for _, gasUpdate := range gasPriceUpdates { + priceUpdate := gasUpdate.GasPriceUpdate // Ordered by ascending timestamps - timestamp := time.Unix(priceUpdate.GasPriceUpdate.TimestampUnixSec.Int64(), 0) - if !timestamp.Before(gasUpdate.timestamp) { - gasUpdate = update{ + timestamp := time.Unix(priceUpdate.TimestampUnixSec.Int64(), 0) + if priceUpdate.Value != nil && !timestamp.Before(latestUpdates[priceUpdate.DestChainSelector].timestamp) { + latestUpdates[priceUpdate.DestChainSelector] = update{ timestamp: timestamp, value: priceUpdate.Value, } } } - r.lggr.Infow("Latest gas price from log poller", "gasPriceUpdateVal", gasUpdate.value, "gasPriceUpdateTs", gasUpdate.timestamp) - return gasUpdate, nil + r.lggr.Infow("Latest gas price from log poller", "latestUpdates", latestUpdates) + return latestUpdates, nil } func (r *CommitReportingPlugin) Report(ctx context.Context, epochAndRound types.ReportTimestamp, _ types.Query, observations []types.AttributedObservation) (bool, types.Report, error) { @@ -259,7 +276,7 @@ func (r *CommitReportingPlugin) Report(ctx context.Context, epochAndRound types. parsableObservations := ccip.GetParsableObservations[ccip.CommitObservation](lggr, observations) - intervals, gasPriceObs, tokenPriceObs, err := extractObservationData(lggr, r.F, parsableObservations) + intervals, gasPriceObs, tokenPriceObs, err := extractObservationData(lggr, r.F, r.sourceChainSelector, parsableObservations) if err != nil { return false, nil, err } @@ -363,18 +380,26 @@ func calculateIntervalConsensus(intervals []cciptypes.CommitStoreInterval, f int // extractObservationData extracts observation fields into their own slices // and filters out observation data that are invalid -func extractObservationData(lggr logger.Logger, f int, observations []ccip.CommitObservation) (intervals []cciptypes.CommitStoreInterval, gasPrices []*big.Int, tokenPrices map[cciptypes.Address][]*big.Int, err error) { - // We require at least f+1 observations to each consensus. Checking to ensure there are at least f+1 parsed observations. +func extractObservationData(lggr logger.Logger, f int, sourceChainSelector uint64, observations []ccip.CommitObservation) (intervals []cciptypes.CommitStoreInterval, gasPrices map[uint64][]*big.Int, tokenPrices map[cciptypes.Address][]*big.Int, err error) { + // We require at least f+1 observations to reach consensus. Checking to ensure there are at least f+1 parsed observations. if len(observations) <= f { return nil, nil, nil, fmt.Errorf("not enough observations to form consensus: #obs=%d, f=%d", len(observations), f) } + gasPriceObservations := make(map[uint64][]*big.Int) tokenPriceObservations := make(map[cciptypes.Address][]*big.Int) for _, obs := range observations { intervals = append(intervals, obs.Interval) - if obs.SourceGasPriceUSD != nil { - gasPrices = append(gasPrices, obs.SourceGasPriceUSD) + for selector, price := range obs.SourceGasPriceUSDPerChain { + if price != nil { + gasPriceObservations[selector] = append(gasPriceObservations[selector], price) + } + } + // During phased rollout, NOPs running old release only report SourceGasPriceUSD. + // An empty `SourceGasPriceUSDPerChain` with a non-nil `SourceGasPriceUSD` can only happen with old release. + if len(obs.SourceGasPriceUSDPerChain) == 0 && obs.SourceGasPriceUSD != nil { + gasPriceObservations[sourceChainSelector] = append(gasPriceObservations[sourceChainSelector], obs.SourceGasPriceUSD) } for token, price := range obs.TokenPricesUSD { @@ -384,21 +409,26 @@ func extractObservationData(lggr logger.Logger, f int, observations []ccip.Commi } } - // Observations are invalid if observed gas price is nil, we require at least f+1 valid observations. - if len(gasPrices) <= f { - return nil, nil, nil, fmt.Errorf("not enough valid observations with non-nil gas prices: #obs=%d, f=%d", len(gasPrices), f) + // Price is dropped if there are not enough valid observations. With a threshold of 2*(f-1) + 1, we achieve a balance between safety and liveness. + // During phased-rollout where some honest nodes may not have started observing the token yet, it requires 5 malicious node with 1 being the leader to successfully alter price. + // During regular operation, it requires 3 malicious nodes with 1 being the leader to temporarily delay price update for the token. + priceReportingThreshold := 2*(f-1) + 1 + + gasPrices = make(map[uint64][]*big.Int) + for selector, perChainPriceObservations := range gasPriceObservations { + if len(perChainPriceObservations) < priceReportingThreshold { + lggr.Warnf("Skipping chain with selector %d due to not enough valid observations: #obs=%d, f=%d, threshold=%d", selector, len(perChainPriceObservations), f, priceReportingThreshold) + continue + } + gasPrices[selector] = perChainPriceObservations } tokenPrices = make(map[cciptypes.Address][]*big.Int) for token, perTokenPriceObservations := range tokenPriceObservations { - // Token price is dropped if there are not enough valid observations. With a threshold of 2*(f-1) + 1, we achieve a balance between safety and liveness. - // During phased-rollout where some honest nodes may not have started observing the token yet, it requires 5 malicious node with 1 being the leader to successfully alter price. - // During regular operation, it requires 3 malicious nodes with 1 being the leader to temporarily delay price update for the token. - if len(perTokenPriceObservations) < (2*(f-1) + 1) { - lggr.Warnf("Skipping token %s due to not enough valid observations: #obs=%d, f=%d", string(token), len(perTokenPriceObservations), f) + if len(perTokenPriceObservations) < priceReportingThreshold { + lggr.Warnf("Skipping token %s due to not enough valid observations: #obs=%d, f=%d, threshold=%d", string(token), len(perTokenPriceObservations), f, priceReportingThreshold) continue } - tokenPrices[token] = perTokenPriceObservations } @@ -406,7 +436,12 @@ func extractObservationData(lggr logger.Logger, f int, observations []ccip.Commi } // selectPriceUpdates filters out gas and token price updates that are already inflight -func (r *CommitReportingPlugin) selectPriceUpdates(ctx context.Context, now time.Time, gasPriceObs []*big.Int, tokenPriceObs map[cciptypes.Address][]*big.Int) ([]cciptypes.GasPrice, []cciptypes.TokenPrice, error) { +func (r *CommitReportingPlugin) selectPriceUpdates(ctx context.Context, now time.Time, gasPriceObs map[uint64][]*big.Int, tokenPriceObs map[cciptypes.Address][]*big.Int) ([]cciptypes.GasPrice, []cciptypes.TokenPrice, error) { + // If price reporting is disabled, there is no need to select price updates. + if r.offchainConfig.PriceReportingDisabled { + return nil, nil, nil + } + latestGasPrice, err := r.getLatestGasPriceUpdate(ctx, now) if err != nil { return nil, nil, err @@ -421,8 +456,9 @@ func (r *CommitReportingPlugin) selectPriceUpdates(ctx context.Context, now time } // Note priceUpdates must be deterministic. -// The provided latestTokenPrices should not contain nil values. -func (r *CommitReportingPlugin) calculatePriceUpdates(gasPriceObs []*big.Int, tokenPriceObs map[cciptypes.Address][]*big.Int, latestGasPrice update, latestTokenPrices map[cciptypes.Address]update) ([]cciptypes.GasPrice, []cciptypes.TokenPrice, error) { +// The provided gasPriceObs and tokenPriceObs should not contain nil values. +// The returned latestGasPrice and latestTokenPrices should not contain nil values. +func (r *CommitReportingPlugin) calculatePriceUpdates(gasPriceObs map[uint64][]*big.Int, tokenPriceObs map[cciptypes.Address][]*big.Int, latestGasPrice map[uint64]update, latestTokenPrices map[cciptypes.Address]update) ([]cciptypes.GasPrice, []cciptypes.TokenPrice, error) { var tokenPriceUpdates []cciptypes.TokenPrice for token, tokenPriceObservations := range tokenPriceObs { medianPrice := ccipcalc.BigIntSortedMiddle(tokenPriceObservations) @@ -432,7 +468,7 @@ func (r *CommitReportingPlugin) calculatePriceUpdates(gasPriceObs []*big.Int, to tokenPriceUpdatedRecently := time.Since(latestTokenPrice.timestamp) < r.offchainConfig.TokenPriceHeartBeat tokenPriceNotChanged := !ccipcalc.Deviates(medianPrice, latestTokenPrice.value, int64(r.offchainConfig.TokenPriceDeviationPPB)) if tokenPriceUpdatedRecently && tokenPriceNotChanged { - r.lggr.Debugw("price was updated recently, skipping the update", + r.lggr.Debugw("token price was updated recently, skipping the update", "token", token, "newPrice", medianPrice, "existingPrice", latestTokenPrice.value) continue // skip the update if we recently had a price update close to the new value } @@ -449,30 +485,38 @@ func (r *CommitReportingPlugin) calculatePriceUpdates(gasPriceObs []*big.Int, to return tokenPriceUpdates[i].Token < tokenPriceUpdates[j].Token }) - newGasPrice, err := r.gasPriceEstimator.Median(gasPriceObs) // Compute the median price - if err != nil { - return nil, nil, err - } - destChainSelector := r.sourceChainSelector // Assuming plugin lane is A->B, we write to B the gas price of A - var gasPriceUpdate []cciptypes.GasPrice - // Default to updating so that we update if there are no prior updates. - shouldUpdate := true - if latestGasPrice.value != nil { - gasPriceUpdatedRecently := time.Since(latestGasPrice.timestamp) < r.offchainConfig.GasPriceHeartBeat - gasPriceDeviated, err := r.gasPriceEstimator.Deviates(newGasPrice, latestGasPrice.value) + for chainSelector, gasPriceObservations := range gasPriceObs { + newGasPrice, err := r.gasPriceEstimator.Median(gasPriceObservations) // Compute the median price if err != nil { - return nil, nil, err + return nil, nil, fmt.Errorf("failed to calculate median gas price for chain selector %d: %w", chainSelector, err) } - if gasPriceUpdatedRecently && !gasPriceDeviated { - shouldUpdate = false + + // Default to updating so that we update if there are no prior updates. + latestGasPrice, exists := latestGasPrice[chainSelector] + if exists && latestGasPrice.value != nil { + gasPriceUpdatedRecently := time.Since(latestGasPrice.timestamp) < r.offchainConfig.GasPriceHeartBeat + gasPriceDeviated, err := r.gasPriceEstimator.Deviates(newGasPrice, latestGasPrice.value) + if err != nil { + return nil, nil, err + } + if gasPriceUpdatedRecently && !gasPriceDeviated { + r.lggr.Debugw("gas price was updated recently and not deviated sufficiently, skipping the update", + "chainSelector", chainSelector, "newPrice", newGasPrice, "existingPrice", latestGasPrice.value) + continue + } } - } - if shouldUpdate { - // Although onchain interface accepts multi gas updates, we only do 1 gas price per report for now. - gasPriceUpdate = append(gasPriceUpdate, cciptypes.GasPrice{DestChainSelector: destChainSelector, Value: newGasPrice}) + + gasPriceUpdate = append(gasPriceUpdate, cciptypes.GasPrice{ + DestChainSelector: chainSelector, + Value: newGasPrice, + }) } + sort.Slice(gasPriceUpdate, func(i, j int) bool { + return gasPriceUpdate[i].DestChainSelector < gasPriceUpdate[j].DestChainSelector + }) + return gasPriceUpdate, tokenPriceUpdates, nil } @@ -608,14 +652,9 @@ func (r *CommitReportingPlugin) isStaleReport(ctx context.Context, lggr logger.L if !hasGasPriceUpdate && !hasTokenPriceUpdates { return true } - // Commit plugin currently only supports 1 gas price per report. If report contains more than 1, reject the report. - if len(report.GasPrices) > 1 { - lggr.Errorw("Report is stale because it contains more than 1 gas price update", "GasPriceUpdates", report.GasPrices) - return true - } // We consider a price update as stale when, there isn't an update or there is an update that is stale. - gasPriceStale := !hasGasPriceUpdate || r.isStaleGasPrice(ctx, lggr, report.GasPrices[0]) + gasPriceStale := !hasGasPriceUpdate || r.isStaleGasPrice(ctx, lggr, report.GasPrices) tokenPricesStale := !hasTokenPriceUpdates || r.isStaleTokenPrices(ctx, lggr, report.TokenPrices) if gasPriceStale && tokenPricesStale { @@ -654,30 +693,35 @@ func (r *CommitReportingPlugin) isStaleMerkleRoot(ctx context.Context, lggr logg return false } -func (r *CommitReportingPlugin) isStaleGasPrice(ctx context.Context, lggr logger.Logger, gasPrice cciptypes.GasPrice) bool { +func (r *CommitReportingPlugin) isStaleGasPrice(ctx context.Context, lggr logger.Logger, gasPriceUpdates []cciptypes.GasPrice) bool { latestGasPrice, err := r.getLatestGasPriceUpdate(ctx, time.Now()) if err != nil { - lggr.Errorw("Report is stale because getLatestGasPriceUpdate failed", "err", err) + lggr.Errorw("Gas price is stale because getLatestGasPriceUpdate failed", "err", err) return true } - if latestGasPrice.value != nil { - gasPriceDeviated, err := r.gasPriceEstimator.Deviates(gasPrice.Value, latestGasPrice.value) + for _, gasPriceUpdate := range gasPriceUpdates { + latestUpdate, exists := latestGasPrice[gasPriceUpdate.DestChainSelector] + if !exists || latestUpdate.value == nil { + lggr.Infow("Found non-stale gas price", "chainSelector", gasPriceUpdate.DestChainSelector, "gasPriceUSd", gasPriceUpdate.Value) + return false + } + + gasPriceDeviated, err := r.gasPriceEstimator.Deviates(gasPriceUpdate.Value, latestUpdate.value) if err != nil { - lggr.Errorw("Report is stale because deviation check failed", "err", err) + lggr.Errorw("Gas price is stale because deviation check failed", "err", err) return true } - if !gasPriceDeviated { - lggr.Infow("Report is stale because of gas price", - "latestGasPriceUpdate", latestGasPrice.value, - "currentUsdPerUnitGas", gasPrice.Value, - "destChainSelector", gasPrice.DestChainSelector) - return true + if gasPriceDeviated { + lggr.Infow("Found non-stale gas price", "chainSelector", gasPriceUpdate.DestChainSelector, "gasPriceUSd", gasPriceUpdate.Value, "latestUpdate", latestUpdate.value) + return false } + lggr.Infow("Gas price is stale", "chainSelector", gasPriceUpdate.DestChainSelector, "gasPriceUSd", gasPriceUpdate.Value, "latestGasPrice", latestUpdate.value) } - return false + lggr.Infow("All gas prices are stale") + return true } func (r *CommitReportingPlugin) isStaleTokenPrices(ctx context.Context, lggr logger.Logger, priceUpdates []cciptypes.TokenPrice) bool { diff --git a/core/services/ocr2/plugins/ccip/ccipcommit/ocr2_test.go b/core/services/ocr2/plugins/ccip/ccipcommit/ocr2_test.go index 880f2208a4..6cf7e4bec7 100644 --- a/core/services/ocr2/plugins/ccip/ccipcommit/ocr2_test.go +++ b/core/services/ocr2/plugins/ccip/ccipcommit/ocr2_test.go @@ -20,13 +20,14 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + + "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + "github.com/smartcontractkit/chainlink-common/pkg/config" "github.com/smartcontractkit/chainlink-common/pkg/hashutil" "github.com/smartcontractkit/chainlink-common/pkg/merklemulti" cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" - "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" - - "github.com/smartcontractkit/libocr/offchainreporting2plus/types" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/mocks" mocks2 "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller/mocks" @@ -59,33 +60,22 @@ func TestCommitReportingPlugin_Observation(t *testing.T) { ccipcalc.HexToAddress("3000"), } - // Token price in 1e18 USD precision - bridgedTokenPrices := map[cciptypes.Address]*big.Int{ - bridgedTokens[0]: big.NewInt(1), - bridgedTokens[1]: big.NewInt(2e18), - } - - bridgedTokenDecimals := map[cciptypes.Address]uint8{ - bridgedTokens[0]: 8, - bridgedTokens[1]: 18, - } - // Token price of 1e18 token amount in 1e18 USD precision - expectedEncodedTokenPrice := map[cciptypes.Address]*big.Int{ + expectedTokenPrice := map[cciptypes.Address]*big.Int{ bridgedTokens[0]: big.NewInt(1e10), bridgedTokens[1]: big.NewInt(2e18), } testCases := []struct { - name string - epochAndRound types.ReportTimestamp - commitStorePaused bool - sourceChainCursed bool - commitStoreSeqNum uint64 - tokenPrices map[cciptypes.Address]*big.Int - sendReqs []cciptypes.EVM2EVMMessageWithTxMeta - tokenDecimals map[cciptypes.Address]uint8 - fee *big.Int + name string + epochAndRound types.ReportTimestamp + commitStorePaused bool + sourceChainCursed bool + commitStoreSeqNum uint64 + gasPrices map[uint64]*big.Int + tokenPrices map[cciptypes.Address]*big.Int + sendReqs []cciptypes.EVM2EVMMessageWithTxMeta + priceReportingDisabled bool expErr bool expObs ccip.CommitObservation @@ -93,20 +83,67 @@ func TestCommitReportingPlugin_Observation(t *testing.T) { { name: "base report", commitStoreSeqNum: 54, - tokenPrices: map[cciptypes.Address]*big.Int{ - bridgedTokens[0]: bridgedTokenPrices[bridgedTokens[0]], - bridgedTokens[1]: bridgedTokenPrices[bridgedTokens[1]], - sourceNativeTokenAddr: big.NewInt(2e18), + gasPrices: map[uint64]*big.Int{ + sourceChainSelector: big.NewInt(2e18), }, + tokenPrices: expectedTokenPrice, sendReqs: []cciptypes.EVM2EVMMessageWithTxMeta{ {EVM2EVMMessage: cciptypes.EVM2EVMMessage{SequenceNumber: 54}}, {EVM2EVMMessage: cciptypes.EVM2EVMMessage{SequenceNumber: 55}}, }, - fee: big.NewInt(2e18), - tokenDecimals: bridgedTokenDecimals, expObs: ccip.CommitObservation{ - TokenPricesUSD: expectedEncodedTokenPrice, - SourceGasPriceUSD: big.NewInt(4e18), + TokenPricesUSD: expectedTokenPrice, + SourceGasPriceUSD: big.NewInt(2e18), + SourceGasPriceUSDPerChain: map[uint64]*big.Int{ + sourceChainSelector: big.NewInt(2e18), + }, + Interval: cciptypes.CommitStoreInterval{ + Min: 54, + Max: 55, + }, + }, + }, + { + name: "base report with multi-chain gas prices", + commitStoreSeqNum: 54, + gasPrices: map[uint64]*big.Int{ + sourceChainSelector + 1: big.NewInt(2e18), + sourceChainSelector + 2: big.NewInt(3e18), + }, + tokenPrices: expectedTokenPrice, + sendReqs: []cciptypes.EVM2EVMMessageWithTxMeta{ + {EVM2EVMMessage: cciptypes.EVM2EVMMessage{SequenceNumber: 54}}, + {EVM2EVMMessage: cciptypes.EVM2EVMMessage{SequenceNumber: 55}}, + }, + expObs: ccip.CommitObservation{ + TokenPricesUSD: expectedTokenPrice, + SourceGasPriceUSD: nil, + SourceGasPriceUSDPerChain: map[uint64]*big.Int{ + sourceChainSelector + 1: big.NewInt(2e18), + sourceChainSelector + 2: big.NewInt(3e18), + }, + Interval: cciptypes.CommitStoreInterval{ + Min: 54, + Max: 55, + }, + }, + }, + { + name: "base report with price reporting disabled", + commitStoreSeqNum: 54, + gasPrices: map[uint64]*big.Int{ + sourceChainSelector: big.NewInt(2e18), + }, + tokenPrices: expectedTokenPrice, + sendReqs: []cciptypes.EVM2EVMMessageWithTxMeta{ + {EVM2EVMMessage: cciptypes.EVM2EVMMessage{SequenceNumber: 54}}, + {EVM2EVMMessage: cciptypes.EVM2EVMMessage{SequenceNumber: 55}}, + }, + priceReportingDisabled: true, + expObs: ccip.CommitObservation{ + TokenPricesUSD: map[cciptypes.Address]*big.Int{}, + SourceGasPriceUSD: nil, + SourceGasPriceUSDPerChain: map[uint64]*big.Int{}, Interval: cciptypes.CommitStoreInterval{ Min: 54, Max: 55, @@ -146,20 +183,9 @@ func TestCommitReportingPlugin_Observation(t *testing.T) { } mockPriceService := ccipdbmocks.NewPriceService(t) - var priceServiceGasResult map[uint64]*big.Int - var priceServiceTokenResult map[cciptypes.Address]*big.Int - if tc.fee != nil { - pUSD := ccipcalc.CalculateUsdPerUnitGas(tc.fee, tc.tokenPrices[sourceNativeTokenAddr]) - priceServiceGasResult = map[uint64]*big.Int{ - sourceChainSelector: pUSD, - } - } - if len(tc.tokenPrices) > 0 { - priceServiceTokenResult = expectedEncodedTokenPrice - } mockPriceService.On("GetGasAndTokenPrices", ctx, destChainSelector).Return( - priceServiceGasResult, - priceServiceTokenResult, + tc.gasPrices, + tc.tokenPrices, nil, ).Maybe() @@ -173,6 +199,9 @@ func TestCommitReportingPlugin_Observation(t *testing.T) { p.priceService = mockPriceService p.destChainSelector = destChainSelector p.sourceChainSelector = sourceChainSelector + p.offchainConfig = cciptypes.CommitOffchainConfig{ + PriceReportingDisabled: tc.priceReportingDisabled, + } obs, err := p.Observation(ctx, tc.epochAndRound, types.Query{}) @@ -203,6 +232,7 @@ func TestCommitReportingPlugin_Report(t *testing.T) { ctx := testutils.Context(t) sourceChainSelector := uint64(rand.Int()) var gasPrice = big.NewInt(1) + var gasPrice2 = big.NewInt(2) gasPriceHeartBeat := *config.MustNewDuration(time.Hour) t.Run("not enough observations", func(t *testing.T) { @@ -241,8 +271,8 @@ func TestCommitReportingPlugin_Report(t *testing.T) { { name: "base", observations: []ccip.CommitObservation{ - {Interval: cciptypes.CommitStoreInterval{Min: 1, Max: 1}, SourceGasPriceUSD: gasPrice}, - {Interval: cciptypes.CommitStoreInterval{Min: 1, Max: 1}, SourceGasPriceUSD: gasPrice}, + {Interval: cciptypes.CommitStoreInterval{Min: 1, Max: 1}, SourceGasPriceUSDPerChain: map[uint64]*big.Int{sourceChainSelector: gasPrice}}, + {Interval: cciptypes.CommitStoreInterval{Min: 1, Max: 1}, SourceGasPriceUSDPerChain: map[uint64]*big.Int{sourceChainSelector: gasPrice}}, }, f: 1, sendRequests: []cciptypes.EVM2EVMMessageWithTxMeta{ @@ -272,6 +302,68 @@ func TestCommitReportingPlugin_Report(t *testing.T) { }, expErr: false, }, + { + name: "observations with mix gas price formats", + observations: []ccip.CommitObservation{ + { + Interval: cciptypes.CommitStoreInterval{Min: 1, Max: 1}, + SourceGasPriceUSDPerChain: map[uint64]*big.Int{ + sourceChainSelector: gasPrice, + sourceChainSelector + 1: gasPrice2, + sourceChainSelector + 2: gasPrice2, + }, + }, + { + Interval: cciptypes.CommitStoreInterval{Min: 1, Max: 1}, + SourceGasPriceUSDPerChain: map[uint64]*big.Int{ + sourceChainSelector: gasPrice, + sourceChainSelector + 1: gasPrice2, + sourceChainSelector + 2: gasPrice2, + }, + }, + { + Interval: cciptypes.CommitStoreInterval{Min: 1, Max: 1}, + SourceGasPriceUSDPerChain: map[uint64]*big.Int{ + sourceChainSelector: gasPrice, + sourceChainSelector + 1: gasPrice2, + }, + }, + { + Interval: cciptypes.CommitStoreInterval{Min: 1, Max: 1}, + SourceGasPriceUSD: gasPrice, + }, + }, + f: 2, + sendRequests: []cciptypes.EVM2EVMMessageWithTxMeta{ + { + EVM2EVMMessage: cciptypes.EVM2EVMMessage{ + SequenceNumber: 1, + }, + }, + }, + gasPriceUpdates: []cciptypes.GasPriceUpdateWithTxMeta{ + { + GasPriceUpdate: cciptypes.GasPriceUpdate{ + GasPrice: cciptypes.GasPrice{ + DestChainSelector: sourceChainSelector, + Value: big.NewInt(1), + }, + TimestampUnixSec: big.NewInt(time.Now().Add(-2 * gasPriceHeartBeat.Duration()).Unix()), + }, + }, + }, + expSeqNumRange: cciptypes.CommitStoreInterval{Min: 1, Max: 1}, + expCommitReport: &cciptypes.CommitStoreReport{ + MerkleRoot: [32]byte{}, + Interval: cciptypes.CommitStoreInterval{Min: 1, Max: 1}, + TokenPrices: nil, + GasPrices: []cciptypes.GasPrice{ + {DestChainSelector: sourceChainSelector, Value: gasPrice}, + {DestChainSelector: sourceChainSelector + 1, Value: gasPrice2}, + }, + }, + expErr: false, + }, { name: "empty", observations: []ccip.CommitObservation{ @@ -283,7 +375,7 @@ func TestCommitReportingPlugin_Report(t *testing.T) { GasPriceUpdate: cciptypes.GasPriceUpdate{ GasPrice: cciptypes.GasPrice{ DestChainSelector: sourceChainSelector, - Value: big.NewInt(1), + Value: big.NewInt(0), }, TimestampUnixSec: big.NewInt(time.Now().Add(-gasPriceHeartBeat.Duration() / 2).Unix()), }, @@ -308,7 +400,7 @@ func TestCommitReportingPlugin_Report(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { destPriceRegistryReader := ccipdatamocks.NewPriceRegistryReader(t) - destPriceRegistryReader.On("GetGasPriceUpdatesCreatedAfter", ctx, sourceChainSelector, mock.Anything, 0).Return(tc.gasPriceUpdates, nil) + destPriceRegistryReader.On("GetAllGasPriceUpdatesCreatedAfter", ctx, mock.Anything, 0).Return(tc.gasPriceUpdates, nil) destPriceRegistryReader.On("GetTokenPriceUpdatesCreatedAfter", ctx, mock.Anything, 0).Return(tc.tokenPriceUpdates, nil) onRampReader := ccipdatamocks.NewOnRampReader(t) @@ -316,13 +408,11 @@ func TestCommitReportingPlugin_Report(t *testing.T) { onRampReader.On("GetSendRequestsBetweenSeqNums", ctx, tc.expSeqNumRange.Min, tc.expSeqNumRange.Max, true).Return(tc.sendRequests, nil) } - gasPriceEstimator := prices.NewMockGasPriceEstimatorCommit(t) - gasPriceEstimator.On("Median", mock.Anything).Return(gasPrice, nil) - if tc.gasPriceUpdates != nil { - gasPriceEstimator.On("Deviates", mock.Anything, mock.Anything, mock.Anything).Return(false, nil) - } + evmEstimator := mocks.NewEvmFeeEstimator(t) + evmEstimator.On("L1Oracle").Return(nil) + gasPriceEstimator := prices.NewDAGasPriceEstimator(evmEstimator, nil, 2e9, 2e9) // 200% deviation - destTokens := []cciptypes.Address{} + var destTokens []cciptypes.Address for tk := range tc.tokenDecimals { destTokens = append(destTokens, tk) } @@ -575,68 +665,52 @@ func TestCommitReportingPlugin_observePriceUpdates(t *testing.T) { token1 := ccipcalc.HexToAddress("0x123") token2 := ccipcalc.HexToAddress("0x234") - gasPrice := big.NewInt(1e18) + gasPrices := map[uint64]*big.Int{ + sourceChainSelector: big.NewInt(1e18), + } tokenPrices := map[cciptypes.Address]*big.Int{ token1: big.NewInt(2e18), token2: big.NewInt(3e18), } testCases := []struct { - name string - psGasPricesResult map[uint64]*big.Int - psTokenPricesResult map[cciptypes.Address]*big.Int + name string + psGasPricesResult map[uint64]*big.Int + psTokenPricesResult map[cciptypes.Address]*big.Int + PriceReportingDisabled bool - expectedGasPrice *big.Int + expectedGasPrice map[uint64]*big.Int expectedTokenPrices map[cciptypes.Address]*big.Int psError bool expectedErr bool }{ { - name: "ORM called successfully", - psGasPricesResult: map[uint64]*big.Int{ - sourceChainSelector: gasPrice, - }, + name: "ORM called successfully", + psGasPricesResult: gasPrices, psTokenPricesResult: tokenPrices, - expectedGasPrice: gasPrice, + expectedGasPrice: gasPrices, expectedTokenPrices: tokenPrices, - psError: false, - expectedErr: false, - }, - { - name: "multiple gas prices", - psGasPricesResult: map[uint64]*big.Int{ - sourceChainSelector: gasPrice, - sourceChainSelector + 1: big.NewInt(200), - sourceChainSelector + 2: big.NewInt(300), - }, - psTokenPricesResult: map[cciptypes.Address]*big.Int{}, - expectedGasPrice: gasPrice, - expectedTokenPrices: map[cciptypes.Address]*big.Int{}, - psError: false, - expectedErr: false, }, { - name: "mismatched gas prices errors", - psGasPricesResult: map[uint64]*big.Int{ - sourceChainSelector + 2: big.NewInt(300), - }, - psTokenPricesResult: map[cciptypes.Address]*big.Int{}, - psError: false, - expectedErr: true, + name: "price reporting disabled", + psGasPricesResult: gasPrices, + psTokenPricesResult: tokenPrices, + PriceReportingDisabled: true, + expectedGasPrice: map[uint64]*big.Int{}, + expectedTokenPrices: map[cciptypes.Address]*big.Int{}, + psError: false, + expectedErr: false, }, { - name: "empty gas prices errors", + name: "price service error", psGasPricesResult: map[uint64]*big.Int{}, psTokenPricesResult: map[cciptypes.Address]*big.Int{}, - psError: false, + expectedGasPrice: nil, + expectedTokenPrices: nil, + psError: true, expectedErr: true, }, - { - name: "price service failed", - psError: true, - expectedErr: true, - }, } for _, tc := range testCases { @@ -655,29 +729,45 @@ func TestCommitReportingPlugin_observePriceUpdates(t *testing.T) { ).Maybe() p := &CommitReportingPlugin{ + lggr: logger.TestLogger(t), destChainSelector: destChainSelector, sourceChainSelector: sourceChainSelector, priceService: mockPriceService, + offchainConfig: cciptypes.CommitOffchainConfig{ + PriceReportingDisabled: tc.PriceReportingDisabled, + }, } - sourceGasPriceUSD, tokenPricesUSD, err := p.observePriceUpdates(ctx) + gasPricesUSD, sourceGasPriceUSD, tokenPricesUSD, err := p.observePriceUpdates(ctx) if tc.expectedErr { assert.Error(t, err) } else { assert.NoError(t, err) - assert.Equal(t, tc.expectedGasPrice, sourceGasPriceUSD) + assert.Equal(t, tc.expectedGasPrice, gasPricesUSD) assert.Equal(t, tc.expectedTokenPrices, tokenPricesUSD) + if tc.expectedGasPrice != nil { + assert.Equal(t, tc.expectedGasPrice[sourceChainSelector], sourceGasPriceUSD) + } } }) } } +type CommitObservationLegacy struct { + Interval cciptypes.CommitStoreInterval `json:"interval"` + TokenPricesUSD map[cciptypes.Address]*big.Int `json:"tokensPerFeeCoin"` + SourceGasPriceUSD *big.Int `json:"sourceGasPrice"` +} + func TestCommitReportingPlugin_extractObservationData(t *testing.T) { token1 := ccipcalc.HexToAddress("0xa") token2 := ccipcalc.HexToAddress("0xb") token1Price := big.NewInt(1) token2Price := big.NewInt(2) unsupportedToken := ccipcalc.HexToAddress("0xc") - gasPrice := big.NewInt(100) + gasPrice1 := big.NewInt(100) + gasPrice2 := big.NewInt(100) + var sourceChainSelector1 uint64 = 10 + var sourceChainSelector2 uint64 = 20 tokenDecimals := make(map[cciptypes.Address]uint8) tokenDecimals[token1] = 18 @@ -686,24 +776,41 @@ func TestCommitReportingPlugin_extractObservationData(t *testing.T) { validInterval := cciptypes.CommitStoreInterval{Min: 1, Max: 2} zeroInterval := cciptypes.CommitStoreInterval{Min: 0, Max: 0} - ob1 := ccip.CommitObservation{ + // mix legacy commit observations with new commit observations to ensure they can work together + legacyObsRaw := CommitObservationLegacy{ + Interval: validInterval, + TokenPricesUSD: map[cciptypes.Address]*big.Int{ + token1: token1Price, + token2: token2Price, + }, + SourceGasPriceUSD: gasPrice1, + } + legacyObsBytes, err := json.Marshal(&legacyObsRaw) + assert.NoError(t, err) + + newObsRaw := ccip.CommitObservation{ Interval: validInterval, TokenPricesUSD: map[cciptypes.Address]*big.Int{ token1: token1Price, token2: token2Price, }, - SourceGasPriceUSD: gasPrice, + SourceGasPriceUSD: gasPrice1, + SourceGasPriceUSDPerChain: map[uint64]*big.Int{ + sourceChainSelector1: gasPrice1, + sourceChainSelector2: gasPrice2, + }, } - ob1Bytes, err := ob1.Marshal() + newObsBytes, err := newObsRaw.Marshal() assert.NoError(t, err) + lggr := logger.TestLogger(t) observations := ccip.GetParsableObservations[ccip.CommitObservation](lggr, []types.AttributedObservation{ - {Observation: ob1Bytes}, - {Observation: ob1Bytes}, + {Observation: legacyObsBytes}, + {Observation: newObsBytes}, }) assert.Len(t, observations, 2) - ob2 := observations[0] - ob3 := observations[1] + legacyObs := observations[0] + newObs := observations[1] obWithNilGasPrice := ccip.CommitObservation{ Interval: zeroInterval, @@ -711,7 +818,8 @@ func TestCommitReportingPlugin_extractObservationData(t *testing.T) { token1: token1Price, token2: token2Price, }, - SourceGasPriceUSD: nil, + SourceGasPriceUSD: nil, + SourceGasPriceUSDPerChain: map[uint64]*big.Int{}, } obWithNilTokenPrice := ccip.CommitObservation{ Interval: zeroInterval, @@ -719,12 +827,20 @@ func TestCommitReportingPlugin_extractObservationData(t *testing.T) { token1: token1Price, token2: nil, }, - SourceGasPriceUSD: gasPrice, + SourceGasPriceUSD: gasPrice1, + SourceGasPriceUSDPerChain: map[uint64]*big.Int{ + sourceChainSelector1: gasPrice1, + sourceChainSelector2: gasPrice2, + }, } obMissingTokenPrices := ccip.CommitObservation{ Interval: zeroInterval, TokenPricesUSD: map[cciptypes.Address]*big.Int{}, - SourceGasPriceUSD: gasPrice, + SourceGasPriceUSD: gasPrice1, + SourceGasPriceUSDPerChain: map[uint64]*big.Int{ + sourceChainSelector1: gasPrice1, + sourceChainSelector2: gasPrice2, + }, } obWithUnsupportedToken := ccip.CommitObservation{ Interval: zeroInterval, @@ -733,12 +849,17 @@ func TestCommitReportingPlugin_extractObservationData(t *testing.T) { token2: token2Price, unsupportedToken: token2Price, }, - SourceGasPriceUSD: gasPrice, + SourceGasPriceUSD: gasPrice1, + SourceGasPriceUSDPerChain: map[uint64]*big.Int{ + sourceChainSelector1: gasPrice1, + sourceChainSelector2: gasPrice2, + }, } obEmpty := ccip.CommitObservation{ - Interval: zeroInterval, - TokenPricesUSD: nil, - SourceGasPriceUSD: nil, + Interval: zeroInterval, + TokenPricesUSD: nil, + SourceGasPriceUSD: nil, + SourceGasPriceUSDPerChain: nil, } testCases := []struct { @@ -746,41 +867,62 @@ func TestCommitReportingPlugin_extractObservationData(t *testing.T) { commitObservations []ccip.CommitObservation f int expIntervals []cciptypes.CommitStoreInterval - expGasPriceObs []*big.Int + expGasPriceObs map[uint64][]*big.Int expTokenPriceObs map[cciptypes.Address][]*big.Int - expValidObs []ccip.CommitObservation expError bool }{ { name: "base", - commitObservations: []ccip.CommitObservation{ob1, ob2}, - f: 1, - expIntervals: []cciptypes.CommitStoreInterval{validInterval, validInterval}, - expGasPriceObs: []*big.Int{ob1.SourceGasPriceUSD, ob2.SourceGasPriceUSD}, + commitObservations: []ccip.CommitObservation{newObs, newObs, newObs}, + f: 2, + expIntervals: []cciptypes.CommitStoreInterval{validInterval, validInterval, validInterval}, + expGasPriceObs: map[uint64][]*big.Int{ + sourceChainSelector1: {gasPrice1, gasPrice1, gasPrice1}, + sourceChainSelector2: {gasPrice2, gasPrice2, gasPrice2}, + }, expTokenPriceObs: map[cciptypes.Address][]*big.Int{ - token1: {token1Price, token1Price}, - token2: {token2Price, token2Price}, + token1: {token1Price, token1Price, token1Price}, + token2: {token2Price, token2Price, token2Price}, }, expError: false, }, { - name: "pass with f=2", - commitObservations: []ccip.CommitObservation{ob1, ob2, ob3}, + name: "pass with f=2 and mixed observations", + commitObservations: []ccip.CommitObservation{legacyObs, newObs, legacyObs, newObs, newObs, obWithNilGasPrice}, f: 2, - expIntervals: []cciptypes.CommitStoreInterval{validInterval, validInterval, validInterval}, - expGasPriceObs: []*big.Int{ob1.SourceGasPriceUSD, ob2.SourceGasPriceUSD, ob3.SourceGasPriceUSD}, + expIntervals: []cciptypes.CommitStoreInterval{validInterval, validInterval, validInterval, validInterval, validInterval, zeroInterval}, + expGasPriceObs: map[uint64][]*big.Int{ + sourceChainSelector1: {gasPrice1, gasPrice1, gasPrice1, gasPrice1, gasPrice1}, + sourceChainSelector2: {gasPrice2, gasPrice2, gasPrice2}, + }, expTokenPriceObs: map[cciptypes.Address][]*big.Int{ - token1: {token1Price, token1Price, token1Price}, - token2: {token2Price, token2Price, token2Price}, + token1: {token1Price, token1Price, token1Price, token1Price, token1Price, token1Price}, + token2: {token2Price, token2Price, token2Price, token2Price, token2Price, token2Price}, + }, + expError: false, + }, + { + name: "pass with f=2 and mixed observations with mostly legacy observations", + commitObservations: []ccip.CommitObservation{legacyObs, legacyObs, legacyObs, legacyObs, newObs}, + f: 2, + expIntervals: []cciptypes.CommitStoreInterval{validInterval, validInterval, validInterval, validInterval, validInterval}, + expGasPriceObs: map[uint64][]*big.Int{ + sourceChainSelector1: {gasPrice1, gasPrice1, gasPrice1, gasPrice1, gasPrice1}, + }, + expTokenPriceObs: map[cciptypes.Address][]*big.Int{ + token1: {token1Price, token1Price, token1Price, token1Price, token1Price}, + token2: {token2Price, token2Price, token2Price, token2Price, token2Price}, }, expError: false, }, { name: "tolerate 1 faulty obs with f=2", - commitObservations: []ccip.CommitObservation{ob1, ob2, ob3, obWithNilGasPrice}, + commitObservations: []ccip.CommitObservation{legacyObs, newObs, legacyObs, obWithNilGasPrice}, f: 2, expIntervals: []cciptypes.CommitStoreInterval{validInterval, validInterval, validInterval, zeroInterval}, - expGasPriceObs: []*big.Int{ob1.SourceGasPriceUSD, ob2.SourceGasPriceUSD, ob3.SourceGasPriceUSD}, + expGasPriceObs: map[uint64][]*big.Int{ + sourceChainSelector1: {gasPrice1, gasPrice1, gasPrice1}, + }, expTokenPriceObs: map[cciptypes.Address][]*big.Int{ token1: {token1Price, token1Price, token1Price, token1Price}, token2: {token2Price, token2Price, token2Price, token2Price}, @@ -789,10 +931,13 @@ func TestCommitReportingPlugin_extractObservationData(t *testing.T) { }, { name: "tolerate 1 nil token price with f=1", - commitObservations: []ccip.CommitObservation{ob1, ob2, obWithNilTokenPrice}, + commitObservations: []ccip.CommitObservation{legacyObs, newObs, obWithNilTokenPrice}, f: 1, expIntervals: []cciptypes.CommitStoreInterval{validInterval, validInterval, zeroInterval}, - expGasPriceObs: []*big.Int{ob1.SourceGasPriceUSD, ob2.SourceGasPriceUSD, obWithNilTokenPrice.SourceGasPriceUSD}, + expGasPriceObs: map[uint64][]*big.Int{ + sourceChainSelector1: {gasPrice1, gasPrice1, gasPrice1}, + sourceChainSelector2: {gasPrice2, gasPrice2}, + }, expTokenPriceObs: map[cciptypes.Address][]*big.Int{ token1: {token1Price, token1Price, token1Price}, token2: {token2Price, token2Price}, @@ -801,10 +946,13 @@ func TestCommitReportingPlugin_extractObservationData(t *testing.T) { }, { name: "tolerate 1 missing token prices with f=1", - commitObservations: []ccip.CommitObservation{ob1, ob2, obMissingTokenPrices}, + commitObservations: []ccip.CommitObservation{legacyObs, newObs, obMissingTokenPrices}, f: 1, expIntervals: []cciptypes.CommitStoreInterval{validInterval, validInterval, zeroInterval}, - expGasPriceObs: []*big.Int{ob1.SourceGasPriceUSD, ob2.SourceGasPriceUSD, obMissingTokenPrices.SourceGasPriceUSD}, + expGasPriceObs: map[uint64][]*big.Int{ + sourceChainSelector1: {gasPrice1, gasPrice1, gasPrice1}, + sourceChainSelector2: {gasPrice2, gasPrice2}, + }, expTokenPriceObs: map[cciptypes.Address][]*big.Int{ token1: {token1Price, token1Price}, token2: {token2Price, token2Price}, @@ -813,10 +961,12 @@ func TestCommitReportingPlugin_extractObservationData(t *testing.T) { }, { name: "tolerate 1 unsupported token with f=2", - commitObservations: []ccip.CommitObservation{ob1, ob2, obWithUnsupportedToken}, + commitObservations: []ccip.CommitObservation{legacyObs, newObs, obWithUnsupportedToken}, f: 2, expIntervals: []cciptypes.CommitStoreInterval{validInterval, validInterval, zeroInterval}, - expGasPriceObs: []*big.Int{ob1.SourceGasPriceUSD, ob2.SourceGasPriceUSD, obWithUnsupportedToken.SourceGasPriceUSD}, + expGasPriceObs: map[uint64][]*big.Int{ + sourceChainSelector1: {gasPrice1, gasPrice1, gasPrice1}, + }, expTokenPriceObs: map[cciptypes.Address][]*big.Int{ token1: {token1Price, token1Price, token1Price}, token2: {token2Price, token2Price, token2Price}, @@ -825,21 +975,13 @@ func TestCommitReportingPlugin_extractObservationData(t *testing.T) { }, { name: "tolerate mis-matched token observations with f=2", - commitObservations: []ccip.CommitObservation{ob1, ob2, obWithNilTokenPrice, obMissingTokenPrices}, + commitObservations: []ccip.CommitObservation{legacyObs, newObs, obWithNilTokenPrice, obMissingTokenPrices}, f: 2, expIntervals: []cciptypes.CommitStoreInterval{validInterval, validInterval, zeroInterval, zeroInterval}, - expGasPriceObs: []*big.Int{ob1.SourceGasPriceUSD, ob2.SourceGasPriceUSD, obWithNilTokenPrice.SourceGasPriceUSD, obMissingTokenPrices.SourceGasPriceUSD}, - expTokenPriceObs: map[cciptypes.Address][]*big.Int{ - token1: {token1Price, token1Price, token1Price}, + expGasPriceObs: map[uint64][]*big.Int{ + sourceChainSelector1: {gasPrice1, gasPrice1, gasPrice1, gasPrice1}, + sourceChainSelector2: {gasPrice2, gasPrice2, gasPrice2}, }, - expError: false, - }, - { - name: "tolerate mis-matched token observations with f=2", - commitObservations: []ccip.CommitObservation{ob1, obWithNilTokenPrice, obWithNilTokenPrice}, - f: 2, - expIntervals: []cciptypes.CommitStoreInterval{validInterval, zeroInterval, zeroInterval}, - expGasPriceObs: []*big.Int{ob1.SourceGasPriceUSD, obWithNilTokenPrice.SourceGasPriceUSD, obWithNilTokenPrice.SourceGasPriceUSD}, expTokenPriceObs: map[cciptypes.Address][]*big.Int{ token1: {token1Price, token1Price, token1Price}, }, @@ -847,32 +989,36 @@ func TestCommitReportingPlugin_extractObservationData(t *testing.T) { }, { name: "tolerate all tokens filtered out with f=2", - commitObservations: []ccip.CommitObservation{ob1, obMissingTokenPrices, obMissingTokenPrices}, + commitObservations: []ccip.CommitObservation{newObs, obMissingTokenPrices, obMissingTokenPrices}, f: 2, expIntervals: []cciptypes.CommitStoreInterval{validInterval, zeroInterval, zeroInterval}, - expGasPriceObs: []*big.Int{ob1.SourceGasPriceUSD, obMissingTokenPrices.SourceGasPriceUSD, obMissingTokenPrices.SourceGasPriceUSD}, - expTokenPriceObs: map[cciptypes.Address][]*big.Int{}, - expError: false, + expGasPriceObs: map[uint64][]*big.Int{ + sourceChainSelector1: {gasPrice1, gasPrice1, gasPrice1}, + sourceChainSelector2: {gasPrice2, gasPrice2, gasPrice2}, + }, + expTokenPriceObs: map[cciptypes.Address][]*big.Int{}, + expError: false, }, { name: "not enough observations", - commitObservations: []ccip.CommitObservation{ob1, ob2}, + commitObservations: []ccip.CommitObservation{legacyObs, newObs}, f: 2, - expValidObs: nil, expError: true, }, { - name: "too many faulty observations", + name: "too many empty observations", commitObservations: []ccip.CommitObservation{obWithNilGasPrice, obWithNilTokenPrice, obEmpty, obEmpty, obEmpty}, - f: 1, - expValidObs: nil, - expError: true, + f: 2, + expIntervals: []cciptypes.CommitStoreInterval{zeroInterval, zeroInterval, zeroInterval, zeroInterval, zeroInterval}, + expGasPriceObs: map[uint64][]*big.Int{}, + expTokenPriceObs: map[cciptypes.Address][]*big.Int{}, + expError: false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - intervals, gasPriceOps, tokenPriceOps, err := extractObservationData(logger.TestLogger(t), tc.f, tc.commitObservations) + intervals, gasPriceOps, tokenPriceOps, err := extractObservationData(logger.TestLogger(t), tc.f, sourceChainSelector1, tc.commitObservations) if tc.expError { assert.Error(t, err) @@ -897,7 +1043,7 @@ func TestCommitReportingPlugin_calculatePriceUpdates(t *testing.T) { name string commitObservations []ccip.CommitObservation f int - latestGasPrice update + latestGasPrice map[uint64]update latestTokenPrices map[cciptypes.Address]update gasPriceHeartBeat config.Duration daGasPriceDeviationPPB int64 @@ -910,14 +1056,16 @@ func TestCommitReportingPlugin_calculatePriceUpdates(t *testing.T) { { name: "median", commitObservations: []ccip.CommitObservation{ - {SourceGasPriceUSD: big.NewInt(1)}, - {SourceGasPriceUSD: big.NewInt(2)}, - {SourceGasPriceUSD: big.NewInt(3)}, - {SourceGasPriceUSD: big.NewInt(4)}, + {SourceGasPriceUSDPerChain: map[uint64]*big.Int{defaultSourceChainSelector: big.NewInt(1)}}, + {SourceGasPriceUSDPerChain: map[uint64]*big.Int{defaultSourceChainSelector: big.NewInt(2)}}, + {SourceGasPriceUSDPerChain: map[uint64]*big.Int{defaultSourceChainSelector: big.NewInt(3)}}, + {SourceGasPriceUSDPerChain: map[uint64]*big.Int{defaultSourceChainSelector: big.NewInt(4)}}, }, - latestGasPrice: update{ - timestamp: time.Now().Add(-30 * time.Minute), // recent - value: val1e18(9), // median deviates + latestGasPrice: map[uint64]update{ + defaultSourceChainSelector: { + timestamp: time.Now().Add(-30 * time.Minute), // recent + value: val1e18(9), // median deviates + }, }, f: 2, expGasUpdates: []cciptypes.GasPrice{{DestChainSelector: defaultSourceChainSelector, Value: big.NewInt(3)}}, @@ -925,17 +1073,19 @@ func TestCommitReportingPlugin_calculatePriceUpdates(t *testing.T) { { name: "gas price update skipped because the latest is similar and was updated recently", commitObservations: []ccip.CommitObservation{ - {SourceGasPriceUSD: val1e18(11)}, - {SourceGasPriceUSD: val1e18(12)}, + {SourceGasPriceUSDPerChain: map[uint64]*big.Int{defaultSourceChainSelector: val1e18(11)}}, + {SourceGasPriceUSDPerChain: map[uint64]*big.Int{defaultSourceChainSelector: val1e18(12)}}, }, gasPriceHeartBeat: *config.MustNewDuration(time.Hour), daGasPriceDeviationPPB: 20e7, execGasPriceDeviationPPB: 20e7, tokenPriceHeartBeat: *config.MustNewDuration(time.Hour), tokenPriceDeviationPPB: 20e7, - latestGasPrice: update{ - timestamp: time.Now().Add(-30 * time.Minute), // recent - value: val1e18(10), // latest value close to the update + latestGasPrice: map[uint64]update{ + defaultSourceChainSelector: { + timestamp: time.Now().Add(-30 * time.Minute), // recent + value: val1e18(10), // median deviates + }, }, f: 1, expGasUpdates: nil, @@ -943,17 +1093,19 @@ func TestCommitReportingPlugin_calculatePriceUpdates(t *testing.T) { { name: "gas price update included, the latest is similar but was not updated recently", commitObservations: []ccip.CommitObservation{ - {SourceGasPriceUSD: val1e18(10)}, - {SourceGasPriceUSD: val1e18(11)}, + {SourceGasPriceUSDPerChain: map[uint64]*big.Int{defaultSourceChainSelector: val1e18(10)}}, + {SourceGasPriceUSDPerChain: map[uint64]*big.Int{defaultSourceChainSelector: val1e18(11)}}, }, gasPriceHeartBeat: *config.MustNewDuration(time.Hour), daGasPriceDeviationPPB: 20e7, execGasPriceDeviationPPB: 20e7, tokenPriceHeartBeat: *config.MustNewDuration(time.Hour), tokenPriceDeviationPPB: 20e7, - latestGasPrice: update{ - timestamp: time.Now().Add(-90 * time.Minute), // recent - value: val1e18(9), // latest value close to the update + latestGasPrice: map[uint64]update{ + defaultSourceChainSelector: { + timestamp: time.Now().Add(-90 * time.Minute), // stale + value: val1e18(9), // median deviates + }, }, f: 1, expGasUpdates: []cciptypes.GasPrice{{DestChainSelector: defaultSourceChainSelector, Value: val1e18(11)}}, @@ -961,27 +1113,69 @@ func TestCommitReportingPlugin_calculatePriceUpdates(t *testing.T) { { name: "gas price update deviates from latest", commitObservations: []ccip.CommitObservation{ - {SourceGasPriceUSD: val1e18(10)}, - {SourceGasPriceUSD: val1e18(20)}, - {SourceGasPriceUSD: val1e18(20)}, + {SourceGasPriceUSDPerChain: map[uint64]*big.Int{defaultSourceChainSelector: val1e18(10)}}, + {SourceGasPriceUSDPerChain: map[uint64]*big.Int{defaultSourceChainSelector: val1e18(20)}}, + {SourceGasPriceUSDPerChain: map[uint64]*big.Int{defaultSourceChainSelector: val1e18(20)}}, }, gasPriceHeartBeat: *config.MustNewDuration(time.Hour), daGasPriceDeviationPPB: 20e7, execGasPriceDeviationPPB: 20e7, tokenPriceHeartBeat: *config.MustNewDuration(time.Hour), tokenPriceDeviationPPB: 20e7, - latestGasPrice: update{ - timestamp: time.Now().Add(-30 * time.Minute), // recent - value: val1e18(11), // latest value close to the update + latestGasPrice: map[uint64]update{ + defaultSourceChainSelector: { + timestamp: time.Now().Add(-30 * time.Minute), // recent + value: val1e18(11), // latest value close to the update + }, }, f: 2, expGasUpdates: []cciptypes.GasPrice{{DestChainSelector: defaultSourceChainSelector, Value: val1e18(20)}}, }, + { + name: "multichain gas prices", + commitObservations: []ccip.CommitObservation{ + {SourceGasPriceUSDPerChain: map[uint64]*big.Int{defaultSourceChainSelector: val1e18(1)}}, + {SourceGasPriceUSDPerChain: map[uint64]*big.Int{defaultSourceChainSelector + 1: val1e18(11)}}, + {SourceGasPriceUSDPerChain: map[uint64]*big.Int{defaultSourceChainSelector + 2: val1e18(111)}}, + {SourceGasPriceUSDPerChain: map[uint64]*big.Int{defaultSourceChainSelector: val1e18(2)}}, + {SourceGasPriceUSDPerChain: map[uint64]*big.Int{defaultSourceChainSelector + 1: val1e18(22)}}, + {SourceGasPriceUSDPerChain: map[uint64]*big.Int{defaultSourceChainSelector + 2: val1e18(222)}}, + {SourceGasPriceUSDPerChain: map[uint64]*big.Int{defaultSourceChainSelector: val1e18(3)}}, + {SourceGasPriceUSDPerChain: map[uint64]*big.Int{defaultSourceChainSelector + 1: val1e18(33)}}, + {SourceGasPriceUSDPerChain: map[uint64]*big.Int{defaultSourceChainSelector + 2: val1e18(333)}}, + }, + gasPriceHeartBeat: *config.MustNewDuration(time.Hour), + daGasPriceDeviationPPB: 20e7, + execGasPriceDeviationPPB: 20e7, + tokenPriceHeartBeat: *config.MustNewDuration(time.Hour), + tokenPriceDeviationPPB: 20e7, + latestGasPrice: map[uint64]update{ + defaultSourceChainSelector: { + timestamp: time.Now().Add(-90 * time.Minute), // stale + value: val1e18(9), // median deviates + }, + defaultSourceChainSelector + 1: { + timestamp: time.Now().Add(-30 * time.Minute), // recent + value: val1e18(20), // median does not deviate + }, + }, + f: 1, + expGasUpdates: []cciptypes.GasPrice{ + {DestChainSelector: defaultSourceChainSelector, Value: val1e18(2)}, + {DestChainSelector: defaultSourceChainSelector + 2, Value: val1e18(222)}, + }, + }, { name: "median one token", commitObservations: []ccip.CommitObservation{ - {TokenPricesUSD: map[cciptypes.Address]*big.Int{feeToken1: big.NewInt(10)}, SourceGasPriceUSD: val1e18(0)}, - {TokenPricesUSD: map[cciptypes.Address]*big.Int{feeToken1: big.NewInt(12)}, SourceGasPriceUSD: val1e18(0)}, + { + TokenPricesUSD: map[cciptypes.Address]*big.Int{feeToken1: big.NewInt(10)}, + SourceGasPriceUSDPerChain: map[uint64]*big.Int{defaultSourceChainSelector: val1e18(0)}, + }, + { + TokenPricesUSD: map[cciptypes.Address]*big.Int{feeToken1: big.NewInt(12)}, + SourceGasPriceUSDPerChain: map[uint64]*big.Int{defaultSourceChainSelector: val1e18(0)}, + }, }, f: 1, expTokenUpdates: []cciptypes.TokenPrice{ @@ -993,8 +1187,14 @@ func TestCommitReportingPlugin_calculatePriceUpdates(t *testing.T) { { name: "median two tokens", commitObservations: []ccip.CommitObservation{ - {TokenPricesUSD: map[cciptypes.Address]*big.Int{feeToken1: big.NewInt(10), feeToken2: big.NewInt(13)}, SourceGasPriceUSD: val1e18(0)}, - {TokenPricesUSD: map[cciptypes.Address]*big.Int{feeToken1: big.NewInt(12), feeToken2: big.NewInt(7)}, SourceGasPriceUSD: val1e18(0)}, + { + TokenPricesUSD: map[cciptypes.Address]*big.Int{feeToken1: big.NewInt(10), feeToken2: big.NewInt(13)}, + SourceGasPriceUSDPerChain: map[uint64]*big.Int{defaultSourceChainSelector: val1e18(0)}, + }, + { + TokenPricesUSD: map[cciptypes.Address]*big.Int{feeToken1: big.NewInt(12), feeToken2: big.NewInt(7)}, + SourceGasPriceUSDPerChain: map[uint64]*big.Int{defaultSourceChainSelector: val1e18(0)}, + }, }, f: 1, expTokenUpdates: []cciptypes.TokenPrice{ @@ -1007,8 +1207,14 @@ func TestCommitReportingPlugin_calculatePriceUpdates(t *testing.T) { { name: "token price update skipped because it is close to the latest", commitObservations: []ccip.CommitObservation{ - {TokenPricesUSD: map[cciptypes.Address]*big.Int{feeToken1: val1e18(11)}, SourceGasPriceUSD: val1e18(0)}, - {TokenPricesUSD: map[cciptypes.Address]*big.Int{feeToken1: val1e18(12)}, SourceGasPriceUSD: val1e18(0)}, + { + TokenPricesUSD: map[cciptypes.Address]*big.Int{feeToken1: val1e18(11)}, + SourceGasPriceUSDPerChain: map[uint64]*big.Int{defaultSourceChainSelector: val1e18(0)}, + }, + { + TokenPricesUSD: map[cciptypes.Address]*big.Int{feeToken1: val1e18(12)}, + SourceGasPriceUSDPerChain: map[uint64]*big.Int{defaultSourceChainSelector: val1e18(0)}, + }, }, f: 1, gasPriceHeartBeat: *config.MustNewDuration(time.Hour), @@ -1028,8 +1234,20 @@ func TestCommitReportingPlugin_calculatePriceUpdates(t *testing.T) { { name: "gas price and token price both included because they are not close to the latest", commitObservations: []ccip.CommitObservation{ - {TokenPricesUSD: map[cciptypes.Address]*big.Int{feeToken1: val1e18(20)}, SourceGasPriceUSD: val1e18(10)}, - {TokenPricesUSD: map[cciptypes.Address]*big.Int{feeToken1: val1e18(21)}, SourceGasPriceUSD: val1e18(11)}, + { + TokenPricesUSD: map[cciptypes.Address]*big.Int{feeToken1: val1e18(20)}, + SourceGasPriceUSDPerChain: map[uint64]*big.Int{ + defaultSourceChainSelector: val1e18(10), + defaultSourceChainSelector + 1: val1e18(20), + }, + }, + { + TokenPricesUSD: map[cciptypes.Address]*big.Int{feeToken1: val1e18(21)}, + SourceGasPriceUSDPerChain: map[uint64]*big.Int{ + defaultSourceChainSelector: val1e18(11), + defaultSourceChainSelector + 1: val1e18(21), + }, + }, }, f: 1, gasPriceHeartBeat: *config.MustNewDuration(time.Hour), @@ -1037,9 +1255,15 @@ func TestCommitReportingPlugin_calculatePriceUpdates(t *testing.T) { execGasPriceDeviationPPB: 10e7, tokenPriceHeartBeat: *config.MustNewDuration(time.Hour), tokenPriceDeviationPPB: 20e7, - latestGasPrice: update{ - timestamp: time.Now().Add(-30 * time.Minute), - value: val1e18(9), + latestGasPrice: map[uint64]update{ + defaultSourceChainSelector: { + timestamp: time.Now().Add(-30 * time.Minute), + value: val1e18(9), + }, + defaultSourceChainSelector + 1: { + timestamp: time.Now().Add(-30 * time.Minute), + value: val1e18(9), + }, }, latestTokenPrices: map[cciptypes.Address]update{ feeToken1: { @@ -1050,13 +1274,28 @@ func TestCommitReportingPlugin_calculatePriceUpdates(t *testing.T) { expTokenUpdates: []cciptypes.TokenPrice{ {Token: feeToken1, Value: val1e18(21)}, }, - expGasUpdates: []cciptypes.GasPrice{{DestChainSelector: defaultSourceChainSelector, Value: val1e18(11)}}, + expGasUpdates: []cciptypes.GasPrice{ + {DestChainSelector: defaultSourceChainSelector, Value: val1e18(11)}, + {DestChainSelector: defaultSourceChainSelector + 1, Value: val1e18(21)}, + }, }, { name: "gas price and token price both included because they not been updated recently", commitObservations: []ccip.CommitObservation{ - {TokenPricesUSD: map[cciptypes.Address]*big.Int{feeToken1: val1e18(20)}, SourceGasPriceUSD: val1e18(10)}, - {TokenPricesUSD: map[cciptypes.Address]*big.Int{feeToken1: val1e18(21)}, SourceGasPriceUSD: val1e18(11)}, + { + TokenPricesUSD: map[cciptypes.Address]*big.Int{feeToken1: val1e18(20)}, + SourceGasPriceUSDPerChain: map[uint64]*big.Int{ + defaultSourceChainSelector: val1e18(10), + defaultSourceChainSelector + 1: val1e18(20), + }, + }, + { + TokenPricesUSD: map[cciptypes.Address]*big.Int{feeToken1: val1e18(21)}, + SourceGasPriceUSDPerChain: map[uint64]*big.Int{ + defaultSourceChainSelector: val1e18(11), + defaultSourceChainSelector + 1: val1e18(21), + }, + }, }, f: 1, gasPriceHeartBeat: *config.MustNewDuration(time.Hour), @@ -1064,9 +1303,15 @@ func TestCommitReportingPlugin_calculatePriceUpdates(t *testing.T) { execGasPriceDeviationPPB: 10e7, tokenPriceHeartBeat: *config.MustNewDuration(2 * time.Hour), tokenPriceDeviationPPB: 20e7, - latestGasPrice: update{ - timestamp: time.Now().Add(-90 * time.Minute), - value: val1e18(11), + latestGasPrice: map[uint64]update{ + defaultSourceChainSelector: { + timestamp: time.Now().Add(-90 * time.Minute), + value: val1e18(11), + }, + defaultSourceChainSelector + 1: { + timestamp: time.Now().Add(-90 * time.Minute), + value: val1e18(21), + }, }, latestTokenPrices: map[cciptypes.Address]update{ feeToken1: { @@ -1077,13 +1322,22 @@ func TestCommitReportingPlugin_calculatePriceUpdates(t *testing.T) { expTokenUpdates: []cciptypes.TokenPrice{ {Token: feeToken1, Value: val1e18(21)}, }, - expGasUpdates: []cciptypes.GasPrice{{DestChainSelector: defaultSourceChainSelector, Value: val1e18(11)}}, + expGasUpdates: []cciptypes.GasPrice{ + {DestChainSelector: defaultSourceChainSelector, Value: val1e18(11)}, + {DestChainSelector: defaultSourceChainSelector + 1, Value: val1e18(21)}, + }, }, { name: "gas price included because it deviates from latest and token price skipped because it does not deviate", commitObservations: []ccip.CommitObservation{ - {TokenPricesUSD: map[cciptypes.Address]*big.Int{feeToken1: val1e18(20)}, SourceGasPriceUSD: val1e18(10)}, - {TokenPricesUSD: map[cciptypes.Address]*big.Int{feeToken1: val1e18(21)}, SourceGasPriceUSD: val1e18(11)}, + { + TokenPricesUSD: map[cciptypes.Address]*big.Int{feeToken1: val1e18(20)}, + SourceGasPriceUSDPerChain: map[uint64]*big.Int{defaultSourceChainSelector: val1e18(10)}, + }, + { + TokenPricesUSD: map[cciptypes.Address]*big.Int{feeToken1: val1e18(21)}, + SourceGasPriceUSDPerChain: map[uint64]*big.Int{defaultSourceChainSelector: val1e18(11)}, + }, }, f: 1, gasPriceHeartBeat: *config.MustNewDuration(time.Hour), @@ -1091,9 +1345,11 @@ func TestCommitReportingPlugin_calculatePriceUpdates(t *testing.T) { execGasPriceDeviationPPB: 10e7, tokenPriceHeartBeat: *config.MustNewDuration(2 * time.Hour), tokenPriceDeviationPPB: 200e7, - latestGasPrice: update{ - timestamp: time.Now().Add(-30 * time.Minute), - value: val1e18(9), + latestGasPrice: map[uint64]update{ + defaultSourceChainSelector: { + timestamp: time.Now().Add(-90 * time.Minute), + value: val1e18(9), + }, }, latestTokenPrices: map[cciptypes.Address]update{ feeToken1: { @@ -1106,8 +1362,14 @@ func TestCommitReportingPlugin_calculatePriceUpdates(t *testing.T) { { name: "gas price skipped because it does not deviate and token price included because it has not been updated recently", commitObservations: []ccip.CommitObservation{ - {TokenPricesUSD: map[cciptypes.Address]*big.Int{feeToken1: val1e18(20)}, SourceGasPriceUSD: val1e18(10)}, - {TokenPricesUSD: map[cciptypes.Address]*big.Int{feeToken1: val1e18(21)}, SourceGasPriceUSD: val1e18(11)}, + { + TokenPricesUSD: map[cciptypes.Address]*big.Int{feeToken1: val1e18(20)}, + SourceGasPriceUSDPerChain: map[uint64]*big.Int{defaultSourceChainSelector: val1e18(10)}, + }, + { + TokenPricesUSD: map[cciptypes.Address]*big.Int{feeToken1: val1e18(21)}, + SourceGasPriceUSDPerChain: map[uint64]*big.Int{defaultSourceChainSelector: val1e18(11)}, + }, }, f: 1, gasPriceHeartBeat: *config.MustNewDuration(time.Hour), @@ -1115,9 +1377,11 @@ func TestCommitReportingPlugin_calculatePriceUpdates(t *testing.T) { execGasPriceDeviationPPB: 10e7, tokenPriceHeartBeat: *config.MustNewDuration(2 * time.Hour), tokenPriceDeviationPPB: 20e7, - latestGasPrice: update{ - timestamp: time.Now().Add(-30 * time.Minute), - value: val1e18(11), + latestGasPrice: map[uint64]update{ + defaultSourceChainSelector: { + timestamp: time.Now().Add(-30 * time.Minute), + value: val1e18(11), + }, }, latestTokenPrices: map[cciptypes.Address]update{ feeToken1: { @@ -1158,10 +1422,12 @@ func TestCommitReportingPlugin_calculatePriceUpdates(t *testing.T) { F: tc.f, } - var gasPriceObs []*big.Int + gasPriceObs := make(map[uint64][]*big.Int) tokenPriceObs := make(map[cciptypes.Address][]*big.Int) for _, obs := range tc.commitObservations { - gasPriceObs = append(gasPriceObs, obs.SourceGasPriceUSD) + for selector, price := range obs.SourceGasPriceUSDPerChain { + gasPriceObs[selector] = append(gasPriceObs[selector], price) + } for token, price := range obs.TokenPricesUSD { tokenPriceObs[token] = append(tokenPriceObs[token], price) } @@ -1320,30 +1586,54 @@ func TestCommitReportingPlugin_calculateMinMaxSequenceNumbers(t *testing.T) { func TestCommitReportingPlugin_getLatestGasPriceUpdate(t *testing.T) { now := time.Now() - chainSelector := uint64(1234) + chainSelector1 := uint64(1234) + chainSelector2 := uint64(5678) + + chain1Value := big.NewInt(1000) + chain2Value := big.NewInt(2000) testCases := []struct { - name string - destGasPriceUpdates []update - expUpdate update - expErr bool + name string + priceRegistryUpdates []cciptypes.GasPriceUpdate + expUpdates map[uint64]update + expErr bool }{ { name: "happy path", - destGasPriceUpdates: []update{ - {timestamp: now, value: big.NewInt(1000)}, + priceRegistryUpdates: []cciptypes.GasPriceUpdate{ + { + GasPrice: cciptypes.GasPrice{DestChainSelector: chainSelector1, Value: chain1Value}, + TimestampUnixSec: big.NewInt(now.Unix()), + }, }, - expUpdate: update{timestamp: now, value: big.NewInt(1000)}, - expErr: false, + expUpdates: map[uint64]update{chainSelector1: {timestamp: now, value: chain1Value}}, + expErr: false, }, { - name: "happy path two updates", - destGasPriceUpdates: []update{ - {timestamp: now.Add(time.Minute), value: big.NewInt(2000)}, - {timestamp: now.Add(2 * time.Minute), value: big.NewInt(3000)}, + name: "happy path multiple updates", + priceRegistryUpdates: []cciptypes.GasPriceUpdate{ + { + GasPrice: cciptypes.GasPrice{DestChainSelector: chainSelector1, Value: big.NewInt(1)}, + TimestampUnixSec: big.NewInt(now.Unix()), + }, + { + GasPrice: cciptypes.GasPrice{DestChainSelector: chainSelector2, Value: big.NewInt(1)}, + TimestampUnixSec: big.NewInt(now.Add(1 * time.Minute).Unix()), + }, + { + GasPrice: cciptypes.GasPrice{DestChainSelector: chainSelector2, Value: chain2Value}, + TimestampUnixSec: big.NewInt(now.Add(2 * time.Minute).Unix()), + }, + { + GasPrice: cciptypes.GasPrice{DestChainSelector: chainSelector1, Value: chain1Value}, + TimestampUnixSec: big.NewInt(now.Add(3 * time.Minute).Unix()), + }, + }, + expUpdates: map[uint64]update{ + chainSelector1: {timestamp: now.Add(3 * time.Minute), value: chain1Value}, + chainSelector2: {timestamp: now.Add(2 * time.Minute), value: chain2Value}, }, - expUpdate: update{timestamp: now.Add(2 * time.Minute), value: big.NewInt(3000)}, - expErr: false, + expErr: false, }, } @@ -1352,35 +1642,30 @@ func TestCommitReportingPlugin_getLatestGasPriceUpdate(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { p := &CommitReportingPlugin{} - p.sourceChainSelector = chainSelector p.lggr = lggr - destPriceRegistry := ccipdatamocks.NewPriceRegistryReader(t) - p.destPriceRegistryReader = destPriceRegistry - - if len(tc.destGasPriceUpdates) > 0 { - var events []cciptypes.GasPriceUpdateWithTxMeta - for _, u := range tc.destGasPriceUpdates { - events = append(events, cciptypes.GasPriceUpdateWithTxMeta{ - GasPriceUpdate: cciptypes.GasPriceUpdate{ - GasPrice: cciptypes.GasPrice{Value: u.value}, - TimestampUnixSec: big.NewInt(u.timestamp.Unix()), - }, - }) - } - destReader := ccipdatamocks.NewPriceRegistryReader(t) - destReader.On("GetGasPriceUpdatesCreatedAfter", ctx, chainSelector, mock.Anything, 0).Return(events, nil) - p.destPriceRegistryReader = destReader + priceReg := ccipdatamocks.NewPriceRegistryReader(t) + p.destPriceRegistryReader = priceReg + + var events []cciptypes.GasPriceUpdateWithTxMeta + for _, update := range tc.priceRegistryUpdates { + events = append(events, cciptypes.GasPriceUpdateWithTxMeta{ + GasPriceUpdate: update, + }) } - priceUpdate, err := p.getLatestGasPriceUpdate(ctx, time.Now()) + priceReg.On("GetAllGasPriceUpdatesCreatedAfter", ctx, mock.Anything, 0).Return(events, nil) + + gotUpdates, err := p.getLatestGasPriceUpdate(ctx, now) if tc.expErr { assert.Error(t, err) return } - assert.NoError(t, err) - assert.Equal(t, tc.expUpdate.timestamp.Truncate(time.Second), priceUpdate.timestamp.Truncate(time.Second)) - assert.Equal(t, tc.expUpdate.value.Uint64(), priceUpdate.value.Uint64()) + assert.Equal(t, len(tc.expUpdates), len(gotUpdates)) + for selector, gotUpdate := range gotUpdates { + assert.Equal(t, tc.expUpdates[selector].timestamp.Truncate(time.Second), gotUpdate.timestamp.Truncate(time.Second)) + assert.Equal(t, tc.expUpdates[selector].value.Uint64(), gotUpdate.value.Uint64()) + } }) } } diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdb/price_service_test.go b/core/services/ocr2/plugins/ccip/internal/ccipdb/price_service_test.go index 5ff9198dde..0bea8af9a1 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdb/price_service_test.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdb/price_service_test.go @@ -233,7 +233,7 @@ func TestPriceService_generatePriceUpdates(t *testing.T) { name: "base", tokenDecimals: map[cciptypes.Address]uint8{ tokens[0]: 18, - tokens[1]: 18, + tokens[1]: 12, }, sourceNativeToken: tokens[0], priceGetterRespData: map[cciptypes.Address]*big.Int{ @@ -248,7 +248,7 @@ func TestPriceService_generatePriceUpdates(t *testing.T) { expSourceGasPriceUSD: big.NewInt(1000), expTokenPricesUSD: map[cciptypes.Address]*big.Int{ tokens[0]: val1e18(100), - tokens[1]: val1e18(200), + tokens[1]: val1e18(200 * 1e6), }, expErr: false, }, @@ -290,29 +290,6 @@ func TestPriceService_generatePriceUpdates(t *testing.T) { priceGetterRespErr: nil, expErr: true, }, - { - name: "base", - tokenDecimals: map[cciptypes.Address]uint8{ - tokens[0]: 18, - tokens[1]: 18, - }, - sourceNativeToken: tokens[0], - priceGetterRespData: map[cciptypes.Address]*big.Int{ - tokens[0]: val1e18(100), - tokens[1]: val1e18(200), - tokens[2]: val1e18(300), // price getter returned a price for this token even though we didn't request it - }, - priceGetterRespErr: nil, - feeEstimatorRespFee: big.NewInt(10), - feeEstimatorRespErr: nil, - maxGasPrice: 1e18, - expSourceGasPriceUSD: big.NewInt(1000), - expTokenPricesUSD: map[cciptypes.Address]*big.Int{ - tokens[0]: val1e18(100), - tokens[1]: val1e18(200), - }, - expErr: false, - }, { name: "dynamic fee cap overrides legacy", tokenDecimals: map[cciptypes.Address]uint8{ diff --git a/core/services/ocr2/plugins/ccip/observations.go b/core/services/ocr2/plugins/ccip/observations.go index 2c09a09e13..f79d667a55 100644 --- a/core/services/ocr2/plugins/ccip/observations.go +++ b/core/services/ocr2/plugins/ccip/observations.go @@ -20,9 +20,10 @@ import ( // will not be able to unmarshal each other's observations. Do not modify unless you // know what you are doing. type CommitObservation struct { - Interval cciptypes.CommitStoreInterval `json:"interval"` - TokenPricesUSD map[cciptypes.Address]*big.Int `json:"tokensPerFeeCoin"` - SourceGasPriceUSD *big.Int `json:"sourceGasPrice"` + Interval cciptypes.CommitStoreInterval `json:"interval"` + TokenPricesUSD map[cciptypes.Address]*big.Int `json:"tokensPerFeeCoin"` + SourceGasPriceUSD *big.Int `json:"sourceGasPrice"` // Deprecated + SourceGasPriceUSDPerChain map[uint64]*big.Int `json:"sourceGasPriceUSDPerChain"` } // Marshal MUST be used instead of raw json.Marshal(o) since it contains backwards compatibility related changes. diff --git a/core/services/ocr2/plugins/ccip/observations_test.go b/core/services/ocr2/plugins/ccip/observations_test.go index 59638c5016..a3143f157d 100644 --- a/core/services/ocr2/plugins/ccip/observations_test.go +++ b/core/services/ocr2/plugins/ccip/observations_test.go @@ -6,7 +6,6 @@ import ( "strings" "testing" - "github.com/ethereum/go-ethereum/common" "github.com/leanovate/gopter" "github.com/leanovate/gopter/gen" "github.com/leanovate/gopter/prop" @@ -15,7 +14,7 @@ import ( "github.com/stretchr/testify/require" cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/commit_store_1_0_0" + "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipcalc" @@ -32,25 +31,24 @@ func TestObservationFilter(t *testing.T) { assert.Equal(t, nonEmpty[0].Interval, obs1.Interval) } -// After 1.2, the observation struct is version agnostic -// so only need to verify the 1.0->1.2 transition. -type CommitObservationV1_0_0 struct { - Interval commit_store_1_0_0.CommitStoreInterval `json:"interval"` - TokenPricesUSD map[common.Address]*big.Int `json:"tokensPerFeeCoin"` - SourceGasPriceUSD *big.Int `json:"sourceGasPrice"` +// This is the observation format up to 1.4.16 release +type CommitObservationLegacy struct { + Interval cciptypes.CommitStoreInterval `json:"interval"` + TokenPricesUSD map[cciptypes.Address]*big.Int `json:"tokensPerFeeCoin"` + SourceGasPriceUSD *big.Int `json:"sourceGasPrice"` } -func TestObservationCompat100_120(t *testing.T) { - v10 := CommitObservationV1_0_0{ - Interval: commit_store_1_0_0.CommitStoreInterval{ +func TestObservationCompat_MultiChainGas(t *testing.T) { + obsLegacy := CommitObservationLegacy{ + Interval: cciptypes.CommitStoreInterval{ Min: 1, Max: 12, }, - TokenPricesUSD: map[common.Address]*big.Int{common.HexToAddress("0x1"): big.NewInt(1)}, + TokenPricesUSD: map[cciptypes.Address]*big.Int{ccipcalc.HexToAddress("0x1"): big.NewInt(1)}, SourceGasPriceUSD: big.NewInt(3)} - b10, err := json.Marshal(v10) + bL, err := json.Marshal(obsLegacy) require.NoError(t, err) - v12 := CommitObservation{ + obsNew := CommitObservation{ Interval: cciptypes.CommitStoreInterval{ Min: 1, Max: 12, @@ -58,10 +56,13 @@ func TestObservationCompat100_120(t *testing.T) { TokenPricesUSD: map[cciptypes.Address]*big.Int{ccipcalc.HexToAddress("0x1"): big.NewInt(1)}, SourceGasPriceUSD: big.NewInt(3), } - b12, err := json.Marshal(v12) + bN, err := json.Marshal(obsNew) require.NoError(t, err) - // Assert identical json. - assert.Equal(t, b10, b12) + + observations := GetParsableObservations[CommitObservation](logger.TestLogger(t), []types.AttributedObservation{{Observation: bL}, {Observation: bN}}) + + assert.Equal(t, 2, len(observations)) + assert.Equal(t, observations[0], observations[1]) } func TestCommitObservationJsonDeserialization(t *testing.T) { @@ -97,13 +98,14 @@ func TestCommitObservationMarshal(t *testing.T) { Min: 1, Max: 12, }, - TokenPricesUSD: map[cciptypes.Address]*big.Int{"0xAaAaAa": big.NewInt(1)}, - SourceGasPriceUSD: big.NewInt(3), + TokenPricesUSD: map[cciptypes.Address]*big.Int{"0xAaAaAa": big.NewInt(1)}, + SourceGasPriceUSD: big.NewInt(3), + SourceGasPriceUSDPerChain: map[uint64]*big.Int{123: big.NewInt(3)}, } b, err := obs.Marshal() require.NoError(t, err) - assert.Equal(t, `{"interval":{"Min":1,"Max":12},"tokensPerFeeCoin":{"0xaaaaaa":1},"sourceGasPrice":3}`, string(b)) + assert.Equal(t, `{"interval":{"Min":1,"Max":12},"tokensPerFeeCoin":{"0xaaaaaa":1},"sourceGasPrice":3,"sourceGasPriceUSDPerChain":{"123":3}}`, string(b)) // Make sure that the call to Marshal did not alter the original observation object. assert.Len(t, obs.TokenPricesUSD, 1) @@ -111,6 +113,10 @@ func TestCommitObservationMarshal(t *testing.T) { assert.True(t, exists) _, exists = obs.TokenPricesUSD["0xaaaaaa"] assert.False(t, exists) + + assert.Len(t, obs.SourceGasPriceUSDPerChain, 1) + _, exists = obs.SourceGasPriceUSDPerChain[123] + assert.True(t, exists) } func TestExecutionObservationJsonDeserialization(t *testing.T) { @@ -226,7 +232,10 @@ func TestCommitObservationJsonSerializationDeserialization(t *testing.T) { "0x507877C2E26f1387432D067D2DaAfa7d0420d90a": 2, "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": 3 }, - "sourceGasPrice": 3 + "sourceGasPrice": 3, + "sourceGasPriceUSDPerChain": { + "123":3 + } }` expectedObservation := CommitObservation{ @@ -240,6 +249,9 @@ func TestCommitObservationJsonSerializationDeserialization(t *testing.T) { cciptypes.Address("0xaAaAaAaaAaAaAaaAaAAAAAAAAaaaAaAaAaaAaaAa"): big.NewInt(3), // json lower->eip55 parsed }, SourceGasPriceUSD: big.NewInt(3), + SourceGasPriceUSDPerChain: map[uint64]*big.Int{ + 123: big.NewInt(3), + }, } observations := GetParsableObservations[CommitObservation](logger.TestLogger(t), []types.AttributedObservation{