From b69f40497361f69dedf38992ccab3dbf87211c84 Mon Sep 17 00:00:00 2001 From: dimitris Date: Mon, 18 Sep 2023 14:26:19 +0300 Subject: [PATCH] offchain - unit testing improvements (#117) --- .gitignore | 2 + .../ccip/abihelpers/abi_helpers_test.go | 28 + .../ocr2/plugins/ccip/commit_plugin.go | 6 +- .../ocr2/plugins/ccip/commit_plugin_test.go | 12 +- .../plugins/ccip/commit_reporting_plugin.go | 14 +- .../ccip/commit_reporting_plugin_test.go | 1388 +++++++------- .../ccip/config/offchain_config_test.go | 3 +- .../plugins/ccip/execution_batch_building.go | 35 +- .../ocr2/plugins/ccip/execution_plugin.go | 11 +- .../plugins/ccip/execution_plugin_test.go | 30 +- .../ccip/execution_reporting_plugin.go | 72 +- .../ccip/execution_reporting_plugin_test.go | 1593 +++++++++++------ .../ocr2/plugins/ccip/integration_test.go | 22 +- .../plugins/ccip/internal/cache/cache_test.go | 4 - .../ccip/internal/cache/snoozed_roots_test.go | 2 +- .../ccip/internal/cache/tokens_test.go | 21 +- .../{ccipevents => ccipdata}/logpoller.go | 99 +- .../logpoller_test.go | 8 +- .../plugins/ccip/internal/ccipdata/mock.go | 246 +++ .../client.go => ccipdata/reader.go} | 15 +- .../ocr2/plugins/ccip/plugins_common.go | 4 +- .../plugins/ccip/testhelpers/commitstore.go | 89 + .../ccip/testhelpers/integration/chainlink.go | 4 +- .../ccip/testhelpers/integration/jobspec.go | 1 + .../ocr2/plugins/ccip/testhelpers/offramp.go | 105 ++ .../ocr2/plugins/ccip/testhelpers/onramp.go | 56 + .../testhelpers/plugins/plugin_harness.go | 275 --- .../plugins/ccip/testhelpers/priceregistry.go | 52 + 28 files changed, 2580 insertions(+), 1617 deletions(-) rename core/services/ocr2/plugins/ccip/internal/{ccipevents => ccipdata}/logpoller.go (74%) rename core/services/ocr2/plugins/ccip/internal/{ccipevents => ccipdata}/logpoller_test.go (96%) create mode 100644 core/services/ocr2/plugins/ccip/internal/ccipdata/mock.go rename core/services/ocr2/plugins/ccip/internal/{ccipevents/client.go => ccipdata/reader.go} (71%) create mode 100644 core/services/ocr2/plugins/ccip/testhelpers/commitstore.go create mode 100644 core/services/ocr2/plugins/ccip/testhelpers/offramp.go create mode 100644 core/services/ocr2/plugins/ccip/testhelpers/onramp.go delete mode 100644 core/services/ocr2/plugins/ccip/testhelpers/plugins/plugin_harness.go create mode 100644 core/services/ocr2/plugins/ccip/testhelpers/priceregistry.go diff --git a/.gitignore b/.gitignore index cd6a6ea09c..9c3a332b0f 100644 --- a/.gitignore +++ b/.gitignore @@ -96,3 +96,5 @@ contracts/yarn.lock /core/scripts/ccip/json/credentials /core/scripts/ccip/revert-reason/bin/ccip-revert-reason +# dependencies generated after running `go mod vendor` +vendor/ diff --git a/core/services/ocr2/plugins/ccip/abihelpers/abi_helpers_test.go b/core/services/ocr2/plugins/ccip/abihelpers/abi_helpers_test.go index 1dddc00c35..4cc5ccfac0 100644 --- a/core/services/ocr2/plugins/ccip/abihelpers/abi_helpers_test.go +++ b/core/services/ocr2/plugins/ccip/abihelpers/abi_helpers_test.go @@ -4,8 +4,12 @@ import ( "fmt" "math" "math/big" + "math/rand" "testing" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/commit_store" + "github.com/smartcontractkit/chainlink/v2/core/utils" + "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/assert" "github.com/test-go/testify/require" @@ -58,6 +62,30 @@ func TestProofFlagToBits(t *testing.T) { } } +func TestCommitReportEncoding(t *testing.T) { + report := commit_store.CommitStoreCommitReport{ + PriceUpdates: commit_store.InternalPriceUpdates{ + TokenPriceUpdates: []commit_store.InternalTokenPriceUpdate{ + { + SourceToken: utils.RandomAddress(), + UsdPerToken: big.NewInt(9e18), + }, + }, + DestChainSelector: rand.Uint64(), + UsdPerUnitGas: big.NewInt(2000e9), + }, + MerkleRoot: [32]byte{123}, + Interval: commit_store.CommitStoreInterval{Min: 1, Max: 10}, + } + + encodedReport, err := EncodeCommitReport(report) + require.NoError(t, err) + + decodedReport, err := DecodeCommitReport(encodedReport) + require.NoError(t, err) + require.Equal(t, report, decodedReport) +} + func TestExecutionReportEncoding(t *testing.T) { // Note could consider some fancier testing here (fuzz/property) // but I think that would essentially be testing geth's abi library diff --git a/core/services/ocr2/plugins/ccip/commit_plugin.go b/core/services/ocr2/plugins/ccip/commit_plugin.go index b5fc663a53..ef1d6cffe3 100644 --- a/core/services/ocr2/plugins/ccip/commit_plugin.go +++ b/core/services/ocr2/plugins/ccip/commit_plugin.go @@ -14,7 +14,7 @@ import ( libocr2 "github.com/smartcontractkit/libocr/offchainreporting2plus" relaylogger "github.com/smartcontractkit/chainlink-relay/pkg/logger" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipevents" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/hashlib" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/oraclelib" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/pricegetter" @@ -106,8 +106,8 @@ func NewCommitServices(lggr logger.Logger, jb job.Job, chainSet evm.LegacyChainC lggr: commitLggr, sourceLP: sourceChain.LogPoller(), destLP: destChain.LogPoller(), - sourceEvents: ccipevents.NewLogPollerClient(sourceChain.LogPoller(), commitLggr, sourceChain.Client()), - destEvents: ccipevents.NewLogPollerClient(destChain.LogPoller(), commitLggr, destChain.Client()), + sourceReader: ccipdata.NewLogPollerReader(sourceChain.LogPoller(), commitLggr, sourceChain.Client()), + destReader: ccipdata.NewLogPollerReader(destChain.LogPoller(), commitLggr, destChain.Client()), offRamp: offRamp, onRampAddress: onRamp.Address(), priceGetter: priceGetterObject, diff --git a/core/services/ocr2/plugins/ccip/commit_plugin_test.go b/core/services/ocr2/plugins/ccip/commit_plugin_test.go index a9dc54e94a..9a4af21a50 100644 --- a/core/services/ocr2/plugins/ccip/commit_plugin_test.go +++ b/core/services/ocr2/plugins/ccip/commit_plugin_test.go @@ -17,6 +17,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/commit_store" mock_contracts "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/mocks" "github.com/smartcontractkit/chainlink/v2/core/services/job" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/testhelpers" ) func TestGetCommitPluginFilterNamesFromSpec(t *testing.T) { @@ -87,13 +88,10 @@ func TestGetCommitPluginFilterNames(t *testing.T) { onRampAddr := common.HexToAddress("0xdafea492d9c6733ae3d56b7ed1adb60692c98bc2") priceRegAddr := common.HexToAddress("0xdafea492d9c6733ae3d56b7ed1adb60692c98bc3") offRampAddr := common.HexToAddress("0xDAFeA492D9c6733Ae3D56b7eD1AdB60692C98BC4") - mockCommitStore := mock_contracts.NewCommitStoreInterface(t) - mockCommitStore.On("GetStaticConfig", mock.Anything).Return(commit_store.CommitStoreStaticConfig{ - OnRamp: onRampAddr, - }, nil) - mockCommitStore.On("GetDynamicConfig", mock.Anything).Return(commit_store.CommitStoreDynamicConfig{ - PriceRegistry: priceRegAddr, - }, nil) + + mockCommitStore, _ := testhelpers.NewFakeCommitStore(t, 1) + mockCommitStore.SetStaticConfig(commit_store.CommitStoreStaticConfig{OnRamp: onRampAddr}) + mockCommitStore.SetDynamicConfig(commit_store.CommitStoreDynamicConfig{PriceRegistry: priceRegAddr}) srcLP := mocklp.NewLogPoller(t) dstLP := mocklp.NewLogPoller(t) diff --git a/core/services/ocr2/plugins/ccip/commit_reporting_plugin.go b/core/services/ocr2/plugins/ccip/commit_reporting_plugin.go index 86d084b91b..f23acef427 100644 --- a/core/services/ocr2/plugins/ccip/commit_reporting_plugin.go +++ b/core/services/ocr2/plugins/ccip/commit_reporting_plugin.go @@ -25,10 +25,10 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" ccipconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/pricegetter" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/cache" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipevents" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/hashlib" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/merklemulti" "github.com/smartcontractkit/chainlink/v2/core/services/pg" @@ -56,8 +56,8 @@ type update struct { type CommitPluginConfig struct { lggr logger.Logger sourceLP, destLP logpoller.LogPoller - sourceEvents ccipevents.Client - destEvents ccipevents.Client + sourceReader ccipdata.Reader + destReader ccipdata.Reader offRamp evm_2_evm_offramp.EVM2EVMOffRampInterface onRampAddress common.Address commitStore commit_store.CommitStoreInterface @@ -238,7 +238,7 @@ func (r *CommitReportingPlugin) calculateMinMaxSequenceNumbers(ctx context.Conte return 0, 0, err } - msgRequests, err := r.config.sourceEvents.GetSendRequestsGteSeqNum(ctx, r.config.onRampAddress, nextInflightMin, r.config.checkFinalityTags, int(r.offchainConfig.SourceFinalityDepth)) + msgRequests, err := r.config.sourceReader.GetSendRequestsGteSeqNum(ctx, r.config.onRampAddress, nextInflightMin, r.config.checkFinalityTags, int(r.offchainConfig.SourceFinalityDepth)) if err != nil { return 0, 0, err } @@ -367,7 +367,7 @@ func calculateUsdPer1e18TokenAmount(price *big.Int, decimals uint8) *big.Int { // 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, checkInflight bool) (map[common.Address]update, error) { - tokenPriceUpdates, err := r.config.destEvents.GetTokenPriceUpdatesCreatedAfter( + tokenPriceUpdates, err := r.config.destReader.GetTokenPriceUpdatesCreatedAfter( ctx, r.destPriceRegistry.Address(), now.Add(-r.offchainConfig.FeeUpdateHeartBeat.Duration()), @@ -423,7 +423,7 @@ func (r *CommitReportingPlugin) getLatestGasPriceUpdate(ctx context.Context, now } // If there are no price updates inflight, check latest prices onchain - gasPriceUpdates, err := r.config.destEvents.GetGasPriceUpdatesCreatedAfter( + gasPriceUpdates, err := r.config.destReader.GetGasPriceUpdatesCreatedAfter( ctx, r.destPriceRegistry.Address(), r.config.sourceChainSelector, @@ -657,7 +657,7 @@ func (r *CommitReportingPlugin) buildReport(ctx context.Context, lggr logger.Log // Logs are guaranteed to be in order of seq num, since these are finalized logs only // and the contract's seq num is auto-incrementing. - sendRequests, err := r.config.sourceEvents.GetSendRequestsBetweenSeqNums( + sendRequests, err := r.config.sourceReader.GetSendRequestsBetweenSeqNums( ctx, r.config.onRampAddress, interval.Min, diff --git a/core/services/ocr2/plugins/ccip/commit_reporting_plugin_test.go b/core/services/ocr2/plugins/ccip/commit_reporting_plugin_test.go index b75cf54ac8..8d7e0ab51b 100644 --- a/core/services/ocr2/plugins/ccip/commit_reporting_plugin_test.go +++ b/core/services/ocr2/plugins/ccip/commit_reporting_plugin_test.go @@ -2,7 +2,6 @@ package ccip import ( "context" - "encoding/json" "fmt" "math/big" "math/rand" @@ -11,6 +10,8 @@ import ( "testing" "time" + gethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/leanovate/gopter" @@ -23,329 +24,404 @@ import ( "github.com/smartcontractkit/libocr/offchainreporting2plus/types" "github.com/smartcontractkit/chainlink/v2/core/assets" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/mocks" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/commit_store" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_onramp" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/price_registry" - mock_contracts "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/mocks" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/generated/link_token_interface" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" ccipconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/cache" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipevents" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/hashlib" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/merklemulti" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/pricegetter" - plugintesthelpers "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/testhelpers/plugins" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/testhelpers" + "github.com/smartcontractkit/chainlink/v2/core/store/models" "github.com/smartcontractkit/chainlink/v2/core/utils" ) -var defaultGasPrice = big.NewInt(3e9) - -type commitTestHarness = struct { - plugintesthelpers.CCIPPluginTestHarness - plugin *CommitReportingPlugin - mockedGetFee *mock.Call -} - -func setupCommitTestHarness(t *testing.T) commitTestHarness { - th := plugintesthelpers.SetupCCIPTestHarness(t) - - sourceFeeEstimator := mocks.NewEvmFeeEstimator(t) - - mockedGetFee := sourceFeeEstimator.On( - "GetFee", - mock.Anything, - mock.Anything, - mock.Anything, - mock.Anything, - ).Maybe().Return(gas.EvmFee{Legacy: assets.NewWei(defaultGasPrice)}, uint32(200e3), nil) - - lggr := logger.TestLogger(t) - priceGetter := pricegetter.NewMockPriceGetter(t) - - backendClient := client.NewSimulatedBackendClient(t, th.Dest.Chain, new(big.Int).SetUint64(th.Dest.ChainID)) - plugin := CommitReportingPlugin{ - config: CommitPluginConfig{ - lggr: th.Lggr, - sourceLP: th.SourceLP, - destLP: th.DestLP, - sourceEvents: ccipevents.NewLogPollerClient(th.SourceLP, lggr, backendClient), - destEvents: ccipevents.NewLogPollerClient(th.DestLP, lggr, backendClient), - offRamp: th.Dest.OffRamp, - onRampAddress: th.Source.OnRamp.Address(), - commitStore: th.Dest.CommitStore, - priceGetter: priceGetter, - sourceNative: utils.RandomAddress(), - sourceFeeEstimator: sourceFeeEstimator, - sourceChainSelector: th.Source.ChainSelector, - destClient: backendClient, - sourceClient: backendClient, - leafHasher: hashlib.NewLeafHasher(th.Source.ChainSelector, th.Dest.ChainSelector, th.Source.OnRamp.Address(), hashlib.NewKeccakCtx()), - }, - inflightReports: newInflightCommitReportsContainer(time.Hour), - onchainConfig: th.CommitOnchainConfig, - offchainConfig: ccipconfig.CommitOffchainConfig{ - SourceFinalityDepth: 0, - DestFinalityDepth: 0, - FeeUpdateDeviationPPB: 5e7, - FeeUpdateHeartBeat: models.MustMakeDuration(12 * time.Hour), - MaxGasPrice: 200e9, - }, - lggr: th.Lggr, - destPriceRegistry: th.Dest.PriceRegistry, - tokenDecimalsCache: cache.NewTokenToDecimals(th.Lggr, th.DestLP, th.Dest.OffRamp, th.Dest.PriceRegistry, backendClient, 0), - } - - priceGetter.On("TokenPricesUSD", mock.Anything, mock.Anything).Return(map[common.Address]*big.Int{ - plugin.config.sourceNative: big.NewInt(0).Mul(big.NewInt(100), big.NewInt(1e18)), - th.Source.LinkToken.Address(): big.NewInt(0).Mul(big.NewInt(200), big.NewInt(1e18)), - th.Dest.LinkToken.Address(): big.NewInt(0).Mul(big.NewInt(200), big.NewInt(1e18)), - }, nil).Maybe() - - return commitTestHarness{ - CCIPPluginTestHarness: th, - plugin: &plugin, - mockedGetFee: mockedGetFee, - } -} - -func TestCommitReportSize(t *testing.T) { - testParams := gopter.DefaultTestParameters() - testParams.MinSuccessfulTests = 100 - p := gopter.NewProperties(testParams) - p.Property("bounded commit report size", prop.ForAll(func(root []byte, min, max uint64) bool { - var root32 [32]byte - copy(root32[:], root) - rep, err := abihelpers.EncodeCommitReport(commit_store.CommitStoreCommitReport{ - MerkleRoot: root32, - Interval: commit_store.CommitStoreInterval{Min: min, Max: max}, - PriceUpdates: commit_store.InternalPriceUpdates{ - TokenPriceUpdates: []commit_store.InternalTokenPriceUpdate{}, - DestChainSelector: 1337, - UsdPerUnitGas: big.NewInt(2000e9), // $2000 per eth * 1gwei = 2000e9 - }, - }) - require.NoError(t, err) - return len(rep) <= MaxCommitReportLength - }, gen.SliceOfN(32, gen.UInt8()), gen.UInt64(), gen.UInt64())) - p.TestingRun(t) -} - -func TestCommitReportEncoding(t *testing.T) { - th := plugintesthelpers.SetupCCIPTestHarness(t) - newTokenPrice := big.NewInt(9e18) // $9 - newGasPrice := big.NewInt(2000e9) // $2000 per eth * 1gwei - - // Send a report. - mctx := hashlib.NewKeccakCtx() - tree, err := merklemulti.NewTree(mctx, [][32]byte{mctx.Hash([]byte{0xaa})}) - require.NoError(t, err) - report := commit_store.CommitStoreCommitReport{ - PriceUpdates: commit_store.InternalPriceUpdates{ - TokenPriceUpdates: []commit_store.InternalTokenPriceUpdate{ - { - SourceToken: th.Dest.LinkToken.Address(), - UsdPerToken: newTokenPrice, - }, - }, - DestChainSelector: th.Source.ChainSelector, - UsdPerUnitGas: newGasPrice, - }, - MerkleRoot: tree.Root(), - Interval: commit_store.CommitStoreInterval{Min: 1, Max: 10}, - } - out, err := abihelpers.EncodeCommitReport(report) - require.NoError(t, err) - decodedReport, err := abihelpers.DecodeCommitReport(out) - require.NoError(t, err) - require.Equal(t, report, decodedReport) - - latestEpocAndRound, err := th.Dest.CommitStoreHelper.GetLatestPriceEpochAndRound(nil) - require.NoError(t, err) - - tx, err := th.Dest.CommitStoreHelper.Report(th.Dest.User, out, big.NewInt(int64(latestEpocAndRound+1))) - require.NoError(t, err) - th.CommitAndPollLogs(t) - res, err := th.Dest.Chain.TransactionReceipt(testutils.Context(t), tx.Hash()) - require.NoError(t, err) - assert.Equal(t, uint64(1), res.Status) - - // Ensure root exists. - ts, err := th.Dest.CommitStore.GetMerkleRoot(nil, tree.Root()) - require.NoError(t, err) - require.NotEqual(t, ts.String(), "0") - - // Ensure price update went through - destChainGasPrice, err := th.Dest.PriceRegistry.GetDestinationChainGasPrice(nil, th.Source.ChainSelector) - require.NoError(t, err) - assert.Equal(t, newGasPrice, destChainGasPrice.Value) - - linkTokenPrice, err := th.Dest.PriceRegistry.GetTokenPrice(nil, th.Dest.LinkToken.Address()) - require.NoError(t, err) - assert.Equal(t, newTokenPrice, linkTokenPrice.Value) -} - -func TestCommitObservation(t *testing.T) { - th := setupCommitTestHarness(t) - th.plugin.F = 1 - - mb := th.GenerateAndSendMessageBatch(t, 1, 0, 0) +func TestCommitReportingPlugin_Observation(t *testing.T) { + sourceNativeTokenAddr := common.HexToAddress("1000") + someTokenAddr := common.HexToAddress("2000") - tests := []struct { - name string - commitStoreDown bool - expected *CommitObservation - expectedError bool + testCases := []struct { + name string + epochAndRound types.ReportTimestamp + commitStoreIsPaused bool + commitStoreSeqNum uint64 + tokenPrices map[common.Address]*big.Int + sendReqs []ccipdata.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested] + tokenDecimals map[common.Address]uint8 + fee *big.Int + + expErr bool + expObs CommitObservation }{ { - "base", - false, - &CommitObservation{ - Interval: mb.Interval, - SourceGasPriceUSD: new(big.Int).Mul(defaultGasPrice, big.NewInt(100)), + name: "base report", + commitStoreSeqNum: 54, + tokenPrices: map[common.Address]*big.Int{ + someTokenAddr: big.NewInt(2), + sourceNativeTokenAddr: big.NewInt(2), + }, + sendReqs: []ccipdata.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested]{ + {Data: evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested{Message: evm_2_evm_onramp.InternalEVM2EVMMessage{SequenceNumber: 54}}}, + {Data: evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested{Message: evm_2_evm_onramp.InternalEVM2EVMMessage{SequenceNumber: 55}}}, + }, + fee: big.NewInt(100), + tokenDecimals: map[common.Address]uint8{ + someTokenAddr: 8, + }, + expObs: CommitObservation{ TokenPricesUSD: map[common.Address]*big.Int{ - th.Dest.LinkToken.Address(): new(big.Int).Mul(big.NewInt(200), big.NewInt(1e18)), + someTokenAddr: big.NewInt(20000000000), + }, + SourceGasPriceUSD: big.NewInt(0), + Interval: commit_store.CommitStoreInterval{ + Min: 54, + Max: 55, }, }, - false, }, { - "commitStore down", - true, - nil, - true, + name: "commit store is down", + commitStoreIsPaused: true, + expErr: true, }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.commitStoreDown && !isCommitStoreDownNow(testutils.Context(t), th.Lggr, th.Dest.CommitStore) { - _, err := th.Dest.CommitStore.Pause(th.Dest.User) - require.NoError(t, err) - th.CommitAndPollLogs(t) - } else if !tt.commitStoreDown && isCommitStoreDownNow(testutils.Context(t), th.Lggr, th.Dest.CommitStore) { - _, err := th.Dest.CommitStore.Unpause(th.Dest.User) - require.NoError(t, err) - th.CommitAndPollLogs(t) + ctx := testutils.Context(t) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + onRampAddress := utils.RandomAddress() + sourceFinalityDepth := 10 + + commitStore, _ := testhelpers.NewFakeCommitStore(t, tc.commitStoreSeqNum) + commitStore.SetPaused(tc.commitStoreIsPaused) + + sourceReader := ccipdata.NewMockReader(t) + if len(tc.sendReqs) > 0 { + sourceReader.On("GetSendRequestsGteSeqNum", ctx, onRampAddress, tc.commitStoreSeqNum, false, sourceFinalityDepth). + Return(tc.sendReqs, nil) } - gotObs, err := th.plugin.Observation(testutils.Context(t), types.ReportTimestamp{}, types.Query{}) + tokenDecimalsCache := cache.NewMockAutoSync[map[common.Address]uint8](t) + if len(tc.tokenDecimals) > 0 { + tokenDecimalsCache.On("Get", ctx).Return(tc.tokenDecimals, nil) + } - if tt.expectedError { - require.Error(t, err) - } else { - require.NoError(t, err) + priceGet := pricegetter.NewMockPriceGetter(t) + if len(tc.tokenPrices) > 0 { + addrs := []common.Address{sourceNativeTokenAddr} + for addr := range tc.tokenDecimals { + addrs = append(addrs, addr) + } + priceGet.On("TokenPricesUSD", mock.Anything, addrs).Return(tc.tokenPrices, nil) } - var decodedObservation *CommitObservation - if gotObs != nil { - decodedObservation = new(CommitObservation) - err = json.Unmarshal(gotObs, decodedObservation) - require.NoError(t, err) + sourceFeeEst := mocks.NewEvmFeeEstimator(t) + if tc.fee != nil { + sourceFeeEst.On("GetFee", ctx, []byte(nil), uint32(0), assets.NewWei(big.NewInt(0))). + Return(gas.EvmFee{Legacy: assets.NewWei(tc.fee)}, uint32(0), nil) + } + + p := &CommitReportingPlugin{} + p.lggr = logger.TestLogger(t) + p.inflightReports = newInflightCommitReportsContainer(time.Hour) + p.config.commitStore = commitStore + p.config.onRampAddress = onRampAddress + p.offchainConfig.SourceFinalityDepth = uint32(sourceFinalityDepth) + p.config.sourceReader = sourceReader + p.tokenDecimalsCache = tokenDecimalsCache + p.config.priceGetter = priceGet + p.config.sourceFeeEstimator = sourceFeeEst + p.config.sourceNative = sourceNativeTokenAddr + obs, err := p.Observation(ctx, tc.epochAndRound, types.Query{}) + + if tc.expErr { + assert.Error(t, err) + return } - assert.Equal(t, tt.expected, decodedObservation) + assert.NoError(t, err) + + expObsBytes, err := tc.expObs.Marshal() + assert.NoError(t, err) + assert.Equal(t, expObsBytes, []byte(obs)) }) } } -func TestCommitReport(t *testing.T) { - th := setupCommitTestHarness(t) - th.plugin.F = 1 - - mb := th.GenerateAndSendMessageBatch(t, 1, 0, 0) +func TestCommitReportingPlugin_Report(t *testing.T) { - tests := []struct { - name string - observations []CommitObservation - shouldReport bool - commitReport *commit_store.CommitStoreCommitReport - expectedError bool + testCases := []struct { + name string + observations []CommitObservation + f int + gasPriceUpdates []ccipdata.Event[price_registry.PriceRegistryUsdPerUnitGasUpdated] + tokenPriceUpdates []ccipdata.Event[price_registry.PriceRegistryUsdPerTokenUpdated] + sendRequests []ccipdata.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested] + + expCommitReport *commit_store.CommitStoreCommitReport + expSeqNumRange commit_store.CommitStoreInterval + expErr bool }{ { - "base", - []CommitObservation{ + name: "base", + observations: []CommitObservation{ {Interval: commit_store.CommitStoreInterval{Min: 1, Max: 1}}, {Interval: commit_store.CommitStoreInterval{Min: 1, Max: 1}}, }, - true, - &commit_store.CommitStoreCommitReport{ - MerkleRoot: mb.Root, + f: 1, + sendRequests: []ccipdata.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested]{ + { + Data: evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested{ + Message: evm_2_evm_onramp.InternalEVM2EVMMessage{ + SequenceNumber: 1, + }, + }, + }, + }, + expSeqNumRange: commit_store.CommitStoreInterval{Min: 1, Max: 1}, + expCommitReport: &commit_store.CommitStoreCommitReport{ + MerkleRoot: [32]byte{123}, Interval: commit_store.CommitStoreInterval{Min: 1, Max: 1}, PriceUpdates: commit_store.InternalPriceUpdates{ - TokenPriceUpdates: []commit_store.InternalTokenPriceUpdate{}, + TokenPriceUpdates: nil, DestChainSelector: 0, - UsdPerUnitGas: new(big.Int), + UsdPerUnitGas: big.NewInt(0), }, }, - false, + expErr: false, }, { - "not enough observations", - []CommitObservation{ + name: "not enough observations", + observations: []CommitObservation{ {Interval: commit_store.CommitStoreInterval{Min: 1, Max: 1}}, }, - false, - nil, - true, + f: 1, + sendRequests: []ccipdata.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested]{{}}, + expSeqNumRange: commit_store.CommitStoreInterval{Min: 1, Max: 1}, + expErr: true, }, { - "empty", - []CommitObservation{ + name: "empty", + observations: []CommitObservation{ {Interval: commit_store.CommitStoreInterval{Min: 0, Max: 0}}, {Interval: commit_store.CommitStoreInterval{Min: 0, Max: 0}}, }, - false, - nil, - false, + f: 1, + expErr: false, }, { - "no leaves", - []CommitObservation{ + name: "no leaves", + observations: []CommitObservation{ {Interval: commit_store.CommitStoreInterval{Min: 2, Max: 2}}, {Interval: commit_store.CommitStoreInterval{Min: 2, Max: 2}}, }, - false, - nil, - true, + f: 1, + sendRequests: []ccipdata.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested]{{}}, + expSeqNumRange: commit_store.CommitStoreInterval{Min: 2, Max: 2}, + expErr: true, }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - aos := make([]types.AttributedObservation, 0, len(tt.observations)) - for _, o := range tt.observations { + ctx := testutils.Context(t) + onRampAddress := utils.RandomAddress() + sourceChainSelector := rand.Int() + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + destPriceRegistry, destPriceRegistryAddress := testhelpers.NewFakePriceRegistry(t) + + destReader := ccipdata.NewMockReader(t) + destReader.On("GetGasPriceUpdatesCreatedAfter", ctx, destPriceRegistryAddress, uint64(sourceChainSelector), mock.Anything, 0).Return(tc.gasPriceUpdates, nil) + destReader.On("GetTokenPriceUpdatesCreatedAfter", ctx, destPriceRegistryAddress, mock.Anything, 0).Return(tc.tokenPriceUpdates, nil) + + sourceReader := ccipdata.NewMockReader(t) + if len(tc.sendRequests) > 0 { + sourceReader.On("GetSendRequestsBetweenSeqNums", ctx, onRampAddress, tc.expSeqNumRange.Min, tc.expSeqNumRange.Max, 0).Return(tc.sendRequests, nil) + } + + p := &CommitReportingPlugin{} + p.lggr = logger.TestLogger(t) + p.inflightReports = newInflightCommitReportsContainer(time.Minute) + p.destPriceRegistry = destPriceRegistry + p.config.destReader = destReader + p.config.sourceReader = sourceReader + p.config.onRampAddress = onRampAddress + p.config.sourceChainSelector = uint64(sourceChainSelector) + p.config.leafHasher = &leafHasher123{} + + aos := make([]types.AttributedObservation, 0, len(tc.observations)) + for _, o := range tc.observations { obs, err := o.Marshal() - require.NoError(t, err) + assert.NoError(t, err) aos = append(aos, types.AttributedObservation{Observation: obs}) } - gotShouldReport, gotReport, err := th.plugin.Report(testutils.Context(t), types.ReportTimestamp{}, types.Query{}, aos) - if tt.expectedError { - require.Error(t, err) - } else { - require.NoError(t, err) + gotSomeReport, gotReport, err := p.Report(ctx, types.ReportTimestamp{}, types.Query{}, aos) + if tc.expErr { + assert.Error(t, err) + return } - assert.Equal(t, tt.shouldReport, gotShouldReport) + assert.NoError(t, err) - var expectedReport types.Report - if tt.commitReport != nil { - expectedReport, err = abihelpers.EncodeCommitReport(*tt.commitReport) - require.NoError(t, err) + if tc.expCommitReport != nil { + assert.True(t, gotSomeReport) + encodedExpectedReport, err := abihelpers.EncodeCommitReport(*tc.expCommitReport) + assert.NoError(t, err) + assert.Equal(t, types.Report(encodedExpectedReport), gotReport) } - assert.Equal(t, expectedReport, gotReport) }) } } -func TestCalculatePriceUpdates(t *testing.T) { - t.Parallel() +func TestCommitReportingPlugin_ShouldAcceptFinalizedReport(t *testing.T) { + ctx := testutils.Context(t) + + newPlugin := func() *CommitReportingPlugin { + p := &CommitReportingPlugin{} + p.lggr = logger.TestLogger(t) + p.inflightReports = newInflightCommitReportsContainer(time.Minute) + return p + } + + t.Run("report cannot be decoded leads to error", func(t *testing.T) { + p := newPlugin() + encodedReport := []byte("whatever") + _, err := p.ShouldAcceptFinalizedReport(ctx, types.ReportTimestamp{}, encodedReport) + assert.Error(t, err) + }) + + t.Run("empty report should not be accepted", func(t *testing.T) { + p := newPlugin() + report := commit_store.CommitStoreCommitReport{ + // UsdPerUnitGas is mandatory otherwise report cannot be encoded/decoded + PriceUpdates: commit_store.InternalPriceUpdates{UsdPerUnitGas: big.NewInt(int64(rand.Int()))}, + } + encodedReport, err := abihelpers.EncodeCommitReport(report) + assert.NoError(t, err) + shouldAccept, err := p.ShouldAcceptFinalizedReport(ctx, types.ReportTimestamp{}, encodedReport) + assert.NoError(t, err) + assert.False(t, shouldAccept) + }) + + t.Run("stale report should not be accepted", func(t *testing.T) { + onChainSeqNum := uint64(100) + + commitStore, _ := testhelpers.NewFakeCommitStore(t, onChainSeqNum) + + p := newPlugin() + p.config.commitStore = commitStore + + report := commit_store.CommitStoreCommitReport{ + PriceUpdates: commit_store.InternalPriceUpdates{UsdPerUnitGas: big.NewInt(int64(rand.Int()))}, + MerkleRoot: [32]byte{123}, // this report is considered non-empty since it has a merkle root + } + + // stale since report interval is behind on chain seq num + report.Interval = commit_store.CommitStoreInterval{Min: onChainSeqNum - 2, Max: onChainSeqNum + 10} + encodedReport, err := abihelpers.EncodeCommitReport(report) + assert.NoError(t, err) + + shouldAccept, err := p.ShouldAcceptFinalizedReport(ctx, types.ReportTimestamp{}, encodedReport) + assert.NoError(t, err) + assert.False(t, shouldAccept) + }) + + t.Run("non-stale report should be accepted and added inflight", func(t *testing.T) { + onChainSeqNum := uint64(100) + + commitStore, _ := testhelpers.NewFakeCommitStore(t, onChainSeqNum) + + p := newPlugin() + p.config.commitStore = commitStore + + report := commit_store.CommitStoreCommitReport{ + PriceUpdates: commit_store.InternalPriceUpdates{ + TokenPriceUpdates: []commit_store.InternalTokenPriceUpdate{ + { + SourceToken: utils.RandomAddress(), + UsdPerToken: big.NewInt(int64(rand.Int())), + }, + }, + DestChainSelector: rand.Uint64(), + UsdPerUnitGas: big.NewInt(int64(rand.Int())), + }, + MerkleRoot: [32]byte{123}, + } + + // non-stale since report interval is not behind on-chain seq num + report.Interval = commit_store.CommitStoreInterval{Min: onChainSeqNum, Max: onChainSeqNum + 10} + encodedReport, err := abihelpers.EncodeCommitReport(report) + assert.NoError(t, err) + + shouldAccept, err := p.ShouldAcceptFinalizedReport(ctx, types.ReportTimestamp{}, encodedReport) + assert.NoError(t, err) + assert.True(t, shouldAccept) + + // make sure that the report was added inflight + tokenPriceUpdates := p.inflightReports.latestInflightTokenPriceUpdates() + priceUpdate := tokenPriceUpdates[report.PriceUpdates.TokenPriceUpdates[0].SourceToken] + assert.Equal(t, report.PriceUpdates.TokenPriceUpdates[0].UsdPerToken.Uint64(), priceUpdate.value.Uint64()) + }) +} + +func TestCommitReportingPlugin_ShouldTransmitAcceptedReport(t *testing.T) { + report := commit_store.CommitStoreCommitReport{ + PriceUpdates: commit_store.InternalPriceUpdates{ + TokenPriceUpdates: []commit_store.InternalTokenPriceUpdate{ + {SourceToken: utils.RandomAddress(), UsdPerToken: big.NewInt(9e18)}, + }, + DestChainSelector: rand.Uint64(), + UsdPerUnitGas: big.NewInt(2000e9), + }, + MerkleRoot: [32]byte{123}, + } + + ctx := testutils.Context(t) + p := &CommitReportingPlugin{} + commitStore, _ := testhelpers.NewFakeCommitStore(t, 0) + p.config.commitStore = commitStore + p.inflightReports = newInflightCommitReportsContainer(time.Minute) + p.lggr = logger.TestLogger(t) + + t.Run("should transmit when report is not stale", func(t *testing.T) { + onChainSeqNum := uint64(100) + commitStore.SetNextSequenceNumber(onChainSeqNum) + // not-stale since report interval is not behind on chain seq num + report.Interval = commit_store.CommitStoreInterval{Min: onChainSeqNum, Max: onChainSeqNum + 10} + encodedReport, err := abihelpers.EncodeCommitReport(report) + assert.NoError(t, err) + shouldTransmit, err := p.ShouldTransmitAcceptedReport(ctx, types.ReportTimestamp{}, encodedReport) + assert.NoError(t, err) + assert.True(t, shouldTransmit) + }) + t.Run("should not transmit when report is stale", func(t *testing.T) { + onChainSeqNum := uint64(100) + commitStore.SetNextSequenceNumber(onChainSeqNum) + // stale since report interval is behind on chain seq num + report.Interval = commit_store.CommitStoreInterval{Min: onChainSeqNum - 2, Max: onChainSeqNum + 10} + encodedReport, err := abihelpers.EncodeCommitReport(report) + assert.NoError(t, err) + shouldTransmit, err := p.ShouldTransmitAcceptedReport(ctx, types.ReportTimestamp{}, encodedReport) + assert.NoError(t, err) + assert.False(t, shouldTransmit) + }) + + t.Run("error when report cannot be decoded", func(t *testing.T) { + _, err := p.ShouldTransmitAcceptedReport(ctx, types.ReportTimestamp{}, []byte("whatever")) + assert.Error(t, err) + }) +} + +func TestCommitReportingPlugin_calculatePriceUpdates(t *testing.T) { const defaultSourceChainSelector = 10 // we reuse this value across all test cases feeToken1 := common.HexToAddress("0xa") feeToken2 := common.HexToAddress("0xb") @@ -570,54 +646,7 @@ func TestCalculatePriceUpdates(t *testing.T) { } } -func TestCalculateIntervalConsensus(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - intervals []commit_store.CommitStoreInterval - rangeLimit uint64 - f int - wantMin uint64 - wantMax uint64 - wantErr bool - }{ - {"no obs", []commit_store.CommitStoreInterval{{Min: 0, Max: 0}}, 0, 0, 0, 0, false}, - {"basic", []commit_store.CommitStoreInterval{ - {Min: 9, Max: 14}, - {Min: 10, Max: 12}, - {Min: 10, Max: 14}, - }, 0, 1, 10, 14, false}, - {"not enough intervals", []commit_store.CommitStoreInterval{}, 0, 1, 0, 0, true}, - {"min > max", []commit_store.CommitStoreInterval{ - {Min: 9, Max: 4}, - {Min: 10, Max: 4}, - {Min: 10, Max: 6}, - }, 0, 1, 0, 0, true}, - { - "range limit", []commit_store.CommitStoreInterval{ - {Min: 10, Max: 100}, - {Min: 1, Max: 1000}, - }, 256, 1, 10, 265, false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := calculateIntervalConsensus(tt.intervals, tt.f, tt.rangeLimit) - if tt.wantErr { - require.Error(t, err) - } else { - require.NoError(t, err) - } - assert.Equal(t, tt.wantMin, got.Min) - assert.Equal(t, tt.wantMax, got.Max) - }) - } -} - -func TestGeneratePriceUpdates(t *testing.T) { - t.Parallel() - +func TestCommitReportingPlugin_generatePriceUpdates(t *testing.T) { val1e18 := func(val int64) *big.Int { return new(big.Int).Mul(big.NewInt(1e18), big.NewInt(val)) } const nTokens = 10 @@ -826,271 +855,516 @@ func TestGeneratePriceUpdates(t *testing.T) { } } -func TestUpdateTokenToDecimalMapping(t *testing.T) { - th := plugintesthelpers.SetupCCIPTestHarness(t) - - destToken, _, _, err := link_token_interface.DeployLinkToken(th.Dest.User, th.Dest.Chain) - require.NoError(t, err) - - feeToken, _, _, err := link_token_interface.DeployLinkToken(th.Dest.User, th.Dest.Chain) - require.NoError(t, err) - th.CommitAndPollLogs(t) - - tokens := []common.Address{} - tokens = append(tokens, destToken) - tokens = append(tokens, feeToken) - - mockOffRamp := &mock_contracts.EVM2EVMOffRampInterface{} - mockOffRamp.On("GetDestinationTokens", mock.Anything).Return([]common.Address{destToken}, nil) - mockOffRamp.On("Address").Return(common.Address{}) - - mockPriceRegistry := &mock_contracts.PriceRegistryInterface{} - mockPriceRegistry.On("GetFeeTokens", mock.Anything).Return([]common.Address{feeToken}, nil) - mockPriceRegistry.On("Address").Return(common.Address{}) - - backendClient := client.NewSimulatedBackendClient(t, th.Dest.Chain, new(big.Int).SetUint64(th.Dest.ChainID)) - plugin := CommitReportingPlugin{ - config: CommitPluginConfig{ - offRamp: mockOffRamp, - destClient: backendClient, - }, - destPriceRegistry: mockPriceRegistry, - tokenDecimalsCache: cache.NewTokenToDecimals(th.Lggr, th.DestLP, mockOffRamp, mockPriceRegistry, backendClient, 0), - } - - tokenMapping, err := plugin.tokenDecimalsCache.Get(testutils.Context(t)) - require.NoError(t, err) - assert.Equal(t, len(tokens), len(tokenMapping)) - assert.Equal(t, uint8(18), tokenMapping[destToken]) - assert.Equal(t, uint8(18), tokenMapping[feeToken]) -} - -func TestCalculateUsdPer1e18TokenAmount(t *testing.T) { - t.Parallel() +func TestCommitReportingPlugin_nextMinSeqNum(t *testing.T) { + lggr := logger.TestLogger(t) + root1 := utils.Keccak256Fixed(hexutil.MustDecode("0xaa")) - tests := []struct { - name string - price *big.Int - decimal uint8 - wantResult *big.Int + var tt = []struct { + onChainMin uint64 + inflight []commit_store.CommitStoreCommitReport + expectedOnChainMin uint64 + expectedInflightMin uint64 }{ { - name: "18-decimal token, $6.5 per token", - price: big.NewInt(65e17), - decimal: 18, - wantResult: big.NewInt(65e17), + onChainMin: uint64(1), + inflight: nil, + expectedInflightMin: uint64(1), + expectedOnChainMin: uint64(1), }, { - name: "6-decimal token, $1 per token", - price: big.NewInt(1e18), - decimal: 6, - wantResult: new(big.Int).Mul(big.NewInt(1e18), big.NewInt(1e12)), // 1e30 - }, + onChainMin: uint64(1), + inflight: []commit_store.CommitStoreCommitReport{ + {Interval: commit_store.CommitStoreInterval{Min: uint64(1), Max: uint64(2)}, MerkleRoot: root1}}, + expectedInflightMin: uint64(3), + expectedOnChainMin: uint64(1), + }, { - name: "0-decimal token, $1 per token", - price: big.NewInt(1e18), - decimal: 0, - wantResult: new(big.Int).Mul(big.NewInt(1e18), big.NewInt(1e18)), // 1e36 + onChainMin: uint64(1), + inflight: []commit_store.CommitStoreCommitReport{ + {Interval: commit_store.CommitStoreInterval{Min: uint64(3), Max: uint64(4)}, MerkleRoot: root1}}, + expectedInflightMin: uint64(5), + expectedOnChainMin: uint64(1), }, { - name: "36-decimal token, $1 per token", - price: big.NewInt(1e18), - decimal: 36, - wantResult: big.NewInt(1), + onChainMin: uint64(1), + inflight: []commit_store.CommitStoreCommitReport{ + {Interval: commit_store.CommitStoreInterval{Min: uint64(1), Max: uint64(MaxInflightSeqNumGap + 2)}, MerkleRoot: root1}}, + expectedInflightMin: uint64(1), + expectedOnChainMin: uint64(1), }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := calculateUsdPer1e18TokenAmount(tt.price, tt.decimal) - assert.Equal(t, tt.wantResult, got) - }) + for _, tc := range tt { + commitStore, _ := testhelpers.NewFakeCommitStore(t, tc.onChainMin) + cp := CommitReportingPlugin{config: CommitPluginConfig{commitStore: commitStore}, inflightReports: newInflightCommitReportsContainer(time.Hour)} + epochAndRound := uint64(1) + for _, rep := range tc.inflight { + rc := rep + require.NoError(t, cp.inflightReports.add(lggr, rc, epochAndRound)) + epochAndRound++ + } + t.Log("inflight", cp.inflightReports.maxInflightSeqNr()) + inflightMin, onchainMin, err := cp.nextMinSeqNum(context.Background(), lggr) + require.NoError(t, err) + assert.Equal(t, tc.expectedInflightMin, inflightMin) + assert.Equal(t, tc.expectedOnChainMin, onchainMin) + cp.inflightReports.reset(lggr) } } -func TestShouldTransmitAcceptedReport(t *testing.T) { - th := setupCommitTestHarness(t) - tokenPrice := big.NewInt(9e18) // $9 - gasPrice := big.NewInt(1500e9) // $1500 per eth * 1gwei +func TestCommitReportingPlugin_isStaleReport(t *testing.T) { + ctx := context.Background() + lggr := logger.TestLogger(t) + merkleRoot1 := utils.Keccak256Fixed([]byte("some merkle root 1")) + merkleRoot2 := utils.Keccak256Fixed([]byte("some merkle root 2")) - nextMinSeqNr := uint64(10) - _, err := th.Dest.CommitStore.SetMinSeqNr(th.Dest.User, nextMinSeqNr) - require.NoError(t, err) - _, err = th.Dest.PriceRegistry.UpdatePrices(th.Dest.User, price_registry.InternalPriceUpdates{ - TokenPriceUpdates: []price_registry.InternalTokenPriceUpdate{ - {SourceToken: th.Dest.LinkToken.Address(), UsdPerToken: tokenPrice}, - }, - DestChainSelector: th.Source.ChainSelector, - UsdPerUnitGas: gasPrice, + t.Run("empty report", func(t *testing.T) { + commitStore, _ := testhelpers.NewFakeCommitStore(t, 1) + r := &CommitReportingPlugin{config: CommitPluginConfig{commitStore: commitStore}} + isStale := r.isStaleReport(ctx, lggr, commit_store.CommitStoreCommitReport{}, false, types.ReportTimestamp{}) + assert.True(t, isStale) }) - require.NoError(t, err) - th.CommitAndPollLogs(t) - round := uint8(1) - tests := []struct { - name string - seq uint64 - gasPrice *big.Int - tokenPrice *big.Int - expected bool + t.Run("merkle root", func(t *testing.T) { + const expNextSeqNum = uint64(9) + commitStore, _ := testhelpers.NewFakeCommitStore(t, expNextSeqNum) + + r := &CommitReportingPlugin{ + config: CommitPluginConfig{commitStore: commitStore}, + inflightReports: &inflightCommitReportsContainer{ + inFlight: map[[32]byte]InflightCommitReport{ + merkleRoot2: { + report: commit_store.CommitStoreCommitReport{ + Interval: commit_store.CommitStoreInterval{Min: expNextSeqNum + 1, Max: expNextSeqNum + 10}, + }, + }, + }, + }, + } + + assert.False(t, r.isStaleReport(ctx, lggr, commit_store.CommitStoreCommitReport{ + MerkleRoot: merkleRoot1, + Interval: commit_store.CommitStoreInterval{Min: expNextSeqNum + 1, Max: expNextSeqNum + 10}, + }, false, types.ReportTimestamp{})) + + assert.True(t, r.isStaleReport(ctx, lggr, commit_store.CommitStoreCommitReport{ + MerkleRoot: merkleRoot1, + Interval: commit_store.CommitStoreInterval{Min: expNextSeqNum + 1, Max: expNextSeqNum + 10}, + }, true, types.ReportTimestamp{})) + + assert.True(t, r.isStaleReport(ctx, lggr, commit_store.CommitStoreCommitReport{ + MerkleRoot: merkleRoot1}, false, types.ReportTimestamp{})) + }) +} + +func TestCommitReportingPlugin_calculateMinMaxSequenceNumbers(t *testing.T) { + testCases := []struct { + name string + commitStoreSeqNum uint64 + inflightSeqNum uint64 + msgSeqNums []uint64 + + expQueryMin uint64 // starting seq num that is used in the query to get messages + expMin uint64 + expMax uint64 + expErr bool }{ - {"base", nextMinSeqNr, nil, nil, true}, - {"future", nextMinSeqNr + 10, nil, nil, true}, - {"empty", 0, nil, nil, false}, - {"gasPrice update", 0, big.NewInt(10), nil, true}, - {"gasPrice stale", 0, gasPrice, nil, false}, - {"tokenPrice update", 0, nil, big.NewInt(20), true}, - {"tokenPrice stale", 0, nil, tokenPrice, false}, - {"token price and gas price stale", 0, gasPrice, tokenPrice, false}, + { + name: "happy flow inflight", + commitStoreSeqNum: 9, + inflightSeqNum: 10, + msgSeqNums: []uint64{11, 12, 13, 14}, + expQueryMin: 11, // inflight+1 + expMin: 11, + expMax: 14, + expErr: false, + }, + { + name: "happy flow no inflight", + commitStoreSeqNum: 9, + msgSeqNums: []uint64{11, 12, 13, 14}, + expQueryMin: 9, // from commit store + expMin: 11, + expMax: 14, + expErr: false, + }, + { + name: "gap in msg seq nums", + commitStoreSeqNum: 10, + inflightSeqNum: 9, + expQueryMin: 10, + msgSeqNums: []uint64{11, 12, 14}, + expErr: true, + }, + { + name: "no new messages", + commitStoreSeqNum: 9, + msgSeqNums: []uint64{}, + expQueryMin: 9, + expMin: 0, + expMax: 0, + expErr: false, + }, + { + name: "unordered seq nums", + commitStoreSeqNum: 9, + msgSeqNums: []uint64{11, 13, 14, 10}, + expQueryMin: 9, + expErr: true, + }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var destChainSelector uint64 - gasPrice := new(big.Int) - if tt.gasPrice != nil { - destChainSelector = th.Source.ChainSelector - gasPrice = tt.gasPrice - } + ctx := testutils.Context(t) + lggr := logger.TestLogger(t) - var tokenPrices []commit_store.InternalTokenPriceUpdate - if tt.tokenPrice != nil { - tokenPrices = []commit_store.InternalTokenPriceUpdate{ - {SourceToken: th.Dest.LinkToken.Address(), UsdPerToken: tt.tokenPrice}, + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + p := &CommitReportingPlugin{} + commitStore, _ := testhelpers.NewFakeCommitStore(t, tc.commitStoreSeqNum) + p.config.commitStore = commitStore + + p.inflightReports = newInflightCommitReportsContainer(time.Minute) + if tc.inflightSeqNum > 0 { + p.inflightReports.inFlight[[32]byte{}] = InflightCommitReport{ + report: commit_store.CommitStoreCommitReport{ + Interval: commit_store.CommitStoreInterval{ + Min: tc.inflightSeqNum, + Max: tc.inflightSeqNum, + }, + }, } - } else { - tokenPrices = []commit_store.InternalTokenPriceUpdate{} } - var root [32]byte - if tt.seq > 0 { - root = testutils.Random32Byte() + sourceReader := ccipdata.NewMockReader(t) + var sendReqs []ccipdata.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested] + for _, seqNum := range tc.msgSeqNums { + sendReqs = append(sendReqs, ccipdata.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested]{ + Data: evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested{ + Message: evm_2_evm_onramp.InternalEVM2EVMMessage{SequenceNumber: seqNum}, + }, + }) } + sourceReader.On("GetSendRequestsGteSeqNum", ctx, mock.Anything, tc.expQueryMin, false, 0).Return(sendReqs, nil) + p.config.sourceReader = sourceReader - report, err := abihelpers.EncodeCommitReport(commit_store.CommitStoreCommitReport{ - PriceUpdates: commit_store.InternalPriceUpdates{ - TokenPriceUpdates: tokenPrices, - DestChainSelector: destChainSelector, - UsdPerUnitGas: gasPrice, - }, - MerkleRoot: root, - Interval: commit_store.CommitStoreInterval{Min: tt.seq, Max: tt.seq}, - }) - require.NoError(t, err) + minSeqNum, maxSeqNum, err := p.calculateMinMaxSequenceNumbers(ctx, lggr) + if tc.expErr { + assert.Error(t, err) + return + } - got, err := th.plugin.ShouldTransmitAcceptedReport(testutils.Context(t), types.ReportTimestamp{Epoch: 1, Round: round}, report) - round++ - require.NoError(t, err) - assert.Equal(t, tt.expected, got) + assert.Equal(t, tc.expMin, minSeqNum) + assert.Equal(t, tc.expMax, maxSeqNum) }) } } -func TestShouldAcceptFinalizedReport(t *testing.T) { - nextMinSeqNr := uint64(10) +func TestCommitReportingPlugin_getLatestGasPriceUpdate(t *testing.T) { + now := time.Now() - tests := []struct { - name string - seq uint64 - latestPriceEpochAndRound int64 - epoch uint32 - round uint8 - destChainSelector int - skipRoot bool - expected bool - err bool + testCases := []struct { + name string + checkInflight bool + inflightGasPriceUpdate *update + destGasPriceUpdates []update + expUpdate update + expErr bool }{ { - name: "future", - seq: nextMinSeqNr * 2, - epoch: 1, - round: 1, + name: "only inflight gas price", + checkInflight: true, + inflightGasPriceUpdate: &update{timestamp: now, value: big.NewInt(1000)}, + expUpdate: update{timestamp: now, value: big.NewInt(1000)}, + expErr: false, }, { - name: "empty", - epoch: 1, - round: 2, - }, - { - name: "stale", - seq: nextMinSeqNr - 1, - epoch: 1, - round: 3, + name: "inflight price is nil", + checkInflight: true, + inflightGasPriceUpdate: &update{timestamp: now, value: nil}, + destGasPriceUpdates: []update{ + {timestamp: now.Add(time.Minute), value: big.NewInt(2000)}, + {timestamp: now.Add(2 * time.Minute), value: big.NewInt(3000)}, + }, + expUpdate: update{timestamp: now.Add(2 * time.Minute), value: big.NewInt(3000)}, + expErr: false, }, { - name: "base", - seq: nextMinSeqNr, - epoch: 1, - round: 4, - expected: true, + name: "inflight updates are skipped", + checkInflight: false, + inflightGasPriceUpdate: &update{timestamp: now, value: big.NewInt(1000)}, + destGasPriceUpdates: []update{ + {timestamp: now.Add(time.Minute), value: big.NewInt(2000)}, + {timestamp: now.Add(2 * time.Minute), value: big.NewInt(3000)}, + }, + expUpdate: update{timestamp: now.Add(2 * time.Minute), value: big.NewInt(3000)}, + expErr: false, }, + } + + ctx := testutils.Context(t) + lggr := logger.TestLogger(t) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + p := &CommitReportingPlugin{} + p.inflightReports = newInflightCommitReportsContainer(time.Minute) + p.lggr = lggr + destPriceRegistry, _ := testhelpers.NewFakePriceRegistry(t) + p.destPriceRegistry = destPriceRegistry + + if tc.inflightGasPriceUpdate != nil { + p.inflightReports.inFlightPriceUpdates = append( + p.inflightReports.inFlightPriceUpdates, + InflightPriceUpdate{ + createdAt: tc.inflightGasPriceUpdate.timestamp, + priceUpdates: commit_store.InternalPriceUpdates{ + DestChainSelector: 1234, + UsdPerUnitGas: tc.inflightGasPriceUpdate.value, + }, + }, + ) + } + + if len(tc.destGasPriceUpdates) > 0 { + var events []ccipdata.Event[price_registry.PriceRegistryUsdPerUnitGasUpdated] + for _, u := range tc.destGasPriceUpdates { + events = append(events, ccipdata.Event[price_registry.PriceRegistryUsdPerUnitGasUpdated]{ + Data: price_registry.PriceRegistryUsdPerUnitGasUpdated{ + Value: u.value, + Timestamp: big.NewInt(u.timestamp.Unix()), + }, + }) + } + destReader := ccipdata.NewMockReader(t) + destReader.On("GetGasPriceUpdatesCreatedAfter", ctx, mock.Anything, uint64(0), mock.Anything, 0).Return(events, nil) + p.config.destReader = destReader + } + + priceUpdate, err := p.getLatestGasPriceUpdate(ctx, time.Now(), tc.checkInflight) + 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()) + }) + } +} + +func TestCommitReportingPlugin_getLatestTokenPriceUpdates(t *testing.T) { + now := time.Now() + tk1 := utils.RandomAddress() + tk2 := utils.RandomAddress() + + testCases := []struct { + name string + priceRegistryUpdates []price_registry.PriceRegistryUsdPerTokenUpdated + checkInflight bool + inflightUpdates map[common.Address]update + expUpdates map[common.Address]update + expErr bool + }{ { - name: "price update - epoch and round is ok", - seq: nextMinSeqNr, - latestPriceEpochAndRound: int64(mergeEpochAndRound(2, 10)), - epoch: 2, - round: 11, - destChainSelector: rand.Int(), - skipRoot: true, - expected: true, + name: "ignore inflight updates", + priceRegistryUpdates: []price_registry.PriceRegistryUsdPerTokenUpdated{ + { + Token: tk1, + Value: big.NewInt(1000), + Timestamp: big.NewInt(now.Add(1 * time.Minute).Unix()), + }, + { + Token: tk2, + Value: big.NewInt(2000), + Timestamp: big.NewInt(now.Add(2 * time.Minute).Unix()), + }, + }, + checkInflight: false, + expUpdates: map[common.Address]update{ + tk1: {timestamp: now.Add(1 * time.Minute), value: big.NewInt(1000)}, + tk2: {timestamp: now.Add(2 * time.Minute), value: big.NewInt(2000)}, + }, + expErr: false, }, { - name: "price update - epoch and round is behind", - seq: nextMinSeqNr, - latestPriceEpochAndRound: int64(mergeEpochAndRound(2, 10)), - epoch: 2, - round: 9, - destChainSelector: rand.Int(), - skipRoot: true, + name: "consider inflight updates", + priceRegistryUpdates: []price_registry.PriceRegistryUsdPerTokenUpdated{ + { + Token: tk1, + Value: big.NewInt(1000), + Timestamp: big.NewInt(now.Add(1 * time.Minute).Unix()), + }, + { + Token: tk2, + Value: big.NewInt(2000), + Timestamp: big.NewInt(now.Add(2 * time.Minute).Unix()), + }, + }, + checkInflight: true, + inflightUpdates: map[common.Address]update{ + tk1: {timestamp: now, value: big.NewInt(500)}, // inflight but older + tk2: {timestamp: now.Add(4 * time.Minute), value: big.NewInt(4000)}, + }, + expUpdates: map[common.Address]update{ + tk1: {timestamp: now.Add(1 * time.Minute), value: big.NewInt(1000)}, + tk2: {timestamp: now.Add(4 * time.Minute), value: big.NewInt(4000)}, + }, + expErr: false, }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - th := setupCommitTestHarness(t) - _, err := th.Dest.CommitStore.SetMinSeqNr(th.Dest.User, nextMinSeqNr) - require.NoError(t, err) - - _, err = th.Dest.CommitStoreHelper.SetLatestPriceEpochAndRound(th.Dest.User, big.NewInt(tt.latestPriceEpochAndRound)) - require.NoError(t, err) - - th.CommitAndPollLogs(t) - - var root [32]byte - if tt.seq > 0 { - root = testutils.Random32Byte() + ctx := testutils.Context(t) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + p := &CommitReportingPlugin{} + + priceReg, priceRegAddr := testhelpers.NewFakePriceRegistry(t) + p.destPriceRegistry = priceReg + + destReader := ccipdata.NewMockReader(t) + var events []ccipdata.Event[price_registry.PriceRegistryUsdPerTokenUpdated] + for _, up := range tc.priceRegistryUpdates { + events = append(events, ccipdata.Event[price_registry.PriceRegistryUsdPerTokenUpdated]{ + Data: price_registry.PriceRegistryUsdPerTokenUpdated{ + Token: up.Token, + Value: up.Value, + Timestamp: up.Timestamp, + }, + }) + } + destReader.On("GetTokenPriceUpdatesCreatedAfter", ctx, priceRegAddr, mock.Anything, 0).Return(events, nil) + p.config.destReader = destReader + + p.inflightReports = newInflightCommitReportsContainer(time.Minute) + if len(tc.inflightUpdates) > 0 { + for tk, upd := range tc.inflightUpdates { + p.inflightReports.inFlightPriceUpdates = append(p.inflightReports.inFlightPriceUpdates, InflightPriceUpdate{ + createdAt: upd.timestamp, + priceUpdates: commit_store.InternalPriceUpdates{ + TokenPriceUpdates: []commit_store.InternalTokenPriceUpdate{ + {SourceToken: tk, UsdPerToken: upd.value}, + }, + }, + }) + } } - r := commit_store.CommitStoreCommitReport{ - PriceUpdates: commit_store.InternalPriceUpdates{ - TokenPriceUpdates: []commit_store.InternalTokenPriceUpdate{}, - DestChainSelector: uint64(tt.destChainSelector), - UsdPerUnitGas: new(big.Int), - }, - Interval: commit_store.CommitStoreInterval{Min: tt.seq, Max: tt.seq}, + updates, err := p.getLatestTokenPriceUpdates(ctx, now, tc.checkInflight) + if tc.expErr { + assert.Error(t, err) + return } - if !tt.skipRoot { - r.MerkleRoot = root + assert.NoError(t, err) + assert.Equal(t, len(tc.expUpdates), len(updates)) + for k, v := range updates { + assert.Equal(t, tc.expUpdates[k].timestamp.Truncate(time.Second), v.timestamp.Truncate(time.Second)) + assert.Equal(t, tc.expUpdates[k].value.Uint64(), v.value.Uint64()) } - report, err := abihelpers.EncodeCommitReport(r) - require.NoError(t, err) + }) + } + +} - got, err := th.plugin.ShouldAcceptFinalizedReport( - testutils.Context(t), - types.ReportTimestamp{Epoch: tt.epoch, Round: tt.round}, report) - if tt.err { +func Test_commitReportSize(t *testing.T) { + testParams := gopter.DefaultTestParameters() + testParams.MinSuccessfulTests = 100 + p := gopter.NewProperties(testParams) + p.Property("bounded commit report size", prop.ForAll(func(root []byte, min, max uint64) bool { + var root32 [32]byte + copy(root32[:], root) + rep, err := abihelpers.EncodeCommitReport(commit_store.CommitStoreCommitReport{ + MerkleRoot: root32, + Interval: commit_store.CommitStoreInterval{Min: min, Max: max}, + PriceUpdates: commit_store.InternalPriceUpdates{ + TokenPriceUpdates: []commit_store.InternalTokenPriceUpdate{}, + DestChainSelector: 1337, + UsdPerUnitGas: big.NewInt(2000e9), // $2000 per eth * 1gwei = 2000e9 + }, + }) + require.NoError(t, err) + return len(rep) <= MaxCommitReportLength + }, gen.SliceOfN(32, gen.UInt8()), gen.UInt64(), gen.UInt64())) + p.TestingRun(t) +} + +func Test_calculateIntervalConsensus(t *testing.T) { + tests := []struct { + name string + intervals []commit_store.CommitStoreInterval + rangeLimit uint64 + f int + wantMin uint64 + wantMax uint64 + wantErr bool + }{ + {"no obs", []commit_store.CommitStoreInterval{{Min: 0, Max: 0}}, 0, 0, 0, 0, false}, + {"basic", []commit_store.CommitStoreInterval{ + {Min: 9, Max: 14}, + {Min: 10, Max: 12}, + {Min: 10, Max: 14}, + }, 0, 1, 10, 14, false}, + {"not enough intervals", []commit_store.CommitStoreInterval{}, 0, 1, 0, 0, true}, + {"min > max", []commit_store.CommitStoreInterval{ + {Min: 9, Max: 4}, + {Min: 10, Max: 4}, + {Min: 10, Max: 6}, + }, 0, 1, 0, 0, true}, + { + "range limit", []commit_store.CommitStoreInterval{ + {Min: 10, Max: 100}, + {Min: 1, Max: 1000}, + }, 256, 1, 10, 265, false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := calculateIntervalConsensus(tt.intervals, tt.f, tt.rangeLimit) + if tt.wantErr { require.Error(t, err) } else { require.NoError(t, err) } - assert.Equal(t, tt.expected, got) + assert.Equal(t, tt.wantMin, got.Min) + assert.Equal(t, tt.wantMax, got.Max) + }) + } +} - if got { // already added to inflight, should not be accepted again - got, err = th.plugin.ShouldAcceptFinalizedReport( - testutils.Context(t), - types.ReportTimestamp{Epoch: tt.epoch, Round: tt.round}, report) - require.NoError(t, err) - assert.False(t, got) - } +func Test_calculateUsdPer1e18TokenAmount(t *testing.T) { + tests := []struct { + name string + price *big.Int + decimal uint8 + wantResult *big.Int + }{ + { + name: "18-decimal token, $6.5 per token", + price: big.NewInt(65e17), + decimal: 18, + wantResult: big.NewInt(65e17), + }, + { + name: "6-decimal token, $1 per token", + price: big.NewInt(1e18), + decimal: 6, + wantResult: new(big.Int).Mul(big.NewInt(1e18), big.NewInt(1e12)), // 1e30 + }, + { + name: "0-decimal token, $1 per token", + price: big.NewInt(1e18), + decimal: 0, + wantResult: new(big.Int).Mul(big.NewInt(1e18), big.NewInt(1e18)), // 1e36 + }, + { + name: "36-decimal token, $1 per token", + price: big.NewInt(1e18), + decimal: 36, + wantResult: big.NewInt(1), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := calculateUsdPer1e18TokenAmount(tt.price, tt.decimal) + assert.Equal(t, tt.wantResult, got) }) } } @@ -1139,105 +1413,9 @@ func TestCommitReportToEthTxMeta(t *testing.T) { } } -func TestNextMin(t *testing.T) { - lggr := logger.TestLogger(t) - commitStore := mock_contracts.CommitStoreInterface{} - cp := CommitReportingPlugin{config: CommitPluginConfig{commitStore: &commitStore}, inflightReports: newInflightCommitReportsContainer(time.Hour)} - root1 := utils.Keccak256Fixed(hexutil.MustDecode("0xaa")) - var tt = []struct { - onChainMin uint64 - inflight []commit_store.CommitStoreCommitReport - expectedOnChainMin uint64 - expectedInflightMin uint64 - }{ - { - onChainMin: uint64(1), - inflight: nil, - expectedInflightMin: uint64(1), - expectedOnChainMin: uint64(1), - }, - { - onChainMin: uint64(1), - inflight: []commit_store.CommitStoreCommitReport{ - {Interval: commit_store.CommitStoreInterval{Min: uint64(1), Max: uint64(2)}, MerkleRoot: root1}}, - expectedInflightMin: uint64(3), - expectedOnChainMin: uint64(1), - }, - { - onChainMin: uint64(1), - inflight: []commit_store.CommitStoreCommitReport{ - {Interval: commit_store.CommitStoreInterval{Min: uint64(3), Max: uint64(4)}, MerkleRoot: root1}}, - expectedInflightMin: uint64(5), - expectedOnChainMin: uint64(1), - }, - { - onChainMin: uint64(1), - inflight: []commit_store.CommitStoreCommitReport{ - {Interval: commit_store.CommitStoreInterval{Min: uint64(1), Max: uint64(MaxInflightSeqNumGap + 2)}, MerkleRoot: root1}}, - expectedInflightMin: uint64(1), - expectedOnChainMin: uint64(1), - }, - } - for _, tc := range tt { - commitStore.On("GetExpectedNextSequenceNumber", mock.Anything).Return(tc.onChainMin, nil) - epochAndRound := uint64(1) - for _, rep := range tc.inflight { - rc := rep - require.NoError(t, cp.inflightReports.add(lggr, rc, epochAndRound)) - epochAndRound++ - } - t.Log("inflight", cp.inflightReports.maxInflightSeqNr()) - inflightMin, onchainMin, err := cp.nextMinSeqNum(context.Background(), lggr) - require.NoError(t, err) - assert.Equal(t, tc.expectedInflightMin, inflightMin) - assert.Equal(t, tc.expectedOnChainMin, onchainMin) - cp.inflightReports.reset(lggr) - } -} - -func Test_isStaleReport(t *testing.T) { - ctx := context.Background() - lggr := logger.TestLogger(t) - merkleRoot1 := utils.Keccak256Fixed([]byte("some merkle root 1")) - merkleRoot2 := utils.Keccak256Fixed([]byte("some merkle root 2")) - - t.Run("empty report", func(t *testing.T) { - commitStore := mock_contracts.NewCommitStoreInterface(t) - r := &CommitReportingPlugin{config: CommitPluginConfig{commitStore: commitStore}} - isStale := r.isStaleReport(ctx, lggr, commit_store.CommitStoreCommitReport{}, false, types.ReportTimestamp{}) - assert.True(t, isStale) - }) - - t.Run("merkle root", func(t *testing.T) { - const expNextSeqNum = uint64(9) - - commitStore := mock_contracts.NewCommitStoreInterface(t) - commitStore.On("GetExpectedNextSequenceNumber", mock.Anything).Return(expNextSeqNum, nil) - - r := &CommitReportingPlugin{ - config: CommitPluginConfig{commitStore: commitStore}, - inflightReports: &inflightCommitReportsContainer{ - inFlight: map[[32]byte]InflightCommitReport{ - merkleRoot2: { - report: commit_store.CommitStoreCommitReport{ - Interval: commit_store.CommitStoreInterval{Min: expNextSeqNum + 1, Max: expNextSeqNum + 10}, - }, - }, - }, - }, - } +// leafHasher123 always returns '123' followed by zeroes in HashLeaf method. +type leafHasher123 struct{} - assert.False(t, r.isStaleReport(ctx, lggr, commit_store.CommitStoreCommitReport{ - MerkleRoot: merkleRoot1, - Interval: commit_store.CommitStoreInterval{Min: expNextSeqNum + 1, Max: expNextSeqNum + 10}, - }, false, types.ReportTimestamp{})) - - assert.True(t, r.isStaleReport(ctx, lggr, commit_store.CommitStoreCommitReport{ - MerkleRoot: merkleRoot1, - Interval: commit_store.CommitStoreInterval{Min: expNextSeqNum + 1, Max: expNextSeqNum + 10}, - }, true, types.ReportTimestamp{})) - - assert.True(t, r.isStaleReport(ctx, lggr, commit_store.CommitStoreCommitReport{ - MerkleRoot: merkleRoot1}, false, types.ReportTimestamp{})) - }) +func (h leafHasher123) HashLeaf(_ gethtypes.Log) ([32]byte, error) { + return [32]byte{123}, nil } diff --git a/core/services/ocr2/plugins/ccip/config/offchain_config_test.go b/core/services/ocr2/plugins/ccip/config/offchain_config_test.go index 2a42d65e96..5edf21c176 100644 --- a/core/services/ocr2/plugins/ccip/config/offchain_config_test.go +++ b/core/services/ocr2/plugins/ccip/config/offchain_config_test.go @@ -109,7 +109,8 @@ func TestExecOffchainConfig_Encoding(t *testing.T) { } for name, tc := range tests { t.Run(name, func(t *testing.T) { - encode, err := EncodeOffchainConfig(&tc.want) + exp := tc.want + encode, err := EncodeOffchainConfig(&exp) require.NoError(t, err) got, err := DecodeOffchainConfig[ExecOffchainConfig](encode) diff --git a/core/services/ocr2/plugins/ccip/execution_batch_building.go b/core/services/ocr2/plugins/ccip/execution_batch_building.go index 5f2019c24f..ee02ccc311 100644 --- a/core/services/ocr2/plugins/ccip/execution_batch_building.go +++ b/core/services/ocr2/plugins/ccip/execution_batch_building.go @@ -7,16 +7,14 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/pkg/errors" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/commit_store" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_offramp" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_onramp" "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipevents" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/hashlib" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/merklemulti" - "github.com/smartcontractkit/chainlink/v2/core/services/pg" ) func getProofData( @@ -24,10 +22,10 @@ func getProofData( lggr logger.Logger, hashLeaf hashlib.LeafHasherInterface[[32]byte], onRampAddress common.Address, - sourceEventsClient ccipevents.Client, + sourceReader ccipdata.Reader, interval commit_store.CommitStoreInterval, -) (sendReqsInRoot []ccipevents.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested], leaves [][32]byte, tree *merklemulti.Tree[[32]byte], err error) { - sendReqs, err := sourceEventsClient.GetSendRequestsBetweenSeqNums( +) (sendReqsInRoot []ccipdata.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested], leaves [][32]byte, tree *merklemulti.Tree[[32]byte], err error) { + sendReqs, err := sourceReader.GetSendRequestsBetweenSeqNums( ctx, onRampAddress, interval.Min, @@ -99,27 +97,18 @@ func validateSeqNumbers(serviceCtx context.Context, commitStore commit_store.Com } // Gets the commit report from the saved logs for a given sequence number. -func getCommitReportForSeqNum(ctx context.Context, dstLogPoller logpoller.LogPoller, commitStore commit_store.CommitStoreInterface, seqNr uint64) (commit_store.CommitStoreCommitReport, error) { - // fetch commitReports which report.Interval.Max >= seqNr - logs, err := dstLogPoller.LogsDataWordGreaterThan( - abihelpers.EventSignatures.ReportAccepted, - commitStore.Address(), - abihelpers.EventSignatures.ReportAcceptedMaxSequenceNumberWord, - logpoller.EvmWord(seqNr), - 0, - pg.WithParentCtx(ctx), - ) +func getCommitReportForSeqNum(ctx context.Context, destReader ccipdata.Reader, commitStore commit_store.CommitStoreInterface, seqNum uint64) (commit_store.CommitStoreCommitReport, error) { + acceptedReports, err := destReader.GetAcceptedCommitReportsGteSeqNum(ctx, commitStore.Address(), seqNum, 0) if err != nil { return commit_store.CommitStoreCommitReport{}, err } - for _, log := range logs { - reportAccepted, err := commitStore.ParseReportAccepted(log.ToGethLog()) - if err != nil { - return commit_store.CommitStoreCommitReport{}, err - } - if reportAccepted.Report.Interval.Min <= seqNr && seqNr <= reportAccepted.Report.Interval.Max { - return reportAccepted.Report, nil + + for _, acceptedReport := range acceptedReports { + reportInterval := acceptedReport.Data.Report.Interval + if reportInterval.Min <= seqNum && seqNum <= reportInterval.Max { + return acceptedReport.Data.Report, nil } } + return commit_store.CommitStoreCommitReport{}, errors.Errorf("seq number not committed") } diff --git a/core/services/ocr2/plugins/ccip/execution_plugin.go b/core/services/ocr2/plugins/ccip/execution_plugin.go index 2e9f45e709..d09ae5fc8e 100644 --- a/core/services/ocr2/plugins/ccip/execution_plugin.go +++ b/core/services/ocr2/plugins/ccip/execution_plugin.go @@ -15,7 +15,7 @@ import ( libocr2 "github.com/smartcontractkit/libocr/offchainreporting2plus" relaylogger "github.com/smartcontractkit/chainlink-relay/pkg/logger" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipevents" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/hashlib" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/oraclelib" @@ -112,8 +112,8 @@ func NewExecutionServices(lggr logger.Logger, jb job.Job, chainSet evm.LegacyCha lggr: execLggr, sourceLP: sourceChain.LogPoller(), destLP: destChain.LogPoller(), - sourceEvents: ccipevents.NewLogPollerClient(sourceChain.LogPoller(), execLggr, sourceChain.Client()), - destEvents: ccipevents.NewLogPollerClient(destChain.LogPoller(), execLggr, destChain.Client()), + sourceReader: ccipdata.NewLogPollerReader(sourceChain.LogPoller(), execLggr, sourceChain.Client()), + destReader: ccipdata.NewLogPollerReader(destChain.LogPoller(), execLggr, destChain.Client()), onRamp: onRamp, offRamp: offRamp, commitStore: commitStore, @@ -130,7 +130,7 @@ func NewExecutionServices(lggr logger.Logger, jb job.Job, chainSet evm.LegacyCha return nil, err } - argsNoPlugin.ReportingPluginFactory = promwrapper.NewPromFactory(wrappedPluginFactory, "CCIPExecution", string(spec.Relay), destChain.ID()) + argsNoPlugin.ReportingPluginFactory = promwrapper.NewPromFactory(wrappedPluginFactory, "CCIPExecution", spec.Relay, destChain.ID()) argsNoPlugin.Logger = relaylogger.NewOCRWrapper(execLggr, true, logError) oracle, err := libocr2.NewOracle(argsNoPlugin) if err != nil { @@ -299,10 +299,9 @@ func unregisterExecutionPluginLpFilters( } // ExecutionReportToEthTxMeta generates a txmgr.EthTxMeta from the given report. -// all the message ids will be added to the tx metadata. +// Only MessageIDs will be populated in the TxMeta. func ExecutionReportToEthTxMeta(report []byte) (*txmgr.TxMeta, error) { execReport, err := abihelpers.DecodeExecutionReport(report) - if err != nil { return nil, err } diff --git a/core/services/ocr2/plugins/ccip/execution_plugin_test.go b/core/services/ocr2/plugins/ccip/execution_plugin_test.go index 6ab5a0098e..4fc7b2a747 100644 --- a/core/services/ocr2/plugins/ccip/execution_plugin_test.go +++ b/core/services/ocr2/plugins/ccip/execution_plugin_test.go @@ -2,7 +2,6 @@ package ccip import ( "context" - "fmt" "testing" "github.com/ethereum/go-ethereum/common" @@ -13,9 +12,8 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/chains/evm/mocks" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_offramp" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_onramp" - mock_contracts "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/mocks" "github.com/smartcontractkit/chainlink/v2/core/services/job" - ccipconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/testhelpers" ) func TestGetExecutionPluginFilterNamesFromSpec(t *testing.T) { @@ -66,29 +64,19 @@ func TestGetExecutionPluginFilterNamesFromSpec(t *testing.T) { } func TestGetExecutionPluginFilterNames(t *testing.T) { - specContractID := common.HexToAddress("0xdafea492d9c6733ae3d56b7ed1adb60692c98bc1") // off-ramp addr - onRampAddr := common.HexToAddress("0xdafea492d9c6733ae3d56b7ed1adb60692c98bc2") commitStoreAddr := common.HexToAddress("0xdafea492d9c6733ae3d56b7ed1adb60692c98bc3") srcPriceRegAddr := common.HexToAddress("0xdafea492d9c6733ae3d56b7ed1adb60692c98bc9") dstPriceRegAddr := common.HexToAddress("0xdafea492d9c6733ae3d56b7ed1adb60692c98b19") - mockOffRamp := mock_contracts.NewEVM2EVMOffRampInterface(t) - mockOffRamp.On("Address").Return(specContractID) - mockOffRamp.On("GetDynamicConfig", mock.Anything).Return( - evm_2_evm_offramp.EVM2EVMOffRampDynamicConfig{ - PriceRegistry: dstPriceRegAddr, - }, nil) + mockOffRamp, offRampAddr := testhelpers.NewFakeOffRamp(t) + mockOffRamp.SetDynamicConfig(evm_2_evm_offramp.EVM2EVMOffRampDynamicConfig{PriceRegistry: dstPriceRegAddr}) - mockOnRamp := mock_contracts.NewEVM2EVMOnRampInterface(t) - mockOnRamp.On("TypeAndVersion", mock.Anything).Return(fmt.Sprintf("%s %s", ccipconfig.EVM2EVMOnRamp, "1.2.0"), nil) - mockOnRamp.On("GetDynamicConfig", mock.Anything).Return( - evm_2_evm_onramp.EVM2EVMOnRampDynamicConfig{ - PriceRegistry: srcPriceRegAddr, - }, nil) + mockOnRamp, onRampAddr := testhelpers.NewFakeOnRamp(t) + mockOnRamp.SetDynamicCfg(evm_2_evm_onramp.EVM2EVMOnRampDynamicConfig{PriceRegistry: srcPriceRegAddr}) srcLP := mocklp.NewLogPoller(t) srcFilters := []string{ - "Exec ccip sends - 0xdafea492D9c6733aE3d56B7ED1aDb60692C98bc2", + "Exec ccip sends - " + onRampAddr.String(), "Fee token added - 0xdAFea492D9c6733aE3d56B7ed1ADb60692c98bC9", "Fee token removed - 0xdAFea492D9c6733aE3d56B7ed1ADb60692c98bC9", } @@ -99,9 +87,9 @@ func TestGetExecutionPluginFilterNames(t *testing.T) { dstLP := mocklp.NewLogPoller(t) dstFilters := []string{ "Exec report accepts - 0xdafEa492d9C6733aE3D56b7eD1aDb60692c98bc3", - "Exec execution state changes - 0xdafeA492d9c6733Ae3d56B7ed1AdB60692C98bC1", - "Token pool added - 0xdafeA492d9c6733Ae3d56B7ed1AdB60692C98bC1", - "Token pool removed - 0xdafeA492d9c6733Ae3d56B7ed1AdB60692C98bC1", + "Exec execution state changes - " + offRampAddr.String(), + "Token pool added - " + offRampAddr.String(), + "Token pool removed - " + offRampAddr.String(), "Fee token added - 0xdaFEa492D9C6733Ae3D56b7ed1adB60692C98b19", "Fee token removed - 0xdaFEa492D9C6733Ae3D56b7ed1adB60692C98b19", } diff --git a/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go b/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go index b35ae4a600..1c3a6da7fe 100644 --- a/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go +++ b/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go @@ -12,7 +12,6 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" - gethtypes "github.com/ethereum/go-ethereum/core/types" "golang.org/x/sync/errgroup" "github.com/pkg/errors" @@ -33,7 +32,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" ccipconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/cache" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipevents" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/hashlib" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/observability" "github.com/smartcontractkit/chainlink/v2/core/services/pg" @@ -55,8 +54,8 @@ var ( type ExecutionPluginConfig struct { lggr logger.Logger sourceLP, destLP logpoller.LogPoller - sourceEvents ccipevents.Client - destEvents ccipevents.Client + sourceReader ccipdata.Reader + destReader ccipdata.Reader onRamp evm_2_evm_onramp.EVM2EVMOnRampInterface offRamp evm_2_evm_offramp.EVM2EVMOffRampInterface commitStore commit_store.CommitStoreInterface @@ -69,17 +68,18 @@ type ExecutionPluginConfig struct { } type ExecutionReportingPlugin struct { - config ExecutionPluginConfig - F int - lggr logger.Logger - inflightReports *inflightExecReportsContainer - snoozedRoots cache.SnoozedRoots - destPriceRegistry price_registry.PriceRegistryInterface - destWrappedNative common.Address - onchainConfig ccipconfig.ExecOnchainConfig - offchainConfig ccipconfig.ExecOffchainConfig - cachedSourceFeeTokens cache.AutoSync[[]common.Address] - cachedDestTokens cache.AutoSync[cache.CachedTokens] + config ExecutionPluginConfig + F int + lggr logger.Logger + inflightReports *inflightExecReportsContainer + snoozedRoots cache.SnoozedRoots + destPriceRegistry price_registry.PriceRegistryInterface + destWrappedNative common.Address + onchainConfig ccipconfig.ExecOnchainConfig + offchainConfig ccipconfig.ExecOffchainConfig + cachedSourceFeeTokens cache.AutoSync[[]common.Address] + cachedDestTokens cache.AutoSync[cache.CachedTokens] + customTokenPoolFactory func(ctx context.Context, poolAddress common.Address, bind bind.ContractBackend) (custom_token_pool.CustomTokenPoolInterface, error) } type ExecutionReportingPluginFactory struct { @@ -142,6 +142,9 @@ func (rf *ExecutionReportingPluginFactory) NewReportingPlugin(config types.Repor offchainConfig: offchainConfig, cachedDestTokens: cachedDestTokens, cachedSourceFeeTokens: cachedSourceFeeTokens, + customTokenPoolFactory: func(ctx context.Context, poolAddress common.Address, contractBackend bind.ContractBackend) (custom_token_pool.CustomTokenPoolInterface, error) { + return custom_token_pool.NewCustomTokenPool(poolAddress, contractBackend) + }, }, types.ReportingPluginInfo{ Name: "CCIPExecution", // Setting this to false saves on calldata since OffRamp doesn't require agreement between NOPs @@ -235,7 +238,7 @@ type evm2EVMOnRampCCIPSendRequestedWithMeta struct { func (r *ExecutionReportingPlugin) getExecutableObservations(ctx context.Context, lggr logger.Logger, timestamp types.ReportTimestamp, inflight []InflightInternalExecutionReport) ([]ObservedMessage, error) { unexpiredReports, err := getUnexpiredCommitReports( ctx, - r.config.destLP, + r.config.destReader, r.config.commitStore, r.onchainConfig.PermissionLessExecutionThresholdDuration(), ) @@ -366,6 +369,8 @@ func (r *ExecutionReportingPlugin) getExecutableObservations(ctx context.Context return []ObservedMessage{}, nil } +// destPoolRateLimits returns a map that consists of the rate limits of each destination tokens of the provided reports. +// If a token is missing from the returned map it either means that token was not found or token pool is disabled for this token. func (r *ExecutionReportingPlugin) destPoolRateLimits(ctx context.Context, commitReports []commitReportWithSendRequests, sourceToDestToken map[common.Address]common.Address) (map[common.Address]*big.Int, error) { dstTokens := make(map[common.Address]struct{}) // todo: replace with a set or uniqueSlice data structure for _, msg := range commitReports { @@ -388,7 +393,7 @@ func (r *ExecutionReportingPlugin) destPoolRateLimits(ctx context.Context, commi return nil, fmt.Errorf("get pool by dest token (%s): %w", dstToken, err) } - tokenPool, err := custom_token_pool.NewCustomTokenPool(poolAddress, r.config.destClient) + tokenPool, err := r.customTokenPoolFactory(ctx, poolAddress, r.config.destClient) if err != nil { return nil, fmt.Errorf("new custom dest token pool %s: %w", poolAddress, err) } @@ -436,7 +441,7 @@ func (r *ExecutionReportingPlugin) sourceDestinationTokens(ctx context.Context) // before. It doesn't matter if the executed succeeded, since we don't retry previous // attempts even if they failed. Value in the map indicates whether the log is finalized or not. func (r *ExecutionReportingPlugin) getExecutedSeqNrsInRange(ctx context.Context, min, max uint64, latestBlock int64) (map[uint64]bool, error) { - stateChanges, err := r.config.destEvents.GetExecutionStateChangesBetweenSeqNums( + stateChanges, err := r.config.destReader.GetExecutionStateChangesBetweenSeqNums( ctx, r.config.offRamp.Address(), min, @@ -731,9 +736,9 @@ func (r *ExecutionReportingPlugin) getReportsWithSendRequests( // use errgroup to fetch send request logs and executed sequence numbers in parallel eg := &errgroup.Group{} - var sendRequests []ccipevents.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested] + var sendRequests []ccipdata.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested] eg.Go(func() error { - sendReqs, err := r.config.sourceEvents.GetSendRequestsBetweenSeqNums( + sendReqs, err := r.config.sourceReader.GetSendRequestsBetweenSeqNums( ctx, r.config.onRamp.Address(), intervalMin, @@ -749,7 +754,7 @@ func (r *ExecutionReportingPlugin) getReportsWithSendRequests( var executedSeqNums map[uint64]bool eg.Go(func() error { - latestBlock, err := r.config.destEvents.LatestBlock(ctx) + latestBlock, err := r.config.destReader.LatestBlock(ctx) if err != nil { return err } @@ -818,13 +823,13 @@ func (r *ExecutionReportingPlugin) buildReport(ctx context.Context, lggr logger. if err := validateSeqNumbers(ctx, r.config.commitStore, observedMessages); err != nil { return nil, err } - commitReport, err := getCommitReportForSeqNum(ctx, r.config.destLP, r.config.commitStore, observedMessages[0].SeqNr) + commitReport, err := getCommitReportForSeqNum(ctx, r.config.destReader, r.config.commitStore, observedMessages[0].SeqNr) if err != nil { return nil, err } lggr.Infow("Building execution report", "observations", observedMessages, "merkleRoot", hexutil.Encode(commitReport.MerkleRoot[:]), "report", commitReport) - sendReqsInRoot, leaves, tree, err := getProofData(ctx, lggr, r.config.leafHasher, r.config.onRamp.Address(), r.config.sourceEvents, commitReport.Interval) + sendReqsInRoot, leaves, tree, err := getProofData(ctx, lggr, r.config.leafHasher, r.config.onRamp.Address(), r.config.sourceReader, commitReport.Interval) if err != nil { return nil, err } @@ -839,7 +844,7 @@ func (r *ExecutionReportingPlugin) buildReport(ctx context.Context, lggr logger. capped := sort.Search(len(observedMessages), func(i int) bool { report, _, err2 := buildExecutionReportForMessages(messages, leaves, tree, commitReport.Interval, observedMessages[:i+1]) if err2 != nil { - r.lggr.Errorw("build execution report", "err", err) + r.lggr.Errorw("build execution report", "err", err2) return false } @@ -1121,30 +1126,23 @@ func getTokensPrices(ctx context.Context, feeTokens []common.Address, priceRegis func getUnexpiredCommitReports( ctx context.Context, - dstLogPoller logpoller.LogPoller, + destReader ccipdata.Reader, commitStore commit_store.CommitStoreInterface, permissionExecutionThreshold time.Duration, ) ([]commit_store.CommitStoreCommitReport, error) { - logs, err := dstLogPoller.LogsCreatedAfter( - abihelpers.EventSignatures.ReportAccepted, + acceptedReports, err := destReader.GetAcceptedCommitReportsGteTimestamp( + ctx, commitStore.Address(), time.Now().Add(-permissionExecutionThreshold), 0, - pg.WithParentCtx(ctx), ) if err != nil { return nil, err } + var reports []commit_store.CommitStoreCommitReport - for _, log := range logs { - reportAccepted, err := commitStore.ParseReportAccepted(gethtypes.Log{ - Topics: log.GetTopics(), - Data: log.Data, - }) - if err != nil { - return nil, err - } - reports = append(reports, reportAccepted.Report) + for _, acceptedReport := range acceptedReports { + reports = append(reports, acceptedReport.Data.Report) } return reports, nil } diff --git a/core/services/ocr2/plugins/ccip/execution_reporting_plugin_test.go b/core/services/ocr2/plugins/ccip/execution_reporting_plugin_test.go index cc1c543566..c5b9722b3f 100644 --- a/core/services/ocr2/plugins/ccip/execution_reporting_plugin_test.go +++ b/core/services/ocr2/plugins/ccip/execution_reporting_plugin_test.go @@ -13,15 +13,23 @@ import ( "testing" "time" + "github.com/cometbft/cometbft/libs/rand" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" + "github.com/pkg/errors" + "github.com/smartcontractkit/libocr/commontypes" + "github.com/smartcontractkit/libocr/offchainreporting2/types" ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "github.com/smartcontractkit/chainlink/v2/core/assets" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/mocks" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" lpMocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller/mocks" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/custom_token_pool" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_onramp" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/price_registry" mock_contracts "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/mocks" @@ -29,398 +37,207 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" ccipconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/cache" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipevents" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/hashlib" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/testhelpers" - plugintesthelpers "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/testhelpers/plugins" "github.com/smartcontractkit/chainlink/v2/core/utils" - "github.com/smartcontractkit/chainlink/v2/core/assets" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/commit_store" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_offramp" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" "github.com/smartcontractkit/chainlink/v2/core/store/models" ) -var ( - MaxTokensPerMessage = 5 - MaxPayloadLength = 100_000 -) - -type execTestHarness = struct { - plugintesthelpers.CCIPPluginTestHarness - plugin *ExecutionReportingPlugin -} - -func setupExecTestHarness(t *testing.T) execTestHarness { - th := plugintesthelpers.SetupCCIPTestHarness(t) - - lggr := logger.TestLogger(t) - destFeeEstimator := mocks.NewEvmFeeEstimator(t) - - destFeeEstimator.On( - "GetFee", - mock.Anything, - mock.Anything, - mock.Anything, - mock.Anything, - ).Maybe().Return(gas.EvmFee{Legacy: assets.NewWei(defaultGasPrice)}, uint32(200e3), nil) - - offchainConfig := ccipconfig.ExecOffchainConfig{ - SourceFinalityDepth: 0, - DestOptimisticConfirmations: 0, - MaxGasPrice: 200e9, - BatchGasLimit: 5e6, - RootSnoozeTime: models.MustMakeDuration(10 * time.Minute), - InflightCacheExpiry: models.MustMakeDuration(3 * time.Minute), - RelativeBoostPerWaitHour: 0.07, - } - plugin := ExecutionReportingPlugin{ - config: ExecutionPluginConfig{ - lggr: th.Lggr, - sourceLP: th.SourceLP, - destLP: th.DestLP, - sourceEvents: ccipevents.NewLogPollerClient(th.SourceLP, lggr, th.SourceClient), - destEvents: ccipevents.NewLogPollerClient(th.DestLP, lggr, th.DestClient), - sourcePriceRegistry: th.Source.PriceRegistry, - onRamp: th.Source.OnRamp, - commitStore: th.Dest.CommitStore, - offRamp: th.Dest.OffRamp, - destClient: th.DestClient, - sourceClient: th.SourceClient, - sourceWrappedNativeToken: th.Source.WrappedNative.Address(), - leafHasher: hashlib.NewLeafHasher(th.Source.ChainSelector, th.Dest.ChainSelector, th.Source.OnRamp.Address(), hashlib.NewKeccakCtx()), - destGasEstimator: destFeeEstimator, - }, - onchainConfig: th.ExecOnchainConfig, - offchainConfig: offchainConfig, - lggr: th.Lggr.Named("ExecutionReportingPlugin"), - snoozedRoots: cache.NewSnoozedRoots(th.ExecOnchainConfig.PermissionLessExecutionThresholdDuration(), offchainConfig.RootSnoozeTime.Duration()), - inflightReports: newInflightExecReportsContainer(offchainConfig.InflightCacheExpiry.Duration()), - destPriceRegistry: th.Dest.PriceRegistry, - destWrappedNative: th.Dest.WrappedNative.Address(), - cachedSourceFeeTokens: cache.NewCachedFeeTokens(th.SourceLP, th.Source.PriceRegistry, int64(offchainConfig.SourceFinalityDepth)), - cachedDestTokens: cache.NewCachedSupportedTokens(th.DestLP, th.Dest.OffRamp, th.Dest.PriceRegistry, int64(offchainConfig.DestOptimisticConfirmations)), - } - return execTestHarness{ - CCIPPluginTestHarness: th, - plugin: &plugin, - } -} - -func TestMaxExecutionReportSize(t *testing.T) { - // Ensure that given max payload size and max num tokens, - // Our report size is under the tx size limit. - th := setupExecTestHarness(t) - th.plugin.F = 1 - mb := th.GenerateAndSendMessageBatch(t, 50, MaxPayloadLength, MaxTokensPerMessage) - - // commit root - encoded, err := abihelpers.EncodeCommitReport(commit_store.CommitStoreCommitReport{ - Interval: mb.Interval, - MerkleRoot: mb.Root, - PriceUpdates: commit_store.InternalPriceUpdates{ - TokenPriceUpdates: []commit_store.InternalTokenPriceUpdate{}, - DestChainSelector: 0, - UsdPerUnitGas: big.NewInt(0), - }, - }) - require.NoError(t, err) - latestEpocAndRound, err := th.Dest.CommitStoreHelper.GetLatestPriceEpochAndRound(nil) - require.NoError(t, err) - _, err = th.Dest.CommitStoreHelper.Report(th.Dest.User, encoded, big.NewInt(int64(latestEpocAndRound+1))) - require.NoError(t, err) - // double commit to ensure enough confirmations - th.CommitAndPollLogs(t) - th.CommitAndPollLogs(t) - - fullReport, err := abihelpers.EncodeExecutionReport(evm_2_evm_offramp.InternalExecutionReport{ - Messages: mb.Messages, - OffchainTokenData: mb.TokenData, - Proofs: mb.Proof.Hashes, - ProofFlagBits: mb.ProofBits, - }) - require.NoError(t, err) - // ensure "naive" full report would be bigger than limit - require.Greater(t, len(fullReport), MaxExecutionReportLength, "full execution report length") - - observations := make([]ObservedMessage, len(mb.Messages)) - for i, msg := range mb.Messages { - observations[i] = NewObservedMessage(msg.SequenceNumber, mb.TokenData[i]) - } - - // buildReport should cap the built report to fit in MaxExecutionReportLength - execReport, err := th.plugin.buildReport(testutils.Context(t), th.Lggr, observations) - require.NoError(t, err) - require.LessOrEqual(t, len(execReport), MaxExecutionReportLength, "built execution report length") -} - -func TestExecutionReportToEthTxMetadata(t *testing.T) { - c := plugintesthelpers.SetupCCIPTestHarness(t) - tests := []struct { - name string - msgBatch plugintesthelpers.MessageBatch - err error - }{ - { - "happy flow", - c.GenerateAndSendMessageBatch(t, 5, MaxPayloadLength, MaxTokensPerMessage), - nil, - }, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - encExecReport, err := abihelpers.EncodeExecutionReport(evm_2_evm_offramp.InternalExecutionReport{ - Messages: tc.msgBatch.Messages, - OffchainTokenData: tc.msgBatch.TokenData, - Proofs: tc.msgBatch.Proof.Hashes, - ProofFlagBits: tc.msgBatch.ProofBits, - }) - require.NoError(t, err) - txMeta, err := ExecutionReportToEthTxMeta(encExecReport) - if tc.err != nil { - require.Equal(t, tc.err.Error(), err.Error()) - return - } - require.NoError(t, err) - require.NotNil(t, txMeta) - require.Len(t, txMeta.MessageIDs, len(tc.msgBatch.Messages)) - }) - } -} - -func TestUpdateSourceToDestTokenMapping(t *testing.T) { - expectedNewBlockNumber := int64(10000) - logs := []logpoller.Log{{BlockNumber: expectedNewBlockNumber}} - mockDestLP := &lpMocks.LogPoller{} - - mockDestLP.On("LatestLogEventSigsAddrsWithConfs", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(logs, nil) - mockDestLP.On("LatestBlock", mock.Anything).Return(expectedNewBlockNumber, nil) - - sourceToken, destToken := common.HexToAddress("111111"), common.HexToAddress("222222") - - mockOffRamp := &mock_contracts.EVM2EVMOffRampInterface{} - mockOffRamp.On("Address").Return(common.HexToAddress("0x01")) - mockOffRamp.On("GetSupportedTokens", mock.Anything).Return([]common.Address{sourceToken}, nil) - mockOffRamp.On("GetDestinationToken", mock.Anything, sourceToken).Return(destToken, nil) - - mockPriceRegistry := &mock_contracts.PriceRegistryInterface{} - mockPriceRegistry.On("Address").Return(common.HexToAddress("0x02")) - mockPriceRegistry.On("GetFeeTokens", mock.Anything).Return([]common.Address{}, nil) - - plugin := ExecutionReportingPlugin{ - config: ExecutionPluginConfig{ - destLP: mockDestLP, - offRamp: mockOffRamp, - }, - cachedDestTokens: cache.NewCachedSupportedTokens(mockDestLP, mockOffRamp, mockPriceRegistry, 0), - } - - value, err := plugin.cachedDestTokens.Get(context.Background()) - require.NoError(t, err) - require.Equal(t, destToken, value.SupportedTokens[sourceToken]) -} - -func TestExecObservation(t *testing.T) { - th := setupExecTestHarness(t) - th.plugin.F = 1 - mb := th.GenerateAndSendMessageBatch(t, 2, 10, 1) - - // commit root - encoded, err := abihelpers.EncodeCommitReport(commit_store.CommitStoreCommitReport{ - Interval: mb.Interval, - MerkleRoot: mb.Root, - PriceUpdates: commit_store.InternalPriceUpdates{ - TokenPriceUpdates: []commit_store.InternalTokenPriceUpdate{}, - DestChainSelector: 0, - UsdPerUnitGas: big.NewInt(0), - }, - }) - require.NoError(t, err) - latestEpocAndRound, err := th.Dest.CommitStoreHelper.GetLatestPriceEpochAndRound(nil) - require.NoError(t, err) - _, err = th.Dest.CommitStoreHelper.Report(th.Dest.User, encoded, big.NewInt(int64(latestEpocAndRound+1))) - require.NoError(t, err) - // double commit to ensure enough confirmations - th.CommitAndPollLogs(t) - th.CommitAndPollLogs(t) - - expectedObservations := NewExecutionObservation([]ObservedMessage{ - {SeqNr: 1, MsgData: MsgData{TokenData: [][]byte{{}}}}, - {SeqNr: 2, MsgData: MsgData{TokenData: [][]byte{{}}}}, - }) - tests := []struct { - name string - commitStoreDown bool - expected *ExecutionObservation - expectedError bool +func TestExecutionReportingPlugin_Observation(t *testing.T) { + testCases := []struct { + name string + commitStorePaused bool + inflightReports []InflightInternalExecutionReport + unexpiredReports []ccipdata.Event[commit_store.CommitStoreReportAccepted] + sendRequests []ccipdata.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested] + executedSeqNums []uint64 + blessedRoots map[[32]byte]bool + senderNonce uint64 + rateLimiterState evm_2_evm_offramp.RateLimiterTokenBucket + expErr bool }{ { - "base", - false, - &expectedObservations, - false, + name: "commit store is down", + commitStorePaused: true, + expErr: true, }, { - "commitStore down", - true, - nil, - true, + name: "happy flow", + commitStorePaused: false, + inflightReports: []InflightInternalExecutionReport{}, + unexpiredReports: []ccipdata.Event[commit_store.CommitStoreReportAccepted]{ + { + Data: commit_store.CommitStoreReportAccepted{ + Report: commit_store.CommitStoreCommitReport{ + PriceUpdates: commit_store.InternalPriceUpdates{}, + Interval: commit_store.CommitStoreInterval{Min: 10, Max: 12}, + MerkleRoot: [32]byte{123}, + }, + }, + }, + }, + blessedRoots: map[[32]byte]bool{ + [32]byte{123}: true, + }, + rateLimiterState: evm_2_evm_offramp.RateLimiterTokenBucket{ + IsEnabled: false, + }, + senderNonce: 9, + sendRequests: []ccipdata.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested]{ + { + Data: evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested{ + Message: evm_2_evm_onramp.InternalEVM2EVMMessage{SequenceNumber: 10}, + }, + }, + { + Data: evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested{ + Message: evm_2_evm_onramp.InternalEVM2EVMMessage{SequenceNumber: 11}, + }, + }, + { + Data: evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested{ + Message: evm_2_evm_onramp.InternalEVM2EVMMessage{SequenceNumber: 12}, + }, + }, + }, }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.commitStoreDown && !isCommitStoreDownNow(testutils.Context(t), th.Lggr, th.Dest.CommitStore) { - _, err := th.Dest.CommitStore.Pause(th.Dest.User) - require.NoError(t, err) - th.CommitAndPollLogs(t) - } else if !tt.commitStoreDown && isCommitStoreDownNow(testutils.Context(t), th.Lggr, th.Dest.CommitStore) { - _, err := th.Dest.CommitStore.Unpause(th.Dest.User) - require.NoError(t, err) - th.CommitAndPollLogs(t) + ctx := testutils.Context(t) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + p := &ExecutionReportingPlugin{} + p.inflightReports = newInflightExecReportsContainer(time.Minute) + p.inflightReports.reports = tc.inflightReports + p.lggr = logger.TestLogger(t) + + commitStore, commitStoreAddr := testhelpers.NewFakeCommitStore(t, 1) + commitStore.SetPaused(tc.commitStorePaused) + commitStore.SetBlessedRoots(tc.blessedRoots) + p.config.commitStore = commitStore + + offRamp, offRampAddr := testhelpers.NewFakeOffRamp(t) + offRamp.SetRateLimiterState(tc.rateLimiterState) + p.config.offRamp = offRamp + + destReader := ccipdata.NewMockReader(t) + destReader.On("GetAcceptedCommitReportsGteTimestamp", ctx, commitStoreAddr, mock.Anything, 0). + Return(tc.unexpiredReports, nil).Maybe() + destReader.On("LatestBlock", ctx).Return(int64(1234), nil).Maybe() + var executionEvents []ccipdata.Event[evm_2_evm_offramp.EVM2EVMOffRampExecutionStateChanged] + for _, seqNum := range tc.executedSeqNums { + executionEvents = append(executionEvents, ccipdata.Event[evm_2_evm_offramp.EVM2EVMOffRampExecutionStateChanged]{ + Data: evm_2_evm_offramp.EVM2EVMOffRampExecutionStateChanged{SequenceNumber: seqNum}, + }) } + destReader.On("GetExecutionStateChangesBetweenSeqNums", ctx, offRampAddr, mock.Anything, mock.Anything, 0). + Return(executionEvents, nil).Maybe() + p.config.destReader = destReader + + onRamp, onRampAddr := testhelpers.NewFakeOnRamp(t) + p.config.onRamp = onRamp + + sourceReader := ccipdata.NewMockReader(t) + sourceReader.On("GetSendRequestsBetweenSeqNums", ctx, onRampAddr, mock.Anything, mock.Anything, 0). + Return(tc.sendRequests, nil).Maybe() + p.config.sourceReader = sourceReader + + cachedDestTokens := cache.NewMockAutoSync[cache.CachedTokens](t) + cachedDestTokens.On("Get", ctx).Return(cache.CachedTokens{ + SupportedTokens: map[common.Address]common.Address{}, + FeeTokens: []common.Address{}, + }, nil).Maybe() + p.cachedDestTokens = cachedDestTokens + + priceRegistry, _ := testhelpers.NewFakePriceRegistry(t) + priceRegistry.SetTokenPrices([]price_registry.InternalTimestampedPackedUint224{ + {Value: big.NewInt(123), Timestamp: uint32(time.Now().Unix())}, + }) + p.destPriceRegistry = priceRegistry + p.config.sourcePriceRegistry = priceRegistry - gotObs, err := th.plugin.Observation(testutils.Context(t), ocrtypes.ReportTimestamp{}, ocrtypes.Query{}) - - if tt.expectedError { - require.Error(t, err) - } else { - require.NoError(t, err) - } + sourceFeeTokens := cache.NewMockAutoSync[[]common.Address](t) + sourceFeeTokens.On("Get", ctx).Return([]common.Address{}, nil).Maybe() + p.cachedSourceFeeTokens = sourceFeeTokens - var decodedObservation *ExecutionObservation - if gotObs != nil { - decodedObservation = new(ExecutionObservation) - err = json.Unmarshal(gotObs, decodedObservation) - require.NoError(t, err) + p.snoozedRoots = cache.NewSnoozedRoots(time.Minute, time.Minute) + _, err := p.Observation(ctx, types.ReportTimestamp{}, types.Query{}) + if tc.expErr { + assert.Error(t, err) + return } - assert.Equal(t, tt.expected, decodedObservation) + assert.NoError(t, err) }) } } -func TestExecReport(t *testing.T) { - th := setupExecTestHarness(t) - th.plugin.F = 1 - mb := th.GenerateAndSendMessageBatch(t, 2, 10, 1) - - // commit root - encoded, err := abihelpers.EncodeCommitReport(commit_store.CommitStoreCommitReport{ - Interval: mb.Interval, - MerkleRoot: mb.Root, - PriceUpdates: commit_store.InternalPriceUpdates{ - TokenPriceUpdates: []commit_store.InternalTokenPriceUpdate{}, - DestChainSelector: 0, - UsdPerUnitGas: big.NewInt(0), - }, - }) - require.NoError(t, err) - execReport := mb.ToExecutionReport() - - latestEpocAndRound, err := th.Dest.CommitStoreHelper.GetLatestPriceEpochAndRound(nil) - require.NoError(t, err) - _, err = th.Dest.CommitStoreHelper.Report(th.Dest.User, encoded, big.NewInt(int64(latestEpocAndRound+1))) - require.NoError(t, err) - // double commit to ensure enough confirmations - th.CommitAndPollLogs(t) - th.CommitAndPollLogs(t) +func TestExecutionReportingPlugin_Report(t *testing.T) { + testCases := []struct { + name string + f int + committedSeqNum uint64 + observations []ExecutionObservation - tests := []struct { - name string - commitStoreDown bool - observations [][]ObservedMessage - expectedShouldReport bool - expectedReport *evm_2_evm_offramp.InternalExecutionReport - expectedError bool + expectingSomeReport bool + expectedReport evm_2_evm_offramp.InternalExecutionReport + expectingSomeErr bool }{ { - "base", - false, - [][]ObservedMessage{ - {NewObservedMessage(1, [][]byte{{}}), NewObservedMessage(2, [][]byte{{}})}, - {NewObservedMessage(1, [][]byte{{}}), NewObservedMessage(2, [][]byte{{}})}, - }, - true, - &execReport, - false, - }, - { - "partial observation", - false, - [][]ObservedMessage{ - {NewObservedMessage(1, [][]byte{{}}), NewObservedMessage(2, [][]byte{{}})}, - {NewObservedMessage(1, [][]byte{{}})}, - }, - true, - func() *evm_2_evm_offramp.InternalExecutionReport { - mb2 := mb - mb2.Messages = mb.Messages[:1] - mb2.Messages = mb.Messages[:1] - mb2.TokenData = mb.TokenData[:1] - mb2.Interval = commit_store.CommitStoreInterval{Min: 1, Max: 1} - mb2.Proof, err = mb2.Tree.Prove([]int{0}) - assert.NoError(t, err) - mb2.ProofBits = abihelpers.ProofFlagsToBits(mb2.Proof.SourceFlags) - report := mb2.ToExecutionReport() - return &report - }(), - false, - }, - { - "empty", - false, - [][]ObservedMessage{ - {NewObservedMessage(1, [][]byte{{}}), NewObservedMessage(2, [][]byte{{}})}, - {}, - }, - false, - nil, - false, + name: "not enough observations to form consensus", + f: 5, + committedSeqNum: 5, + observations: []ExecutionObservation{ + {Messages: map[uint64]MsgData{3: {}, 4: {}}}, + {Messages: map[uint64]MsgData{3: {}, 4: {}}}, + }, + expectingSomeErr: false, + expectingSomeReport: false, }, { - "unknown seqNr", - false, - [][]ObservedMessage{ - {NewObservedMessage(1, [][]byte{{}}), NewObservedMessage(2, [][]byte{{}}), NewObservedMessage(3, [][]byte{{}})}, - {NewObservedMessage(1, [][]byte{{}}), NewObservedMessage(2, [][]byte{{}}), NewObservedMessage(3, [][]byte{{}})}, - }, - false, - nil, - true, + name: "zero observations", + f: 0, + committedSeqNum: 5, + observations: []ExecutionObservation{}, + expectingSomeErr: false, + expectingSomeReport: false, }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var obs []ocrtypes.AttributedObservation - for _, o := range tt.observations { - encoded, err := NewExecutionObservation(o).Marshal() - require.NoError(t, err) - obs = append(obs, ocrtypes.AttributedObservation{Observation: encoded}) - } - gotShouldReport, gotReport, err := th.plugin.Report(testutils.Context(t), ocrtypes.ReportTimestamp{}, ocrtypes.Query{}, obs) + ctx := testutils.Context(t) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + p := ExecutionReportingPlugin{} + p.lggr = logger.TestLogger(t) + p.F = tc.f + + commitStore, _ := testhelpers.NewFakeCommitStore(t, tc.committedSeqNum) - if tt.expectedError { - require.Error(t, err) - } else { - require.NoError(t, err) + p.config.commitStore = commitStore + + observations := make([]types.AttributedObservation, len(tc.observations)) + for i := range observations { + b, err := json.Marshal(tc.observations[i]) + assert.NoError(t, err) + observations[i] = types.AttributedObservation{Observation: b, Observer: commontypes.OracleID(i + 1)} } - require.Equal(t, tt.expectedShouldReport, gotShouldReport) - var encodedReport ocrtypes.Report - if tt.expectedReport != nil { - encodedReport, err = abihelpers.EncodeExecutionReport(*tt.expectedReport) - require.NoError(t, err) + _, _, err := p.Report(ctx, types.ReportTimestamp{}, types.Query{}, observations) + if tc.expectingSomeErr { + assert.Error(t, err) + return } - assert.Equal(t, encodedReport, gotReport) + assert.NoError(t, err) }) } + } -func TestExecShouldAcceptFinalizedReport(t *testing.T) { +func TestExecutionReportingPlugin_ShouldAcceptFinalizedReport(t *testing.T) { msg := evm_2_evm_offramp.InternalEVM2EVMMessage{ SequenceNumber: 12, FeeTokenAmount: big.NewInt(1e9), @@ -443,7 +260,7 @@ func TestExecShouldAcceptFinalizedReport(t *testing.T) { encodedReport, err := abihelpers.EncodeExecutionReport(report) require.NoError(t, err) - mockOffRamp := &mock_contracts.EVM2EVMOffRampInterface{} + mockOffRamp, _ := testhelpers.NewFakeOffRamp(t) plugin := ExecutionReportingPlugin{ config: ExecutionPluginConfig{ offRamp: mockOffRamp, @@ -465,7 +282,7 @@ func TestExecShouldAcceptFinalizedReport(t *testing.T) { assert.Equal(t, false, should) } -func TestExecShouldTransmitAcceptedReport(t *testing.T) { +func TestExecutionReportingPlugin_ShouldTransmitAcceptedReport(t *testing.T) { msg := evm_2_evm_offramp.InternalEVM2EVMMessage{ SequenceNumber: 12, FeeTokenAmount: big.NewInt(1e9), @@ -512,9 +329,85 @@ func TestExecShouldTransmitAcceptedReport(t *testing.T) { assert.Equal(t, false, should) } -func TestBuildBatch(t *testing.T) { +func TestExecutionReportingPlugin_buildReport(t *testing.T) { + ctx := testutils.Context(t) + + const numMessages = 100 + const tokensPerMessage = 20 + const bytesPerMessage = 1000 + + executionReport := generateExecutionReport(t, numMessages, tokensPerMessage, bytesPerMessage) + encodedReport, err := abihelpers.EncodeExecutionReport(executionReport) + assert.NoError(t, err) + // ensure "naive" full report would be bigger than limit + assert.Greater(t, len(encodedReport), MaxExecutionReportLength, "full execution report length") + + observations := make([]ObservedMessage, len(executionReport.Messages)) + for i, msg := range executionReport.Messages { + observations[i] = NewObservedMessage(msg.SequenceNumber, executionReport.OffchainTokenData[i]) + } + + // ensure that buildReport should cap the built report to fit in MaxExecutionReportLength + p := &ExecutionReportingPlugin{} + p.lggr = logger.TestLogger(t) + + commitStore, commitStoreAddress := testhelpers.NewFakeCommitStore(t, executionReport.Messages[len(executionReport.Messages)-1].SequenceNumber+1) + commitStore.On("Verify", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(big.NewInt(math.MaxInt64), nil) + p.config.commitStore = commitStore + + destReader := ccipdata.NewMockReader(t) + destReader.On("GetAcceptedCommitReportsGteSeqNum", ctx, commitStoreAddress, observations[0].SeqNr, 0). + Return([]ccipdata.Event[commit_store.CommitStoreReportAccepted]{ + { + Data: commit_store.CommitStoreReportAccepted{ + Report: commit_store.CommitStoreCommitReport{ + Interval: commit_store.CommitStoreInterval{ + Min: observations[0].SeqNr, + Max: observations[len(observations)-1].SeqNr, + }, + }, + }, + }, + }, nil) + p.config.destReader = destReader + + p.config.leafHasher = leafHasher123{} + + onRamp, onRampAddr := testhelpers.NewFakeOnRamp(t) + p.config.onRamp = onRamp + + sendReqs := make([]ccipdata.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested], len(observations)) + for i := range observations { + sendReqs[i] = ccipdata.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested]{ + Data: evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested{Message: evm_2_evm_onramp.InternalEVM2EVMMessage{ + SourceChainSelector: math.MaxUint64, + SequenceNumber: uint64(i + 1), + FeeTokenAmount: big.NewInt(math.MaxInt64), + Sender: utils.RandomAddress(), + Nonce: math.MaxUint64, + GasLimit: big.NewInt(math.MaxInt64), + Strict: false, + Receiver: utils.RandomAddress(), + Data: bytes.Repeat([]byte{0}, bytesPerMessage), + TokenAmounts: nil, + FeeToken: utils.RandomAddress(), + MessageId: [32]byte{12}, + }}, + } + } + sourceReader := ccipdata.NewMockReader(t) + sourceReader.On("GetSendRequestsBetweenSeqNums", + ctx, onRampAddr, observations[0].SeqNr, observations[len(observations)-1].SeqNr, 0).Return(sendReqs, nil) + p.config.sourceReader = sourceReader + + execReport, err := p.buildReport(ctx, p.lggr, observations) + assert.NoError(t, err) + assert.LessOrEqual(t, len(execReport), MaxExecutionReportLength, "built execution report length") +} + +func TestExecutionReportingPlugin_buildBatch(t *testing.T) { c, _ := testhelpers.SetupChain(t) - mockOffRamp := mock_contracts.EVM2EVMOffRampInterface{} + offRamp, _ := testhelpers.NewFakeOffRamp(t) // We do this just to have the parsing available. onRamp, err := evm_2_evm_onramp.NewEVM2EVMOnRamp(common.HexToAddress("0x1"), c) require.NoError(t, err) @@ -525,9 +418,8 @@ func TestBuildBatch(t *testing.T) { srcNative := common.HexToAddress("0xc") plugin := ExecutionReportingPlugin{ config: ExecutionPluginConfig{ - offRamp: &mockOffRamp, - // We use a real onRamp for parsing - onRamp: onRamp, + offRamp: offRamp, + onRamp: onRamp, }, destWrappedNative: destNative, offchainConfig: ccipconfig.ExecOffchainConfig{ @@ -745,10 +637,7 @@ func TestBuildBatch(t *testing.T) { for _, tc := range tt { tc := tc t.Run(tc.name, func(t *testing.T) { - for sender, nonce := range tc.offRampNoncesBySender { - mockOffRamp.On("GetSenderNonce", mock.Anything, sender).Return(nonce, nil) - } - + offRamp.SetSenderNonces(tc.offRampNoncesBySender) seqNrs := plugin.buildBatch( lggr, commitReportWithSendRequests{sendRequestsWithMeta: tc.reqs}, @@ -765,72 +654,574 @@ func TestBuildBatch(t *testing.T) { } } -func Test_calculateObservedMessagesConsensus(t *testing.T) { - type args struct { - observations []ExecutionObservation - f int - } - tests := []struct { - name string - args args - want []ObservedMessage +func TestExecutionReportingPlugin_isRateLimitEnoughForTokenPool(t *testing.T) { + testCases := []struct { + name string + destTokenPoolRateLimits map[common.Address]*big.Int + tokenAmounts []evm_2_evm_offramp.ClientEVMTokenAmount + inflightTokenAmounts map[common.Address]*big.Int + srcToDestToken map[common.Address]common.Address + exp bool }{ { - name: "no observations", - args: args{ - observations: nil, - f: 0, + name: "base", + destTokenPoolRateLimits: map[common.Address]*big.Int{ + common.HexToAddress("10"): big.NewInt(100), + common.HexToAddress("20"): big.NewInt(50), }, - want: []ObservedMessage{}, + tokenAmounts: []evm_2_evm_offramp.ClientEVMTokenAmount{ + {Token: common.HexToAddress("1"), Amount: big.NewInt(50)}, + {Token: common.HexToAddress("2"), Amount: big.NewInt(20)}, + }, + srcToDestToken: map[common.Address]common.Address{ + common.HexToAddress("1"): common.HexToAddress("10"), + common.HexToAddress("2"): common.HexToAddress("20"), + }, + inflightTokenAmounts: map[common.Address]*big.Int{ + common.HexToAddress("1"): big.NewInt(20), + common.HexToAddress("2"): big.NewInt(30), + }, + exp: true, }, { - name: "common path", - args: args{ - observations: []ExecutionObservation{ - { - Messages: map[uint64]MsgData{ - 1: {TokenData: [][]byte{{0x1}, {0x1}, {0x1}}}, - 2: {TokenData: [][]byte{{0x2}, {0x2}, {0x2}}}, - }, - }, - { - Messages: map[uint64]MsgData{ - 1: {TokenData: [][]byte{{0x1}, {0x1}, {0xff}}}, // different token data - should not be picked - 2: {TokenData: [][]byte{{0x2}, {0x2}, {0x2}}}, - 3: {TokenData: [][]byte{{0x3}, {0x3}, {0x3}}}, - }, - }, - { - Messages: map[uint64]MsgData{ - 1: {TokenData: [][]byte{{0x1}, {0x1}, {0x1}}}, - 2: {TokenData: [][]byte{{0x2}, {0x2}, {0x2}}}, - }, - }, - }, - f: 1, + name: "rate limit hit", + destTokenPoolRateLimits: map[common.Address]*big.Int{ + common.HexToAddress("10"): big.NewInt(100), + common.HexToAddress("20"): big.NewInt(50), }, - want: []ObservedMessage{ - {SeqNr: 1, MsgData: MsgData{TokenData: [][]byte{{0x1}, {0x1}, {0x1}}}}, - {SeqNr: 2, MsgData: MsgData{TokenData: [][]byte{{0x2}, {0x2}, {0x2}}}}, + srcToDestToken: map[common.Address]common.Address{ + common.HexToAddress("1"): common.HexToAddress("10"), + common.HexToAddress("2"): common.HexToAddress("20"), + }, + tokenAmounts: []evm_2_evm_offramp.ClientEVMTokenAmount{ + {Token: common.HexToAddress("1"), Amount: big.NewInt(50)}, + {Token: common.HexToAddress("2"), Amount: big.NewInt(51)}, }, + exp: true, }, { - name: "similar token data", - args: args{ - observations: []ExecutionObservation{ - { - Messages: map[uint64]MsgData{ - 1: {TokenData: [][]byte{{0x1}, {0x1}, {0x1}}}, - }, - }, - { - Messages: map[uint64]MsgData{ - 1: {TokenData: [][]byte{{0x1}, {0x1, 0x1}}}, - }, - }, - { - Messages: map[uint64]MsgData{ - 1: {TokenData: [][]byte{{0x1}, {0x1, 0x1}}}, + name: "rate limit hit, inflight included", + destTokenPoolRateLimits: map[common.Address]*big.Int{ + common.HexToAddress("10"): big.NewInt(100), + common.HexToAddress("20"): big.NewInt(50), + }, + srcToDestToken: map[common.Address]common.Address{ + common.HexToAddress("1"): common.HexToAddress("10"), + common.HexToAddress("2"): common.HexToAddress("20"), + }, + tokenAmounts: []evm_2_evm_offramp.ClientEVMTokenAmount{ + {Token: common.HexToAddress("1"), Amount: big.NewInt(50)}, + {Token: common.HexToAddress("2"), Amount: big.NewInt(20)}, + }, + inflightTokenAmounts: map[common.Address]*big.Int{ + common.HexToAddress("1"): big.NewInt(51), + common.HexToAddress("2"): big.NewInt(30), + }, + exp: true, + }, + { + destTokenPoolRateLimits: map[common.Address]*big.Int{}, + tokenAmounts: []evm_2_evm_offramp.ClientEVMTokenAmount{ + {Token: common.HexToAddress("1"), Amount: big.NewInt(50)}, + {Token: common.HexToAddress("2"), Amount: big.NewInt(20)}, + }, + srcToDestToken: map[common.Address]common.Address{ + common.HexToAddress("1"): common.HexToAddress("10"), + common.HexToAddress("2"): common.HexToAddress("20"), + }, + inflightTokenAmounts: map[common.Address]*big.Int{ + common.HexToAddress("1"): big.NewInt(20), + common.HexToAddress("2"): big.NewInt(30), + }, + name: "rate limit not applied to token", + exp: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + p := &ExecutionReportingPlugin{lggr: logger.TestLogger(t)} + p.isRateLimitEnoughForTokenPool(tc.destTokenPoolRateLimits, tc.tokenAmounts, tc.inflightTokenAmounts, tc.srcToDestToken) + }) + } +} + +func TestExecutionReportingPlugin_destPoolRateLimits(t *testing.T) { + tk1 := utils.RandomAddress() + tk1dest := utils.RandomAddress() + tk1pool := utils.RandomAddress() + + tk2 := utils.RandomAddress() + tk2dest := utils.RandomAddress() + tk2pool := utils.RandomAddress() + + testCases := []struct { + name string + tokenAmounts []evm_2_evm_offramp.ClientEVMTokenAmount + sourceToDestToken map[common.Address]common.Address + destPools map[common.Address]common.Address + poolRateLimits map[common.Address]custom_token_pool.RateLimiterTokenBucket + + expRateLimits map[common.Address]*big.Int + expErr bool + }{ + { + name: "happy flow", + tokenAmounts: []evm_2_evm_offramp.ClientEVMTokenAmount{ + {Token: tk1}, + {Token: tk2}, + {Token: tk1}, + {Token: tk1}, + }, + sourceToDestToken: map[common.Address]common.Address{ + tk1: tk1dest, + tk2: tk2dest, + }, + destPools: map[common.Address]common.Address{ + tk1dest: tk1pool, + tk2dest: tk2pool, + }, + poolRateLimits: map[common.Address]custom_token_pool.RateLimiterTokenBucket{ + tk1pool: {Tokens: big.NewInt(1000), IsEnabled: true}, + tk2pool: {Tokens: big.NewInt(2000), IsEnabled: true}, + }, + expRateLimits: map[common.Address]*big.Int{ + tk1dest: big.NewInt(1000), + tk2dest: big.NewInt(2000), + }, + expErr: false, + }, + { + name: "token missing from source to dest mapping", + tokenAmounts: []evm_2_evm_offramp.ClientEVMTokenAmount{ + {Token: tk1}, + {Token: tk2}, // <-- missing form sourceToDestToken + }, + sourceToDestToken: map[common.Address]common.Address{ + tk1: tk1dest, + }, + destPools: map[common.Address]common.Address{ + tk1dest: tk1pool, + }, + poolRateLimits: map[common.Address]custom_token_pool.RateLimiterTokenBucket{ + tk1pool: {Tokens: big.NewInt(1000), IsEnabled: true}, + }, + expRateLimits: map[common.Address]*big.Int{ + tk1dest: big.NewInt(1000), + }, + expErr: false, + }, + { + name: "pool is disabled", + tokenAmounts: []evm_2_evm_offramp.ClientEVMTokenAmount{ + {Token: tk1}, + {Token: tk2}, + }, + sourceToDestToken: map[common.Address]common.Address{ + tk1: tk1dest, + tk2: tk2dest, + }, + destPools: map[common.Address]common.Address{ + tk1dest: tk1pool, + tk2dest: tk2pool, + }, + poolRateLimits: map[common.Address]custom_token_pool.RateLimiterTokenBucket{ + tk1pool: {Tokens: big.NewInt(1000), IsEnabled: true}, + tk2pool: {Tokens: big.NewInt(2000), IsEnabled: false}, // <--- pool disabled + }, + expRateLimits: map[common.Address]*big.Int{ + tk1dest: big.NewInt(1000), + }, + expErr: false, + }, + } + + ctx := testutils.Context(t) + lggr := logger.TestLogger(t) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + p := &ExecutionReportingPlugin{} + p.lggr = lggr + + offRamp, offRampAddr := testhelpers.NewFakeOffRamp(t) + offRamp.SetTokenPools(tc.destPools) + p.config.offRamp = offRamp + + p.customTokenPoolFactory = func(ctx context.Context, poolAddress common.Address, _ bind.ContractBackend) (custom_token_pool.CustomTokenPoolInterface, error) { + mp := &mockPool{} + mp.On("CurrentOffRampRateLimiterState", mock.Anything, offRampAddr).Return(tc.poolRateLimits[poolAddress], nil) + return mp, nil + } + + rateLimits, err := p.destPoolRateLimits(ctx, []commitReportWithSendRequests{ + { + sendRequestsWithMeta: []evm2EVMOnRampCCIPSendRequestedWithMeta{ + { + InternalEVM2EVMMessage: evm_2_evm_offramp.InternalEVM2EVMMessage{ + TokenAmounts: tc.tokenAmounts, + }, + }, + }, + }, + }, tc.sourceToDestToken) + if tc.expErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.Equal(t, tc.expRateLimits, rateLimits) + }) + } +} + +func TestExecutionReportingPlugin_estimateDestinationGasPrice(t *testing.T) { + testCases := []struct { + name string + evmFee gas.EvmFee + evmFeeErr error + + expRes *big.Int + expErr bool + }{ + { + name: "dynamic fee cap has precedence over legacy", + evmFee: gas.EvmFee{ + Legacy: assets.NewWei(big.NewInt(1000)), + DynamicFeeCap: assets.NewWei(big.NewInt(2000)), + }, + expRes: big.NewInt(2000), + }, + { + name: "legacy is used if dynamic fee cap is not provided", + evmFee: gas.EvmFee{ + Legacy: assets.NewWei(big.NewInt(1000)), + }, + expRes: big.NewInt(1000), + }, + { + name: "stop on error", + evmFeeErr: errors.New("some error"), + expErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + p := &ExecutionReportingPlugin{} + mockEstimator := mocks.NewEvmFeeEstimator(t) + mockEstimator.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.evmFee, uint32(0), tc.evmFeeErr) + p.config.destGasEstimator = mockEstimator + + res, err := p.estimateDestinationGasPrice(testutils.Context(t)) + if tc.expErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.Equal(t, tc.expRes, res) + }) + } +} + +func TestExecutionReportingPlugin_getReportsWithSendRequests(t *testing.T) { + testCases := []struct { + name string + reports []commit_store.CommitStoreCommitReport + expQueryMin uint64 // expected min/max used in the query to get ccipevents + expQueryMax uint64 + onchainEvents []ccipdata.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested] + destLatestBlock int64 + destExecutedSeqNums []uint64 + + expReports []commitReportWithSendRequests + expErr bool + }{ + { + name: "no reports", + reports: nil, + expReports: nil, + expErr: false, + }, + { + name: "two reports happy flow", + reports: []commit_store.CommitStoreCommitReport{ + { + Interval: commit_store.CommitStoreInterval{Min: 1, Max: 2}, + MerkleRoot: [32]byte{100}, + }, + { + Interval: commit_store.CommitStoreInterval{Min: 3, Max: 3}, + MerkleRoot: [32]byte{200}, + }, + }, + expQueryMin: 1, + expQueryMax: 3, + onchainEvents: []ccipdata.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested]{ + {Data: evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested{ + Message: evm_2_evm_onramp.InternalEVM2EVMMessage{SequenceNumber: 1}, + }}, + {Data: evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested{ + Message: evm_2_evm_onramp.InternalEVM2EVMMessage{SequenceNumber: 2}, + }}, + {Data: evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested{ + Message: evm_2_evm_onramp.InternalEVM2EVMMessage{SequenceNumber: 3}, + }}, + }, + destLatestBlock: 10_000, + destExecutedSeqNums: []uint64{1}, + expReports: []commitReportWithSendRequests{ + { + commitReport: commit_store.CommitStoreCommitReport{ + Interval: commit_store.CommitStoreInterval{Min: 1, Max: 2}, + MerkleRoot: [32]byte{100}, + }, + sendRequestsWithMeta: []evm2EVMOnRampCCIPSendRequestedWithMeta{ + { + InternalEVM2EVMMessage: evm_2_evm_offramp.InternalEVM2EVMMessage{SequenceNumber: 1}, + executed: true, + finalized: true, + }, + { + InternalEVM2EVMMessage: evm_2_evm_offramp.InternalEVM2EVMMessage{SequenceNumber: 2}, + executed: false, + finalized: false, + }, + }, + }, + { + commitReport: commit_store.CommitStoreCommitReport{ + Interval: commit_store.CommitStoreInterval{Min: 3, Max: 3}, + MerkleRoot: [32]byte{200}, + }, + sendRequestsWithMeta: []evm2EVMOnRampCCIPSendRequestedWithMeta{ + { + InternalEVM2EVMMessage: evm_2_evm_offramp.InternalEVM2EVMMessage{SequenceNumber: 3}, + executed: false, + finalized: false, + }, + }, + }, + }, + expErr: false, + }, + } + + ctx := testutils.Context(t) + lggr := logger.TestLogger(t) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + p := &ExecutionReportingPlugin{} + p.lggr = lggr + + onRamp, onRampAddr := testhelpers.NewFakeOnRamp(t) + p.config.onRamp = onRamp + + offRamp, offRampAddr := testhelpers.NewFakeOffRamp(t) + p.config.offRamp = offRamp + + sourceReader := ccipdata.NewMockReader(t) + sourceReader.On("GetSendRequestsBetweenSeqNums", ctx, onRampAddr, tc.expQueryMin, tc.expQueryMax, 0). + Return(tc.onchainEvents, nil).Maybe() + p.config.sourceReader = sourceReader + + destReader := ccipdata.NewMockReader(t) + destReader.On("LatestBlock", ctx).Return(tc.destLatestBlock, nil).Maybe() + var executedEvents []ccipdata.Event[evm_2_evm_offramp.EVM2EVMOffRampExecutionStateChanged] + for _, executedSeqNum := range tc.destExecutedSeqNums { + executedEvents = append(executedEvents, ccipdata.Event[evm_2_evm_offramp.EVM2EVMOffRampExecutionStateChanged]{ + Data: evm_2_evm_offramp.EVM2EVMOffRampExecutionStateChanged{SequenceNumber: executedSeqNum}, + BlockMeta: ccipdata.BlockMeta{BlockNumber: tc.destLatestBlock - 10}, + }) + } + destReader.On("GetExecutionStateChangesBetweenSeqNums", ctx, offRampAddr, tc.expQueryMin, tc.expQueryMax, 0).Return(executedEvents, nil).Maybe() + p.config.destReader = destReader + + populatedReports, err := p.getReportsWithSendRequests(ctx, tc.reports) + if tc.expErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.Equal(t, len(tc.expReports), len(populatedReports)) + for i, expReport := range tc.expReports { + assert.Equal(t, len(expReport.sendRequestsWithMeta), len(populatedReports[i].sendRequestsWithMeta)) + for j, expReq := range expReport.sendRequestsWithMeta { + assert.Equal(t, expReq.executed, populatedReports[i].sendRequestsWithMeta[j].executed) + assert.Equal(t, expReq.finalized, populatedReports[i].sendRequestsWithMeta[j].finalized) + assert.Equal(t, expReq.SequenceNumber, populatedReports[i].sendRequestsWithMeta[j].SequenceNumber) + } + } + }) + } +} + +func TestExecutionReportingPluginFactory_UpdateLogPollerFilters(t *testing.T) { + const numFilters = 10 + filters := make([]logpoller.Filter, numFilters) + for i := range filters { + filters[i] = logpoller.Filter{ + Name: fmt.Sprintf("filter-%d", i), + EventSigs: []common.Hash{common.HexToHash(fmt.Sprintf("%d", i))}, + Addresses: []common.Address{common.HexToAddress(fmt.Sprintf("%d", i))}, + Retention: time.Duration(i) * time.Second, + } + } + + destLP := lpMocks.NewLogPoller(t) + sourceLP := lpMocks.NewLogPoller(t) + + onRamp, _ := testhelpers.NewFakeOnRamp(t) + sourcePriceRegistry, _ := testhelpers.NewFakePriceRegistry(t) + commitStore, _ := testhelpers.NewFakeCommitStore(t, 1) + offRamp, _ := testhelpers.NewFakeOffRamp(t) + + destPriceRegistryAddr := utils.RandomAddress() + + rf := &ExecutionReportingPluginFactory{ + filtersMu: &sync.Mutex{}, + sourceChainFilters: filters[:5], + destChainFilters: filters[5:10], + config: ExecutionPluginConfig{ + destLP: destLP, + sourceLP: sourceLP, + onRamp: onRamp, + commitStore: commitStore, + offRamp: offRamp, + sourcePriceRegistry: sourcePriceRegistry, + }, + } + + for _, f := range getExecutionPluginSourceLpChainFilters(onRamp.Address(), sourcePriceRegistry.Address()) { + sourceLP.On("RegisterFilter", f).Return(nil) + } + for _, f := range getExecutionPluginDestLpChainFilters(commitStore.Address(), offRamp.Address(), destPriceRegistryAddr) { + destLP.On("RegisterFilter", f).Return(nil) + } + for _, f := range rf.sourceChainFilters[1:] { // zero address is skipped + sourceLP.On("UnregisterFilter", f.Name, mock.Anything).Return(nil) + } + for _, f := range rf.destChainFilters { + destLP.On("UnregisterFilter", f.Name, mock.Anything).Return(nil) + } + + err := rf.UpdateLogPollerFilters(destPriceRegistryAddr) + assert.NoError(t, err) +} + +func TestExecutionReportToEthTxMeta(t *testing.T) { + t.Run("happy flow", func(t *testing.T) { + executionReport := generateExecutionReport(t, 10, 3, 1000) + encExecReport, err := abihelpers.EncodeExecutionReport(executionReport) + assert.NoError(t, err) + txMeta, err := ExecutionReportToEthTxMeta(encExecReport) + assert.NoError(t, err) + assert.Len(t, txMeta.MessageIDs, len(executionReport.Messages)) + }) + + t.Run("invalid report", func(t *testing.T) { + _, err := ExecutionReportToEthTxMeta([]byte("whatever")) + assert.Error(t, err) + }) +} + +func TestUpdateSourceToDestTokenMapping(t *testing.T) { + expectedNewBlockNumber := int64(10000) + logs := []logpoller.Log{{BlockNumber: expectedNewBlockNumber}} + mockDestLP := &lpMocks.LogPoller{} + + mockDestLP.On("LatestLogEventSigsAddrsWithConfs", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(logs, nil) + mockDestLP.On("LatestBlock", mock.Anything).Return(expectedNewBlockNumber, nil) + + sourceToken, destToken := common.HexToAddress("111111"), common.HexToAddress("222222") + + mockOffRamp := &mock_contracts.EVM2EVMOffRampInterface{} + mockOffRamp.On("Address").Return(common.HexToAddress("0x01")) + mockOffRamp.On("GetSupportedTokens", mock.Anything).Return([]common.Address{sourceToken}, nil) + mockOffRamp.On("GetDestinationToken", mock.Anything, sourceToken).Return(destToken, nil) + + mockPriceRegistry := &mock_contracts.PriceRegistryInterface{} + mockPriceRegistry.On("Address").Return(common.HexToAddress("0x02")) + mockPriceRegistry.On("GetFeeTokens", mock.Anything).Return([]common.Address{}, nil) + + plugin := ExecutionReportingPlugin{ + config: ExecutionPluginConfig{ + destLP: mockDestLP, + offRamp: mockOffRamp, + }, + cachedDestTokens: cache.NewCachedSupportedTokens(mockDestLP, mockOffRamp, mockPriceRegistry, 0), + } + + value, err := plugin.cachedDestTokens.Get(context.Background()) + require.NoError(t, err) + require.Equal(t, destToken, value.SupportedTokens[sourceToken]) +} + +func Test_calculateObservedMessagesConsensus(t *testing.T) { + type args struct { + observations []ExecutionObservation + f int + } + tests := []struct { + name string + args args + want []ObservedMessage + }{ + { + name: "no observations", + args: args{ + observations: nil, + f: 0, + }, + want: []ObservedMessage{}, + }, + { + name: "common path", + args: args{ + observations: []ExecutionObservation{ + { + Messages: map[uint64]MsgData{ + 1: {TokenData: [][]byte{{0x1}, {0x1}, {0x1}}}, + 2: {TokenData: [][]byte{{0x2}, {0x2}, {0x2}}}, + }, + }, + { + Messages: map[uint64]MsgData{ + 1: {TokenData: [][]byte{{0x1}, {0x1}, {0xff}}}, // different token data - should not be picked + 2: {TokenData: [][]byte{{0x2}, {0x2}, {0x2}}}, + 3: {TokenData: [][]byte{{0x3}, {0x3}, {0x3}}}, + }, + }, + { + Messages: map[uint64]MsgData{ + 1: {TokenData: [][]byte{{0x1}, {0x1}, {0x1}}}, + 2: {TokenData: [][]byte{{0x2}, {0x2}, {0x2}}}, + }, + }, + }, + f: 1, + }, + want: []ObservedMessage{ + {SeqNr: 1, MsgData: MsgData{TokenData: [][]byte{{0x1}, {0x1}, {0x1}}}}, + {SeqNr: 2, MsgData: MsgData{TokenData: [][]byte{{0x2}, {0x2}, {0x2}}}}, + }, + }, + { + name: "similar token data", + args: args{ + observations: []ExecutionObservation{ + { + Messages: map[uint64]MsgData{ + 1: {TokenData: [][]byte{{0x1}, {0x1}, {0x1}}}, + }, + }, + { + Messages: map[uint64]MsgData{ + 1: {TokenData: [][]byte{{0x1}, {0x1, 0x1}}}, + }, + }, + { + Messages: map[uint64]MsgData{ + 1: {TokenData: [][]byte{{0x1}, {0x1, 0x1}}}, }, }, }, @@ -957,11 +1348,8 @@ func Test_getTokensPrices(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - priceReg := mock_contracts.NewPriceRegistryInterface(t) - - priceReg.On("GetTokenPrices", mock.Anything, append(tc.feeTokens, tc.tokens...)). - Return(tc.retPrices, nil) - priceReg.On("Address").Return(common.HexToAddress("1234"), nil) + priceReg, _ := testhelpers.NewFakePriceRegistry(t) + priceReg.SetTokenPrices(tc.retPrices) prices, err := getTokensPrices(context.Background(), tc.feeTokens, priceReg, tc.tokens) if tc.expErr { @@ -1023,158 +1411,6 @@ func Test_calculateMessageMaxGas(t *testing.T) { } } -func TestExecutionReportingPlugin_isRateLimitEnoughForTokenPool(t *testing.T) { - testCases := []struct { - name string - destTokenPoolRateLimits map[common.Address]*big.Int - tokenAmounts []evm_2_evm_offramp.ClientEVMTokenAmount - inflightTokenAmounts map[common.Address]*big.Int - srcToDestToken map[common.Address]common.Address - exp bool - }{ - { - name: "base", - destTokenPoolRateLimits: map[common.Address]*big.Int{ - common.HexToAddress("10"): big.NewInt(100), - common.HexToAddress("20"): big.NewInt(50), - }, - tokenAmounts: []evm_2_evm_offramp.ClientEVMTokenAmount{ - {Token: common.HexToAddress("1"), Amount: big.NewInt(50)}, - {Token: common.HexToAddress("2"), Amount: big.NewInt(20)}, - }, - srcToDestToken: map[common.Address]common.Address{ - common.HexToAddress("1"): common.HexToAddress("10"), - common.HexToAddress("2"): common.HexToAddress("20"), - }, - inflightTokenAmounts: map[common.Address]*big.Int{ - common.HexToAddress("1"): big.NewInt(20), - common.HexToAddress("2"): big.NewInt(30), - }, - exp: true, - }, - { - name: "rate limit hit", - destTokenPoolRateLimits: map[common.Address]*big.Int{ - common.HexToAddress("10"): big.NewInt(100), - common.HexToAddress("20"): big.NewInt(50), - }, - srcToDestToken: map[common.Address]common.Address{ - common.HexToAddress("1"): common.HexToAddress("10"), - common.HexToAddress("2"): common.HexToAddress("20"), - }, - tokenAmounts: []evm_2_evm_offramp.ClientEVMTokenAmount{ - {Token: common.HexToAddress("1"), Amount: big.NewInt(50)}, - {Token: common.HexToAddress("2"), Amount: big.NewInt(51)}, - }, - exp: true, - }, - { - name: "rate limit hit, inflight included", - destTokenPoolRateLimits: map[common.Address]*big.Int{ - common.HexToAddress("10"): big.NewInt(100), - common.HexToAddress("20"): big.NewInt(50), - }, - srcToDestToken: map[common.Address]common.Address{ - common.HexToAddress("1"): common.HexToAddress("10"), - common.HexToAddress("2"): common.HexToAddress("20"), - }, - tokenAmounts: []evm_2_evm_offramp.ClientEVMTokenAmount{ - {Token: common.HexToAddress("1"), Amount: big.NewInt(50)}, - {Token: common.HexToAddress("2"), Amount: big.NewInt(20)}, - }, - inflightTokenAmounts: map[common.Address]*big.Int{ - common.HexToAddress("1"): big.NewInt(51), - common.HexToAddress("2"): big.NewInt(30), - }, - exp: true, - }, - { - destTokenPoolRateLimits: map[common.Address]*big.Int{}, - tokenAmounts: []evm_2_evm_offramp.ClientEVMTokenAmount{ - {Token: common.HexToAddress("1"), Amount: big.NewInt(50)}, - {Token: common.HexToAddress("2"), Amount: big.NewInt(20)}, - }, - srcToDestToken: map[common.Address]common.Address{ - common.HexToAddress("1"): common.HexToAddress("10"), - common.HexToAddress("2"): common.HexToAddress("20"), - }, - inflightTokenAmounts: map[common.Address]*big.Int{ - common.HexToAddress("1"): big.NewInt(20), - common.HexToAddress("2"): big.NewInt(30), - }, - name: "rate limit not applied to token", - exp: false, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - p := &ExecutionReportingPlugin{lggr: logger.TestLogger(t)} - p.isRateLimitEnoughForTokenPool(tc.destTokenPoolRateLimits, tc.tokenAmounts, tc.inflightTokenAmounts, tc.srcToDestToken) - }) - } -} - -func TestExecutionReportingPluginFactory_UpdateLogPollerFilters(t *testing.T) { - const numFilters = 10 - filters := make([]logpoller.Filter, numFilters) - for i := range filters { - filters[i] = logpoller.Filter{ - Name: fmt.Sprintf("filter-%d", i), - EventSigs: []common.Hash{common.HexToHash(fmt.Sprintf("%d", i))}, - Addresses: []common.Address{common.HexToAddress(fmt.Sprintf("%d", i))}, - Retention: time.Duration(i) * time.Second, - } - } - - destLP := lpMocks.NewLogPoller(t) - sourceLP := lpMocks.NewLogPoller(t) - - onRamp := mock_contracts.NewEVM2EVMOnRampInterface(t) - onRamp.On("Address").Return(utils.RandomAddress(), nil) - - sourcePriceRegistry := mock_contracts.NewPriceRegistryInterface(t) - sourcePriceRegistry.On("Address").Return(utils.RandomAddress(), nil) - - commitStore := mock_contracts.NewCommitStoreInterface(t) - commitStore.On("Address").Return(utils.RandomAddress(), nil) - - offRamp := mock_contracts.NewEVM2EVMOffRampInterface(t) - offRamp.On("Address").Return(utils.RandomAddress(), nil) - - destPriceRegistryAddr := utils.RandomAddress() - - rf := &ExecutionReportingPluginFactory{ - filtersMu: &sync.Mutex{}, - sourceChainFilters: filters[:5], - destChainFilters: filters[5:10], - config: ExecutionPluginConfig{ - destLP: destLP, - sourceLP: sourceLP, - onRamp: onRamp, - commitStore: commitStore, - offRamp: offRamp, - sourcePriceRegistry: sourcePriceRegistry, - }, - } - - for _, f := range getExecutionPluginSourceLpChainFilters(onRamp.Address(), sourcePriceRegistry.Address()) { - sourceLP.On("RegisterFilter", f).Return(nil) - } - for _, f := range getExecutionPluginDestLpChainFilters(commitStore.Address(), offRamp.Address(), destPriceRegistryAddr) { - destLP.On("RegisterFilter", f).Return(nil) - } - for _, f := range rf.sourceChainFilters[1:] { // zero address is skipped - sourceLP.On("UnregisterFilter", f.Name, mock.Anything).Return(nil) - } - for _, f := range rf.destChainFilters { - destLP.On("UnregisterFilter", f.Name, mock.Anything).Return(nil) - } - - err := rf.UpdateLogPollerFilters(destPriceRegistryAddr) - assert.NoError(t, err) -} - func Test_inflightAggregates(t *testing.T) { const n = 10 addrs := make([]common.Address, n) @@ -1297,3 +1533,208 @@ func Test_inflightAggregates(t *testing.T) { }) } } + +func Test_commitReportWithSendRequests_validate(t *testing.T) { + testCases := []struct { + name string + reportInterval commit_store.CommitStoreInterval + numReqs int + expValid bool + }{ + { + name: "valid report", + reportInterval: commit_store.CommitStoreInterval{Min: 10, Max: 20}, + numReqs: 11, + expValid: true, + }, + { + name: "report with one request", + reportInterval: commit_store.CommitStoreInterval{Min: 1234, Max: 1234}, + numReqs: 1, + expValid: true, + }, + { + name: "request is missing", + reportInterval: commit_store.CommitStoreInterval{Min: 1234, Max: 1234}, + numReqs: 0, + expValid: false, + }, + { + name: "requests are missing", + reportInterval: commit_store.CommitStoreInterval{Min: 1, Max: 10}, + numReqs: 5, + expValid: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + rep := commitReportWithSendRequests{ + commitReport: commit_store.CommitStoreCommitReport{ + Interval: tc.reportInterval, + }, + sendRequestsWithMeta: make([]evm2EVMOnRampCCIPSendRequestedWithMeta, tc.numReqs), + } + err := rep.validate() + isValid := err == nil + assert.Equal(t, tc.expValid, isValid) + }) + } +} + +func Test_commitReportWithSendRequests_allRequestsAreExecutedAndFinalized(t *testing.T) { + testCases := []struct { + name string + reqs []evm2EVMOnRampCCIPSendRequestedWithMeta + expRes bool + }{ + { + name: "all requests executed and finalized", + reqs: []evm2EVMOnRampCCIPSendRequestedWithMeta{ + {executed: true, finalized: true}, + {executed: true, finalized: true}, + {executed: true, finalized: true}, + }, + expRes: true, + }, + { + name: "true when there are zero requests", + reqs: []evm2EVMOnRampCCIPSendRequestedWithMeta{}, + expRes: true, + }, + { + name: "some request not executed", + reqs: []evm2EVMOnRampCCIPSendRequestedWithMeta{ + {executed: true, finalized: true}, + {executed: true, finalized: true}, + {executed: false, finalized: true}, + }, + expRes: false, + }, + { + name: "some request not finalized", + reqs: []evm2EVMOnRampCCIPSendRequestedWithMeta{ + {executed: true, finalized: true}, + {executed: true, finalized: true}, + {executed: true, finalized: false}, + }, + expRes: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + rep := commitReportWithSendRequests{sendRequestsWithMeta: tc.reqs} + res := rep.allRequestsAreExecutedAndFinalized() + assert.Equal(t, tc.expRes, res) + }) + } +} + +func Test_commitReportWithSendRequests_sendReqFits(t *testing.T) { + testCases := []struct { + name string + req evm2EVMOnRampCCIPSendRequestedWithMeta + report commit_store.CommitStoreCommitReport + expRes bool + }{ + { + name: "all requests executed and finalized", + req: evm2EVMOnRampCCIPSendRequestedWithMeta{ + InternalEVM2EVMMessage: evm_2_evm_offramp.InternalEVM2EVMMessage{SequenceNumber: 1}, + }, + report: commit_store.CommitStoreCommitReport{ + Interval: commit_store.CommitStoreInterval{Min: 1, Max: 10}, + }, + expRes: true, + }, + { + name: "all requests executed and finalized", + req: evm2EVMOnRampCCIPSendRequestedWithMeta{ + InternalEVM2EVMMessage: evm_2_evm_offramp.InternalEVM2EVMMessage{SequenceNumber: 10}, + }, + report: commit_store.CommitStoreCommitReport{ + Interval: commit_store.CommitStoreInterval{Min: 1, Max: 10}, + }, + expRes: true, + }, + { + name: "all requests executed and finalized", + req: evm2EVMOnRampCCIPSendRequestedWithMeta{ + InternalEVM2EVMMessage: evm_2_evm_offramp.InternalEVM2EVMMessage{SequenceNumber: 11}, + }, + report: commit_store.CommitStoreCommitReport{ + Interval: commit_store.CommitStoreInterval{Min: 1, Max: 10}, + }, + expRes: false, + }, + { + name: "all requests executed and finalized", + req: evm2EVMOnRampCCIPSendRequestedWithMeta{ + InternalEVM2EVMMessage: evm_2_evm_offramp.InternalEVM2EVMMessage{SequenceNumber: 10}, + }, + report: commit_store.CommitStoreCommitReport{ + Interval: commit_store.CommitStoreInterval{Min: 10, Max: 10}, + }, + expRes: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + r := &commitReportWithSendRequests{commitReport: tc.report} + assert.Equal(t, tc.expRes, r.sendReqFits(tc.req)) + }) + } +} + +// generateExecutionReport generates an execution report that can be used in tests +func generateExecutionReport(t *testing.T, numMsgs, tokensPerMsg, bytesPerMsg int) evm_2_evm_offramp.InternalExecutionReport { + messages := make([]evm_2_evm_offramp.InternalEVM2EVMMessage, numMsgs) + + offChainTokenData := make([][][]byte, numMsgs) + for i := range messages { + tokenAmounts := make([]evm_2_evm_offramp.ClientEVMTokenAmount, tokensPerMsg) + for j := range tokenAmounts { + tokenAmounts[j] = evm_2_evm_offramp.ClientEVMTokenAmount{ + Token: utils.RandomAddress(), + Amount: big.NewInt(math.MaxInt64), + } + } + + messages[i] = evm_2_evm_offramp.InternalEVM2EVMMessage{ + SourceChainSelector: rand.Uint64(), + SequenceNumber: uint64(i + 1), + FeeTokenAmount: big.NewInt(rand.Int64()), + Sender: utils.RandomAddress(), + Nonce: rand.Uint64(), + GasLimit: big.NewInt(rand.Int64()), + Strict: false, + Receiver: utils.RandomAddress(), + Data: bytes.Repeat([]byte{1}, bytesPerMsg), + TokenAmounts: tokenAmounts, + FeeToken: utils.RandomAddress(), + MessageId: utils.RandomBytes32(), + } + + data := []byte(`{"foo": "bar"}`) + offChainTokenData[i] = [][]byte{data, data, data} + } + + return evm_2_evm_offramp.InternalExecutionReport{ + Messages: messages, + OffchainTokenData: offChainTokenData, + Proofs: make([][32]byte, numMsgs), + ProofFlagBits: big.NewInt(rand.Int64()), + } +} + +type mockPool struct { + custom_token_pool.CustomTokenPoolInterface + mock.Mock +} + +func (mp *mockPool) CurrentOffRampRateLimiterState(opts *bind.CallOpts, offRamp common.Address) (custom_token_pool.RateLimiterTokenBucket, error) { + args := mp.Called(opts, offRamp) + return args.Get(0).(custom_token_pool.RateLimiterTokenBucket), args.Error(1) +} diff --git a/core/services/ocr2/plugins/ccip/integration_test.go b/core/services/ocr2/plugins/ccip/integration_test.go index 958fa06480..912b6444b4 100644 --- a/core/services/ocr2/plugins/ccip/integration_test.go +++ b/core/services/ocr2/plugins/ccip/integration_test.go @@ -33,8 +33,8 @@ func TestIntegration_CCIP(t *testing.T) { _, err := w.Write([]byte(`{"UsdPerETH": "1700000000000000000000"}`)) require.NoError(t, err) })) - wrapped, err := ccipTH.Source.Router.GetWrappedNative(nil) - require.NoError(t, err) + wrapped, err1 := ccipTH.Source.Router.GetWrappedNative(nil) + require.NoError(t, err1) tokenPricesUSDPipeline := fmt.Sprintf(` // Price 1 link [type=http method=GET url="%s"]; @@ -310,7 +310,7 @@ merge [type=merge left="{}" right="{\\\"%s\\\":$(link_parse), \\\"%s\\\":$(eth_p linkToTransferToOnRamp := big.NewInt(1e18) // transfer some link to onramp to pay the nops - _, err = ccipTH.Source.LinkToken.Transfer(ccipTH.Source.User, ccipTH.Source.OnRamp.Address(), linkToTransferToOnRamp) + _, err := ccipTH.Source.LinkToken.Transfer(ccipTH.Source.User, ccipTH.Source.OnRamp.Address(), linkToTransferToOnRamp) require.NoError(t, err) ccipTH.Source.Chain.Commit() @@ -477,15 +477,13 @@ merge [type=merge left="{}" right="{\\\"%s\\\":$(link_parse), \\\"%s\\\":$(eth_p seqNumber := currentSeqNum + 1 defer msgWg.Done() for { - select { - case <-ticker.C: - t.Logf("sending request for seqnum %d", seqNumber) - ccipContracts.SendMessage(t, gasLimit, tokenAmount, ccipTH.Dest.Receivers[0].Receiver.Address()) - ccipContracts.Source.Chain.Commit() - seqNumber++ - if seqNumber == endSeq { - return - } + <-ticker.C // wait for ticker + t.Logf("sending request for seqnum %d", seqNumber) + ccipContracts.SendMessage(t, gasLimit, tokenAmount, ccipTH.Dest.Receivers[0].Receiver.Address()) + ccipContracts.Source.Chain.Commit() + seqNumber++ + if seqNumber == endSeq { + return } } }(ccipTH.CCIPContracts, currentSeqNum) diff --git a/core/services/ocr2/plugins/ccip/internal/cache/cache_test.go b/core/services/ocr2/plugins/ccip/internal/cache/cache_test.go index e37b09b95e..373b6cbf36 100644 --- a/core/services/ocr2/plugins/ccip/internal/cache/cache_test.go +++ b/core/services/ocr2/plugins/ccip/internal/cache/cache_test.go @@ -120,10 +120,6 @@ func TestGet_ConcurrentAccess(t *testing.T) { // 1 init block + 100 iterations require.Equal(t, int64(101), contract.lastChangeBlock) - - // Make sure that recent value is stored in cache - val := contract.copyCachedValue() - require.Equal(t, "value_99", val) } func newCachedContract(lp logpoller.LogPoller, cacheValue string, originValue []string, lastChangeBlock int64) *CachedChain[string] { diff --git a/core/services/ocr2/plugins/ccip/internal/cache/snoozed_roots_test.go b/core/services/ocr2/plugins/ccip/internal/cache/snoozed_roots_test.go index f3813df575..07ea14d32e 100644 --- a/core/services/ocr2/plugins/ccip/internal/cache/snoozed_roots_test.go +++ b/core/services/ocr2/plugins/ccip/internal/cache/snoozed_roots_test.go @@ -34,7 +34,7 @@ func TestEvictingElements(t *testing.T) { k1 := [32]byte{1} c.Snooze(k1) - time.Sleep(1 * time.Second) + time.Sleep(10 * time.Millisecond) assert.False(t, c.IsSnoozed(k1)) } diff --git a/core/services/ocr2/plugins/ccip/internal/cache/tokens_test.go b/core/services/ocr2/plugins/ccip/internal/cache/tokens_test.go index b562c44563..657e7d5fa1 100644 --- a/core/services/ocr2/plugins/ccip/internal/cache/tokens_test.go +++ b/core/services/ocr2/plugins/ccip/internal/cache/tokens_test.go @@ -16,6 +16,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/generated/link_token_interface" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/testhelpers" "github.com/smartcontractkit/chainlink/v2/core/utils" ) @@ -117,14 +118,12 @@ func TestCallOrigin(t *testing.T) { dst2 := common.HexToAddress("21") testCases := []struct { - name string - srcTokens []common.Address - srcToDst map[common.Address]common.Address - expErr bool + name string + srcToDst map[common.Address]common.Address + expErr bool }{ { - name: "base", - srcTokens: []common.Address{src1, src2}, + name: "base", srcToDst: map[common.Address]common.Address{ src1: dst1, src2: dst2, @@ -132,8 +131,7 @@ func TestCallOrigin(t *testing.T) { expErr: false, }, { - name: "dup dst token", - srcTokens: []common.Address{src1, src2}, + name: "dup dst token", srcToDst: map[common.Address]common.Address{ src1: dst1, src2: dst1, @@ -144,11 +142,8 @@ func TestCallOrigin(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - offRamp := mock_contracts.NewEVM2EVMOffRampInterface(t) - offRamp.On("GetSupportedTokens", mock.Anything).Return(tc.srcTokens, nil) - for src, dst := range tc.srcToDst { - offRamp.On("GetDestinationToken", mock.Anything, src).Return(dst, nil) - } + offRamp, _ := testhelpers.NewFakeOffRamp(t) + offRamp.SetSourceToDestTokens(tc.srcToDst) o := supportedTokensOrigin{offRamp: offRamp} srcToDst, err := o.CallOrigin(context.Background()) diff --git a/core/services/ocr2/plugins/ccip/internal/ccipevents/logpoller.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/logpoller.go similarity index 74% rename from core/services/ocr2/plugins/ccip/internal/ccipevents/logpoller.go rename to core/services/ocr2/plugins/ccip/internal/ccipdata/logpoller.go index 2fe7411d01..4473e72703 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipevents/logpoller.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/logpoller.go @@ -1,4 +1,4 @@ -package ccipevents +package ccipdata import ( "context" @@ -14,6 +14,7 @@ import ( evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/commit_store" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_offramp" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_onramp" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/price_registry" @@ -22,10 +23,10 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/pg" ) -var _ Client = &LogPollerClient{} +var _ Reader = &LogPollerReader{} -// LogPollerClient implements the Client interface by using a logPoller instance to fetch the events. -type LogPollerClient struct { +// LogPollerReader implements the Reader interface by using a logPoller instance to fetch the events. +type LogPollerReader struct { lp logpoller.LogPoller lggr logger.Logger client evmclient.Client @@ -33,15 +34,15 @@ type LogPollerClient struct { dependencyCache sync.Map } -func NewLogPollerClient(lp logpoller.LogPoller, lggr logger.Logger, client evmclient.Client) *LogPollerClient { - return &LogPollerClient{ +func NewLogPollerReader(lp logpoller.LogPoller, lggr logger.Logger, client evmclient.Client) *LogPollerReader { + return &LogPollerReader{ lp: lp, lggr: lggr, client: client, } } -func (c *LogPollerClient) GetSendRequestsGteSeqNum(ctx context.Context, onRampAddress common.Address, seqNum uint64, checkFinalityTags bool, confs int) (sendReqs []Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested], err error) { +func (c *LogPollerReader) GetSendRequestsGteSeqNum(ctx context.Context, onRampAddress common.Address, seqNum uint64, checkFinalityTags bool, confs int) (sendReqs []Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested], err error) { onRamp, err := c.loadOnRamp(onRampAddress) if err != nil { return nil, err @@ -107,7 +108,7 @@ func (c *LogPollerClient) GetSendRequestsGteSeqNum(ctx context.Context, onRampAd ) } -func (c *LogPollerClient) GetSendRequestsBetweenSeqNums(ctx context.Context, onRampAddress common.Address, seqNumMin, seqNumMax uint64, confs int) ([]Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested], error) { +func (c *LogPollerReader) GetSendRequestsBetweenSeqNums(ctx context.Context, onRampAddress common.Address, seqNumMin, seqNumMax uint64, confs int) ([]Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested], error) { onRamp, err := c.loadOnRamp(onRampAddress) if err != nil { return nil, err @@ -134,7 +135,7 @@ func (c *LogPollerClient) GetSendRequestsBetweenSeqNums(ctx context.Context, onR ) } -func (c *LogPollerClient) GetTokenPriceUpdatesCreatedAfter(ctx context.Context, priceRegistryAddress common.Address, ts time.Time, confs int) ([]Event[price_registry.PriceRegistryUsdPerTokenUpdated], error) { +func (c *LogPollerReader) GetTokenPriceUpdatesCreatedAfter(ctx context.Context, priceRegistryAddress common.Address, ts time.Time, confs int) ([]Event[price_registry.PriceRegistryUsdPerTokenUpdated], error) { priceRegistry, err := c.loadPriceRegistry(priceRegistryAddress) if err != nil { return nil, err @@ -160,7 +161,7 @@ func (c *LogPollerClient) GetTokenPriceUpdatesCreatedAfter(ctx context.Context, ) } -func (c *LogPollerClient) GetGasPriceUpdatesCreatedAfter(ctx context.Context, priceRegistryAddress common.Address, chainSelector uint64, ts time.Time, confs int) ([]Event[price_registry.PriceRegistryUsdPerUnitGasUpdated], error) { +func (c *LogPollerReader) GetGasPriceUpdatesCreatedAfter(ctx context.Context, priceRegistryAddress common.Address, chainSelector uint64, ts time.Time, confs int) ([]Event[price_registry.PriceRegistryUsdPerUnitGasUpdated], error) { priceRegistry, err := c.loadPriceRegistry(priceRegistryAddress) if err != nil { return nil, err @@ -188,7 +189,7 @@ func (c *LogPollerClient) GetGasPriceUpdatesCreatedAfter(ctx context.Context, pr ) } -func (c *LogPollerClient) GetExecutionStateChangesBetweenSeqNums(ctx context.Context, offRampAddress common.Address, seqNumMin, seqNumMax uint64, confs int) ([]Event[evm_2_evm_offramp.EVM2EVMOffRampExecutionStateChanged], error) { +func (c *LogPollerReader) GetExecutionStateChangesBetweenSeqNums(ctx context.Context, offRampAddress common.Address, seqNumMin, seqNumMax uint64, confs int) ([]Event[evm_2_evm_offramp.EVM2EVMOffRampExecutionStateChanged], error) { offRamp, err := c.loadOffRamp(offRampAddress) if err != nil { return nil, err @@ -216,10 +217,63 @@ func (c *LogPollerClient) GetExecutionStateChangesBetweenSeqNums(ctx context.Con ) } -func (c *LogPollerClient) LatestBlock(ctx context.Context) (int64, error) { +func (c *LogPollerReader) LatestBlock(ctx context.Context) (int64, error) { return c.lp.LatestBlock(pg.WithParentCtx(ctx)) } +func (c *LogPollerReader) GetAcceptedCommitReportsGteSeqNum(ctx context.Context, commitStoreAddress common.Address, seqNum uint64, confs int) ([]Event[commit_store.CommitStoreReportAccepted], error) { + commitStore, err := c.loadCommitStore(commitStoreAddress) + if err != nil { + return nil, err + } + + logs, err := c.lp.LogsDataWordGreaterThan( + abihelpers.EventSignatures.ReportAccepted, + commitStoreAddress, + abihelpers.EventSignatures.ReportAcceptedMaxSequenceNumberWord, + logpoller.EvmWord(seqNum), + confs, + pg.WithParentCtx(ctx), + ) + if err != nil { + return nil, err + } + + return parseLogs[commit_store.CommitStoreReportAccepted]( + logs, + c.lggr, + func(log types.Log) (*commit_store.CommitStoreReportAccepted, error) { + return commitStore.ParseReportAccepted(log) + }, + ) +} + +func (c *LogPollerReader) GetAcceptedCommitReportsGteTimestamp(ctx context.Context, commitStoreAddress common.Address, ts time.Time, confs int) ([]Event[commit_store.CommitStoreReportAccepted], error) { + commitStore, err := c.loadCommitStore(commitStoreAddress) + if err != nil { + return nil, err + } + + logs, err := c.lp.LogsCreatedAfter( + abihelpers.EventSignatures.ReportAccepted, + commitStoreAddress, + ts, + confs, + pg.WithParentCtx(ctx), + ) + if err != nil { + return nil, err + } + + return parseLogs[commit_store.CommitStoreReportAccepted]( + logs, + c.lggr, + func(log types.Log) (*commit_store.CommitStoreReportAccepted, error) { + return commitStore.ParseReportAccepted(log) + }, + ) +} + func parseLogs[T any](logs []logpoller.Log, lggr logger.Logger, parseFunc func(log types.Log) (*T, error)) ([]Event[T], error) { reqs := make([]Event[T], 0, len(logs)) for _, log := range logs { @@ -241,7 +295,7 @@ func parseLogs[T any](logs []logpoller.Log, lggr logger.Logger, parseFunc func(l return reqs, nil } -func (c *LogPollerClient) loadOnRamp(addr common.Address) (*evm_2_evm_onramp.EVM2EVMOnRampFilterer, error) { +func (c *LogPollerReader) loadOnRamp(addr common.Address) (*evm_2_evm_onramp.EVM2EVMOnRampFilterer, error) { onRamp, exists := loadCachedDependency[*evm_2_evm_onramp.EVM2EVMOnRampFilterer](&c.dependencyCache, addr) if exists { return onRamp, nil @@ -256,7 +310,7 @@ func (c *LogPollerClient) loadOnRamp(addr common.Address) (*evm_2_evm_onramp.EVM return onRamp, nil } -func (c *LogPollerClient) loadPriceRegistry(addr common.Address) (*price_registry.PriceRegistryFilterer, error) { +func (c *LogPollerReader) loadPriceRegistry(addr common.Address) (*price_registry.PriceRegistryFilterer, error) { priceRegistry, exists := loadCachedDependency[*price_registry.PriceRegistryFilterer](&c.dependencyCache, addr) if exists { return priceRegistry, nil @@ -271,7 +325,7 @@ func (c *LogPollerClient) loadPriceRegistry(addr common.Address) (*price_registr return priceRegistry, nil } -func (c *LogPollerClient) loadOffRamp(addr common.Address) (*evm_2_evm_offramp.EVM2EVMOffRampFilterer, error) { +func (c *LogPollerReader) loadOffRamp(addr common.Address) (*evm_2_evm_offramp.EVM2EVMOffRampFilterer, error) { offRamp, exists := loadCachedDependency[*evm_2_evm_offramp.EVM2EVMOffRampFilterer](&c.dependencyCache, addr) if exists { return offRamp, nil @@ -286,6 +340,21 @@ func (c *LogPollerClient) loadOffRamp(addr common.Address) (*evm_2_evm_offramp.E return offRamp, nil } +func (c *LogPollerReader) loadCommitStore(addr common.Address) (*commit_store.CommitStoreFilterer, error) { + commitStore, exists := loadCachedDependency[*commit_store.CommitStoreFilterer](&c.dependencyCache, addr) + if exists { + return commitStore, nil + } + + commitStore, err := commit_store.NewCommitStoreFilterer(addr, c.client) + if err != nil { + return nil, err + } + + c.dependencyCache.Store(addr, commitStore) + return commitStore, nil +} + func loadCachedDependency[T any](cache *sync.Map, addr common.Address) (T, bool) { var empty T diff --git a/core/services/ocr2/plugins/ccip/internal/ccipevents/logpoller_test.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/logpoller_test.go similarity index 96% rename from core/services/ocr2/plugins/ccip/internal/ccipevents/logpoller_test.go rename to core/services/ocr2/plugins/ccip/internal/ccipdata/logpoller_test.go index 27de1c09ab..42cb107c64 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipevents/logpoller_test.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/logpoller_test.go @@ -1,4 +1,4 @@ -package ccipevents +package ccipdata import ( "context" @@ -20,7 +20,7 @@ import ( ) func TestLogPollerClient_loadDependency(t *testing.T) { - c := &LogPollerClient{} + c := &LogPollerReader{} someAddr := utils.RandomAddress() @@ -90,7 +90,7 @@ func TestLogPollerClient_GetSendRequestsGteSeqNum(t *testing.T) { mock.Anything, ).Return([]logpoller.Log{}, nil) - c := &LogPollerClient{lp: lp} + c := &LogPollerReader{lp: lp} events, err := c.GetSendRequestsGteSeqNum( context.Background(), onRampAddr, @@ -119,7 +119,7 @@ func TestLogPollerClient_GetSendRequestsGteSeqNum(t *testing.T) { cl := evmClientMocks.NewClient(t) cl.On("HeaderByNumber", mock.Anything, mock.Anything).Return(h, nil) - c := &LogPollerClient{lp: lp, client: cl} + c := &LogPollerReader{lp: lp, client: cl} events, err := c.GetSendRequestsGteSeqNum( context.Background(), onRampAddr, diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/mock.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/mock.go new file mode 100644 index 0000000000..4389ed2fcb --- /dev/null +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/mock.go @@ -0,0 +1,246 @@ +// Code generated by mockery v2.28.1. DO NOT EDIT. + +package ccipdata + +import ( + common "github.com/ethereum/go-ethereum/common" + commit_store "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/commit_store" + + context "context" + + evm_2_evm_offramp "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_offramp" + + evm_2_evm_onramp "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_onramp" + + mock "github.com/stretchr/testify/mock" + + price_registry "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/price_registry" + + time "time" +) + +// MockReader is an autogenerated mock type for the Reader type +type MockReader struct { + mock.Mock +} + +// GetAcceptedCommitReportsGteSeqNum provides a mock function with given fields: ctx, commitStoreAddress, seqNum, confs +func (_m *MockReader) GetAcceptedCommitReportsGteSeqNum(ctx context.Context, commitStoreAddress common.Address, seqNum uint64, confs int) ([]Event[commit_store.CommitStoreReportAccepted], error) { + ret := _m.Called(ctx, commitStoreAddress, seqNum, confs) + + var r0 []Event[commit_store.CommitStoreReportAccepted] + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, common.Address, uint64, int) ([]Event[commit_store.CommitStoreReportAccepted], error)); ok { + return rf(ctx, commitStoreAddress, seqNum, confs) + } + if rf, ok := ret.Get(0).(func(context.Context, common.Address, uint64, int) []Event[commit_store.CommitStoreReportAccepted]); ok { + r0 = rf(ctx, commitStoreAddress, seqNum, confs) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]Event[commit_store.CommitStoreReportAccepted]) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, common.Address, uint64, int) error); ok { + r1 = rf(ctx, commitStoreAddress, seqNum, confs) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetAcceptedCommitReportsGteTimestamp provides a mock function with given fields: ctx, commitStoreAddress, ts, confs +func (_m *MockReader) GetAcceptedCommitReportsGteTimestamp(ctx context.Context, commitStoreAddress common.Address, ts time.Time, confs int) ([]Event[commit_store.CommitStoreReportAccepted], error) { + ret := _m.Called(ctx, commitStoreAddress, ts, confs) + + var r0 []Event[commit_store.CommitStoreReportAccepted] + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, common.Address, time.Time, int) ([]Event[commit_store.CommitStoreReportAccepted], error)); ok { + return rf(ctx, commitStoreAddress, ts, confs) + } + if rf, ok := ret.Get(0).(func(context.Context, common.Address, time.Time, int) []Event[commit_store.CommitStoreReportAccepted]); ok { + r0 = rf(ctx, commitStoreAddress, ts, confs) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]Event[commit_store.CommitStoreReportAccepted]) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, common.Address, time.Time, int) error); ok { + r1 = rf(ctx, commitStoreAddress, ts, confs) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetExecutionStateChangesBetweenSeqNums provides a mock function with given fields: ctx, offRamp, seqNumMin, seqNumMax, confs +func (_m *MockReader) GetExecutionStateChangesBetweenSeqNums(ctx context.Context, offRamp common.Address, seqNumMin uint64, seqNumMax uint64, confs int) ([]Event[evm_2_evm_offramp.EVM2EVMOffRampExecutionStateChanged], error) { + ret := _m.Called(ctx, offRamp, seqNumMin, seqNumMax, confs) + + var r0 []Event[evm_2_evm_offramp.EVM2EVMOffRampExecutionStateChanged] + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, common.Address, uint64, uint64, int) ([]Event[evm_2_evm_offramp.EVM2EVMOffRampExecutionStateChanged], error)); ok { + return rf(ctx, offRamp, seqNumMin, seqNumMax, confs) + } + if rf, ok := ret.Get(0).(func(context.Context, common.Address, uint64, uint64, int) []Event[evm_2_evm_offramp.EVM2EVMOffRampExecutionStateChanged]); ok { + r0 = rf(ctx, offRamp, seqNumMin, seqNumMax, confs) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]Event[evm_2_evm_offramp.EVM2EVMOffRampExecutionStateChanged]) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, common.Address, uint64, uint64, int) error); ok { + r1 = rf(ctx, offRamp, seqNumMin, seqNumMax, confs) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetGasPriceUpdatesCreatedAfter provides a mock function with given fields: ctx, priceRegistry, chainSelector, ts, confs +func (_m *MockReader) GetGasPriceUpdatesCreatedAfter(ctx context.Context, priceRegistry common.Address, chainSelector uint64, ts time.Time, confs int) ([]Event[price_registry.PriceRegistryUsdPerUnitGasUpdated], error) { + ret := _m.Called(ctx, priceRegistry, chainSelector, ts, confs) + + var r0 []Event[price_registry.PriceRegistryUsdPerUnitGasUpdated] + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, common.Address, uint64, time.Time, int) ([]Event[price_registry.PriceRegistryUsdPerUnitGasUpdated], error)); ok { + return rf(ctx, priceRegistry, chainSelector, ts, confs) + } + if rf, ok := ret.Get(0).(func(context.Context, common.Address, uint64, time.Time, int) []Event[price_registry.PriceRegistryUsdPerUnitGasUpdated]); ok { + r0 = rf(ctx, priceRegistry, chainSelector, ts, confs) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]Event[price_registry.PriceRegistryUsdPerUnitGasUpdated]) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, common.Address, uint64, time.Time, int) error); ok { + r1 = rf(ctx, priceRegistry, chainSelector, ts, confs) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetSendRequestsBetweenSeqNums provides a mock function with given fields: ctx, onRamp, seqNumMin, seqNumMax, confs +func (_m *MockReader) GetSendRequestsBetweenSeqNums(ctx context.Context, onRamp common.Address, seqNumMin uint64, seqNumMax uint64, confs int) ([]Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested], error) { + ret := _m.Called(ctx, onRamp, seqNumMin, seqNumMax, confs) + + var r0 []Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested] + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, common.Address, uint64, uint64, int) ([]Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested], error)); ok { + return rf(ctx, onRamp, seqNumMin, seqNumMax, confs) + } + if rf, ok := ret.Get(0).(func(context.Context, common.Address, uint64, uint64, int) []Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested]); ok { + r0 = rf(ctx, onRamp, seqNumMin, seqNumMax, confs) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested]) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, common.Address, uint64, uint64, int) error); ok { + r1 = rf(ctx, onRamp, seqNumMin, seqNumMax, confs) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetSendRequestsGteSeqNum provides a mock function with given fields: ctx, onRamp, seqNum, checkFinalityTags, confs +func (_m *MockReader) GetSendRequestsGteSeqNum(ctx context.Context, onRamp common.Address, seqNum uint64, checkFinalityTags bool, confs int) ([]Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested], error) { + ret := _m.Called(ctx, onRamp, seqNum, checkFinalityTags, confs) + + var r0 []Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested] + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, common.Address, uint64, bool, int) ([]Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested], error)); ok { + return rf(ctx, onRamp, seqNum, checkFinalityTags, confs) + } + if rf, ok := ret.Get(0).(func(context.Context, common.Address, uint64, bool, int) []Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested]); ok { + r0 = rf(ctx, onRamp, seqNum, checkFinalityTags, confs) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested]) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, common.Address, uint64, bool, int) error); ok { + r1 = rf(ctx, onRamp, seqNum, checkFinalityTags, confs) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetTokenPriceUpdatesCreatedAfter provides a mock function with given fields: ctx, priceRegistry, ts, confs +func (_m *MockReader) GetTokenPriceUpdatesCreatedAfter(ctx context.Context, priceRegistry common.Address, ts time.Time, confs int) ([]Event[price_registry.PriceRegistryUsdPerTokenUpdated], error) { + ret := _m.Called(ctx, priceRegistry, ts, confs) + + var r0 []Event[price_registry.PriceRegistryUsdPerTokenUpdated] + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, common.Address, time.Time, int) ([]Event[price_registry.PriceRegistryUsdPerTokenUpdated], error)); ok { + return rf(ctx, priceRegistry, ts, confs) + } + if rf, ok := ret.Get(0).(func(context.Context, common.Address, time.Time, int) []Event[price_registry.PriceRegistryUsdPerTokenUpdated]); ok { + r0 = rf(ctx, priceRegistry, ts, confs) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]Event[price_registry.PriceRegistryUsdPerTokenUpdated]) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, common.Address, time.Time, int) error); ok { + r1 = rf(ctx, priceRegistry, ts, confs) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// LatestBlock provides a mock function with given fields: ctx +func (_m *MockReader) LatestBlock(ctx context.Context) (int64, error) { + ret := _m.Called(ctx) + + var r0 int64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (int64, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) int64); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(int64) + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewMockReader interface { + mock.TestingT + Cleanup(func()) +} + +// NewMockReader creates a new instance of MockReader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewMockReader(t mockConstructorTestingTNewMockReader) *MockReader { + mock := &MockReader{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/services/ocr2/plugins/ccip/internal/ccipevents/client.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/reader.go similarity index 71% rename from core/services/ocr2/plugins/ccip/internal/ccipevents/client.go rename to core/services/ocr2/plugins/ccip/internal/ccipdata/reader.go index d3419b744a..4f43187e67 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipevents/client.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/reader.go @@ -1,4 +1,4 @@ -package ccipevents +package ccipdata import ( "context" @@ -6,6 +6,7 @@ import ( "github.com/ethereum/go-ethereum/common" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/commit_store" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_offramp" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_onramp" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/price_registry" @@ -21,8 +22,10 @@ type BlockMeta struct { BlockNumber int64 } -// Client can be used to fetch CCIP related parsed on-chain events. -type Client interface { +// Client can be used to fetch CCIP related parsed on-chain data. +// +//go:generate mockery --quiet --name Reader --output . --filename mock.go --inpackage --case=underscore +type Reader interface { // GetSendRequestsGteSeqNum returns all the message send requests with sequence number greater than or equal to the provided. // If checkFinalityTags is set to true then confs param is ignored, the latest finalized block is used in the query. GetSendRequestsGteSeqNum(ctx context.Context, onRamp common.Address, seqNum uint64, checkFinalityTags bool, confs int) ([]Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested], error) @@ -39,6 +42,12 @@ type Client interface { // GetExecutionStateChangesBetweenSeqNums returns all the execution state change events for the provided message sequence numbers (inclusive). GetExecutionStateChangesBetweenSeqNums(ctx context.Context, offRamp common.Address, seqNumMin, seqNumMax uint64, confs int) ([]Event[evm_2_evm_offramp.EVM2EVMOffRampExecutionStateChanged], error) + // GetAcceptedCommitReportsGteSeqNum returns all the accepted commit reports that have sequence number greater than or equal to the provided. + GetAcceptedCommitReportsGteSeqNum(ctx context.Context, commitStoreAddress common.Address, seqNum uint64, confs int) ([]Event[commit_store.CommitStoreReportAccepted], error) + + // GetAcceptedCommitReportsGteTimestamp returns all the commit reports with timestamp greater than or equal to the provided. + GetAcceptedCommitReportsGteTimestamp(ctx context.Context, commitStoreAddress common.Address, ts time.Time, confs int) ([]Event[commit_store.CommitStoreReportAccepted], error) + // LatestBlock returns the latest known/parsed block of the underlying implementation. LatestBlock(ctx context.Context) (int64, error) } diff --git a/core/services/ocr2/plugins/ccip/plugins_common.go b/core/services/ocr2/plugins/ccip/plugins_common.go index 9fed840bd9..93e3f94f38 100644 --- a/core/services/ocr2/plugins/ccip/plugins_common.go +++ b/core/services/ocr2/plugins/ccip/plugins_common.go @@ -20,7 +20,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_onramp_1_1_0" "github.com/smartcontractkit/chainlink/v2/core/logger" ccipconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipevents" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/hashlib" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/observability" "github.com/smartcontractkit/chainlink/v2/core/services/pg" @@ -141,7 +141,7 @@ func leavesFromIntervals( lggr logger.Logger, interval commit_store.CommitStoreInterval, hasher hashlib.LeafHasherInterface[[32]byte], - sendReqs []ccipevents.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested], + sendReqs []ccipdata.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested], ) ([][32]byte, error) { var seqNrs []uint64 for _, req := range sendReqs { diff --git a/core/services/ocr2/plugins/ccip/testhelpers/commitstore.go b/core/services/ocr2/plugins/ccip/testhelpers/commitstore.go new file mode 100644 index 0000000000..b41104c44f --- /dev/null +++ b/core/services/ocr2/plugins/ccip/testhelpers/commitstore.go @@ -0,0 +1,89 @@ +package testhelpers + +import ( + "sync" + "testing" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/commit_store" + mock_contracts "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/mocks" + "github.com/smartcontractkit/chainlink/v2/core/utils" +) + +type FakeCommitStore struct { + *mock_contracts.CommitStoreInterface + + isPaused bool + blessedRoots map[[32]byte]bool + staticCfg commit_store.CommitStoreStaticConfig + dynamicCfg commit_store.CommitStoreDynamicConfig + nextSeqNum uint64 + + mu sync.RWMutex +} + +func NewFakeCommitStore(t *testing.T, nextSeqNum uint64) (*FakeCommitStore, common.Address) { + addr := utils.RandomAddress() + mockCommitStore := mock_contracts.NewCommitStoreInterface(t) + mockCommitStore.On("Address").Return(addr).Maybe() + + commitStore := &FakeCommitStore{CommitStoreInterface: mockCommitStore} + commitStore.SetPaused(false) + commitStore.SetNextSequenceNumber(nextSeqNum) + + return commitStore, addr +} + +func (cs *FakeCommitStore) SetPaused(isPaused bool) { + setCommitStoreVal(cs, func(cs *FakeCommitStore) { cs.isPaused = isPaused }) +} + +func (cs *FakeCommitStore) IsUnpausedAndARMHealthy(opts *bind.CallOpts) (bool, error) { + return getCommitStoreVal(cs, func(cs *FakeCommitStore) bool { return !cs.isPaused }), nil +} + +func (cs *FakeCommitStore) SetBlessedRoots(roots map[[32]byte]bool) { + setCommitStoreVal(cs, func(cs *FakeCommitStore) { cs.blessedRoots = roots }) +} + +func (cs *FakeCommitStore) IsBlessed(opts *bind.CallOpts, root [32]byte) (bool, error) { + return getCommitStoreVal(cs, func(cs *FakeCommitStore) bool { return cs.blessedRoots[root] }), nil +} + +func (cs *FakeCommitStore) SetStaticConfig(cfg commit_store.CommitStoreStaticConfig) { + setCommitStoreVal(cs, func(cs *FakeCommitStore) { cs.staticCfg = cfg }) +} + +func (cs *FakeCommitStore) GetStaticConfig(opts *bind.CallOpts) (commit_store.CommitStoreStaticConfig, error) { + return getCommitStoreVal(cs, func(cs *FakeCommitStore) commit_store.CommitStoreStaticConfig { return cs.staticCfg }), nil +} + +func (cs *FakeCommitStore) SetDynamicConfig(cfg commit_store.CommitStoreDynamicConfig) { + setCommitStoreVal(cs, func(cs *FakeCommitStore) { cs.dynamicCfg = cfg }) +} + +func (cs *FakeCommitStore) GetDynamicConfig(opts *bind.CallOpts) (commit_store.CommitStoreDynamicConfig, error) { + return getCommitStoreVal(cs, func(cs *FakeCommitStore) commit_store.CommitStoreDynamicConfig { return cs.dynamicCfg }), nil +} + +func (cs *FakeCommitStore) SetNextSequenceNumber(seqNum uint64) { + setCommitStoreVal(cs, func(cs *FakeCommitStore) { cs.nextSeqNum = seqNum }) +} + +func (cs *FakeCommitStore) GetExpectedNextSequenceNumber(opts *bind.CallOpts) (uint64, error) { + return getCommitStoreVal(cs, func(cs *FakeCommitStore) uint64 { return cs.nextSeqNum }), nil +} + +func getCommitStoreVal[T any](cs *FakeCommitStore, getter func(cs *FakeCommitStore) T) T { + cs.mu.RLock() + defer cs.mu.RUnlock() + return getter(cs) +} + +func setCommitStoreVal(cs *FakeCommitStore, setter func(cs *FakeCommitStore)) { + cs.mu.Lock() + defer cs.mu.Unlock() + setter(cs) +} diff --git a/core/services/ocr2/plugins/ccip/testhelpers/integration/chainlink.go b/core/services/ocr2/plugins/ccip/testhelpers/integration/chainlink.go index 9dc4ddd6f8..1b92a30996 100644 --- a/core/services/ocr2/plugins/ccip/testhelpers/integration/chainlink.go +++ b/core/services/ocr2/plugins/ccip/testhelpers/integration/chainlink.go @@ -18,9 +18,7 @@ import ( "github.com/google/uuid" "github.com/onsi/gomega" "github.com/pkg/errors" - "github.com/smartcontractkit/chainlink-relay/pkg/loop" - ctfClient "github.com/smartcontractkit/chainlink/integration-tests/client" "github.com/smartcontractkit/libocr/commontypes" "github.com/smartcontractkit/libocr/offchainreporting2/confighelper" types4 "github.com/smartcontractkit/libocr/offchainreporting2plus/types" @@ -28,6 +26,8 @@ import ( "go.uber.org/zap" "k8s.io/utils/pointer" + "github.com/smartcontractkit/chainlink-relay/pkg/loop" + ctfClient "github.com/smartcontractkit/chainlink/integration-tests/client" "github.com/smartcontractkit/chainlink/v2/core/chains/evm" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" v2 "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/toml" diff --git a/core/services/ocr2/plugins/ccip/testhelpers/integration/jobspec.go b/core/services/ocr2/plugins/ccip/testhelpers/integration/jobspec.go index d3372d7ed2..13e75f8084 100644 --- a/core/services/ocr2/plugins/ccip/testhelpers/integration/jobspec.go +++ b/core/services/ocr2/plugins/ccip/testhelpers/integration/jobspec.go @@ -6,6 +6,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/lib/pq" + "github.com/smartcontractkit/chainlink-relay/pkg/types" "github.com/smartcontractkit/chainlink/integration-tests/client" diff --git a/core/services/ocr2/plugins/ccip/testhelpers/offramp.go b/core/services/ocr2/plugins/ccip/testhelpers/offramp.go new file mode 100644 index 0000000000..16b572b5c0 --- /dev/null +++ b/core/services/ocr2/plugins/ccip/testhelpers/offramp.go @@ -0,0 +1,105 @@ +package testhelpers + +import ( + "sync" + "testing" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/pkg/errors" + + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_offramp" + mock_contracts "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/mocks" + "github.com/smartcontractkit/chainlink/v2/core/utils" +) + +type FakeOffRamp struct { + *mock_contracts.EVM2EVMOffRampInterface + + rateLimiterState evm_2_evm_offramp.RateLimiterTokenBucket + senderNonces map[common.Address]uint64 + tokenToPool map[common.Address]common.Address + dynamicConfig evm_2_evm_offramp.EVM2EVMOffRampDynamicConfig + sourceToDestTokens map[common.Address]common.Address + + mu sync.RWMutex +} + +func NewFakeOffRamp(t *testing.T) (*FakeOffRamp, common.Address) { + addr := utils.RandomAddress() + mockOffRamp := mock_contracts.NewEVM2EVMOffRampInterface(t) + mockOffRamp.On("Address").Return(addr).Maybe() + + offRamp := &FakeOffRamp{EVM2EVMOffRampInterface: mockOffRamp} + return offRamp, addr +} + +func (o *FakeOffRamp) CurrentRateLimiterState(opts *bind.CallOpts) (evm_2_evm_offramp.RateLimiterTokenBucket, error) { + return getOffRampVal(o, func(o *FakeOffRamp) (evm_2_evm_offramp.RateLimiterTokenBucket, error) { return o.rateLimiterState, nil }) +} + +func (o *FakeOffRamp) SetRateLimiterState(state evm_2_evm_offramp.RateLimiterTokenBucket) { + setOffRampVal(o, func(o *FakeOffRamp) { o.rateLimiterState = state }) +} + +func (o *FakeOffRamp) GetSenderNonce(opts *bind.CallOpts, sender common.Address) (uint64, error) { + return getOffRampVal(o, func(o *FakeOffRamp) (uint64, error) { return o.senderNonces[sender], nil }) +} + +func (o *FakeOffRamp) SetSenderNonces(senderNonces map[common.Address]uint64) { + setOffRampVal(o, func(o *FakeOffRamp) { o.senderNonces = senderNonces }) +} + +func (o *FakeOffRamp) GetPoolByDestToken(opts *bind.CallOpts, destToken common.Address) (common.Address, error) { + return getOffRampVal(o, func(o *FakeOffRamp) (common.Address, error) { return o.tokenToPool[destToken], nil }) +} + +func (o *FakeOffRamp) SetTokenPools(tokenToPool map[common.Address]common.Address) { + setOffRampVal(o, func(o *FakeOffRamp) { o.tokenToPool = tokenToPool }) +} + +func (o *FakeOffRamp) GetDynamicConfig(opts *bind.CallOpts) (evm_2_evm_offramp.EVM2EVMOffRampDynamicConfig, error) { + return getOffRampVal(o, func(o *FakeOffRamp) (evm_2_evm_offramp.EVM2EVMOffRampDynamicConfig, error) { + return o.dynamicConfig, nil + }) +} + +func (o *FakeOffRamp) SetDynamicConfig(cfg evm_2_evm_offramp.EVM2EVMOffRampDynamicConfig) { + setOffRampVal(o, func(o *FakeOffRamp) { o.dynamicConfig = cfg }) +} + +func (o *FakeOffRamp) SetSourceToDestTokens(m map[common.Address]common.Address) { + setOffRampVal(o, func(o *FakeOffRamp) { o.sourceToDestTokens = m }) +} + +func (o *FakeOffRamp) GetSupportedTokens(opts *bind.CallOpts) ([]common.Address, error) { + return getOffRampVal(o, func(o *FakeOffRamp) ([]common.Address, error) { + tks := make([]common.Address, 0, len(o.sourceToDestTokens)) + for tk := range o.sourceToDestTokens { + tks = append(tks, tk) + } + return tks, nil + }) +} + +func (o *FakeOffRamp) GetDestinationToken(opts *bind.CallOpts, sourceToken common.Address) (common.Address, error) { + return getOffRampVal(o, func(o *FakeOffRamp) (common.Address, error) { + addr, exists := o.sourceToDestTokens[sourceToken] + if !exists { + return common.Address{}, errors.New("token does not exist") + } + return addr, nil + }) +} + +func getOffRampVal[T any](o *FakeOffRamp, getter func(o *FakeOffRamp) (T, error)) (T, error) { + o.mu.RLock() + defer o.mu.RUnlock() + return getter(o) +} + +func setOffRampVal(o *FakeOffRamp, setter func(o *FakeOffRamp)) { + o.mu.Lock() + defer o.mu.Unlock() + setter(o) +} diff --git a/core/services/ocr2/plugins/ccip/testhelpers/onramp.go b/core/services/ocr2/plugins/ccip/testhelpers/onramp.go new file mode 100644 index 0000000000..bd51b2ccd2 --- /dev/null +++ b/core/services/ocr2/plugins/ccip/testhelpers/onramp.go @@ -0,0 +1,56 @@ +package testhelpers + +import ( + "fmt" + "sync" + "testing" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_onramp" + mock_contracts "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/mocks" + ccipconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" + "github.com/smartcontractkit/chainlink/v2/core/utils" +) + +type FakeOnRamp struct { + *mock_contracts.EVM2EVMOnRampInterface + + dynamicConfig evm_2_evm_onramp.EVM2EVMOnRampDynamicConfig + + mu sync.RWMutex +} + +func NewFakeOnRamp(t *testing.T) (*FakeOnRamp, common.Address) { + addr := utils.RandomAddress() + mockOnRamp := mock_contracts.NewEVM2EVMOnRampInterface(t) + mockOnRamp.On("Address").Return(addr).Maybe() + + onRamp := &FakeOnRamp{EVM2EVMOnRampInterface: mockOnRamp} + return onRamp, addr +} + +func (o *FakeOnRamp) TypeAndVersion(opts *bind.CallOpts) (string, error) { + return fmt.Sprintf("%s %s", ccipconfig.EVM2EVMOnRamp, "1.2.0"), nil +} + +func (o *FakeOnRamp) SetDynamicCfg(cfg evm_2_evm_onramp.EVM2EVMOnRampDynamicConfig) { + setOnRampVal(o, func(o *FakeOnRamp) { o.dynamicConfig = cfg }) +} + +func (o *FakeOnRamp) GetDynamicConfig(opts *bind.CallOpts) (evm_2_evm_onramp.EVM2EVMOnRampDynamicConfig, error) { + return getOnRampVal(o, func(o *FakeOnRamp) (evm_2_evm_onramp.EVM2EVMOnRampDynamicConfig, error) { return o.dynamicConfig, nil }) +} + +func getOnRampVal[T any](o *FakeOnRamp, getter func(o *FakeOnRamp) (T, error)) (T, error) { + o.mu.RLock() + defer o.mu.RUnlock() + return getter(o) +} + +func setOnRampVal(o *FakeOnRamp, setter func(o *FakeOnRamp)) { + o.mu.Lock() + defer o.mu.Unlock() + setter(o) +} diff --git a/core/services/ocr2/plugins/ccip/testhelpers/plugins/plugin_harness.go b/core/services/ocr2/plugins/ccip/testhelpers/plugins/plugin_harness.go deleted file mode 100644 index 61de46d80f..0000000000 --- a/core/services/ocr2/plugins/ccip/testhelpers/plugins/plugin_harness.go +++ /dev/null @@ -1,275 +0,0 @@ -package plugintesthelpers - -import ( - "math/big" - "testing" - "time" - - "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" - "github.com/stretchr/testify/require" - - evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/commit_store" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_offramp" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/price_registry" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/router" - "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" - "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/pgtest" - "github.com/smartcontractkit/chainlink/v2/core/logger" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" - ccipconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/hashlib" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/merklemulti" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/testhelpers" - "github.com/smartcontractkit/chainlink/v2/core/utils" -) - -type OCR2TestContract interface { - SetOCR2Config(opts *bind.TransactOpts, signers []common.Address, transmitters []common.Address, f uint8, onchainConfig []byte, offchainConfigVersion uint64, offchainConfig []byte) (*types.Transaction, error) -} - -func generateAndSetTestOCR2Config(contract OCR2TestContract, owner *bind.TransactOpts, onChainConfig []byte) (*types.Transaction, error) { - var signers []common.Address - var transmitters []common.Address - - for i := 0; i < 4; i++ { - signers = append(signers, utils.RandomAddress()) - transmitters = append(transmitters, utils.RandomAddress()) - } - - return contract.SetOCR2Config(owner, signers, transmitters, 1, onChainConfig, 2, nil) -} - -type CCIPPluginTestHarness struct { - testhelpers.CCIPContracts - Lggr logger.Logger - - SourceLP logpoller.LogPollerTest - DestLP logpoller.LogPollerTest - DestClient evmclient.Client - SourceClient evmclient.Client - - CommitOnchainConfig ccipconfig.CommitOnchainConfig - ExecOnchainConfig ccipconfig.ExecOnchainConfig -} - -func (th *CCIPPluginTestHarness) CommitAndPollLogs(t *testing.T) { - th.Source.Chain.Commit() - th.SourceLP.PollAndSaveLogs(testutils.Context(t), th.Source.Chain.Blockchain().CurrentBlock().Number.Int64()) - - th.Dest.Chain.Commit() - th.DestLP.PollAndSaveLogs(testutils.Context(t), th.Dest.Chain.Blockchain().CurrentBlock().Number.Int64()) -} - -func SetupCCIPTestHarness(t *testing.T) CCIPPluginTestHarness { - c := testhelpers.SetupCCIPContracts(t, testhelpers.SourceChainID, testhelpers.SourceChainSelector, testhelpers.DestChainID, testhelpers.DestChainSelector) - - lggr := logger.TestLogger(t) - - // db, clients and logpollers - db := pgtest.NewSqlxDB(t) - - sourceORM := logpoller.NewORM(new(big.Int).SetUint64(c.Source.ChainID), db, lggr, pgtest.NewQConfig(true)) - var sourceLP logpoller.LogPollerTest = logpoller.NewLogPoller( - sourceORM, - evmclient.NewSimulatedBackendClient(t, c.Source.Chain, new(big.Int).SetUint64(c.Source.ChainID)), - lggr.Named("sourceLP"), - 1*time.Hour, 1, 10, 10, 1000, - ) - - destORM := logpoller.NewORM(new(big.Int).SetUint64(c.Dest.ChainID), db, lggr, pgtest.NewQConfig(true)) - var destLP logpoller.LogPollerTest = logpoller.NewLogPoller( - destORM, - evmclient.NewSimulatedBackendClient(t, c.Dest.Chain, new(big.Int).SetUint64(c.Dest.ChainID)), - lggr.Named("destLP"), - 1*time.Hour, 1, 10, 10, 1000, - ) - - // onChain configs - encodedCommitOnchainConfig := c.CreateDefaultCommitOnchainConfig(t) - commitOnchainConfig, err := abihelpers.DecodeAbiStruct[ccipconfig.CommitOnchainConfig](encodedCommitOnchainConfig) - require.NoError(t, err) - - _, err = generateAndSetTestOCR2Config(c.Dest.CommitStore, c.Dest.User, encodedCommitOnchainConfig) - require.NoError(t, err) - - encodedExecOnchainConfig := c.CreateDefaultExecOnchainConfig(t) - execOnchainConfig, err := abihelpers.DecodeAbiStruct[ccipconfig.ExecOnchainConfig](encodedExecOnchainConfig) - require.NoError(t, err) - - _, err = generateAndSetTestOCR2Config(c.Dest.OffRamp, c.Dest.User, encodedExecOnchainConfig) - require.NoError(t, err) - c.Dest.Chain.Commit() - - // approve router - _, err = c.Source.LinkToken.Approve(c.Source.User, c.Source.Router.Address(), testhelpers.Link(500)) - require.NoError(t, err) - c.Source.Chain.Commit() - - _, err = c.Dest.PriceRegistry.UpdatePrices(c.Dest.User, price_registry.InternalPriceUpdates{ - TokenPriceUpdates: []price_registry.InternalTokenPriceUpdate{ - {SourceToken: c.Dest.LinkToken.Address(), UsdPerToken: big.NewInt(5)}, - {SourceToken: c.Dest.WrappedNative.Address(), UsdPerToken: big.NewInt(5)}, - }, - DestChainSelector: c.Dest.ChainSelector, - UsdPerUnitGas: big.NewInt(1), - }) - require.NoError(t, err) - - // register filters in logPoller - require.NoError(t, sourceLP.RegisterFilter(logpoller.Filter{ - Name: logpoller.FilterName("Commit ccip sends", c.Source.OnRamp.Address().String()), - EventSigs: []common.Hash{abihelpers.EventSignatures.SendRequested}, Addresses: []common.Address{c.Source.OnRamp.Address()}, - })) - require.NoError(t, destLP.RegisterFilter(logpoller.Filter{ - Name: logpoller.FilterName("Commit price updates", c.Dest.PriceRegistry.Address()), - EventSigs: []common.Hash{abihelpers.EventSignatures.UsdPerUnitGasUpdated, abihelpers.EventSignatures.UsdPerTokenUpdated}, Addresses: []common.Address{c.Dest.PriceRegistry.Address()}, - })) - require.NoError(t, destLP.RegisterFilter(logpoller.Filter{ - Name: logpoller.FilterName("Exec report accepts", c.Dest.CommitStore.Address().String()), - EventSigs: []common.Hash{abihelpers.EventSignatures.ReportAccepted}, Addresses: []common.Address{c.Dest.CommitStore.Address()}, - })) - require.NoError(t, destLP.RegisterFilter(logpoller.Filter{ - Name: logpoller.FilterName("Exec execution state changes", c.Dest.OffRamp.Address().String()), - EventSigs: []common.Hash{abihelpers.EventSignatures.ExecutionStateChanged}, Addresses: []common.Address{c.Dest.OffRamp.Address()}, - })) - require.NoError(t, destLP.RegisterFilter(logpoller.Filter{ - Name: logpoller.FilterName("Token pool added", c.Dest.OffRamp.Address().String()), - EventSigs: []common.Hash{abihelpers.EventSignatures.PoolAdded}, Addresses: []common.Address{c.Dest.OffRamp.Address()}, - })) - require.NoError(t, destLP.RegisterFilter(logpoller.Filter{ - Name: logpoller.FilterName("Token pool removed", c.Dest.OffRamp.Address().String()), - EventSigs: []common.Hash{abihelpers.EventSignatures.PoolRemoved}, Addresses: []common.Address{c.Dest.OffRamp.Address()}, - })) - require.NoError(t, destLP.RegisterFilter(logpoller.Filter{ - Name: logpoller.FilterName("Fee token added", c.Dest.PriceRegistry.Address().String()), - EventSigs: []common.Hash{abihelpers.EventSignatures.FeeTokenAdded}, Addresses: []common.Address{c.Dest.PriceRegistry.Address()}, - })) - require.NoError(t, destLP.RegisterFilter(logpoller.Filter{ - Name: logpoller.FilterName("Fee token removed", c.Dest.PriceRegistry.Address().String()), - EventSigs: []common.Hash{abihelpers.EventSignatures.FeeTokenRemoved}, Addresses: []common.Address{c.Dest.PriceRegistry.Address()}, - })) - - // start and backfill logpollers - require.NoError(t, sourceLP.Start(testutils.Context(t))) - require.NoError(t, destLP.Start(testutils.Context(t))) - require.NoError(t, sourceLP.Replay(testutils.Context(t), 1)) - require.NoError(t, destLP.Replay(testutils.Context(t), 1)) - - th := CCIPPluginTestHarness{ - CCIPContracts: c, - Lggr: lggr, - - SourceLP: sourceLP, - DestLP: destLP, - DestClient: evmclient.NewSimulatedBackendClient(t, c.Dest.Chain, new(big.Int).SetUint64(c.Dest.ChainID)), - SourceClient: evmclient.NewSimulatedBackendClient(t, c.Source.Chain, new(big.Int).SetUint64(c.Source.ChainID)), - CommitOnchainConfig: commitOnchainConfig, - ExecOnchainConfig: execOnchainConfig, - } - - th.CommitAndPollLogs(t) - return th -} - -type MessageBatch struct { - TokenData [][][]byte - Messages []evm_2_evm_offramp.InternalEVM2EVMMessage - Interval commit_store.CommitStoreInterval - Root [32]byte - Proof merklemulti.Proof[[32]byte] - ProofBits *big.Int - Tree *merklemulti.Tree[[32]byte] -} - -func (mb MessageBatch) ToExecutionReport() evm_2_evm_offramp.InternalExecutionReport { - return evm_2_evm_offramp.InternalExecutionReport{ - Messages: mb.Messages, - OffchainTokenData: mb.TokenData, - Proofs: mb.Proof.Hashes, - ProofFlagBits: mb.ProofBits, - } -} - -func (th *CCIPPluginTestHarness) GenerateAndSendMessageBatch(t *testing.T, nMessages int, payloadSize int, nTokensPerMessage int) MessageBatch { - mctx := hashlib.NewKeccakCtx() - leafHasher := hashlib.NewLeafHasher(th.Source.ChainSelector, th.Dest.ChainSelector, th.Source.OnRamp.Address(), mctx) - - maxPayload := make([]byte, payloadSize) - for i := 0; i < payloadSize; i++ { - maxPayload[i] = 0xa - } - - var offchainTokenData [][]byte - var tokenAmounts []router.ClientEVMTokenAmount - for i := 0; i < nTokensPerMessage; i++ { - tokenAmounts = append(tokenAmounts, router.ClientEVMTokenAmount{ - Token: th.Source.LinkToken.Address(), - Amount: big.NewInt(int64(1 + i)), - }) - offchainTokenData = append(offchainTokenData, []byte{}) - } - - th.Source.Chain.Commit() - startBlock := th.Source.Chain.Blockchain().CurrentBlock().Number - var lastFlush int - for i := 0; i < nMessages; i++ { - routerMsg := router.ClientEVM2AnyMessage{ - Receiver: testhelpers.MustEncodeAddress(t, th.Dest.Receivers[0].Receiver.Address()), - FeeToken: th.Source.LinkToken.Address(), - TokenAmounts: tokenAmounts, - Data: maxPayload, - ExtraArgs: []byte{}, - } - _, err := th.Source.Router.CcipSend(th.Source.User, th.Dest.ChainSelector, routerMsg) - require.NoError(t, err) - lastFlush++ - if lastFlush*payloadSize > 700_000 { - th.CommitAndPollLogs(t) - lastFlush = 0 - } - } - th.CommitAndPollLogs(t) - - leafHashes := make([][32]byte, nMessages) - tokenData := make([][][]byte, nMessages) - indices := make([]int, nMessages) - messages := make([]evm_2_evm_offramp.InternalEVM2EVMMessage, nMessages) - seqNums := make([]uint64, nMessages) - - sendEvents, err := th.Source.OnRamp.FilterCCIPSendRequested(&bind.FilterOpts{Start: startBlock.Uint64(), Context: testutils.Context(t)}) - require.NoError(t, err) - var i int - for ; sendEvents.Next(); i++ { - indices[i] = i - tokenData[i] = offchainTokenData - messages[i] = abihelpers.OnRampMessageToOffRampMessage(sendEvents.Event.Message) - leafHash, err2 := leafHasher.HashLeaf(sendEvents.Event.Raw) - require.NoError(t, err2) - leafHashes[i] = leafHash - seqNums[i] = sendEvents.Event.Message.SequenceNumber - } - require.Equal(t, nMessages, i) - - tree, err := merklemulti.NewTree(mctx, leafHashes) - require.NoError(t, err) - proof, err := tree.Prove(indices) - require.NoError(t, err) - root := tree.Root() - rootLocal, err := merklemulti.VerifyComputeRoot(mctx, leafHashes, proof) - require.NoError(t, err) - require.Equal(t, root, rootLocal) - - return MessageBatch{ - Messages: messages, - Interval: commit_store.CommitStoreInterval{Min: seqNums[0], Max: seqNums[len(seqNums)-1]}, - TokenData: tokenData, - Root: root, - Proof: proof, - ProofBits: abihelpers.ProofFlagsToBits(proof.SourceFlags), - Tree: tree, - } -} diff --git a/core/services/ocr2/plugins/ccip/testhelpers/priceregistry.go b/core/services/ocr2/plugins/ccip/testhelpers/priceregistry.go new file mode 100644 index 0000000000..5b127e685c --- /dev/null +++ b/core/services/ocr2/plugins/ccip/testhelpers/priceregistry.go @@ -0,0 +1,52 @@ +package testhelpers + +import ( + "sync" + "testing" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/price_registry" + mock_contracts "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/mocks" + "github.com/smartcontractkit/chainlink/v2/core/utils" +) + +type FakePriceRegistry struct { + *mock_contracts.PriceRegistryInterface + + tokenPrices []price_registry.InternalTimestampedPackedUint224 + + mu sync.RWMutex +} + +func NewFakePriceRegistry(t *testing.T) (*FakePriceRegistry, common.Address) { + addr := utils.RandomAddress() + mockPriceRegistry := mock_contracts.NewPriceRegistryInterface(t) + mockPriceRegistry.On("Address").Return(addr).Maybe() + + priceRegistry := &FakePriceRegistry{PriceRegistryInterface: mockPriceRegistry} + return priceRegistry, addr +} + +func (p *FakePriceRegistry) SetTokenPrices(prices []price_registry.InternalTimestampedPackedUint224) { + setPriceRegistryVal(p, func(p *FakePriceRegistry) { p.tokenPrices = prices }) +} + +func (p *FakePriceRegistry) GetTokenPrices(opts *bind.CallOpts, tokens []common.Address) ([]price_registry.InternalTimestampedPackedUint224, error) { + return getPriceRegistryVal(p, func(p *FakePriceRegistry) ([]price_registry.InternalTimestampedPackedUint224, error) { + return p.tokenPrices, nil + }) +} + +func getPriceRegistryVal[T any](p *FakePriceRegistry, getter func(p *FakePriceRegistry) (T, error)) (T, error) { + p.mu.RLock() + defer p.mu.RUnlock() + return getter(p) +} + +func setPriceRegistryVal(p *FakePriceRegistry, setter func(p *FakePriceRegistry)) { + p.mu.Lock() + defer p.mu.Unlock() + setter(p) +}