diff --git a/.github/actions/build_contract_artifacts/action.yml b/.github/actions/build_contract_artifacts/action.yml index 155f1bcb4..9fa29a92b 100644 --- a/.github/actions/build_contract_artifacts/action.yml +++ b/.github/actions/build_contract_artifacts/action.yml @@ -46,45 +46,6 @@ runs: docker stop build-container docker rm build-container - # should be used again after moving from projectserum/build to backpackapp/build - - name: Install latest Git version (>= 2.18.0) for actions/checkout - if: ${{ inputs.image == '' && inputs.image-version == '' }} - shell: bash - run: | - apt-get update - apt-get install software-properties-common -y - add-apt-repository ppa:git-core/ppa - apt update - apt install git -y - git config --global --add safe.directory "$GITHUB_WORKSPACE" - - name: Setup go - if: ${{ inputs.image == '' && inputs.image-version == '' }} - uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 - with: - go-version-file: "go.mod" - check-latest: true - - name: Generate build artifacts for go bindings check - if: ${{ inputs.image == '' && inputs.image-version == '' }} - shell: bash - run: anchor build - working-directory: contracts - - name: Check generated go bindings are up to date - if: ${{ inputs.image == '' && inputs.image-version == '' }} - shell: bash - run: | - go install github.com/gagliardetto/anchor-go@v0.2.3 - ./scripts/anchor-go-gen.sh - git diff --stat --exit-code - - name: Generate program_ids - if: ${{ inputs.image == '' && inputs.image-version == '' }} - shell: bash - run: ./scripts/programs-keys-gen.sh - - name: Generate build artifacts with custom program_ids - if: ${{ inputs.image == '' && inputs.image-version == '' }} - shell: bash - run: anchor build - working-directory: contracts - #save the contracts artifacts - name: Upload Artifacts uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 diff --git a/.github/workflows/e2e_custom_cl.yml b/.github/workflows/e2e_custom_cl.yml index cefa85e95..f515e1706 100644 --- a/.github/workflows/e2e_custom_cl.yml +++ b/.github/workflows/e2e_custom_cl.yml @@ -47,6 +47,21 @@ jobs: id: psversion uses: ./.github/actions/projectserum_version + contract-changes: + name: Check for contract changes + runs-on: ubuntu-latest + outputs: + changed: ${{ steps.changes.outputs.contracts }} + steps: + - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + id: changes + with: + base: develop + list-files: csv + filters: | + contracts: + - 'contracts/**' + solana-test-image-exists: environment: integration permissions: @@ -85,6 +100,91 @@ jobs: image: backpackapp/build image-version: ${{ needs.get_projectserum_version.outputs.projectserum_version }} + e2e_custom_build_previous_artifacts: + name: E2E Build Previous Release Artifacts + environment: integration + permissions: + id-token: write + contents: read + runs-on: ubuntu-latest-32cores-128GB + needs: [contract-changes,get_projectserum_version] + if: needs.contract-changes.outputs.changed == 'true' + steps: + - name: Checkout previous release + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + with: + ref: v1.0.2 # set to v1.1.0 after v1.1.0 is deployed to mainnet + - name: build contracts + id: previous + run: | + # for use with 1.0.2 release + docker run -d -v "$(pwd)":/repo --name build-container projectserum/build:v0.25.0 tail -f /dev/null + # for use with 1.1.0 release and onward + # docker run -d -v "$(pwd)":/repo --name build-container backpackapp/build:${{ needs.get_projectserum_version.outputs.projectserum_version }} tail -f /dev/null + + # build with keys + docker exec build-container bash -c "\ + export RUSTUP_HOME=\"/root/.rustup\" &&\ + cd /repo &&\ + ./scripts/programs-keys-gen.sh &&\ + cd ./contracts &&\ + anchor build &&\ + chown -R $(id -u):$(id -g) /repo" + # clean up the container + docker stop build-container + docker rm build-container + - uses: actions/upload-artifact@v4 + with: + name: artifacts-previous + path: ${{ env.CONTRACT_ARTIFACTS_PATH }} + + e2e_program_upgrade_generate_artifacts: + name: Combine artifacts for upgrade test + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + needs: [contract-changes,e2e_custom_build_artifacts,e2e_custom_build_previous_artifacts] + if: needs.contract-changes.outputs.changed == 'true' + outputs: + changed: ${{ steps.check.outputs.continue }} + steps: + - uses: actions/download-artifact@v4 + with: + name: artifacts + path: ./temp/artifacts + - name: hash current artifacts + id: current + run: | + cd ./temp/artifacts # need to be in directory for hashes to compare without path differences + # shellcheck disable=SC2035 + echo "hash=$(sha1sum * | sha1sum)" >> "$GITHUB_OUTPUT" + - uses: actions/download-artifact@v4 + with: + name: artifacts-previous + path: ./temp/artifacts/previous + - name: hash previous artifacts + id: previous + run: | + cd ./temp/artifacts/previous # need to be in directory for hashes to compare without path differences + # shellcheck disable=SC2035 + echo "hash=$(sha1sum * | sha1sum)" >> "$GITHUB_OUTPUT" + - name: compare hashes + id: check + run: | + echo "previous: ${{ steps.previous.outputs.hash }}" + echo "current: ${{ steps.current.outputs.hash }}" + + if [ "${{ steps.previous.outputs.hash }}" != "${{ steps.current.outputs.hash }}" ]; then + echo "continue=true" >> "$GITHUB_OUTPUT" + fi + - uses: actions/upload-artifact@v4 + if: steps.check.outputs.continue + with: + name: artifacts + path: ./temp/artifacts/ # combine artifacts + overwrite: true + e2e_custom_build_custom_chainlink_image: name: E2E Custom Build Custom CL Image runs-on: ubuntu-latest-16cores-64GB @@ -215,9 +315,88 @@ jobs: # shellcheck disable=SC2086 echo "BASE64_CONFIG_OVERRIDE=$BASE64_CONFIG_OVERRIDE" >> $GITHUB_ENV - name: Run Tests - uses: smartcontractkit/chainlink-github-actions/chainlink-testing-framework/run-tests@b49a9d04744b0237908831730f8553f26d73a94b # v2.3.17 + uses: smartcontractkit/chainlink-github-actions/chainlink-testing-framework/run-tests@fc3e0df622521019f50d772726d6bf8dc919dd38 # v2.3.19 + with: + test_command_to_run: cd ./integration-tests && go test -timeout 24h -count=1 -run TestSolanaOCRV2Smoke -json $(args) ./smoke 2>&1 | tee /tmp/gotest.log | gotestloghelper -ci=true -singlepackage=true -hidepassingtests=false -hidepassinglogs=false + test_download_vendor_packages_command: cd ./integration-tests && go mod download + download_contract_artifacts_path: ${{ env.CONTRACT_ARTIFACTS_PATH }} + go_mod_path: ./integration-tests/go.mod + cl_repo: ${{ env.CL_ECR }} + cl_image_tag: solana.${{ env.CUSTOM_CORE_REF || github.event.inputs.cl_branch_ref || github.sha }} + token: ${{ secrets.GITHUB_TOKEN }} + aws_registries: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }} + artifacts_name: smoke-test-logs + artifacts_location: ./integration-tests/smoke/logs/ + QA_AWS_REGION: ${{ secrets.QA_AWS_REGION }} + QA_AWS_ROLE_TO_ASSUME: ${{ secrets.QA_AWS_ROLE_TO_ASSUME }} + QA_KUBECONFIG: ${{ secrets.QA_KUBECONFIG }} + cache_key_id: solana-e2e-${{ env.MOD_CACHE_VERSION }} + cache_restore_only: "false" + + e2e_program_upgrade_tests: + name: E2E Program Upgrade Tests + environment: integration + permissions: + checks: write + pull-requests: write + id-token: write + contents: read + runs-on: ubuntu-latest-16cores-64GB + needs: [contract-changes,e2e_program_upgrade_generate_artifacts,e2e_custom_build_custom_chainlink_image, check_test_compilation] + if: needs.contract-changes.outputs.changed == 'true' && needs.e2e_program_upgrade_generate_artifacts.outputs.changed == 'true' + env: + TEST_SUITE: smoke + TEST_ARGS: -test.timeout 30m + TEST_LOG_LEVEL: debug + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - name: Collect Metrics + id: collect-gha-metrics + uses: smartcontractkit/push-gha-metrics-action@dea9b546553cb4ca936607c2267a09c004e4ab3f # v3.0.0 + with: + id: solana-e2e-program-upgrade + org-id: ${{ secrets.GRAFANA_INTERNAL_TENANT_ID }} + basic-auth: ${{ secrets.GRAFANA_INTERNAL_BASIC_AUTH }} + hostname: ${{ secrets.GRAFANA_INTERNAL_HOST }} + this-job-name: E2E Program Upgrade Tests + test-results-file: '{"testType":"go","filePath":"/tmp/gotest.log"}' + continue-on-error: true + - name: Get core ref from PR body + if: github.event_name == 'pull_request' + run: | + comment="$(gh pr view https://github.com/${{ github.repository }}/pull/${{ github.event.pull_request.number }} --json body -q '.body')" + # shellcheck disable=SC2086 + core_ref="$(echo $comment | grep -oP 'core ref: \K\S+' || true)" + if [ -n "$core_ref" ]; then + echo "CUSTOM_CORE_REF=${core_ref}" >> "${GITHUB_ENV}" + fi + - name: Checkout the repo + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + - name: Install Solana CLI # required for ensuring the local test validator is configured correctly + run: ./scripts/install-solana-ci.sh + - name: Install gauntlet + uses: ./.github/actions/build-gauntlet + - name: Generate config overrides + run: | # https://github.com/smartcontractkit/chainlink-testing-framework/blob/main/config/README.md + cat << EOF > config.toml + [ChainlinkImage] + image="${{ env.CL_ECR }}" + version="solana.${{ env.CUSTOM_CORE_REF || github.event.inputs.cl_branch_ref || github.sha }}" + [Common] + user="${{ github.actor }}" + internal_docker_repo = "${{ secrets.QA_AWS_ACCOUNT_NUMBER }}.dkr.ecr.${{ secrets.QA_AWS_REGION }}.amazonaws.com" + EOF + + # shellcheck disable=SC2002 + BASE64_CONFIG_OVERRIDE=$(cat config.toml | base64 -w 0) + # shellcheck disable=SC2086 + echo ::add-mask::$BASE64_CONFIG_OVERRIDE + # shellcheck disable=SC2086 + echo "BASE64_CONFIG_OVERRIDE=$BASE64_CONFIG_OVERRIDE" >> $GITHUB_ENV + - name: Run Upgrade Test + uses: smartcontractkit/chainlink-github-actions/chainlink-testing-framework/run-tests@fc3e0df622521019f50d772726d6bf8dc919dd38 # v2.3.19 with: - test_command_to_run: cd ./integration-tests && go test -timeout 24h -count=1 -run TestSolanaOCRV2Smoke -json $(args) ./smoke 2>&1 | tee /tmp/gotest.log | gotestloghelper -ci -singlepackage + test_command_to_run: cd ./integration-tests && go test -timeout 24h -count=1 -run TestSolanaOCRV2UpgradeSmoke -json $(args) ./smoke 2>&1 | tee /tmp/gotest.log | gotestloghelper -ci=true -singlepackage=true -hidepassingtests=false -hidepassinglogs=false test_download_vendor_packages_command: cd ./integration-tests && go mod download download_contract_artifacts_path: ${{ env.CONTRACT_ARTIFACTS_PATH }} go_mod_path: ./integration-tests/go.mod diff --git a/.github/workflows/upstream-tracker.yml b/.github/workflows/upstream-tracker.yml index 806dd5568..8debc3bba 100644 --- a/.github/workflows/upstream-tracker.yml +++ b/.github/workflows/upstream-tracker.yml @@ -31,18 +31,18 @@ jobs: WIKI="${WIKI//$'\n'/\\n}" # preserve new line printf -v WIKI "%q " "$WIKI" # escape chars that may cause issues - echo "open<> $GITHUB_OUTPUT - echo "$OPEN" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT + echo "open<> "$GITHUB_OUTPUT" + echo "$OPEN" >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" - echo "closed<> $GITHUB_OUTPUT - echo "$CLOSED" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT + echo "closed<> "$GITHUB_OUTPUT" + echo "$CLOSED" >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" - echo "wiki<> $GITHUB_OUTPUT - echo "$WIKI" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - echo "wikirange=$WIKIRANGE" >> $GITHUB_OUTPUT + echo "wiki<> "$GITHUB_OUTPUT" + echo "$WIKI" >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + echo "wikirange=$WIKIRANGE" >> "$GITHUB_OUTPUT" - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 if: steps.updates.outputs.open || steps.updates.outputs.closed || steps.updates.outputs.wiki - name: Open Issue diff --git a/contracts/README.md b/contracts/README.md index 8407e12cb..7ee37520d 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -56,5 +56,5 @@ Install `https://github.com/gagliardetto/anchor-go` Current version: [v0.2.3](https://github.com/gagliardetto/anchor-go/tree/v0.2.3) ```bash -./scripts/anchor-go-gen.sh +./scripts/anchor-go-gen.sh ``` diff --git a/integration-tests/README.md b/integration-tests/README.md index 898df43b1..b700c5a96 100644 --- a/integration-tests/README.md +++ b/integration-tests/README.md @@ -1,3 +1,64 @@ # Contents -- [How To Run E2E Tests](../docs/RunningE2eTests.md) \ No newline at end of file +- [How To Run E2E Tests](../docs/RunningE2eTests.md) + +## Automated Program Upgrade Testing + +Main Benefit: automatically test that changes made to programs are upgradeable from the deployed version + +Previously, program upgrade testing is a manual process where a node cluster is spun up against deployed programs, +and then steps are manually executed. The following workflow describes automated program testing for any changes +that occur in the `chainlink-solana/contracts` folder. + +This workflow adds additional steps to the base [e2e_custom_cl workflow](https://github.com/smartcontractkit/chainlink-solana/blob/develop/.github/workflows/e2e_custom_cl.yml). + +```mermaid +flowchart TD + + classDef base stroke:#00f + testimage[Check Test Image Existence] + buildver[Build Container Version] + smoketest[Execute Smoke Tests] + currentartifacts[Build Current Artifacts] + buildtestimage[Build & Push Test Image] + compcheck[Test Compilation Check] + climage[Build Custom CL Image] + class compcheck,buildver,climage,testimage,currentartifacts,smoketest,buildtestimage base + + compcheck --> smoketest + buildver --> currentartifacts + currentartifacts --> smoketest + climage --> smoketest + currentartifacts .->|is labeled?| buildtestimage + + classDef upgrade stroke:#0f0 + changecheck[Check Contract Changes] + prevartifacts[Build Previous Artifacts] + combineartifacts[Combine Artifacts For Test] + upgradetest[Execute Upgrade Test] + class changecheck,prevartifacts,combineartifacts,upgradetest upgrade + + changecheck .->|changed?| prevartifacts + changecheck .->|changed?| combineartifacts + changecheck .->|changed?| upgradetest + buildver --> prevartifacts + prevartifacts --> combineartifacts + currentartifacts --> combineartifacts + combineartifacts .->|artifacts different?| upgradetest + climage --> upgradetest + compcheck --> upgradetest +``` + +- 🟦 **BLUE**: represents the base smoke test jobs +- 🟩 **GREEN**: represents the program upgrade test jobs +- the program upgrade test is only run when two conditions are true: + - files within `./contract` are changed: this prevents running an integration test unless needed + - generated program artifacts differ from the *pinned* version in the workflow: this prevents running an integration test unless actual program changes were made, not just trivial changes + - note: the pinned version needs to be manually updated to track with the deployed production version + +### Workflow Runs for Upgrade Testing + +✅ **v1.0.2 -> v1.1.0** + +- [commit](https://github.com/smartcontractkit/chainlink-solana/pull/811/commits/0699d595e373e4a3d781b3249f38dda0087421dc) +- [e2e tests job run with upgrade test](https://github.com/smartcontractkit/chainlink-solana/actions/runs/10292293982/job/28486398103?pr=811) diff --git a/integration-tests/common/test_common.go b/integration-tests/common/test_common.go index 170cdcc68..488c3250d 100644 --- a/integration-tests/common/test_common.go +++ b/integration-tests/common/test_common.go @@ -4,6 +4,8 @@ import ( "fmt" "math/big" "net/http" + "path/filepath" + "strings" "testing" "time" @@ -168,7 +170,6 @@ func (m *OCRv2TestState) DeployCluster(contractsDir string) { m.SetupClients() m.SetChainlinkNodes() - m.DeployContracts(contractsDir) } // UploadProgramBinaries uploads programs binary files to solana-validator container @@ -222,16 +223,44 @@ func (m *OCRv2TestState) SetupClients() { } // DeployContracts deploys contracts -func (m *OCRv2TestState) DeployContracts(contractsDir string) { +// baseDir is the root folder where contracts are stored +// subDir allows for pointing to a subdirectory within baseDir (can be left empty) +func (m *OCRv2TestState) DeployContracts(baseDir, subDir string) { var err error m.Clients.ChainlinkClient.NKeys, err = m.Common.CreateNodeKeysBundle(m.Clients.ChainlinkClient.ChainlinkNodes) require.NoError(m.Config.T, err) cd, err := solclient.NewContractDeployer(m.Clients.SolanaClient, nil) require.NoError(m.Config.T, err) if *m.Config.TestConfig.Common.InsideK8s { - err = cd.DeployAnchorProgramsRemote(contractsDir, m.Common.Env) + err = cd.DeployAnchorProgramsRemote(baseDir, m.Common.Env) } else { - err = cd.DeployAnchorProgramsRemoteDocker(contractsDir, m.Common.DockerEnv.Sol) + err = cd.DeployAnchorProgramsRemoteDocker(baseDir, subDir, m.Common.DockerEnv.Sol, solclient.BuildProgramIDKeypairPath) + } + require.NoError(m.Config.T, err) +} + +func (m *OCRv2TestState) UpgradeContracts(baseDir, subDir string) { + cd, err := solclient.NewContractDeployer(m.Clients.SolanaClient, nil) + require.NoError(m.Config.T, err) + + // fetch corresponding program address for program + programIDBuilder := func(programName string) string { + // remove extra directories + .so suffix from lookup + programName, _ = strings.CutSuffix(filepath.Base(programName), ".so") + ids := map[string]string{ + "ocr_2": m.Common.ChainDetails.ProgramAddresses.OCR2, + "access_controller": m.Common.ChainDetails.ProgramAddresses.AccessController, + "store": m.Common.ChainDetails.ProgramAddresses.Store, + } + val, ok := ids[programName] + require.True(m.Config.T, ok, fmt.Sprintf("unable to find corresponding key (%s) within %+v", programName, ids)) + return val + } + + if *m.Config.TestConfig.Common.InsideK8s { + err = fmt.Errorf("not implemented") + } else { + err = cd.DeployAnchorProgramsRemoteDocker(baseDir, subDir, m.Common.DockerEnv.Sol, programIDBuilder) } require.NoError(m.Config.T, err) } diff --git a/integration-tests/smoke/ocr2_test.go b/integration-tests/smoke/ocr2_test.go index c10ee820c..9b2645fcd 100644 --- a/integration-tests/smoke/ocr2_test.go +++ b/integration-tests/smoke/ocr2_test.go @@ -38,126 +38,136 @@ func TestSolanaOCRV2Smoke(t *testing.T) { test := test t.Run(test.name, func(t *testing.T) { t.Parallel() + _, sg := startOCR2DataFeedsSmokeTest(t, test.name, test.env, config, "") + validateRounds(t, test.name, sg, *config.OCR2.NumberOfRounds) + }) + } +} - name := "gauntlet-" + test.name - state, err := common.NewOCRv2State(t, 1, name, &config) - require.NoError(t, err, "Could not setup the ocrv2 state") - if len(test.env) > 0 { - state.Common.TestEnvDetails.NodeOpts = append(state.Common.TestEnvDetails.NodeOpts, func(n *test_env.ClNode) { - if n.ContainerEnvs == nil { - n.ContainerEnvs = map[string]string{} - } - maps.Copy(n.ContainerEnvs, test.env) - }) +func startOCR2DataFeedsSmokeTest(t *testing.T, testname string, testenv map[string]string, config tc.TestConfig, subDir string) (*common.OCRv2TestState, *gauntlet.SolanaGauntlet) { + name := "gauntlet-" + testname + state, err := common.NewOCRv2State(t, 1, name, &config) + require.NoError(t, err, "Could not setup the ocrv2 state") + if len(testenv) > 0 { + state.Common.TestEnvDetails.NodeOpts = append(state.Common.TestEnvDetails.NodeOpts, func(n *test_env.ClNode) { + if n.ContainerEnvs == nil { + n.ContainerEnvs = map[string]string{} } + maps.Copy(n.ContainerEnvs, testenv) + }) + } - state.DeployCluster(utils.ContractsDir) - if state.Common.Env.WillUseRemoteRunner() { - return - } + state.DeployCluster(utils.ContractsDir) + state.DeployContracts(utils.ContractsDir, subDir) + if state.Common.Env.WillUseRemoteRunner() { + return state, nil + } - // copy gauntlet folder to run in parallel (gauntlet generates an output file that is read by the e2e tests - causes conflict if shared) - gauntletCopyPath := utils.ProjectRoot + "/" + name - if out, cpErr := exec.Command("cp", "-r", utils.ProjectRoot+"/gauntlet", gauntletCopyPath).Output(); cpErr != nil { // nolint:gosec - require.NoError(t, err, "output: "+string(out)) - } + // copy gauntlet folder to run in parallel (gauntlet generates an output file that is read by the e2e tests - causes conflict if shared) + gauntletCopyPath := utils.ProjectRoot + "/" + name + if out, cpErr := exec.Command("cp", "-r", utils.ProjectRoot+"/gauntlet", gauntletCopyPath).Output(); cpErr != nil { // nolint:gosec + require.NoError(t, err, "output: "+string(out)) + } + + sg, err := gauntlet.NewSolanaGauntlet(gauntletCopyPath) + require.NoError(t, err) + state.Gauntlet = sg - sg, err := gauntlet.NewSolanaGauntlet(gauntletCopyPath) - require.NoError(t, err) - state.Gauntlet = sg - - if *config.Common.InsideK8s { - t.Cleanup(func() { - err = state.Common.Env.Shutdown() - if err != nil { - log.Err(err) - } - }) + if *config.Common.InsideK8s { + t.Cleanup(func() { + err = state.Common.Env.Shutdown() + if err != nil { + log.Err(err) } + }) + } - state.SetupClients() - require.NoError(t, err) + state.SetupClients() + require.NoError(t, err) - gauntletConfig := map[string]string{ - "SECRET": fmt.Sprintf("\"%s\"", *config.SolanaConfig.Secret), - "NODE_URL": state.Common.ChainDetails.RPCURLExternal, - "WS_URL": state.Common.ChainDetails.WSURLExternal, - "PRIVATE_KEY": state.Common.AccountDetails.PrivateKey, - } + gauntletConfig := map[string]string{ + "SECRET": fmt.Sprintf("\"%s\"", *config.SolanaConfig.Secret), + "NODE_URL": state.Common.ChainDetails.RPCURLExternal, + "WS_URL": state.Common.ChainDetails.WSURLExternal, + "PRIVATE_KEY": state.Common.AccountDetails.PrivateKey, + } - err = sg.SetupNetwork(gauntletConfig) - require.NoError(t, err, "Error setting gauntlet network") - err = sg.InstallDependencies() - require.NoError(t, err, "Error installing gauntlet dependencies") - - if *config.Common.Network == "devnet" { - state.Common.ChainDetails.ProgramAddresses.OCR2 = *config.SolanaConfig.OCR2ProgramID - state.Common.ChainDetails.ProgramAddresses.AccessController = *config.SolanaConfig.AccessControllerProgramID - state.Common.ChainDetails.ProgramAddresses.Store = *config.SolanaConfig.StoreProgramID - sg.LinkAddress = *config.SolanaConfig.LinkTokenAddress - sg.VaultAddress = *config.SolanaConfig.VaultAddress - } else { - // Deploying LINK in case of localnet - err = sg.DeployLinkToken() - require.NoError(t, err) - } + err = sg.SetupNetwork(gauntletConfig) + require.NoError(t, err, "Error setting gauntlet network") + err = sg.InstallDependencies() + require.NoError(t, err, "Error installing gauntlet dependencies") + + if *config.Common.Network == "devnet" { + state.Common.ChainDetails.ProgramAddresses.OCR2 = *config.SolanaConfig.OCR2ProgramID + state.Common.ChainDetails.ProgramAddresses.AccessController = *config.SolanaConfig.AccessControllerProgramID + state.Common.ChainDetails.ProgramAddresses.Store = *config.SolanaConfig.StoreProgramID + sg.LinkAddress = *config.SolanaConfig.LinkTokenAddress + sg.VaultAddress = *config.SolanaConfig.VaultAddress + } else { + // Deploying LINK in case of localnet + err = sg.DeployLinkToken() + require.NoError(t, err) + } - err = sg.G.WriteNetworkConfigVar(sg.NetworkFilePath, "PROGRAM_ID_OCR2", state.Common.ChainDetails.ProgramAddresses.OCR2) - require.NoError(t, err, "Error adding gauntlet variable") - err = sg.G.WriteNetworkConfigVar(sg.NetworkFilePath, "PROGRAM_ID_ACCESS_CONTROLLER", state.Common.ChainDetails.ProgramAddresses.AccessController) - require.NoError(t, err, "Error adding gauntlet variable") - err = sg.G.WriteNetworkConfigVar(sg.NetworkFilePath, "PROGRAM_ID_STORE", state.Common.ChainDetails.ProgramAddresses.Store) - require.NoError(t, err, "Error adding gauntlet variable") - err = sg.G.WriteNetworkConfigVar(sg.NetworkFilePath, "LINK", sg.LinkAddress) - require.NoError(t, err, "Error adding gauntlet variable") - err = sg.G.WriteNetworkConfigVar(sg.NetworkFilePath, "VAULT_ADDRESS", sg.VaultAddress) - require.NoError(t, err, "Error adding gauntlet variable") - - _, err = sg.DeployOCR2() - require.NoError(t, err, "Error deploying OCR") - // Generating default OCR2 config - ocr2Config := ocr_config.NewOCR2Config(state.Clients.ChainlinkClient.NKeys, sg.ProposalAddress, sg.VaultAddress, *config.SolanaConfig.Secret) - ocr2Config.Default() - sg.OCR2Config = ocr2Config - - err = sg.ConfigureOCR2() - require.NoError(t, err) - - state.CreateJobs() - - // Test start - stuck := 0 - successFullRounds := 0 - prevRound := gauntlet.Transmission{ - RoundID: 0, - } - for successFullRounds < *config.OCR2.NumberOfRounds { - time.Sleep(time.Second * 6) - require.Less(t, stuck, 10, fmt.Sprintf("%s: Rounds have been stuck for more than 10 iterations", name)) - log.Info().Str("Transmission", sg.OcrAddress).Msg("Inspecting transmissions") - transmissions, err := sg.FetchTransmissions(sg.OcrAddress) - require.NoError(t, err) - if len(transmissions) <= 1 { - log.Info().Str("Contract", sg.OcrAddress).Msg(fmt.Sprintf("%s: No Transmissions", name)) - stuck++ - continue - } - currentRound := common.GetLatestRound(transmissions) - if prevRound.RoundID == 0 { - prevRound = currentRound - } - if currentRound.RoundID <= prevRound.RoundID { - log.Info().Str("Transmission", sg.OcrAddress).Msg(fmt.Sprintf("%s: No new transmissions", name)) - stuck++ - continue - } - log.Info().Str("Contract", sg.OcrAddress).Interface("Answer", currentRound.Answer).Int64("RoundID", currentRound.RoundID).Msg(fmt.Sprintf("%s: New answer found", name)) - require.Equal(t, currentRound.Answer, int64(5), fmt.Sprintf("Actual: %d, Expected: 5", currentRound.Answer)) - require.Less(t, prevRound.RoundID, currentRound.RoundID, fmt.Sprintf("Expected round %d to be less than %d", prevRound.RoundID, currentRound.RoundID)) - prevRound = currentRound - successFullRounds++ - stuck = 0 - } - }) + err = sg.G.WriteNetworkConfigVar(sg.NetworkFilePath, "PROGRAM_ID_OCR2", state.Common.ChainDetails.ProgramAddresses.OCR2) + require.NoError(t, err, "Error adding gauntlet variable") + err = sg.G.WriteNetworkConfigVar(sg.NetworkFilePath, "PROGRAM_ID_ACCESS_CONTROLLER", state.Common.ChainDetails.ProgramAddresses.AccessController) + require.NoError(t, err, "Error adding gauntlet variable") + err = sg.G.WriteNetworkConfigVar(sg.NetworkFilePath, "PROGRAM_ID_STORE", state.Common.ChainDetails.ProgramAddresses.Store) + require.NoError(t, err, "Error adding gauntlet variable") + err = sg.G.WriteNetworkConfigVar(sg.NetworkFilePath, "LINK", sg.LinkAddress) + require.NoError(t, err, "Error adding gauntlet variable") + err = sg.G.WriteNetworkConfigVar(sg.NetworkFilePath, "VAULT_ADDRESS", sg.VaultAddress) + require.NoError(t, err, "Error adding gauntlet variable") + + _, err = sg.DeployOCR2() + require.NoError(t, err, "Error deploying OCR") + // Generating default OCR2 config + ocr2Config := ocr_config.NewOCR2Config(state.Clients.ChainlinkClient.NKeys, sg.ProposalAddress, sg.VaultAddress, *config.SolanaConfig.Secret) + ocr2Config.Default() + sg.OCR2Config = ocr2Config + + err = sg.ConfigureOCR2() + require.NoError(t, err) + + state.CreateJobs() + return state, sg +} + +func validateRounds(t *testing.T, testname string, sg *gauntlet.SolanaGauntlet, rounds int) { + name := "gauntlet" + testname + + // Test start + stuck := 0 + successFullRounds := 0 + prevRound := gauntlet.Transmission{ + RoundID: 0, + } + for successFullRounds < rounds { + time.Sleep(time.Second * 6) + require.Less(t, stuck, 10, fmt.Sprintf("%s: Rounds have been stuck for more than 10 iterations", name)) + log.Info().Str("Transmission", sg.OcrAddress).Msg("Inspecting transmissions") + transmissions, err := sg.FetchTransmissions(sg.OcrAddress) + require.NoError(t, err) + if len(transmissions) <= 1 { + log.Info().Str("Contract", sg.OcrAddress).Msg(fmt.Sprintf("%s: No Transmissions", name)) + stuck++ + continue + } + currentRound := common.GetLatestRound(transmissions) + if prevRound.RoundID == 0 { + prevRound = currentRound + } + if currentRound.RoundID <= prevRound.RoundID { + log.Info().Str("Transmission", sg.OcrAddress).Msg(fmt.Sprintf("%s: No new transmissions", name)) + stuck++ + continue + } + log.Info().Str("Contract", sg.OcrAddress).Interface("Answer", currentRound.Answer).Int64("RoundID", currentRound.RoundID).Msg(fmt.Sprintf("%s: New answer found", name)) + require.Equal(t, currentRound.Answer, int64(5), fmt.Sprintf("Actual: %d, Expected: 5", currentRound.Answer)) + require.Less(t, prevRound.RoundID, currentRound.RoundID, fmt.Sprintf("Expected round %d to be less than %d", prevRound.RoundID, currentRound.RoundID)) + prevRound = currentRound + successFullRounds++ + stuck = 0 } } diff --git a/integration-tests/smoke/ocr2upgrade_test.go b/integration-tests/smoke/ocr2upgrade_test.go new file mode 100644 index 000000000..8e057f9fa --- /dev/null +++ b/integration-tests/smoke/ocr2upgrade_test.go @@ -0,0 +1,37 @@ +package smoke + +import ( + "testing" + + "github.com/rs/zerolog/log" + + tc "github.com/smartcontractkit/chainlink-solana/integration-tests/testconfig" + "github.com/smartcontractkit/chainlink-solana/integration-tests/utils" +) + +func TestSolanaOCRV2UpgradeSmoke(t *testing.T) { + name := "plugins-program-upgrade" + env := map[string]string{ + "CL_MEDIAN_CMD": "chainlink-feeds", + "CL_SOLANA_CMD": "chainlink-solana", + } + config, err := tc.GetConfig("Smoke", tc.OCR2) + if err != nil { + t.Fatal(err) + } + s, sg := startOCR2DataFeedsSmokeTest(t, name, env, config, "previous") + // validate cluster is functioning + validateRounds(t, name, sg, *config.OCR2.NumberOfRounds) + + // make it very obvious with logging for redeploying contracts + log.Info().Msg("---------------------------------------------") + log.Info().Msg("| REDEPLOYING CONTRACTS |") + log.Info().Msg("---------------------------------------------") + s.UpgradeContracts(utils.ContractsDir, "") + log.Info().Msg("---------------------------------------------") + log.Info().Msg("| |") + log.Info().Msg("---------------------------------------------") + + // validate cluster is still functioning after program upgrade + validateRounds(t, name, sg, *config.OCR2.NumberOfRounds) +} diff --git a/integration-tests/solclient/deployer.go b/integration-tests/solclient/deployer.go index 0d6d1e2ef..2ee04319a 100644 --- a/integration-tests/solclient/deployer.go +++ b/integration-tests/solclient/deployer.go @@ -361,12 +361,18 @@ func (c *ContractDeployer) DeployProgramRemote(programName string, env *environm return nil } -func (c *ContractDeployer) DeployProgramRemoteLocal(programName string, sol *test_env_sol.Solana) error { +func BuildProgramIDKeypairPath(programName string) string { + programKeyFileName := strings.Replace(programName, ".so", keypairSuffix, -1) + return filepath.Join("programs", programKeyFileName) +} + +// DeployProgramRemoteLocal takes in a programIDBuilder which allows for building the program keypair path from the name or passing the deployed program address +func (c *ContractDeployer) DeployProgramRemoteLocal(programName string, sol *test_env_sol.Solana, programIDBuilder func(string) string) error { log.Info().Str("Program", programName).Msg("Deploying program") programPath := filepath.Join("programs", programName) - programKeyFileName := strings.Replace(programName, ".so", keypairSuffix, -1) - programKeyFilePath := filepath.Join("programs", programKeyFileName) - cmd := fmt.Sprintf("solana program deploy --program-id %s %s", programKeyFilePath, programPath) + + cmd := fmt.Sprintf("solana program deploy --program-id %s %s", programIDBuilder(programName), programPath) + log.Info().Str("Cmd", cmd).Msg("Deploying " + programName) _, res, err := sol.Container.Exec(context.Background(), strings.Split(cmd, " ")) if err != nil { return err @@ -505,17 +511,17 @@ func (c *ContractDeployer) DeployAnchorProgramsRemote(contractsDir string, env * return g.Wait() } -func (c *ContractDeployer) DeployAnchorProgramsRemoteDocker(contractsDir string, sol *test_env_sol.Solana) error { - contractBinaries, err := c.Client.ListDirFilenamesByExt(contractsDir, ".so") +func (c *ContractDeployer) DeployAnchorProgramsRemoteDocker(baseDir, subDir string, sol *test_env_sol.Solana, programIDBuilder func(string) string) error { + contractBinaries, err := c.Client.ListDirFilenamesByExt(filepath.Join(baseDir, subDir), ".so") if err != nil { return err } - log.Debug().Interface("Binaries", contractBinaries).Msg("Program binaries") + log.Info().Interface("Binaries", contractBinaries).Msg(fmt.Sprintf("Program binaries [%s]", filepath.Join("programs", subDir))) g := errgroup.Group{} for _, bin := range contractBinaries { bin := bin g.Go(func() error { - return c.DeployProgramRemoteLocal(bin, sol) + return c.DeployProgramRemoteLocal(filepath.Join(subDir, bin), sol, programIDBuilder) }) } return g.Wait() diff --git a/integration-tests/solclient/solclient.go b/integration-tests/solclient/solclient.go index d2d3a638c..c24b73449 100644 --- a/integration-tests/solclient/solclient.go +++ b/integration-tests/solclient/solclient.go @@ -292,8 +292,8 @@ func (c *Client) AirdropAddresses(addr []string, solAmount uint64) error { func (c *Client) ListDirFilenamesByExt(dir string, ext string) ([]string, error) { keyFiles := make([]string, 0) err := filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error { - if info.IsDir() { - return nil + if info.IsDir() && info.Name() != filepath.Base(dir) { + return filepath.SkipDir // only check first depth of folders } if filepath.Ext(path) == ext { keyFiles = append(keyFiles, info.Name())