From e6e2e51ff24c58b4ad7eb97de4b2fb59af7fa11b Mon Sep 17 00:00:00 2001 From: Anindita Ghosh <88458927+AnieeG@users.noreply.github.com> Date: Wed, 21 Aug 2024 04:19:23 -0700 Subject: [PATCH 1/2] Add deployment test in ci (#73) * add deployment test in ci * fix * address comments * fix test command * debug * change directory to changeset --------- Co-authored-by: Makram Kamaleddine --- .github/workflows/ccip-integration-test.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ccip-integration-test.yml b/.github/workflows/ccip-integration-test.yml index 8d4f8ac04..3fac7bedd 100644 --- a/.github/workflows/ccip-integration-test.yml +++ b/.github/workflows/ccip-integration-test.yml @@ -53,6 +53,8 @@ jobs: run: | cd $GITHUB_WORKSPACE/ccip go mod download + cd $GITHUB_WORKSPACE/ccip/integration-tests + go mod download - name: Build binary run: | cd $GITHUB_WORKSPACE/ccip @@ -65,8 +67,8 @@ jobs: CL_DATABASE_URL: ${{ env.DB_URL }} - name: Run ccip ocr3 integration test run: | - cd $GITHUB_WORKSPACE/ccip - go test -v -timeout 3m -run "^TestIntegration_OCR3Nodes$" ./core/capabilities/ccip/ccip_integration_tests + cd $GITHUB_WORKSPACE/ccip/integration-tests + go test -v -run '^Test0002_InitialDeploy$' -timeout 5m ./deployment/ccip/changeset EXITCODE=${PIPESTATUS[0]} if [ $EXITCODE -ne 0 ]; then echo "Integration test failed" From 4b625815be415abbee96b65797fe725c767baeaf Mon Sep 17 00:00:00 2001 From: Will Winder Date: Thu, 22 Aug 2024 10:16:14 -0400 Subject: [PATCH 2/2] exec: Implement filter state and nonce filtering business logic. (#69) * Move report generation into Filter state. * Add Nonces function to reader.CCIP interface. * Observe sender nonces during Filter state. * Consider sender nonces during message filtering. * Feature flag for checking message nonces. --- .mockery.yaml | 1 + commit/plugin_e2e_test.go | 11 +- commit/plugin_functions_test.go | 30 +- commitrmnocb/observation_test.go | 23 +- execute/exectypes/observation.go | 24 +- execute/exectypes/outcome.go | 3 +- execute/exectypes/outcome_test.go | 7 +- execute/plugin.go | 156 ++++--- execute/plugin_e2e_test.go | 19 +- execute/plugin_test.go | 13 +- execute/report/builder.go | 12 + execute/report/report.go | 90 +++- execute/report/report_test.go | 255 ++++++++++-- go.mod | 2 +- go.sum | 4 +- internal/mocks/ccipreader.go | 64 --- internal/mocks/inmem/ccipreader_inmem.go | 8 + internal/plugincommon/ccipreader_test.go | 8 +- internal/reader/ccip.go | 19 +- mocks/internal_/reader/ccip.go | 505 +++++++++++++++++++++++ 20 files changed, 1029 insertions(+), 225 deletions(-) delete mode 100644 internal/mocks/ccipreader.go create mode 100644 mocks/internal_/reader/ccip.go diff --git a/.mockery.yaml b/.mockery.yaml index 102b9d1d4..4b1e4213e 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -11,3 +11,4 @@ packages: github.com/smartcontractkit/chainlink-ccip/internal/reader: interfaces: HomeChain: + CCIP: diff --git a/commit/plugin_e2e_test.go b/commit/plugin_e2e_test.go index ac87c5959..cf7656e0a 100644 --- a/commit/plugin_e2e_test.go +++ b/commit/plugin_e2e_test.go @@ -23,6 +23,7 @@ import ( helpers "github.com/smartcontractkit/chainlink-ccip/internal/libs/testhelpers" "github.com/smartcontractkit/chainlink-ccip/internal/mocks" "github.com/smartcontractkit/chainlink-ccip/internal/reader" + reader_mock "github.com/smartcontractkit/chainlink-ccip/mocks/internal_/reader" "github.com/smartcontractkit/chainlink-ccip/pkg/consts" "github.com/smartcontractkit/chainlink-ccip/pluginconfig" "github.com/smartcontractkit/chainlink-ccip/plugintypes" @@ -360,7 +361,7 @@ func setupNodesDoNotReportGasPrices(ctx context.Context, t *testing.T, lggr logg type nodeSetup struct { node *Plugin - ccipReader *mocks.CCIPReader + ccipReader *reader_mock.MockCCIP priceReader *mocks.TokenPricesReader reportCodec *mocks.CommitPluginJSONReportCodec msgHasher *mocks.MessageHasher @@ -368,14 +369,14 @@ type nodeSetup struct { func newNode( _ context.Context, - _ *testing.T, + t *testing.T, lggr logger.Logger, id int, cfg pluginconfig.CommitPluginConfig, homeChain reader.HomeChain, oracleIDToP2pID map[commontypes.OracleID]libocrtypes.PeerID, ) nodeSetup { - ccipReader := mocks.NewCCIPReader() + ccipReader := reader_mock.NewMockCCIP(t) priceReader := mocks.NewTokenPricesReader() reportCodec := mocks.NewCommitPluginJSONReportCodec() msgHasher := mocks.NewMessageHasher() @@ -447,7 +448,7 @@ func setupHomeChainPoller(lggr logger.Logger, chainConfigInfos []reader.ChainCon // the gas prices are returned in the same order as the chains func mockGasPrices( ctx context.Context, - ccipReader *mocks.CCIPReader, + ccipReader *reader_mock.MockCCIP, chains []cciptypes.ChainSelector, gasPrices []int64) { gasPricesBigInt := make([]cciptypes.BigInt, len(gasPrices)) @@ -461,7 +462,7 @@ func mockGasPrices( func mockMsgsBetweenSeqNums( ctx context.Context, - ccipReader *mocks.CCIPReader, + ccipReader *reader_mock.MockCCIP, chain cciptypes.ChainSelector, seqNum cciptypes.SeqNum, msgs []cciptypes.Message) { diff --git a/commit/plugin_functions_test.go b/commit/plugin_functions_test.go index 576eb3da7..90c96af7c 100644 --- a/commit/plugin_functions_test.go +++ b/commit/plugin_functions_test.go @@ -9,19 +9,20 @@ import ( "time" mapset "github.com/deckarep/golang-set/v2" + libocrtypes "github.com/smartcontractkit/libocr/ragep2p/types" - "github.com/smartcontractkit/libocr/offchainreporting2plus/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" + "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + "github.com/smartcontractkit/chainlink-ccip/internal/libs/slicelib" "github.com/smartcontractkit/chainlink-ccip/internal/mocks" + reader_mock "github.com/smartcontractkit/chainlink-ccip/mocks/internal_/reader" "github.com/smartcontractkit/chainlink-ccip/plugintypes" - - cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" ) func Test_observeMaxSeqNumsPerChain(t *testing.T) { @@ -71,7 +72,7 @@ func Test_observeMaxSeqNumsPerChain(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { ctx := context.Background() - mockReader := mocks.NewCCIPReader() + mockReader := reader_mock.NewMockCCIP(t) knownSourceChains := slicelib.Filter( tc.readChains, func(ch cciptypes.ChainSelector) bool { return ch != tc.destChain }, @@ -86,13 +87,16 @@ func Test_observeMaxSeqNumsPerChain(t *testing.T) { onChainSeqNums = append(onChainSeqNums, v) } } - mockReader.On("NextSeqNum", ctx, knownSourceChains).Return(onChainSeqNums, nil) + readableChains := mapset.NewSet(tc.readChains...) + if readableChains.Contains(tc.destChain) { + mockReader.On("NextSeqNum", ctx, knownSourceChains).Return(onChainSeqNums, nil) + } seqNums, err := observeLatestCommittedSeqNums( ctx, lggr, mockReader, - mapset.NewSet(tc.readChains...), + readableChains, tc.destChain, knownSourceChains, ) @@ -212,7 +216,7 @@ func Test_observeNewMsgs(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { ctx := context.Background() - mockReader := mocks.NewCCIPReader() + mockReader := reader_mock.NewMockCCIP(t) msgHasher := mocks.NewMessageHasher() for i := range tc.expMsgs { // make sure the hashes are populated h, err := msgHasher.Hash(ctx, tc.expMsgs[i]) @@ -270,7 +274,7 @@ func Benchmark_observeNewMsgs(b *testing.B) { for i := 0; i < b.N; i++ { ctx := context.Background() lggr, _ := logger.New() - ccipReader := mocks.NewCCIPReader() + ccipReader := reader_mock.NewMockCCIP(b) msgHasher := mocks.NewMessageHasher() expNewMsgs := make([]cciptypes.Message, 0, newMsgsPerChain*numChains) @@ -349,7 +353,7 @@ func Test_observeGasPrices(t *testing.T) { ctx := context.Background() t.Run("happy path", func(t *testing.T) { - mockReader := mocks.NewCCIPReader() + mockReader := reader_mock.NewMockCCIP(t) chains := []cciptypes.ChainSelector{1, 2, 3} mockGasPrices := []cciptypes.BigInt{ cciptypes.NewBigIntFromInt64(10), @@ -367,7 +371,7 @@ func Test_observeGasPrices(t *testing.T) { }) t.Run("gas reader internal issue", func(t *testing.T) { - mockReader := mocks.NewCCIPReader() + mockReader := reader_mock.NewMockCCIP(t) chains := []cciptypes.ChainSelector{1, 2, 3} mockGasPrices := []cciptypes.BigInt{ cciptypes.NewBigIntFromInt64(10), @@ -1535,7 +1539,7 @@ func Test_validateMerkleRootsState(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - reader := mocks.NewCCIPReader() + reader := reader_mock.NewMockCCIP(t) rep := cciptypes.CommitPluginReport{} chains := make([]cciptypes.ChainSelector, 0, len(tc.reportSeqNums)) for _, snc := range tc.reportSeqNums { diff --git a/commitrmnocb/observation_test.go b/commitrmnocb/observation_test.go index 50ec5b5c5..862dff1aa 100644 --- a/commitrmnocb/observation_test.go +++ b/commitrmnocb/observation_test.go @@ -7,16 +7,18 @@ import ( "testing" mapset "github.com/deckarep/golang-set/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/smartcontractkit/libocr/commontypes" "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" "github.com/smartcontractkit/libocr/offchainreporting2plus/types" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" "github.com/smartcontractkit/chainlink-common/pkg/logger" cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" "github.com/smartcontractkit/chainlink-ccip/internal/mocks" + reader_mock "github.com/smartcontractkit/chainlink-ccip/mocks/internal_/reader" "github.com/smartcontractkit/chainlink-ccip/plugintypes" ) @@ -223,10 +225,12 @@ func Test_ObserveOffRampNextSeqNums(t *testing.T) { t.Run(tc.name, func(t *testing.T) { ctx := context.Background() var nodeID commontypes.OracleID = 1 - reader := mocks.NewCCIPReader() - reader.On( - "NextSeqNum", ctx, tc.knownSourceChains, - ).Return(tc.nextSeqNums, tc.nextSeqNumsError) + reader := reader_mock.NewMockCCIP(t) + if tc.supportsDestChain && tc.supportsDestChainError == nil && tc.knownSourceChainsError == nil { + reader.On( + "NextSeqNum", ctx, tc.knownSourceChains, + ).Return(tc.nextSeqNums, tc.nextSeqNumsError) + } chainSupport := mocks.NewChainSupport() chainSupport.On( @@ -426,8 +430,13 @@ func Test_ObserveMerkleRoots(t *testing.T) { t.Run(tc.name, func(t *testing.T) { ctx := context.Background() var nodeID commontypes.OracleID = 1 - reader := mocks.NewCCIPReader() + reader := reader_mock.NewMockCCIP(t) for _, r := range tc.ranges { + // Skip unexpected calls. + if tc.supportedChainsFails || !tc.supportedChains.Contains(r.ChainSel) { + continue + } + var err error if e, exists := tc.msgsBetweenSeqNumsErrors[r.ChainSel]; exists { err = e diff --git a/execute/exectypes/observation.go b/execute/exectypes/observation.go index 92dc9baa4..33010b03a 100644 --- a/execute/exectypes/observation.go +++ b/execute/exectypes/observation.go @@ -6,9 +6,18 @@ import ( cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" ) +// CommitObservations contain the commit plugin report data organized by the source chain selector. type CommitObservations map[cciptypes.ChainSelector][]CommitData + +// MessageObservations contain messages for commit plugin reports organized by source chain selector +// and sequence number. type MessageObservations map[cciptypes.ChainSelector]map[cciptypes.SeqNum]cciptypes.Message +// NonceObservations contain the latest nonce for senders in the previously observed messages. +// Nonces are organized by source chain selector and the string encoded sender address. The address +// must be encoding according to the destination chain requirements with typeconv.AddressBytesToString. +type NonceObservations map[cciptypes.ChainSelector]map[string]uint64 + // Observation is the observation of the ExecutePlugin. // TODO: revisit observation types. The maps used here are more space efficient and easier to work // with but require more transformations compared to the on-chain representations. @@ -16,27 +25,38 @@ type Observation struct { // CommitReports are determined during the first phase of execute. // It contains the commit reports we would like to execute in the following round. CommitReports CommitObservations `json:"commitReports"` + // Messages are determined during the second phase of execute. // Ideally, it contains all the messages identified by the previous outcome's // NextCommits. With the previous outcome, and these messsages, we can build the // execute report. Messages MessageObservations `json:"messages"` - // TODO: some of the nodes configuration may need to be included here. + + // Nonces are determined during the third phase of execute. + // It contains the nonces of senders who are being considered for the final report. + Nonces NonceObservations `json:"nonces"` } +// NewObservation constructs a Observation object. func NewObservation( - commitReports CommitObservations, messages MessageObservations) Observation { + commitReports CommitObservations, messages MessageObservations, nonces NonceObservations) Observation { return Observation{ CommitReports: commitReports, Messages: messages, + Nonces: nonces, } } +// Encode the Observation into a byte slice. func (obs Observation) Encode() ([]byte, error) { return json.Marshal(obs) } +// DecodeObservation from a byte slice into an Observation. func DecodeObservation(b []byte) (Observation, error) { + if len(b) == 0 { + return Observation{}, nil + } obs := Observation{} err := json.Unmarshal(b, &obs) return obs, err diff --git a/execute/exectypes/outcome.go b/execute/exectypes/outcome.go index 20dda0053..92712aea9 100644 --- a/execute/exectypes/outcome.go +++ b/execute/exectypes/outcome.go @@ -30,8 +30,7 @@ func (p PluginState) Next() PluginState { return GetMessages case GetMessages: - // TODO: go to Filter after GetMessages - return GetCommitReports + return Filter case Unknown: fallthrough diff --git a/execute/exectypes/outcome_test.go b/execute/exectypes/outcome_test.go index 35dd376b7..d8f6ace46 100644 --- a/execute/exectypes/outcome_test.go +++ b/execute/exectypes/outcome_test.go @@ -24,8 +24,13 @@ func TestPluginState_Next(t *testing.T) { want: GetMessages, }, { - name: "Phase 2 to 1", + name: "Phase 2 to 3", p: GetMessages, + want: Filter, + }, + { + name: "Phase 3 to 1", + p: Filter, want: GetCommitReports, }, { diff --git a/execute/plugin.go b/execute/plugin.go index 7a75fcf35..adb792284 100644 --- a/execute/plugin.go +++ b/execute/plugin.go @@ -7,6 +7,7 @@ import ( "time" mapset "github.com/deckarep/golang-set/v2" + "golang.org/x/exp/maps" "github.com/smartcontractkit/libocr/commontypes" "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" @@ -19,6 +20,7 @@ import ( "github.com/smartcontractkit/chainlink-ccip/execute/exectypes" "github.com/smartcontractkit/chainlink-ccip/execute/internal/gas" "github.com/smartcontractkit/chainlink-ccip/execute/report" + typeconv "github.com/smartcontractkit/chainlink-ccip/internal/libs/typeconv" "github.com/smartcontractkit/chainlink-ccip/internal/plugincommon" "github.com/smartcontractkit/chainlink-ccip/internal/reader" "github.com/smartcontractkit/chainlink-ccip/pluginconfig" @@ -168,6 +170,7 @@ func (p *Plugin) Observation( } state := previousOutcome.State.Next() + p.lggr.Debugw("Execute plugin performing observation", "state", state) switch state { case exectypes.GetCommitReports: fetchFrom := time.Now().Add(-p.cfg.OffchainConfig.MessageVisibilityInterval.Duration()).UTC() @@ -185,7 +188,7 @@ func (p *Plugin) Observation( } // TODO: truncate grouped to a maximum observation size? - return exectypes.NewObservation(groupedCommits, nil).Encode() + return exectypes.NewObservation(groupedCommits, nil, nil).Encode() } // No observation for non-dest readers. @@ -203,7 +206,7 @@ func (p *Plugin) Observation( commitReportCache[report.SourceChain] = append(commitReportCache[report.SourceChain], report) } - for selector, reports := range commitReportCache { + for srcChain, reports := range commitReportCache { if len(reports) == 0 { continue } @@ -215,15 +218,16 @@ func (p *Plugin) Observation( // Read messages for each range. for _, seqRange := range ranges { - msgs, err := p.ccipReader.MsgsBetweenSeqNums(ctx, selector, seqRange) + // TODO: check if srcChain is supported. + msgs, err := p.ccipReader.MsgsBetweenSeqNums(ctx, srcChain, seqRange) if err != nil { return nil, err } for _, msg := range msgs { - if _, ok := messages[selector]; !ok { - messages[selector] = make(map[cciptypes.SeqNum]cciptypes.Message) + if _, ok := messages[srcChain]; !ok { + messages[srcChain] = make(map[cciptypes.SeqNum]cciptypes.Message) } - messages[selector][msg.Header.SequenceNumber] = msg + messages[srcChain][msg.Header.SequenceNumber] = msg } } } @@ -240,11 +244,36 @@ func (p *Plugin) Observation( } // TODO: Fire off messages for an attestation check service. - return exectypes.NewObservation(groupedCommits, messages).Encode() + return exectypes.NewObservation(groupedCommits, messages, nil).Encode() case exectypes.Filter: - // TODO: pass the previous two through, add in the nonces. - return types.Observation{}, fmt.Errorf("unknown state") + // TODO: add in nonces, other data comes from previous outcome. + nonceRequestArgs := make(map[cciptypes.ChainSelector]map[string]struct{}) + + // Collect unique senders. + for _, commitReport := range previousOutcome.Report.ChainReports { + if _, ok := nonceRequestArgs[commitReport.SourceChainSelector]; !ok { + nonceRequestArgs[commitReport.SourceChainSelector] = make(map[string]struct{}) + } + for _, msg := range commitReport.Messages { + sender := typeconv.AddressBytesToString(msg.Sender[:], uint64(p.cfg.DestChain)) + nonceRequestArgs[commitReport.SourceChainSelector][sender] = struct{}{} + } + } + + // Read args from chain. + nonceObservations := make(exectypes.NonceObservations) + for srcChain, addrSet := range nonceRequestArgs { + // TODO: check if srcSelector is supported. + addrs := maps.Keys(addrSet) + nonces, err := p.ccipReader.Nonces(ctx, srcChain, p.cfg.DestChain, addrs) + if err != nil { + return types.Observation{}, fmt.Errorf("unable to get nonces: %w", err) + } + nonceObservations[srcChain] = nonces + } + + return exectypes.NewObservation(nil, nil, nonceObservations).Encode() default: return types.Observation{}, fmt.Errorf("unknown state") } @@ -286,28 +315,13 @@ func (p *Plugin) ObservationQuorum(outctx ocr3types.OutcomeContext, query types. // If there is not enough space in the final report, it may be partially executed by searching for a subset of messages // which can fit in the final report. func selectReport( - ctx context.Context, lggr logger.Logger, - hasher cciptypes.MessageHasher, - encoder cciptypes.ExecutePluginCodec, - tokenDataReader exectypes.TokenDataReader, - estimateProvider gas.EstimateProvider, commitReports []exectypes.CommitData, - maxReportSizeBytes int, - maxGas uint64, + builder report.ExecReportBuilder, ) ([]cciptypes.ExecutePluginReportSingleChain, []exectypes.CommitData, error) { // TODO: It may be desirable for this entire function to be an interface so that // different selection algorithms can be used. - builder := report.NewBuilder( - ctx, - lggr, - hasher, - tokenDataReader, - encoder, - estimateProvider, - uint64(maxReportSizeBytes), - maxGas) var stillPendingReports []exectypes.CommitData for i, report := range commitReports { // Reports at the end may not have messages yet. @@ -363,6 +377,7 @@ func (p *Plugin) Outcome( p.lggr.Debugw( fmt.Sprintf("[oracle %d] exec outcome: decoded observations", p.reportingCfg.OracleID), + "oracle", p.reportingCfg.OracleID, "decodedObservations", decodedObservations) fChain, err := p.homeChain.GetFChain() @@ -377,6 +392,7 @@ func (p *Plugin) Outcome( p.lggr.Debugw( fmt.Sprintf("[oracle %d] exec outcome: merged commit observations", p.reportingCfg.OracleID), + "oracle", p.reportingCfg.OracleID, "mergedCommitObservations", mergedCommitObservations) mergedMessageObservations, err := mergeMessageObservations(decodedObservations, fChain) @@ -386,35 +402,31 @@ func (p *Plugin) Outcome( p.lggr.Debugw( fmt.Sprintf("[oracle %d] exec outcome: merged message observations", p.reportingCfg.OracleID), + "oracle", p.reportingCfg.OracleID, "mergedMessageObservations", mergedMessageObservations) observation := exectypes.NewObservation( mergedCommitObservations, - mergedMessageObservations) - - ////////////////////////// - // common preprocessing // - ////////////////////////// - - // flatten commit reports and sort by timestamp. - var commitReports []exectypes.CommitData - for _, report := range observation.CommitReports { - commitReports = append(commitReports, report...) - } - sort.Slice(commitReports, func(i, j int) bool { - return commitReports[i].Timestamp.Before(commitReports[j].Timestamp) - }) - - p.lggr.Debugw( - fmt.Sprintf("[oracle %d] exec outcome: commit reports", p.reportingCfg.OracleID), - "commitReports", commitReports) + mergedMessageObservations, + nil) + var outcome exectypes.Outcome state := previousOutcome.State.Next() switch state { case exectypes.GetCommitReports: - outcome := exectypes.NewOutcome(state, commitReports, cciptypes.ExecutePluginReport{}) - return outcome.Encode() + // flatten commit reports and sort by timestamp. + var commitReports []exectypes.CommitData + for _, report := range observation.CommitReports { + commitReports = append(commitReports, report...) + } + sort.Slice(commitReports, func(i, j int) bool { + return commitReports[i].Timestamp.Before(commitReports[j].Timestamp) + }) + + outcome = exectypes.NewOutcome(state, commitReports, cciptypes.ExecutePluginReport{}) case exectypes.GetMessages: + commitReports := previousOutcome.PendingCommitReports + // add messages to their commitReports. for i, report := range commitReports { report.Messages = nil @@ -426,17 +438,26 @@ func (p *Plugin) Outcome( commitReports[i].Messages = report.Messages } + outcome = exectypes.NewOutcome(state, commitReports, cciptypes.ExecutePluginReport{}) + case exectypes.Filter: + commitReports := previousOutcome.PendingCommitReports + // TODO: this function should be pure, a context should not be needed. - outcomeReports, commitReports, err := - selectReport( - context.Background(), - p.lggr, p.msgHasher, - p.reportCodec, - p.tokenDataReader, - p.estimateProvider, - commitReports, - maxReportSizeBytes, - p.cfg.OffchainConfig.BatchGasLimit) + builder := report.NewBuilder( + context.Background(), + p.lggr, + p.msgHasher, + p.tokenDataReader, + p.reportCodec, + p.estimateProvider, + observation.Nonces, + p.cfg.DestChain, + uint64(maxReportSizeBytes), + p.cfg.OffchainConfig.BatchGasLimit) + outcomeReports, commitReports, err := selectReport( + p.lggr, + commitReports, + builder) if err != nil { return ocr3types.Outcome{}, fmt.Errorf("unable to extract proofs: %w", err) } @@ -445,21 +466,22 @@ func (p *Plugin) Outcome( ChainReports: outcomeReports, } - outcome := exectypes.NewOutcome(state, commitReports, execReport) - if outcome.IsEmpty() { - return nil, nil - } - - p.lggr.Infow( - fmt.Sprintf("[oracle %d] exec outcome: generated outcome", p.reportingCfg.OracleID), - "outcome", outcome) - - return outcome.Encode() - case exectypes.Filter: - panic("not implemented") + outcome = exectypes.NewOutcome(state, commitReports, execReport) default: panic("unknown state") } + + if outcome.IsEmpty() { + return nil, nil + } + + p.lggr.Infow( + fmt.Sprintf("[oracle %d] exec outcome: generated outcome", p.reportingCfg.OracleID), + "oracle", p.reportingCfg.OracleID, + "execPluginState", state, + "outcome", outcome) + + return outcome.Encode() } func (p *Plugin) Reports(seqNr uint64, outcome ocr3types.Outcome) ([]ocr3types.ReportWithInfo[[]byte], error) { diff --git a/execute/plugin_e2e_test.go b/execute/plugin_e2e_test.go index b58612bb5..138973bce 100644 --- a/execute/plugin_e2e_test.go +++ b/execute/plugin_e2e_test.go @@ -50,7 +50,8 @@ func TestPlugin(t *testing.T) { runner := testhelpers.NewOCR3Runner(nodes, nodeIDs, nil) - // In the first round there is a pending commit report only. + // Round 1. + // One pending commit report only. // Two of the messages are executed which should be indicated in the Outcome. res, err := runner.RunRound(ctx) require.NoError(t, err) @@ -60,18 +61,26 @@ func TestPlugin(t *testing.T) { require.Len(t, outcome.PendingCommitReports, 1) require.ElementsMatch(t, outcome.PendingCommitReports[0].ExecutedMessages, []cciptypes.SeqNum{100, 101}) - // In the second round there is an exec report and the pending commit report is removed. - // The exec report should indicate the following messages are executed: 102, 103, 104, 105. + // Round 2. + // Messages now attached to the pending commit. + res, err = runner.RunRound(ctx) + require.NoError(t, err) + outcome, err = exectypes.DecodeOutcome(res.Outcome) + require.NoError(t, err) + require.Len(t, outcome.Report.ChainReports, 0) + require.Len(t, outcome.PendingCommitReports, 1) + + // Round 3. + // An execute report with the following messages executed: 102, 103, 104, 105. res, err = runner.RunRound(ctx) require.NoError(t, err) outcome, err = exectypes.DecodeOutcome(res.Outcome) require.NoError(t, err) - require.Len(t, outcome.Report.ChainReports, 1) - require.Len(t, outcome.PendingCommitReports, 0) sequenceNumbers := slicelib.Map(outcome.Report.ChainReports[0].Messages, func(m cciptypes.Message) cciptypes.SeqNum { return m.Header.SequenceNumber }) require.ElementsMatch(t, sequenceNumbers, []cciptypes.SeqNum{102, 103, 104, 105}) + } type nodeSetup struct { diff --git a/execute/plugin_test.go b/execute/plugin_test.go index 3119940ce..c92d2acbd 100644 --- a/execute/plugin_test.go +++ b/execute/plugin_test.go @@ -23,7 +23,6 @@ import ( "github.com/smartcontractkit/chainlink-ccip/execute/exectypes" "github.com/smartcontractkit/chainlink-ccip/internal/libs/slicelib" - "github.com/smartcontractkit/chainlink-ccip/internal/mocks" "github.com/smartcontractkit/chainlink-ccip/internal/plugincommon" "github.com/smartcontractkit/chainlink-ccip/internal/reader" codec_mocks "github.com/smartcontractkit/chainlink-ccip/mocks/execute/internal_/gen" @@ -137,7 +136,7 @@ func Test_getPendingExecutedReports(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - mockReader := mocks.NewCCIPReader() + mockReader := reader_mock.NewMockCCIP(t) mockReader.On( "CommitReportsGTETimestamp", mock.Anything, mock.Anything, mock.Anything, mock.Anything, ).Return(tt.reports, nil) @@ -166,7 +165,7 @@ func Test_getPendingExecutedReports(t *testing.T) { } func TestPlugin_Close(t *testing.T) { - mockReader := mocks.NewCCIPReader() + mockReader := reader_mock.NewMockCCIP(t) mockReader.On("Close", mock.Anything).Return(nil) lggr := logger.Test(t) @@ -230,7 +229,7 @@ func TestPlugin_ValidateObservation_IneligibleObserver(t *testing.T) { }, }, }, - }) + }, nil) encoded, err := observation.Encode() require.NoError(t, err) err = p.ValidateObservation(ocr3types.OutcomeContext{}, types.Query{}, types.AttributedObservation{ @@ -263,7 +262,7 @@ func TestPlugin_ValidateObservation_ValidateObservedSeqNum_Error(t *testing.T) { {MerkleRoot: root}, }, } - observation := exectypes.NewObservation(commitReports, nil) + observation := exectypes.NewObservation(commitReports, nil, nil) encoded, err := observation.Encode() require.NoError(t, err) err = p.ValidateObservation(ocr3types.OutcomeContext{}, types.Query{}, types.AttributedObservation{ @@ -350,7 +349,7 @@ func TestPlugin_Outcome_CommitReportsMergeError(t *testing.T) { commitReports := map[cciptypes.ChainSelector][]exectypes.CommitData{ 1: {}, } - observation, err := exectypes.NewObservation(commitReports, nil).Encode() + observation, err := exectypes.NewObservation(commitReports, nil, nil).Encode() require.NoError(t, err) _, err = p.Outcome(ocr3types.OutcomeContext{}, nil, []types.AttributedObservation{ { @@ -383,7 +382,7 @@ func TestPlugin_Outcome_MessagesMergeError(t *testing.T) { }, }, } - observation, err := exectypes.NewObservation(nil, messages).Encode() + observation, err := exectypes.NewObservation(nil, messages, nil).Encode() require.NoError(t, err) _, err = p.Outcome(ocr3types.OutcomeContext{}, nil, []types.AttributedObservation{ { diff --git a/execute/report/builder.go b/execute/report/builder.go index 912d03166..5d65986b5 100644 --- a/execute/report/builder.go +++ b/execute/report/builder.go @@ -26,6 +26,8 @@ func NewBuilder( tokenDataReader exectypes.TokenDataReader, encoder cciptypes.ExecutePluginCodec, estimateProvider gas.EstimateProvider, + nonces map[cciptypes.ChainSelector]map[string]uint64, + destChainSelector cciptypes.ChainSelector, maxReportSizeBytes uint64, maxGas uint64, ) ExecReportBuilder { @@ -37,7 +39,10 @@ func NewBuilder( encoder: encoder, hasher: hasher, estimateProvider: estimateProvider, + sendersNonce: nonces, + expectedNonce: make(map[cciptypes.ChainSelector]map[string]uint64), + destChainSelector: destChainSelector, maxReportSizeBytes: maxReportSizeBytes, maxGas: maxGas, } @@ -65,16 +70,23 @@ type execReportBuilder struct { encoder cciptypes.ExecutePluginCodec hasher cciptypes.MessageHasher estimateProvider gas.EstimateProvider + sendersNonce map[cciptypes.ChainSelector]map[string]uint64 // Config + destChainSelector cciptypes.ChainSelector maxReportSizeBytes uint64 maxGas uint64 // State accumulated validationMetadata + // expectedNonce is used to track nonces for multiple messages from the same sender. + expectedNonce map[cciptypes.ChainSelector]map[string]uint64 // Result execReports []cciptypes.ExecutePluginReportSingleChain + + // TODO: remove temporary feature flagging + nonceCheckingEnabled bool // defaults to disabled for backwards compatibility. } func (b *execReportBuilder) Add( diff --git a/execute/report/report.go b/execute/report/report.go index 36e10bb47..146ffb316 100644 --- a/execute/report/report.go +++ b/execute/report/report.go @@ -13,6 +13,7 @@ import ( "github.com/smartcontractkit/chainlink-ccip/execute/exectypes" "github.com/smartcontractkit/chainlink-ccip/internal/libs/slicelib" + typeconv "github.com/smartcontractkit/chainlink-ccip/internal/libs/typeconv" ) // buildSingleChainReportHelper converts the on-chain event data stored in cciptypes.ExecutePluginCommitData into the @@ -38,6 +39,12 @@ func buildSingleChainReportHelper( } } + if len(readyMessages) == 0 { + lggr.Infow("no messages ready for execution", + "sourceChain", report.SourceChain) + return cciptypes.ExecutePluginReportSingleChain{}, nil + } + numMsg := len(report.Messages) if len(report.TokenData) != numMsg { return cciptypes.ExecutePluginReportSingleChain{}, @@ -124,12 +131,13 @@ const ( TokenDataNotReady messageStatus = "token_data_not_ready" //nolint:gosec // this is not a password TokenDataFetchError messageStatus = "token_data_fetch_error" InsufficientRemainingBatchGas messageStatus = "insufficient_remaining_batch_gas" + MissingNoncesForChain messageStatus = "missing_nonces_for_chain" + MissingNonce messageStatus = "missing_nonce" + InvalidNonce messageStatus = "invalid_nonce" /* SenderAlreadySkipped messageStatus = "sender_already_skipped" MessageMaxGasCalcError messageStatus = "message_max_gas_calc_error" InsufficientRemainingBatchDataLength messageStatus = "insufficient_remaining_batch_data_length" - MissingNonce messageStatus = "missing_nonce" - InvalidNonce messageStatus = "invalid_nonce" AggregateTokenValueComputeError messageStatus = "aggregate_token_value_compute_error" AggregateTokenLimitExceeded messageStatus = "aggregate_token_limit_exceeded" TokenNotInDestTokenPrices messageStatus = "token_not_in_dest_token_prices" @@ -150,6 +158,8 @@ func (b *execReportBuilder) checkMessage( ctx context.Context, idx int, execReport exectypes.CommitData, // TODO: get rid of the nolint when the error is used ) (exectypes.CommitData, messageStatus, error) { // nolint this will use the error eventually + result := execReport + if idx >= len(execReport.Messages) { b.lggr.Errorw("message index out of range", "index", idx, "numMessages", len(execReport.Messages)) return execReport, Unknown, fmt.Errorf("message index out of range") @@ -157,17 +167,18 @@ func (b *execReportBuilder) checkMessage( msg := execReport.Messages[idx] - // Check if the message has already been executed. + // 1. Check if the message has already been executed. if slices.Contains(execReport.ExecutedMessages, msg.Header.SequenceNumber) { b.lggr.Infow( "message already executed", "messageID", msg.Header.MessageID, "sourceChain", execReport.SourceChain, - "seqNum", msg.Header.SequenceNumber) + "seqNum", msg.Header.SequenceNumber, + "messageState", AlreadyExecuted) return execReport, AlreadyExecuted, nil } - // Check if token data is ready. + // 2. Check if token data is ready. if b.tokenDataReader == nil { return execReport, Unknown, fmt.Errorf("token data reader must be initialized") } @@ -179,20 +190,22 @@ func (b *execReportBuilder) checkMessage( "messageID", msg.Header.MessageID, "sourceChain", execReport.SourceChain, "seqNum", msg.Header.SequenceNumber, - "error", err) + "error", err, + "messageState", TokenDataNotReady) return execReport, TokenDataNotReady, nil } - b.lggr.Infow( + b.lggr.Errorw( "unable to read token data - unknown error", "messageID", msg.Header.MessageID, "sourceChain", execReport.SourceChain, "seqNum", msg.Header.SequenceNumber, - "error", err) + "error", err, + "messageState", TokenDataFetchError) return execReport, TokenDataFetchError, nil } - execReport.TokenData = padSlice(execReport.TokenData, idx+1, nil) - execReport.TokenData[idx] = tokenData + result.TokenData = padSlice(execReport.TokenData, idx+1, nil) + result.TokenData[idx] = tokenData b.lggr.Infow( "read token data", "messageID", msg.Header.MessageID, @@ -200,10 +213,59 @@ func (b *execReportBuilder) checkMessage( "seqNum", msg.Header.SequenceNumber, "data", tokenData) - // TODO: Check for valid nonce + // 3. Check if the message has a valid nonce. + if msg.Header.Nonce != 0 && b.nonceCheckingEnabled { + // Sequenced messages have non-zero nonces. + + if _, ok := b.sendersNonce[execReport.SourceChain]; !ok { + b.lggr.Errorw("Skipping message - nonces not available for chain", + "messageID", msg.Header.MessageID, + "sourceChain", execReport.SourceChain, + "seqNum", msg.Header.SequenceNumber, + "messageState", MissingNoncesForChain) + return execReport, MissingNoncesForChain, nil + } + + chainNonces := b.sendersNonce[execReport.SourceChain] + sender := typeconv.AddressBytesToString(msg.Sender[:], uint64(b.destChainSelector)) + if _, ok := chainNonces[sender]; !ok { + b.lggr.Errorw("Skipping message - missing nonce", + "messageID", msg.Header.MessageID, + "sourceChain", execReport.SourceChain, + "seqNum", msg.Header.SequenceNumber, + "messageState", MissingNonce) + return execReport, MissingNonce, nil + } + + if b.expectedNonce == nil { + // initialize expected nonce if needed. + b.expectedNonce = make(map[cciptypes.ChainSelector]map[string]uint64) + } + if _, ok := b.expectedNonce[execReport.SourceChain]; !ok { + // initialize expected nonce if needed. + b.expectedNonce[execReport.SourceChain] = make(map[string]uint64) + } + if _, ok := b.expectedNonce[execReport.SourceChain][sender]; !ok { + b.expectedNonce[execReport.SourceChain][sender] = chainNonces[sender] + 1 + } + + // Check expected nonce is valid for sequenced messages. + if msg.Header.Nonce != b.expectedNonce[execReport.SourceChain][sender] { + b.lggr.Warnw("Skipping message - invalid nonce", + "messageID", msg.Header.MessageID, + "sourceChain", execReport.SourceChain, + "seqNum", msg.Header.SequenceNumber, + "have", msg.Header.Nonce, + "want", b.expectedNonce[execReport.SourceChain][sender], + "messageState", InvalidNonce) + return execReport, InvalidNonce, nil + } + b.expectedNonce[execReport.SourceChain][sender] = b.expectedNonce[execReport.SourceChain][sender] + 1 + } + // TODO: Check for fee boost - return execReport, ReadyToExecute, nil + return result, ReadyToExecute, nil } func (b *execReportBuilder) verifyReport( @@ -284,6 +346,10 @@ func (b *execReportBuilder) buildSingleChainReport( } } + if len(readyMessages) == 0 { + return cciptypes.ExecutePluginReportSingleChain{}, report, ErrEmptyReport + } + // Attempt to include all messages in the report. finalReport, err := buildSingleChainReportHelper(b.ctx, b.lggr, b.hasher, report, readyMessages) diff --git a/execute/report/report_test.go b/execute/report/report_test.go index 2c1d59fee..fe4325695 100644 --- a/execute/report/report_test.go +++ b/execute/report/report_test.go @@ -2,6 +2,7 @@ package report import ( "context" + crand "crypto/rand" "fmt" "math/rand" "reflect" @@ -25,6 +26,12 @@ import ( "github.com/smartcontractkit/chainlink-ccip/internal/mocks" ) +func randomAddress() string { + b := make([]byte, 20) + _, _ = crand.Read(b) // Assignment for errcheck. Only used in tests so we can ignore. + return cciptypes.Bytes(b).String() +} + // mustMakeBytes parses a given string into a byte array, any error causes a panic. Pass in an empty string for a // random byte array. // nolint:unparam // surly this will be useful at some point... @@ -169,10 +176,37 @@ func makeMessage(src cciptypes.ChainSelector, num cciptypes.SeqNum, nonce uint64 SourceChainSelector: src, SequenceNumber: num, MsgHash: cciptypes.Bytes32{}, + Nonce: nonce, }, } } +// makeTestCommitReportWithSenders is the same as makeTestCommitReport but overrides the senders. +func makeTestCommitReportWithSenders( + hasher cciptypes.MessageHasher, + numMessages, + srcChain, + firstSeqNum, + block int, + timestamp int64, + senders []cciptypes.Bytes, + rootOverride cciptypes.Bytes32, + executed []cciptypes.SeqNum, +) exectypes.CommitData { + if len(senders) == 0 || len(senders) != numMessages { + panic("wrong number of senders provided") + } + + data := makeTestCommitReport(hasher, numMessages, srcChain, firstSeqNum, + block, timestamp, senders[0], rootOverride, executed) + + for i := range data.Messages { + data.Messages[i].Sender = senders[i] + } + + return data +} + // makeTestCommitReport creates a basic commit report with messages given different parameters. This function // will panic if the input parameters are inconsistent. func makeTestCommitReport( @@ -182,6 +216,7 @@ func makeTestCommitReport( firstSeqNum, block int, timestamp int64, + sender cciptypes.Bytes, rootOverride cciptypes.Bytes32, executed []cciptypes.SeqNum, ) exectypes.CommitData { @@ -195,10 +230,12 @@ func makeTestCommitReport( } var messages []cciptypes.Message for i := 0; i < numMessages; i++ { - messages = append(messages, makeMessage( + msg := makeMessage( cciptypes.ChainSelector(srcChain), cciptypes.SeqNum(i+firstSeqNum), - uint64(i))) + uint64(i)+1) + msg.Sender = sender + messages = append(messages, msg) } commitReport := exectypes.CommitData{ @@ -263,10 +300,14 @@ func Test_buildSingleChainReport_Errors(t *testing.T) { }{ { name: "token data mismatch", - wantErr: "token data length mismatch: got 2, expected 0", + wantErr: "token data length mismatch: got 2, expected 1", args: args{ report: exectypes.CommitData{ - TokenData: make([][][]byte, 2), + TokenData: make([][][]byte, 2), + SequenceNumberRange: cciptypes.NewSeqNumRange(cciptypes.SeqNum(100), cciptypes.SeqNum(100)), + Messages: []cciptypes.Message{ + {Header: cciptypes.RampMessageHeader{}}, + }, }, }, }, @@ -384,9 +425,25 @@ func Test_Builder_Build(t *testing.T) { codec := mocks.NewExecutePluginJSONReportCodec() lggr := logger.Test(t) tokenDataReader := tdr{mode: good} + sender, err := cciptypes.NewBytesFromString(randomAddress()) + require.NoError(t, err) + defaultNonces := map[cciptypes.ChainSelector]map[string]uint64{ + 1: { + sender.String(): 0, + }, + 2: { + sender.String(): 0, + }, + } + tenSenders := make([]cciptypes.Bytes, 10) + for i := range tenSenders { + tenSenders[i], err = cciptypes.NewBytesFromString(randomAddress()) + require.NoError(t, err) + } type args struct { reports []exectypes.CommitData + nonces map[cciptypes.ChainSelector]map[string]uint64 maxReportSize uint64 maxGasLimit uint64 } @@ -410,10 +467,12 @@ func Test_Builder_Build(t *testing.T) { { name: "half report", args: args{ - maxReportSize: 2300, + maxReportSize: 2529, maxGasLimit: 10000000, + nonces: defaultNonces, reports: []exectypes.CommitData{ makeTestCommitReport(hasher, 10, 1, 100, 999, 10101010101, + sender, cciptypes.Bytes32{}, // generate a correct root. nil), }, @@ -428,8 +487,10 @@ func Test_Builder_Build(t *testing.T) { args: args{ maxReportSize: 10000, maxGasLimit: 10000000, + nonces: defaultNonces, reports: []exectypes.CommitData{ makeTestCommitReport(hasher, 10, 1, 100, 999, 10101010101, + sender, cciptypes.Bytes32{}, // generate a correct root. nil), }, @@ -443,11 +504,14 @@ func Test_Builder_Build(t *testing.T) { args: args{ maxReportSize: 15000, maxGasLimit: 10000000, + nonces: defaultNonces, reports: []exectypes.CommitData{ makeTestCommitReport(hasher, 10, 1, 100, 999, 10101010101, + sender, cciptypes.Bytes32{}, // generate a correct root. nil), makeTestCommitReport(hasher, 20, 2, 100, 999, 10101010101, + sender, cciptypes.Bytes32{}, // generate a correct root. nil), }, @@ -459,13 +523,16 @@ func Test_Builder_Build(t *testing.T) { { name: "one and half reports", args: args{ - maxReportSize: 8500, + maxReportSize: 9500, maxGasLimit: 10000000, + nonces: defaultNonces, reports: []exectypes.CommitData{ makeTestCommitReport(hasher, 10, 1, 100, 999, 10101010101, + sender, cciptypes.Bytes32{}, // generate a correct root. nil), makeTestCommitReport(hasher, 20, 2, 100, 999, 10101010101, + sender, cciptypes.Bytes32{}, // generate a correct root. nil), }, @@ -478,13 +545,16 @@ func Test_Builder_Build(t *testing.T) { { name: "exactly one report", args: args{ - maxReportSize: 4200, + maxReportSize: 4600, maxGasLimit: 10000000, + nonces: defaultNonces, reports: []exectypes.CommitData{ makeTestCommitReport(hasher, 10, 1, 100, 999, 10101010101, + sender, cciptypes.Bytes32{}, // generate a correct root. nil), makeTestCommitReport(hasher, 20, 2, 100, 999, 10101010101, + sender, cciptypes.Bytes32{}, // generate a correct root. nil), }, @@ -497,10 +567,16 @@ func Test_Builder_Build(t *testing.T) { { name: "execute remainder of partially executed report", args: args{ - maxReportSize: 2500, + maxReportSize: 2600, maxGasLimit: 10000000, + nonces: map[cciptypes.ChainSelector]map[string]uint64{ + 1: { + sender.String(): 5, + }, + }, reports: []exectypes.CommitData{ makeTestCommitReport(hasher, 10, 1, 100, 999, 10101010101, + sender, cciptypes.Bytes32{}, // generate a correct root. []cciptypes.SeqNum{100, 101, 102, 103, 104}), }, @@ -512,10 +588,16 @@ func Test_Builder_Build(t *testing.T) { { name: "partially execute remainder of partially executed report", args: args{ - maxReportSize: 2050, + maxReportSize: 2200, maxGasLimit: 10000000, + nonces: map[cciptypes.ChainSelector]map[string]uint64{ + 1: { + sender.String(): 5, + }, + }, reports: []exectypes.CommitData{ makeTestCommitReport(hasher, 10, 1, 100, 999, 10101010101, + sender, cciptypes.Bytes32{}, // generate a correct root. []cciptypes.SeqNum{100, 101, 102, 103, 104}), }, @@ -530,8 +612,18 @@ func Test_Builder_Build(t *testing.T) { args: args{ maxReportSize: 3500, maxGasLimit: 10000000, + nonces: map[cciptypes.ChainSelector]map[string]uint64{ + 1: { + tenSenders[1].String(): 1, + tenSenders[3].String(): 3, + tenSenders[5].String(): 5, + tenSenders[7].String(): 7, + tenSenders[9].String(): 9, + }, + }, reports: []exectypes.CommitData{ - makeTestCommitReport(hasher, 10, 1, 100, 999, 10101010101, + makeTestCommitReportWithSenders(hasher, 10, 1, 100, 999, 10101010101, + tenSenders, cciptypes.Bytes32{}, // generate a correct root. []cciptypes.SeqNum{100, 102, 104, 106, 108}), }, @@ -543,10 +635,20 @@ func Test_Builder_Build(t *testing.T) { { name: "partially execute remainder of partially executed sparse report", args: args{ - maxReportSize: 2050, + maxReportSize: 2250, maxGasLimit: 10000000, + nonces: map[cciptypes.ChainSelector]map[string]uint64{ + 1: { + tenSenders[1].String(): 1, + tenSenders[3].String(): 3, + tenSenders[5].String(): 5, + tenSenders[7].String(): 7, + tenSenders[9].String(): 9, + }, + }, reports: []exectypes.CommitData{ - makeTestCommitReport(hasher, 10, 1, 100, 999, 10101010101, + makeTestCommitReportWithSenders(hasher, 10, 1, 100, 999, 10101010101, + tenSenders, cciptypes.Bytes32{}, // generate a correct root. []cciptypes.SeqNum{100, 102, 104, 106, 108}), }, @@ -561,8 +663,10 @@ func Test_Builder_Build(t *testing.T) { args: args{ maxReportSize: 10000, maxGasLimit: 10000000, + nonces: defaultNonces, reports: []exectypes.CommitData{ breakCommitReport(makeTestCommitReport(hasher, 10, 1, 101, 1000, 10101010102, + sender, cciptypes.Bytes32{}, // generate a correct root. nil)), }, @@ -572,8 +676,10 @@ func Test_Builder_Build(t *testing.T) { { name: "invalid merkle root", args: args{ + nonces: defaultNonces, reports: []exectypes.CommitData{ makeTestCommitReport(hasher, 10, 1, 100, 999, 10101010101, + sender, mustMakeBytes(""), // random root nil), }, @@ -585,9 +691,11 @@ func Test_Builder_Build(t *testing.T) { args: args{ maxReportSize: 10000, maxGasLimit: 10000000, + nonces: defaultNonces, reports: []exectypes.CommitData{ setMessageData(5, 20000, makeTestCommitReport(hasher, 10, 1, 100, 999, 10101010101, + sender, cciptypes.Bytes32{}, // generate a correct root. nil)), }, @@ -602,10 +710,12 @@ func Test_Builder_Build(t *testing.T) { args: args{ maxReportSize: 10000, maxGasLimit: 10000000, + nonces: defaultNonces, reports: []exectypes.CommitData{ setMessageData(8, 20000, setMessageData(5, 20000, makeTestCommitReport(hasher, 10, 1, 100, 999, 10101010101, + sender, cciptypes.Bytes32{}, // generate a correct root. nil))), }, @@ -624,15 +734,36 @@ func Test_Builder_Build(t *testing.T) { // look for error in Add or Build foundError := false - builder := NewBuilder( - ctx, - lggr, - hasher, - tokenDataReader, - codec, - evm.EstimateProvider{}, - tt.args.maxReportSize, - tt.args.maxGasLimit) + builder := &execReportBuilder{ + ctx: ctx, + lggr: lggr, + + tokenDataReader: tokenDataReader, + encoder: codec, + hasher: hasher, + estimateProvider: evm.EstimateProvider{}, + sendersNonce: tt.args.nonces, + expectedNonce: make(map[cciptypes.ChainSelector]map[string]uint64), + + maxReportSizeBytes: tt.args.maxReportSize, + maxGas: tt.args.maxGasLimit, + nonceCheckingEnabled: true, // TODO: remove feature flag. + } + + /* + // TODO: switch back to this when removing the feature flag. + builder := NewBuilder( + ctx, + lggr, + hasher, + tokenDataReader, + codec, + evm.EstimateProvider{}, + tt.args.nonces, + tt.args.maxReportSize, + tt.args.maxGasLimit) + */ + var updatedMessages []exectypes.CommitData for _, report := range tt.args.reports { updatedMessage, err := builder.Add(report) @@ -815,13 +946,14 @@ func Test_execReportBuilder_verifyReport(t *testing.T) { } b := &execReportBuilder{ - ctx: context.Background(), - lggr: lggr, - encoder: resolvedEncoder, - estimateProvider: tt.fields.estimateProvider, - maxReportSizeBytes: tt.fields.maxReportSizeBytes, - maxGas: tt.fields.maxGas, - accumulated: tt.fields.accumulated, + nonceCheckingEnabled: true, // TODO: remove feature flag. + ctx: context.Background(), + lggr: lggr, + encoder: resolvedEncoder, + estimateProvider: tt.fields.estimateProvider, + maxReportSizeBytes: tt.fields.maxReportSizeBytes, + maxGas: tt.fields.maxGas, + accumulated: tt.fields.accumulated, } isValid, metadata, err := b.verifyReport(context.Background(), tt.args.execReport) if tt.expectedError != "" { @@ -880,6 +1012,7 @@ func Test_execReportBuilder_checkMessage(t *testing.T) { } type args struct { idx int + nonces map[cciptypes.ChainSelector]map[string]uint64 execReport exectypes.CommitData } tests := []struct { @@ -1010,6 +1143,62 @@ func Test_execReportBuilder_checkMessage(t *testing.T) { TokenData: [][][]byte{nil, nil}, }, }, + { + name: "missing chain nonce", + args: args{ + idx: 0, + execReport: exectypes.CommitData{ + SourceChain: 1, + Messages: []cciptypes.Message{ + makeMessage(1, 100, 1), + }, + }, + }, + fields: fields{ + tokenDataReader: tdr{mode: noop}, + }, + expectedStatus: MissingNoncesForChain, + }, + { + name: "missing sender nonce", + args: args{ + idx: 0, + nonces: map[cciptypes.ChainSelector]map[string]uint64{ + 1: {}, + }, + execReport: exectypes.CommitData{ + SourceChain: 1, + Messages: []cciptypes.Message{ + makeMessage(1, 100, 1), + }, + }, + }, + fields: fields{ + tokenDataReader: tdr{mode: noop}, + }, + expectedStatus: MissingNonce, + }, + { + name: "invalid sender nonce", + args: args{ + idx: 0, + nonces: map[cciptypes.ChainSelector]map[string]uint64{ + 1: { + "0x": 99, + }, + }, + execReport: exectypes.CommitData{ + SourceChain: 1, + Messages: []cciptypes.Message{ + makeMessage(1, 100, 1), + }, + }, + }, + fields: fields{ + tokenDataReader: tdr{mode: noop}, + }, + expectedStatus: InvalidNonce, + }, } for _, tt := range tests { tt := tt @@ -1026,10 +1215,12 @@ func Test_execReportBuilder_checkMessage(t *testing.T) { } b := &execReportBuilder{ - lggr: lggr, - tokenDataReader: resolvedTokenDataReader, - estimateProvider: evm.EstimateProvider{}, - accumulated: tt.fields.accumulated, + nonceCheckingEnabled: true, // TODO: remove feature flag. + lggr: lggr, + tokenDataReader: resolvedTokenDataReader, + estimateProvider: evm.EstimateProvider{}, + accumulated: tt.fields.accumulated, + sendersNonce: tt.args.nonces, } data, status, err := b.checkMessage(context.Background(), tt.args.idx, tt.args.execReport) if tt.expectedError != "" { diff --git a/go.mod b/go.mod index d4c9afa18..64dd6b6fd 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.22.5 require ( github.com/deckarep/golang-set/v2 v2.6.0 - github.com/smartcontractkit/chainlink-common v0.1.7-0.20240712162033-89bd3351ce6e + github.com/smartcontractkit/chainlink-common v0.2.1-0.20240717132349-ee5af9b79834 github.com/smartcontractkit/libocr v0.0.0-20240717100443-f6226e09bee7 github.com/stretchr/testify v1.9.0 go.uber.org/zap v1.26.0 diff --git a/go.sum b/go.sum index 07ffbc2bd..4185ff1ae 100644 --- a/go.sum +++ b/go.sum @@ -52,8 +52,8 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6Ng github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/smartcontractkit/chainlink-common v0.1.7-0.20240712162033-89bd3351ce6e h1:vKVNJfFXy4Wdq5paOV0/fNgql2GoXkei10+D+SmC+Qs= -github.com/smartcontractkit/chainlink-common v0.1.7-0.20240712162033-89bd3351ce6e/go.mod h1:fh9eBbrReCmv31bfz52ENCAMa7nTKQbdhb2B3+S2VGo= +github.com/smartcontractkit/chainlink-common v0.2.1-0.20240717132349-ee5af9b79834 h1:pTf4xdcmiWBqWZ6rTy2RMTDBzhHk89VC1pM7jXKQztI= +github.com/smartcontractkit/chainlink-common v0.2.1-0.20240717132349-ee5af9b79834/go.mod h1:fh9eBbrReCmv31bfz52ENCAMa7nTKQbdhb2B3+S2VGo= github.com/smartcontractkit/libocr v0.0.0-20240717100443-f6226e09bee7 h1:e38V5FYE7DA1JfKXeD5Buo/7lczALuVXlJ8YNTAUxcw= github.com/smartcontractkit/libocr v0.0.0-20240717100443-f6226e09bee7/go.mod h1:fb1ZDVXACvu4frX3APHZaEBp0xi1DIm34DcA0CwTsZM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/internal/mocks/ccipreader.go b/internal/mocks/ccipreader.go deleted file mode 100644 index 5a8819120..000000000 --- a/internal/mocks/ccipreader.go +++ /dev/null @@ -1,64 +0,0 @@ -package mocks - -import ( - "context" - "time" - - "github.com/stretchr/testify/mock" - - cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" - - "github.com/smartcontractkit/chainlink-ccip/plugintypes" -) - -type CCIPReader struct { - *mock.Mock -} - -func NewCCIPReader() *CCIPReader { - return &CCIPReader{ - Mock: &mock.Mock{}, - } -} - -func (r CCIPReader) CommitReportsGTETimestamp( - ctx context.Context, dest cciptypes.ChainSelector, ts time.Time, limit int, -) ([]plugintypes.CommitPluginReportWithMeta, error) { - args := r.Called(ctx, dest, ts, limit) - return args.Get(0).([]plugintypes.CommitPluginReportWithMeta), args.Error(1) -} - -func (r CCIPReader) ExecutedMessageRanges( - ctx context.Context, source, dest cciptypes.ChainSelector, seqNumRange cciptypes.SeqNumRange, -) ([]cciptypes.SeqNumRange, error) { - args := r.Called(ctx, source, dest, seqNumRange) - return args.Get(0).([]cciptypes.SeqNumRange), args.Error(1) -} - -func (r CCIPReader) MsgsBetweenSeqNums( - ctx context.Context, chain cciptypes.ChainSelector, seqNumRange cciptypes.SeqNumRange, -) ([]cciptypes.Message, error) { - args := r.Called(ctx, chain, seqNumRange) - return args.Get(0).([]cciptypes.Message), args.Error(1) -} - -func (r CCIPReader) NextSeqNum(ctx context.Context, chains []cciptypes.ChainSelector) ( - seqNum []cciptypes.SeqNum, err error) { - args := r.Called(ctx, chains) - return args.Get(0).([]cciptypes.SeqNum), args.Error(1) -} - -func (r CCIPReader) GasPrices(ctx context.Context, chains []cciptypes.ChainSelector) ([]cciptypes.BigInt, error) { - args := r.Called(ctx, chains) - return args.Get(0).([]cciptypes.BigInt), args.Error(1) -} - -func (r CCIPReader) Sync(ctx context.Context) (bool, error) { - args := r.Called(ctx) - return args.Bool(0), args.Error(1) -} - -func (r CCIPReader) Close(ctx context.Context) error { - args := r.Called(ctx) - return args.Error(0) -} diff --git a/internal/mocks/inmem/ccipreader_inmem.go b/internal/mocks/inmem/ccipreader_inmem.go index 1680988b4..3f62e0d29 100644 --- a/internal/mocks/inmem/ccipreader_inmem.go +++ b/internal/mocks/inmem/ccipreader_inmem.go @@ -97,6 +97,14 @@ func (r InMemoryCCIPReader) NextSeqNum( panic("implement me") } +func (r InMemoryCCIPReader) Nonces( + ctx context.Context, + source, dest cciptypes.ChainSelector, + addresses []string, +) (map[string]uint64, error) { + panic("implement me") +} + func (r InMemoryCCIPReader) GasPrices( ctx context.Context, chains []cciptypes.ChainSelector, ) ([]cciptypes.BigInt, error) { diff --git a/internal/plugincommon/ccipreader_test.go b/internal/plugincommon/ccipreader_test.go index b311a8280..5f017f504 100644 --- a/internal/plugincommon/ccipreader_test.go +++ b/internal/plugincommon/ccipreader_test.go @@ -10,14 +10,14 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" - "github.com/smartcontractkit/chainlink-ccip/internal/mocks" - "github.com/smartcontractkit/chainlink-common/pkg/logger" + + reader_mock "github.com/smartcontractkit/chainlink-ccip/mocks/internal_/reader" ) func TestBackgroundReaderSyncer(t *testing.T) { lggr := logger.Test(t) - mockReader := mocks.NewCCIPReader() + mockReader := reader_mock.NewMockCCIP(t) t.Run("start/stop checks", func(t *testing.T) { readerSyncer := NewBackgroundReaderSyncer(lggr, mockReader, time.Hour, time.Hour) @@ -61,7 +61,7 @@ func TestBackgroundReaderSyncer(t *testing.T) { func Test_backgroundReaderSync(t *testing.T) { ctx, cf := context.WithCancel(context.Background()) lggr := logger.Test(t) - reader := mocks.NewCCIPReader() + reader := reader_mock.NewMockCCIP(t) syncTimeout := 50 * time.Millisecond ticker := make(chan time.Time) wg := &sync.WaitGroup{} diff --git a/internal/reader/ccip.go b/internal/reader/ccip.go index e877e86e5..3e665dff8 100644 --- a/internal/reader/ccip.go +++ b/internal/reader/ccip.go @@ -9,9 +9,10 @@ import ( "sync" "time" - types2 "github.com/smartcontractkit/libocr/offchainreporting2plus/types" "golang.org/x/sync/errgroup" + types2 "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/types" cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" @@ -57,6 +58,14 @@ type CCIP interface { // TODO: if destination was a parameter, this could be a capability reused across plugin instances. NextSeqNum(ctx context.Context, chains []cciptypes.ChainSelector) (seqNum []cciptypes.SeqNum, err error) + // Nonces fetches all nonces for the provided selector/address pairs. Addresses are a string encoded raw address, + // it must be encoding according to the destination chain requirements with typeconv.AddressBytesToString. + Nonces( + ctx context.Context, + source, dest cciptypes.ChainSelector, + addresses []string, + ) (map[string]uint64, error) + // GasPrices reads the provided chains gas prices. GasPrices(ctx context.Context, chains []cciptypes.ChainSelector) ([]cciptypes.BigInt, error) @@ -401,6 +410,14 @@ func (r *CCIPChainReader) NextSeqNum( return res, err } +func (r *CCIPChainReader) Nonces( + ctx context.Context, + source, dest cciptypes.ChainSelector, + addresses []string, +) (map[string]uint64, error) { + return nil, fmt.Errorf("implement me") +} + func (r *CCIPChainReader) GasPrices(ctx context.Context, chains []cciptypes.ChainSelector) ([]cciptypes.BigInt, error) { if err := r.validateWriterExistence(chains...); err != nil { return nil, err diff --git a/mocks/internal_/reader/ccip.go b/mocks/internal_/reader/ccip.go new file mode 100644 index 000000000..e66847586 --- /dev/null +++ b/mocks/internal_/reader/ccip.go @@ -0,0 +1,505 @@ +// Code generated by mockery v2.43.0. DO NOT EDIT. + +package reader + +import ( + context "context" + + ccipocr3 "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" + + mock "github.com/stretchr/testify/mock" + + plugintypes "github.com/smartcontractkit/chainlink-ccip/plugintypes" + + time "time" +) + +// MockCCIP is an autogenerated mock type for the CCIP type +type MockCCIP struct { + mock.Mock +} + +type MockCCIP_Expecter struct { + mock *mock.Mock +} + +func (_m *MockCCIP) EXPECT() *MockCCIP_Expecter { + return &MockCCIP_Expecter{mock: &_m.Mock} +} + +// Close provides a mock function with given fields: ctx +func (_m *MockCCIP) Close(ctx context.Context) error { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for Close") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockCCIP_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' +type MockCCIP_Close_Call struct { + *mock.Call +} + +// Close is a helper method to define mock.On call +// - ctx context.Context +func (_e *MockCCIP_Expecter) Close(ctx interface{}) *MockCCIP_Close_Call { + return &MockCCIP_Close_Call{Call: _e.mock.On("Close", ctx)} +} + +func (_c *MockCCIP_Close_Call) Run(run func(ctx context.Context)) *MockCCIP_Close_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *MockCCIP_Close_Call) Return(_a0 error) *MockCCIP_Close_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockCCIP_Close_Call) RunAndReturn(run func(context.Context) error) *MockCCIP_Close_Call { + _c.Call.Return(run) + return _c +} + +// CommitReportsGTETimestamp provides a mock function with given fields: ctx, dest, ts, limit +func (_m *MockCCIP) CommitReportsGTETimestamp(ctx context.Context, dest ccipocr3.ChainSelector, ts time.Time, limit int) ([]plugintypes.CommitPluginReportWithMeta, error) { + ret := _m.Called(ctx, dest, ts, limit) + + if len(ret) == 0 { + panic("no return value specified for CommitReportsGTETimestamp") + } + + var r0 []plugintypes.CommitPluginReportWithMeta + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, ccipocr3.ChainSelector, time.Time, int) ([]plugintypes.CommitPluginReportWithMeta, error)); ok { + return rf(ctx, dest, ts, limit) + } + if rf, ok := ret.Get(0).(func(context.Context, ccipocr3.ChainSelector, time.Time, int) []plugintypes.CommitPluginReportWithMeta); ok { + r0 = rf(ctx, dest, ts, limit) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]plugintypes.CommitPluginReportWithMeta) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, ccipocr3.ChainSelector, time.Time, int) error); ok { + r1 = rf(ctx, dest, ts, limit) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockCCIP_CommitReportsGTETimestamp_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CommitReportsGTETimestamp' +type MockCCIP_CommitReportsGTETimestamp_Call struct { + *mock.Call +} + +// CommitReportsGTETimestamp is a helper method to define mock.On call +// - ctx context.Context +// - dest ccipocr3.ChainSelector +// - ts time.Time +// - limit int +func (_e *MockCCIP_Expecter) CommitReportsGTETimestamp(ctx interface{}, dest interface{}, ts interface{}, limit interface{}) *MockCCIP_CommitReportsGTETimestamp_Call { + return &MockCCIP_CommitReportsGTETimestamp_Call{Call: _e.mock.On("CommitReportsGTETimestamp", ctx, dest, ts, limit)} +} + +func (_c *MockCCIP_CommitReportsGTETimestamp_Call) Run(run func(ctx context.Context, dest ccipocr3.ChainSelector, ts time.Time, limit int)) *MockCCIP_CommitReportsGTETimestamp_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(ccipocr3.ChainSelector), args[2].(time.Time), args[3].(int)) + }) + return _c +} + +func (_c *MockCCIP_CommitReportsGTETimestamp_Call) Return(_a0 []plugintypes.CommitPluginReportWithMeta, _a1 error) *MockCCIP_CommitReportsGTETimestamp_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockCCIP_CommitReportsGTETimestamp_Call) RunAndReturn(run func(context.Context, ccipocr3.ChainSelector, time.Time, int) ([]plugintypes.CommitPluginReportWithMeta, error)) *MockCCIP_CommitReportsGTETimestamp_Call { + _c.Call.Return(run) + return _c +} + +// ExecutedMessageRanges provides a mock function with given fields: ctx, source, dest, seqNumRange +func (_m *MockCCIP) ExecutedMessageRanges(ctx context.Context, source ccipocr3.ChainSelector, dest ccipocr3.ChainSelector, seqNumRange ccipocr3.SeqNumRange) ([]ccipocr3.SeqNumRange, error) { + ret := _m.Called(ctx, source, dest, seqNumRange) + + if len(ret) == 0 { + panic("no return value specified for ExecutedMessageRanges") + } + + var r0 []ccipocr3.SeqNumRange + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, ccipocr3.ChainSelector, ccipocr3.ChainSelector, ccipocr3.SeqNumRange) ([]ccipocr3.SeqNumRange, error)); ok { + return rf(ctx, source, dest, seqNumRange) + } + if rf, ok := ret.Get(0).(func(context.Context, ccipocr3.ChainSelector, ccipocr3.ChainSelector, ccipocr3.SeqNumRange) []ccipocr3.SeqNumRange); ok { + r0 = rf(ctx, source, dest, seqNumRange) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]ccipocr3.SeqNumRange) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, ccipocr3.ChainSelector, ccipocr3.ChainSelector, ccipocr3.SeqNumRange) error); ok { + r1 = rf(ctx, source, dest, seqNumRange) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockCCIP_ExecutedMessageRanges_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ExecutedMessageRanges' +type MockCCIP_ExecutedMessageRanges_Call struct { + *mock.Call +} + +// ExecutedMessageRanges is a helper method to define mock.On call +// - ctx context.Context +// - source ccipocr3.ChainSelector +// - dest ccipocr3.ChainSelector +// - seqNumRange ccipocr3.SeqNumRange +func (_e *MockCCIP_Expecter) ExecutedMessageRanges(ctx interface{}, source interface{}, dest interface{}, seqNumRange interface{}) *MockCCIP_ExecutedMessageRanges_Call { + return &MockCCIP_ExecutedMessageRanges_Call{Call: _e.mock.On("ExecutedMessageRanges", ctx, source, dest, seqNumRange)} +} + +func (_c *MockCCIP_ExecutedMessageRanges_Call) Run(run func(ctx context.Context, source ccipocr3.ChainSelector, dest ccipocr3.ChainSelector, seqNumRange ccipocr3.SeqNumRange)) *MockCCIP_ExecutedMessageRanges_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(ccipocr3.ChainSelector), args[2].(ccipocr3.ChainSelector), args[3].(ccipocr3.SeqNumRange)) + }) + return _c +} + +func (_c *MockCCIP_ExecutedMessageRanges_Call) Return(_a0 []ccipocr3.SeqNumRange, _a1 error) *MockCCIP_ExecutedMessageRanges_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockCCIP_ExecutedMessageRanges_Call) RunAndReturn(run func(context.Context, ccipocr3.ChainSelector, ccipocr3.ChainSelector, ccipocr3.SeqNumRange) ([]ccipocr3.SeqNumRange, error)) *MockCCIP_ExecutedMessageRanges_Call { + _c.Call.Return(run) + return _c +} + +// GasPrices provides a mock function with given fields: ctx, chains +func (_m *MockCCIP) GasPrices(ctx context.Context, chains []ccipocr3.ChainSelector) ([]ccipocr3.BigInt, error) { + ret := _m.Called(ctx, chains) + + if len(ret) == 0 { + panic("no return value specified for GasPrices") + } + + var r0 []ccipocr3.BigInt + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []ccipocr3.ChainSelector) ([]ccipocr3.BigInt, error)); ok { + return rf(ctx, chains) + } + if rf, ok := ret.Get(0).(func(context.Context, []ccipocr3.ChainSelector) []ccipocr3.BigInt); ok { + r0 = rf(ctx, chains) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]ccipocr3.BigInt) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, []ccipocr3.ChainSelector) error); ok { + r1 = rf(ctx, chains) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockCCIP_GasPrices_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GasPrices' +type MockCCIP_GasPrices_Call struct { + *mock.Call +} + +// GasPrices is a helper method to define mock.On call +// - ctx context.Context +// - chains []ccipocr3.ChainSelector +func (_e *MockCCIP_Expecter) GasPrices(ctx interface{}, chains interface{}) *MockCCIP_GasPrices_Call { + return &MockCCIP_GasPrices_Call{Call: _e.mock.On("GasPrices", ctx, chains)} +} + +func (_c *MockCCIP_GasPrices_Call) Run(run func(ctx context.Context, chains []ccipocr3.ChainSelector)) *MockCCIP_GasPrices_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].([]ccipocr3.ChainSelector)) + }) + return _c +} + +func (_c *MockCCIP_GasPrices_Call) Return(_a0 []ccipocr3.BigInt, _a1 error) *MockCCIP_GasPrices_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockCCIP_GasPrices_Call) RunAndReturn(run func(context.Context, []ccipocr3.ChainSelector) ([]ccipocr3.BigInt, error)) *MockCCIP_GasPrices_Call { + _c.Call.Return(run) + return _c +} + +// MsgsBetweenSeqNums provides a mock function with given fields: ctx, chain, seqNumRange +func (_m *MockCCIP) MsgsBetweenSeqNums(ctx context.Context, chain ccipocr3.ChainSelector, seqNumRange ccipocr3.SeqNumRange) ([]ccipocr3.Message, error) { + ret := _m.Called(ctx, chain, seqNumRange) + + if len(ret) == 0 { + panic("no return value specified for MsgsBetweenSeqNums") + } + + var r0 []ccipocr3.Message + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, ccipocr3.ChainSelector, ccipocr3.SeqNumRange) ([]ccipocr3.Message, error)); ok { + return rf(ctx, chain, seqNumRange) + } + if rf, ok := ret.Get(0).(func(context.Context, ccipocr3.ChainSelector, ccipocr3.SeqNumRange) []ccipocr3.Message); ok { + r0 = rf(ctx, chain, seqNumRange) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]ccipocr3.Message) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, ccipocr3.ChainSelector, ccipocr3.SeqNumRange) error); ok { + r1 = rf(ctx, chain, seqNumRange) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockCCIP_MsgsBetweenSeqNums_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MsgsBetweenSeqNums' +type MockCCIP_MsgsBetweenSeqNums_Call struct { + *mock.Call +} + +// MsgsBetweenSeqNums is a helper method to define mock.On call +// - ctx context.Context +// - chain ccipocr3.ChainSelector +// - seqNumRange ccipocr3.SeqNumRange +func (_e *MockCCIP_Expecter) MsgsBetweenSeqNums(ctx interface{}, chain interface{}, seqNumRange interface{}) *MockCCIP_MsgsBetweenSeqNums_Call { + return &MockCCIP_MsgsBetweenSeqNums_Call{Call: _e.mock.On("MsgsBetweenSeqNums", ctx, chain, seqNumRange)} +} + +func (_c *MockCCIP_MsgsBetweenSeqNums_Call) Run(run func(ctx context.Context, chain ccipocr3.ChainSelector, seqNumRange ccipocr3.SeqNumRange)) *MockCCIP_MsgsBetweenSeqNums_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(ccipocr3.ChainSelector), args[2].(ccipocr3.SeqNumRange)) + }) + return _c +} + +func (_c *MockCCIP_MsgsBetweenSeqNums_Call) Return(_a0 []ccipocr3.Message, _a1 error) *MockCCIP_MsgsBetweenSeqNums_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockCCIP_MsgsBetweenSeqNums_Call) RunAndReturn(run func(context.Context, ccipocr3.ChainSelector, ccipocr3.SeqNumRange) ([]ccipocr3.Message, error)) *MockCCIP_MsgsBetweenSeqNums_Call { + _c.Call.Return(run) + return _c +} + +// NextSeqNum provides a mock function with given fields: ctx, chains +func (_m *MockCCIP) NextSeqNum(ctx context.Context, chains []ccipocr3.ChainSelector) ([]ccipocr3.SeqNum, error) { + ret := _m.Called(ctx, chains) + + if len(ret) == 0 { + panic("no return value specified for NextSeqNum") + } + + var r0 []ccipocr3.SeqNum + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []ccipocr3.ChainSelector) ([]ccipocr3.SeqNum, error)); ok { + return rf(ctx, chains) + } + if rf, ok := ret.Get(0).(func(context.Context, []ccipocr3.ChainSelector) []ccipocr3.SeqNum); ok { + r0 = rf(ctx, chains) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]ccipocr3.SeqNum) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, []ccipocr3.ChainSelector) error); ok { + r1 = rf(ctx, chains) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockCCIP_NextSeqNum_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'NextSeqNum' +type MockCCIP_NextSeqNum_Call struct { + *mock.Call +} + +// NextSeqNum is a helper method to define mock.On call +// - ctx context.Context +// - chains []ccipocr3.ChainSelector +func (_e *MockCCIP_Expecter) NextSeqNum(ctx interface{}, chains interface{}) *MockCCIP_NextSeqNum_Call { + return &MockCCIP_NextSeqNum_Call{Call: _e.mock.On("NextSeqNum", ctx, chains)} +} + +func (_c *MockCCIP_NextSeqNum_Call) Run(run func(ctx context.Context, chains []ccipocr3.ChainSelector)) *MockCCIP_NextSeqNum_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].([]ccipocr3.ChainSelector)) + }) + return _c +} + +func (_c *MockCCIP_NextSeqNum_Call) Return(seqNum []ccipocr3.SeqNum, err error) *MockCCIP_NextSeqNum_Call { + _c.Call.Return(seqNum, err) + return _c +} + +func (_c *MockCCIP_NextSeqNum_Call) RunAndReturn(run func(context.Context, []ccipocr3.ChainSelector) ([]ccipocr3.SeqNum, error)) *MockCCIP_NextSeqNum_Call { + _c.Call.Return(run) + return _c +} + +// Nonces provides a mock function with given fields: ctx, source, dest, addresses +func (_m *MockCCIP) Nonces(ctx context.Context, source ccipocr3.ChainSelector, dest ccipocr3.ChainSelector, addresses []string) (map[string]uint64, error) { + ret := _m.Called(ctx, source, dest, addresses) + + if len(ret) == 0 { + panic("no return value specified for Nonces") + } + + var r0 map[string]uint64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, ccipocr3.ChainSelector, ccipocr3.ChainSelector, []string) (map[string]uint64, error)); ok { + return rf(ctx, source, dest, addresses) + } + if rf, ok := ret.Get(0).(func(context.Context, ccipocr3.ChainSelector, ccipocr3.ChainSelector, []string) map[string]uint64); ok { + r0 = rf(ctx, source, dest, addresses) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]uint64) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, ccipocr3.ChainSelector, ccipocr3.ChainSelector, []string) error); ok { + r1 = rf(ctx, source, dest, addresses) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockCCIP_Nonces_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Nonces' +type MockCCIP_Nonces_Call struct { + *mock.Call +} + +// Nonces is a helper method to define mock.On call +// - ctx context.Context +// - source ccipocr3.ChainSelector +// - dest ccipocr3.ChainSelector +// - addresses []string +func (_e *MockCCIP_Expecter) Nonces(ctx interface{}, source interface{}, dest interface{}, addresses interface{}) *MockCCIP_Nonces_Call { + return &MockCCIP_Nonces_Call{Call: _e.mock.On("Nonces", ctx, source, dest, addresses)} +} + +func (_c *MockCCIP_Nonces_Call) Run(run func(ctx context.Context, source ccipocr3.ChainSelector, dest ccipocr3.ChainSelector, addresses []string)) *MockCCIP_Nonces_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(ccipocr3.ChainSelector), args[2].(ccipocr3.ChainSelector), args[3].([]string)) + }) + return _c +} + +func (_c *MockCCIP_Nonces_Call) Return(_a0 map[string]uint64, _a1 error) *MockCCIP_Nonces_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockCCIP_Nonces_Call) RunAndReturn(run func(context.Context, ccipocr3.ChainSelector, ccipocr3.ChainSelector, []string) (map[string]uint64, error)) *MockCCIP_Nonces_Call { + _c.Call.Return(run) + return _c +} + +// Sync provides a mock function with given fields: ctx +func (_m *MockCCIP) Sync(ctx context.Context) (bool, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for Sync") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (bool, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) bool); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockCCIP_Sync_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Sync' +type MockCCIP_Sync_Call struct { + *mock.Call +} + +// Sync is a helper method to define mock.On call +// - ctx context.Context +func (_e *MockCCIP_Expecter) Sync(ctx interface{}) *MockCCIP_Sync_Call { + return &MockCCIP_Sync_Call{Call: _e.mock.On("Sync", ctx)} +} + +func (_c *MockCCIP_Sync_Call) Run(run func(ctx context.Context)) *MockCCIP_Sync_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *MockCCIP_Sync_Call) Return(_a0 bool, _a1 error) *MockCCIP_Sync_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockCCIP_Sync_Call) RunAndReturn(run func(context.Context) (bool, error)) *MockCCIP_Sync_Call { + _c.Call.Return(run) + return _c +} + +// NewMockCCIP creates a new instance of MockCCIP. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockCCIP(t interface { + mock.TestingT + Cleanup(func()) +}) *MockCCIP { + mock := &MockCCIP{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +}