diff --git a/.github/workflows/ccip-ocr3-build-lint-test.yml b/.github/workflows/ccip-ocr3-build-lint-test.yml deleted file mode 100644 index 554995dc65..0000000000 --- a/.github/workflows/ccip-ocr3-build-lint-test.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: "Build lint and test CCIP-OCR3" - -on: - pull_request: - paths: - - core/services/ocr3/plugins/ccip/** - -jobs: - build-lint-test: - runs-on: ubuntu-20.04 - strategy: - matrix: - go-version: ['1.21'] - defaults: - run: - working-directory: ./core/services/ocr3/plugins/ccip - steps: - - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 - - name: Setup Go ${{ matrix.go-version }} - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 - with: - go-version: ${{ matrix.go-version }} - - name: Display Go version - run: go version - - name: Build - run: go build -v ./... - - name: Install linter - run: | - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.59.0 - - name: Run linter - run: golangci-lint run -c .golangci.yml - - name: Run tests - run: go test -race -fullpath -shuffle on -count 20 ./... diff --git a/core/services/ocr3/plugins/ccip/.golangci.yml b/core/services/ocr3/plugins/ccip/.golangci.yml deleted file mode 100644 index 6765266f3d..0000000000 --- a/core/services/ocr3/plugins/ccip/.golangci.yml +++ /dev/null @@ -1,93 +0,0 @@ -run: - timeout: 60s -linters: - enable: - - exhaustive - - exportloopref - - revive - - goimports - - gosec - - misspell - - rowserrcheck - - errorlint - - unconvert - - sqlclosecheck - - noctx - - depguard - - lll -linters-settings: - exhaustive: - default-signifies-exhaustive: true - goimports: - local-prefixes: "github.com/smartcontractkit/ccipocr3" - gosec: - excludes: - errorlint: - errorf: true # Disallow formatting of errors without %w - revive: - confidence: 0.8 - rules: - - name: blank-imports - - name: context-as-argument - - name: context-keys-type - - name: dot-imports - - name: error-return - - name: error-strings - - name: error-naming - - name: if-return - - name: increment-decrement - - name: var-naming - - name: var-declaration - - name: package-comments - - name: range - - name: receiver-naming - - name: time-naming - - name: unexported-return - - name: indent-error-flow - - name: errorf - - name: empty-block - - name: superfluous-else - # - name: unused-parameter - - name: unreachable-code - - name: redefines-builtin-id - - name: waitgroup-by-value - - name: unconditional-recursion - - name: struct-tag - - name: string-format - - name: string-of-int - - name: range-val-address - - name: range-val-in-closure - - name: modifies-value-receiver - - name: modifies-parameter - - name: identical-branches - - name: get-return - #- name: flag-parameter - - name: early-return - - name: defer - - name: constant-logical-expr - - name: confusing-naming - - name: confusing-results - - name: bool-literal-in-expr - - name: atomic - depguard: - rules: - main: - list-mode: lax - deny: - - pkg: github.com/test-go/testify/assert - desc: Use github.com/stretchr/testify/assert instead - - pkg: github.com/test-go/testify/mock - desc: Use github.com/stretchr/testify/mock instead - - pkg: github.com/test-go/testify/require - desc: Use github.com/stretchr/testify/require instead - lll: - # Max line length, lines longer will be reported. - # '\t' is counted as 1 character by default, and can be changed with the tab-width option. - # Default: 120. - line-length: 120 -issues: - exclude-rules: - - path: test - text: "^G404:" - linters: - - gosec diff --git a/core/services/ocr3/plugins/ccip/Makefile b/core/services/ocr3/plugins/ccip/Makefile deleted file mode 100644 index 358b75feb5..0000000000 --- a/core/services/ocr3/plugins/ccip/Makefile +++ /dev/null @@ -1,12 +0,0 @@ - -ensure_go_1_21: - @go version | grep -q 'go1.21' || (echo "Please use go1.21" && exit 1) - -ensure_golangcilint_1_59: - @golangci-lint --version | grep -q '1.59' || (echo "Please use golangci-lint 1.59" && exit 1) - -test: ensure_go_1_21 - go test -race -fullpath -shuffle on -count 10 ./... - -lint: ensure_go_1_21 - golangci-lint run -c .golangci.yml diff --git a/core/services/ocr3/plugins/ccip/commit/factory.go b/core/services/ocr3/plugins/ccip/commit/factory.go deleted file mode 100644 index 2b10231c01..0000000000 --- a/core/services/ocr3/plugins/ccip/commit/factory.go +++ /dev/null @@ -1,98 +0,0 @@ -package commit - -import ( - "context" - "math/big" - - "github.com/smartcontractkit/ccipocr3/internal/reader" - - "google.golang.org/grpc" - - cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" - "github.com/smartcontractkit/chainlink-common/pkg/types/core" - - "github.com/smartcontractkit/libocr/commontypes" - "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" - ocr2types "github.com/smartcontractkit/libocr/offchainreporting2plus/types" - libocrtypes "github.com/smartcontractkit/libocr/ragep2p/types" -) - -// PluginFactoryConstructor implements common OCR3ReportingPluginClient and is used for initializing a plugin factory -// and a validation service. -type PluginFactoryConstructor struct{} - -func NewPluginFactoryConstructor() *PluginFactoryConstructor { - return &PluginFactoryConstructor{} -} -func (p PluginFactoryConstructor) NewReportingPluginFactory( - ctx context.Context, - config core.ReportingPluginServiceConfig, - grpcProvider grpc.ClientConnInterface, - pipelineRunner core.PipelineRunnerService, - telemetry core.TelemetryService, - errorLog core.ErrorLog, - capRegistry core.CapabilitiesRegistry, - keyValueStore core.KeyValueStore, - relayerSet core.RelayerSet, -) (core.OCR3ReportingPluginFactory, error) { - return NewPluginFactory(), nil -} - -func (p PluginFactoryConstructor) NewValidationService(ctx context.Context) (core.ValidationService, error) { - panic("implement me") -} - -// PluginFactory implements common ReportingPluginFactory and is used for (re-)initializing commit plugin instances. -type PluginFactory struct{} - -func NewPluginFactory() *PluginFactory { - return &PluginFactory{} -} - -func (p PluginFactory) NewReportingPlugin(config ocr3types.ReportingPluginConfig, -) (ocr3types.ReportingPlugin[[]byte], ocr3types.ReportingPluginInfo, error) { - // TODO: Get this from ocr config, it's the mapping of the oracleId index in the DON - var oracleIDToP2pID map[commontypes.OracleID]libocrtypes.PeerID - onChainTokenPricesReader := reader.NewOnchainTokenPricesReader( - reader.TokenPriceConfig{ // TODO: Inject config - StaticPrices: map[ocr2types.Account]big.Int{}, - }, - nil, // TODO: Inject this - ) - return NewPlugin( - context.Background(), - config.OracleID, - oracleIDToP2pID, - cciptypes.CommitPluginConfig{}, - nil, //ccipReader - onChainTokenPricesReader, - nil, //reportCodec - nil, //msgHasher - nil, // lggr - nil, //homeChain - ), ocr3types.ReportingPluginInfo{}, nil -} - -func (p PluginFactory) Name() string { - panic("implement me") -} - -func (p PluginFactory) Start(ctx context.Context) error { - panic("implement me") -} - -func (p PluginFactory) Close() error { - panic("implement me") -} - -func (p PluginFactory) Ready() error { - panic("implement me") -} - -func (p PluginFactory) HealthReport() map[string]error { - panic("implement me") -} - -// Interface compatibility checks. -var _ core.OCR3ReportingPluginClient = &PluginFactoryConstructor{} -var _ core.OCR3ReportingPluginFactory = &PluginFactory{} diff --git a/core/services/ocr3/plugins/ccip/commit/plugin.go b/core/services/ocr3/plugins/ccip/commit/plugin.go deleted file mode 100644 index d6e28eab05..0000000000 --- a/core/services/ocr3/plugins/ccip/commit/plugin.go +++ /dev/null @@ -1,382 +0,0 @@ -package commit - -import ( - "context" - "fmt" - "sort" - "time" - - mapset "github.com/deckarep/golang-set/v2" - - "github.com/smartcontractkit/ccipocr3/internal/reader" - - "github.com/smartcontractkit/libocr/commontypes" - "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" - "github.com/smartcontractkit/libocr/offchainreporting2plus/types" - libocrtypes "github.com/smartcontractkit/libocr/ragep2p/types" - - "github.com/smartcontractkit/ccipocr3/internal/libs/slicelib" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" -) - -// Plugin implements the main ocr3 ccip commit plugin logic. -// To learn more about the plugin lifecycle, see the ocr3types.ReportingPlugin interface. -// -// NOTE: If you are changing core plugin logic, you should also update the commit plugin python spec. -type Plugin struct { - nodeID commontypes.OracleID - oracleIDToP2pID map[commontypes.OracleID]libocrtypes.PeerID - cfg cciptypes.CommitPluginConfig - ccipReader cciptypes.CCIPReader - tokenPricesReader cciptypes.TokenPricesReader - reportCodec cciptypes.CommitPluginCodec - msgHasher cciptypes.MessageHasher - lggr logger.Logger - - homeChain reader.HomeChain -} - -func NewPlugin( - _ context.Context, - nodeID commontypes.OracleID, - oracleIDToP2pID map[commontypes.OracleID]libocrtypes.PeerID, - cfg cciptypes.CommitPluginConfig, - ccipReader cciptypes.CCIPReader, - tokenPricesReader cciptypes.TokenPricesReader, - reportCodec cciptypes.CommitPluginCodec, - msgHasher cciptypes.MessageHasher, - lggr logger.Logger, - homeChain reader.HomeChain, -) *Plugin { - return &Plugin{ - nodeID: nodeID, - oracleIDToP2pID: oracleIDToP2pID, - cfg: cfg, - ccipReader: ccipReader, - tokenPricesReader: tokenPricesReader, - reportCodec: reportCodec, - msgHasher: msgHasher, - lggr: lggr, - homeChain: homeChain, - } -} - -// Query phase is not used. -func (p *Plugin) Query(_ context.Context, _ ocr3types.OutcomeContext) (types.Query, error) { - return types.Query{}, nil -} - -// Observation phase is used to discover max chain sequence numbers, new messages, gas and token prices. -// -// Max Chain Sequence Numbers: -// -// It is the sequence number of the last known committed message for each known source chain. -// If there was a previous outcome we start with the max sequence numbers of the previous outcome. -// We then read the sequence numbers from the destination chain and override when the on-chain sequence number -// is greater than previous outcome or when previous outcome did not contain a sequence number for a known source chain. -// -// New Messages: -// -// We discover new ccip messages only for the chains that the current node is allowed to read from based on the -// previously discovered max chain sequence numbers. For each chain we scan for new messages -// in the [max_sequence_number+1, max_sequence_number+1+p.cfg.NewMsgScanBatchSize] range. -// -// Gas Prices: -// -// We discover the gas prices for each readable source chain. -// -// Token Prices: -// -// We discover the token prices only for the tokens that are used to pay for ccip fees. -// The fee tokens are configured in the plugin config. -func (p *Plugin) Observation( - ctx context.Context, outctx ocr3types.OutcomeContext, _ types.Query, -) (types.Observation, error) { - supportedChains, err := p.supportedChains() - if err != nil { - return types.Observation{}, fmt.Errorf("error finding supported chains by node: %w", err) - } - - msgBaseDetails := make([]cciptypes.CCIPMsgBaseDetails, 0) - latestCommittedSeqNumsObservation, err := observeLatestCommittedSeqNums( - ctx, p.lggr, p.ccipReader, supportedChains, p.cfg.DestChain, p.knownSourceChainsSlice(), - ) - if err != nil { - return types.Observation{}, fmt.Errorf("observe latest committed sequence numbers: %w", err) - } - - var tokenPrices []cciptypes.TokenPrice - if p.cfg.TokenPricesObserver { - tokenPrices, err = observeTokenPrices( - ctx, - p.tokenPricesReader, - p.cfg.PricedTokens, - ) - if err != nil { - return types.Observation{}, fmt.Errorf("observe token prices: %w", err) - } - } - - // Find the gas prices for each source chain. - var gasPrices []cciptypes.GasPriceChain - gasPrices, err = observeGasPrices(ctx, p.ccipReader, p.knownSourceChainsSlice()) - if err != nil { - return types.Observation{}, fmt.Errorf("observe gas prices: %w", err) - } - - fChain, err := p.homeChain.GetFChain() - if err != nil { - return types.Observation{}, fmt.Errorf("get f chain: %w", err) - } - - // If there's no previous outcome (first round ever), we only observe the latest committed sequence numbers. - // and on the next round we use those to look for messages. - if outctx.PreviousOutcome == nil { - p.lggr.Debugw("first round ever, can't observe new messages yet") - return cciptypes.NewCommitPluginObservation( - msgBaseDetails, gasPrices, tokenPrices, latestCommittedSeqNumsObservation, fChain, - ).Encode() - } - - prevOutcome, err := cciptypes.DecodeCommitPluginOutcome(outctx.PreviousOutcome) - if err != nil { - return types.Observation{}, fmt.Errorf("decode commit plugin previous outcome: %w", err) - } - p.lggr.Debugw("previous outcome decoded", "outcome", prevOutcome.String()) - - // Always observe based on previous outcome. We'll filter out stale messages in the outcome phase. - newMsgs, err := observeNewMsgs( - ctx, - p.lggr, - p.ccipReader, - p.msgHasher, - supportedChains, - prevOutcome.MaxSeqNums, // TODO: Chainlink common PR to rename. - p.cfg.NewMsgScanBatchSize, - ) - if err != nil { - return types.Observation{}, fmt.Errorf("observe new messages: %w", err) - } - - p.lggr.Infow("submitting observation", - "observedNewMsgs", len(newMsgs), - "gasPrices", len(gasPrices), - "tokenPrices", len(tokenPrices), - "latestCommittedSeqNums", latestCommittedSeqNumsObservation, - "fChain", fChain) - - for _, msg := range newMsgs { - msgBaseDetails = append(msgBaseDetails, msg.CCIPMsgBaseDetails) - } - - return cciptypes.NewCommitPluginObservation( - msgBaseDetails, gasPrices, tokenPrices, latestCommittedSeqNumsObservation, fChain, - ).Encode() - -} - -func (p *Plugin) ValidateObservation(_ ocr3types.OutcomeContext, _ types.Query, ao types.AttributedObservation) error { - obs, err := cciptypes.DecodeCommitPluginObservation(ao.Observation) - if err != nil { - return fmt.Errorf("decode commit plugin observation: %w", err) - } - - if err := validateObservedSequenceNumbers(obs.NewMsgs, obs.MaxSeqNums); err != nil { - return fmt.Errorf("validate sequence numbers: %w", err) - } - - destSupportedChains, err := p.supportedChains() - if err != nil { - return fmt.Errorf("error finding supported chains by node: %w", err) - } - - err = validateObserverReadingEligibility(obs.NewMsgs, obs.MaxSeqNums, destSupportedChains, p.cfg.DestChain) - if err != nil { - return fmt.Errorf("validate observer %d reading eligibility: %w", ao.Observer, err) - } - - if err := validateObservedTokenPrices(obs.TokenPrices); err != nil { - return fmt.Errorf("validate token prices: %w", err) - } - - if err := validateObservedGasPrices(obs.GasPrices); err != nil { - return fmt.Errorf("validate gas prices: %w", err) - } - - return nil -} - -func (p *Plugin) ObservationQuorum(_ ocr3types.OutcomeContext, _ types.Query) (ocr3types.Quorum, error) { - // Across all chains we require at least 2F+1 observations. - return ocr3types.QuorumTwoFPlusOne, nil -} - -// Outcome phase is used to construct the final outcome based on the observations of multiple followers. -// -// The outcome contains: -// - Max Sequence Numbers: The max sequence number for each source chain. -// - Merkle Roots: One merkle tree root per source chain. The leaves of the tree are the IDs of the observed messages. -// The merkle root data type contains information about the chain and the sequence numbers range. -func (p *Plugin) Outcome( - _ ocr3types.OutcomeContext, _ types.Query, aos []types.AttributedObservation, -) (ocr3types.Outcome, error) { - decodedObservations := make([]cciptypes.CommitPluginObservation, 0) - for _, ao := range aos { - obs, err := cciptypes.DecodeCommitPluginObservation(ao.Observation) - if err != nil { - return ocr3types.Outcome{}, fmt.Errorf("decode commit plugin observation: %w", err) - } - decodedObservations = append(decodedObservations, obs) - } - - fChains := fChainConsensus(decodedObservations) - - fChainDest, ok := fChains[p.cfg.DestChain] - if !ok { - return ocr3types.Outcome{}, fmt.Errorf("missing destination chain %d in fChain config", p.cfg.DestChain) - } - - maxSeqNums := maxSeqNumsConsensus(p.lggr, fChainDest, decodedObservations) - p.lggr.Debugw("max sequence numbers consensus", "maxSeqNumsConsensus", maxSeqNums) - - merkleRoots, err := newMsgsConsensus(p.lggr, maxSeqNums, decodedObservations, fChains) - if err != nil { - return ocr3types.Outcome{}, fmt.Errorf("new messages consensus: %w", err) - } - p.lggr.Debugw("new messages consensus", "merkleRoots", merkleRoots) - - tokenPrices, err := tokenPricesConsensus(decodedObservations, fChainDest) - if err != nil { - return ocr3types.Outcome{}, fmt.Errorf("token prices consensus: %w", err) - } - - gasPrices := gasPricesConsensus(p.lggr, decodedObservations, fChainDest) - p.lggr.Debugw("gas prices consensus", "gasPrices", gasPrices) - - outcome := cciptypes.NewCommitPluginOutcome(maxSeqNums, merkleRoots, tokenPrices, gasPrices) - if outcome.IsEmpty() { - p.lggr.Debugw("empty outcome") - return ocr3types.Outcome{}, nil - } - p.lggr.Debugw("sending outcome", "outcome", outcome) - - return outcome.Encode() -} - -func (p *Plugin) Reports(seqNr uint64, outcome ocr3types.Outcome) ([]ocr3types.ReportWithInfo[[]byte], error) { - outc, err := cciptypes.DecodeCommitPluginOutcome(outcome) - if err != nil { - p.lggr.Errorw("decode commit plugin outcome", "outcome", outcome, "err", err) - return nil, fmt.Errorf("decode commit plugin outcome: %w", err) - } - - /* - todo: Once token/gas prices are implemented, we would want to probably check if outc.MerkleRoots is empty or not - and only create a report if outc.MerkleRoots is non-empty OR gas/token price timer has expired - */ - - rep := cciptypes.NewCommitPluginReport(outc.MerkleRoots, outc.TokenPrices, outc.GasPrices) - - encodedReport, err := p.reportCodec.Encode(context.Background(), rep) - if err != nil { - return nil, fmt.Errorf("encode commit plugin report: %w", err) - } - - return []ocr3types.ReportWithInfo[[]byte]{{Report: encodedReport, Info: nil}}, nil -} - -func (p *Plugin) ShouldAcceptAttestedReport( - ctx context.Context, u uint64, r ocr3types.ReportWithInfo[[]byte], -) (bool, error) { - decodedReport, err := p.reportCodec.Decode(ctx, r.Report) - if err != nil { - return false, fmt.Errorf("decode commit plugin report: %w", err) - } - - isEmpty := decodedReport.IsEmpty() - if isEmpty { - p.lggr.Infow("skipping empty report") - return false, nil - } - - return true, nil -} - -func (p *Plugin) ShouldTransmitAcceptedReport( - ctx context.Context, u uint64, r ocr3types.ReportWithInfo[[]byte], -) (bool, error) { - isWriter, err := p.supportsDestChain() - if err != nil { - return false, fmt.Errorf("can't know if it's a writer: %w", err) - } - if !isWriter { - p.lggr.Debugw("not a writer, skipping report transmission") - return false, nil - } - - decodedReport, err := p.reportCodec.Decode(ctx, r.Report) - if err != nil { - return false, fmt.Errorf("decode commit plugin report: %w", err) - } - - p.lggr.Debugw("transmitting report", - "roots", len(decodedReport.MerkleRoots), - "tokenPriceUpdates", len(decodedReport.PriceUpdates.TokenPriceUpdates), - "gasPriceUpdates", len(decodedReport.PriceUpdates.GasPriceUpdates), - ) - - // todo: if report is stale -> do not transmit (check the spec for the exact condition) - return true, nil -} - -func (p *Plugin) Close() error { - timeout := 10 * time.Second - ctx, cf := context.WithTimeout(context.Background(), timeout) - defer cf() - - if err := p.ccipReader.Close(ctx); err != nil { - return fmt.Errorf("close ccip reader: %w", err) - } - return nil -} - -func (p *Plugin) knownSourceChainsSlice() []cciptypes.ChainSelector { - knownSourceChains, err := p.homeChain.GetKnownCCIPChains() - if err != nil { - p.lggr.Errorw("error getting known chains", "err", err) - return nil - } - knownSourceChainsSlice := knownSourceChains.ToSlice() - sort.Slice( - knownSourceChainsSlice, - func(i, j int) bool { return knownSourceChainsSlice[i] < knownSourceChainsSlice[j] }, - ) - return slicelib.Filter(knownSourceChainsSlice, func(ch cciptypes.ChainSelector) bool { return ch != p.cfg.DestChain }) -} - -func (p *Plugin) supportedChains() (mapset.Set[cciptypes.ChainSelector], error) { - p2pID, exists := p.oracleIDToP2pID[p.nodeID] - if !exists { - return nil, fmt.Errorf("oracle ID %d not found in oracleIDToP2pID", p.nodeID) - } - supportedChains, err := p.homeChain.GetSupportedChainsForPeer(p2pID) - if err != nil { - p.lggr.Warnw("error getting supported chains", err) - return mapset.NewSet[cciptypes.ChainSelector](), fmt.Errorf("error getting supported chains: %w", err) - } - - return supportedChains, nil -} - -func (p *Plugin) supportsDestChain() (bool, error) { - destChainConfig, err := p.homeChain.GetChainConfig(p.cfg.DestChain) - if err != nil { - return false, fmt.Errorf("get chain config: %w", err) - } - return destChainConfig.SupportedNodes.Contains(p.oracleIDToP2pID[p.nodeID]), nil -} - -// Interface compatibility checks. -var _ ocr3types.ReportingPlugin[[]byte] = &Plugin{} diff --git a/core/services/ocr3/plugins/ccip/commit/plugin_e2e_test.go b/core/services/ocr3/plugins/ccip/commit/plugin_e2e_test.go deleted file mode 100644 index a1786ad973..0000000000 --- a/core/services/ocr3/plugins/ccip/commit/plugin_e2e_test.go +++ /dev/null @@ -1,519 +0,0 @@ -package commit - -import ( - "context" - "reflect" - "strconv" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - - "github.com/smartcontractkit/libocr/commontypes" - - "github.com/smartcontractkit/ccipocr3/internal/libs/testhelpers" - "github.com/smartcontractkit/ccipocr3/internal/mocks" - "github.com/smartcontractkit/ccipocr3/internal/reader" - - "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" - "github.com/smartcontractkit/libocr/offchainreporting2plus/types" - libocrtypes "github.com/smartcontractkit/libocr/ragep2p/types" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" -) - -func TestPlugin(t *testing.T) { - ctx := context.Background() - lggr := logger.Test(t) - - testCases := []struct { - name string - description string - nodes []nodeSetup - expErr func(*testing.T, error) - expOutcome cciptypes.CommitPluginOutcome - expTransmittedReports []cciptypes.CommitPluginReport - initialOutcome cciptypes.CommitPluginOutcome - }{ - { - name: "EmptyOutcome", - description: "Empty observations are returned by all nodes which leads to an empty outcome.", - nodes: setupEmptyOutcome(ctx, t, lggr), - expErr: func(t *testing.T, err error) { assert.Equal(t, testhelpers.ErrEmptyOutcome, err) }, - }, - { - name: "AllNodesReadAllChains", - description: "Nodes observe the latest sequence numbers and new messages after those sequence numbers. " + - "They also observe gas prices. In this setup all nodes can read all chains.", - nodes: setupAllNodesReadAllChains(ctx, t, lggr), - expOutcome: cciptypes.CommitPluginOutcome{ - MaxSeqNums: []cciptypes.SeqNumChain{ - {ChainSel: chainA, SeqNum: 10}, - {ChainSel: chainB, SeqNum: 20}, - }, - MerkleRoots: []cciptypes.MerkleRootChain{ - {ChainSel: chainB, MerkleRoot: cciptypes.Bytes32{}, SeqNumsRange: cciptypes.NewSeqNumRange(21, 22)}, - }, - TokenPrices: []cciptypes.TokenPrice{}, - GasPrices: []cciptypes.GasPriceChain{ - {ChainSel: chainA, GasPrice: cciptypes.NewBigIntFromInt64(1000)}, - {ChainSel: chainB, GasPrice: cciptypes.NewBigIntFromInt64(20_000)}, - }, - }, - expTransmittedReports: []cciptypes.CommitPluginReport{ - { - MerkleRoots: []cciptypes.MerkleRootChain{ - {ChainSel: chainB, SeqNumsRange: cciptypes.NewSeqNumRange(21, 22)}, - }, - PriceUpdates: cciptypes.PriceUpdates{ - TokenPriceUpdates: []cciptypes.TokenPrice{}, - GasPriceUpdates: []cciptypes.GasPriceChain{ - {ChainSel: chainA, GasPrice: cciptypes.NewBigIntFromInt64(1000)}, - {ChainSel: chainB, GasPrice: cciptypes.NewBigIntFromInt64(20_000)}, - }, - }, - }, - }, - initialOutcome: cciptypes.CommitPluginOutcome{ - MaxSeqNums: []cciptypes.SeqNumChain{ - {ChainSel: chainA, SeqNum: 10}, - {ChainSel: chainB, SeqNum: 20}, - }, - MerkleRoots: []cciptypes.MerkleRootChain{}, - TokenPrices: []cciptypes.TokenPrice{}, - GasPrices: []cciptypes.GasPriceChain{}, - }, - }, - { - name: "NodesDoNotAgreeOnMsgs", - description: "Nodes do not agree on messages which leads to an outcome with empty merkle roots.", - nodes: setupNodesDoNotAgreeOnMsgs(ctx, t, lggr), - expOutcome: cciptypes.CommitPluginOutcome{ - MaxSeqNums: []cciptypes.SeqNumChain{ - {ChainSel: chainA, SeqNum: 10}, - {ChainSel: chainB, SeqNum: 20}, - }, - MerkleRoots: []cciptypes.MerkleRootChain{}, - TokenPrices: []cciptypes.TokenPrice{}, - GasPrices: []cciptypes.GasPriceChain{ - {ChainSel: chainA, GasPrice: cciptypes.NewBigIntFromInt64(1000)}, - {ChainSel: chainB, GasPrice: cciptypes.NewBigIntFromInt64(20_000)}, - }, - }, - expTransmittedReports: []cciptypes.CommitPluginReport{ - { - MerkleRoots: []cciptypes.MerkleRootChain{}, - PriceUpdates: cciptypes.PriceUpdates{ - TokenPriceUpdates: []cciptypes.TokenPrice{}, - GasPriceUpdates: []cciptypes.GasPriceChain{ - {ChainSel: chainA, GasPrice: cciptypes.NewBigIntFromInt64(1000)}, - {ChainSel: chainB, GasPrice: cciptypes.NewBigIntFromInt64(20_000)}, - }, - }, - }, - }, - initialOutcome: cciptypes.CommitPluginOutcome{ - MaxSeqNums: []cciptypes.SeqNumChain{ - {ChainSel: chainA, SeqNum: 10}, - {ChainSel: chainB, SeqNum: 20}, - }, - MerkleRoots: []cciptypes.MerkleRootChain{}, - TokenPrices: []cciptypes.TokenPrice{}, - GasPrices: []cciptypes.GasPriceChain{}, - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - t.Log("-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-") - t.Logf(">>> [%s]\n", tc.name) - t.Logf(">>> %s\n", tc.description) - defer t.Log("-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-") - - nodesSetup := tc.nodes - nodes := make([]ocr3types.ReportingPlugin[[]byte], 0, len(nodesSetup)) - for _, n := range nodesSetup { - nodes = append(nodes, n.node) - } - - nodeIDs := make([]commontypes.OracleID, 0, len(nodesSetup)) - for _, n := range nodesSetup { - nodeIDs = append(nodeIDs, n.node.nodeID) - } - o, err := tc.initialOutcome.Encode() - require.NoError(t, err) - runner := testhelpers.NewOCR3Runner(nodes, nodeIDs, o) - - res, err := runner.RunRound(ctx) - if tc.expErr != nil { - tc.expErr(t, err) - } else { - assert.NoError(t, err) - } - - if !reflect.DeepEqual(tc.expOutcome, cciptypes.CommitPluginOutcome{}) { - outcome, err := cciptypes.DecodeCommitPluginOutcome(res.Outcome) - assert.NoError(t, err) - assert.Equal(t, tc.expOutcome.TokenPrices, outcome.TokenPrices) - assert.Equal(t, tc.expOutcome.MaxSeqNums, outcome.MaxSeqNums) - assert.Equal(t, tc.expOutcome.GasPrices, outcome.GasPrices) - - assert.Equal(t, len(tc.expOutcome.MerkleRoots), len(outcome.MerkleRoots)) - for i, exp := range tc.expOutcome.MerkleRoots { - assert.Equal(t, exp.ChainSel, outcome.MerkleRoots[i].ChainSel) - assert.Equal(t, exp.SeqNumsRange, outcome.MerkleRoots[i].SeqNumsRange) - } - } - - assert.Equal(t, len(tc.expTransmittedReports), len(res.Transmitted)) - for i, exp := range tc.expTransmittedReports { - actual, err := nodesSetup[0].reportCodec.Decode(ctx, res.Transmitted[i].Report) - assert.NoError(t, err) - assert.Equal(t, exp.PriceUpdates, actual.PriceUpdates) - assert.Equal(t, len(exp.MerkleRoots), len(actual.MerkleRoots)) - for j, expRoot := range exp.MerkleRoots { - assert.Equal(t, expRoot.ChainSel, actual.MerkleRoots[j].ChainSel) - assert.Equal(t, expRoot.SeqNumsRange, actual.MerkleRoots[j].SeqNumsRange) - } - } - }) - } -} - -func setupEmptyOutcome(ctx context.Context, t *testing.T, lggr logger.Logger) []nodeSetup { - cfg := cciptypes.CommitPluginConfig{ - DestChain: chainC, - PricedTokens: []types.Account{tokenX}, - TokenPricesObserver: false, - NewMsgScanBatchSize: 256, - } - - chainConfigInfos := []reader.ChainConfigInfo{ - { - ChainSelector: chainC, - ChainConfig: reader.HomeChainConfigMapper{ - FChain: 1, - Readers: []libocrtypes.PeerID{ - {1}, {2}, {3}, - }, - Config: []byte{0}, - }, - }, - } - - homeChain := setupHomeChainPoller(lggr, chainConfigInfos) - err := homeChain.Start(ctx) - if err != nil { - return nil - } - - oracleIDToP2pID := GetP2pIDs(1, 2, 3) - nodes := []nodeSetup{ - newNode(ctx, t, lggr, 1, cfg, homeChain, oracleIDToP2pID), - newNode(ctx, t, lggr, 2, cfg, homeChain, oracleIDToP2pID), - newNode(ctx, t, lggr, 3, cfg, homeChain, oracleIDToP2pID), - } - - for _, n := range nodes { - // All nodes have issue reading the latest sequence number, should lead to empty outcomes - n.ccipReader.On( - "NextSeqNum", - ctx, - mock.Anything, - ).Return([]cciptypes.SeqNum{}, nil) - } - - err = homeChain.Close() - if err != nil { - return nil - } - return nodes -} - -func setupAllNodesReadAllChains(ctx context.Context, t *testing.T, lggr logger.Logger) []nodeSetup { - cfg := cciptypes.CommitPluginConfig{ - DestChain: chainC, - PricedTokens: []types.Account{tokenX}, - TokenPricesObserver: false, - NewMsgScanBatchSize: 256, - } - - chainConfigInfos := []reader.ChainConfigInfo{ - { - ChainSelector: chainA, - ChainConfig: reader.HomeChainConfigMapper{ - FChain: 1, - Readers: []libocrtypes.PeerID{ - {1}, {2}, {3}, - }, - Config: []byte{0}, - }, - }, - { - ChainSelector: chainB, - ChainConfig: reader.HomeChainConfigMapper{ - FChain: 1, - Readers: []libocrtypes.PeerID{ - {1}, {2}, {3}, - }, - Config: []byte{0}, - }, - }, - { - ChainSelector: chainC, - ChainConfig: reader.HomeChainConfigMapper{ - FChain: 1, - Readers: []libocrtypes.PeerID{ - {1}, {2}, {3}, - }, - Config: []byte{0}, - }, - }, - } - - homeChain := setupHomeChainPoller(lggr, chainConfigInfos) - err := homeChain.Start(ctx) - if err != nil { - return nil - } - oracleIDToP2pID := GetP2pIDs(1, 2, 3) - n1 := newNode(ctx, t, lggr, 1, cfg, homeChain, oracleIDToP2pID) - n2 := newNode(ctx, t, lggr, 2, cfg, homeChain, oracleIDToP2pID) - n3 := newNode(ctx, t, lggr, 3, cfg, homeChain, oracleIDToP2pID) - nodes := []nodeSetup{n1, n2, n3} - - for _, n := range nodes { - // then they fetch new msgs, there is nothing new on chainA - n.ccipReader.On( - "MsgsBetweenSeqNums", - ctx, - chainA, - cciptypes.NewSeqNumRange(11, cciptypes.SeqNum(11+cfg.NewMsgScanBatchSize)), - ).Return([]cciptypes.CCIPMsg{}, nil) - - // and there are two new message on chainB - n.ccipReader.On( - "MsgsBetweenSeqNums", - ctx, - chainB, - cciptypes.NewSeqNumRange(21, cciptypes.SeqNum(21+cfg.NewMsgScanBatchSize)), - ).Return([]cciptypes.CCIPMsg{ - { - CCIPMsgBaseDetails: cciptypes.CCIPMsgBaseDetails{ - MsgHash: cciptypes.Bytes32{1}, ID: "1", SourceChain: chainB, SeqNum: 21, - }, - }, - { - CCIPMsgBaseDetails: cciptypes.CCIPMsgBaseDetails{ - MsgHash: cciptypes.Bytes32{2}, ID: "2", SourceChain: chainB, SeqNum: 22, - }, - }, - }, nil) - - n.ccipReader.On("GasPrices", ctx, []cciptypes.ChainSelector{chainA, chainB}). - Return([]cciptypes.BigInt{ - cciptypes.NewBigIntFromInt64(1000), - cciptypes.NewBigIntFromInt64(20_000), - }, nil) - - // all nodes observe the same sequence numbers 10 for chainA and 20 for chainB - n.ccipReader.On("NextSeqNum", ctx, []cciptypes.ChainSelector{chainA, chainB}). - Return([]cciptypes.SeqNum{10, 20}, nil) - - } - - // No need to keep it running in the background anymore for this test - err = homeChain.Close() - if err != nil { - return nil - } - - return nodes -} - -func setupNodesDoNotAgreeOnMsgs(ctx context.Context, t *testing.T, lggr logger.Logger) []nodeSetup { - cfg := cciptypes.CommitPluginConfig{ - DestChain: chainC, - PricedTokens: []types.Account{tokenX}, - TokenPricesObserver: false, - NewMsgScanBatchSize: 256, - } - - chainConfigInfos := []reader.ChainConfigInfo{ - { - ChainSelector: chainA, - ChainConfig: reader.HomeChainConfigMapper{ - FChain: 1, - Readers: []libocrtypes.PeerID{ - {1}, {2}, {3}, - }, - Config: []byte{0}, - }, - }, - { - ChainSelector: chainB, - ChainConfig: reader.HomeChainConfigMapper{ - FChain: 1, - Readers: []libocrtypes.PeerID{ - {1}, {2}, {3}, - }, - Config: []byte{0}, - }, - }, - { - ChainSelector: chainC, - ChainConfig: reader.HomeChainConfigMapper{ - FChain: 1, - Readers: []libocrtypes.PeerID{ - {1}, {2}, {3}, - }, - Config: []byte{0}, - }, - }, - } - - homeChain := setupHomeChainPoller(lggr, chainConfigInfos) - err := homeChain.Start(ctx) - if err != nil { - return nil - } - oracleIDToP2pID := GetP2pIDs(1, 2, 3) - n1 := newNode(ctx, t, lggr, 1, cfg, homeChain, oracleIDToP2pID) - n2 := newNode(ctx, t, lggr, 2, cfg, homeChain, oracleIDToP2pID) - n3 := newNode(ctx, t, lggr, 3, cfg, homeChain, oracleIDToP2pID) - nodes := []nodeSetup{n1, n2, n3} - - for i, n := range nodes { - // all nodes observe the same sequence numbers 10 for chainA and 20 for chainB - n.ccipReader.On("NextSeqNum", ctx, []cciptypes.ChainSelector{chainA, chainB}). - Return([]cciptypes.SeqNum{10, 20}, nil) - - // then they fetch new msgs, there is nothing new on chainA - n.ccipReader.On( - "MsgsBetweenSeqNums", - ctx, - chainA, - cciptypes.NewSeqNumRange(11, cciptypes.SeqNum(11+cfg.NewMsgScanBatchSize)), - ).Return([]cciptypes.CCIPMsg{}, nil) - - // and there are two new message on chainB - n.ccipReader.On( - "MsgsBetweenSeqNums", - ctx, - chainB, - cciptypes.NewSeqNumRange( - 21, - cciptypes.SeqNum(21+cfg.NewMsgScanBatchSize), - ), - ).Return([]cciptypes.CCIPMsg{ - {CCIPMsgBaseDetails: cciptypes.CCIPMsgBaseDetails{ - MsgHash: cciptypes.Bytes32{1}, - ID: "1" + strconv.Itoa(i), - SourceChain: chainB, - SeqNum: 21 + cciptypes.SeqNum(i*10)}}, - {CCIPMsgBaseDetails: cciptypes.CCIPMsgBaseDetails{ - MsgHash: cciptypes.Bytes32{2}, - ID: "2" + strconv.Itoa(i), - SourceChain: chainB, - SeqNum: 22 + cciptypes.SeqNum(i*20)}}, - }, nil) - - n.ccipReader.On("GasPrices", ctx, []cciptypes.ChainSelector{chainA, chainB}). - Return([]cciptypes.BigInt{ - cciptypes.NewBigIntFromInt64(1000), - cciptypes.NewBigIntFromInt64(20_000), - }, nil) - } - - // No need to keep it running in the background anymore for this test - err = homeChain.Close() - if err != nil { - return nil - } - - return nodes -} - -type nodeSetup struct { - node *Plugin - ccipReader *mocks.CCIPReader - priceReader *mocks.TokenPricesReader - reportCodec *mocks.CommitPluginJSONReportCodec - msgHasher *mocks.MessageHasher -} - -func newNode( - ctx context.Context, - t *testing.T, - lggr logger.Logger, - id int, - cfg cciptypes.CommitPluginConfig, - homeChain reader.HomeChain, - oracleIDToP2pID map[commontypes.OracleID]libocrtypes.PeerID, -) nodeSetup { - ccipReader := mocks.NewCCIPReader() - priceReader := mocks.NewTokenPricesReader() - reportCodec := mocks.NewCommitPluginJSONReportCodec() - msgHasher := mocks.NewMessageHasher() - - node1 := NewPlugin( - context.Background(), - commontypes.OracleID(id), - oracleIDToP2pID, - cfg, - ccipReader, - priceReader, - reportCodec, - msgHasher, - lggr, - homeChain, - ) - - return nodeSetup{ - node: node1, - ccipReader: ccipReader, - priceReader: priceReader, - reportCodec: reportCodec, - msgHasher: msgHasher, - } -} - -func setupHomeChainPoller(lggr logger.Logger, chainConfigInfos []reader.ChainConfigInfo) reader.HomeChain { - homeChainReader := mocks.NewContractReaderMock() - homeChainReader.On( - "GetLatestValue", mock.Anything, "CCIPCapabilityConfiguration", "getAllChainConfigs", mock.Anything, mock.Anything, - ).Run( - func(args mock.Arguments) { - arg := args.Get(4).(*[]reader.ChainConfigInfo) - *arg = chainConfigInfos - }).Return(nil) - - homeChain := reader.NewHomeChainConfigPoller( - homeChainReader, - lggr, - // to prevent linting error because of logging after finishing tests, we close the poller after each test, having - // lower polling interval make it catch up faster - 10*time.Millisecond, - ) - - return homeChain -} -func GetP2pIDs(ids ...int) map[commontypes.OracleID]libocrtypes.PeerID { - res := make(map[commontypes.OracleID]libocrtypes.PeerID) - for _, id := range ids { - res[commontypes.OracleID(id)] = libocrtypes.PeerID{byte(id)} - } - return res -} - -var ( - chainA = cciptypes.ChainSelector(1) - chainB = cciptypes.ChainSelector(2) - chainC = cciptypes.ChainSelector(3) - - tokenX = types.Account("tk_xxx") -) diff --git a/core/services/ocr3/plugins/ccip/commit/plugin_functions.go b/core/services/ocr3/plugins/ccip/commit/plugin_functions.go deleted file mode 100644 index 9f4c73faef..0000000000 --- a/core/services/ocr3/plugins/ccip/commit/plugin_functions.go +++ /dev/null @@ -1,598 +0,0 @@ -package commit - -import ( - "context" - "fmt" - "sort" - - mapset "github.com/deckarep/golang-set/v2" - "github.com/smartcontractkit/libocr/offchainreporting2plus/types" - "golang.org/x/sync/errgroup" - - "github.com/smartcontractkit/ccipocr3/internal/libs/slicelib" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" - - "github.com/smartcontractkit/chainlink-common/pkg/hashutil" - "github.com/smartcontractkit/chainlink-common/pkg/merklemulti" -) - -// observeLatestCommittedSeqNums finds the maximum committed sequence numbers for each source chain. -// If we cannot observe the dest we return an empty slice and no error. -func observeLatestCommittedSeqNums( - ctx context.Context, - lggr logger.Logger, - ccipReader cciptypes.CCIPReader, - readableChains mapset.Set[cciptypes.ChainSelector], - destChain cciptypes.ChainSelector, - knownSourceChains []cciptypes.ChainSelector, -) ([]cciptypes.SeqNumChain, error) { - sort.Slice(knownSourceChains, func(i, j int) bool { return knownSourceChains[i] < knownSourceChains[j] }) - latestCommittedSeqNumsObservation := make([]cciptypes.SeqNumChain, 0) - if readableChains.Contains(destChain) { - lggr.Debugw("reading latest committed sequence from destination") - onChainLatestCommittedSeqNums, err := ccipReader.NextSeqNum(ctx, knownSourceChains) - if err != nil { - return latestCommittedSeqNumsObservation, fmt.Errorf("get next seq nums: %w", err) - } - lggr.Debugw("observed latest committed sequence numbers on destination", - "latestCommittedSeqNumsObservation", onChainLatestCommittedSeqNums) - for i, ch := range knownSourceChains { - latestCommittedSeqNumsObservation = append( - latestCommittedSeqNumsObservation, - cciptypes.NewSeqNumChain(ch, onChainLatestCommittedSeqNums[i]), - ) - } - } - return latestCommittedSeqNumsObservation, nil -} - -// observeNewMsgs finds the new messages for each supported chain based on the provided max sequence numbers. -// If latestCommitSeqNums is empty (first ever OCR round), it will return an empty slice. -func observeNewMsgs( - ctx context.Context, - lggr logger.Logger, - ccipReader cciptypes.CCIPReader, - msgHasher cciptypes.MessageHasher, - readableChains mapset.Set[cciptypes.ChainSelector], - latestCommittedSeqNums []cciptypes.SeqNumChain, - msgScanBatchSize int, -) ([]cciptypes.CCIPMsg, error) { - // Find the new msgs for each supported chain based on the discovered max sequence numbers. - newMsgsPerChain := make([][]cciptypes.CCIPMsg, len(latestCommittedSeqNums)) - eg := new(errgroup.Group) - - for chainIdx, seqNumChain := range latestCommittedSeqNums { - if !readableChains.Contains(seqNumChain.ChainSel) { - lggr.Debugw("reading chain is not supported", "chain", seqNumChain.ChainSel) - continue - } - - seqNumChain := seqNumChain - chainIdx := chainIdx - eg.Go(func() error { - minSeqNum := seqNumChain.SeqNum + 1 - maxSeqNum := minSeqNum + cciptypes.SeqNum(msgScanBatchSize) - lggr.Debugw("scanning for new messages", - "chain", seqNumChain.ChainSel, "minSeqNum", minSeqNum, "maxSeqNum", maxSeqNum) - - newMsgs, err := ccipReader.MsgsBetweenSeqNums( - ctx, seqNumChain.ChainSel, cciptypes.NewSeqNumRange(minSeqNum, maxSeqNum)) - if err != nil { - return fmt.Errorf("get messages between seq nums: %w", err) - } - - if len(newMsgs) > 0 { - lggr.Debugw("discovered new messages", "chain", seqNumChain.ChainSel, "newMsgs", len(newMsgs)) - } else { - lggr.Debugw("no new messages discovered", "chain", seqNumChain.ChainSel) - } - - for i := range newMsgs { - h, err := msgHasher.Hash(ctx, newMsgs[i]) - if err != nil { - return fmt.Errorf("hash message: %w", err) - } - newMsgs[i].MsgHash = h // populate msgHash field - } - - newMsgsPerChain[chainIdx] = newMsgs - return nil - }) - } - - if err := eg.Wait(); err != nil { - return nil, fmt.Errorf("wait for new msg observations: %w", err) - } - - observedNewMsgs := make([]cciptypes.CCIPMsg, 0) - for chainIdx := range latestCommittedSeqNums { - observedNewMsgs = append(observedNewMsgs, newMsgsPerChain[chainIdx]...) - } - return observedNewMsgs, nil -} - -func observeTokenPrices( - ctx context.Context, - tokenPricesReader cciptypes.TokenPricesReader, - tokens []types.Account, -) ([]cciptypes.TokenPrice, error) { - tokenPrices, err := tokenPricesReader.GetTokenPricesUSD(ctx, tokens) - if err != nil { - return nil, fmt.Errorf("get token prices: %w", err) - } - - if len(tokenPrices) != len(tokens) { - return nil, fmt.Errorf("internal critical error token prices length mismatch: got %d, want %d", - len(tokenPrices), len(tokens)) - } - - tokenPricesUSD := make([]cciptypes.TokenPrice, 0, len(tokens)) - for i, token := range tokens { - tokenPricesUSD = append(tokenPricesUSD, cciptypes.NewTokenPrice(token, tokenPrices[i])) - } - - return tokenPricesUSD, nil -} - -func observeGasPrices( - ctx context.Context, - ccipReader cciptypes.CCIPReader, - chains []cciptypes.ChainSelector, -) ([]cciptypes.GasPriceChain, error) { - if len(chains) == 0 { - return nil, nil - } - - gasPrices, err := ccipReader.GasPrices(ctx, chains) - if err != nil { - return nil, fmt.Errorf("get gas prices: %w", err) - } - - if len(gasPrices) != len(chains) { - return nil, fmt.Errorf("internal critical error gas prices length mismatch: got %d, want %d", - len(gasPrices), len(chains)) - } - - gasPricesGwei := make([]cciptypes.GasPriceChain, 0, len(chains)) - for i, chain := range chains { - gasPricesGwei = append(gasPricesGwei, cciptypes.NewGasPriceChain(gasPrices[i].Int, chain)) - } - - return gasPricesGwei, nil -} - -// newMsgsConsensus comes in consensus on the observed messages for each source chain. Generates one merkle root -// for each source chain based on the consensus on the messages. -func newMsgsConsensus( - lggr logger.Logger, - maxSeqNums []cciptypes.SeqNumChain, - observations []cciptypes.CommitPluginObservation, - fChainCfg map[cciptypes.ChainSelector]int, -) ([]cciptypes.MerkleRootChain, error) { - maxSeqNumsPerChain := make(map[cciptypes.ChainSelector]cciptypes.SeqNum) - for _, seqNumChain := range maxSeqNums { - maxSeqNumsPerChain[seqNumChain.ChainSel] = seqNumChain.SeqNum - } - - // Gather all messages from all observations. - msgsFromObservations := make([]cciptypes.CCIPMsgBaseDetails, 0) - for _, obs := range observations { - msgsFromObservations = append(msgsFromObservations, obs.NewMsgs...) - } - lggr.Debugw("total observed messages across all followers", "msgs", len(msgsFromObservations)) - - // Filter out messages less than or equal to the max sequence numbers. - msgsFromObservations = slicelib.Filter(msgsFromObservations, func(msg cciptypes.CCIPMsgBaseDetails) bool { - maxSeqNum, ok := maxSeqNumsPerChain[msg.SourceChain] - if !ok { - return false - } - return msg.SeqNum > maxSeqNum - }) - lggr.Debugw("observed messages after filtering", "msgs", len(msgsFromObservations)) - - // Group messages by source chain. - sourceChains, groupedMsgs := slicelib.GroupBy( - msgsFromObservations, - func(msg cciptypes.CCIPMsgBaseDetails) cciptypes.ChainSelector { return msg.SourceChain }, - ) - - // Come to consensus on the observed messages by source chain. - consensusBySourceChain := make(map[cciptypes.ChainSelector]observedMsgsConsensus) - for _, sourceChain := range sourceChains { // note: we iterate using sourceChains slice for deterministic order. - observedMsgs, ok := groupedMsgs[sourceChain] - if !ok { - lggr.Panicw("source chain not found in grouped messages", "sourceChain", sourceChain) - } - - msgsConsensus, err := newMsgsConsensusForChain(lggr, sourceChain, observedMsgs, fChainCfg) - if err != nil { - return nil, fmt.Errorf("calculate observed msgs consensus: %w", err) - } - - if msgsConsensus.isEmpty() { - lggr.Debugw("no consensus on observed messages", "sourceChain", sourceChain) - continue - } - consensusBySourceChain[sourceChain] = msgsConsensus - lggr.Debugw("observed messages consensus", "sourceChain", sourceChain, "consensus", msgsConsensus) - } - - merkleRoots := make([]cciptypes.MerkleRootChain, 0) - for sourceChain, consensus := range consensusBySourceChain { - merkleRoots = append( - merkleRoots, - cciptypes.NewMerkleRootChain(sourceChain, consensus.seqNumRange, consensus.merkleRoot), - ) - } - - sort.Slice(merkleRoots, func(i, j int) bool { return merkleRoots[i].ChainSel < merkleRoots[j].ChainSel }) - return merkleRoots, nil -} - -// Given a list of observed msgs -// - Keep the messages that were observed by at least 2f_chain+1 followers. -// - Starting from the first message (min seq num), keep adding the messages to the merkle tree until a gap is found. -func newMsgsConsensusForChain( - lggr logger.Logger, - chainSel cciptypes.ChainSelector, - observedMsgs []cciptypes.CCIPMsgBaseDetails, - fChainCfg map[cciptypes.ChainSelector]int, -) (observedMsgsConsensus, error) { - fChain, ok := fChainCfg[chainSel] - if !ok { - return observedMsgsConsensus{}, fmt.Errorf("fchain not found for chain %d", chainSel) - } - lggr.Debugw("observed messages consensus", - "chain", chainSel, "fChain", fChain, "observedMsgs", len(observedMsgs)) - - // First come to consensus about the (sequence number, msg hash) pairs. - // For each sequence number consider the Hash with the most votes. - msgSeqNumToHashCounts := make(map[cciptypes.SeqNum]map[string]int) // seqNum -> msgHash -> count - for _, msg := range observedMsgs { - if _, exists := msgSeqNumToHashCounts[msg.SeqNum]; !exists { - msgSeqNumToHashCounts[msg.SeqNum] = make(map[string]int) - } - msgSeqNumToHashCounts[msg.SeqNum][msg.MsgHash.String()]++ - } - lggr.Debugw("observed message counts", "chain", chainSel, "msgSeqNumToHashCounts", msgSeqNumToHashCounts) - - msgObservationsCount := make(map[cciptypes.SeqNum]int) - msgSeqNumToHash := make(map[cciptypes.SeqNum]cciptypes.Bytes32) - for seqNum, hashCounts := range msgSeqNumToHashCounts { - if len(hashCounts) == 0 { - lggr.Fatalw("hash counts should never be empty", "seqNum", seqNum) - continue - } - - // Find the MsgHash with the most votes for each sequence number. - hashesSlice := make([]string, 0, len(hashCounts)) - for h := range hashCounts { - hashesSlice = append(hashesSlice, h) - } - // determinism in case we have the same count for different hashes - sort.Slice(hashesSlice, func(i, j int) bool { return hashesSlice[i] < hashesSlice[j] }) - - maxCnt := hashCounts[hashesSlice[0]] - mostVotedHash := hashesSlice[0] - for _, h := range hashesSlice[1:] { - cnt := hashCounts[h] - if cnt > maxCnt { - maxCnt = cnt - mostVotedHash = h - } - } - - msgObservationsCount[seqNum] = maxCnt - hashBytes, err := cciptypes.NewBytes32FromString(mostVotedHash) - if err != nil { - return observedMsgsConsensus{}, fmt.Errorf("critical issue converting hash '%s' to bytes32: %w", - mostVotedHash, err) - } - msgSeqNumToHash[seqNum] = hashBytes - } - lggr.Debugw("observed message consensus", "chain", chainSel, "msgSeqNumToHash", msgSeqNumToHash) - - // Filter out msgs not observed by at least 2f_chain+1 followers. - msgSeqNumsQuorum := mapset.NewSet[cciptypes.SeqNum]() - for seqNum, count := range msgObservationsCount { - if count >= 2*fChain+1 { - msgSeqNumsQuorum.Add(seqNum) - } - } - if msgSeqNumsQuorum.Cardinality() == 0 { - return observedMsgsConsensus{}, nil - } - - // Come to consensus on the observed messages sequence numbers range. - msgSeqNumsQuorumSlice := msgSeqNumsQuorum.ToSlice() - sort.Slice(msgSeqNumsQuorumSlice, func(i, j int) bool { return msgSeqNumsQuorumSlice[i] < msgSeqNumsQuorumSlice[j] }) - seqNumConsensusRange := cciptypes.NewSeqNumRange(msgSeqNumsQuorumSlice[0], msgSeqNumsQuorumSlice[0]) - for _, seqNum := range msgSeqNumsQuorumSlice[1:] { - if seqNum != seqNumConsensusRange.End()+1 { - break // Found a gap in the sequence numbers. - } - seqNumConsensusRange.SetEnd(seqNum) - } - - treeLeaves := make([][32]byte, 0) - for seqNum := seqNumConsensusRange.Start(); seqNum <= seqNumConsensusRange.End(); seqNum++ { - msgHash, ok := msgSeqNumToHash[seqNum] - if !ok { - return observedMsgsConsensus{}, fmt.Errorf("msg hash not found for seq num %d", seqNum) - } - treeLeaves = append(treeLeaves, msgHash) - } - - lggr.Debugw("constructing merkle tree", "chain", chainSel, "treeLeaves", len(treeLeaves)) - tree, err := merklemulti.NewTree(hashutil.NewKeccak(), treeLeaves) - if err != nil { - return observedMsgsConsensus{}, fmt.Errorf("construct merkle tree from %d leaves: %w", len(treeLeaves), err) - } - - return observedMsgsConsensus{ - seqNumRange: seqNumConsensusRange, - merkleRoot: tree.Root(), - }, nil -} - -// maxSeqNumsConsensus groups the observed max seq nums across all followers per chain. -// Orders the sequence numbers and selects the one at the index of destination chain fChain. -// -// For example: -// -// seqNums: [1, 1, 1, 10, 10, 10, 10, 10, 10] -// fChain: 4 -// result: 10 -// -// Selecting seqNums[fChain] ensures: -// - At least one honest node has seen this value, so adversary cannot bias the value lower which would cause reverts -// - If an honest oracle reports sorted_min[f] which happens to be stale i.e. that oracle has a delayed view -// of the chain, then the report will revert onchain but still succeed upon retry -// - We minimize the risk of naturally hitting the error condition minSeqNum > maxSeqNum due to oracles -// delayed views of the chain (would be an issue with taking sorted_mins[-f]) -func maxSeqNumsConsensus( - lggr logger.Logger, fChain int, observations []cciptypes.CommitPluginObservation, -) []cciptypes.SeqNumChain { - observedSeqNumsPerChain := make(map[cciptypes.ChainSelector][]cciptypes.SeqNum) - for _, obs := range observations { - for _, maxSeqNum := range obs.MaxSeqNums { - if _, exists := observedSeqNumsPerChain[maxSeqNum.ChainSel]; !exists { - observedSeqNumsPerChain[maxSeqNum.ChainSel] = make([]cciptypes.SeqNum, 0) - } - observedSeqNumsPerChain[maxSeqNum.ChainSel] = - append(observedSeqNumsPerChain[maxSeqNum.ChainSel], maxSeqNum.SeqNum) - } - } - - seqNums := make([]cciptypes.SeqNumChain, 0, len(observedSeqNumsPerChain)) - for ch, observedSeqNums := range observedSeqNumsPerChain { - if len(observedSeqNums) < 2*fChain+1 { - lggr.Warnw("not enough observations for chain", "chain", ch, "observedSeqNums", observedSeqNums) - continue - } - - sort.Slice(observedSeqNums, func(i, j int) bool { return observedSeqNums[i] < observedSeqNums[j] }) - seqNums = append(seqNums, cciptypes.NewSeqNumChain(ch, observedSeqNums[fChain])) - } - - sort.Slice(seqNums, func(i, j int) bool { return seqNums[i].ChainSel < seqNums[j].ChainSel }) - return seqNums -} - -// tokenPricesConsensus returns the median price for tokens that have at least 2f_chain+1 observations. -func tokenPricesConsensus( - observations []cciptypes.CommitPluginObservation, - fChain int, -) ([]cciptypes.TokenPrice, error) { - pricesPerToken := make(map[types.Account][]cciptypes.BigInt) - for _, obs := range observations { - for _, price := range obs.TokenPrices { - if _, exists := pricesPerToken[price.TokenID]; !exists { - pricesPerToken[price.TokenID] = make([]cciptypes.BigInt, 0) - } - pricesPerToken[price.TokenID] = append(pricesPerToken[price.TokenID], price.Price) - } - } - - // Keep the median - consensusPrices := make([]cciptypes.TokenPrice, 0) - for token, prices := range pricesPerToken { - if len(prices) < 2*fChain+1 { - continue - } - consensusPrices = append(consensusPrices, cciptypes.NewTokenPrice(token, slicelib.BigIntSortedMiddle(prices).Int)) - } - - sort.Slice(consensusPrices, func(i, j int) bool { return consensusPrices[i].TokenID < consensusPrices[j].TokenID }) - return consensusPrices, nil -} - -func gasPricesConsensus( - lggr logger.Logger, observations []cciptypes.CommitPluginObservation, fChain int, -) []cciptypes.GasPriceChain { - // Group the observed gas prices by chain. - gasPricePerChain := make(map[cciptypes.ChainSelector][]cciptypes.BigInt) - for _, obs := range observations { - for _, gasPrice := range obs.GasPrices { - if _, exists := gasPricePerChain[gasPrice.ChainSel]; !exists { - gasPricePerChain[gasPrice.ChainSel] = make([]cciptypes.BigInt, 0) - } - gasPricePerChain[gasPrice.ChainSel] = append(gasPricePerChain[gasPrice.ChainSel], gasPrice.GasPrice) - } - } - - // Keep the median - consensusGasPrices := make([]cciptypes.GasPriceChain, 0) - for chain, gasPrices := range gasPricePerChain { - if len(gasPrices) < 2*fChain+1 { - lggr.Warnw("not enough gas price observations", "chain", chain, "gasPrices", gasPrices) - continue - } - - consensusGasPrices = append( - consensusGasPrices, - cciptypes.NewGasPriceChain(slicelib.BigIntSortedMiddle(gasPrices).Int, chain), - ) - } - - sort.Slice( - consensusGasPrices, - func(i, j int) bool { return consensusGasPrices[i].ChainSel < consensusGasPrices[j].ChainSel }, - ) - return consensusGasPrices -} - -// fChainConsensus comes to consensus on the plugin config based on the observations. -// We cannot trust the state of a single follower, so we need to come to consensus on the config. -func fChainConsensus( - observations []cciptypes.CommitPluginObservation, // observations from all followers -) map[cciptypes.ChainSelector]int { - // Come to consensus on fChain. - // Use the fChain observed by most followers for each chain. - fChainCounts := make(map[cciptypes.ChainSelector]map[int]int) // {chain: {fChain: count}} - for _, obs := range observations { - for chain, fChain := range obs.FChain { - if _, exists := fChainCounts[chain]; !exists { - fChainCounts[chain] = make(map[int]int) - } - fChainCounts[chain][fChain]++ - } - } - consensusFChain := make(map[cciptypes.ChainSelector]int) - for chain, counts := range fChainCounts { - maxCount := 0 - for fChain, count := range counts { - if count > maxCount { - maxCount = count - consensusFChain[chain] = fChain - } - } - } - - return consensusFChain -} - -// validateObservedSequenceNumbers checks if the sequence numbers of the provided messages are unique for each chain and -// that they match the observed max sequence numbers. -func validateObservedSequenceNumbers(msgs []cciptypes.CCIPMsgBaseDetails, maxSeqNums []cciptypes.SeqNumChain) error { - // If the observer did not include sequence numbers it means that it's not a destination chain reader. - // In that case we cannot do any msg sequence number validations. - if len(maxSeqNums) == 0 { - return nil - } - - // MaxSeqNums must be unique for each chain. - maxSeqNumsMap := make(map[cciptypes.ChainSelector]cciptypes.SeqNum) - for _, maxSeqNum := range maxSeqNums { - if _, exists := maxSeqNumsMap[maxSeqNum.ChainSel]; exists { - return fmt.Errorf("duplicate max sequence number for chain %d", maxSeqNum.ChainSel) - } - maxSeqNumsMap[maxSeqNum.ChainSel] = maxSeqNum.SeqNum - } - - seqNums := make(map[cciptypes.ChainSelector]mapset.Set[cciptypes.SeqNum], len(msgs)) - hashes := mapset.NewSet[string]() - for _, msg := range msgs { - if msg.MsgHash.IsEmpty() { - return fmt.Errorf("observed msg hash must not be empty") - } - - if _, exists := seqNums[msg.SourceChain]; !exists { - seqNums[msg.SourceChain] = mapset.NewSet[cciptypes.SeqNum]() - } - - // The same sequence number must not appear more than once for the same chain and must be valid. - if seqNums[msg.SourceChain].Contains(msg.SeqNum) { - return fmt.Errorf("duplicate sequence number %d for chain %d", msg.SeqNum, msg.SourceChain) - } - seqNums[msg.SourceChain].Add(msg.SeqNum) - - // The observed msg hash cannot appear twice for different msgs. - if hashes.Contains(msg.MsgHash.String()) { - return fmt.Errorf("duplicate msg hash %s", msg.MsgHash.String()) - } - hashes.Add(msg.MsgHash.String()) - - // The observed msg sequence number cannot be less than or equal to the max observed sequence number. - maxSeqNum, exists := maxSeqNumsMap[msg.SourceChain] - if !exists { - return fmt.Errorf("max sequence number observation not found for chain %d", msg.SourceChain) - } - if msg.SeqNum <= maxSeqNum { - return fmt.Errorf("max sequence number %d must be greater than observed sequence number %d for chain %d", - maxSeqNum, msg.SeqNum, msg.SourceChain) - } - } - - return nil -} - -// validateObserverReadingEligibility checks if the observer is eligible to observe the messages it observed. -func validateObserverReadingEligibility( - msgs []cciptypes.CCIPMsgBaseDetails, - seqNums []cciptypes.SeqNumChain, - nodeSupportedChains mapset.Set[cciptypes.ChainSelector], - destChain cciptypes.ChainSelector, -) error { - - if len(seqNums) > 0 && !nodeSupportedChains.Contains(destChain) { - return fmt.Errorf("observer must be a writer if it observes sequence numbers") - } - - if len(msgs) == 0 { - return nil - } - - for _, msg := range msgs { - // Observer must be able to read the chain that the message is coming from. - if !nodeSupportedChains.Contains(msg.SourceChain) { - return fmt.Errorf("observer not allowed to read chain %d", msg.SourceChain) - } - } - - return nil -} - -func validateObservedTokenPrices(tokenPrices []cciptypes.TokenPrice) error { - tokensWithPrice := mapset.NewSet[types.Account]() - for _, t := range tokenPrices { - if tokensWithPrice.Contains(t.TokenID) { - return fmt.Errorf("duplicate token price for token: %s", t.TokenID) - } - tokensWithPrice.Add(t.TokenID) - - if t.Price.IsEmpty() { - return fmt.Errorf("token price must not be empty") - } - } - - return nil -} - -func validateObservedGasPrices(gasPrices []cciptypes.GasPriceChain) error { - // Duplicate gas prices must not appear for the same chain and must not be empty. - gasPriceChains := mapset.NewSet[cciptypes.ChainSelector]() - for _, g := range gasPrices { - if gasPriceChains.Contains(g.ChainSel) { - return fmt.Errorf("duplicate gas price for chain %d", g.ChainSel) - } - gasPriceChains.Add(g.ChainSel) - if g.GasPrice.IsEmpty() { - return fmt.Errorf("gas price must not be empty") - } - } - - return nil -} - -type observedMsgsConsensus struct { - seqNumRange cciptypes.SeqNumRange - merkleRoot [32]byte -} - -func (o observedMsgsConsensus) isEmpty() bool { - return o.seqNumRange.Start() == 0 && o.seqNumRange.End() == 0 && o.merkleRoot == [32]byte{} -} diff --git a/core/services/ocr3/plugins/ccip/commit/plugin_functions_test.go b/core/services/ocr3/plugins/ccip/commit/plugin_functions_test.go deleted file mode 100644 index 31785fbbbf..0000000000 --- a/core/services/ocr3/plugins/ccip/commit/plugin_functions_test.go +++ /dev/null @@ -1,1190 +0,0 @@ -package commit - -import ( - "context" - "math/big" - "slices" - "strconv" - "testing" - "time" - - mapset "github.com/deckarep/golang-set/v2" - libocrtypes "github.com/smartcontractkit/libocr/ragep2p/types" - - "github.com/smartcontractkit/ccipocr3/internal/libs/slicelib" - "github.com/smartcontractkit/ccipocr3/internal/mocks" - - cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" - "github.com/smartcontractkit/libocr/offchainreporting2plus/types" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" -) - -func Test_observeMaxSeqNumsPerChain(t *testing.T) { - testCases := []struct { - name string - prevOutcome cciptypes.CommitPluginOutcome - onChainSeqNums map[cciptypes.ChainSelector]cciptypes.SeqNum - readChains []cciptypes.ChainSelector - destChain cciptypes.ChainSelector - expErr bool - expSeqNumsInSync bool - expMaxSeqNums []cciptypes.SeqNumChain - }{ - { - name: "report on chain seq num and can read dest", - onChainSeqNums: map[cciptypes.ChainSelector]cciptypes.SeqNum{ - 1: 10, - 2: 20, - }, - readChains: []cciptypes.ChainSelector{1, 2, 3}, - destChain: 3, - expErr: false, - expMaxSeqNums: []cciptypes.SeqNumChain{ - {ChainSel: 1, SeqNum: 10}, - {ChainSel: 2, SeqNum: 20}, - }, - }, - { - name: "cannot read dest", - prevOutcome: cciptypes.CommitPluginOutcome{ - MaxSeqNums: []cciptypes.SeqNumChain{ - {ChainSel: 1, SeqNum: 11}, // for chain 1 previous outcome is higher than on-chain state - {ChainSel: 2, SeqNum: 19}, // for chain 2 previous outcome is behind on-chain state - }, - }, - onChainSeqNums: map[cciptypes.ChainSelector]cciptypes.SeqNum{ - 1: 10, - 2: 20, - }, - readChains: []cciptypes.ChainSelector{1, 2}, - destChain: 3, - expErr: false, - expMaxSeqNums: []cciptypes.SeqNumChain{}, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - ctx := context.Background() - mockReader := mocks.NewCCIPReader() - knownSourceChains := slicelib.Filter( - tc.readChains, - func(ch cciptypes.ChainSelector) bool { return ch != tc.destChain }, - ) - lggr := logger.Test(t) - - onChainSeqNums := make([]cciptypes.SeqNum, 0) - for _, chain := range knownSourceChains { - if v, ok := tc.onChainSeqNums[chain]; !ok { - t.Fatalf("invalid test case missing on chain seq num expectation for %d", chain) - } else { - onChainSeqNums = append(onChainSeqNums, v) - } - } - mockReader.On("NextSeqNum", ctx, knownSourceChains).Return(onChainSeqNums, nil) - - seqNums, err := observeLatestCommittedSeqNums( - ctx, - lggr, - mockReader, - mapset.NewSet(tc.readChains...), - tc.destChain, - knownSourceChains, - ) - - if tc.expErr { - assert.Error(t, err) - return - } - assert.NoError(t, err) - assert.Equal(t, tc.expMaxSeqNums, seqNums) - }) - } -} - -func Test_observeNewMsgs(t *testing.T) { - testCases := []struct { - name string - maxSeqNumsPerChain []cciptypes.SeqNumChain - readChains []cciptypes.ChainSelector - destChain cciptypes.ChainSelector - msgScanBatchSize int - newMsgs map[cciptypes.ChainSelector][]cciptypes.CCIPMsg - expMsgs []cciptypes.CCIPMsg - expErr bool - }{ - { - name: "no new messages", - maxSeqNumsPerChain: []cciptypes.SeqNumChain{ - {ChainSel: 1, SeqNum: 10}, - {ChainSel: 2, SeqNum: 20}, - }, - readChains: []cciptypes.ChainSelector{1, 2}, - msgScanBatchSize: 256, - newMsgs: map[cciptypes.ChainSelector][]cciptypes.CCIPMsg{ - 1: {}, - 2: {}, - }, - expMsgs: []cciptypes.CCIPMsg{}, - expErr: false, - }, - { - name: "new messages", - maxSeqNumsPerChain: []cciptypes.SeqNumChain{ - {ChainSel: 1, SeqNum: 10}, - {ChainSel: 2, SeqNum: 20}, - }, - readChains: []cciptypes.ChainSelector{1, 2}, - msgScanBatchSize: 256, - newMsgs: map[cciptypes.ChainSelector][]cciptypes.CCIPMsg{ - 1: { - {CCIPMsgBaseDetails: cciptypes.CCIPMsgBaseDetails{ID: "1", SourceChain: 1, SeqNum: 11}}, - }, - 2: { - {CCIPMsgBaseDetails: cciptypes.CCIPMsgBaseDetails{ID: "2", SourceChain: 2, SeqNum: 21}}, - {CCIPMsgBaseDetails: cciptypes.CCIPMsgBaseDetails{ID: "3", SourceChain: 2, SeqNum: 22}}, - }, - }, - expMsgs: []cciptypes.CCIPMsg{ - {CCIPMsgBaseDetails: cciptypes.CCIPMsgBaseDetails{ID: "1", SourceChain: 1, SeqNum: 11}}, - {CCIPMsgBaseDetails: cciptypes.CCIPMsgBaseDetails{ID: "2", SourceChain: 2, SeqNum: 21}}, - {CCIPMsgBaseDetails: cciptypes.CCIPMsgBaseDetails{ID: "3", SourceChain: 2, SeqNum: 22}}, - }, - expErr: false, - }, - { - name: "new messages but one chain is not readable", - maxSeqNumsPerChain: []cciptypes.SeqNumChain{ - {ChainSel: 1, SeqNum: 10}, - {ChainSel: 2, SeqNum: 20}, - }, - readChains: []cciptypes.ChainSelector{2}, - msgScanBatchSize: 256, - newMsgs: map[cciptypes.ChainSelector][]cciptypes.CCIPMsg{ - 2: { - {CCIPMsgBaseDetails: cciptypes.CCIPMsgBaseDetails{ID: "2", SourceChain: 2, SeqNum: 21}}, - {CCIPMsgBaseDetails: cciptypes.CCIPMsgBaseDetails{ID: "3", SourceChain: 2, SeqNum: 22}}, - }, - }, - expMsgs: []cciptypes.CCIPMsg{ - {CCIPMsgBaseDetails: cciptypes.CCIPMsgBaseDetails{ID: "2", SourceChain: 2, SeqNum: 21}}, - {CCIPMsgBaseDetails: cciptypes.CCIPMsgBaseDetails{ID: "3", SourceChain: 2, SeqNum: 22}}, - }, - expErr: false, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - ctx := context.Background() - mockReader := mocks.NewCCIPReader() - msgHasher := mocks.NewMessageHasher() - for i := range tc.expMsgs { // make sure the hashes are populated - h, err := msgHasher.Hash(ctx, tc.expMsgs[i]) - assert.NoError(t, err) - tc.expMsgs[i].MsgHash = h - } - - lggr := logger.Test(t) - - for _, seqNumChain := range tc.maxSeqNumsPerChain { - if slices.Contains(tc.readChains, seqNumChain.ChainSel) { - mockReader.On( - "MsgsBetweenSeqNums", - ctx, - seqNumChain.ChainSel, - cciptypes.NewSeqNumRange(seqNumChain.SeqNum+1, seqNumChain.SeqNum+cciptypes.SeqNum(1+tc.msgScanBatchSize)), - ).Return(tc.newMsgs[seqNumChain.ChainSel], nil) - } - } - - msgs, err := observeNewMsgs( - ctx, - lggr, - mockReader, - msgHasher, - mapset.NewSet(tc.readChains...), - tc.maxSeqNumsPerChain, - tc.msgScanBatchSize, - ) - if tc.expErr { - assert.Error(t, err) - return - } - assert.NoError(t, err) - assert.Equal(t, tc.expMsgs, msgs) - mockReader.AssertExpectations(t) - }) - } -} - -func Benchmark_observeNewMsgs(b *testing.B) { - const ( - numChains = 5 - readerDelayMS = 100 - newMsgsPerChain = 256 - ) - - readChains := make([]cciptypes.ChainSelector, numChains) - maxSeqNumsPerChain := make([]cciptypes.SeqNumChain, numChains) - for i := 0; i < numChains; i++ { - readChains[i] = cciptypes.ChainSelector(i + 1) - maxSeqNumsPerChain[i] = cciptypes.SeqNumChain{ChainSel: cciptypes.ChainSelector(i + 1), SeqNum: cciptypes.SeqNum(1)} - } - - for i := 0; i < b.N; i++ { - ctx := context.Background() - lggr, _ := logger.New() - ccipReader := mocks.NewCCIPReader() - msgHasher := mocks.NewMessageHasher() - - expNewMsgs := make([]cciptypes.CCIPMsg, 0, newMsgsPerChain*numChains) - for _, seqNumChain := range maxSeqNumsPerChain { - newMsgs := make([]cciptypes.CCIPMsg, 0, newMsgsPerChain) - for msgSeqNum := 1; msgSeqNum <= newMsgsPerChain; msgSeqNum++ { - newMsgs = append(newMsgs, cciptypes.CCIPMsg{ - CCIPMsgBaseDetails: cciptypes.CCIPMsgBaseDetails{ - ID: strconv.Itoa(msgSeqNum), - SourceChain: seqNumChain.ChainSel, - SeqNum: cciptypes.SeqNum(msgSeqNum), - }, - }) - } - - ccipReader.On( - "MsgsBetweenSeqNums", - ctx, - []cciptypes.ChainSelector{seqNumChain.ChainSel}, - cciptypes.NewSeqNumRange( - seqNumChain.SeqNum+1, - seqNumChain.SeqNum+cciptypes.SeqNum(1+newMsgsPerChain), - ), - ).Run(func(args mock.Arguments) { - time.Sleep(time.Duration(readerDelayMS) * time.Millisecond) - }).Return(newMsgs, nil) - expNewMsgs = append(expNewMsgs, newMsgs...) - } - - msgs, err := observeNewMsgs( - ctx, - lggr, - ccipReader, - msgHasher, - mapset.NewSet(readChains...), - maxSeqNumsPerChain, - newMsgsPerChain, - ) - assert.NoError(b, err) - assert.Equal(b, expNewMsgs, msgs) - - // (old) sequential: 509.345 ms/op (numChains * readerDelayMS) - // (current) parallel: 102.543 ms/op (readerDelayMS) - } -} - -func Test_observeTokenPrices(t *testing.T) { - ctx := context.Background() - - t.Run("happy path", func(t *testing.T) { - priceReader := mocks.NewTokenPricesReader() - tokens := []types.Account{"0x1", "0x2", "0x3"} - mockPrices := []*big.Int{big.NewInt(10), big.NewInt(20), big.NewInt(30)} - priceReader.On("GetTokenPricesUSD", ctx, tokens).Return(mockPrices, nil) - prices, err := observeTokenPrices(ctx, priceReader, tokens) - assert.NoError(t, err) - assert.Equal(t, []cciptypes.TokenPrice{ - cciptypes.NewTokenPrice("0x1", big.NewInt(10)), - cciptypes.NewTokenPrice("0x2", big.NewInt(20)), - cciptypes.NewTokenPrice("0x3", big.NewInt(30)), - }, prices) - }) - - t.Run("price reader internal issue", func(t *testing.T) { - priceReader := mocks.NewTokenPricesReader() - tokens := []types.Account{"0x1", "0x2", "0x3"} - mockPrices := []*big.Int{big.NewInt(10), big.NewInt(20)} // returned two prices for three tokens - priceReader.On("GetTokenPricesUSD", ctx, tokens).Return(mockPrices, nil) - _, err := observeTokenPrices(ctx, priceReader, tokens) - assert.Error(t, err) - }) - -} - -func Test_observeGasPrices(t *testing.T) { - ctx := context.Background() - - t.Run("happy path", func(t *testing.T) { - mockReader := mocks.NewCCIPReader() - chains := []cciptypes.ChainSelector{1, 2, 3} - mockGasPrices := []cciptypes.BigInt{ - cciptypes.NewBigIntFromInt64(10), - cciptypes.NewBigIntFromInt64(20), - cciptypes.NewBigIntFromInt64(30), - } - mockReader.On("GasPrices", ctx, chains).Return(mockGasPrices, nil) - gasPrices, err := observeGasPrices(ctx, mockReader, chains) - assert.NoError(t, err) - assert.Equal(t, []cciptypes.GasPriceChain{ - cciptypes.NewGasPriceChain(mockGasPrices[0].Int, chains[0]), - cciptypes.NewGasPriceChain(mockGasPrices[1].Int, chains[1]), - cciptypes.NewGasPriceChain(mockGasPrices[2].Int, chains[2]), - }, gasPrices) - }) - - t.Run("gas reader internal issue", func(t *testing.T) { - mockReader := mocks.NewCCIPReader() - chains := []cciptypes.ChainSelector{1, 2, 3} - mockGasPrices := []cciptypes.BigInt{ - cciptypes.NewBigIntFromInt64(10), - cciptypes.NewBigIntFromInt64(20), - } // return 2 prices for 3 chains - mockReader.On("GasPrices", ctx, chains).Return(mockGasPrices, nil) - _, err := observeGasPrices(ctx, mockReader, chains) - assert.Error(t, err) - }) -} - -func Test_validateObservedSequenceNumbers(t *testing.T) { - testCases := []struct { - name string - msgs []cciptypes.CCIPMsgBaseDetails - maxSeqNums []cciptypes.SeqNumChain - expErr bool - }{ - { - name: "empty", - msgs: nil, - maxSeqNums: nil, - expErr: false, - }, - { - name: "dup seq num observation", - msgs: nil, - maxSeqNums: []cciptypes.SeqNumChain{ - {ChainSel: 1, SeqNum: 10}, - {ChainSel: 2, SeqNum: 20}, - {ChainSel: 1, SeqNum: 10}, - }, - expErr: true, - }, - { - name: "seq nums ok", - msgs: nil, - maxSeqNums: []cciptypes.SeqNumChain{ - {ChainSel: 1, SeqNum: 10}, - {ChainSel: 2, SeqNum: 20}, - }, - expErr: false, - }, - { - name: "dup msg seq num", - msgs: []cciptypes.CCIPMsgBaseDetails{ - {ID: "1", SourceChain: 1, SeqNum: 12}, - {ID: "1", SourceChain: 1, SeqNum: 13}, - {ID: "1", SourceChain: 1, SeqNum: 14}, - {ID: "1", SourceChain: 1, SeqNum: 13}, // dup - }, - maxSeqNums: []cciptypes.SeqNumChain{ - {ChainSel: 1, SeqNum: 10}, - {ChainSel: 2, SeqNum: 20}, - }, - expErr: true, - }, - { - name: "msg seq nums ok", - msgs: []cciptypes.CCIPMsgBaseDetails{ - {MsgHash: cciptypes.Bytes32{1}, ID: "1", SourceChain: 1, SeqNum: 12}, - {MsgHash: cciptypes.Bytes32{2}, ID: "1", SourceChain: 1, SeqNum: 13}, - {MsgHash: cciptypes.Bytes32{3}, ID: "1", SourceChain: 1, SeqNum: 14}, - {MsgHash: cciptypes.Bytes32{4}, ID: "1", SourceChain: 2, SeqNum: 21}, - }, - maxSeqNums: []cciptypes.SeqNumChain{ - {ChainSel: 1, SeqNum: 10}, - {ChainSel: 2, SeqNum: 20}, - }, - expErr: false, - }, - { - name: "msg seq nums does not match observed max seq num", - msgs: []cciptypes.CCIPMsgBaseDetails{ - {ID: "1", SourceChain: 1, SeqNum: 12}, - {ID: "1", SourceChain: 1, SeqNum: 13}, - {ID: "1", SourceChain: 1, SeqNum: 10}, // max seq num is already 10 - {ID: "1", SourceChain: 2, SeqNum: 21}, - }, - maxSeqNums: []cciptypes.SeqNumChain{ - {ChainSel: 1, SeqNum: 10}, - {ChainSel: 2, SeqNum: 20}, - }, - expErr: true, - }, - { - name: "max seq num not found", - msgs: []cciptypes.CCIPMsgBaseDetails{ - {ID: "1", SourceChain: 1, SeqNum: 12}, - {ID: "1", SourceChain: 1, SeqNum: 13}, - {ID: "1", SourceChain: 1, SeqNum: 14}, - {ID: "1", SourceChain: 2, SeqNum: 21}, // max seq num not reported - }, - maxSeqNums: []cciptypes.SeqNumChain{ - {ChainSel: 1, SeqNum: 10}, - }, - expErr: true, - }, - { - name: "msg hashes ok", - msgs: []cciptypes.CCIPMsgBaseDetails{ - {MsgHash: cciptypes.Bytes32{123}, ID: "1", SourceChain: 1, SeqNum: 12}, - {MsgHash: cciptypes.Bytes32{99}, ID: "1", SourceChain: 1, SeqNum: 13}, - {MsgHash: cciptypes.Bytes32{12}, ID: "1", SourceChain: 300, SeqNum: 23}, - }, - maxSeqNums: []cciptypes.SeqNumChain{ - {ChainSel: 1, SeqNum: 10}, - {ChainSel: 2, SeqNum: 20}, - {ChainSel: 300, SeqNum: 22}, - }, - expErr: false, - }, - { - name: "dup msg hashes", - msgs: []cciptypes.CCIPMsgBaseDetails{ - {MsgHash: cciptypes.Bytes32{123}, ID: "1", SourceChain: 1, SeqNum: 12}, - {MsgHash: cciptypes.Bytes32{99}, ID: "1", SourceChain: 1, SeqNum: 13}, - {MsgHash: cciptypes.Bytes32{123}, ID: "1", SourceChain: 300, SeqNum: 23}, // dup hash - }, - maxSeqNums: []cciptypes.SeqNumChain{ - {ChainSel: 1, SeqNum: 10}, - {ChainSel: 2, SeqNum: 20}, - {ChainSel: 300, SeqNum: 22}, - }, - expErr: true, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - err := validateObservedSequenceNumbers(tc.msgs, tc.maxSeqNums) - if tc.expErr { - assert.Error(t, err) - return - } - assert.NoError(t, err) - }) - } -} - -func Test_validateObserverReadingEligibility(t *testing.T) { - testCases := []struct { - name string - observer libocrtypes.PeerID - msgs []cciptypes.CCIPMsgBaseDetails - seqNums []cciptypes.SeqNumChain - nodeSupportedChains mapset.Set[cciptypes.ChainSelector] - destChain cciptypes.ChainSelector - expErr bool - }{ - { - name: "observer can read all chains", - observer: libocrtypes.PeerID{10}, - msgs: []cciptypes.CCIPMsgBaseDetails{ - {ID: "1", SourceChain: 1, SeqNum: 12}, - {ID: "3", SourceChain: 2, SeqNum: 12}, - {ID: "1", SourceChain: 3, SeqNum: 12}, - {ID: "2", SourceChain: 3, SeqNum: 12}, - }, - nodeSupportedChains: mapset.NewSet[cciptypes.ChainSelector](1, 2, 3), - destChain: 1, - expErr: false, - }, - { - name: "observer is a writer so can observe seq nums", - observer: libocrtypes.PeerID{10}, - msgs: []cciptypes.CCIPMsgBaseDetails{}, - seqNums: []cciptypes.SeqNumChain{ - {ChainSel: 1, SeqNum: 12}, - }, - nodeSupportedChains: mapset.NewSet[cciptypes.ChainSelector](1, 3), - destChain: 1, - expErr: false, - }, - { - name: "observer is not a writer so cannot observe seq nums", - observer: libocrtypes.PeerID{10}, - msgs: []cciptypes.CCIPMsgBaseDetails{}, - seqNums: []cciptypes.SeqNumChain{ - {ChainSel: 1, SeqNum: 12}, - }, - nodeSupportedChains: mapset.NewSet[cciptypes.ChainSelector](3), - destChain: 1, - expErr: true, - }, - { - name: "observer cfg not found", - observer: libocrtypes.PeerID{10}, - msgs: []cciptypes.CCIPMsgBaseDetails{ - {ID: "1", SourceChain: 1, SeqNum: 12}, - {ID: "3", SourceChain: 2, SeqNum: 12}, - {ID: "1", SourceChain: 3, SeqNum: 12}, - {ID: "2", SourceChain: 3, SeqNum: 12}, - }, - nodeSupportedChains: mapset.NewSet[cciptypes.ChainSelector](1, 3), // observer 10 not found - destChain: 1, - expErr: true, - }, - { - name: "no msgs", - observer: libocrtypes.PeerID{10}, - msgs: []cciptypes.CCIPMsgBaseDetails{}, - nodeSupportedChains: mapset.NewSet[cciptypes.ChainSelector](1, 3), - expErr: false, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - err := validateObserverReadingEligibility(tc.msgs, tc.seqNums, tc.nodeSupportedChains, tc.destChain) - if tc.expErr { - assert.Error(t, err) - return - } - assert.NoError(t, err) - }) - } -} - -func Test_validateObservedTokenPrices(t *testing.T) { - testCases := []struct { - name string - tokenPrices []cciptypes.TokenPrice - expErr bool - }{ - { - name: "empty is valid", - tokenPrices: []cciptypes.TokenPrice{}, - expErr: false, - }, - { - name: "all valid", - tokenPrices: []cciptypes.TokenPrice{ - cciptypes.NewTokenPrice("0x1", big.NewInt(1)), - cciptypes.NewTokenPrice("0x2", big.NewInt(1)), - cciptypes.NewTokenPrice("0x3", big.NewInt(1)), - cciptypes.NewTokenPrice("0xa", big.NewInt(1)), - }, - expErr: false, - }, - { - name: "dup price", - tokenPrices: []cciptypes.TokenPrice{ - cciptypes.NewTokenPrice("0x1", big.NewInt(1)), - cciptypes.NewTokenPrice("0x2", big.NewInt(1)), - cciptypes.NewTokenPrice("0x1", big.NewInt(1)), // dup - cciptypes.NewTokenPrice("0xa", big.NewInt(1)), - }, - expErr: true, - }, - { - name: "nil price", - tokenPrices: []cciptypes.TokenPrice{ - cciptypes.NewTokenPrice("0x1", big.NewInt(1)), - cciptypes.NewTokenPrice("0x2", big.NewInt(1)), - cciptypes.NewTokenPrice("0x3", nil), // nil price - cciptypes.NewTokenPrice("0xa", big.NewInt(1)), - }, - expErr: true, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - err := validateObservedTokenPrices(tc.tokenPrices) - if tc.expErr { - assert.Error(t, err) - return - } - assert.NoError(t, err) - }) - - } -} - -func Test_validateObservedGasPrices(t *testing.T) { - testCases := []struct { - name string - gasPrices []cciptypes.GasPriceChain - expErr bool - }{ - { - name: "empty is valid", - gasPrices: []cciptypes.GasPriceChain{}, - expErr: false, - }, - { - name: "all valid", - gasPrices: []cciptypes.GasPriceChain{ - cciptypes.NewGasPriceChain(big.NewInt(10), 1), - cciptypes.NewGasPriceChain(big.NewInt(20), 2), - cciptypes.NewGasPriceChain(big.NewInt(1312), 3), - }, - expErr: false, - }, - { - name: "duplicate gas price", - gasPrices: []cciptypes.GasPriceChain{ - cciptypes.NewGasPriceChain(big.NewInt(10), 1), - cciptypes.NewGasPriceChain(big.NewInt(20), 2), - cciptypes.NewGasPriceChain(big.NewInt(1312), 1), // notice we already have a gas price for chain 1 - }, - expErr: true, - }, - { - name: "empty gas price", - gasPrices: []cciptypes.GasPriceChain{ - cciptypes.NewGasPriceChain(big.NewInt(10), 1), - cciptypes.NewGasPriceChain(big.NewInt(20), 2), - cciptypes.NewGasPriceChain(nil, 3), // nil - }, - expErr: true, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - err := validateObservedGasPrices(tc.gasPrices) - if tc.expErr { - assert.Error(t, err) - return - } - assert.NoError(t, err) - }) - } -} - -func Test_newMsgsConsensusForChain(t *testing.T) { - testCases := []struct { - name string - maxSeqNums []cciptypes.SeqNumChain - observations []cciptypes.CommitPluginObservation - expMerkleRoots []cciptypes.MerkleRootChain - fChain map[cciptypes.ChainSelector]int - expErr bool - }{ - { - name: "empty", - maxSeqNums: []cciptypes.SeqNumChain{}, - observations: nil, - expMerkleRoots: []cciptypes.MerkleRootChain{}, - expErr: false, - }, - { - name: "one message but not reaching 2fChain+1 observations", - fChain: map[cciptypes.ChainSelector]int{ - 1: 2, - }, - maxSeqNums: []cciptypes.SeqNumChain{ - {ChainSel: 1, SeqNum: 10}, - }, - observations: []cciptypes.CommitPluginObservation{ - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "1", SourceChain: 1, SeqNum: 11}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "1", SourceChain: 1, SeqNum: 11}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "1", SourceChain: 1, SeqNum: 11}}}, - }, - expMerkleRoots: []cciptypes.MerkleRootChain{}, - expErr: false, - }, - { - name: "one message reaching 2fChain+1 observations", - fChain: map[cciptypes.ChainSelector]int{ - 1: 2, - }, - maxSeqNums: []cciptypes.SeqNumChain{ - {ChainSel: 1, SeqNum: 10}, - }, - observations: []cciptypes.CommitPluginObservation{ - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "1", SourceChain: 1, SeqNum: 11}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "1", SourceChain: 1, SeqNum: 11}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "1", SourceChain: 1, SeqNum: 11}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "1", SourceChain: 1, SeqNum: 11}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "1", SourceChain: 1, SeqNum: 11}}}, - }, - expMerkleRoots: []cciptypes.MerkleRootChain{ - { - ChainSel: 1, - SeqNumsRange: cciptypes.NewSeqNumRange(11, 11), - }, - }, - expErr: false, - }, - { - name: "multiple messages all of them reaching 2fChain+1 observations", - fChain: map[cciptypes.ChainSelector]int{ - 1: 2, - }, - maxSeqNums: []cciptypes.SeqNumChain{ - {ChainSel: 1, SeqNum: 10}, - }, - observations: []cciptypes.CommitPluginObservation{ - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "1", SourceChain: 1, SeqNum: 11}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "1", SourceChain: 1, SeqNum: 11}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "1", SourceChain: 1, SeqNum: 11}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "1", SourceChain: 1, SeqNum: 11}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "1", SourceChain: 1, SeqNum: 11}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "2", SourceChain: 1, SeqNum: 12}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "2", SourceChain: 1, SeqNum: 12}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "2", SourceChain: 1, SeqNum: 12}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "2", SourceChain: 1, SeqNum: 12}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "2", SourceChain: 1, SeqNum: 12}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "3", SourceChain: 1, SeqNum: 13}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "3", SourceChain: 1, SeqNum: 13}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "3", SourceChain: 1, SeqNum: 13}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "3", SourceChain: 1, SeqNum: 13}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "3", SourceChain: 1, SeqNum: 13}}}, - }, - expMerkleRoots: []cciptypes.MerkleRootChain{ - { - ChainSel: 1, - SeqNumsRange: cciptypes.NewSeqNumRange(11, 13), - }, - }, - expErr: false, - }, - { - name: "one message sequence number is lower than consensus max seq num", - fChain: map[cciptypes.ChainSelector]int{ - 1: 2, - }, - maxSeqNums: []cciptypes.SeqNumChain{ - {ChainSel: 1, SeqNum: 10}, - }, - observations: []cciptypes.CommitPluginObservation{ - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "1", SourceChain: 1, SeqNum: 10}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "1", SourceChain: 1, SeqNum: 10}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "1", SourceChain: 1, SeqNum: 10}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "1", SourceChain: 1, SeqNum: 10}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "1", SourceChain: 1, SeqNum: 10}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "2", SourceChain: 1, SeqNum: 12}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "2", SourceChain: 1, SeqNum: 12}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "2", SourceChain: 1, SeqNum: 12}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "2", SourceChain: 1, SeqNum: 12}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "2", SourceChain: 1, SeqNum: 12}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "3", SourceChain: 1, SeqNum: 13}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "3", SourceChain: 1, SeqNum: 13}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "3", SourceChain: 1, SeqNum: 13}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "3", SourceChain: 1, SeqNum: 13}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "3", SourceChain: 1, SeqNum: 13}}}, - }, - expMerkleRoots: []cciptypes.MerkleRootChain{ - { - ChainSel: 1, - SeqNumsRange: cciptypes.NewSeqNumRange(12, 13), - }, - }, - expErr: false, - }, - { - name: "multiple messages some of them not reaching 2fChain+1 observations", - fChain: map[cciptypes.ChainSelector]int{ - 1: 2, - }, - maxSeqNums: []cciptypes.SeqNumChain{ - {ChainSel: 1, SeqNum: 10}, - }, - observations: []cciptypes.CommitPluginObservation{ - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "1", SourceChain: 1, SeqNum: 11}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "1", SourceChain: 1, SeqNum: 11}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "1", SourceChain: 1, SeqNum: 11}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "1", SourceChain: 1, SeqNum: 11}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "1", SourceChain: 1, SeqNum: 11}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "2", SourceChain: 1, SeqNum: 12}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "2", SourceChain: 1, SeqNum: 12}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "3", SourceChain: 1, SeqNum: 13}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "3", SourceChain: 1, SeqNum: 13}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "3", SourceChain: 1, SeqNum: 13}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "3", SourceChain: 1, SeqNum: 13}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "3", SourceChain: 1, SeqNum: 13}}}, - }, - expMerkleRoots: []cciptypes.MerkleRootChain{ - { - ChainSel: 1, - SeqNumsRange: cciptypes.NewSeqNumRange(11, 11), // we stop at 11 because there is a gap for going to 13 - }, - }, - expErr: false, - }, - { - name: "multiple messages on different chains", - fChain: map[cciptypes.ChainSelector]int{ - 1: 2, - 2: 1, - }, - maxSeqNums: []cciptypes.SeqNumChain{ - {ChainSel: 1, SeqNum: 10}, - {ChainSel: 2, SeqNum: 20}, - }, - observations: []cciptypes.CommitPluginObservation{ - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "1", SourceChain: 1, SeqNum: 11}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "1", SourceChain: 1, SeqNum: 11}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "1", SourceChain: 1, SeqNum: 11}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "1", SourceChain: 1, SeqNum: 11}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "1", SourceChain: 1, SeqNum: 11}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "3", SourceChain: 2, SeqNum: 21}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "3", SourceChain: 2, SeqNum: 21}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "3", SourceChain: 2, SeqNum: 21}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "4", SourceChain: 2, SeqNum: 22}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "4", SourceChain: 2, SeqNum: 22}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "4", SourceChain: 2, SeqNum: 22}}}, - }, - expMerkleRoots: []cciptypes.MerkleRootChain{ - { - ChainSel: 1, - SeqNumsRange: cciptypes.NewSeqNumRange(11, 11), // we stop at 11 because there is a gap for going to 13 - }, - { - ChainSel: 2, - SeqNumsRange: cciptypes.NewSeqNumRange(21, 22), // we stop at 11 because there is a gap for going to 13 - }, - }, - expErr: false, - }, - { - name: "one message seq num with multiple reported ids", - fChain: map[cciptypes.ChainSelector]int{ - 1: 2, - }, - maxSeqNums: []cciptypes.SeqNumChain{ - {ChainSel: 1, SeqNum: 10}, - }, - observations: []cciptypes.CommitPluginObservation{ - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "1", SourceChain: 1, SeqNum: 11}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "1", SourceChain: 1, SeqNum: 11}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "1", SourceChain: 1, SeqNum: 11}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "1", SourceChain: 1, SeqNum: 11}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "1", SourceChain: 1, SeqNum: 11}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "10", SourceChain: 1, SeqNum: 11}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "10", SourceChain: 1, SeqNum: 11}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "111", SourceChain: 1, SeqNum: 11}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "111", SourceChain: 1, SeqNum: 11}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "3", SourceChain: 1, SeqNum: 11}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "2", SourceChain: 1, SeqNum: 11}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "2", SourceChain: 1, SeqNum: 11}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "2", SourceChain: 1, SeqNum: 11}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "2", SourceChain: 1, SeqNum: 11}}}, - {NewMsgs: []cciptypes.CCIPMsgBaseDetails{{ID: "2", SourceChain: 1, SeqNum: 11}}}, - }, - expMerkleRoots: []cciptypes.MerkleRootChain{ - { - ChainSel: 1, - SeqNumsRange: cciptypes.NewSeqNumRange(11, 11), - }, - }, - expErr: false, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - lggr := logger.Test(t) - merkleRoots, err := newMsgsConsensus(lggr, tc.maxSeqNums, tc.observations, tc.fChain) - if tc.expErr { - assert.Error(t, err) - return - } - assert.NoError(t, err) - assert.Equal(t, len(tc.expMerkleRoots), len(merkleRoots)) - for i, exp := range tc.expMerkleRoots { - assert.Equal(t, exp.ChainSel, merkleRoots[i].ChainSel) - assert.Equal(t, exp.SeqNumsRange, merkleRoots[i].SeqNumsRange) - } - }) - } -} - -func Test_maxSeqNumsConsensus(t *testing.T) { - testCases := []struct { - name string - observations []cciptypes.CommitPluginObservation - fChain int - expSeqNums []cciptypes.SeqNumChain - }{ - { - name: "empty observations", - observations: []cciptypes.CommitPluginObservation{}, - fChain: 2, - expSeqNums: []cciptypes.SeqNumChain{}, - }, - { - name: "one chain all followers agree", - observations: []cciptypes.CommitPluginObservation{ - { - MaxSeqNums: []cciptypes.SeqNumChain{ - {ChainSel: 2, SeqNum: 20}, - {ChainSel: 2, SeqNum: 20}, - {ChainSel: 2, SeqNum: 20}, - {ChainSel: 2, SeqNum: 20}, - {ChainSel: 2, SeqNum: 20}, - {ChainSel: 2, SeqNum: 20}, - {ChainSel: 2, SeqNum: 20}, - }, - }, - }, - fChain: 2, - expSeqNums: []cciptypes.SeqNumChain{ - {ChainSel: 2, SeqNum: 20}, - }, - }, - { - name: "one chain all followers agree but not enough observations", - observations: []cciptypes.CommitPluginObservation{ - { - MaxSeqNums: []cciptypes.SeqNumChain{ - {ChainSel: 2, SeqNum: 20}, - {ChainSel: 2, SeqNum: 20}, - {ChainSel: 2, SeqNum: 20}, - {ChainSel: 2, SeqNum: 20}, - {ChainSel: 2, SeqNum: 20}, - }, - }, - }, - fChain: 3, - expSeqNums: []cciptypes.SeqNumChain{}, - }, - { - name: "one chain 3 followers not in sync, 4 in sync", - observations: []cciptypes.CommitPluginObservation{ - { - MaxSeqNums: []cciptypes.SeqNumChain{ - {ChainSel: 2, SeqNum: 20}, - {ChainSel: 2, SeqNum: 19}, - {ChainSel: 2, SeqNum: 20}, - {ChainSel: 2, SeqNum: 19}, - {ChainSel: 2, SeqNum: 20}, - {ChainSel: 2, SeqNum: 19}, - {ChainSel: 2, SeqNum: 20}, - }, - }, - }, - fChain: 3, - expSeqNums: []cciptypes.SeqNumChain{ - {ChainSel: 2, SeqNum: 20}, - }, - }, - { - name: "two chains", - observations: []cciptypes.CommitPluginObservation{ - { - MaxSeqNums: []cciptypes.SeqNumChain{ - {ChainSel: 2, SeqNum: 20}, - {ChainSel: 2, SeqNum: 20}, - {ChainSel: 2, SeqNum: 20}, - {ChainSel: 2, SeqNum: 20}, - {ChainSel: 2, SeqNum: 20}, - {ChainSel: 2, SeqNum: 20}, - {ChainSel: 2, SeqNum: 20}, - - {ChainSel: 3, SeqNum: 30}, - {ChainSel: 3, SeqNum: 30}, - {ChainSel: 3, SeqNum: 30}, - {ChainSel: 3, SeqNum: 30}, - {ChainSel: 3, SeqNum: 30}, - }, - }, - }, - fChain: 2, - expSeqNums: []cciptypes.SeqNumChain{ - {ChainSel: 2, SeqNum: 20}, - {ChainSel: 3, SeqNum: 30}, - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - lggr := logger.Test(t) - seqNums := maxSeqNumsConsensus(lggr, tc.fChain, tc.observations) - assert.Equal(t, tc.expSeqNums, seqNums) - }) - } -} - -func Test_tokenPricesConsensus(t *testing.T) { - testCases := []struct { - name string - observations []cciptypes.CommitPluginObservation - fChain int - expPrices []cciptypes.TokenPrice - expErr bool - }{ - { - name: "empty", - observations: make([]cciptypes.CommitPluginObservation, 0), - fChain: 2, - expPrices: make([]cciptypes.TokenPrice, 0), - expErr: false, - }, - { - name: "happy flow", - observations: []cciptypes.CommitPluginObservation{ - { - TokenPrices: []cciptypes.TokenPrice{ - cciptypes.NewTokenPrice("0x1", big.NewInt(10)), - cciptypes.NewTokenPrice("0x2", big.NewInt(20)), - }, - }, - { - TokenPrices: []cciptypes.TokenPrice{ - cciptypes.NewTokenPrice("0x1", big.NewInt(11)), - cciptypes.NewTokenPrice("0x2", big.NewInt(21)), - }, - }, - { - TokenPrices: []cciptypes.TokenPrice{ - cciptypes.NewTokenPrice("0x1", big.NewInt(11)), - cciptypes.NewTokenPrice("0x2", big.NewInt(21)), - }, - }, - { - TokenPrices: []cciptypes.TokenPrice{ - cciptypes.NewTokenPrice("0x1", big.NewInt(10)), - cciptypes.NewTokenPrice("0x2", big.NewInt(21)), - }, - }, - { - TokenPrices: []cciptypes.TokenPrice{ - cciptypes.NewTokenPrice("0x1", big.NewInt(11)), - cciptypes.NewTokenPrice("0x2", big.NewInt(20)), - }, - }, - }, - fChain: 2, - expPrices: []cciptypes.TokenPrice{ - cciptypes.NewTokenPrice("0x1", big.NewInt(11)), - cciptypes.NewTokenPrice("0x2", big.NewInt(21)), - }, - expErr: false, - }, - { - name: "not enough observations for some token", - observations: []cciptypes.CommitPluginObservation{ - { - TokenPrices: []cciptypes.TokenPrice{ - cciptypes.NewTokenPrice("0x2", big.NewInt(20)), - }, - }, - { - TokenPrices: []cciptypes.TokenPrice{ - cciptypes.NewTokenPrice("0x1", big.NewInt(11)), - cciptypes.NewTokenPrice("0x2", big.NewInt(21)), - }, - }, - { - TokenPrices: []cciptypes.TokenPrice{ - cciptypes.NewTokenPrice("0x1", big.NewInt(11)), - cciptypes.NewTokenPrice("0x2", big.NewInt(21)), - }, - }, - { - TokenPrices: []cciptypes.TokenPrice{ - cciptypes.NewTokenPrice("0x1", big.NewInt(10)), - cciptypes.NewTokenPrice("0x2", big.NewInt(21)), - }, - }, - { - TokenPrices: []cciptypes.TokenPrice{ - cciptypes.NewTokenPrice("0x1", big.NewInt(10)), - cciptypes.NewTokenPrice("0x2", big.NewInt(20)), - }, - }, - }, - fChain: 2, - expPrices: []cciptypes.TokenPrice{ - cciptypes.NewTokenPrice("0x2", big.NewInt(21)), - }, - expErr: false, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - prices, err := tokenPricesConsensus(tc.observations, tc.fChain) - if tc.expErr { - assert.Error(t, err) - return - } - assert.NoError(t, err) - assert.Equal(t, tc.expPrices, prices) - }) - } -} - -func Test_gasPricesConsensus(t *testing.T) { - testCases := []struct { - name string - observations []cciptypes.CommitPluginObservation - fChain int - expPrices []cciptypes.GasPriceChain - }{ - { - name: "empty", - observations: make([]cciptypes.CommitPluginObservation, 0), - fChain: 2, - expPrices: make([]cciptypes.GasPriceChain, 0), - }, - { - name: "one chain happy path", - observations: []cciptypes.CommitPluginObservation{ - {GasPrices: []cciptypes.GasPriceChain{cciptypes.NewGasPriceChain(big.NewInt(20), 1)}}, - {GasPrices: []cciptypes.GasPriceChain{cciptypes.NewGasPriceChain(big.NewInt(10), 1)}}, - {GasPrices: []cciptypes.GasPriceChain{cciptypes.NewGasPriceChain(big.NewInt(10), 1)}}, - {GasPrices: []cciptypes.GasPriceChain{cciptypes.NewGasPriceChain(big.NewInt(11), 1)}}, - {GasPrices: []cciptypes.GasPriceChain{cciptypes.NewGasPriceChain(big.NewInt(10), 1)}}, - }, - fChain: 2, - expPrices: []cciptypes.GasPriceChain{ - cciptypes.NewGasPriceChain(big.NewInt(10), 1), - }, - }, - { - name: "one chain no consensus", - observations: []cciptypes.CommitPluginObservation{ - {GasPrices: []cciptypes.GasPriceChain{cciptypes.NewGasPriceChain(big.NewInt(20), 1)}}, - {GasPrices: []cciptypes.GasPriceChain{cciptypes.NewGasPriceChain(big.NewInt(10), 1)}}, - {GasPrices: []cciptypes.GasPriceChain{cciptypes.NewGasPriceChain(big.NewInt(10), 1)}}, - {GasPrices: []cciptypes.GasPriceChain{cciptypes.NewGasPriceChain(big.NewInt(11), 1)}}, - {GasPrices: []cciptypes.GasPriceChain{cciptypes.NewGasPriceChain(big.NewInt(10), 1)}}, - }, - fChain: 3, // notice fChain is 3, means we need at least 2*3+1=7 observations - expPrices: []cciptypes.GasPriceChain{}, - }, - { - name: "two chains determinism check", - observations: []cciptypes.CommitPluginObservation{ - {GasPrices: []cciptypes.GasPriceChain{cciptypes.NewGasPriceChain(big.NewInt(20), 1)}}, - {GasPrices: []cciptypes.GasPriceChain{cciptypes.NewGasPriceChain(big.NewInt(10), 1)}}, - {GasPrices: []cciptypes.GasPriceChain{cciptypes.NewGasPriceChain(big.NewInt(10), 1)}}, - {GasPrices: []cciptypes.GasPriceChain{cciptypes.NewGasPriceChain(big.NewInt(11), 1)}}, - {GasPrices: []cciptypes.GasPriceChain{cciptypes.NewGasPriceChain(big.NewInt(10), 1)}}, - {GasPrices: []cciptypes.GasPriceChain{cciptypes.NewGasPriceChain(big.NewInt(200), 10)}}, - {GasPrices: []cciptypes.GasPriceChain{cciptypes.NewGasPriceChain(big.NewInt(100), 10)}}, - {GasPrices: []cciptypes.GasPriceChain{cciptypes.NewGasPriceChain(big.NewInt(100), 10)}}, - {GasPrices: []cciptypes.GasPriceChain{cciptypes.NewGasPriceChain(big.NewInt(110), 10)}}, - {GasPrices: []cciptypes.GasPriceChain{cciptypes.NewGasPriceChain(big.NewInt(100), 10)}}, - }, - fChain: 2, - expPrices: []cciptypes.GasPriceChain{ - cciptypes.NewGasPriceChain(big.NewInt(10), 1), - cciptypes.NewGasPriceChain(big.NewInt(100), 10), - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - lggr := logger.Test(t) - prices := gasPricesConsensus(lggr, tc.observations, tc.fChain) - assert.Equal(t, tc.expPrices, prices) - }) - } -} diff --git a/core/services/ocr3/plugins/ccip/execute/factory.go b/core/services/ocr3/plugins/ccip/execute/factory.go deleted file mode 100644 index a75139d12e..0000000000 --- a/core/services/ocr3/plugins/ccip/execute/factory.go +++ /dev/null @@ -1,79 +0,0 @@ -package execute - -import ( - "context" - - "google.golang.org/grpc" - - "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" - - cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" - "github.com/smartcontractkit/chainlink-common/pkg/types/core" -) - -// PluginFactoryConstructor implements common OCR3ReportingPluginClient and is used for initializing a plugin factory -// and a validation service. -type PluginFactoryConstructor struct{} - -func NewPluginFactoryConstructor() *PluginFactoryConstructor { - return &PluginFactoryConstructor{} -} -func (p PluginFactoryConstructor) NewReportingPluginFactory( - ctx context.Context, - config core.ReportingPluginServiceConfig, - grpcProvider grpc.ClientConnInterface, - pipelineRunner core.PipelineRunnerService, - telemetry core.TelemetryService, - errorLog core.ErrorLog, - capRegistry core.CapabilitiesRegistry, - keyValueStore core.KeyValueStore, - relayerSet core.RelayerSet, -) (core.OCR3ReportingPluginFactory, error) { - return NewPluginFactory(), nil -} - -func (p PluginFactoryConstructor) NewValidationService(ctx context.Context) (core.ValidationService, error) { - panic("implement me") -} - -// PluginFactory implements common ReportingPluginFactory and is used for (re-)initializing commit plugin instances. -type PluginFactory struct{} - -func NewPluginFactory() *PluginFactory { - return &PluginFactory{} -} - -func (p PluginFactory) NewReportingPlugin( - config ocr3types.ReportingPluginConfig, -) (ocr3types.ReportingPlugin[[]byte], ocr3types.ReportingPluginInfo, error) { - return NewPlugin( - context.Background(), - config, - cciptypes.ExecutePluginConfig{}, - nil, - ), ocr3types.ReportingPluginInfo{}, nil -} - -func (p PluginFactory) Name() string { - panic("implement me") -} - -func (p PluginFactory) Start(ctx context.Context) error { - panic("implement me") -} - -func (p PluginFactory) Close() error { - panic("implement me") -} - -func (p PluginFactory) Ready() error { - panic("implement me") -} - -func (p PluginFactory) HealthReport() map[string]error { - panic("implement me") -} - -// Interface compatibility checks. -var _ core.OCR3ReportingPluginClient = &PluginFactoryConstructor{} -var _ core.OCR3ReportingPluginFactory = &PluginFactory{} diff --git a/core/services/ocr3/plugins/ccip/execute/internal/validation/reports.go b/core/services/ocr3/plugins/ccip/execute/internal/validation/reports.go deleted file mode 100644 index 61114a0232..0000000000 --- a/core/services/ocr3/plugins/ccip/execute/internal/validation/reports.go +++ /dev/null @@ -1,58 +0,0 @@ -package validation - -import ( - cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" -) - -type counter[T any] struct { - data T - count int -} - -// MinObservationFilter provides a way to ensure a minimum number of observations for -// some piece of data have occurred. It maintains an internal cache and provides a list -// of valid or invalid data points. -type MinObservationFilter[T any] interface { - Add(data T) error - GetValid() ([]T, error) -} - -// minObservationValidator is a helper object to validate reports for a single chain. -// It keeps track of all reports and determines if they observations are consistent -// with one another and whether they meet the required fChain threshold. -type minObservationValidator[T any] struct { - minObservation int - cache map[cciptypes.Bytes32]*counter[T] - idFunc func(T) [32]byte -} - -// NewMinObservationValidator constructs a concrete MinObservationFilter object. The -// supplied idFunc is used to generate a uniqueID for the type being observed. -func NewMinObservationValidator[T any](min int, idFunc func(T) [32]byte) MinObservationFilter[T] { - return &minObservationValidator[T]{ - minObservation: min, - cache: make(map[cciptypes.Bytes32]*counter[T]), - idFunc: idFunc, - } -} - -func (cv *minObservationValidator[T]) Add(data T) error { - id := cv.idFunc(data) - if _, ok := cv.cache[id]; ok { - cv.cache[id].count++ - } else { - cv.cache[id] = &counter[T]{data: data, count: 1} - } - return nil -} - -func (cv *minObservationValidator[T]) GetValid() ([]T, error) { - var validated []T - for _, rc := range cv.cache { - if rc.count >= cv.minObservation { - rc := rc - validated = append(validated, rc.data) - } - } - return validated, nil -} diff --git a/core/services/ocr3/plugins/ccip/execute/internal/validation/reports_test.go b/core/services/ocr3/plugins/ccip/execute/internal/validation/reports_test.go deleted file mode 100644 index 73db4bcef9..0000000000 --- a/core/services/ocr3/plugins/ccip/execute/internal/validation/reports_test.go +++ /dev/null @@ -1,151 +0,0 @@ -package validation - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "golang.org/x/crypto/sha3" - - cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" -) - -func Test_CommitReportValidator_ExecutePluginCommitData(t *testing.T) { - tests := []struct { - name string - min int - reports []cciptypes.ExecutePluginCommitData - valid []cciptypes.ExecutePluginCommitData - wantErr assert.ErrorAssertionFunc - }{ - { - name: "empty", - valid: nil, - wantErr: assert.NoError, - }, - { - name: "single report, enough observations", - min: 1, - reports: []cciptypes.ExecutePluginCommitData{ - {MerkleRoot: [32]byte{1}}, - }, - valid: []cciptypes.ExecutePluginCommitData{ - {MerkleRoot: [32]byte{1}}, - }, - wantErr: assert.NoError, - }, - { - name: "single report, not enough observations", - min: 2, - reports: []cciptypes.ExecutePluginCommitData{ - {MerkleRoot: [32]byte{1}}, - }, - valid: nil, - wantErr: assert.NoError, - }, - { - name: "multiple reports, partial observations", - min: 2, - reports: []cciptypes.ExecutePluginCommitData{ - {MerkleRoot: [32]byte{3}}, - {MerkleRoot: [32]byte{1}}, - {MerkleRoot: [32]byte{2}}, - {MerkleRoot: [32]byte{1}}, - {MerkleRoot: [32]byte{2}}, - }, - valid: []cciptypes.ExecutePluginCommitData{ - {MerkleRoot: [32]byte{1}}, - {MerkleRoot: [32]byte{2}}, - }, - wantErr: assert.NoError, - }, - { - name: "multiple reports for same root", - min: 2, - reports: []cciptypes.ExecutePluginCommitData{ - {MerkleRoot: [32]byte{1}, BlockNum: 1}, - {MerkleRoot: [32]byte{1}, BlockNum: 2}, - {MerkleRoot: [32]byte{1}, BlockNum: 3}, - {MerkleRoot: [32]byte{1}, BlockNum: 4}, - {MerkleRoot: [32]byte{1}, BlockNum: 1}, - }, - valid: []cciptypes.ExecutePluginCommitData{ - {MerkleRoot: [32]byte{1}, BlockNum: 1}, - }, - wantErr: assert.NoError, - }, - { - name: "different executed messages same root", - min: 2, - reports: []cciptypes.ExecutePluginCommitData{ - {MerkleRoot: [32]byte{1}, ExecutedMessages: []cciptypes.SeqNum{1, 2}}, - {MerkleRoot: [32]byte{1}, ExecutedMessages: []cciptypes.SeqNum{2, 3}}, - {MerkleRoot: [32]byte{1}, ExecutedMessages: []cciptypes.SeqNum{3, 4}}, - {MerkleRoot: [32]byte{1}, ExecutedMessages: []cciptypes.SeqNum{4, 5}}, - {MerkleRoot: [32]byte{1}, ExecutedMessages: []cciptypes.SeqNum{5, 6}}, - {MerkleRoot: [32]byte{1}, ExecutedMessages: []cciptypes.SeqNum{1, 2}}, - }, - valid: []cciptypes.ExecutePluginCommitData{ - {MerkleRoot: [32]byte{1}, ExecutedMessages: []cciptypes.SeqNum{1, 2}}, - }, - wantErr: assert.NoError, - }, - } - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - // Initialize the minObservationValidator - idFunc := func(data cciptypes.ExecutePluginCommitData) [32]byte { - return sha3.Sum256([]byte(fmt.Sprintf("%v", data))) - } - validator := NewMinObservationValidator[cciptypes.ExecutePluginCommitData](tt.min, idFunc) - for _, report := range tt.reports { - err := validator.Add(report) - require.NoError(t, err) - } - - // Test the results - got, err := validator.GetValid() - if !tt.wantErr(t, err, "GetValid()") { - return - } - if !assert.ElementsMatch(t, got, tt.valid) { - t.Errorf("GetValid() = %v, valid %v", got, tt.valid) - } - }) - } -} - -func Test_CommitReportValidator_Generics(t *testing.T) { - type Generic struct { - number int - } - - // Initialize the minObservationValidator - idFunc := func(data Generic) [32]byte { - return sha3.Sum256([]byte(fmt.Sprintf("%v", data))) - } - validator := NewMinObservationValidator[Generic](2, idFunc) - - wantValue := Generic{number: 1} - otherValue := Generic{number: 2} - - err := validator.Add(wantValue) - require.NoError(t, err) - err = validator.Add(wantValue) - require.NoError(t, err) - err = validator.Add(otherValue) - require.NoError(t, err) - - // Test the results - - wantValid := []Generic{wantValue} - got, err := validator.GetValid() - require.NoError(t, err) - if !assert.ElementsMatch(t, got, wantValid) { - t.Errorf("GetValid() = %v, valid %v", got, wantValid) - } -} diff --git a/core/services/ocr3/plugins/ccip/execute/plugin.go b/core/services/ocr3/plugins/ccip/execute/plugin.go deleted file mode 100644 index cb252d0cf6..0000000000 --- a/core/services/ocr3/plugins/ccip/execute/plugin.go +++ /dev/null @@ -1,275 +0,0 @@ -package execute - -import ( - "context" - "fmt" - "slices" - "sort" - "sync/atomic" - "time" - - "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" - "github.com/smartcontractkit/libocr/offchainreporting2plus/types" - - cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" -) - -// Plugin implements the main ocr3 plugin logic. -type Plugin struct { - reportingCfg ocr3types.ReportingPluginConfig - cfg cciptypes.ExecutePluginConfig - ccipReader cciptypes.CCIPReader - - //commitRootsCache cache.CommitsRootsCache - lastReportTS *atomic.Int64 -} - -func NewPlugin( - _ context.Context, - reportingCfg ocr3types.ReportingPluginConfig, - cfg cciptypes.ExecutePluginConfig, - ccipReader cciptypes.CCIPReader, -) *Plugin { - lastReportTS := &atomic.Int64{} - lastReportTS.Store(time.Now().Add(-cfg.MessageVisibilityInterval).UnixMilli()) - - return &Plugin{ - reportingCfg: reportingCfg, - cfg: cfg, - ccipReader: ccipReader, - lastReportTS: lastReportTS, - } -} - -func (p *Plugin) Query(ctx context.Context, outctx ocr3types.OutcomeContext) (types.Query, error) { - return types.Query{}, nil -} - -func getPendingExecutedReports( - ctx context.Context, ccipReader cciptypes.CCIPReader, dest cciptypes.ChainSelector, ts time.Time, -) (cciptypes.ExecutePluginCommitObservations, time.Time, error) { - latestReportTS := time.Time{} - commitReports, err := ccipReader.CommitReportsGTETimestamp(ctx, dest, ts, 1000) - if err != nil { - return nil, time.Time{}, err - } - // TODO: this could be more efficient. reports is also traversed in 'filterOutExecutedMessages' function. - for _, report := range commitReports { - if report.Timestamp.After(latestReportTS) { - latestReportTS = report.Timestamp - } - } - - // TODO: this could be more efficient. commitReports is also traversed in 'groupByChainSelector'. - for _, report := range commitReports { - if report.Timestamp.After(latestReportTS) { - latestReportTS = report.Timestamp - } - } - - groupedCommits := groupByChainSelector(commitReports) - - // Remove fully executed reports. - for selector, reports := range groupedCommits { - if len(reports) == 0 { - continue - } - - ranges, err := computeRanges(reports) - if err != nil { - return nil, time.Time{}, err - } - - var executedMessages []cciptypes.SeqNumRange - for _, seqRange := range ranges { - executedMessagesForRange, err2 := ccipReader.ExecutedMessageRanges(ctx, selector, dest, seqRange) - if err2 != nil { - return nil, time.Time{}, err2 - } - executedMessages = append(executedMessages, executedMessagesForRange...) - } - - // Remove fully executed reports. - groupedCommits[selector], err = filterOutExecutedMessages(reports, executedMessages) - if err != nil { - return nil, time.Time{}, err - } - } - - return groupedCommits, latestReportTS, nil -} - -// Observation collects data across two phases which happen in separate rounds. -// These phases happen continuously so that except for the first round, every -// subsequent round can have a new execution report. -// -// Phase 1: Gather commit reports from the destination chain and determine -// which messages are required to build a valid execution report. -// -// Phase 2: Gather messages from the source chains and build the execution -// report. -func (p *Plugin) Observation( - ctx context.Context, outctx ocr3types.OutcomeContext, _ types.Query, -) (types.Observation, error) { - previousOutcome, err := cciptypes.DecodeExecutePluginOutcome(outctx.PreviousOutcome) - if err != nil { - return types.Observation{}, err - } - - // Phase 1: Gather commit reports from the destination chain and determine which messages are required to build a - // valid execution report. - ownConfig := p.cfg.ObserverInfo[p.reportingCfg.OracleID] - var groupedCommits cciptypes.ExecutePluginCommitObservations - if slices.Contains(ownConfig.Reads, p.cfg.DestChain) { - var latestReportTS time.Time - groupedCommits, latestReportTS, err = - getPendingExecutedReports(ctx, p.ccipReader, p.cfg.DestChain, time.UnixMilli(p.lastReportTS.Load())) - if err != nil { - return types.Observation{}, err - } - // Update timestamp to the last report. - p.lastReportTS.Store(latestReportTS.UnixMilli()) - - // TODO: truncate grouped commits to a maximum observation size. - // Cache everything which is not executed. - } - - // Phase 2: Gather messages from the source chains and build the execution report. - messages := make(cciptypes.ExecutePluginMessageObservations) - if len(previousOutcome.PendingCommitReports) == 0 { - fmt.Println("TODO: No reports to execute. This is expected after a cold start.") - // No reports to execute. - // This is expected after a cold start. - } else { - commitReportCache := make(map[cciptypes.ChainSelector][]cciptypes.ExecutePluginCommitDataWithMessages) - for _, report := range previousOutcome.PendingCommitReports { - commitReportCache[report.SourceChain] = append(commitReportCache[report.SourceChain], report) - } - - for selector, reports := range commitReportCache { - if len(reports) == 0 { - continue - } - - ranges, err := computeRanges(reports) - if err != nil { - return types.Observation{}, err - } - - // Read messages for each range. - for _, seqRange := range ranges { - msgs, err := p.ccipReader.MsgsBetweenSeqNums(ctx, selector, seqRange) - if err != nil { - return nil, err - } - for _, msg := range msgs { - messages[selector][msg.SeqNum] = msg - } - } - } - } - - // TODO: Fire off messages for an attestation check service. - - return cciptypes.NewExecutePluginObservation(groupedCommits, messages).Encode() -} - -func (p *Plugin) ValidateObservation( - outctx ocr3types.OutcomeContext, query types.Query, ao types.AttributedObservation, -) error { - decodedObservation, err := cciptypes.DecodeExecutePluginObservation(ao.Observation) - if err != nil { - return fmt.Errorf("decode observation: %w", err) - } - - err = validateObserverReadingEligibility(p.reportingCfg.OracleID, p.cfg.ObserverInfo, decodedObservation.Messages) - if err != nil { - return fmt.Errorf("validate observer reading eligibility: %w", err) - } - - if err := validateObservedSequenceNumbers(decodedObservation.CommitReports); err != nil { - return fmt.Errorf("validate observed sequence numbers: %w", err) - } - - return nil -} - -func (p *Plugin) ObservationQuorum(outctx ocr3types.OutcomeContext, query types.Query) (ocr3types.Quorum, error) { - // TODO: should we use f+1 (or less) instead of 2f+1 because it is not needed for security? - return ocr3types.QuorumFPlusOne, nil -} - -func (p *Plugin) Outcome( - outctx ocr3types.OutcomeContext, query types.Query, aos []types.AttributedObservation, -) (ocr3types.Outcome, error) { - decodedObservations, err := decodeAttributedObservations(aos) - if err != nil { - return ocr3types.Outcome{}, err - - } - if len(decodedObservations) < p.reportingCfg.F { - return ocr3types.Outcome{}, fmt.Errorf("below F threshold") - } - - mergedCommitObservations, err := mergeCommitObservations(decodedObservations, p.cfg.FChain) - if err != nil { - return ocr3types.Outcome{}, err - } - - mergedMessageObservations, err := mergeMessageObservations(decodedObservations, p.cfg.FChain) - if err != nil { - return ocr3types.Outcome{}, err - } - - observation := cciptypes.NewExecutePluginObservation( - mergedCommitObservations, - mergedMessageObservations) - - // flatten commit reports and sort by timestamp. - var reports []cciptypes.ExecutePluginCommitDataWithMessages - for _, report := range observation.CommitReports { - reports = append(reports, report...) - } - sort.Slice(reports, func(i, j int) bool { - return reports[i].Timestamp.Before(reports[j].Timestamp) - }) - - // add messages to their reports. - for _, report := range reports { - report.Messages = nil - for i := report.SequenceNumberRange.Start(); i <= report.SequenceNumberRange.End(); i++ { - if msg, ok := observation.Messages[report.SourceChain][i]; ok { - report.Messages = append(report.Messages, msg) - } - } - } - - // TODO: select reports and messages for the final exec report. - // TODO: may only need the proofs for the final exec report rather than the report and the messages. - - return cciptypes.NewExecutePluginOutcome(reports).Encode() -} - -func (p *Plugin) Reports(seqNr uint64, outcome ocr3types.Outcome) ([]ocr3types.ReportWithInfo[[]byte], error) { - - panic("implement me") -} - -func (p *Plugin) ShouldAcceptAttestedReport( - ctx context.Context, u uint64, r ocr3types.ReportWithInfo[[]byte], -) (bool, error) { - panic("implement me") -} - -func (p *Plugin) ShouldTransmitAcceptedReport( - ctx context.Context, u uint64, r ocr3types.ReportWithInfo[[]byte], -) (bool, error) { - panic("implement me") -} - -func (p *Plugin) Close() error { - panic("implement me") -} - -// Interface compatibility checks. -var _ ocr3types.ReportingPlugin[[]byte] = &Plugin{} diff --git a/core/services/ocr3/plugins/ccip/execute/plugin_functions.go b/core/services/ocr3/plugins/ccip/execute/plugin_functions.go deleted file mode 100644 index 529938939a..0000000000 --- a/core/services/ocr3/plugins/ccip/execute/plugin_functions.go +++ /dev/null @@ -1,317 +0,0 @@ -package execute - -import ( - "errors" - "fmt" - "sort" - - mapset "github.com/deckarep/golang-set/v2" - "golang.org/x/crypto/sha3" - - cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" - "github.com/smartcontractkit/libocr/commontypes" - "github.com/smartcontractkit/libocr/offchainreporting2plus/types" - - "github.com/smartcontractkit/ccipocr3/execute/internal/validation" -) - -// validateObserverReadingEligibility checks if the observer is eligible to observe the messages it observed. -func validateObserverReadingEligibility( - observer commontypes.OracleID, - observerCfg map[commontypes.OracleID]cciptypes.ObserverInfo, - observedMsgs cciptypes.ExecutePluginMessageObservations, -) error { - observerInfo, exists := observerCfg[observer] - if !exists { - return fmt.Errorf("observer not found in config") - } - - observerReadChains := mapset.NewSet(observerInfo.Reads...) - - for chainSel, msgs := range observedMsgs { - if len(msgs) == 0 { - continue - } - - if !observerReadChains.Contains(chainSel) { - return fmt.Errorf("observer not allowed to read from chain %d", chainSel) - } - } - - return nil -} - -// validateObservedSequenceNumbers checks if the sequence numbers of the provided messages are unique for each chain -// and that they match the observed max sequence numbers. -func validateObservedSequenceNumbers( - observedData map[cciptypes.ChainSelector][]cciptypes.ExecutePluginCommitDataWithMessages, -) error { - for _, commitData := range observedData { - // observed commitData must not contain duplicates - - observedMerkleRoots := mapset.NewSet[string]() - observedRanges := make([]cciptypes.SeqNumRange, 0) - - for _, data := range commitData { - rootStr := data.MerkleRoot.String() - if observedMerkleRoots.Contains(rootStr) { - return fmt.Errorf("duplicate merkle root %s observed", rootStr) - } - observedMerkleRoots.Add(rootStr) - - for _, rng := range observedRanges { - if rng.Overlaps(data.SequenceNumberRange) { - return fmt.Errorf("sequence number range %v overlaps with %v", data.SequenceNumberRange, rng) - } - } - observedRanges = append(observedRanges, data.SequenceNumberRange) - - // Executed sequence numbers should belong in the observed range. - for _, seqNum := range data.ExecutedMessages { - if !data.SequenceNumberRange.Contains(seqNum) { - return fmt.Errorf("executed message %d not in observed range %v", seqNum, data.SequenceNumberRange) - } - } - } - } - - return nil -} - -var errOverlappingRanges = errors.New("overlapping sequence numbers in reports") - -// computeRanges takes a slice of reports and computes the smallest number of contiguous ranges -// that cover all the sequence numbers in the reports. -// Note: reports need all messages to create a proof even if some are already executed. -func computeRanges(reports []cciptypes.ExecutePluginCommitDataWithMessages) ([]cciptypes.SeqNumRange, error) { - var ranges []cciptypes.SeqNumRange - - if len(reports) == 0 { - return nil, nil - } - - var seqRange cciptypes.SeqNumRange - for i, report := range reports { - if i == 0 { - // initialize - seqRange = cciptypes.NewSeqNumRange(report.SequenceNumberRange.Start(), report.SequenceNumberRange.End()) - } else if seqRange.End()+1 == report.SequenceNumberRange.Start() { - // extend the contiguous range - seqRange.SetEnd(report.SequenceNumberRange.End()) - } else if report.SequenceNumberRange.Start() < seqRange.End() { - return nil, errOverlappingRanges - } else { - ranges = append(ranges, seqRange) - - // Reset the range. - seqRange = cciptypes.NewSeqNumRange(report.SequenceNumberRange.Start(), report.SequenceNumberRange.End()) - } - } - // add final range - ranges = append(ranges, seqRange) - - return ranges, nil -} - -func groupByChainSelector(reports []cciptypes.CommitPluginReportWithMeta) cciptypes.ExecutePluginCommitObservations { - commitReportCache := make(map[cciptypes.ChainSelector][]cciptypes.ExecutePluginCommitDataWithMessages) - for _, report := range reports { - for _, singleReport := range report.Report.MerkleRoots { - commitReportCache[singleReport.ChainSel] = append(commitReportCache[singleReport.ChainSel], - cciptypes.ExecutePluginCommitDataWithMessages{ - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SourceChain: singleReport.ChainSel, - Timestamp: report.Timestamp, - BlockNum: report.BlockNum, - MerkleRoot: singleReport.MerkleRoot, - SequenceNumberRange: singleReport.SeqNumsRange, - ExecutedMessages: nil, - }}) - } - } - return commitReportCache -} - -// filterOutExecutedMessages returns a new reports slice with fully executed messages removed. -// Unordered inputs are supported. -func filterOutExecutedMessages( - reports []cciptypes.ExecutePluginCommitDataWithMessages, executedMessages []cciptypes.SeqNumRange, -) ([]cciptypes.ExecutePluginCommitDataWithMessages, error) { - sort.Slice(reports, func(i, j int) bool { - return reports[i].SequenceNumberRange.Start() < reports[j].SequenceNumberRange.Start() - }) - - // If none are executed, return the (sorted) input. - if len(executedMessages) == 0 { - return reports, nil - } - - sort.Slice(executedMessages, func(i, j int) bool { - return executedMessages[i].Start() < executedMessages[j].Start() - }) - - // Make sure they do not overlap - previousMax := cciptypes.SeqNum(0) - for _, seqRange := range executedMessages { - if seqRange.Start() < previousMax { - return nil, errOverlappingRanges - } - previousMax = seqRange.End() - } - - var filtered []cciptypes.ExecutePluginCommitDataWithMessages - - reportIdx := 0 - for _, executed := range executedMessages { - for i := reportIdx; i < len(reports); i++ { - reportRange := reports[i].SequenceNumberRange - if executed.End() < reportRange.Start() { - // need to go to the next set of executed messages. - break - } - - if executed.End() < reportRange.Start() { - // add report that has non-executed messages. - reportIdx++ - filtered = append(filtered, reports[i]) - continue - } - - if reportRange.Start() >= executed.Start() && reportRange.End() <= executed.End() { - // skip fully executed report. - reportIdx++ - continue - } - - s := executed.Start() - if reportRange.Start() > executed.Start() { - s = reportRange.Start() - } - for ; s <= executed.End(); s++ { - // This range runs into the next report. - if s > reports[i].SequenceNumberRange.End() { - reportIdx++ - filtered = append(filtered, reports[i]) - break - } - reports[i].ExecutedMessages = append(reports[i].ExecutedMessages, s) - } - } - } - - // Add any remaining reports that were not fully executed. - for i := reportIdx; i < len(reports); i++ { - filtered = append(filtered, reports[i]) - } - - return filtered, nil -} - -type decodedAttributedObservation struct { - Observation cciptypes.ExecutePluginObservation - Observer commontypes.OracleID -} - -func decodeAttributedObservations(aos []types.AttributedObservation) ([]decodedAttributedObservation, error) { - decoded := make([]decodedAttributedObservation, len(aos)) - for i, ao := range aos { - observation, err := cciptypes.DecodeExecutePluginObservation(ao.Observation) - if err != nil { - return nil, err - } - decoded[i] = decodedAttributedObservation{ - Observation: observation, - Observer: ao.Observer, - } - } - return decoded, nil -} - -func mergeMessageObservations( - aos []decodedAttributedObservation, fChain map[cciptypes.ChainSelector]int, -) (cciptypes.ExecutePluginMessageObservations, error) { - // Create a validator for each chain - validators := make(map[cciptypes.ChainSelector]validation.MinObservationFilter[cciptypes.CCIPMsg]) - idFunc := func(data cciptypes.CCIPMsg) [32]byte { - return sha3.Sum256([]byte(fmt.Sprintf("%v", data))) - } - for selector, f := range fChain { - validators[selector] = validation.NewMinObservationValidator[cciptypes.CCIPMsg](f+1, idFunc) - } - - // Add messages to the validator for each chain selector. - for _, ao := range aos { - for selector, messages := range ao.Observation.Messages { - validator, ok := validators[selector] - if !ok { - return cciptypes.ExecutePluginMessageObservations{}, fmt.Errorf("no validator for chain %d", selector) - } - // Add reports - for _, msg := range messages { - if err := validator.Add(msg); err != nil { - return cciptypes.ExecutePluginMessageObservations{}, err - } - } - } - } - - results := make(cciptypes.ExecutePluginMessageObservations) - for selector, validator := range validators { - msgs, err := validator.GetValid() - if err != nil { - return cciptypes.ExecutePluginMessageObservations{}, err - } - if _, ok := results[selector]; !ok { - results[selector] = make(map[cciptypes.SeqNum]cciptypes.CCIPMsg) - } - for _, msg := range msgs { - results[selector][msg.SeqNum] = msg - } - } - - return results, nil -} - -// mergeCommitObservations merges all observations which reach the fChain threshold into a single result. -// Any observations, or subsets of observations, which do not reach the threshold are ignored. -func mergeCommitObservations( - aos []decodedAttributedObservation, fChain map[cciptypes.ChainSelector]int, -) (cciptypes.ExecutePluginCommitObservations, error) { - // Create a validator for each chain - validators := - make(map[cciptypes.ChainSelector]validation.MinObservationFilter[cciptypes.ExecutePluginCommitDataWithMessages]) - idFunc := func(data cciptypes.ExecutePluginCommitDataWithMessages) [32]byte { - return sha3.Sum256([]byte(fmt.Sprintf("%v", data))) - } - for selector, f := range fChain { - validators[selector] = - validation.NewMinObservationValidator[cciptypes.ExecutePluginCommitDataWithMessages](f+1, idFunc) - } - - // Add reports to the validator for each chain selector. - for _, ao := range aos { - for selector, commitReports := range ao.Observation.CommitReports { - validator, ok := validators[selector] - if !ok { - return cciptypes.ExecutePluginCommitObservations{}, fmt.Errorf("no validator for chain %d", selector) - } - // Add reports - for _, commitReport := range commitReports { - if err := validator.Add(commitReport); err != nil { - return cciptypes.ExecutePluginCommitObservations{}, err - } - } - } - } - - results := make(cciptypes.ExecutePluginCommitObservations) - for selector, validator := range validators { - var err error - results[selector], err = validator.GetValid() - if err != nil { - return cciptypes.ExecutePluginCommitObservations{}, err - } - } - - return results, nil -} diff --git a/core/services/ocr3/plugins/ccip/execute/plugin_functions_test.go b/core/services/ocr3/plugins/ccip/execute/plugin_functions_test.go deleted file mode 100644 index 01400bda5f..0000000000 --- a/core/services/ocr3/plugins/ccip/execute/plugin_functions_test.go +++ /dev/null @@ -1,815 +0,0 @@ -package execute - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/smartcontractkit/libocr/commontypes" - - "github.com/smartcontractkit/libocr/offchainreporting2plus/types" - - cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" -) - -func Test_validateObserverReadingEligibility(t *testing.T) { - tests := []struct { - name string - observer commontypes.OracleID - observerCfg map[commontypes.OracleID]cciptypes.ObserverInfo - observedMsgs cciptypes.ExecutePluginMessageObservations - expErr string - }{ - { - name: "ValidObserverAndMessages", - observer: commontypes.OracleID(1), - observerCfg: map[commontypes.OracleID]cciptypes.ObserverInfo{ - 1: {Reads: []cciptypes.ChainSelector{1, 2}}, - }, - observedMsgs: cciptypes.ExecutePluginMessageObservations{ - 1: {1: {}, 2: {}}, - 2: {}, - }, - }, - { - name: "ObserverNotFound", - observer: commontypes.OracleID(1), - observerCfg: map[commontypes.OracleID]cciptypes.ObserverInfo{ - 2: {Reads: []cciptypes.ChainSelector{1, 2}}, - }, - observedMsgs: cciptypes.ExecutePluginMessageObservations{ - 1: {1: {}, 2: {}}, - }, - expErr: "observer not found in config", - }, - { - name: "ObserverNotAllowedToReadChain", - observer: commontypes.OracleID(1), - observerCfg: map[commontypes.OracleID]cciptypes.ObserverInfo{ - 1: {Reads: []cciptypes.ChainSelector{1}}, - }, - observedMsgs: cciptypes.ExecutePluginMessageObservations{ - 2: {1: {}}, - }, - expErr: "observer not allowed to read from chain 2", - }, - { - name: "NoMessagesObserved", - observer: commontypes.OracleID(1), - observerCfg: map[commontypes.OracleID]cciptypes.ObserverInfo{ - 1: {Reads: []cciptypes.ChainSelector{1, 2}}, - }, - observedMsgs: cciptypes.ExecutePluginMessageObservations{}, - }, - { - name: "EmptyMessagesInChain", - observer: commontypes.OracleID(1), - observerCfg: map[commontypes.OracleID]cciptypes.ObserverInfo{ - 1: {Reads: []cciptypes.ChainSelector{1, 2}}, - }, - observedMsgs: cciptypes.ExecutePluginMessageObservations{ - 1: {}, - 2: {1: {}, 2: {}}, - }, - }, - { - name: "AllMessagesEmpty", - observer: commontypes.OracleID(1), - observerCfg: map[commontypes.OracleID]cciptypes.ObserverInfo{ - 1: {Reads: []cciptypes.ChainSelector{1, 2}}, - }, - observedMsgs: cciptypes.ExecutePluginMessageObservations{ - 1: {}, - 2: {}, - }, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - err := validateObserverReadingEligibility(tc.observer, tc.observerCfg, tc.observedMsgs) - if len(tc.expErr) != 0 { - assert.Error(t, err) - assert.ErrorContains(t, err, tc.expErr) - return - } - assert.NoError(t, err) - }) - } -} - -func Test_validateObservedSequenceNumbers(t *testing.T) { - testCases := []struct { - name string - observedData map[cciptypes.ChainSelector][]cciptypes.ExecutePluginCommitDataWithMessages - expErr bool - }{ - { - name: "ValidData", - observedData: map[cciptypes.ChainSelector][]cciptypes.ExecutePluginCommitDataWithMessages{ - 1: { - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - MerkleRoot: cciptypes.Bytes32{1}, - SequenceNumberRange: cciptypes.SeqNumRange{1, 10}, - ExecutedMessages: []cciptypes.SeqNum{1, 2, 3}, - }, - }, - }, - 2: { - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - MerkleRoot: cciptypes.Bytes32{2}, - SequenceNumberRange: cciptypes.SeqNumRange{11, 20}, - ExecutedMessages: []cciptypes.SeqNum{11, 12, 13}, - }, - }, - }, - }, - }, - { - name: "DuplicateMerkleRoot", - observedData: map[cciptypes.ChainSelector][]cciptypes.ExecutePluginCommitDataWithMessages{ - 1: { - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - MerkleRoot: cciptypes.Bytes32{1}, - SequenceNumberRange: cciptypes.SeqNumRange{1, 10}, - ExecutedMessages: []cciptypes.SeqNum{1, 2, 3}, - }, - }, - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - MerkleRoot: cciptypes.Bytes32{1}, - SequenceNumberRange: cciptypes.SeqNumRange{11, 20}, - ExecutedMessages: []cciptypes.SeqNum{11, 12, 13}, - }, - }, - }, - }, - expErr: true, - }, - { - name: "OverlappingSequenceNumberRange", - observedData: map[cciptypes.ChainSelector][]cciptypes.ExecutePluginCommitDataWithMessages{ - 1: { - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - MerkleRoot: cciptypes.Bytes32{1}, - SequenceNumberRange: cciptypes.SeqNumRange{1, 10}, - ExecutedMessages: []cciptypes.SeqNum{1, 2, 3}, - }, - }, - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - MerkleRoot: cciptypes.Bytes32{2}, - SequenceNumberRange: cciptypes.SeqNumRange{5, 15}, - ExecutedMessages: []cciptypes.SeqNum{6, 7, 8}, - }, - }, - }, - }, - expErr: true, - }, - { - name: "ExecutedMessageOutsideObservedRange", - observedData: map[cciptypes.ChainSelector][]cciptypes.ExecutePluginCommitDataWithMessages{ - 1: { - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - MerkleRoot: cciptypes.Bytes32{1}, - SequenceNumberRange: cciptypes.SeqNumRange{1, 10}, - ExecutedMessages: []cciptypes.SeqNum{1, 2, 11}, - }, - }, - }, - }, - expErr: true, - }, - { - name: "NoCommitData", - observedData: map[cciptypes.ChainSelector][]cciptypes.ExecutePluginCommitDataWithMessages{ - 1: {}, - }, - }, - { - name: "EmptyObservedData", - observedData: map[cciptypes.ChainSelector][]cciptypes.ExecutePluginCommitDataWithMessages{}, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - err := validateObservedSequenceNumbers(tc.observedData) - if tc.expErr { - assert.Error(t, err) - return - } - assert.NoError(t, err) - }) - } -} - -func Test_computeRanges(t *testing.T) { - type args struct { - reports []cciptypes.ExecutePluginCommitDataWithMessages - } - - tests := []struct { - name string - args args - want []cciptypes.SeqNumRange - err error - }{ - { - name: "empty", - args: args{reports: []cciptypes.ExecutePluginCommitDataWithMessages{}}, - want: nil, - }, - { - name: "overlapping ranges", - args: args{reports: []cciptypes.ExecutePluginCommitDataWithMessages{ - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(10, 20), - }, - }, - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(15, 25), - }, - }, - }, - }, - err: errOverlappingRanges, - }, - { - name: "simple ranges collapsed", - args: args{reports: []cciptypes.ExecutePluginCommitDataWithMessages{ - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(10, 20), - }, - }, - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(21, 40), - }, - }, - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(41, 60), - }, - }, - }, - }, - want: []cciptypes.SeqNumRange{{10, 60}}, - }, - { - name: "non-contiguous ranges", - args: args{reports: []cciptypes.ExecutePluginCommitDataWithMessages{ - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(10, 20), - }, - }, - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(30, 40), - }, - }, - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(50, 60)}, - }, - }, - }, - want: []cciptypes.SeqNumRange{{10, 20}, {30, 40}, {50, 60}}, - }, - { - name: "contiguous and non-contiguous ranges", - args: args{reports: []cciptypes.ExecutePluginCommitDataWithMessages{ - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(10, 20), - }, - }, - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(21, 40), - }, - }, - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(50, 60), - }, - }, - }, - }, - want: []cciptypes.SeqNumRange{{10, 40}, {50, 60}}, - }, - } - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - got, err := computeRanges(tt.args.reports) - if tt.err != nil { - assert.ErrorIs(t, err, tt.err) - } else { - assert.NoError(t, err) - assert.Equal(t, tt.want, got) - } - }) - } -} - -func Test_groupByChainSelector(t *testing.T) { - type args struct { - reports []cciptypes.CommitPluginReportWithMeta - } - tests := []struct { - name string - args args - want cciptypes.ExecutePluginCommitObservations - }{ - { - name: "empty", - args: args{reports: []cciptypes.CommitPluginReportWithMeta{}}, - want: cciptypes.ExecutePluginCommitObservations{}, - }, - { - name: "reports", - args: args{reports: []cciptypes.CommitPluginReportWithMeta{{ - Report: cciptypes.CommitPluginReport{ - MerkleRoots: []cciptypes.MerkleRootChain{ - {ChainSel: 1, SeqNumsRange: cciptypes.NewSeqNumRange(10, 20), MerkleRoot: cciptypes.Bytes32{1}}, - {ChainSel: 2, SeqNumsRange: cciptypes.NewSeqNumRange(30, 40), MerkleRoot: cciptypes.Bytes32{2}}, - }}}}}, - want: cciptypes.ExecutePluginCommitObservations{ - 1: { - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SourceChain: 1, - MerkleRoot: cciptypes.Bytes32{1}, - SequenceNumberRange: cciptypes.NewSeqNumRange(10, 20), - ExecutedMessages: nil, - }, - }, - }, - 2: { - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SourceChain: 2, - MerkleRoot: cciptypes.Bytes32{2}, - SequenceNumberRange: cciptypes.NewSeqNumRange(30, 40), - ExecutedMessages: nil, - }, - }, - }, - }, - }, - } - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - assert.Equalf(t, tt.want, groupByChainSelector(tt.args.reports), "groupByChainSelector(%v)", tt.args.reports) - }) - } -} - -func Test_filterOutFullyExecutedMessages(t *testing.T) { - type args struct { - reports []cciptypes.ExecutePluginCommitDataWithMessages - executedMessages []cciptypes.SeqNumRange - } - tests := []struct { - name string - args args - want []cciptypes.ExecutePluginCommitDataWithMessages - wantErr assert.ErrorAssertionFunc - }{ - { - name: "empty", - args: args{ - reports: nil, - executedMessages: nil, - }, - want: nil, - wantErr: assert.NoError, - }, - { - name: "empty2", - args: args{ - reports: []cciptypes.ExecutePluginCommitDataWithMessages{}, - executedMessages: nil, - }, - want: []cciptypes.ExecutePluginCommitDataWithMessages{}, - wantErr: assert.NoError, - }, - { - name: "no executed messages", - args: args{ - reports: []cciptypes.ExecutePluginCommitDataWithMessages{ - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(10, 20), - }, - }, - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(30, 40), - }, - }, - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(50, 60), - }, - }, - }, - executedMessages: nil, - }, - want: []cciptypes.ExecutePluginCommitDataWithMessages{ - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(10, 20)}}, - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(30, 40)}}, - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(50, 60)}}, - }, - wantErr: assert.NoError, - }, - { - name: "executed messages", - args: args{ - reports: []cciptypes.ExecutePluginCommitDataWithMessages{ - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(10, 20)}}, - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(30, 40)}}, - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(50, 60)}}, - }, - executedMessages: []cciptypes.SeqNumRange{ - cciptypes.NewSeqNumRange(0, 100), - }, - }, - want: nil, - wantErr: assert.NoError, - }, - { - name: "2 partially executed", - args: args{ - reports: []cciptypes.ExecutePluginCommitDataWithMessages{ - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(10, 20)}, - }, - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(30, 40)}, - }, - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(50, 60)}, - }, - }, - executedMessages: []cciptypes.SeqNumRange{ - cciptypes.NewSeqNumRange(15, 35), - }, - }, - want: []cciptypes.ExecutePluginCommitDataWithMessages{ - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(10, 20), - ExecutedMessages: []cciptypes.SeqNum{15, 16, 17, 18, 19, 20}, - }, - }, - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(30, 40), - ExecutedMessages: []cciptypes.SeqNum{30, 31, 32, 33, 34, 35}, - }, - }, - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(50, 60), - }, - }, - }, - wantErr: assert.NoError, - }, - { - name: "2 partially executed 1 fully executed", - args: args{ - reports: []cciptypes.ExecutePluginCommitDataWithMessages{ - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(10, 20), - }, - }, - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(30, 40), - }, - }, - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(50, 60), - }, - }, - }, - executedMessages: []cciptypes.SeqNumRange{ - cciptypes.NewSeqNumRange(15, 55), - }, - }, - want: []cciptypes.ExecutePluginCommitDataWithMessages{ - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(10, 20), - ExecutedMessages: []cciptypes.SeqNum{15, 16, 17, 18, 19, 20}, - }, - }, - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(50, 60), - ExecutedMessages: []cciptypes.SeqNum{50, 51, 52, 53, 54, 55}, - }, - }, - }, - wantErr: assert.NoError, - }, - { - name: "first report executed", - args: args{ - reports: []cciptypes.ExecutePluginCommitDataWithMessages{ - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(10, 20), - }, - }, - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(30, 40), - }, - }, - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(50, 60), - }, - }, - }, - executedMessages: []cciptypes.SeqNumRange{ - cciptypes.NewSeqNumRange(10, 20), - }, - }, - want: []cciptypes.ExecutePluginCommitDataWithMessages{ - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(30, 40), - }, - }, - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(50, 60), - }, - }, - }, - wantErr: assert.NoError, - }, - { - name: "last report executed", - args: args{ - reports: []cciptypes.ExecutePluginCommitDataWithMessages{ - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(10, 20), - }, - }, - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(30, 40), - }, - }, - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(50, 60), - }, - }, - }, - executedMessages: []cciptypes.SeqNumRange{ - cciptypes.NewSeqNumRange(50, 60), - }, - }, - want: []cciptypes.ExecutePluginCommitDataWithMessages{ - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(10, 20), - }, - }, - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(30, 40), - }, - }, - }, - wantErr: assert.NoError, - }, - { - name: "sort-report", - args: args{ - reports: []cciptypes.ExecutePluginCommitDataWithMessages{ - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(30, 40), - }, - }, - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(50, 60), - }, - }, - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(10, 20), - }, - }, - }, - executedMessages: nil, - }, - want: []cciptypes.ExecutePluginCommitDataWithMessages{ - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(10, 20), - }, - }, - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(30, 40), - }, - }, - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(50, 60), - }, - }, - }, - wantErr: assert.NoError, - }, - { - name: "sort-executed", - args: args{ - reports: []cciptypes.ExecutePluginCommitDataWithMessages{ - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(10, 20), - }, - }, - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(30, 40), - }, - }, - { - ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SequenceNumberRange: cciptypes.NewSeqNumRange(50, 60), - }, - }, - }, - executedMessages: []cciptypes.SeqNumRange{ - cciptypes.NewSeqNumRange(50, 60), - cciptypes.NewSeqNumRange(10, 20), - cciptypes.NewSeqNumRange(30, 40), - }, - }, - want: nil, - wantErr: assert.NoError, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := filterOutExecutedMessages(tt.args.reports, tt.args.executedMessages) - if !tt.wantErr(t, err, fmt.Sprintf("filterOutExecutedMessages(%v, %v)", tt.args.reports, tt.args.executedMessages)) { - return - } - assert.Equalf(t, tt.want, got, "filterOutExecutedMessages(%v, %v)", tt.args.reports, tt.args.executedMessages) - }) - } -} - -func Test_decodeAttributedObservations(t *testing.T) { - mustEncode := func(obs cciptypes.ExecutePluginObservation) []byte { - enc, err := obs.Encode() - if err != nil { - t.Fatal("Unable to encode") - } - return enc - } - tests := []struct { - name string - args []types.AttributedObservation - want []decodedAttributedObservation - wantErr assert.ErrorAssertionFunc - }{ - // TODO: Add test cases. - { - name: "empty", - args: nil, - want: []decodedAttributedObservation{}, - wantErr: assert.NoError, - }, - { - name: "one observation", - args: []types.AttributedObservation{ - { - Observer: commontypes.OracleID(1), - Observation: mustEncode(cciptypes.ExecutePluginObservation{ - CommitReports: cciptypes.ExecutePluginCommitObservations{ - 1: {{ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{MerkleRoot: cciptypes.Bytes32{1}}}}, - }, - }), - }, - }, - want: []decodedAttributedObservation{ - { - Observer: commontypes.OracleID(1), - Observation: cciptypes.ExecutePluginObservation{ - CommitReports: cciptypes.ExecutePluginCommitObservations{ - 1: {{ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{MerkleRoot: cciptypes.Bytes32{1}}}}, - }, - }, - }, - }, - wantErr: assert.NoError, - }, - { - name: "multiple observations", - args: []types.AttributedObservation{ - { - Observer: commontypes.OracleID(1), - Observation: mustEncode(cciptypes.ExecutePluginObservation{ - CommitReports: cciptypes.ExecutePluginCommitObservations{ - 1: {{ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{MerkleRoot: cciptypes.Bytes32{1}}}}, - }, - }), - }, - { - Observer: commontypes.OracleID(2), - Observation: mustEncode(cciptypes.ExecutePluginObservation{ - CommitReports: cciptypes.ExecutePluginCommitObservations{ - 2: {{ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{MerkleRoot: cciptypes.Bytes32{2}}}}, - }, - }), - }, - }, - want: []decodedAttributedObservation{ - { - Observer: commontypes.OracleID(1), - Observation: cciptypes.ExecutePluginObservation{ - CommitReports: cciptypes.ExecutePluginCommitObservations{ - 1: {{ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{MerkleRoot: cciptypes.Bytes32{1}}}}, - }, - }, - }, - { - Observer: commontypes.OracleID(2), - Observation: cciptypes.ExecutePluginObservation{ - CommitReports: cciptypes.ExecutePluginCommitObservations{ - 2: {{ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{MerkleRoot: cciptypes.Bytes32{2}}}}, - }, - }, - }, - }, - wantErr: assert.NoError, - }, - { - name: "invalid observation", - args: []types.AttributedObservation{ - { - Observer: commontypes.OracleID(1), - Observation: []byte("invalid"), - }, - }, - want: nil, - wantErr: assert.Error, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := decodeAttributedObservations(tt.args) - if !tt.wantErr(t, err, fmt.Sprintf("decodeAttributedObservations(%v)", tt.args)) { - return - } - assert.Equalf(t, tt.want, got, "decodeAttributedObservations(%v)", tt.args) - }) - } -} diff --git a/core/services/ocr3/plugins/ccip/execute/plugin_test.go b/core/services/ocr3/plugins/ccip/execute/plugin_test.go deleted file mode 100644 index 64fd4c69ec..0000000000 --- a/core/services/ocr3/plugins/ccip/execute/plugin_test.go +++ /dev/null @@ -1,168 +0,0 @@ -package execute - -import ( - "context" - "encoding/json" - "math" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - - "github.com/smartcontractkit/ccipocr3/internal/mocks" - - cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" -) - -func TestSomethingCool(t *testing.T) { - - foo := map[cciptypes.ChainSelector]int{ - cciptypes.ChainSelector(1): 1, - cciptypes.ChainSelector(math.MaxUint64): 1, - } - - js, _ := json.Marshal(foo) - t.Log(string(js)) - - b := []byte(`{"1":1,"18446744073709551615":1}`) - var bar map[cciptypes.ChainSelector]int - assert.NoError(t, json.Unmarshal(b, &bar)) - t.Log(bar) -} - -func Test_getPendingExecutedReports(t *testing.T) { - tests := []struct { - name string - reports []cciptypes.CommitPluginReportWithMeta - ranges map[cciptypes.ChainSelector][]cciptypes.SeqNumRange - want cciptypes.ExecutePluginCommitObservations - want1 time.Time - wantErr assert.ErrorAssertionFunc - }{ - // TODO: Add test cases. - { - name: "empty", - reports: nil, - ranges: nil, - want: cciptypes.ExecutePluginCommitObservations{}, - want1: time.Time{}, - wantErr: assert.NoError, - }, - { - name: "single non-executed report", - reports: []cciptypes.CommitPluginReportWithMeta{ - { - BlockNum: 999, - Timestamp: time.UnixMilli(10101010101), - Report: cciptypes.CommitPluginReport{ - MerkleRoots: []cciptypes.MerkleRootChain{ - { - ChainSel: 1, - SeqNumsRange: cciptypes.NewSeqNumRange(1, 10), - }, - }, - }, - }, - }, - ranges: map[cciptypes.ChainSelector][]cciptypes.SeqNumRange{ - 1: nil, - }, - want: cciptypes.ExecutePluginCommitObservations{ - 1: []cciptypes.ExecutePluginCommitDataWithMessages{ - {ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SourceChain: 1, - SequenceNumberRange: cciptypes.NewSeqNumRange(1, 10), - ExecutedMessages: nil, - Timestamp: time.UnixMilli(10101010101), - BlockNum: 999, - }}, - }, - }, - want1: time.UnixMilli(10101010101), - wantErr: assert.NoError, - }, - { - name: "single half-executed report", - reports: []cciptypes.CommitPluginReportWithMeta{ - { - BlockNum: 999, - Timestamp: time.UnixMilli(10101010101), - Report: cciptypes.CommitPluginReport{ - MerkleRoots: []cciptypes.MerkleRootChain{ - { - ChainSel: 1, - SeqNumsRange: cciptypes.NewSeqNumRange(1, 10), - }, - }, - }, - }, - }, - ranges: map[cciptypes.ChainSelector][]cciptypes.SeqNumRange{ - 1: { - cciptypes.NewSeqNumRange(1, 3), - cciptypes.NewSeqNumRange(7, 8), - }, - }, - want: cciptypes.ExecutePluginCommitObservations{ - 1: []cciptypes.ExecutePluginCommitDataWithMessages{ - {ExecutePluginCommitData: cciptypes.ExecutePluginCommitData{ - SourceChain: 1, - SequenceNumberRange: cciptypes.NewSeqNumRange(1, 10), - Timestamp: time.UnixMilli(10101010101), - BlockNum: 999, - ExecutedMessages: []cciptypes.SeqNum{1, 2, 3, 7, 8}, - }}, - }, - }, - want1: time.UnixMilli(10101010101), - wantErr: assert.NoError, - }, - { - name: "last timestamp", - reports: []cciptypes.CommitPluginReportWithMeta{ - { - BlockNum: 999, - Timestamp: time.UnixMilli(10101010101), - Report: cciptypes.CommitPluginReport{}, - }, - { - BlockNum: 999, - Timestamp: time.UnixMilli(9999999999999999), - Report: cciptypes.CommitPluginReport{}, - }, - }, - ranges: map[cciptypes.ChainSelector][]cciptypes.SeqNumRange{}, - want: cciptypes.ExecutePluginCommitObservations{}, - want1: time.UnixMilli(9999999999999999), - wantErr: assert.NoError, - }, - } - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - mockReader := mocks.NewCCIPReader() - mockReader.On( - "CommitReportsGTETimestamp", mock.Anything, mock.Anything, mock.Anything, mock.Anything, - ).Return(tt.reports, nil) - for k, v := range tt.ranges { - mockReader.On("ExecutedMessageRanges", mock.Anything, k, mock.Anything, mock.Anything).Return(v, nil) - } - - // CCIP Reader mocks: - // once: - // CommitReportsGTETimestamp(ctx, dest, ts, 1000) -> ([]cciptypes.CommitPluginReportWithMeta, error) - // for each chain selector: - // ExecutedMessageRanges(ctx, selector, dest, seqRange) -> ([]cciptypes.SeqNumRange, error) - - got, got1, err := getPendingExecutedReports(context.Background(), mockReader, 123, time.Now()) - if !tt.wantErr(t, err, "getPendingExecutedReports(...)") { - return - } - assert.Equalf(t, tt.want, got, "getPendingExecutedReports(...)") - assert.Equalf(t, tt.want1, got1, "getPendingExecutedReports(...)") - }) - } -} diff --git a/core/services/ocr3/plugins/ccip/go.mod b/core/services/ocr3/plugins/ccip/go.mod deleted file mode 100644 index b497e58564..0000000000 --- a/core/services/ocr3/plugins/ccip/go.mod +++ /dev/null @@ -1,50 +0,0 @@ -module github.com/smartcontractkit/ccipocr3 - -go 1.21.7 - -require ( - github.com/deckarep/golang-set/v2 v2.6.0 - github.com/smartcontractkit/chainlink-common v0.1.7-0.20240625074419-c278d083facf - github.com/smartcontractkit/libocr v0.0.0-20240419185742-fd3cab206b2c - github.com/stretchr/testify v1.9.0 - golang.org/x/crypto v0.24.0 - golang.org/x/sync v0.7.0 - google.golang.org/grpc v1.64.0 -) - -require ( - github.com/bahlo/generic-list-go v0.2.0 // indirect - github.com/beorn7/perks v1.0.1 // indirect - github.com/buger/jsonparser v1.1.1 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/invopop/jsonschema v0.12.0 // indirect - github.com/mailru/easyjson v0.7.7 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/mr-tron/base58 v1.2.0 // indirect - github.com/pelletier/go-toml/v2 v2.2.2 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_golang v1.19.1 // indirect - github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.54.0 // indirect - github.com/prometheus/procfs v0.15.1 // indirect - github.com/rogpeppe/go-internal v1.11.0 // indirect - github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect - github.com/shopspring/decimal v1.4.0 // indirect - github.com/stretchr/objx v0.5.2 // indirect - github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect - go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.0 // indirect - golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect - gonum.org/v1/gonum v0.15.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 // indirect - google.golang.org/protobuf v1.34.1 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) - -// replicating the replace directive on cosmos SDK -replace github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.3-alpha.regen.1 diff --git a/core/services/ocr3/plugins/ccip/go.sum b/core/services/ocr3/plugins/ccip/go.sum deleted file mode 100644 index c71295378b..0000000000 --- a/core/services/ocr3/plugins/ccip/go.sum +++ /dev/null @@ -1,98 +0,0 @@ -github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= -github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= -github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= -github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI= -github.com/invopop/jsonschema v0.12.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= -github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= -github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= -github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.54.0 h1:ZlZy0BgJhTwVZUn7dLOkwCZHUkrAqd3WYtcFCWnM1D8= -github.com/prometheus/common v0.54.0/go.mod h1:/TQgMJP5CuVYveyT7n/0Ix8yLNNXy9yRSkhnLTHPDIQ= -github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= -github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= -github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= -github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= -github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= -github.com/smartcontractkit/chainlink-common v0.1.7-0.20240625074419-c278d083facf h1:d9AS/K8RSVG64USb20N/U7RaPOsYPcmuLGJq7iE+caM= -github.com/smartcontractkit/chainlink-common v0.1.7-0.20240625074419-c278d083facf/go.mod h1:L32xvCpk84Nglit64OhySPMP1tM3TTBK7Tw0qZl7Sd4= -github.com/smartcontractkit/libocr v0.0.0-20240419185742-fd3cab206b2c h1:lIyMbTaF2H0Q71vkwZHX/Ew4KF2BxiKhqEXwF8rn+KI= -github.com/smartcontractkit/libocr v0.0.0-20240419185742-fd3cab206b2c/go.mod h1:fb1ZDVXACvu4frX3APHZaEBp0xi1DIm34DcA0CwTsZM= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= -github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= -go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -gonum.org/v1/gonum v0.15.0 h1:2lYxjRbTYyxkJxlhC+LvJIx3SsANPdRybu1tGj9/OrQ= -gonum.org/v1/gonum v0.15.0/go.mod h1:xzZVBJBtS+Mz4q0Yl2LJTk+OxOg4jiXZ7qBoM0uISGo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 h1:1GBuWVLM/KMVUv1t1En5Gs+gFZCNd360GGb4sSxtrhU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= -google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= -google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= -google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/core/services/ocr3/plugins/ccip/internal/libs/slicelib/bigint.go b/core/services/ocr3/plugins/ccip/internal/libs/slicelib/bigint.go deleted file mode 100644 index fd24676b92..0000000000 --- a/core/services/ocr3/plugins/ccip/internal/libs/slicelib/bigint.go +++ /dev/null @@ -1,26 +0,0 @@ -package slicelib - -import ( - "sort" - - cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" -) - -// BigIntSortedMiddle returns the middle number after sorting the provided numbers. -// nil is returned if the provided slice is empty. -// If length of the provided slice is even, the right-hand-side value of the middle 2 numbers is returned. -// The objective of this function is to always pick within the range of values reported by honest nodes -// when we have 2f+1 values. -func BigIntSortedMiddle(vals []cciptypes.BigInt) cciptypes.BigInt { - if len(vals) == 0 { - return cciptypes.BigInt{} - } - - valsCopy := make([]cciptypes.BigInt, len(vals)) - copy(valsCopy[:], vals[:]) - - sort.Slice(valsCopy, func(i, j int) bool { - return (valsCopy[i].Int).Cmp(valsCopy[j].Int) < 0 - }) - return valsCopy[len(valsCopy)/2] -} diff --git a/core/services/ocr3/plugins/ccip/internal/libs/slicelib/bigint_test.go b/core/services/ocr3/plugins/ccip/internal/libs/slicelib/bigint_test.go deleted file mode 100644 index 29ce5f2068..0000000000 --- a/core/services/ocr3/plugins/ccip/internal/libs/slicelib/bigint_test.go +++ /dev/null @@ -1,55 +0,0 @@ -package slicelib - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" -) - -func TestBigIntSortedMiddle(t *testing.T) { - tests := []struct { - name string - vals []cciptypes.BigInt - want cciptypes.BigInt - }{ - { - name: "base case", - vals: []cciptypes.BigInt{ - cciptypes.NewBigIntFromInt64(1), - cciptypes.NewBigIntFromInt64(2), - cciptypes.NewBigIntFromInt64(4), - cciptypes.NewBigIntFromInt64(5), - }, - want: cciptypes.NewBigIntFromInt64(4), - }, - { - name: "not sorted", - vals: []cciptypes.BigInt{ - cciptypes.NewBigIntFromInt64(100), - cciptypes.NewBigIntFromInt64(50), - cciptypes.NewBigIntFromInt64(30), - cciptypes.NewBigIntFromInt64(110), - }, - want: cciptypes.NewBigIntFromInt64(100), - }, - { - name: "empty slice", - vals: []cciptypes.BigInt{}, - want: cciptypes.BigInt{}, - }, - { - name: "one item", - vals: []cciptypes.BigInt{ - cciptypes.NewBigIntFromInt64(123), - }, - want: cciptypes.NewBigIntFromInt64(123), - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equalf(t, tt.want, BigIntSortedMiddle(tt.vals), "BigIntSortedMiddle(%v)", tt.vals) - }) - } -} diff --git a/core/services/ocr3/plugins/ccip/internal/libs/slicelib/generic.go b/core/services/ocr3/plugins/ccip/internal/libs/slicelib/generic.go deleted file mode 100644 index 3080bd89e5..0000000000 --- a/core/services/ocr3/plugins/ccip/internal/libs/slicelib/generic.go +++ /dev/null @@ -1,43 +0,0 @@ -package slicelib - -// GroupBy groups a slice based on a specific item property. The returned groups slice is deterministic. -func GroupBy[T any, K comparable](items []T, prop func(T) K) ([]K, map[K][]T) { - groups := make([]K, 0) - grouped := make(map[K][]T) - for _, item := range items { - k := prop(item) - if _, exists := grouped[k]; !exists { - groups = append(groups, k) - } - grouped[k] = append(grouped[k], item) - } - return groups, grouped -} - -// CountUnique counts the unique items of the provided slice. -func CountUnique[T comparable](items []T) int { - m := make(map[T]struct{}) - for _, item := range items { - m[item] = struct{}{} - } - return len(m) -} - -// Flatten flattens a slice of slices into a single slice. -func Flatten[T any](slices [][]T) []T { - res := make([]T, 0) - for _, s := range slices { - res = append(res, s...) - } - return res -} - -func Filter[T any](slice []T, valid func(T) bool) []T { - res := make([]T, 0, len(slice)) - for _, item := range slice { - if valid(item) { - res = append(res, item) - } - } - return res -} diff --git a/core/services/ocr3/plugins/ccip/internal/libs/slicelib/generic_test.go b/core/services/ocr3/plugins/ccip/internal/libs/slicelib/generic_test.go deleted file mode 100644 index 906af817cc..0000000000 --- a/core/services/ocr3/plugins/ccip/internal/libs/slicelib/generic_test.go +++ /dev/null @@ -1,179 +0,0 @@ -package slicelib - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestGroupBy(t *testing.T) { - type person struct { - id string - name string - age int - } - - testCases := []struct { - name string - items []person - expGroupNames []string - expGroups map[string][]person - }{ - { - name: "empty slice", - items: []person{}, - expGroupNames: []string{}, - expGroups: map[string][]person{}, - }, - { - name: "no duplicate", - items: []person{ - {id: "2", name: "Bob", age: 25}, - {id: "1", name: "Alice", age: 23}, - {id: "3", name: "Charlie", age: 22}, - {id: "4", name: "Dim", age: 13}, - }, - expGroupNames: []string{"2", "1", "3", "4"}, // should be deterministic - expGroups: map[string][]person{ - "1": {{id: "1", name: "Alice", age: 23}}, - "2": {{id: "2", name: "Bob", age: 25}}, - "3": {{id: "3", name: "Charlie", age: 22}}, - "4": {{id: "4", name: "Dim", age: 13}}, - }, - }, - { - name: "with duplicate", - items: []person{ - {id: "1", name: "Alice", age: 23}, - {id: "1", name: "Bob", age: 25}, - {id: "3", name: "Charlie", age: 22}, - }, - expGroupNames: []string{"1", "3"}, - expGroups: map[string][]person{ - "1": {{id: "1", name: "Alice", age: 23}, {id: "1", name: "Bob", age: 25}}, - "3": {{id: "3", name: "Charlie", age: 22}}, - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - keys, groups := GroupBy(tc.items, func(p person) string { return p.id }) - assert.Equal(t, tc.expGroupNames, keys) - assert.Equal(t, len(tc.expGroups), len(groups)) - for _, k := range keys { - assert.Equal(t, tc.expGroups[k], groups[k]) - } - }) - } -} - -func TestCountUnique(t *testing.T) { - testCases := []struct { - name string - items []string - expCount int - }{ - { - name: "empty slice", - items: []string{}, - expCount: 0, - }, - { - name: "no duplicate", - items: []string{"a", "b", "c"}, - expCount: 3, - }, - { - name: "with duplicate", - items: []string{"a", "a", "b", "c", "b"}, - expCount: 3, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - assert.Equal(t, tc.expCount, CountUnique(tc.items)) - }) - } -} - -func TestFlatten(t *testing.T) { - testCases := []struct { - name string - slices [][]int - expFlatten []int - }{ - { - name: "empty slice", - slices: [][]int{}, - expFlatten: []int{}, - }, - { - name: "no duplicate", - slices: [][]int{{1, 2}, {3, 4}, {5, 6}}, - expFlatten: []int{1, 2, 3, 4, 5, 6}, - }, - { - name: "with duplicate", - slices: [][]int{{1, 2}, {1, 2}, {3, 4}, {5, 6}}, - expFlatten: []int{1, 2, 1, 2, 3, 4, 5, 6}, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - assert.Equal(t, tc.expFlatten, Flatten(tc.slices)) - }) - } -} - -func TestFilter(t *testing.T) { - type person struct { - id string - name string - age int - } - - testCases := []struct { - name string - items []person - valid func(person) bool - expResults []person - }{ - { - name: "empty slice", - items: []person{}, - valid: func(p person) bool { return p.age > 20 }, - expResults: []person{}, - }, - { - name: "no valid item", - items: []person{ - {id: "1", name: "Alice", age: 18}, - {id: "2", name: "Bob", age: 20}, - {id: "3", name: "Charlie", age: 19}, - }, - valid: func(p person) bool { return p.age > 20 }, - expResults: []person{}, - }, - { - name: "with valid item", - items: []person{ - {id: "1", name: "Alice", age: 18}, - {id: "2", name: "Bob", age: 25}, - {id: "3", name: "Charlie", age: 19}, - }, - valid: func(p person) bool { return p.age > 20 }, - expResults: []person{ - {id: "2", name: "Bob", age: 25}, - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - assert.Equal(t, tc.expResults, Filter(tc.items, tc.valid)) - }) - } -} diff --git a/core/services/ocr3/plugins/ccip/internal/libs/testhelpers/ocr3runner.go b/core/services/ocr3/plugins/ccip/internal/libs/testhelpers/ocr3runner.go deleted file mode 100644 index 4a1aec2d48..0000000000 --- a/core/services/ocr3/plugins/ccip/internal/libs/testhelpers/ocr3runner.go +++ /dev/null @@ -1,201 +0,0 @@ -package testhelpers - -import ( - "context" - "crypto/sha256" - "encoding/hex" - "errors" - "fmt" - "math/rand" - - "github.com/smartcontractkit/libocr/commontypes" - "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" - "github.com/smartcontractkit/libocr/offchainreporting2plus/types" - - "github.com/smartcontractkit/ccipocr3/internal/libs/slicelib" -) - -var ( - ErrQuery = errors.New("error in query phase") - ErrObservation = errors.New("error in observation phase") - ErrValidateObservation = errors.New("error in validate observation phase") - ErrOutcome = errors.New("error in outcome phase") - ErrEmptyOutcome = errors.New("outcome is empty") - ErrReports = errors.New("error in reports phase") - ErrShouldAcceptAttestedReport = errors.New("error in should accept attested report phase") - ErrShouldTransmitAcceptedReport = errors.New("error in should transmit accepted report phase") -) - -// OCR3Runner is a simple runner for OCR3. -// -// TODO: move to a shared repository. -type OCR3Runner[RI any] struct { - nodes []ocr3types.ReportingPlugin[RI] - nodeIDs []commontypes.OracleID - round int - previousOutcome ocr3types.Outcome -} - -func NewOCR3Runner[RI any]( - nodes []ocr3types.ReportingPlugin[RI], nodeIDs []commontypes.OracleID, initialOutcome ocr3types.Outcome, -) *OCR3Runner[RI] { - return &OCR3Runner[RI]{ - nodes: nodes, - nodeIDs: nodeIDs, - round: 0, - previousOutcome: initialOutcome, - } -} - -// RunRound will run some basic steps of an OCR3 flow. -// This is not a full OCR3 round but only the bare minimum. -func (r *OCR3Runner[RI]) RunRound(ctx context.Context) (result RoundResult[RI], err error) { - r.round++ - seqNr := uint64(r.round) - - leaderNode := r.selectLeader() - - outcomeCtx := ocr3types.OutcomeContext{SeqNr: seqNr, PreviousOutcome: r.previousOutcome} - - q, err := leaderNode.Query(ctx, outcomeCtx) - if err != nil { - return RoundResult[RI]{}, fmt.Errorf("%w: %w", err, ErrQuery) - } - - attributedObservations := make([]types.AttributedObservation, len(r.nodes)) - for i, n := range r.nodes { - obs, err2 := n.Observation(ctx, outcomeCtx, q) - if err2 != nil { - return RoundResult[RI]{}, fmt.Errorf("%w: %w", err2, ErrObservation) - } - - attrObs := types.AttributedObservation{Observation: obs, Observer: r.nodeIDs[i]} - err = leaderNode.ValidateObservation(outcomeCtx, q, attrObs) - if err != nil { - return RoundResult[RI]{}, fmt.Errorf("%w: %w", err, ErrValidateObservation) - } - - attributedObservations[i] = attrObs - } - - outcomes := make([]ocr3types.Outcome, len(r.nodes)) - for i, n := range r.nodes { - outcome, err2 := n.Outcome(outcomeCtx, q, attributedObservations) - if err2 != nil { - return RoundResult[RI]{}, fmt.Errorf("%w: %w", err2, ErrOutcome) - } - if len(outcome) == 0 { - return RoundResult[RI]{}, ErrEmptyOutcome - } - - outcomes[i] = outcome - } - - // check that all the outcomes are the same. - if countUniqueOutcomes(outcomes) > 1 { - return RoundResult[RI]{}, fmt.Errorf("outcomes are not equal") - } - - r.previousOutcome = outcomes[0] - - allReports := make([][]ocr3types.ReportWithInfo[RI], len(r.nodes)) - for i, n := range r.nodes { - reportsWithInfo, err2 := n.Reports(seqNr, outcomes[0]) - if err2 != nil { - return RoundResult[RI]{}, fmt.Errorf("%w: %w", err2, ErrReports) - } - - allReports[i] = reportsWithInfo - } - - // check that all the reports are the same. - if countUniqueReports(slicelib.Flatten(allReports)) > 1 { - return RoundResult[RI]{}, fmt.Errorf("reports are not equal") - } - - transmitted := make([]ocr3types.ReportWithInfo[RI], 0) - notAccepted := make([]ocr3types.ReportWithInfo[RI], 0) - notTransmitted := make([]ocr3types.ReportWithInfo[RI], 0) - - for _, report := range allReports[0] { - allShouldAccept := make([]bool, len(r.nodes)) - for i, n := range r.nodes { - shouldAccept, err2 := n.ShouldAcceptAttestedReport(ctx, seqNr, report) - if err2 != nil { - return RoundResult[RI]{}, fmt.Errorf("%w: %w", err2, ErrShouldAcceptAttestedReport) - } - - allShouldAccept[i] = shouldAccept - } - if slicelib.CountUnique(allShouldAccept) > 1 { - return RoundResult[RI]{}, fmt.Errorf("should accept attested report from all oracles is not equal") - } - - if !allShouldAccept[0] { - notAccepted = append(notAccepted, report) - continue - } - - allShouldTransmit := make([]bool, len(r.nodes)) - for i, n := range r.nodes { - shouldTransmit, err2 := n.ShouldTransmitAcceptedReport(ctx, seqNr, report) - if err2 != nil { - return RoundResult[RI]{}, fmt.Errorf("%w: %w", err2, ErrShouldTransmitAcceptedReport) - } - - allShouldTransmit[i] = shouldTransmit - } - if slicelib.CountUnique(allShouldTransmit) > 1 { - return RoundResult[RI]{}, fmt.Errorf("should transmit accepted report from all oracles is not equal") - } - - if !allShouldTransmit[0] { - notTransmitted = append(notTransmitted, report) - continue - } - - transmitted = append(transmitted, report) - } - - return RoundResult[RI]{ - Transmitted: transmitted, - NotAccepted: notAccepted, - NotTransmitted: notTransmitted, - Outcome: outcomes[0], - }, nil -} - -func (r *OCR3Runner[RI]) selectLeader() ocr3types.ReportingPlugin[RI] { - numNodes := len(r.nodes) - if numNodes == 0 { - return nil - } - return r.nodes[rand.Intn(numNodes)] -} - -type RoundResult[RI any] struct { - Transmitted []ocr3types.ReportWithInfo[RI] - NotAccepted []ocr3types.ReportWithInfo[RI] - NotTransmitted []ocr3types.ReportWithInfo[RI] - Outcome []byte -} - -func countUniqueOutcomes(outcomes []ocr3types.Outcome) int { - flattenedHashes := make([]string, 0, len(outcomes)) - for _, o := range outcomes { - h := sha256.New() - h.Write(o) - flattenedHashes = append(flattenedHashes, hex.EncodeToString(h.Sum(nil))) - } - return slicelib.CountUnique(flattenedHashes) -} - -func countUniqueReports[RI any](reports []ocr3types.ReportWithInfo[RI]) int { - flattenedHashes := make([]string, 0, len(reports)) - for _, report := range reports { - h := sha256.New() - h.Write(report.Report) - flattenedHashes = append(flattenedHashes, hex.EncodeToString(h.Sum(nil))) - } - return slicelib.CountUnique(flattenedHashes) -} diff --git a/core/services/ocr3/plugins/ccip/internal/mocks/ccipreader.go b/core/services/ocr3/plugins/ccip/internal/mocks/ccipreader.go deleted file mode 100644 index 8da37ec710..0000000000 --- a/core/services/ocr3/plugins/ccip/internal/mocks/ccipreader.go +++ /dev/null @@ -1,60 +0,0 @@ -package mocks - -import ( - "context" - "time" - - "github.com/stretchr/testify/mock" - - cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" -) - -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, -) ([]cciptypes.CommitPluginReportWithMeta, error) { - args := r.Called(ctx, dest, ts, limit) - return args.Get(0).([]cciptypes.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.CCIPMsg, error) { - args := r.Called(ctx, chain, seqNumRange) - return args.Get(0).([]cciptypes.CCIPMsg), 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) Close(ctx context.Context) error { - args := r.Called(ctx) - return args.Error(0) -} - -// Interface compatibility check. -var _ cciptypes.CCIPReader = (*CCIPReader)(nil) diff --git a/core/services/ocr3/plugins/ccip/internal/mocks/contract_reader.go b/core/services/ocr3/plugins/ccip/internal/mocks/contract_reader.go deleted file mode 100644 index 25185805b9..0000000000 --- a/core/services/ocr3/plugins/ccip/internal/mocks/contract_reader.go +++ /dev/null @@ -1,67 +0,0 @@ -package mocks - -import ( - "context" - - "github.com/smartcontractkit/chainlink-common/pkg/types" - "github.com/smartcontractkit/chainlink-common/pkg/types/query" - "github.com/stretchr/testify/mock" -) - -type ContractReaderMock struct { - *mock.Mock -} - -func NewContractReaderMock() *ContractReaderMock { - return &ContractReaderMock{ - Mock: &mock.Mock{}, - } -} - -// GetLatestValue Returns given configs at initialization -func (cr *ContractReaderMock) GetLatestValue( - ctx context.Context, contractName, method string, params, returnVal any, -) error { - args := cr.Called(ctx, contractName, method, params, returnVal) - return args.Error(0) -} - -func (cr *ContractReaderMock) Bind(ctx context.Context, bindings []types.BoundContract) error { - args := cr.Called(ctx, bindings) - return args.Error(0) -} - -func (cr *ContractReaderMock) QueryKey( - ctx context.Context, - contractName string, - filter query.KeyFilter, - limitAndSort query.LimitAndSort, - sequenceDataType any, -) ([]types.Sequence, error) { - args := cr.Called(ctx, contractName, filter, limitAndSort, sequenceDataType) - return args.Get(0).([]types.Sequence), args.Error(1) -} - -func (cr *ContractReaderMock) Start(ctx context.Context) error { - args := cr.Called(ctx) - return args.Error(0) -} - -func (cr *ContractReaderMock) Close() error { - args := cr.Called() - return args.Error(0) -} - -func (cr *ContractReaderMock) Ready() error { - args := cr.Called() - return args.Error(0) -} - -func (cr *ContractReaderMock) HealthReport() map[string]error { - args := cr.Called() - return args.Get(0).(map[string]error) -} - -func (cr *ContractReaderMock) Name() string { - return "ContractReaderMock" -} diff --git a/core/services/ocr3/plugins/ccip/internal/mocks/messagehasher.go b/core/services/ocr3/plugins/ccip/internal/mocks/messagehasher.go deleted file mode 100644 index 5a85e25f4c..0000000000 --- a/core/services/ocr3/plugins/ccip/internal/mocks/messagehasher.go +++ /dev/null @@ -1,20 +0,0 @@ -package mocks - -import ( - "context" - - cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" -) - -type MessageHasher struct{} - -func NewMessageHasher() *MessageHasher { - return &MessageHasher{} -} - -func (m *MessageHasher) Hash(ctx context.Context, msg cciptypes.CCIPMsg) (cciptypes.Bytes32, error) { - // simply return the msg id as bytes32 - var b32 [32]byte - copy(b32[:], msg.ID) - return b32, nil -} diff --git a/core/services/ocr3/plugins/ccip/internal/mocks/pricesreader.go b/core/services/ocr3/plugins/ccip/internal/mocks/pricesreader.go deleted file mode 100644 index 9d1f4c5f75..0000000000 --- a/core/services/ocr3/plugins/ccip/internal/mocks/pricesreader.go +++ /dev/null @@ -1,29 +0,0 @@ -package mocks - -import ( - "context" - "math/big" - - ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" - "github.com/stretchr/testify/mock" - - cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" -) - -type TokenPricesReader struct { - *mock.Mock -} - -func NewTokenPricesReader() *TokenPricesReader { - return &TokenPricesReader{ - Mock: &mock.Mock{}, - } -} - -func (t TokenPricesReader) GetTokenPricesUSD(ctx context.Context, tokens []ocrtypes.Account) ([]*big.Int, error) { - args := t.Called(ctx, tokens) - return args.Get(0).([]*big.Int), args.Error(1) -} - -// Interface compatibility check. -var _ cciptypes.TokenPricesReader = (*TokenPricesReader)(nil) diff --git a/core/services/ocr3/plugins/ccip/internal/mocks/reportcodec.go b/core/services/ocr3/plugins/ccip/internal/mocks/reportcodec.go deleted file mode 100644 index b29c93603c..0000000000 --- a/core/services/ocr3/plugins/ccip/internal/mocks/reportcodec.go +++ /dev/null @@ -1,24 +0,0 @@ -package mocks - -import ( - "context" - "encoding/json" - - cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" -) - -type CommitPluginJSONReportCodec struct{} - -func NewCommitPluginJSONReportCodec() *CommitPluginJSONReportCodec { - return &CommitPluginJSONReportCodec{} -} - -func (c CommitPluginJSONReportCodec) Encode(ctx context.Context, report cciptypes.CommitPluginReport) ([]byte, error) { - return json.Marshal(report) -} - -func (c CommitPluginJSONReportCodec) Decode(ctx context.Context, bytes []byte) (cciptypes.CommitPluginReport, error) { - report := cciptypes.CommitPluginReport{} - err := json.Unmarshal(bytes, &report) - return report, err -} diff --git a/core/services/ocr3/plugins/ccip/internal/reader/ccip.go b/core/services/ocr3/plugins/ccip/internal/reader/ccip.go deleted file mode 100644 index cdf8d046bc..0000000000 --- a/core/services/ocr3/plugins/ccip/internal/reader/ccip.go +++ /dev/null @@ -1,194 +0,0 @@ -package reader - -import ( - "context" - "errors" - "fmt" - "time" - - "github.com/smartcontractkit/chainlink-common/pkg/types" - cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" - "github.com/smartcontractkit/chainlink-common/pkg/types/query" - "github.com/smartcontractkit/chainlink-common/pkg/types/query/primitives" - "golang.org/x/sync/errgroup" -) - -var ( - ErrContractReaderNotFound = errors.New("contract reader not found") - ErrContractWriterNotFound = errors.New("contract writer not found") -) - -// TODO: unit test the implementation when the actual contract reader and writer interfaces are finalized and mocks -// can be generated. -type CCIPChainReader struct { - contractReaders map[cciptypes.ChainSelector]types.ContractReader - contractWriters map[cciptypes.ChainSelector]types.ChainWriter - destChain cciptypes.ChainSelector -} - -func NewCCIPChainReader( - contractReaders map[cciptypes.ChainSelector]types.ContractReader, - contractWriters map[cciptypes.ChainSelector]types.ChainWriter, - destChain cciptypes.ChainSelector, -) *CCIPChainReader { - return &CCIPChainReader{ - contractReaders: contractReaders, - contractWriters: contractWriters, - destChain: destChain, - } -} - -func (r *CCIPChainReader) CommitReportsGTETimestamp( - ctx context.Context, dest cciptypes.ChainSelector, ts time.Time, limit int, -) ([]cciptypes.CommitPluginReportWithMeta, error) { - if err := r.validateReaderExistence(dest); err != nil { - return nil, err - } - panic("implement me") -} - -func (r *CCIPChainReader) ExecutedMessageRanges( - ctx context.Context, source, dest cciptypes.ChainSelector, seqNumRange cciptypes.SeqNumRange, -) ([]cciptypes.SeqNumRange, error) { - if err := r.validateReaderExistence(source, dest); err != nil { - return nil, err - } - panic("implement me") -} - -func (r *CCIPChainReader) MsgsBetweenSeqNums( - ctx context.Context, chain cciptypes.ChainSelector, seqNumRange cciptypes.SeqNumRange, -) ([]cciptypes.CCIPMsg, error) { - if err := r.validateReaderExistence(chain); err != nil { - return nil, err - } - - const ( - contractName = "OnRamp" - eventName = "CCIPSendRequested" - eventAttributeName = "SequenceNumber" - ) - - seq, err := r.contractReaders[chain].QueryKey( - ctx, - contractName, - query.KeyFilter{ - Key: eventName, - Expressions: []query.Expression{ - { - Primitive: &primitives.Comparator{ - Name: eventAttributeName, - ValueComparators: []primitives.ValueComparator{ - { - Value: seqNumRange.Start().String(), - Operator: primitives.Gte, - }, - { - Value: seqNumRange.End().String(), - Operator: primitives.Lte, - }, - }, - }, - BoolExpression: query.BoolExpression{}, - }, - }, - }, - query.LimitAndSort{ - SortBy: []query.SortBy{ - query.NewSortByTimestamp(query.Asc), - }, - Limit: query.Limit{ - Count: uint64(seqNumRange.End() - seqNumRange.Start() + 1), - }, - }, - &cciptypes.CCIPMsg{}, - ) - if err != nil { - return nil, fmt.Errorf("failed to query onRamp: %w", err) - } - - msgs := make([]cciptypes.CCIPMsg, 0) - for _, item := range seq { - msg, ok := item.Data.(cciptypes.CCIPMsg) - if !ok { - return nil, fmt.Errorf("failed to cast %v to CCIPMsg", item.Data) - } - msgs = append(msgs, msg) - } - - return msgs, nil -} - -func (r *CCIPChainReader) NextSeqNum( - ctx context.Context, chains []cciptypes.ChainSelector, -) ([]cciptypes.SeqNum, error) { - if err := r.validateReaderExistence(r.destChain); err != nil { - return nil, err - } - - const ( - contractName = "OffRamp" - funcName = "getExpectedNextSequenceNumbers" - ) - - seqNums := make([]cciptypes.SeqNum, 0) - err := r.contractReaders[r.destChain].GetLatestValue( - ctx, - contractName, - funcName, - map[string]any{ - "chains": chains, - }, - &seqNums, - ) - return seqNums, err -} - -func (r *CCIPChainReader) GasPrices(ctx context.Context, chains []cciptypes.ChainSelector) ([]cciptypes.BigInt, error) { - if err := r.validateWriterExistence(chains...); err != nil { - return nil, err - } - - eg := new(errgroup.Group) - gasPrices := make([]cciptypes.BigInt, len(chains)) - for i, chain := range chains { - i, chain := i, chain - eg.Go(func() error { - gasPrice, err := r.contractWriters[chain].GetFeeComponents(ctx) - if err != nil { - return fmt.Errorf("failed to get gas price: %w", err) - } - gasPrices[i] = cciptypes.NewBigInt(gasPrice.ExecutionFee) - return nil - }) - } - - if err := eg.Wait(); err != nil { - return nil, err - } - return gasPrices, nil -} - -func (r *CCIPChainReader) Close(ctx context.Context) error { - return nil -} - -func (r *CCIPChainReader) validateReaderExistence(chains ...cciptypes.ChainSelector) error { - for _, ch := range chains { - _, exists := r.contractReaders[ch] - if !exists { - return fmt.Errorf("chain %d: %w", ch, ErrContractReaderNotFound) - } - } - return nil -} - -func (r *CCIPChainReader) validateWriterExistence(chains ...cciptypes.ChainSelector) error { - for _, ch := range chains { - _, exists := r.contractWriters[ch] - if !exists { - return fmt.Errorf("chain %d: %w", ch, ErrContractWriterNotFound) - } - } - return nil -} diff --git a/core/services/ocr3/plugins/ccip/internal/reader/home_chain.go b/core/services/ocr3/plugins/ccip/internal/reader/home_chain.go deleted file mode 100644 index ff2409250a..0000000000 --- a/core/services/ocr3/plugins/ccip/internal/reader/home_chain.go +++ /dev/null @@ -1,326 +0,0 @@ -package reader - -import ( - "context" - "fmt" - "sync" - "time" - - mapset "github.com/deckarep/golang-set/v2" - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-common/pkg/services" - "github.com/smartcontractkit/chainlink-common/pkg/types" - cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" - libocrtypes "github.com/smartcontractkit/libocr/ragep2p/types" -) - -type HomeChain interface { - GetChainConfig(chainSelector cciptypes.ChainSelector) (ChainConfig, error) - GetAllChainConfigs() (map[cciptypes.ChainSelector]ChainConfig, error) - // GetSupportedChainsForPeer Gets all chain selectors that the peerID can read/write from/to - GetSupportedChainsForPeer(id libocrtypes.PeerID) (mapset.Set[cciptypes.ChainSelector], error) - // GetKnownCCIPChains Gets all chain selectors that are known to CCIP - GetKnownCCIPChains() (mapset.Set[cciptypes.ChainSelector], error) - // GetFChain Gets the FChain value for each chain - GetFChain() (map[cciptypes.ChainSelector]int, error) - // GetOCRConfigs Gets the OCR3Configs for a given donID and pluginType - GetOCRConfigs(ctx context.Context, donID uint32, pluginType uint8) ([]OCR3ConfigWithMeta, error) - services.Service -} - -type state struct { - // gets updated by the polling loop - chainConfigs map[cciptypes.ChainSelector]ChainConfig - // mapping between each node's peerID and the chains it supports. derived from chainConfigs - nodeSupportedChains map[libocrtypes.PeerID]mapset.Set[cciptypes.ChainSelector] - // set of chains that are known to CCIP, derived from chainConfigs - knownSourceChains mapset.Set[cciptypes.ChainSelector] - // map of chain to FChain value, derived from chainConfigs - fChain map[cciptypes.ChainSelector]int -} - -type homeChainPoller struct { - stopCh services.StopChan - sync services.StateMachine - homeChainReader types.ContractReader - lggr logger.Logger - mutex *sync.RWMutex - state state - failedPolls uint - // How frequently the poller fetches the chain configs - pollingDuration time.Duration -} - -const MaxFailedPolls = 10 - -func NewHomeChainConfigPoller( - homeChainReader types.ContractReader, - lggr logger.Logger, - pollingInterval time.Duration, -) HomeChain { - return &homeChainPoller{ - stopCh: make(chan struct{}), - homeChainReader: homeChainReader, - state: state{}, - mutex: &sync.RWMutex{}, - failedPolls: 0, - lggr: lggr, - pollingDuration: pollingInterval, - } -} - -func (r *homeChainPoller) Start(ctx context.Context) error { - err := r.fetchAndSetConfigs(ctx) - if err != nil { - // Just log, don't return error as we want to keep polling - r.lggr.Errorw("Initial fetch of on-chain configs failed", "err", err) - } - r.lggr.Infow("Start Polling ChainConfig") - return r.sync.StartOnce(r.Name(), func() error { - go r.poll() - return nil - }) -} - -func (r *homeChainPoller) poll() { - ctx, cancel := r.stopCh.NewCtx() - defer cancel() - ticker := time.NewTicker(r.pollingDuration) - defer ticker.Stop() - for { - select { - case <-ctx.Done(): - r.mutex.Lock() - r.failedPolls = 0 - r.mutex.Unlock() - return - case <-ticker.C: - if err := r.fetchAndSetConfigs(ctx); err != nil { - r.mutex.Lock() - r.failedPolls++ - r.mutex.Unlock() - r.lggr.Errorw("fetching and setting configs failed", "failedPolls", r.failedPolls, "err", err) - } - } - } -} - -func (r *homeChainPoller) fetchAndSetConfigs(ctx context.Context) error { - var chainConfigInfos []ChainConfigInfo - err := r.homeChainReader.GetLatestValue( - ctx, "CCIPCapabilityConfiguration", "getAllChainConfigs", nil, &chainConfigInfos, - ) - if err != nil { - r.lggr.Errorw("fetching on-chain configs failed", "err", err) - return err - } - if len(chainConfigInfos) == 0 { - // That's a legitimate case if there are no chain configs on chain yet - r.lggr.Warnw("no on chain configs found") - return nil - } - homeChainConfigs, err := convertOnChainConfigToHomeChainConfig(chainConfigInfos) - if err != nil { - r.lggr.Errorw("error converting OnChainConfigs to ChainConfig", "err", err) - return err - } - r.lggr.Infow("Setting ChainConfig") - r.setState(homeChainConfigs) - return nil -} - -func (r *homeChainPoller) setState(chainConfigs map[cciptypes.ChainSelector]ChainConfig) { - r.mutex.Lock() - defer r.mutex.Unlock() - s := &r.state - s.chainConfigs = chainConfigs - s.nodeSupportedChains = createNodesSupportedChains(chainConfigs) - s.knownSourceChains = createKnownChains(chainConfigs) - s.fChain = createFChain(chainConfigs) -} - -func (r *homeChainPoller) GetChainConfig(chainSelector cciptypes.ChainSelector) (ChainConfig, error) { - r.mutex.RLock() - defer r.mutex.RUnlock() - s := r.state - if chainConfig, ok := s.chainConfigs[chainSelector]; ok { - return chainConfig, nil - } - return ChainConfig{}, fmt.Errorf("chain config not found for chain %v", chainSelector) -} - -func (r *homeChainPoller) GetAllChainConfigs() (map[cciptypes.ChainSelector]ChainConfig, error) { - r.mutex.RLock() - defer r.mutex.RUnlock() - return r.state.chainConfigs, nil -} - -func (r *homeChainPoller) GetSupportedChainsForPeer( - id libocrtypes.PeerID, -) (mapset.Set[cciptypes.ChainSelector], error) { - r.mutex.RLock() - defer r.mutex.RUnlock() - s := r.state - if _, ok := s.nodeSupportedChains[id]; !ok { - // empty set to denote no chains supported - return mapset.NewSet[cciptypes.ChainSelector](), nil - } - return s.nodeSupportedChains[id], nil -} - -func (r *homeChainPoller) GetKnownCCIPChains() (mapset.Set[cciptypes.ChainSelector], error) { - r.mutex.RLock() - defer r.mutex.RUnlock() - knownSourceChains := mapset.NewSet[cciptypes.ChainSelector]() - for chain := range r.state.chainConfigs { - knownSourceChains.Add(chain) - } - - return knownSourceChains, nil -} - -func (r *homeChainPoller) GetFChain() (map[cciptypes.ChainSelector]int, error) { - r.mutex.RLock() - defer r.mutex.RUnlock() - return r.state.fChain, nil -} - -func (r *homeChainPoller) GetOCRConfigs( - ctx context.Context, donID uint32, pluginType uint8, -) ([]OCR3ConfigWithMeta, error) { - var ocrConfigs []OCR3ConfigWithMeta - err := r.homeChainReader.GetLatestValue(ctx, "CCIPCapabilityConfiguration", "getOCRConfig", map[string]any{ - "donId": donID, - "pluginType": pluginType, - }, &ocrConfigs) - if err != nil { - return nil, fmt.Errorf("error fetching OCR configs: %w", err) - } - - return ocrConfigs, nil -} - -func (r *homeChainPoller) Close() error { - return r.sync.StopOnce(r.Name(), func() error { - close(r.stopCh) - return nil - }) -} - -func (r *homeChainPoller) Ready() error { - r.mutex.RLock() - defer r.mutex.RUnlock() - return r.sync.Ready() -} - -func (r *homeChainPoller) HealthReport() map[string]error { - r.mutex.RLock() - defer r.mutex.RUnlock() - if r.failedPolls >= MaxFailedPolls { - r.sync.SvcErrBuffer.Append(fmt.Errorf("polling failed %d times in a row", MaxFailedPolls)) - } - return map[string]error{r.Name(): r.sync.Healthy()} -} - -func (r *homeChainPoller) Name() string { - return "homeChainPoller" -} - -func createFChain(chainConfigs map[cciptypes.ChainSelector]ChainConfig) map[cciptypes.ChainSelector]int { - fChain := map[cciptypes.ChainSelector]int{} - for chain, config := range chainConfigs { - fChain[chain] = config.FChain - } - return fChain -} - -func createKnownChains(chainConfigs map[cciptypes.ChainSelector]ChainConfig) mapset.Set[cciptypes.ChainSelector] { - knownChains := mapset.NewSet[cciptypes.ChainSelector]() - for chain := range chainConfigs { - knownChains.Add(chain) - } - return knownChains -} - -func createNodesSupportedChains( - chainConfigs map[cciptypes.ChainSelector]ChainConfig, -) map[libocrtypes.PeerID]mapset.Set[cciptypes.ChainSelector] { - nodeSupportedChains := map[libocrtypes.PeerID]mapset.Set[cciptypes.ChainSelector]{} - for chainSelector, config := range chainConfigs { - for _, p2pID := range config.SupportedNodes.ToSlice() { - if _, ok := nodeSupportedChains[p2pID]; !ok { - nodeSupportedChains[p2pID] = mapset.NewSet[cciptypes.ChainSelector]() - } - //add chain to SupportedChains - nodeSupportedChains[p2pID].Add(chainSelector) - } - } - return nodeSupportedChains -} - -func convertOnChainConfigToHomeChainConfig( - capabilityConfigs []ChainConfigInfo, -) (map[cciptypes.ChainSelector]ChainConfig, error) { - chainConfigs := make(map[cciptypes.ChainSelector]ChainConfig) - for _, capabilityConfig := range capabilityConfigs { - chainSelector := capabilityConfig.ChainSelector - config := capabilityConfig.ChainConfig - - chainConfigs[chainSelector] = ChainConfig{ - FChain: int(config.FChain), - SupportedNodes: mapset.NewSet(config.Readers...), - } - } - return chainConfigs, nil -} - -// HomeChainConfigMapper This is a 1-1 mapping between the config that we get from the contract to make -// se/deserializing easier -type HomeChainConfigMapper struct { - Readers []libocrtypes.PeerID `json:"readers"` - FChain uint8 `json:"fChain"` - Config []byte `json:"config"` -} - -// ChainConfigInfo This is a 1-1 mapping between the config that we get from the contract to make -// se/deserializing easier -type ChainConfigInfo struct { - // nolint:lll // don't split up the long url - // Calling function https://github.com/smartcontractkit/ccip/blob/330c5e98f624cfb10108c92fe1e00ced6d345a99/contracts/src/v0.8/ccip/capability/CCIPCapabilityConfiguration.sol#L140 - ChainSelector cciptypes.ChainSelector `json:"chainSelector"` - ChainConfig HomeChainConfigMapper `json:"chainConfig"` -} - -// ChainConfig will live on the home chain and will be used to update chain configuration like F value and supported -// nodes dynamically. -type ChainConfig struct { - // FChain defines the FChain value for the chain. FChain is used while forming consensus based on the observations. - FChain int `json:"fChain"` - // SupportedNodes is a map of PeerIDs to SupportedChains. - SupportedNodes mapset.Set[libocrtypes.PeerID] `json:"supportedNodes"` - // Config is the chain specific configuration. - Config []byte `json:"config"` -} - -// OCR3Config mirrors CCIPCapabilityConfiguration.sol's OCR3Config struct -type OCR3Config struct { - PluginType uint8 `json:"pluginType"` - ChainSelector cciptypes.ChainSelector `json:"chainSelector"` - F uint8 `json:"F"` - OffchainConfigVersion uint64 `json:"offchainConfigVersion"` - OfframpAddress []byte `json:"offrampAddress"` - BootstrapP2PIds [][32]byte `json:"bootstrapP2PIds"` - P2PIds [][32]byte `json:"p2pIds"` - Signers [][]byte `json:"signers"` - Transmitters [][]byte `json:"transmitters"` - OffchainConfig []byte `json:"offchainConfig"` -} - -// OCR3ConfigWithmeta mirrors CCIPCapabilityConfiguration.sol's OCR3ConfigWithMeta struct -type OCR3ConfigWithMeta struct { - Config OCR3Config `json:"config"` - ConfigCount uint64 `json:"configCount"` - ConfigDigest [32]byte `json:"configDigest"` -} - -var _ HomeChain = (*homeChainPoller)(nil) diff --git a/core/services/ocr3/plugins/ccip/internal/reader/home_chain_test.go b/core/services/ocr3/plugins/ccip/internal/reader/home_chain_test.go deleted file mode 100644 index f08f44bf7f..0000000000 --- a/core/services/ocr3/plugins/ccip/internal/reader/home_chain_test.go +++ /dev/null @@ -1,201 +0,0 @@ -package reader - -import ( - "context" - "fmt" - "testing" - "time" - - mapset "github.com/deckarep/golang-set/v2" - libocrtypes "github.com/smartcontractkit/libocr/ragep2p/types" - - "github.com/smartcontractkit/ccipocr3/internal/mocks" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" - "github.com/smartcontractkit/libocr/commontypes" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" -) - -var ( - chainA = cciptypes.ChainSelector(1) - chainB = cciptypes.ChainSelector(2) - chainC = cciptypes.ChainSelector(3) - oracleAId = commontypes.OracleID(1) - p2pOracleAId = libocrtypes.PeerID{byte(oracleAId)} - oracleBId = commontypes.OracleID(2) - p2pOracleBId = libocrtypes.PeerID{byte(oracleBId)} - oracleCId = commontypes.OracleID(3) - p2pOracleCId = libocrtypes.PeerID{byte(oracleCId)} -) - -func TestHomeChainConfigPoller_HealthReport(t *testing.T) { - homeChainReader := mocks.NewContractReaderMock() - homeChainReader.On( - "GetLatestValue", - mock.Anything, - "CCIPCapabilityConfiguration", - "getAllChainConfigs", - mock.Anything, - mock.Anything).Return(fmt.Errorf("error")) - - var ( - tickTime = 10 * time.Millisecond - totalSleepTime = 11 * tickTime - ) - - configPoller := NewHomeChainConfigPoller( - homeChainReader, - logger.Test(t), - tickTime, - ) - _ = configPoller.Start(context.Background()) - - // Initially it's healthy - healthy := configPoller.HealthReport() - assert.Equal(t, map[string]error{configPoller.Name(): error(nil)}, healthy) - - // After one second it will try polling 10 times and fail - time.Sleep(totalSleepTime) - - errors := configPoller.HealthReport() - - err := configPoller.Close() - time.Sleep(tickTime) - assert.NoError(t, err) - assert.Equal(t, 1, len(errors)) - assert.Errorf(t, errors[configPoller.Name()], "polling failed %d times in a row", MaxFailedPolls) -} - -func Test_PollingWorking(t *testing.T) { - onChainConfigs := []ChainConfigInfo{ - { - ChainSelector: chainA, - ChainConfig: HomeChainConfigMapper{ - FChain: 1, - Readers: []libocrtypes.PeerID{ - p2pOracleAId, - p2pOracleBId, - p2pOracleCId, - }, - Config: []byte{0}, - }, - }, - { - ChainSelector: chainB, - ChainConfig: HomeChainConfigMapper{ - FChain: 2, - Readers: []libocrtypes.PeerID{ - p2pOracleAId, - p2pOracleBId, - }, - Config: []byte{0}, - }, - }, - { - ChainSelector: chainC, - ChainConfig: HomeChainConfigMapper{ - FChain: 3, - Readers: []libocrtypes.PeerID{ - p2pOracleCId, - }, - Config: []byte{0}, - }, - }, - } - homeChainConfig := map[cciptypes.ChainSelector]ChainConfig{ - chainA: { - FChain: 1, - SupportedNodes: mapset.NewSet(p2pOracleAId, p2pOracleBId, p2pOracleCId), - }, - chainB: { - FChain: 2, - SupportedNodes: mapset.NewSet(p2pOracleAId, p2pOracleBId), - }, - chainC: { - FChain: 3, - SupportedNodes: mapset.NewSet(p2pOracleCId), - }, - } - - homeChainReader := mocks.NewContractReaderMock() - homeChainReader.On( - "GetLatestValue", mock.Anything, "CCIPCapabilityConfiguration", "getAllChainConfigs", mock.Anything, mock.Anything, - ).Run( - func(args mock.Arguments) { - arg := args.Get(4).(*[]ChainConfigInfo) - *arg = onChainConfigs - }).Return(nil) - - var ( - tickTime = 20 * time.Millisecond - totalSleepTime = (tickTime * 2) + (10 * time.Millisecond) - expNumCalls = int(totalSleepTime/tickTime) + 1 // +1 for the initial call - ) - - configPoller := NewHomeChainConfigPoller( - homeChainReader, - logger.Test(t), - tickTime, - ) - - ctx := context.Background() - err := configPoller.Start(ctx) - assert.NoError(t, err) - time.Sleep(totalSleepTime) - err = configPoller.Close() - assert.NoError(t, err) - - // called 3 times, once when it's started, and 2 times when it's polling - homeChainReader.AssertNumberOfCalls(t, "GetLatestValue", expNumCalls) - - configs, err := configPoller.GetAllChainConfigs() - assert.NoError(t, err) - assert.Equal(t, homeChainConfig, configs) -} - -func Test_HomeChainPoller_GetOCRConfig(t *testing.T) { - donID := uint32(1) - pluginType := uint8(1) // execution - homeChainReader := mocks.NewContractReaderMock() - homeChainReader.On( - "GetLatestValue", - mock.Anything, - "CCIPCapabilityConfiguration", - "getOCRConfig", - map[string]any{ - "donId": donID, - "pluginType": pluginType, - }, - mock.AnythingOfType("*[]reader.OCR3ConfigWithMeta"), - ).Return(nil).Run(func(args mock.Arguments) { - arg := args.Get(4).(*[]OCR3ConfigWithMeta) - *arg = append(*arg, OCR3ConfigWithMeta{ - ConfigCount: 1, - Config: OCR3Config{ - PluginType: pluginType, - ChainSelector: 1, - F: 1, - OfframpAddress: []byte("offramp"), - }, - }) - }) - defer homeChainReader.AssertExpectations(t) - - configPoller := NewHomeChainConfigPoller( - homeChainReader, - logger.Test(t), - 10*time.Millisecond, - ) - - configs, err := configPoller.GetOCRConfigs(context.Background(), donID, pluginType) - require.NoError(t, err) - require.Len(t, configs, 1) - require.Equal(t, uint8(1), configs[0].Config.PluginType) - require.Equal(t, cciptypes.ChainSelector(1), configs[0].Config.ChainSelector) - require.Equal(t, uint8(1), configs[0].Config.F) - require.Equal(t, []byte("offramp"), configs[0].Config.OfframpAddress) -} diff --git a/core/services/ocr3/plugins/ccip/internal/reader/onchain_prices_reader.go b/core/services/ocr3/plugins/ccip/internal/reader/onchain_prices_reader.go deleted file mode 100644 index 6c747bf48b..0000000000 --- a/core/services/ocr3/plugins/ccip/internal/reader/onchain_prices_reader.go +++ /dev/null @@ -1,70 +0,0 @@ -package reader - -import ( - "context" - "fmt" - "math/big" - - commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" - ocr2types "github.com/smartcontractkit/libocr/offchainreporting2plus/types" - "golang.org/x/sync/errgroup" -) - -type TokenPriceConfig struct { - // This is mainly used for inputTokens on testnet to give them a price - StaticPrices map[ocr2types.Account]big.Int `json:"staticPrices"` -} - -type OnchainTokenPricesReader struct { - TokenPriceConfig TokenPriceConfig - // Reader for the chain that will have the token prices on-chain - ContractReader commontypes.ContractReader -} - -func NewOnchainTokenPricesReader( - tokenPriceConfig TokenPriceConfig, contractReader commontypes.ContractReader, -) *OnchainTokenPricesReader { - return &OnchainTokenPricesReader{ - TokenPriceConfig: tokenPriceConfig, - ContractReader: contractReader, - } -} - -func (pr *OnchainTokenPricesReader) GetTokenPricesUSD( - ctx context.Context, tokens []ocr2types.Account, -) ([]*big.Int, error) { - const ( - contractName = "PriceAggregator" - functionName = "getTokenPrice" - ) - prices := make([]*big.Int, len(tokens)) - eg := new(errgroup.Group) - for idx, token := range tokens { - idx := idx - token := token - eg.Go(func() error { - price := new(big.Int) - if staticPrice, exists := pr.TokenPriceConfig.StaticPrices[token]; exists { - price.Set(&staticPrice) - } else { - if err := pr.ContractReader.GetLatestValue(ctx, contractName, functionName, token, price); err != nil { - return fmt.Errorf("failed to get token price for %s: %w", token, err) - } - } - prices[idx] = price - return nil - }) - } - - if err := eg.Wait(); err != nil { - return nil, fmt.Errorf("failed to get all token prices successfully: %w", err) - } - - for _, price := range prices { - if price == nil { - return nil, fmt.Errorf("failed to get all token prices successfully, some prices are nil") - } - } - - return prices, nil -} diff --git a/core/services/ocr3/plugins/ccip/internal/reader/onchain_prices_reader_test.go b/core/services/ocr3/plugins/ccip/internal/reader/onchain_prices_reader_test.go deleted file mode 100644 index f57183134d..0000000000 --- a/core/services/ocr3/plugins/ccip/internal/reader/onchain_prices_reader_test.go +++ /dev/null @@ -1,113 +0,0 @@ -package reader - -import ( - "context" - "fmt" - "math/big" - "testing" - - "github.com/smartcontractkit/ccipocr3/internal/mocks" - - ocr2types "github.com/smartcontractkit/libocr/offchainreporting2plus/types" - - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" -) - -const ( - EthAcc = ocr2types.Account("ETH") - OpAcc = ocr2types.Account("OP") - ArbAcc = ocr2types.Account("ARB") -) - -var ( - EthPrice = big.NewInt(100) - OpPrice = big.NewInt(10) - ArbPrice = big.NewInt(1) -) - -func TestOnchainTokenPricesReader_GetTokenPricesUSD(t *testing.T) { - testCases := []struct { - name string - staticPrices map[ocr2types.Account]big.Int - inputTokens []ocr2types.Account - mockPrices map[ocr2types.Account]*big.Int - want []*big.Int - errorAccounts []ocr2types.Account - wantErr bool - }{ - { - name: "Static price only", - staticPrices: map[ocr2types.Account]big.Int{EthAcc: *EthPrice, OpAcc: *OpPrice}, - inputTokens: []ocr2types.Account{EthAcc, OpAcc}, - mockPrices: map[ocr2types.Account]*big.Int{}, - want: []*big.Int{EthPrice, OpPrice}, - }, - { - name: "On-chain price only", - staticPrices: map[ocr2types.Account]big.Int{}, - inputTokens: []ocr2types.Account{ArbAcc, OpAcc, EthAcc}, - mockPrices: map[ocr2types.Account]*big.Int{OpAcc: OpPrice, ArbAcc: ArbPrice, EthAcc: EthPrice}, - want: []*big.Int{ArbPrice, OpPrice, EthPrice}, - }, - { - name: "Mix of static price and onchain price", - staticPrices: map[ocr2types.Account]big.Int{EthAcc: *EthPrice}, - inputTokens: []ocr2types.Account{EthAcc, OpAcc, ArbAcc}, - mockPrices: map[ocr2types.Account]*big.Int{ArbAcc: ArbPrice, OpAcc: OpPrice}, - want: []*big.Int{EthPrice, OpPrice, ArbPrice}, - }, - { - name: "Missing price should error", - staticPrices: map[ocr2types.Account]big.Int{}, - inputTokens: []ocr2types.Account{ArbAcc, OpAcc, EthAcc}, - mockPrices: map[ocr2types.Account]*big.Int{OpAcc: OpPrice, ArbAcc: ArbPrice}, - errorAccounts: []ocr2types.Account{EthAcc}, - want: nil, - wantErr: true, - }, - } - - for _, tc := range testCases { - contractReader := createMockReader(tc.mockPrices, tc.errorAccounts) - tokenPricesReader := OnchainTokenPricesReader{ - TokenPriceConfig: TokenPriceConfig{StaticPrices: tc.staticPrices}, - ContractReader: contractReader, - } - t.Run(tc.name, func(t *testing.T) { - ctx := context.Background() - result, err := tokenPricesReader.GetTokenPricesUSD(ctx, tc.inputTokens) - - if tc.wantErr { - require.Error(t, err) - return - } - - require.NoError(t, err) - require.Equal(t, tc.want, result) - }) - } - -} - -func createMockReader( - mockPrices map[ocr2types.Account]*big.Int, errorAccounts []ocr2types.Account, -) *mocks.ContractReaderMock { - reader := mocks.NewContractReaderMock() - for _, acc := range errorAccounts { - acc := acc - reader.On( - "GetLatestValue", mock.Anything, "PriceAggregator", "getTokenPrice", acc, mock.Anything, - ).Return(fmt.Errorf("error")) - } - for acc, price := range mockPrices { - acc := acc - price := price - reader.On("GetLatestValue", mock.Anything, "PriceAggregator", "getTokenPrice", acc, mock.Anything).Run( - func(args mock.Arguments) { - arg := args.Get(4).(*big.Int) - arg.Set(price) - }).Return(nil) - } - return reader -} diff --git a/core/services/ocr3/plugins/ccip/pkg/reader/home_chain.go b/core/services/ocr3/plugins/ccip/pkg/reader/home_chain.go deleted file mode 100644 index 7d56122952..0000000000 --- a/core/services/ocr3/plugins/ccip/pkg/reader/home_chain.go +++ /dev/null @@ -1,24 +0,0 @@ -package reader - -import ( - "time" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-common/pkg/types" - - reader_internal "github.com/smartcontractkit/ccipocr3/internal/reader" -) - -type HomeChain = reader_internal.HomeChain - -type ChainConfig = reader_internal.ChainConfig - -type ChainConfigInfo = reader_internal.ChainConfigInfo - -func NewHomeChainReader( - homeChainReader types.ContractReader, - lggr logger.Logger, - pollingInterval time.Duration, -) HomeChain { - return reader_internal.NewHomeChainConfigPoller(homeChainReader, lggr, pollingInterval) -} diff --git a/core/services/ocr3/plugins/ccip/spec/commit_plugin.py b/core/services/ocr3/plugins/ccip/spec/commit_plugin.py deleted file mode 100644 index 5698650153..0000000000 --- a/core/services/ocr3/plugins/ccip/spec/commit_plugin.py +++ /dev/null @@ -1,243 +0,0 @@ -# -# High-level Python specification for the CCIP OCR3 Commit Plugin. -# -# This specification aims to provide a clear and comprehensive understanding -# of the plugin's functionality. It is highly recommended for engineers working on CCIP -# to familiarize themselves with this specification prior to reading the -# corresponding Go implementation. -# -# NOTE: Even though the specification is written in a high-level programming language, it's purpose -# is not to be executed. It is meant to be just a reference for the Go implementation. -# -from dataclasses import dataclass -from typing import List, Dict - -ChainSelector = int - -@dataclass -class Interval: - min: int - max: int - -@dataclass -class Message: - seq_nr: int - message_id: bytes # a unique message identifier computed on the source chain - message_hash: bytes # hash of message body computed on the destination chain and used on merkle tree - # TODO: - -@dataclass -class Commit: - interval: Interval - root: bytes - -@dataclass -class CommitOutcome: - latest_committed_seq_nums: Dict[ChainSelector, int] - commits: Dict[ChainSelector, Commit] - token_prices: Dict[str, int] - gas_prices: Dict[ChainSelector, int] - -@dataclass -class CommitObservation: - latest_committed_seq_nums: Dict[ChainSelector, int] - new_msgs: Dict[ChainSelector, List[Message]] - token_prices: Dict[str, int] - gas_prices: Dict[ChainSelector, int] - f_chain: Dict[ChainSelector, int] - -@dataclass -class CommitConfig: - oracle: int # our own observer - dest_chain: ChainSelector - f_chain: Dict[ChainSelector, int] - # oracleIndex -> supported chains - oracle_info: Dict[int, Dict[ChainSelector, bool]] - priced_tokens: List[str] - -class CommitPlugin: - def __init__(self): - self.cfg = CommitConfig( - oracle=1, - dest_chain=10, - f_chain={1: 2, 2: 3, 10: 3}, - oracle_info={ - 0: {1: True, 2: True, 10: True}, - # TODO: other oracles - }, - # TODO: will likely need aggregator address as well to - # actually get the price. - priced_tokens=["tokenA", "tokenB"], - ) - self.keep_cfg_in_sync() - - def get_token_prices(self): - # Read token prices which are required for the destination chain. - # We only read them if we have the capability to read from the price chain (e.g. arbitrum) - pass - - def get_gas_prices(self): - # Read all gas prices for the chains we support. - pass - - def query(self): - pass - - def observation(self, previous_outcome: CommitOutcome) -> CommitObservation: - # max_committed_seq_nr={sourceChainA: 10, sourceChainB: 20,...} - # Provided by the nodes that can read from the destination on the previous round. - # Observe msgs for our supported chains since the prev outcome. - new_msgs = {} - for (chain, seq_num) in previous_outcome.latest_committed_seq_nums: - if chain in self.cfg.oracle_info[self.cfg.oracle]: - msgs = self.onRamp(chain).get_msgs(chain, start=seq_num+1, limit=256) - for msg in msgs: - msg.message_hash = msg.compute_hash() - new_msgs[chain] = msgs - - # Observe token prices. {token: price} - token_prices = self.get_token_prices() - - # Observe gas prices. {chain: gasPrice} - # TODO: Should be a way to combine the loops over support chains for gas prices and new messages. - gas_prices = self.get_gas_prices() - - # Observe fChain for each chain. {chain: f_chain} - # We observe this because configuration changes may be detected at different times by different nodes. - # We always use the configuration which is seen by a majority of nodes. - f_chain = self.cfg.f_chain - - # If we support the destination chain, then we contribute an observation of the max committed seq nums. - # We use these in outcome to filter out messages that have already been committed. - latest_committed_seq_nums = {} - if self.cfg.dest_chain in self.cfg.oracle_info[self.cfg.oracle]: - latest_committed_seq_nums = self.offRamp.latest_committed_seq_nums() - - return CommitObservation(latest_committed_seq_nums, new_msgs, token_prices, gas_prices, f_chain) - - - def validate_observation(self, attributed_observation): - observation = attributed_observation.observation - oracle = attributed_observation.oracle - - # Only accept dest observations from nodes that support the dest chain - if observation.latest_committed_seq_nums is not None: - assert self.cfg.dest_chain in self.cfg.oracle_info[oracle] - - # Only accept source observations from nodes which support those sources. - msg_ids = set() - msg_hashes = set() - for (chain, msgs) in observation.new_msgs.items(): - assert(chain in self.cfg.oracle_info[oracle]) - # Don't allow duplicates of (chain, seqNr), (id) and (hash). Required to prevent double counting. - assert(len(msgs) == len(set([msg.seq_num for msg in msgs]))) - for msg in msgs: - assert msg.message_id not in msg_ids - assert msg.message_hash not in msg_hashes - msg_ids.add(msg.message_id) - msg_hashes.add(msg.message_hash) - - def observation_quorum(self): - return "2F+1" - - def outcome(self, observations: List[CommitObservation])->CommitOutcome: - f_chain = consensus_f_chain(observations) - latest_committed_seq_nums = consensus_latest_committed_seq_nums(observations, f_chain) - - # all_msgs contains all messages from all observations, grouped by source chain - all_msgs = [observation.new_msgs for observation in observations].group_by_source_chain() - - commits = {} # { chain: (root, min_seq_num, max_seq_num) } - for (chain, msgs) in all_msgs: - # Keep only msgs with seq nums greater than the consensus max commited seq nums. - # Note right after a report has been submitted, we'll expect those same messages - # to appear in the next observation, because the message observations are built - # on the previous max committed seq nums. - msgs = [msg for msg in msgs if msg.seq_num > latest_committed_seq_nums[chain]] - - msgs_by_seq_num = msgs.group_by_seq_num() # { 423: [0x1, 0x1, 0x2] } - # 2 nodes say that msg hash is 0x1 and 1 node says it's 0x2 - # if different hashes have the same number of votes, we select the - # hash with the lowest lexicographic order - - msg_hashes = { seq_num: elem_most_occurrences(hashes) for (seq_num, hashes) in msgs_by_seq_num.items() } - for (seq_num, hash) in msg_hashes.items(): # require at least 2f+1 observations of the voted hash - assert(msgs_by_seq_num[seq_num].count(hash) >= 2*f_chain[chain]+1) - - msgs_for_tree = [] # [ (seq_num, hash) ] - for (seq_num, hash) in msg_hashes.ordered_by_seq_num(): - if len(msgs_for_tree) > 0 and msgs_for_tree[-1].seq_num+1 != seq_num: - break # gap in sequence numbers, stop here - msgs_for_tree.append((seq_num, hash)) - - commits[chain] = Commit(root=build_merkle_tree(msgs_for_tree), interval=Interval(min=msgs_for_tree[0].seq_num, max=msgs_for_tree[-1].seq_num)) - - # TODO: we only want to put token/gas prices onchain - # on a regular cadence unless huge deviation. - token_prices = { tk: median(prices) for (tk, prices) in observations.group_token_prices_by_token() } - gas_prices = { chain: median(prices) for (chain, prices) in observations.group_gas_prices_by_chain() } - - return CommitOutcome(latest_committed_seq_nums=latest_committed_seq_nums, commits=commits, token_price=token_prices, gas_prices=gas_prices) - - def reports(self, outcome): - report = report_from_outcome(outcome) - encoded = report.chain_encode() # abi_encode for evm chains - return [encoded] - - def should_accept(self, report): - if len(report) == 0 or self.validate_report(report): - return False - - def should_transmit(self, report): - if not self.is_writer(): - return False - - if len(report) == 0 or not self.validate_report(report): - return False - - on_chain_seq_nums = self.offRamp.get_sequence_numbers() - for (chain, tree) in report.trees(): - if not (on_chain_seq_nums[chain]+1 == tree.min_seq_num): - return False - - return True - - def validate_report(self, report): - pass - - def keep_cfg_in_sync(self): - # Polling the configuration on the on-chain contract. - # When the config is updated on-chain, updates the plugin's local copy to the most recent version. - pass - -def consensus_f_chain(observations): - f_chain_votes = observations["f_chain"].group_by_chain() # { chainA: [1, 1, 16, 16, 16, 16] } - return { ch: elem_most_occurrences(fs) for (ch, fs) in f_chain_votes.items() } # { chainA: 16 } - -def consensus_latest_committed_seq_nums(observations, f_chains): - all_latest_committed_seq_nums = {} - for observation in observations: - for (chain, seq_num) in observation.latest_committed_seq_nums.items(): - if chain not in all_latest_committed_seq_nums: - all_latest_committed_seq_nums[chain] = [] - all_latest_committed_seq_nums[chain].append(seq_num) - - latest_committed_seq_nums_consensus = {} - # { chainA: [4, 5, 5, 5, 5, 6, 6] } - for (chain, latest_committed_seq_nums) in all_latest_committed_seq_nums.items(): - if len(latest_committed_seq_nums) >= 2*f_chains[chain]+1: - # 2f+1 = 2*5+1 = 11 - latest_committed_seq_nums_consensus[chain] = sorted(latest_committed_seq_nums)[f_chains[chain]]# with f=4 { chainA: 5 } - return latest_committed_seq_nums_consensus - -def elem_most_occurrences(lst): - pass - -def build_merkle_tree(messages): - pass - -def median(lst): - pass - -def report_from_outcome(outcome: CommitOutcome)->bytes: - pass