diff --git a/.github/workflows/ccip-integration-test-deprecated.yml b/.github/workflows/ccip-integration-test-deprecated.yml new file mode 100644 index 000000000..1d60099a4 --- /dev/null +++ b/.github/workflows/ccip-integration-test-deprecated.yml @@ -0,0 +1,124 @@ +name: "Run CCIP OCR3 Integration Test" + +on: + pull_request: + push: + branches: + - 'main' + +jobs: + integration-test-ccip-ocr3: + env: + # We explicitly have this env var not be "CL_DATABASE_URL" to avoid having it be used by core related tests + # when they should not be using it, while still allowing us to DRY up the setup + DB_URL: postgresql://postgres:postgres@localhost:5432/chainlink_test?sslmode=disable + + runs-on: ubuntu-latest + strategy: + matrix: + go-version: ['1.22.5'] + steps: + - name: Checkout the chainlink-ccip repo + 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: Fetch latest pull request data + id: fetch_pr_data + uses: actions/github-script@v6 + # only run this step if the event is a pull request + if: github.event_name == 'pull_request' + with: + script: | + const pr = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number + }); + return pr.data.body; + - name: Get the chainlink commit sha from PR description, if applicable + id: get_chainlink_sha + run: | + default="develop" + if [ "${{ github.event_name }}" == "pull_request" ]; then + comment='${{ steps.fetch_pr_data.outputs.result }}' + echo $comment + core_ref=$(echo "$comment" | grep -oE 'core ref: [a-f0-9]{40}' | cut -d' ' -f3 || true) + if [ -n "$core_ref" ]; then + echo "Overriding chainlink repository commit hash with: $core_ref" + echo "::set-output name=ref::$core_ref" + else + echo "Using default chainlink repository branch: $default" + echo "::set-output name=ref::$default" + fi + else + echo "::set-output name=ref::$default" + fi + - name: Clone Chainlink repo + uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 + with: + repository: smartcontractkit/chainlink + ref: ${{ steps.get_chainlink_sha.outputs.ref }} + path: chainlink + - name: Get the correct chainlink-ccip commit SHA via GitHub API + id: get_sha + run: | + if [ "${{ github.event_name }}" == "pull_request" ]; then + COMMIT_SHA=${{ github.event.pull_request.head.sha }} + else + COMMIT_SHA=$(curl -s -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ + "https://api.github.com/repos/${{ github.repository }}/commits/${{ github.ref }}" | jq -r .sha) + fi + echo "::set-output name=sha::$COMMIT_SHA" + - name: Update chainlink-ccip dependency in chainlink + run: | + cd $GITHUB_WORKSPACE/chainlink + go get github.com/smartcontractkit/chainlink-ccip@${{ steps.get_sha.outputs.sha }} + make gomodtidy + - name: Setup Postgres + uses: ./.github/actions/setup-postgres + - name: Download Go vendor packages + run: | + cd $GITHUB_WORKSPACE/chainlink + go mod download + cd $GITHUB_WORKSPACE/chainlink/integration-tests + go mod download + - name: Build binary + run: | + cd $GITHUB_WORKSPACE/chainlink + go build -o ccip.test . + - name: Setup DB + run: | + cd $GITHUB_WORKSPACE/chainlink + ./ccip.test local db preparetest + env: + CL_DATABASE_URL: ${{ env.DB_URL }} + - name: Run ccip ocr3 initial deploy integration test + run: | + cd $GITHUB_WORKSPACE/chainlink/deployment + go test -v -run '^TestInitialDeploy$' -timeout 6m ./ccip/changeset + EXITCODE=${PIPESTATUS[0]} + if [ $EXITCODE -ne 0 ]; then + echo "Integration test failed" + else + echo "Integration test passed!" + fi + exit $EXITCODE + env: + CL_DATABASE_URL: ${{ env.DB_URL }} + - name: Run ccip ocr3 add chain integration test + run: | + cd $GITHUB_WORKSPACE/chainlink/deployment + go test -v -run '^TestAddChainInbound$' -timeout 6m ./ccip/changeset + EXITCODE=${PIPESTATUS[0]} + if [ $EXITCODE -ne 0 ]; then + echo "Integration test failed" + else + echo "Integration test passed!" + fi + exit $EXITCODE + env: + CL_DATABASE_URL: ${{ env.DB_URL }} diff --git a/.github/workflows/ccip-integration-test.yml b/.github/workflows/ccip-integration-test.yml index 900f4e3fc..1434444be 100644 --- a/.github/workflows/ccip-integration-test.yml +++ b/.github/workflows/ccip-integration-test.yml @@ -20,10 +20,13 @@ jobs: steps: - name: Checkout the chainlink-ccip repo uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 - - name: Setup Go ${{ matrix.go-version }} + - name: Determine Go version + id: go_version + run: echo "GO_VERSION=$(cat go.mod |grep "^go"|cut -d' ' -f 2)" >> $GITHUB_ENV + - name: Setup Go ${{ env.GO_VERSION }} uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 with: - go-version: ${{ matrix.go-version }} + go-version: ${{ env.GO_VERSION }} - name: Display Go version run: go version - name: Fetch latest pull request data diff --git a/.github/workflows/ccip-ocr3-build-lint-test-deprecated.yml b/.github/workflows/ccip-ocr3-build-lint-test-deprecated.yml new file mode 100644 index 000000000..67757701f --- /dev/null +++ b/.github/workflows/ccip-ocr3-build-lint-test-deprecated.yml @@ -0,0 +1,83 @@ +name: "Build lint and test CCIP-OCR3" + +on: + pull_request: + push: + branches: + - 'main' + +jobs: + build-lint-test: + runs-on: ubuntu-20.04 + strategy: + matrix: + go-version: ['1.22'] + defaults: + run: + working-directory: . + 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: make + - 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: make lint + - name: Run tests + run: TEST_COUNT=20 COVERAGE_FILE=coverage.out make test + - name: Generate coverage report + if: github.event_name == 'pull_request' + run: | + total=$(go tool cover -func=coverage.out | grep total | awk '{print $3}') + echo "coverage=$total" >> $GITHUB_ENV + - name: Coverage on target branch + if: github.event_name == 'pull_request' + run: | + git fetch origin ${{ github.base_ref }} + git checkout ${{ github.base_ref }} + TEST_COUNT=1 COVERAGE_FILE=coverage_target.out make test + total=$(go tool cover -func=coverage_target.out | grep total | awk '{print $3}') + echo "coverage_target=$total" >> $GITHUB_ENV + - name: Remove previous coverage comments + uses: actions/github-script@v6 + if: github.event_name == 'pull_request' + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { owner, repo, number: issue_number } = context.issue; + const comments = await github.rest.issues.listComments({ + owner, + repo, + issue_number + }); + const coverageCommentPrefix = "| Metric |"; + for (const comment of comments.data) { + if (comment.body.startsWith(coverageCommentPrefix)) { + await github.rest.issues.deleteComment({ + owner, + repo, + comment_id: comment.id + }); + } + } + - name: Display coverage in PR comment + uses: actions/github-script@v6 + if: github.event_name == 'pull_request' + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const coverage = process.env.coverage; + const coverage_target = process.env.coverage_target; + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: `| Metric | \`${{ github.head_ref }}\` | \`${{ github.base_ref }}\` |\n|--|--|--|\n| **Coverage** | ${coverage} | ${coverage_target} |` + }); diff --git a/.github/workflows/ccip-ocr3-build-lint-test.yml b/.github/workflows/ccip-ocr3-build-lint-test.yml index 4934c67f8..38d0f9076 100644 --- a/.github/workflows/ccip-ocr3-build-lint-test.yml +++ b/.github/workflows/ccip-ocr3-build-lint-test.yml @@ -17,10 +17,13 @@ jobs: working-directory: . steps: - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 - - name: Setup Go ${{ matrix.go-version }} + - name: Determine Go version + id: go_version + run: echo "GO_VERSION=$(cat go.mod |grep "^go"|cut -d' ' -f 2)" >> $GITHUB_ENV + - name: Setup Go ${{ env.GO_VERSION }} uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 with: - go-version: ${{ matrix.go-version }} + go-version: ${{ env.GO_VERSION }} - name: Display Go version run: go version - name: Build diff --git a/.github/workflows/codegen-deprecated.yml b/.github/workflows/codegen-deprecated.yml new file mode 100644 index 000000000..bcf85f9cf --- /dev/null +++ b/.github/workflows/codegen-deprecated.yml @@ -0,0 +1,43 @@ +# All code generation should be run prior to pull request. Running it again should not produce a diff. +name: "Codegen Verifier" + +on: + pull_request: + push: + branches: + - 'main' + +jobs: + codegen-verifier: + runs-on: ubuntu-20.04 + strategy: + matrix: + go-version: ['1.22'] + defaults: + run: + working-directory: . + 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: Install protoc + run: make install-protoc + - name: Re-Generate files + run: | + make generate + - name: Tidy + run: go mod tidy + - name: ensure no changes + run: | + set -e + git_status=$(git status --porcelain=v1) + if [ ! -z "$git_status" ]; then + git status + git diff + echo "Error: modified files detected, run 'make generate' / 'go mod tidy'." + exit 1 + fi diff --git a/.github/workflows/codegen.yml b/.github/workflows/codegen.yml index 0a6890233..73dab6a94 100644 --- a/.github/workflows/codegen.yml +++ b/.github/workflows/codegen.yml @@ -18,10 +18,13 @@ jobs: working-directory: . steps: - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 - - name: Setup Go ${{ matrix.go-version }} + - name: Determine Go version + id: go_version + run: echo "GO_VERSION=$(cat go.mod |grep "^go"|cut -d' ' -f 2)" >> $GITHUB_ENV + - name: Setup Go ${{ env.GO_VERSION }} uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 with: - go-version: ${{ matrix.go-version }} + go-version: ${{ env.GO_VERSION }} - name: Display Go version run: go version - name: Install protoc diff --git a/commit/merkleroot/rmn/controller.go b/commit/merkleroot/rmn/controller.go index f178dc83e..20581aef1 100644 --- a/commit/merkleroot/rmn/controller.go +++ b/commit/merkleroot/rmn/controller.go @@ -554,12 +554,19 @@ func (c *controller) validateSignedObservationResponse( signedObs.Observation.RmnHomeContractConfigDigest) } + seenSourceChainSelectors := mapset.NewSet[uint64]() + for _, signedObsLu := range signedObs.Observation.FixedDestLaneUpdates { updateReq, exists := lurs[signedObsLu.LaneSource.SourceChainSelector] if !exists { return fmt.Errorf("unexpected source chain selector %d", signedObsLu.LaneSource.SourceChainSelector) } + if seenSourceChainSelectors.Contains(signedObsLu.LaneSource.SourceChainSelector) { + return fmt.Errorf("duplicate source chain %d", signedObsLu.LaneSource.SourceChainSelector) + } + seenSourceChainSelectors.Add(signedObsLu.LaneSource.SourceChainSelector) + if !updateReq.RmnNodes.Contains(rmnNodeID) { return fmt.Errorf("rmn node %d not expected to read chain %d", rmnNodeID, signedObsLu.LaneSource.SourceChainSelector) diff --git a/commit/merkleroot/rmn/controller_test.go b/commit/merkleroot/rmn/controller_test.go index c904b7ebf..dc7c44807 100644 --- a/commit/merkleroot/rmn/controller_test.go +++ b/commit/merkleroot/rmn/controller_test.go @@ -599,6 +599,140 @@ func TestClient_ComputeReportSignatures(t *testing.T) { }) } +func Test_controller_validateSignedObservationResponse(t *testing.T) { + configDigest123 := [32]byte{1, 2, 3} + + testCases := []struct { + name string + signedObservationResponse *rmnpb.SignedObservation + rmnNodeID rmntypes.NodeID + lurs map[uint64]updateRequestWithMeta + destChain *rmnpb.LaneDest + homeConfigDigest [32]byte + rmnNodesInfo []rmntypes.HomeNodeInfo + expErrContains string + }{ + { + name: "single valid observation", + signedObservationResponse: &rmnpb.SignedObservation{ + Observation: &rmnpb.Observation{ + RmnHomeContractConfigDigest: configDigest123[:], + LaneDest: &rmnpb.LaneDest{DestChainSelector: 1}, + FixedDestLaneUpdates: []*rmnpb.FixedDestLaneUpdate{ + { + LaneSource: &rmnpb.LaneSource{SourceChainSelector: 2}, + Root: []byte{1, 2, 33}, + ClosedInterval: &rmnpb.ClosedInterval{MinMsgNr: 1, MaxMsgNr: 2}, + }, + }, + Timestamp: uint64(time.Now().Unix()), + }, + Signature: []byte{10, 20, 30}, + }, + rmnNodeID: 20, + lurs: map[uint64]updateRequestWithMeta{ + 2: { + Data: &rmnpb.FixedDestLaneUpdateRequest{ + LaneSource: &rmnpb.LaneSource{SourceChainSelector: 2}, + ClosedInterval: &rmnpb.ClosedInterval{ + MinMsgNr: 1, + MaxMsgNr: 2, + }, + }, + RmnNodes: mapset.NewSet(rmntypes.NodeID(20)), + }, + }, + destChain: &rmnpb.LaneDest{DestChainSelector: 1}, + homeConfigDigest: configDigest123, + rmnNodesInfo: []rmntypes.HomeNodeInfo{ + { + ID: 20, + SupportedSourceChains: mapset.NewSet[cciptypes.ChainSelector](cciptypes.ChainSelector(2)), + OffchainPublicKey: &ed25519.PublicKey{}, + }, + }, + }, + { + name: "duplicate valid source lane updates should be rejected", + signedObservationResponse: &rmnpb.SignedObservation{ + Observation: &rmnpb.Observation{ + RmnHomeContractConfigDigest: configDigest123[:], + LaneDest: &rmnpb.LaneDest{DestChainSelector: 1}, + FixedDestLaneUpdates: []*rmnpb.FixedDestLaneUpdate{ + { + LaneSource: &rmnpb.LaneSource{SourceChainSelector: 2}, + Root: []byte{1, 2, 33}, + ClosedInterval: &rmnpb.ClosedInterval{MinMsgNr: 1, MaxMsgNr: 2}, + }, + { + LaneSource: &rmnpb.LaneSource{SourceChainSelector: 2}, + Root: []byte{1, 2, 33}, + ClosedInterval: &rmnpb.ClosedInterval{MinMsgNr: 1, MaxMsgNr: 2}, + }, + }, + Timestamp: uint64(time.Now().Unix()), + }, + }, + rmnNodeID: 20, + lurs: map[uint64]updateRequestWithMeta{ + 2: { + Data: &rmnpb.FixedDestLaneUpdateRequest{ + LaneSource: &rmnpb.LaneSource{SourceChainSelector: 2}, + ClosedInterval: &rmnpb.ClosedInterval{ + MinMsgNr: 1, + MaxMsgNr: 2, + }, + }, + RmnNodes: mapset.NewSet(rmntypes.NodeID(20)), + }, + }, + destChain: &rmnpb.LaneDest{DestChainSelector: 1}, + homeConfigDigest: configDigest123, + rmnNodesInfo: []rmntypes.HomeNodeInfo{ + { + ID: 20, + SupportedSourceChains: mapset.NewSet[cciptypes.ChainSelector](cciptypes.ChainSelector(2)), + }, + }, + expErrContains: "duplicate source chain 2", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + lggr := logger.Test(t) + + rmnHomeReaderMock := readerpkg_mock.NewMockRMNHome(t) + rmnHomeReaderMock.EXPECT().GetRMNNodesInfo(cciptypes.Bytes32(tc.homeConfigDigest)). + Return(tc.rmnNodesInfo, nil) + + cl := &controller{ + lggr: lggr, + ed25519Verifier: signatureVerifierAlwaysTrue{}, + rmnCrypto: signatureVerifierAlwaysTrue{}, + rmnHomeReader: rmnHomeReaderMock, + } + + err := cl.validateSignedObservationResponse( + &rmnpb.Response{ + RequestId: 0, + Response: &rmnpb.Response_SignedObservation{SignedObservation: tc.signedObservationResponse}, + }, + tc.rmnNodeID, + tc.lurs, + tc.destChain, + tc.homeConfigDigest, + ) + if tc.expErrContains != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.expErrContains) + return + } + require.NoError(t, err) + }) + } +} + func (ts *testSetup) waitForObservationRequestsToBeSent( rmnClient *mockPeerClient, homeF int,