diff --git a/.github/workflows/integration-tests-smoke.yml b/.github/workflows/integration-tests-smoke.yml index 7454a964d..aa43faabd 100644 --- a/.github/workflows/integration-tests-smoke.yml +++ b/.github/workflows/integration-tests-smoke.yml @@ -16,7 +16,6 @@ concurrency: cancel-in-progress: true env: - CHAINLINK_ENV_USER: ${{ github.actor }} TEST_LOG_LEVEL: debug CL_ECR: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }}.dkr.ecr.${{ secrets.QA_AWS_REGION }}.amazonaws.com/chainlink ENV_JOB_IMAGE: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }}.dkr.ecr.${{ secrets.QA_AWS_REGION }}.amazonaws.com/chainlink-starknet-tests:${{ github.sha }} @@ -114,18 +113,12 @@ jobs: image: - name: "" tag-suffix: "" - - name: (plugins) + test-name: embedded + - name: plugins tag-suffix: -plugins + test-name: plugins env: - TEST_SUITE: smoke - TEST_ARGS: -test.timeout 1h - PRIVATE_KEY: ${{ secrets.GOERLI_PRIVATE_KEY }} - ACCOUNT: ${{ secrets.GOERLI_ACCOUNT }} - TTL: 3h - TEST_DURATION: 15m - NODE_COUNT: 5 - CHAINLINK_IMAGE: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }}.dkr.ecr.${{ secrets.QA_AWS_REGION }}.amazonaws.com/chainlink - CHAINLINK_VERSION: starknet.${{ github.sha }}${{ matrix.image.tag-suffix }} + INTERNAL_DOCKER_REPO: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }}.dkr.ecr.${{ secrets.QA_AWS_REGION }}.amazonaws.com permissions: checks: write pull-requests: write @@ -147,10 +140,37 @@ jobs: uses: cachix/install-nix-action@29bd9290ef037a3ecbdafe83cbd2185e9dd0fa0a # v20 with: nix_path: nixpkgs=channel:nixos-unstable + - name: Install Cairo + uses: ./.github/actions/install-cairo + - name: Build contracts + run: | + cd contracts && scarb --profile release build + - name: Build gauntlet + run: | + yarn install && yarn build + - name: Generate config overrides + run: | # https://github.com/smartcontractkit/chainlink-testing-framework/blob/main/config/README.md + cat << EOF > config.toml + [ChainlinkImage] + image="${{ secrets.QA_AWS_ACCOUNT_NUMBER }}.dkr.ecr.${{ secrets.QA_AWS_REGION }}.amazonaws.com/chainlink" + version="starknet.${{ github.sha }}${{ matrix.image.tag-suffix }}" + [Network] + selected_networks=["SIMULATED"] + [Common] + internal_docker_repo = "${{ env.INTERNAL_DOCKER_REPO }}" + stateful_db = false + 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 Tests ${{ matrix.image.name }} uses: smartcontractkit/chainlink-github-actions/chainlink-testing-framework/run-tests@ea889b3133bd7f16ab19ba4ba130de5d9162c669 # v2.3.4 with: - test_command_to_run: nix develop -c helm repo update && make test-integration-smoke-ci + aws_registries: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }} + test_command_to_run: nix develop -c helm repo update && make test=${{ matrix.image.test-name }} test-integration-smoke-ci test_download_vendor_packages_command: cd integration-tests && nix develop -c go mod download cl_repo: ${{ env.CL_ECR }} cl_image_tag: starknet.${{ github.sha }}${{ matrix.image.tag-suffix }} @@ -159,4 +179,4 @@ jobs: QA_AWS_REGION: ${{ secrets.QA_AWS_REGION }} QA_AWS_ROLE_TO_ASSUME: ${{ secrets.QA_AWS_ROLE_TO_ASSUME }} QA_KUBECONFIG: ${{ secrets.QA_KUBECONFIG }} - artifacts_location: /home/runner/work/chainlink-starknet/chainlink-starknet/integration-tests/smoke/logs + artifacts_location: /home/runner/work/chainlink-starknet/chainlink-starknet/integration-tests/smoke/logs \ No newline at end of file diff --git a/.github/workflows/integration-tests-soak.yml b/.github/workflows/integration-tests-soak.yml deleted file mode 100644 index 20696281f..000000000 --- a/.github/workflows/integration-tests-soak.yml +++ /dev/null @@ -1,132 +0,0 @@ -name: Integration Tests - Soak - -on: - push: - branches: - - main - - develop - workflow_dispatch: - inputs: - cl_branch_ref: - description: Chainlink repo branch to integrate with - required: true - default: develop - type: string - l2_rpc_url: - description: Override default RPC url which points to local devnet (Optional) - required: false - type: string - node_count: - description: Number of ocr nodes - required: true - default: 5 - type: string - ttl: - description: TTL for namespace - required: true - default: 72h - type: string - test_duration: - description: Duration of soak - required: true - default: 72h - type: string - private_key: - description: Private key, ignore for devnet - required: false - type: string - account: - description: Account address, ignore for devnet - required: false - -# Only run 1 of this workflow at a time per PR -concurrency: - group: integration-tests-starknet-${{ github.ref }} - cancel-in-progress: true - -env: - ENV_JOB_IMAGE: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }}.dkr.ecr.${{ secrets.QA_AWS_REGION }}.amazonaws.com/chainlink-starknet-tests:${{ github.sha }} - -jobs: - build_custom_chainlink_image: - name: Build Custom CL Image - runs-on: ubuntu-latest - environment: integration - permissions: - id-token: write - contents: read - steps: - - name: Check if image exists - id: check-image - uses: smartcontractkit/chainlink-github-actions/docker/image-exists@2c9f401149f6c25fb632067b7e6626aebeee5d69 # v2.1.6 - with: - repository: chainlink - tag: starknet.${{ github.sha }} - AWS_REGION: ${{ secrets.QA_AWS_REGION }} - AWS_ROLE_TO_ASSUME: ${{ secrets.QA_AWS_ROLE_TO_ASSUME }} - - name: Build Image - if: steps.check-image.outputs.exists == 'false' - uses: smartcontractkit/chainlink-github-actions/chainlink-testing-framework/build-image@2c9f401149f6c25fb632067b7e6626aebeee5d69 - with: - cl_repo: smartcontractkit/chainlink - # By default we are integrating with CL develop - cl_ref: ${{ github.event.inputs.cl_branch_ref }} - # commit of the caller branch - dep_starknet_sha: ${{ github.event.pull_request.head.sha || github.sha }} - push_tag: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }}.dkr.ecr.${{ secrets.QA_AWS_REGION }}.amazonaws.com/chainlink:starknet.${{ github.sha }} - QA_AWS_REGION: ${{ secrets.QA_AWS_REGION }} - QA_AWS_ROLE_TO_ASSUME: ${{ secrets.QA_AWS_ROLE_TO_ASSUME }} - QA_PRIVATE_GHA_PULL: ${{ secrets.QA_PRIVATE_GHA_PULL }} - - name: Print Chainlink Image Built - run: | - echo "### chainlink image tag used for this test run :link:" >>$GITHUB_STEP_SUMMARY - echo "\`starknet.${{ github.sha }}\`" >>$GITHUB_STEP_SUMMARY - - run_tests: - name: Run Soak Tests - runs-on: ubuntu-latest - needs: [ build_custom_chainlink_image ] - environment: integration - env: - CHAINLINK_ENV_USER: ${{ github.actor }} - L2_RPC_URL: ${{ github.event.inputs.l2_rpc_url }} - TEST_DURATION: ${{ github.event.inputs.test_duration }} - NODE_COUNT: ${{ github.event.inputs.node_count }} - PRIVATE_KEY: ${{ github.event.inputs.private_key }} - ACCOUNT: ${{ github.event.inputs.account }} - TTL: ${{ github.event.inputs.ttl }} - DETACH_RUNNER: true - TEST_SUITE: soak - TEST_ARGS: -test.timeout ${{ github.event.inputs.ttl }} - TEST_LOG_LEVEL: debug - INTERNAL_DOCKER_REPO: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }}.dkr.ecr.${{ secrets.QA_AWS_REGION }}.amazonaws.com - permissions: - checks: write - pull-requests: write - id-token: write - contents: read - steps: - - name: Checkout the repo - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 - - name: Install Nix - uses: cachix/install-nix-action@29bd9290ef037a3ecbdafe83cbd2185e9dd0fa0a # v20 - with: - nix_path: nixpkgs=channel:nixos-unstable - - name: Build Image - uses: ./.github/actions/build-test-image - with: - QA_AWS_ROLE_TO_ASSUME: ${{ secrets.QA_AWS_ROLE_TO_ASSUME }} - QA_AWS_REGION: ${{ secrets.QA_AWS_REGION }} - QA_AWS_ACCOUNT_NUMBER: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }} - - name: Run Tests - uses: smartcontractkit/chainlink-github-actions/chainlink-testing-framework/run-tests@ea889b3133bd7f16ab19ba4ba130de5d9162c669 # v2.3.4 - with: - test_command_to_run: nix develop -c make test-integration-soak-ci - test_download_vendor_packages_command: cd integration-tests && nix develop -c go mod download - cl_repo: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }}.dkr.ecr.${{ secrets.QA_AWS_REGION }}.amazonaws.com/chainlink - cl_image_tag: starknet.${{ github.sha }} - token: ${{ secrets.GITHUB_TOKEN }} - go_mod_path: ./integration-tests/go.mod - QA_AWS_REGION: ${{ secrets.QA_AWS_REGION }} - QA_AWS_ROLE_TO_ASSUME: ${{ secrets.QA_AWS_ROLE_TO_ASSUME }} - QA_KUBECONFIG: ${{ secrets.QA_KUBECONFIG }} diff --git a/.gitignore b/.gitignore index ffd70d468..1ec5b7415 100644 --- a/.gitignore +++ b/.gitignore @@ -166,3 +166,4 @@ ztarrepo.tar.gz eslint-report.json .run.id .local-mock-server +override*.toml diff --git a/Makefile b/Makefile index 22937f60b..79cb1feca 100644 --- a/Makefile +++ b/Makefile @@ -200,12 +200,12 @@ test-integration-smoke: test-integration-prep .PHONY: test-integration-smoke-ci test-integration-smoke-ci: cd integration-tests/ && \ - go test --timeout=2h -v -count=1 -json ./smoke 2>&1 | tee /tmp/gotest.log | gotestfmt + go test --timeout=2h -v -count=1 -run TestOCRBasic/$(test) -json ./smoke .PHONY: test-integration-soak test-integration-soak: test-integration-prep cd integration-tests/ && \ - go test --timeout=1h -v -json./soak + go test --timeout=1h -v -json ./soak # CI Already has already ran test-integration-prep .PHONY: test-integration-soak-ci diff --git a/contracts/Scarb.lock b/contracts/Scarb.lock index 2ba9d1f72..9d2d099e8 100644 --- a/contracts/Scarb.lock +++ b/contracts/Scarb.lock @@ -11,4 +11,4 @@ dependencies = [ [[package]] name = "openzeppelin" version = "0.9.0" -source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.9.0#861fc416f87addbe23a3b47f9d19ab27c10d5dc8" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.9.0#364db5b1aecc1335d2e65db887291d19aa28937d" diff --git a/integration-tests/.root_dir b/integration-tests/.root_dir new file mode 100644 index 000000000..e69de29bb diff --git a/integration-tests/README.md b/integration-tests/README.md index cbf5276b5..84432a0e4 100644 --- a/integration-tests/README.md +++ b/integration-tests/README.md @@ -1,38 +1,28 @@ -# Local e2e testing +## Integration tests - HOWTO -Make sure to have `psql` installed locally. We use it to create a new database for each node. +### Prerequisites +1. `cd contracts && scarb --profile release build` +2. `yarn install` +3. `yarn build` -Create a new network for containers (only needs to be done once). A custom network allows containers to DNS resolve each other using container names. +#### TOML preparation +The integration tests are using TOML as the configuration input. The logic and parsing is located under [Test config](./testconfig) -``` -docker network create chainlink -``` +By default, the tests will be running with the default config set in [default.toml](./testconfig/default.toml). This configuration is set to run on devnet with local docker. -Build a custom core image with starknet relayer bumped to some commit. +Fields in the default toml can be overriden by creating an `overrides.toml`file. Any values specified here take precedence and will be overwritten if they overlap with `default.toml`. -``` -cd ../core -go get github.com/smartcontractkit/chainlink-starknet/relayer@ -docker build . -t smartcontract/chainlink:starknet -f ./core/chainlink.Dockerfile -``` +##### Testnet runs +In order to run the tests on Testnet, additional variables need to be specified in the TOML, these would also be pointed out if `network = "testnet"` is set. The additional variables are: -Compile contracts and gauntlet: +- `l2_rpc_url` - L2 RPC url +- `account` - Account address on L2 +- `private_key` - Private key for L2 account -``` -yarn build -cd contracts -scarb --profile release build -``` +##### Running in k8s -Run the tests! +Set `inside_k8 = true` under `[Common]`. -``` -cd integration-tests -go test -count 1 -v -timeout 30m --run OCRBasic ./smoke -``` +#### Run tests -Use `something.down.sh` scripts to teardown everything afterwards if the tests don't properly clean up. - -# Old docs - -For more information, see the [Chainlink Starknet Documentation | Integration Tests](../docs/integration-tests). +`cd integration-tests && go test --timeout=2h -v -count=1 -json ./smoke` \ No newline at end of file diff --git a/integration-tests/common/common.go b/integration-tests/common/common.go index e7d4ac85d..dd3cca152 100644 --- a/integration-tests/common/common.go +++ b/integration-tests/common/common.go @@ -1,12 +1,18 @@ package common import ( - "bytes" "fmt" - "io" + chainconfig "github.com/smartcontractkit/chainlink-starknet/integration-tests/config" + "github.com/smartcontractkit/chainlink-starknet/integration-tests/testconfig" + "github.com/smartcontractkit/chainlink-starknet/ops/devnet" + "github.com/smartcontractkit/chainlink-starknet/relayer/pkg/chainlink/config" + ctfconfig "github.com/smartcontractkit/chainlink-testing-framework/config" + "github.com/smartcontractkit/chainlink-testing-framework/k8s/pkg/helm/chainlink" + mock_adapter "github.com/smartcontractkit/chainlink-testing-framework/k8s/pkg/helm/mock-adapter" + "github.com/smartcontractkit/chainlink-testing-framework/utils/ptr" + "github.com/smartcontractkit/chainlink/integration-tests/docker/test_env" "os" "os/exec" - "regexp" "strconv" "strings" "testing" @@ -18,218 +24,140 @@ import ( "github.com/stretchr/testify/require" "gopkg.in/guregu/null.v4" + common_cfg "github.com/smartcontractkit/chainlink-common/pkg/config" "github.com/smartcontractkit/chainlink-testing-framework/k8s/environment" - "github.com/smartcontractkit/chainlink-testing-framework/k8s/pkg/alias" "github.com/smartcontractkit/chainlink/integration-tests/client" + "github.com/smartcontractkit/chainlink/integration-tests/types/config/node" + cl "github.com/smartcontractkit/chainlink/v2/core/services/chainlink" "github.com/smartcontractkit/chainlink/v2/core/services/job" "github.com/smartcontractkit/chainlink/v2/core/services/relay" ) -var ( - serviceKeyL1 = "Hardhat" - serviceKeyL2 = "chainlink-starknet.starknet-devnet" - serviceKeyChainlink = "chainlink" - chainName = "starknet" - chainId = "SN_GOERLI" - defaultNodeUrl = "http://127.0.0.1:5050" -) - type Common struct { - P2PPort string - ServiceKeyL1 string - ServiceKeyL2 string - ServiceKeyChainlink string - ChainName string - ChainId string - NodeCount int - TTL time.Duration - TestDuration time.Duration - Testnet bool - L2RPCUrl string - MockUrl string - PrivateKey string - Account string - ChainlinkConfig string - Env *environment.Environment + ChainDetails *chainconfig.Config + TestEnvDetails *TestEnvDetails + Env *environment.Environment + RPCDetails *RPCDetails + ChainlinkConfig string + TestConfig *testconfig.TestConfig } -// getEnv gets the environment variable if it exists and sets it for the remote runner -func getEnv(v string) string { - val := os.Getenv(v) - if val != "" { - os.Setenv(fmt.Sprintf("TEST_%s", v), val) - } - return val +type TestEnvDetails struct { + TestDuration time.Duration + K8Config *environment.Config + NodeOpts []test_env.ClNodeOption } -func getNodeCount() int { - // Checking if count of OCR nodes is defined in ENV - nodeCountSet := getEnv("NODE_COUNT") - if nodeCountSet == "" { - nodeCountSet = "4" - } - nodeCount, err := strconv.Atoi(nodeCountSet) - if err != nil { - panic(fmt.Sprintf("Please define a proper node count for the test: %v", err)) - } - return nodeCount +type RPCDetails struct { + RPCL1Internal string + RPCL2Internal string + RPCL1External string + RPCL2External string + MockServerUrl string + MockServerEndpoint string + P2PPort string } -func getTTL() time.Duration { - ttlValue := getEnv("TTL") - if ttlValue == "" { - ttlValue = "72h" - } - duration, err := time.ParseDuration(ttlValue) - if err != nil { - panic(fmt.Sprintf("Please define a proper TTL for the test: %v", err)) - } - t, err := time.ParseDuration(*alias.ShortDur(duration)) - if err != nil { - panic(fmt.Sprintf("Please define a proper TTL for the test: %v", err)) - } - return t -} +func New(testConfig *testconfig.TestConfig) *Common { + var c *Common + chainDetails := chainconfig.DevnetConfig() -func getTestDuration() time.Duration { - testDurationValue := getEnv("TEST_DURATION") - if testDurationValue == "" { - return time.Duration(time.Minute * 15) - } - duration, err := time.ParseDuration(testDurationValue) - if err != nil { - panic(fmt.Sprintf("Please define a proper duration for the test: %v", err)) - } - t, err := time.ParseDuration(*alias.ShortDur(duration)) + duration, err := time.ParseDuration(*testConfig.OCR2.TestDuration) if err != nil { - panic(fmt.Sprintf("Please define a proper duration for the test: %v", err)) + panic("Invalid test duration") } - return t -} -func New(t *testing.T) *Common { - c := &Common{ - ChainName: chainName, - ChainId: chainId, - NodeCount: getNodeCount(), - TTL: getTTL(), - TestDuration: getTestDuration(), - ServiceKeyChainlink: serviceKeyChainlink, - ServiceKeyL1: serviceKeyL1, - ServiceKeyL2: serviceKeyL2, - L2RPCUrl: getEnv("L2_RPC_URL"), // Fetch L2 RPC url if defined - MockUrl: "http://host.containers.internal:6060", - PrivateKey: getEnv("PRIVATE_KEY"), - Account: getEnv("ACCOUNT"), - // P2PPort: "6690", + if *testConfig.Common.Network == "testnet" { + chainDetails = chainconfig.SepoliaConfig() + chainDetails.L2RPCInternal = *testConfig.Common.L2RPCUrl } - c.Testnet = c.L2RPCUrl != "" - // TODO: HAXX: we force the URL to a local docker container - c.L2RPCUrl = defaultNodeUrl - - starknetUrl := fmt.Sprintf("http://%s:%d/rpc", serviceKeyL2, 5050) - if c.Testnet { - starknetUrl = c.L2RPCUrl + c = &Common{ + TestConfig: testConfig, + ChainDetails: chainDetails, + TestEnvDetails: &TestEnvDetails{ + TestDuration: duration, + }, + RPCDetails: &RPCDetails{ + P2PPort: "6690", + RPCL2Internal: chainDetails.L2RPCInternal, + }, } - chainlinkConfig := fmt.Sprintf(`[[Starknet]] -Enabled = true -ChainID = '%s' -[[Starknet.Nodes]] -Name = 'primary' -URL = '%s' - -[OCR2] -Enabled = true - -[P2P] -[P2P.V2] -Enabled = true -DeltaDial = '5s' -DeltaReconcile = '5s' -ListenAddresses = ['0.0.0.0:6690'] - -[WebServer] -HTTPPort = 6688 -[WebServer.TLS] -HTTPSPort = 0 -`, c.ChainId, starknetUrl) - - c.ChainlinkConfig = chainlinkConfig - log.Debug().Str("toml", chainlinkConfig).Msg("Created chainlink config") - - c.Env = &environment.Environment{} return c } -// CapturingPassThroughWriter is a writer that remembers -// data written to it and passes it to w -type CapturingPassThroughWriter struct { - buf bytes.Buffer - w io.Writer -} - -// NewCapturingPassThroughWriter creates new CapturingPassThroughWriter -func NewCapturingPassThroughWriter(w io.Writer) *CapturingPassThroughWriter { - return &CapturingPassThroughWriter{ - w: w, - } -} - -func (w *CapturingPassThroughWriter) Write(d []byte) (int, error) { - w.buf.Write(d) - return w.w.Write(d) -} - -// Bytes returns bytes written to the writer -func (w *CapturingPassThroughWriter) Bytes() []byte { - return w.buf.Bytes() -} - -func debug(cmd *exec.Cmd) error { - stdout, err := cmd.StdoutPipe() - if err != nil { - panic(err) - } - stderr, err := cmd.StderrPipe() - if err != nil { - panic(err) +func (c *Common) Default(t *testing.T, namespacePrefix string) (*Common, error) { + c.TestEnvDetails.K8Config = &environment.Config{ + NamespacePrefix: fmt.Sprintf("starknet-%s", namespacePrefix), + TTL: c.TestEnvDetails.TestDuration, + Test: t, } - if startErr := cmd.Start(); startErr != nil { - panic(startErr) + if *c.TestConfig.Common.InsideK8s { + toml := c.DefaultNodeConfig() + tomlString, err := toml.TOMLString() + if err != nil { + return nil, err + } + var overrideFn = func(_ interface{}, target interface{}) { + ctfconfig.MustConfigOverrideChainlinkVersion(c.TestConfig.ChainlinkImage, target) + } + cd := chainlink.NewWithOverride(0, map[string]any{ + "toml": tomlString, + "replicas": *c.TestConfig.OCR2.NodeCount, + "chainlink": map[string]interface{}{ + "resources": map[string]interface{}{ + "requests": map[string]interface{}{ + "cpu": "2000m", + "memory": "4Gi", + }, + "limits": map[string]interface{}{ + "cpu": "2000m", + "memory": "4Gi", + }, + }, + }, + "db": map[string]any{ + "image": map[string]any{ + "version": "15.5", + }, + "stateful": c.TestConfig.Common.Stateful, + }, + }, c.TestConfig.ChainlinkImage, overrideFn) + c.Env = environment.New(c.TestEnvDetails.K8Config). + AddHelm(devnet.New(nil)). + AddHelm(mock_adapter.New(nil)). + AddHelm(cd) } - doneStdOut := make(chan any) - doneStdErr := make(chan any) - osstdout := NewCapturingPassThroughWriter(os.Stdout) - osstderr := NewCapturingPassThroughWriter(os.Stderr) - go handleOutput(osstdout, stdout, doneStdOut) - go handleOutput(osstderr, stderr, doneStdErr) - - err = cmd.Wait() - - errStdOut := <-doneStdOut - if errStdOut != nil { - fmt.Println("error writing to standard out") - } + return c, nil +} - errStdErr := <-doneStdErr - if errStdErr != nil { - fmt.Println("error writing to standard in") +func (c *Common) DefaultNodeConfig() *cl.Config { + starkConfig := config.TOMLConfig{ + Enabled: ptr.Ptr(true), + ChainID: ptr.Ptr(c.ChainDetails.ChainID), + Nodes: []*config.Node{ + { + Name: ptr.Ptr("primary"), + URL: common_cfg.MustParseURL(c.RPCDetails.RPCL2Internal), + }, + }, } - - if err != nil { - fmt.Printf("Command finished with error: %v\n", err) + baseConfig := node.NewBaseConfig() + baseConfig.Starknet = config.TOMLConfigs{ + &starkConfig, } + baseConfig.OCR2.Enabled = ptr.Ptr(true) + baseConfig.P2P.V2.Enabled = ptr.Ptr(true) + fiveSecondDuration := common_cfg.MustNewDuration(5 * time.Second) - return err -} + baseConfig.P2P.V2.DeltaDial = fiveSecondDuration + baseConfig.P2P.V2.DeltaReconcile = fiveSecondDuration + baseConfig.P2P.V2.ListenAddresses = &[]string{"0.0.0.0:6690"} -func handleOutput(dst io.Writer, src io.Reader, done chan<- any) { - _, err := io.Copy(dst, src) - done <- err + return baseConfig } func (c *Common) SetLocalEnvironment(t *testing.T) { @@ -247,15 +175,14 @@ func (c *Common) SetLocalEnvironment(t *testing.T) { log.Info().Msg("Starting core nodes...") cmd := exec.Command("../../scripts/core.sh") cmd.Env = append(os.Environ(), fmt.Sprintf("CL_CONFIG=%s", c.ChainlinkConfig)) - // easy debug - err = debug(cmd) + err = cmd.Run() require.NoError(t, err, "Could not start core nodes") log.Info().Msg("Set up local stack complete.") // Set ChainlinkNodeDetails var nodeDetails []*environment.ChainlinkNodeDetail var basePort = 50100 - for i := 0; i < c.NodeCount; i++ { + for i := 0; i < *c.TestConfig.OCR2.NodeCount; i++ { dbLocalIP := fmt.Sprintf("postgresql://postgres:postgres@chainlink.postgres:5432/starknet_test_%d?sslmode=disable", i+1) nodeDetails = append(nodeDetails, &environment.ChainlinkNodeDetail{ ChartName: "unused", @@ -285,76 +212,40 @@ func (c *Common) TearDownLocalEnvironment(t *testing.T) { log.Info().Msg("Tear down local stack complete.") } -// connectChainlinkNodes creates a chainlink client for each node in the environment -// This is a non k8s version of the function in chainlink_k8s.go -// https://github.com/smartcontractkit/chainlink/blob/cosmos-test-keys/integration-tests/client/chainlink_k8s.go#L77 -func connectChainlinkNodes(e *environment.Environment) ([]*client.ChainlinkClient, error) { - var clients []*client.ChainlinkClient - for _, nodeDetails := range e.ChainlinkNodeDetails { - c, err := client.NewChainlinkClient(&client.ChainlinkConfig{ - URL: nodeDetails.LocalIP, - Email: "notreal@fakeemail.ch", - Password: "fj293fbBnlQ!f9vNs", - InternalIP: parseHostname(nodeDetails.InternalIP), - }, log.Logger) +func (c *Common) CreateNodeKeysBundle(nodes []*client.ChainlinkClient) ([]client.NodeKeysBundle, error) { + nkb := make([]client.NodeKeysBundle, 0) + for _, n := range nodes { + p2pkeys, err := n.MustReadP2PKeys() if err != nil { return nil, err } - log.Debug(). - Str("URL", c.Config.URL). - Str("Internal IP", c.Config.InternalIP). - Str("Chart Name", nodeDetails.ChartName). - Str("Pod Name", nodeDetails.PodName). - Msg("Connected to Chainlink node") - clients = append(clients, c) - } - return clients, nil -} - -func parseHostname(s string) string { - r := regexp.MustCompile(`://(?P.*):`) - return r.FindStringSubmatch(s)[1] -} -// CreateKeys Creates node keys and defines chain and nodes for each node -func (c *Common) CreateKeys(env *environment.Environment) ([]client.NodeKeysBundle, []*client.ChainlinkClient, error) { - nodes, err := connectChainlinkNodes(env) - if err != nil { - return nil, nil, err - } + peerID := p2pkeys.Data[0].Attributes.PeerID + txKey, _, err := n.CreateTxKey(c.ChainDetails.ChainName, c.ChainDetails.ChainID) + if err != nil { + return nil, err + } + ocrKey, _, err := n.CreateOCR2Key(c.ChainDetails.ChainName) + if err != nil { + return nil, err + } - NKeys, _, err := client.CreateNodeKeysBundle(nodes, c.ChainName, c.ChainId) - if err != nil { - return nil, nil, err + nkb = append(nkb, client.NodeKeysBundle{ + PeerID: peerID, + OCR2Key: *ocrKey, + TXKey: *txKey, + }) } - // for _, n := range nodes { - // _, _, err = n.CreateStarkNetChain(&client.StarkNetChainAttributes{ - // Type: c.ChainName, - // ChainID: c.ChainId, - // Config: client.StarkNetChainConfig{}, - // }) - // if err != nil { - // return nil, nil, err - // } - // _, _, err = n.CreateStarkNetNode(&client.StarkNetNodeAttributes{ - // Name: c.ChainName, - // ChainID: c.ChainId, - // Url: "http://", // TODO: - // }) - // if err != nil { - // return nil, nil, err - // } - // } - return NKeys, nodes, nil + return nkb, nil } // CreateJobsForContract Creates and sets up the boostrap jobs as well as OCR jobs -func (c *Common) CreateJobsForContract(cc *ChainlinkClient, mockUrl string, observationSource string, juelsPerFeeCoinSource string, ocrControllerAddress string, accountAddresses []string) error { +func (c *Common) CreateJobsForContract(cc *ChainlinkClient, observationSource string, juelsPerFeeCoinSource string, ocrControllerAddress string, accountAddresses []string) error { // Define node[0] as bootstrap node cc.bootstrapPeers = []client.P2PData{ { InternalIP: cc.ChainlinkNodes[0].InternalIP(), - InternalPort: c.P2PPort, + InternalPort: c.RPCDetails.P2PPort, PeerID: cc.NKeys[0].PeerID, }, } @@ -363,7 +254,7 @@ func (c *Common) CreateJobsForContract(cc *ChainlinkClient, mockUrl string, obse bootstrapRelayConfig := job.JSONConfig{ "nodeName": fmt.Sprintf("starknet-OCRv2-%s-%s", "node", uuid.New().String()), "accountAddress": accountAddresses[0], - "chainID": c.ChainId, + "chainID": c.ChainDetails.ChainID, } oracleSpec := job.OCR2OracleSpec{ @@ -378,7 +269,6 @@ func (c *Common) CreateJobsForContract(cc *ChainlinkClient, mockUrl string, obse JobType: "bootstrap", OCR2OracleSpec: oracleSpec, } - _, _, err := cc.ChainlinkNodes[0].CreateJob(jobSpec) if err != nil { return err @@ -391,9 +281,8 @@ func (c *Common) CreateJobsForContract(cc *ChainlinkClient, mockUrl string, obse } sourceValueBridge := &client.BridgeTypeAttributes{ - Name: "mockserver-bridge", - URL: fmt.Sprintf("%s/%s", mockUrl, "five"), - RequestData: "{}", + Name: "mockserver-bridge", + URL: c.RPCDetails.MockServerEndpoint + "/" + strings.TrimPrefix(c.RPCDetails.MockServerUrl, "/"), } // Setting up job specs @@ -401,7 +290,7 @@ func (c *Common) CreateJobsForContract(cc *ChainlinkClient, mockUrl string, obse if nIdx == 0 { continue } - _, err := n.CreateBridge(sourceValueBridge) + err := n.MustCreateBridge(sourceValueBridge) if err != nil { return err } @@ -431,7 +320,7 @@ func (c *Common) CreateJobsForContract(cc *ChainlinkClient, mockUrl string, obse OCR2OracleSpec: oracleSpec, ObservationSource: observationSource, } - _, _, err = n.CreateJob(jobSpec) + _, err = n.MustCreateJob(jobSpec) if err != nil { return err } diff --git a/integration-tests/common/gauntlet_common.go b/integration-tests/common/gauntlet_common.go index 606c7741a..f293f4a5f 100644 --- a/integration-tests/common/gauntlet_common.go +++ b/integration-tests/common/gauntlet_common.go @@ -4,83 +4,87 @@ import ( "encoding/json" "errors" "fmt" - "os" - "github.com/smartcontractkit/chainlink-starknet/integration-tests/utils" + "os" ) -var ( - ethAddressGoerli = "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7" - nAccount string -) - -func (testState *Test) fundNodes() ([]string, error) { - l := utils.GetTestLogger(testState.T) +func (m *OCRv2TestState) fundNodes() ([]string, error) { + l := utils.GetTestLogger(m.TestConfig.T) var nAccounts []string - var err error - for _, key := range testState.GetNodeKeys() { + for _, key := range m.GetNodeKeys() { if key.TXKey.Data.Attributes.StarkKey == "" { return nil, errors.New("stark key can't be empty") } - nAccount, err = testState.Sg.DeployAccountContract(100, key.TXKey.Data.Attributes.StarkKey) + nAccount, err := m.Clients.GauntletClient.DeployAccountContract(100, key.TXKey.Data.Attributes.StarkKey) if err != nil { return nil, err } nAccounts = append(nAccounts, nAccount) } - if err != nil { - return nil, err - } - - if testState.Common.Testnet { + if *m.Common.TestConfig.Common.Network == "testnet" { for _, key := range nAccounts { // We are not deploying in parallel here due to testnet limitations (429 too many requests) l.Debug().Msg(fmt.Sprintf("Funding node with address: %s", key)) - _, err = testState.Sg.TransferToken(ethAddressGoerli, key, "100000000000000000") // Transferring 1 ETH to each node + _, err := m.Clients.GauntletClient.TransferToken(m.Common.ChainDetails.StarkTokenAddress, key, "100000000000000000") // Transferring 0.1 STRK to each node if err != nil { return nil, err } } - } else { - err = testState.Devnet.FundAccounts(nAccounts) - if err != nil { - return nil, err + // The starknet provided mint method does not work so we send a req directly + for _, key := range nAccounts { + res, err := m.TestConfig.Resty.R().SetBody(map[string]any{ + "address": key, + "amount": 900000000000000000, + }).Post("/mint") + if err != nil { + return nil, err + } + l.Info().Msg(fmt.Sprintf("Funding account (WEI): %s", string(res.Body()))) + res, err = m.TestConfig.Resty.R().SetBody(map[string]any{ + "address": key, + "amount": 900000000000000000, + "unit": m.Common.ChainDetails.TokenName, + }).Post("/mint") + if err != nil { + return nil, err + } + l.Info().Msg(fmt.Sprintf("Funding account (FRI): %s", string(res.Body()))) } } return nAccounts, nil } -func (testState *Test) deployLinkToken() error { +func (m *OCRv2TestState) deployLinkToken() error { var err error - testState.LinkTokenAddr, err = testState.Sg.DeployLinkTokenContract() + m.Contracts.LinkTokenAddr, err = m.Clients.GauntletClient.DeployLinkTokenContract() if err != nil { return err } - err = os.Setenv("LINK", testState.LinkTokenAddr) + err = os.Setenv("LINK", m.Contracts.LinkTokenAddr) if err != nil { return err } return nil } -func (testState *Test) deployAccessController() error { +func (m *OCRv2TestState) deployAccessController() error { var err error - testState.AccessControllerAddr, err = testState.Sg.DeployAccessControllerContract() + m.Contracts.AccessControllerAddr, err = m.Clients.GauntletClient.DeployAccessControllerContract() if err != nil { return err } - err = os.Setenv("BILLING_ACCESS_CONTROLLER", testState.AccessControllerAddr) + err = os.Setenv("BILLING_ACCESS_CONTROLLER", m.Contracts.AccessControllerAddr) if err != nil { return err } return nil } -func (testState *Test) setConfigDetails(ocrAddress string) error { - cfg, err := testState.LoadOCR2Config() +func (m *OCRv2TestState) setConfigDetails(ocrAddress string) error { + cfg, err := m.LoadOCR2Config() if err != nil { return err } @@ -89,54 +93,54 @@ func (testState *Test) setConfigDetails(ocrAddress string) error { if err != nil { return err } - _, err = testState.Sg.SetConfigDetails(string(parsedConfig), ocrAddress) + _, err = m.Clients.GauntletClient.SetConfigDetails(string(parsedConfig), ocrAddress) return err } -func (testState *Test) DeployGauntlet(minSubmissionValue int64, maxSubmissionValue int64, decimals int, name string, observationPaymentGjuels int64, transmissionPaymentGjuels int64) error { - err := testState.Sg.InstallDependencies() +func (m *OCRv2TestState) DeployGauntlet(minSubmissionValue int64, maxSubmissionValue int64, decimals int, name string, observationPaymentGjuels int64, transmissionPaymentGjuels int64) error { + err := m.Clients.GauntletClient.InstallDependencies() if err != nil { return err } - testState.AccountAddresses, err = testState.fundNodes() + m.Clients.ChainlinkClient.AccountAddresses, err = m.fundNodes() if err != nil { return err } - err = testState.deployLinkToken() + err = m.deployLinkToken() if err != nil { return err } - err = testState.deployAccessController() + err = m.deployAccessController() if err != nil { return err } - testState.OCRAddr, err = testState.Sg.DeployOCR2ControllerContract(minSubmissionValue, maxSubmissionValue, decimals, name, testState.LinkTokenAddr) + m.Contracts.OCRAddr, err = m.Clients.GauntletClient.DeployOCR2ControllerContract(minSubmissionValue, maxSubmissionValue, decimals, name, m.Contracts.LinkTokenAddr) if err != nil { return err } - testState.ProxyAddr, err = testState.Sg.DeployOCR2ProxyContract(testState.OCRAddr) + m.Contracts.ProxyAddr, err = m.Clients.GauntletClient.DeployOCR2ProxyContract(m.Contracts.OCRAddr) if err != nil { return err } - _, err = testState.Sg.AddAccess(testState.OCRAddr, testState.ProxyAddr) + _, err = m.Clients.GauntletClient.AddAccess(m.Contracts.OCRAddr, m.Contracts.ProxyAddr) if err != nil { return err } - _, err = testState.Sg.MintLinkToken(testState.LinkTokenAddr, testState.OCRAddr, "100000000000000000000") + _, err = m.Clients.GauntletClient.MintLinkToken(m.Contracts.LinkTokenAddr, m.Contracts.OCRAddr, "100000000000000000000") if err != nil { return err } - _, err = testState.Sg.SetOCRBilling(observationPaymentGjuels, transmissionPaymentGjuels, testState.OCRAddr) + _, err = m.Clients.GauntletClient.SetOCRBilling(observationPaymentGjuels, transmissionPaymentGjuels, m.Contracts.OCRAddr) if err != nil { return err } - err = testState.setConfigDetails(testState.OCRAddr) + err = m.setConfigDetails(m.Contracts.OCRAddr) return err } diff --git a/integration-tests/common/test_common.go b/integration-tests/common/test_common.go index 81cced561..fcec8f904 100644 --- a/integration-tests/common/test_common.go +++ b/integration-tests/common/test_common.go @@ -3,135 +3,251 @@ package common import ( "context" "fmt" + starknetdevnet "github.com/NethermindEth/starknet.go/devnet" + "github.com/go-resty/resty/v2" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + test_env_ctf "github.com/smartcontractkit/chainlink-testing-framework/docker/test_env" + "net/http" + + test_env_starknet "github.com/smartcontractkit/chainlink-starknet/integration-tests/docker/test_env" + "github.com/smartcontractkit/chainlink-starknet/integration-tests/testconfig" + "github.com/smartcontractkit/chainlink-testing-framework/logging" + "github.com/smartcontractkit/chainlink/integration-tests/docker/test_env" "math/big" - "os" "testing" "time" "github.com/NethermindEth/juno/core/felt" - starknetdevnet "github.com/NethermindEth/starknet.go/devnet" starknetutils "github.com/NethermindEth/starknet.go/utils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-starknet/relayer/pkg/chainlink/ocr2" "github.com/smartcontractkit/chainlink-starknet/relayer/pkg/starknet" "github.com/smartcontractkit/chainlink-starknet/ops" - "github.com/smartcontractkit/chainlink-starknet/ops/devnet" "github.com/smartcontractkit/chainlink-starknet/ops/gauntlet" "github.com/smartcontractkit/chainlink/integration-tests/client" - - "github.com/smartcontractkit/chainlink-starknet/integration-tests/utils" ) var ( rpcRequestTimeout = time.Second * 300 - dumpPath = "/dumps/dump.pkl" ) -type Test struct { - Devnet *devnet.StarknetDevnetClient - Cc *ChainlinkClient - Starknet *starknet.Client - OCR2Client *ocr2.Client - Sg *gauntlet.StarknetGauntlet - L1RPCUrl string - Common *Common - AccountAddresses []string +// OCRv2TestState Main testing state struct +type OCRv2TestState struct { + Account *AccountDetails + Clients *Clients + ChainlinkNodesK8s []*client.ChainlinkK8sClient + Common *Common + TestConfig *TestConfig + Contracts *Contracts +} + +// AccountDetails for deployment and funding +type AccountDetails struct { + Account string + PrivateKey string +} + +// Clients to access internal methods +type Clients struct { + StarknetClient *starknet.Client + DevnetClient *starknetdevnet.DevNet + KillgraveClient *test_env_ctf.Killgrave + OCR2Client *ocr2.Client + ChainlinkClient *ChainlinkClient + GauntletClient *gauntlet.StarknetGauntlet + DockerEnv *StarknetClusterTestEnv +} + +// Contracts to store current deployed contract state +type Contracts struct { LinkTokenAddr string OCRAddr string AccessControllerAddr string ProxyAddr string ObservationSource string JuelsPerFeeCoinSource string - T *testing.T } +// ChainlinkClient core node configs type ChainlinkClient struct { - NKeys []client.NodeKeysBundle - ChainlinkNodes []*client.ChainlinkClient - bTypeAttr *client.BridgeTypeAttributes - bootstrapPeers []client.P2PData + NKeys []client.NodeKeysBundle + ChainlinkNodes []*client.ChainlinkClient + bTypeAttr *client.BridgeTypeAttributes + bootstrapPeers []client.P2PData + AccountAddresses []string } -// DeployCluster Deploys and sets up config of the environment and nodes -func (testState *Test) DeployCluster() { - lggr := logger.Nop() - testState.Cc = &ChainlinkClient{} - testState.ObservationSource = testState.GetDefaultObservationSource() - testState.JuelsPerFeeCoinSource = testState.GetDefaultJuelsPerFeeCoinSource() - testState.DeployEnv() - if testState.Common.Env.WillUseRemoteRunner() { - return // short circuit here if using a remote runner +type StarknetClusterTestEnv struct { + *test_env.CLClusterTestEnv + Starknet *test_env_starknet.Starknet + Killgrave *test_env_ctf.Killgrave +} + +type TestConfig struct { + T *testing.T + L zerolog.Logger + TestConfig *testconfig.TestConfig + Resty *resty.Client + err error +} + +func NewOCRv2State(t *testing.T, namespacePrefix string, testConfig *testconfig.TestConfig) (*OCRv2TestState, error) { + c, err := New(testConfig).Default(t, namespacePrefix) + if err != nil { + return nil, err } - testState.SetupClients() - if testState.Common.Testnet { - testState.Common.Env.URLs[testState.Common.ServiceKeyL2][1] = testState.Common.L2RPCUrl + state := &OCRv2TestState{ + Account: &AccountDetails{}, + Clients: &Clients{ + ChainlinkClient: &ChainlinkClient{}, + }, + Common: c, + TestConfig: &TestConfig{ + T: t, + L: log.Logger, + TestConfig: testConfig, + Resty: nil, + err: nil, + }, + Contracts: &Contracts{}, } - var err error - testState.Cc.NKeys, testState.Cc.ChainlinkNodes, err = testState.Common.CreateKeys(testState.Common.Env) - require.NoError(testState.T, err, "Creating chains and keys should not fail") // TODO; fails here - baseURL := testState.Common.L2RPCUrl - if !testState.Common.Testnet { // devnet! - // chainlink starknet client needs the RPC API url which is at /rpc on devnet - baseURL += "/rpc" + + // Setting default job configs + state.Contracts.ObservationSource = state.GetDefaultObservationSource() + state.Contracts.JuelsPerFeeCoinSource = state.GetDefaultJuelsPerFeeCoinSource() + + if state.TestConfig.T != nil { + state.TestConfig.L = logging.GetTestLogger(state.TestConfig.T) } - testState.Starknet, err = starknet.NewClient(testState.Common.ChainId, baseURL, lggr, &rpcRequestTimeout) - require.NoError(testState.T, err, "Creating starknet client should not fail") - testState.OCR2Client, err = ocr2.NewClient(testState.Starknet, lggr) - require.NoError(testState.T, err, "Creating ocr2 client should not fail") - if !testState.Common.Testnet { + + return state, nil +} + +// DeployCluster Deploys and sets up config of the environment and nodes +func (m *OCRv2TestState) DeployCluster() { + // When running soak we need to use K8S + if *m.Common.TestConfig.Common.InsideK8s { + m.DeployEnv() + if m.Common.Env.WillUseRemoteRunner() { + return + } + // Setting RPC details + m.Common.RPCDetails.RPCL2External = m.Common.Env.URLs["starknet-dev"][0] + if *m.Common.TestConfig.Common.Network == "testnet" { + m.Common.RPCDetails.RPCL2External = *m.Common.TestConfig.Common.L2RPCUrl + m.Common.RPCDetails.RPCL2Internal = *m.Common.TestConfig.Common.L2RPCUrl + } + m.Common.RPCDetails.MockServerEndpoint = m.Common.Env.URLs["qa_mock_adapter_internal"][0] + m.Common.RPCDetails.MockServerUrl = "five" + + } else { // Otherwise use docker + env, err := test_env.NewTestEnv() + require.NoError(m.TestConfig.T, err) + stark := test_env_starknet.NewStarknet([]string{env.Network.Name}, *m.Common.TestConfig.Common.DevnetImage) + err = stark.StartContainer() + require.NoError(m.TestConfig.T, err) + + // Setting RPC details + m.Common.RPCDetails.RPCL2External = stark.ExternalHttpUrl + m.Common.RPCDetails.RPCL2Internal = stark.InternalHttpUrl + + if *m.Common.TestConfig.Common.Network == "testnet" { + m.Common.RPCDetails.RPCL2External = *m.Common.TestConfig.Common.L2RPCUrl + m.Common.RPCDetails.RPCL2Internal = *m.Common.TestConfig.Common.L2RPCUrl + } + + // Creating docker containers + b, err := test_env.NewCLTestEnvBuilder(). + WithNonEVM(). + WithTestInstance(m.TestConfig.T). + WithTestConfig(m.TestConfig.TestConfig). + WithMockAdapter(). + WithCLNodeConfig(m.Common.DefaultNodeConfig()). + WithCLNodes(*m.Common.TestConfig.OCR2.NodeCount). + WithCLNodeOptions(m.Common.TestEnvDetails.NodeOpts...). + WithStandardCleanup(). + WithTestEnv(env) + require.NoError(m.TestConfig.T, err) + env, err = b.Build() + require.NoError(m.TestConfig.T, err) + m.Clients.DockerEnv = &StarknetClusterTestEnv{ + CLClusterTestEnv: env, + Starknet: stark, + Killgrave: env.MockAdapter, + } + + // Setting up Mock adapter + m.Clients.KillgraveClient = env.MockAdapter + m.Common.RPCDetails.MockServerEndpoint = m.Clients.KillgraveClient.InternalEndpoint + m.Common.RPCDetails.MockServerUrl = "mockserver-bridge" + err = m.Clients.KillgraveClient.SetAdapterBasedIntValuePath("/mockserver-bridge", []string{http.MethodGet, http.MethodPost}, 10) + require.NoError(m.TestConfig.T, err, "Failed to set mock adapter value") + } + + m.TestConfig.Resty = resty.New().SetBaseURL(m.Common.RPCDetails.RPCL2External) + m.SetupClients() + if *m.Common.TestConfig.Common.InsideK8s { + m.Clients.ChainlinkClient.ChainlinkNodes = m.GetChainlinkNodes() + m.Clients.ChainlinkClient.NKeys, m.TestConfig.err = m.Common.CreateNodeKeysBundle(m.Clients.ChainlinkClient.ChainlinkNodes) + require.NoError(m.TestConfig.T, m.TestConfig.err) + } else { + m.Clients.ChainlinkClient.NKeys, m.TestConfig.err = m.Common.CreateNodeKeysBundle(m.Clients.DockerEnv.ClCluster.NodeAPIs()) + require.NoError(m.TestConfig.T, m.TestConfig.err) + } + lggr := logger.Nop() + m.Clients.StarknetClient, m.TestConfig.err = starknet.NewClient(m.Common.ChainDetails.ChainID, m.Common.RPCDetails.RPCL2External, lggr, &rpcRequestTimeout) + require.NoError(m.TestConfig.T, m.TestConfig.err, "Creating starknet client should not fail") + m.Clients.OCR2Client, m.TestConfig.err = ocr2.NewClient(m.Clients.StarknetClient, lggr) + require.NoError(m.TestConfig.T, m.TestConfig.err, "Creating ocr2 client should not fail") + + // If we are using devnet fetch the default keys + if *m.Common.TestConfig.Common.Network == "localnet" { // fetch predeployed account 0 to use as funder - devnet := starknetdevnet.NewDevNet(testState.Common.L2RPCUrl) - accounts, err := devnet.Accounts() - require.NoError(testState.T, err) + m.Clients.DevnetClient = starknetdevnet.NewDevNet(m.Common.RPCDetails.RPCL2External) + accounts, err := m.Clients.DevnetClient.Accounts() + require.NoError(m.TestConfig.T, err) account := accounts[0] - - err = os.Setenv("PRIVATE_KEY", account.PrivateKey) - require.NoError(testState.T, err, "Setting private key should not fail") - err = os.Setenv("ACCOUNT", account.Address) - require.NoError(testState.T, err, "Setting account address should not fail") - testState.Devnet.AutoDumpState() // Auto dumping devnet state to avoid losing contracts on crash + m.Account.Account = account.Address + m.Account.PrivateKey = account.PrivateKey + } else { + m.Account.Account = *m.TestConfig.TestConfig.Common.Account + m.Account.PrivateKey = *m.TestConfig.TestConfig.Common.PrivateKey } } // DeployEnv Deploys the environment -func (testState *Test) DeployEnv() { - testState.Common.SetLocalEnvironment(testState.T) - // if testState.Common.Env.WillUseRemoteRunner() { - // return // short circuit here if using a remote runner - // } +func (m *OCRv2TestState) DeployEnv() { + err := m.Common.Env.Run() + require.NoError(m.TestConfig.T, err) } // SetupClients Sets up the starknet client -func (testState *Test) SetupClients() { - l := utils.GetTestLogger(testState.T) - if testState.Common.Testnet { - l.Debug().Msg(fmt.Sprintf("Overriding L2 RPC: %s", testState.Common.L2RPCUrl)) +func (m *OCRv2TestState) SetupClients() { + + if *m.Common.TestConfig.Common.InsideK8s { + m.ChainlinkNodesK8s, m.TestConfig.err = client.ConnectChainlinkNodes(m.Common.Env) + require.NoError(m.TestConfig.T, m.TestConfig.err) } else { - // TODO: HAXX: - // testState.Common.L2RPCUrl = testState.Common.Env.URLs[testState.Common.ServiceKeyL2][0] // For local runs setting local ip - // if testState.Common.Env.Cfg.InsideK8s { - // testState.Common.L2RPCUrl = testState.Common.Env.URLs[testState.Common.ServiceKeyL2][1] // For remote runner setting remote IP - // } - l.Debug().Msg(fmt.Sprintf("L2 RPC: %s", testState.Common.L2RPCUrl)) - testState.Devnet = testState.Devnet.NewStarknetDevnetClient(testState.Common.L2RPCUrl, dumpPath) + m.Clients.ChainlinkClient.ChainlinkNodes = m.Clients.DockerEnv.ClCluster.NodeAPIs() } } // LoadOCR2Config Loads and returns the default starknet gauntlet config -func (testState *Test) LoadOCR2Config() (*ops.OCR2Config, error) { +func (m *OCRv2TestState) LoadOCR2Config() (*ops.OCR2Config, error) { var offChaiNKeys []string var onChaiNKeys []string var peerIds []string var txKeys []string var cfgKeys []string - for i, key := range testState.Cc.NKeys { + for i, key := range m.Clients.ChainlinkClient.NKeys { offChaiNKeys = append(offChaiNKeys, key.OCR2Key.Data.Attributes.OffChainPublicKey) peerIds = append(peerIds, key.PeerID) - txKeys = append(txKeys, testState.AccountAddresses[i]) + txKeys = append(txKeys, m.Clients.ChainlinkClient.AccountAddresses[i]) onChaiNKeys = append(onChaiNKeys, key.OCR2Key.Data.Attributes.OnChainPublicKey) cfgKeys = append(cfgKeys, key.OCR2Key.Data.Attributes.ConfigPublicKey) } @@ -146,39 +262,34 @@ func (testState *Test) LoadOCR2Config() (*ops.OCR2Config, error) { return &payload, nil } -func (testState *Test) SetUpNodes() { - err := testState.Common.CreateJobsForContract(testState.GetChainlinkClient(), testState.Common.MockUrl, testState.ObservationSource, testState.JuelsPerFeeCoinSource, testState.OCRAddr, testState.AccountAddresses) - require.NoError(testState.T, err, "Creating jobs should not fail") +func (m *OCRv2TestState) SetUpNodes() { + err := m.Common.CreateJobsForContract(m.GetChainlinkClient(), m.Contracts.ObservationSource, m.Contracts.JuelsPerFeeCoinSource, m.Contracts.OCRAddr, m.Clients.ChainlinkClient.AccountAddresses) + require.NoError(m.TestConfig.T, err, "Creating jobs should not fail") } // GetNodeKeys Returns the node key bundles -func (testState *Test) GetNodeKeys() []client.NodeKeysBundle { - return testState.Cc.NKeys +func (m *OCRv2TestState) GetNodeKeys() []client.NodeKeysBundle { + return m.Clients.ChainlinkClient.NKeys } -func (testState *Test) GetChainlinkNodes() []*client.ChainlinkClient { - return testState.Cc.ChainlinkNodes -} - -func (testState *Test) GetChainlinkClient() *ChainlinkClient { - return testState.Cc -} - -func (testState *Test) GetStarknetDevnetClient() *devnet.StarknetDevnetClient { - return testState.Devnet +func (m *OCRv2TestState) GetChainlinkNodes() []*client.ChainlinkClient { + // retrieve client from K8s client + var chainlinkNodes []*client.ChainlinkClient + for i := range m.ChainlinkNodesK8s { + chainlinkNodes = append(chainlinkNodes, m.ChainlinkNodesK8s[i].ChainlinkClient) + } + return chainlinkNodes } -func (testState *Test) SetBridgeTypeAttrs(attr *client.BridgeTypeAttributes) { - testState.Cc.bTypeAttr = attr +func (m *OCRv2TestState) GetChainlinkClient() *ChainlinkClient { + return m.Clients.ChainlinkClient } -// ConfigureL1Messaging Sets the L1 messaging contract location and RPC url on L2 -func (testState *Test) ConfigureL1Messaging() { - err := testState.Devnet.LoadL1MessagingContract(testState.L1RPCUrl) - require.NoError(testState.T, err, "Setting up L1 messaging should not fail") +func (m *OCRv2TestState) SetBridgeTypeAttrs(attr *client.BridgeTypeAttributes) { + m.Clients.ChainlinkClient.bTypeAttr = attr } -func (testState *Test) GetDefaultObservationSource() string { +func (m *OCRv2TestState) GetDefaultObservationSource() string { return ` val [type = "bridge" name="mockserver-bridge"] parse [type="jsonparse" path="data,result"] @@ -186,7 +297,7 @@ func (testState *Test) GetDefaultObservationSource() string { ` } -func (testState *Test) GetDefaultJuelsPerFeeCoinSource() string { +func (m *OCRv2TestState) GetDefaultJuelsPerFeeCoinSource() string { return `""" sum [type="sum" values=<[451000]> ] sum @@ -194,8 +305,7 @@ func (testState *Test) GetDefaultJuelsPerFeeCoinSource() string { ` } -func (testState *Test) ValidateRounds(rounds int, isSoak bool) error { - l := utils.GetTestLogger(testState.T) +func (m *OCRv2TestState) ValidateRounds(rounds int, isSoak bool) error { ctx := context.Background() // context background used because timeout handled by requestTimeout param // assert new rounds are occurring details := ocr2.TransmissionDetails{} @@ -205,25 +315,25 @@ func (testState *Test) ValidateRounds(rounds int, isSoak bool) error { var positive bool // validate balance in aggregator - linkContractAddress, err := starknetutils.HexToFelt(testState.LinkTokenAddr) + linkContractAddress, err := starknetutils.HexToFelt(m.Contracts.LinkTokenAddr) if err != nil { return err } - contractAddress, err := starknetutils.HexToFelt(testState.OCRAddr) + contractAddress, err := starknetutils.HexToFelt(m.Contracts.OCRAddr) if err != nil { return err } - resLINK, errLINK := testState.Starknet.CallContract(ctx, starknet.CallOps{ + resLINK, errLINK := m.Clients.StarknetClient.CallContract(ctx, starknet.CallOps{ ContractAddress: linkContractAddress, Selector: starknetutils.GetSelectorFromNameFelt("balance_of"), Calldata: []*felt.Felt{contractAddress}, }) - require.NoError(testState.T, errLINK, "Reader balance from LINK contract should not fail", "err", errLINK) - resAgg, errAgg := testState.Starknet.CallContract(ctx, starknet.CallOps{ + require.NoError(m.TestConfig.T, errLINK, "Reader balance from LINK contract should not fail", "err", errLINK) + resAgg, errAgg := m.Clients.StarknetClient.CallContract(ctx, starknet.CallOps{ ContractAddress: contractAddress, Selector: starknetutils.GetSelectorFromNameFelt("link_available_for_payment"), }) - require.NoError(testState.T, errAgg, "link_available_for_payment should not fail", "err", errAgg) + require.NoError(m.TestConfig.T, errAgg, "link_available_for_payment should not fail", "err", errAgg) balLINK := resLINK[0].BigInt(big.NewInt(0)) balAgg := resAgg[1].BigInt(big.NewInt(0)) isNegative := resAgg[0].BigInt(big.NewInt(0)) @@ -231,13 +341,13 @@ func (testState *Test) ValidateRounds(rounds int, isSoak bool) error { balAgg = new(big.Int).Neg(balAgg) } - assert.Equal(testState.T, balLINK.Cmp(big.NewInt(0)), 1, "Aggregator should have non-zero balance") - assert.GreaterOrEqual(testState.T, balLINK.Cmp(balAgg), 0, "Aggregator payment balance should be <= actual LINK balance") + assert.Equal(m.TestConfig.T, balLINK.Cmp(big.NewInt(0)), 1, "Aggregator should have non-zero balance") + assert.GreaterOrEqual(m.TestConfig.T, balLINK.Cmp(balAgg), 0, "Aggregator payment balance should be <= actual LINK balance") - for start := time.Now(); time.Since(start) < testState.Common.TestDuration; { - l.Info().Msg(fmt.Sprintf("Elapsed time: %s, Round wait: %s ", time.Since(start), testState.Common.TestDuration)) - res, err2 := testState.OCR2Client.LatestTransmissionDetails(ctx, contractAddress) - require.NoError(testState.T, err2, "Failed to get latest transmission details") + for start := time.Now(); time.Since(start) < m.Common.TestEnvDetails.TestDuration; { + m.TestConfig.L.Info().Msg(fmt.Sprintf("Elapsed time: %s, Round wait: %s ", time.Since(start), m.Common.TestEnvDetails.TestDuration)) + res, err2 := m.Clients.OCR2Client.LatestTransmissionDetails(ctx, contractAddress) + require.NoError(m.TestConfig.T, err2, "Failed to get latest transmission details") // end condition: enough rounds have occurred if !isSoak && increasing >= rounds && positive { break @@ -245,7 +355,7 @@ func (testState *Test) ValidateRounds(rounds int, isSoak bool) error { // end condition: rounds have been stuck if stuck && stuckCount > 50 { - l.Debug().Msg("failing to fetch transmissions means blockchain may have stopped") + m.TestConfig.L.Debug().Msg("failing to fetch transmissions means blockchain may have stopped") break } @@ -253,10 +363,10 @@ func (testState *Test) ValidateRounds(rounds int, isSoak bool) error { time.Sleep(5 * time.Second) if err != nil { - l.Error().Msg(fmt.Sprintf("Transmission Error: %+v", err)) + m.TestConfig.L.Error().Msg(fmt.Sprintf("Transmission Error: %+v", err)) continue } - l.Info().Msg(fmt.Sprintf("Transmission Details: %+v", res)) + m.TestConfig.L.Info().Msg(fmt.Sprintf("Transmission Details: %+v", res)) // continue if no changes if res.Epoch == 0 && res.Round == 0 { @@ -269,21 +379,21 @@ func (testState *Test) ValidateRounds(rounds int, isSoak bool) error { // if changes from zero values set (should only initially) if res.Epoch > 0 && details.Epoch == 0 { if !isSoak { - assert.Greater(testState.T, res.Epoch, details.Epoch) - assert.GreaterOrEqual(testState.T, res.Round, details.Round) - assert.NotEqual(testState.T, ansCmp, 0) // assert changed from 0 - assert.NotEqual(testState.T, res.Digest, details.Digest) - assert.Equal(testState.T, details.LatestTimestamp.Before(res.LatestTimestamp), true) + assert.Greater(m.TestConfig.T, res.Epoch, details.Epoch) + assert.GreaterOrEqual(m.TestConfig.T, res.Round, details.Round) + assert.NotEqual(m.TestConfig.T, ansCmp, 0) // assert changed from 0 + assert.NotEqual(m.TestConfig.T, res.Digest, details.Digest) + assert.Equal(m.TestConfig.T, details.LatestTimestamp.Before(res.LatestTimestamp), true) } details = res continue } // check increasing rounds if !isSoak { - assert.Equal(testState.T, res.Digest, details.Digest, "Config digest should not change") + assert.Equal(m.TestConfig.T, res.Digest, details.Digest, "Config digest should not change") } else { if res.Digest != details.Digest { - l.Error().Msg(fmt.Sprintf("Config digest should not change, expected %s got %s", details.Digest, res.Digest)) + m.TestConfig.L.Error().Msg(fmt.Sprintf("Config digest should not change, expected %s got %s", details.Digest, res.Digest)) } } if (res.Epoch > details.Epoch || (res.Epoch == details.Epoch && res.Round > details.Round)) && details.LatestTimestamp.Before(res.LatestTimestamp) { @@ -301,31 +411,31 @@ func (testState *Test) ValidateRounds(rounds int, isSoak bool) error { } } if !isSoak { - assert.GreaterOrEqual(testState.T, increasing, rounds, "Round + epochs should be increasing") - assert.Equal(testState.T, positive, true, "Positive value should have been submitted") - assert.Equal(testState.T, stuck, false, "Round + epochs should not be stuck") + assert.GreaterOrEqual(m.TestConfig.T, increasing, rounds, "Round + epochs should be increasing") + assert.Equal(m.TestConfig.T, positive, true, "Positive value should have been submitted") + assert.Equal(m.TestConfig.T, stuck, false, "Round + epochs should not be stuck") } // Test proxy reading // TODO: would be good to test proxy switching underlying feeds - proxyAddress, err := starknetutils.HexToFelt(testState.ProxyAddr) + proxyAddress, err := starknetutils.HexToFelt(m.Contracts.ProxyAddr) if err != nil { return err } - roundDataRaw, err := testState.Starknet.CallContract(ctx, starknet.CallOps{ + roundDataRaw, err := m.Clients.StarknetClient.CallContract(ctx, starknet.CallOps{ ContractAddress: proxyAddress, Selector: starknetutils.GetSelectorFromNameFelt("latest_round_data"), }) if !isSoak { - require.NoError(testState.T, err, "Reading round data from proxy should not fail") - assert.Equal(testState.T, len(roundDataRaw), 5, "Round data from proxy should match expected size") + require.NoError(m.TestConfig.T, err, "Reading round data from proxy should not fail") + assert.Equal(m.TestConfig.T, len(roundDataRaw), 5, "Round data from proxy should match expected size") } valueBig := roundDataRaw[1].BigInt(big.NewInt(0)) - require.NoError(testState.T, err) + require.NoError(m.TestConfig.T, err) value := valueBig.Int64() if value < 0 { - assert.Equal(testState.T, value, int64(5), "Reading from proxy should return correct value") + assert.Equal(m.TestConfig.T, value, int64(5), "Reading from proxy should return correct value") } return nil diff --git a/integration-tests/config/config.go b/integration-tests/config/config.go new file mode 100644 index 000000000..d54e05c12 --- /dev/null +++ b/integration-tests/config/config.go @@ -0,0 +1,34 @@ +package config + +var ( + starkTokenAddress = "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d" +) + +type Config struct { + ChainName string + ChainID string + StarkTokenAddress string + L2RPCInternal string + TokenName string +} + +func SepoliaConfig() *Config { + return &Config{ + ChainName: "starknet", + ChainID: "SN_SEPOLIA", + StarkTokenAddress: starkTokenAddress, + // Will be overridden if set in toml + L2RPCInternal: "https://starknet-sepolia.public.blastapi.io/rpc/v0_6", + } +} + +func DevnetConfig() *Config { + return &Config{ + ChainName: "starknet", + ChainID: "SN_GOERLI", + StarkTokenAddress: starkTokenAddress, + // Will be overridden if set in toml + L2RPCInternal: "http://starknet-dev:5000", + TokenName: "FRI", + } +} diff --git a/integration-tests/docker/test_env/stark.go b/integration-tests/docker/test_env/stark.go new file mode 100644 index 000000000..a1f3bd3a9 --- /dev/null +++ b/integration-tests/docker/test_env/stark.go @@ -0,0 +1,107 @@ +package test_env + +import ( + "fmt" + "testing" + "time" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + tc "github.com/testcontainers/testcontainers-go" + tcwait "github.com/testcontainers/testcontainers-go/wait" + + "github.com/smartcontractkit/chainlink-testing-framework/docker/test_env" + "github.com/smartcontractkit/chainlink-testing-framework/logging" + "github.com/smartcontractkit/chainlink-testing-framework/utils/testcontext" +) + +const ( + STARK_HTTP_PORT = "5050" +) + +type Starknet struct { + test_env.EnvComponent + ExternalHttpUrl string + InternalHttpUrl string + t *testing.T + l zerolog.Logger + Image string +} + +func NewStarknet(networks []string, image string, opts ...test_env.EnvComponentOption) *Starknet { + ms := &Starknet{ + Image: image, + EnvComponent: test_env.EnvComponent{ + ContainerName: "starknet", + Networks: networks, + }, + + l: log.Logger, + } + for _, opt := range opts { + opt(&ms.EnvComponent) + } + return ms +} + +func (s *Starknet) WithTestLogger(t *testing.T) *Starknet { + s.l = logging.GetTestLogger(t) + s.t = t + return s +} + +func (s *Starknet) StartContainer() error { + l := tc.Logger + if s.t != nil { + l = logging.CustomT{ + T: s.t, + L: s.l, + } + } + cReq, err := s.getContainerRequest() + if err != nil { + return err + } + c, err := tc.GenericContainer(testcontext.Get(s.t), tc.GenericContainerRequest{ + ContainerRequest: *cReq, + Reuse: true, + Started: true, + Logger: l, + }) + if err != nil { + return fmt.Errorf("cannot start Starknet container: %w", err) + } + s.Container = c + host, err := test_env.GetHost(testcontext.Get(s.t), c) + if err != nil { + return err + } + httpPort, err := c.MappedPort(testcontext.Get(s.t), test_env.NatPort(STARK_HTTP_PORT)) + if err != nil { + return err + } + + s.ExternalHttpUrl = fmt.Sprintf("http://%s:%s", host, httpPort.Port()) + s.InternalHttpUrl = fmt.Sprintf("http://%s:%s", s.ContainerName, STARK_HTTP_PORT) + + s.l.Info(). + Any("ExternalHttpUrl", s.ExternalHttpUrl). + Any("InternalHttpUrl", s.InternalHttpUrl). + Str("containerName", s.ContainerName). + Msgf("Started Starknet container") + + return nil +} + +func (s *Starknet) getContainerRequest() (*tc.ContainerRequest, error) { + return &tc.ContainerRequest{ + Name: s.ContainerName, + Image: s.Image, + ExposedPorts: []string{test_env.NatPortFormat(STARK_HTTP_PORT)}, + Networks: s.Networks, + WaitingFor: tcwait.ForLog("Starknet Devnet listening"). + WithStartupTimeout(30 * time.Second). + WithPollInterval(100 * time.Millisecond), + Entrypoint: []string{"sh", "-c", "tini -- starknet-devnet --host 0.0.0.0 --port 5050 --seed 0 --account-class cairo1 --gas-price 1"}, + }, nil +} diff --git a/integration-tests/go.mod b/integration-tests/go.mod index 89ec2a6b5..3437c1f10 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -5,8 +5,12 @@ go 1.21.4 require ( github.com/NethermindEth/juno v0.3.1 github.com/NethermindEth/starknet.go v0.6.1-0.20231218140327-915109ab5bc1 + github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df + github.com/go-resty/resty/v2 v2.7.0 github.com/google/uuid v1.4.0 github.com/lib/pq v1.10.9 + github.com/pelletier/go-toml/v2 v2.1.1 + github.com/pkg/errors v0.9.1 github.com/rs/zerolog v1.30.0 github.com/smartcontractkit/chainlink-common v0.1.7-0.20240213113935-001c2f4befd4 github.com/smartcontractkit/chainlink-starknet/ops v0.0.0-20231205180940-ea2e3e916725 @@ -15,7 +19,9 @@ require ( github.com/smartcontractkit/chainlink/integration-tests v0.0.0-20240215151806-009c99876c4c github.com/smartcontractkit/chainlink/v2 v2.9.0-beta0.0.20240215151806-009c99876c4c github.com/stretchr/testify v1.8.4 + github.com/testcontainers/testcontainers-go v0.23.0 go.uber.org/zap v1.26.0 + golang.org/x/text v0.14.0 gopkg.in/guregu/null.v4 v4.0.0 ) @@ -65,7 +71,6 @@ require ( github.com/aws/constructs-go/constructs/v10 v10.1.255 // indirect github.com/aws/jsii-runtime-go v1.75.0 // indirect github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59 // indirect - github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 // indirect github.com/blendle/zapdriver v1.3.1 // indirect @@ -173,7 +178,6 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.15.5 // indirect github.com/go-redis/redis/v8 v8.11.5 // indirect - github.com/go-resty/resty/v2 v2.7.0 // indirect github.com/go-sql-driver/mysql v1.7.1 // indirect github.com/go-webauthn/webauthn v0.9.4 // indirect github.com/go-webauthn/x v0.1.5 // indirect @@ -317,11 +321,9 @@ require ( github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.1.1 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/petermattis/goid v0.0.0-20230317030725-371a4b8eda08 // indirect github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/prometheus/alertmanager v0.26.0 // indirect @@ -375,7 +377,6 @@ require ( github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect github.com/tendermint/go-amino v0.16.0 // indirect github.com/teris-io/shortid v0.0.0-20201117134242-e59966efd125 // indirect - github.com/testcontainers/testcontainers-go v0.23.0 // indirect github.com/theodesp/go-heaps v0.0.0-20190520121037-88e35354fe0a // indirect github.com/tidwall/btree v1.6.0 // indirect github.com/tidwall/gjson v1.17.0 // indirect @@ -430,7 +431,6 @@ require ( golang.org/x/sync v0.6.0 // indirect golang.org/x/sys v0.17.0 // indirect golang.org/x/term v0.17.0 // indirect - golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.18.0 // indirect gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect diff --git a/integration-tests/infra_deployments/deployer_test.go b/integration-tests/infra_deployments/deployer_test.go deleted file mode 100644 index cb5753b16..000000000 --- a/integration-tests/infra_deployments/deployer_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package infra_deployments_test - -//import ( -//"fmt" -//"net/url" -//"testing" - -//"github.com/stretchr/testify/require" - -//"github.com/smartcontractkit/chainlink-starknet/integration-tests/common" -//"github.com/smartcontractkit/chainlink-starknet/ops/gauntlet" -//"github.com/smartcontractkit/chainlink-starknet/ops/utils" -//"github.com/smartcontractkit/chainlink/integration-tests/client" -//) - -//const ( -//L2RpcUrl = "https://alpha4-2.starknet.io" -//P2pPort = "6691" -//) - -//var ( -//observationSource = ` -//val [type="bridge" name="bridge-coinmetrics" requestData=<{"data": {"from":"LINK","to":"USD"}}>] -//parse [type="jsonparse" path="result"] -//val -> parse -//` -//juelsPerFeeCoinSource = `""" -//sum [type="sum" values=<[451000]> ] -//sum -//""" -//` -//) - -//func createKeys(testState *testing.T) ([]*client.ChainlinkK8sClient, error) { -//urls := [][]string{ -//// Node access params -//{"NODE_URL", "NODE_USER", "NODE_PASS"}, -//} -//var clients []*client.ChainlinkK8sClient - -//for _, nodeUrl := range urls { -//u, _ := url.Parse(nodeUrl[0]) -//c, err := client.NewChainlinkK8sClient(&client.ChainlinkConfig{ -//URL: nodeUrl[0], -//Email: nodeUrl[1], -//Password: nodeUrl[2], -//InternalIP: u.Host, -//}, "", "") -//if err != nil { -//return nil, err -//} -//key, _ := c.MustReadP2PKeys() -//if key == nil { -//_, _, err = c.CreateP2PKey() -//require.NoError(testState, err) -//} -//clients = append(clients, c) -//} -//return clients, nil -//} -//func TestOCRBasic(testState *testing.T) { -//var err error -//t := &common.Test{} -//t.Common = common.New(testState) -//t.Cc = &common.ChainlinkClient{} -//t.Common.P2PPort = P2pPort -//t.Cc.ChainlinkNodes, err = createKeys(testState) -//require.NoError(testState, err) -//t.Cc.NKeys, _, err = client.CreateNodeKeysBundle(t.GetChainlinkNodes(), t.Common.ChainName, t.Common.ChainId) -//require.NoError(testState, err) -//for _, n := range t.Cc.ChainlinkNodes { -//_, _, err = n.CreateStarkNetChain(&client.StarkNetChainAttributes{ -//Type: t.Common.ChainName, -//ChainID: t.Common.ChainId, -//Config: client.StarkNetChainConfig{}, -//}) -//require.NoError(testState, err) -//_, _, err = n.CreateStarkNetNode(&client.StarkNetNodeAttributes{ -//Name: t.Common.ChainName, -//ChainID: t.Common.ChainId, -//Url: L2RpcUrl, -//}) -//require.NoError(testState, err) -//} -//t.Common.Testnet = true -//t.Common.L2RPCUrl = L2RpcUrl -//t.Sg, err = gauntlet.NewStarknetGauntlet(fmt.Sprintf("%s/", utils.ProjectRoot)) -//require.NoError(testState, err, "Could not get a new gauntlet struct") -//err = t.Sg.SetupNetwork(t.Common.L2RPCUrl) -//require.NoError(testState, err, "Setting up gauntlet network should not fail") -//err = t.DeployGauntlet(0, 100000000000, 9, "auto", 1, 1) -//require.NoError(testState, err, "Deploying contracts should not fail") -//t.SetBridgeTypeAttrs(&client.BridgeTypeAttributes{ -//Name: "bridge-coinmetrics", -//URL: "ADAPTER_URL", // ADAPTER_URL e.g https://adapters.main.sand.cldev.sh/coinmetrics -//}) - -//err = t.Common.CreateJobsForContract(t.Cc, observationSource, juelsPerFeeCoinSource, t.OCRAddr, t.AccountAddresses) -//require.NoError(testState, err) -//} diff --git a/integration-tests/scripts/entrypoint b/integration-tests/scripts/entrypoint index 94ea19cc2..462aaaf2f 100755 --- a/integration-tests/scripts/entrypoint +++ b/integration-tests/scripts/entrypoint @@ -14,4 +14,5 @@ cd "$SCRIPT_DIR"/../ || exit 1 # SUITE=${SUITE:=} the suite of tests you want to run # TEST_NAME=${TEST_NAME:=} The specific test to run # run the tests +nix develop -c helm repo update nix develop -c ./"${SUITE}".test -test.v -test.count 1 ${ARGS} -test.run ^${TEST_NAME}$ diff --git a/integration-tests/smoke/ocr2_test.go b/integration-tests/smoke/ocr2_test.go index 9a25fbc5f..c9f0213fa 100644 --- a/integration-tests/smoke/ocr2_test.go +++ b/integration-tests/smoke/ocr2_test.go @@ -3,59 +3,83 @@ package smoke_test import ( "flag" "fmt" - "testing" - - "github.com/stretchr/testify/require" - "github.com/smartcontractkit/chainlink-starknet/integration-tests/common" + tc "github.com/smartcontractkit/chainlink-starknet/integration-tests/testconfig" "github.com/smartcontractkit/chainlink-starknet/ops/gauntlet" "github.com/smartcontractkit/chainlink-starknet/ops/utils" + "github.com/smartcontractkit/chainlink-testing-framework/logging" + "github.com/smartcontractkit/chainlink/integration-tests/actions" + "github.com/smartcontractkit/chainlink/integration-tests/docker/test_env" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zapcore" + "maps" + "os" + "testing" ) var ( keepAlive bool + decimals = 9 ) func init() { flag.BoolVar(&keepAlive, "keep-alive", false, "enable to keep the cluster alive") } -var ( - err error - testState *common.Test - decimals = 9 -) - func TestOCRBasic(t *testing.T) { - testState = &common.Test{ - T: t, - } - testState.Common = common.New(t) - // Setting this to the root of the repo for cmd exec func for Gauntlet - testState.Sg, err = gauntlet.NewStarknetGauntlet(fmt.Sprintf("%s/", utils.ProjectRoot)) - require.NoError(t, err, "Could not get a new gauntlet struct") + for _, test := range []struct { + name string + env map[string]string + }{ + {name: "embedded"}, + {name: "plugins", 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) + } + err = os.Setenv("CHAINLINK_ENV_USER", *config.Common.User) + require.NoError(t, err, "Could not set CHAINLINK_ENV_USER") + err = os.Setenv("INTERNAL_DOCKER_REPO", *config.Common.InternalDockerRepo) + require.NoError(t, err, "Could not set INTERNAL_DOCKER_REPO") + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + logging.Init() + state, err := common.NewOCRv2State(t, "smoke-ocr2", &config) + require.NoError(t, err, "Could not setup the ocrv2 state") - testState.DeployCluster() - require.NoError(t, err, "Deploying cluster should not fail") - // if testState.Common.Env.WillUseRemoteRunner() { - // return // short circuit here if using a remote runner - // } - err = testState.Sg.SetupNetwork(testState.Common.L2RPCUrl) - require.NoError(t, err, "Setting up gauntlet network should not fail") - err = testState.DeployGauntlet(0, 100000000000, decimals, "auto", 1, 1) - require.NoError(t, err, "Deploying contracts should not fail") - if !testState.Common.Testnet { - testState.Devnet.AutoLoadState(testState.OCR2Client, testState.OCRAddr) - } - testState.SetUpNodes() + // K8s specific config and cleanup + if *config.Common.InsideK8s { + t.Cleanup(func() { + if err = actions.TeardownSuite(t, state.Common.Env, state.ChainlinkNodesK8s, nil, zapcore.PanicLevel, nil); err != nil { + state.TestConfig.L.Error().Err(err).Msg("Error tearing down environment") + } + }) + } + 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) + }) + } + state.DeployCluster() + state.Clients.GauntletClient, err = gauntlet.NewStarknetGauntlet(fmt.Sprintf("%s/", utils.ProjectRoot)) + require.NoError(t, err, "Setting up gauntlet should not fail") + err = state.Clients.GauntletClient.SetupNetwork(state.Common.RPCDetails.RPCL2External, state.Account.Account, state.Account.PrivateKey) + require.NoError(t, err, "Setting up gauntlet network should not fail") + err = state.DeployGauntlet(0, 100000000000, decimals, "auto", 1, 1) + require.NoError(t, err, "Deploying contracts should not fail") - err = testState.ValidateRounds(10, false) - require.NoError(t, err, "Validating round should not fail") + state.SetUpNodes() - t.Cleanup(func() { - testState.Common.TearDownLocalEnvironment(t) - // TODO: - // err = actions.TeardownSuite(t, testState.Common.Env, testState.Cc.ChainlinkNodes, nil, zapcore.ErrorLevel, nil, nil) - // require.NoError(t, err, "Error tearing down environment") - }) + err = state.ValidateRounds(*config.OCR2.Smoke.NumberOfRounds, false) + require.NoError(t, err, "Validating round should not fail") + }) + } } diff --git a/integration-tests/test.Dockerfile b/integration-tests/test.Dockerfile index aaf35e838..1c87a720e 100644 --- a/integration-tests/test.Dockerfile +++ b/integration-tests/test.Dockerfile @@ -6,5 +6,6 @@ ENV PATH="/repo/cairo-build/bin:/repo/scarb-build/bin:${PATH}" COPY . /repo/ WORKDIR /repo +RUN nix develop -c helm repo update RUN nix develop -c /repo/integration-tests/scripts/buildTests "${SUITES}" ENTRYPOINT ["/repo/integration-tests/scripts/entrypoint"] diff --git a/integration-tests/testconfig/configs_embed.go b/integration-tests/testconfig/configs_embed.go new file mode 100644 index 000000000..e9decaa4c --- /dev/null +++ b/integration-tests/testconfig/configs_embed.go @@ -0,0 +1,14 @@ +//go:build embed +// +build embed + +package testconfig + +import "embed" + +//go:embed default.toml +//go:embed ocr2/ocr2.toml +var embeddedConfigsFs embed.FS + +func init() { + areConfigsEmbedded = true +} diff --git a/integration-tests/testconfig/configs_noembed.go b/integration-tests/testconfig/configs_noembed.go new file mode 100644 index 000000000..95572c4a0 --- /dev/null +++ b/integration-tests/testconfig/configs_noembed.go @@ -0,0 +1,12 @@ +//go:build !embed +// +build !embed + +package testconfig + +import "embed" + +var embeddedConfigsFs embed.FS + +func init() { + areConfigsEmbedded = false +} diff --git a/integration-tests/testconfig/default.toml b/integration-tests/testconfig/default.toml new file mode 100644 index 000000000..c9883cc37 --- /dev/null +++ b/integration-tests/testconfig/default.toml @@ -0,0 +1,36 @@ +# This is the default configuration so OCR2 tests can run without issues +[ChainlinkImage] +image="public.ecr.aws/chainlink/chainlink" +version="2.9.0" + +[Logging] +test_log_collect=false + +[Logging.LogStream] +log_targets=["file"] +log_producer_timeout="10s" +log_producer_retry_limit=10 + +[Network] +selected_networks=["SIMULATED"] # Not needed for Starknet but mandatory from CTF (do not change) + +[Network.RpcHttpUrls] +simulated = ["http://127.0.0.1"] # Not needed for Starknet but mandatory from CTF (do not change) + +[Network.RpcWsUrls] +simulated = ["wss://127.0.0.1"] # Not needed for Starknet but mandatory from CTF (do not change) + +[Common] +internal_docker_repo = "public.ecr.aws/chainlink" +inside_k8 = false +network = "localnet" +user = "satoshi" +stateful_db = false +devnet_image = "shardlabs/starknet-devnet-rs:b41e566a3f17aa0e51871f02d5165959e50ce358" + +[OCR2] +node_count = 6 +test_duration = "30m" + +[OCR2.Smoke] +number_of_rounds = 10 diff --git a/integration-tests/testconfig/ocr2/ocr2.go b/integration-tests/testconfig/ocr2/ocr2.go new file mode 100644 index 000000000..7555fb6fa --- /dev/null +++ b/integration-tests/testconfig/ocr2/ocr2.go @@ -0,0 +1,42 @@ +package ocr2 + +import ( + "errors" +) + +type Config struct { + Smoke *SmokeConfig `toml:"Smoke"` + NodeCount *int `toml:"node_count"` + TestDuration *string `toml:"test_duration"` +} + +func (o *Config) Validate() error { + if o.NodeCount != nil && *o.NodeCount < 3 { + return errors.New("node_count must be set and cannot be less than 3") + } + + if o.TestDuration == nil { + return errors.New("test_duration must be set") + } + + if o.Smoke == nil { + return errors.New("smoke must be defined") + } + err := o.Smoke.Validate() + if err != nil { + return err + } + + return nil +} + +type SmokeConfig struct { + NumberOfRounds *int `toml:"number_of_rounds"` +} + +func (o *SmokeConfig) Validate() error { + if o.NumberOfRounds == nil { + return errors.New("number_of_rounds must be set") + } + return nil +} diff --git a/integration-tests/testconfig/ocr2/ocr2.toml b/integration-tests/testconfig/ocr2/ocr2.toml new file mode 100644 index 000000000..9d1493a89 --- /dev/null +++ b/integration-tests/testconfig/ocr2/ocr2.toml @@ -0,0 +1,3 @@ +[Common] +node_count = 6 +test_duration = "30m" \ No newline at end of file diff --git a/integration-tests/testconfig/testconfig.go b/integration-tests/testconfig/testconfig.go new file mode 100644 index 000000000..c47684018 --- /dev/null +++ b/integration-tests/testconfig/testconfig.go @@ -0,0 +1,351 @@ +package testconfig + +import ( + "embed" + "encoding/base64" + "fmt" + "github.com/smartcontractkit/chainlink-testing-framework/docker/test_env" + "os" + "strings" + + "github.com/barkimedes/go-deepcopy" + "github.com/google/uuid" + "github.com/pelletier/go-toml/v2" + "github.com/pkg/errors" + "github.com/rs/zerolog" + "golang.org/x/text/cases" + "golang.org/x/text/language" + + ocr2_config "github.com/smartcontractkit/chainlink-starknet/integration-tests/testconfig/ocr2" + ctf_config "github.com/smartcontractkit/chainlink-testing-framework/config" + k8s_config "github.com/smartcontractkit/chainlink-testing-framework/k8s/config" + "github.com/smartcontractkit/chainlink-testing-framework/logging" + "github.com/smartcontractkit/chainlink-testing-framework/utils/osutil" +) + +type TestConfig struct { + ChainlinkImage *ctf_config.ChainlinkImageConfig `toml:"ChainlinkImage"` + Logging *ctf_config.LoggingConfig `toml:"Logging"` + ChainlinkUpgradeImage *ctf_config.ChainlinkImageConfig `toml:"ChainlinkUpgradeImage"` + Network *ctf_config.NetworkConfig `toml:"Network"` + Common *Common `toml:"Common"` + OCR2 *ocr2_config.Config `toml:"OCR2"` + ConfigurationName string `toml:"-"` +} + +func (c *TestConfig) GetLoggingConfig() *ctf_config.LoggingConfig { + return c.Logging +} + +func (c *TestConfig) GetPrivateEthereumNetworkConfig() *test_env.EthereumNetwork { + return &test_env.EthereumNetwork{} +} + +func (c *TestConfig) GetPyroscopeConfig() *ctf_config.PyroscopeConfig { + return &ctf_config.PyroscopeConfig{} +} + +var embeddedConfigs embed.FS +var areConfigsEmbedded bool + +func init() { + embeddedConfigs = embeddedConfigsFs +} + +// Saves Test Config to a local file +func (c *TestConfig) Save() (string, error) { + filePath := fmt.Sprintf("test_config-%s.toml", uuid.New()) + + content, err := toml.Marshal(*c) + if err != nil { + return "", errors.Wrapf(err, "error marshaling test config") + } + + err = os.WriteFile(filePath, content, 0600) + if err != nil { + return "", errors.Wrapf(err, "error writing test config") + } + + return filePath, nil +} + +// MustCopy Returns a deep copy of the Test Config or panics on error +func (c TestConfig) MustCopy() any { + return deepcopy.MustAnything(c).(TestConfig) +} + +// MustCopy Returns a deep copy of struct passed to it and returns a typed copy (or panics on error) +func MustCopy[T any](c T) T { + return deepcopy.MustAnything(c).(T) +} + +func (c TestConfig) GetNetworkConfig() *ctf_config.NetworkConfig { + return c.Network +} + +func (c TestConfig) GetChainlinkImageConfig() *ctf_config.ChainlinkImageConfig { + return c.ChainlinkImage +} + +func (c TestConfig) GetCommonConfig() *Common { + return c.Common +} + +func (c TestConfig) GetChainlinkUpgradeImageConfig() *ctf_config.ChainlinkImageConfig { + return c.ChainlinkUpgradeImage +} + +func (c TestConfig) GetConfigurationName() string { + return c.ConfigurationName +} + +func (c *TestConfig) AsBase64() (string, error) { + content, err := toml.Marshal(*c) + if err != nil { + return "", errors.Wrapf(err, "error marshaling test config") + } + + return base64.StdEncoding.EncodeToString(content), nil +} + +type Common struct { + Network *string `toml:"network"` + InsideK8s *bool `toml:"inside_k8"` + User *string `toml:"user"` + L2RPCUrl *string `toml:"l2_rpc_url"` + PrivateKey *string `toml:"private_key"` + Account *string `toml:"account"` + Stateful *bool `toml:"stateful_db"` + InternalDockerRepo *string `toml:"internal_docker_repo"` + DevnetImage *string `toml:"devnet_image"` +} + +func (c *Common) Validate() error { + if c.Network == nil { + return fmt.Errorf("network must be set") + } + + switch *c.Network { + case "localnet": + if c.DevnetImage == nil { + return fmt.Errorf("devnet_image must be set") + } + case "testnet": + if c.PrivateKey == nil { + return fmt.Errorf("private_key must be set") + } + if c.L2RPCUrl == nil { + return fmt.Errorf("l2_rpc_url must be set") + } + + if c.Account == nil { + return fmt.Errorf("account must be set") + } + default: + return fmt.Errorf("network must be either 'localnet' or 'testnet'") + } + + if c.InsideK8s == nil { + return fmt.Errorf("inside_k8 must be set") + } + + if c.InternalDockerRepo == nil { + return fmt.Errorf("internal_docker_repo must be set") + } + + if c.User == nil { + return fmt.Errorf("user must be set") + } + + if c.Stateful == nil { + return fmt.Errorf("stateful_db state for db must be set") + } + + return nil +} + +type Product string + +const ( + OCR2 Product = "ocr2" +) + +const TestTypeEnvVarName = "TEST_TYPE" + +const ( + Base64OverrideEnvVarName = k8s_config.EnvBase64ConfigOverride + NoKey = "NO_KEY" +) + +func GetConfig(configurationName string, product Product) (TestConfig, error) { + logger := logging.GetTestLogger(nil) + + configurationName = strings.ReplaceAll(configurationName, "/", "_") + configurationName = strings.ReplaceAll(configurationName, " ", "_") + configurationName = cases.Title(language.English, cases.NoLower).String(configurationName) + fileNames := []string{ + "default.toml", + fmt.Sprintf("%s.toml", product), + "overrides.toml", + } + + testConfig := TestConfig{} + testConfig.ConfigurationName = configurationName + logger.Debug().Msgf("Will apply configuration named '%s' if it is found in any of the configs", configurationName) + + var handleSpecialOverrides = func(logger zerolog.Logger, filename, configurationName string, target *TestConfig, content []byte, product Product) error { + switch product { + default: + err := ctf_config.BytesToAnyTomlStruct(logger, filename, configurationName, &testConfig, content) + if err != nil { + return errors.Wrapf(err, "error reading file %s", filename) + } + + return nil + } + } + + // read embedded configs is build tag "embed" is set + // this makes our life much easier when using a binary + if areConfigsEmbedded { + logger.Info().Msg("Reading embedded configs") + embeddedFiles := []string{"default.toml", fmt.Sprintf("%s/%s.toml", product, product)} + for _, fileName := range embeddedFiles { + file, err := embeddedConfigs.ReadFile(fileName) + if err != nil && errors.Is(err, os.ErrNotExist) { + logger.Debug().Msgf("Embedded config file %s not found. Continuing", fileName) + continue + } else if err != nil { + return TestConfig{}, errors.Wrapf(err, "error reading embedded config") + } + + err = handleSpecialOverrides(logger, fileName, configurationName, &testConfig, file, product) + if err != nil { + return TestConfig{}, errors.Wrapf(err, "error unmarshalling embedded config") + } + } + } + + logger.Info().Msg("Reading configs from file system") + for _, fileName := range fileNames { + logger.Debug().Msgf("Looking for config file %s", fileName) + filePath, err := osutil.FindFile(fileName, osutil.DEFAULT_STOP_FILE_NAME, 3) + + if err != nil && errors.Is(err, os.ErrNotExist) { + logger.Debug().Msgf("Config file %s not found", fileName) + continue + } else if err != nil { + return TestConfig{}, errors.Wrapf(err, "error looking for file %s", filePath) + } + logger.Debug().Str("location", filePath).Msgf("Found config file %s", fileName) + + content, err := readFile(filePath) + if err != nil { + return TestConfig{}, errors.Wrapf(err, "error reading file %s", filePath) + } + + err = handleSpecialOverrides(logger, fileName, configurationName, &testConfig, content, product) + if err != nil { + return TestConfig{}, errors.Wrapf(err, "error reading file %s", filePath) + } + } + + logger.Info().Msg("Reading configs from Base64 override env var") + configEncoded, isSet := os.LookupEnv(Base64OverrideEnvVarName) + if isSet && configEncoded != "" { + logger.Debug().Msgf("Found base64 config override environment variable '%s' found", Base64OverrideEnvVarName) + decoded, err := base64.StdEncoding.DecodeString(configEncoded) + if err != nil { + return TestConfig{}, err + } + + err = handleSpecialOverrides(logger, Base64OverrideEnvVarName, configurationName, &testConfig, decoded, product) + if err != nil { + return TestConfig{}, errors.Wrapf(err, "error unmarshaling base64 config") + } + } else { + logger.Debug().Msg("Base64 config override from environment variable not found") + } + + // it neede some custom logic, so we do it separately + err := testConfig.readNetworkConfiguration() + if err != nil { + return TestConfig{}, errors.Wrapf(err, "error reading network config") + } + + logger.Debug().Msg("Validating test config") + err = testConfig.Validate() + if err != nil { + return TestConfig{}, errors.Wrapf(err, "error validating test config") + } + + if testConfig.Common == nil { + testConfig.Common = &Common{} + } + + logger.Debug().Msg("Correct test config constructed successfully") + return testConfig, nil +} + +func (c *TestConfig) readNetworkConfiguration() error { + // currently we need to read that kind of secrets only for network configuration + if c == nil { + c.Network = &ctf_config.NetworkConfig{} + } + + c.Network.UpperCaseNetworkNames() + err := c.Network.Default() + if err != nil { + return errors.Wrapf(err, "error reading default network config") + } + + return nil +} + +func (c *TestConfig) Validate() error { + defer func() { + if r := recover(); r != nil { + panic(fmt.Errorf("Panic during test config validation: '%v'. Most probably due to presence of partial product config", r)) + } + }() + if c.ChainlinkImage == nil { + return fmt.Errorf("chainlink image config must be set") + } + if err := c.ChainlinkImage.Validate(); err != nil { + return errors.Wrapf(err, "chainlink image config validation failed") + } + if c.ChainlinkUpgradeImage != nil { + if err := c.ChainlinkUpgradeImage.Validate(); err != nil { + return errors.Wrapf(err, "chainlink upgrade image config validation failed") + } + } + if err := c.Network.Validate(); err != nil { + return errors.Wrapf(err, "network config validation failed") + } + + if c.Common == nil { + return fmt.Errorf("common config must be set") + } + + if err := c.Common.Validate(); err != nil { + return errors.Wrapf(err, "Common config validation failed") + } + + if c.OCR2 == nil { + return fmt.Errorf("OCR2 config must be set") + } + + if err := c.OCR2.Validate(); err != nil { + return errors.Wrapf(err, "OCR2 config validation failed") + } + return nil +} + +func readFile(filePath string) ([]byte, error) { + content, err := os.ReadFile(filePath) + if err != nil { + return nil, errors.Wrapf(err, "error reading file %s", filePath) + } + + return content, nil +} diff --git a/ops/devnet/devnet.go b/ops/devnet/devnet.go deleted file mode 100644 index 8c54c83a7..000000000 --- a/ops/devnet/devnet.go +++ /dev/null @@ -1,137 +0,0 @@ -package devnet - -import ( - "context" - "fmt" - "strings" - "time" - - starknetutils "github.com/NethermindEth/starknet.go/utils" - "github.com/smartcontractkit/chainlink-starknet/relayer/pkg/chainlink/ocr2" - - "github.com/go-resty/resty/v2" - "github.com/rs/zerolog/log" -) - -type StarknetDevnetClient struct { - ctx context.Context - cancel context.CancelFunc - client *resty.Client - dumpPath string -} - -func (devnet *StarknetDevnetClient) NewStarknetDevnetClient(rpcUrl string, dumpPath string) *StarknetDevnetClient { - ctx, cancel := context.WithCancel(context.Background()) - return &StarknetDevnetClient{ - ctx: ctx, - cancel: cancel, - client: resty.New().SetBaseURL(rpcUrl), - dumpPath: dumpPath, - } -} - -// AutoSyncL1 auto calls /flush/ every 2 seconds to sync L1<>L2 -func (devnet *StarknetDevnetClient) AutoSyncL1() { - t := time.NewTicker(2 * time.Second) - go func() { - for { - select { - case <-devnet.ctx.Done(): - log.Debug().Msg("Shutting down L1 sync") - return - case <-t.C: - log.Debug().Msg("Syncing L1") - _, err := devnet.client.R().Post("/postman/flush") - if err != nil { - log.Error().Err(err).Msg("failed to sync L1") - } - } - } - }() -} - -// AutoDumpState dumps devnet state every 10 sec -func (devnet *StarknetDevnetClient) AutoDumpState() { - t := time.NewTicker(20 * time.Minute) - go func() { - for { - select { - case <-devnet.ctx.Done(): - log.Debug().Msg("Shutting down devnet dump") - return - case <-t.C: - log.Debug().Msg("Dumping state") - _, err := devnet.client.R().SetBody(map[string]any{ - "path": devnet.dumpPath, - }).Post("/dump") - if err != nil { - log.Error().Err(err).Msg("Failed to dump devnet state") - } - } - } - }() -} - -// AutoLoadState auto loads last saved devnet state on contract not found -func (devnet *StarknetDevnetClient) AutoLoadState(client *ocr2.Client, ocrAddress string) { - addr, _ := starknetutils.HexToFelt(ocrAddress) - t := time.NewTicker(15 * time.Second) - go func() { - for { - select { - case <-devnet.ctx.Done(): - log.Debug().Msg("Shutting down devnet dump") - return - case <-t.C: - log.Debug().Msg("Checking for devnet OCR contract errors") - - _, err := client.LatestTransmissionDetails(devnet.ctx, addr) - if err != nil && strings.Contains(err.Error(), "is not deployed") { - _, err = devnet.client.R().SetBody(map[string]any{ - "path": devnet.dumpPath, - }).Post("/load") - if err != nil { - log.Error().Err(err).Msg("Failed to dump devnet state") - } - } - - } - } - }() -} - -// FundAccounts Funds provided accounts with 100 eth each -func (devnet *StarknetDevnetClient) FundAccounts(l2AccList []string) error { - for _, key := range l2AccList { - res, err := devnet.client.R().SetBody(map[string]any{ - "address": key, - "amount": 900000000000000000, - }).Post("/mint") - if err != nil { - return err - } - log.Info().Msg(fmt.Sprintf("Funding account (WEI): %s", string(res.Body()))) - res, err = devnet.client.R().SetBody(map[string]any{ - "address": key, - "amount": 900000000000000000, - "unit": "FRI", - }).Post("/mint") - if err != nil { - return err - } - log.Info().Msg(fmt.Sprintf("Funding account (FRI): %s", string(res.Body()))) - } - return nil -} - -// LoadL1MessagingContract loads and sets up the L1 messaging contract and URL -func (devnet *StarknetDevnetClient) LoadL1MessagingContract(l1RpcUrl string) error { - resp, err := devnet.client.R().SetBody(map[string]any{ - "networkUrl": l1RpcUrl, - }).Post("/postman/load_l1_messaging_contract") - if err != nil { - return err - } - log.Warn().Interface("Response", resp.String()).Msg("Set up L1 messaging contract") - return nil -} diff --git a/ops/devnet/environment.go b/ops/devnet/environment.go index c1ef5aa6c..990636a6c 100644 --- a/ops/devnet/environment.go +++ b/ops/devnet/environment.go @@ -73,7 +73,7 @@ func defaultProps() map[string]any { "starknet-dev": map[string]any{ "image": map[string]any{ "image": "shardlabs/starknet-devnet-rs", - "version": "latest", + "version": "b41e566a3f17aa0e51871f02d5165959e50ce358", }, "resources": map[string]any{ "requests": map[string]any{ diff --git a/ops/gauntlet/gauntlet_starknet.go b/ops/gauntlet/gauntlet_starknet.go index 50bb8de5e..2b11779ce 100644 --- a/ops/gauntlet/gauntlet_starknet.go +++ b/ops/gauntlet/gauntlet_starknet.go @@ -72,8 +72,10 @@ func (sg *StarknetGauntlet) FetchGauntletJsonOutput() (*GauntletResponse, error) } // SetupNetwork Sets up a new network and sets the NODE_URL for Devnet / Starknet RPC -func (sg *StarknetGauntlet) SetupNetwork(addr string) error { +func (sg *StarknetGauntlet) SetupNetwork(addr string, account string, privateKey string) error { sg.G.AddNetworkConfigVar("NODE_URL", addr) + sg.G.AddNetworkConfigVar("ACCOUNT", account) + sg.G.AddNetworkConfigVar("PRIVATE_KEY", privateKey) err := sg.G.WriteNetworkConfigMap(sg.dir + "packages-ts/starknet-gauntlet-cli/networks/") if err != nil { return err diff --git a/packages-ts/starknet-gauntlet-cli/networks/.env.testnet b/packages-ts/starknet-gauntlet-cli/networks/.env.testnet index 1011ef7a1..0ca97eb16 100644 --- a/packages-ts/starknet-gauntlet-cli/networks/.env.testnet +++ b/packages-ts/starknet-gauntlet-cli/networks/.env.testnet @@ -1 +1 @@ -NODE_URL=https://starknet-testnet.public.blastapi.io +NODE_URL=https://starknet-sepolia.public.blastapi.io/rpc/v0_6 \ No newline at end of file diff --git a/relayer/pkg/chainlink/txm/txm.go b/relayer/pkg/chainlink/txm/txm.go index e5e84b9b9..54f68433d 100644 --- a/relayer/pkg/chainlink/txm/txm.go +++ b/relayer/pkg/chainlink/txm/txm.go @@ -19,6 +19,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/services" "github.com/smartcontractkit/chainlink-common/pkg/utils" + ethrpc "github.com/ethereum/go-ethereum/rpc" "github.com/smartcontractkit/chainlink-starknet/relayer/pkg/starknet" ) @@ -198,7 +199,13 @@ func (txm *starktxm) broadcast(ctx context.Context, publicKey *felt.Felt, accoun simFlags := []starknetrpc.SimulationFlag{} feeEstimate, err := account.EstimateFee(ctx, []starknetrpc.BroadcastTxn{tx}, simFlags, starknetrpc.BlockID{Tag: "latest"}) if err != nil { - return txhash, fmt.Errorf("failed to estimate fee: %+w", err) + var data any + var dataErr ethrpc.DataError + if errors.As(err, &dataErr) { + data = dataErr.ErrorData() + } + txm.lggr.Errorw("failed to estimate fee", "error", err, "data", data) + return txhash, fmt.Errorf("failed to estimate fee: %T %+w", err, err) } txm.lggr.Infow("Account", "account", account.AccountAddress) @@ -219,12 +226,12 @@ func (txm *starktxm) broadcast(ctx context.Context, publicKey *felt.Felt, accoun gasConsumed := friEstimate.GasConsumed.BigInt(new(big.Int)) expandedGas := new(big.Int).Mul(gasConsumed, big.NewInt(140)) maxGas := new(big.Int).Div(expandedGas, big.NewInt(100)) - tx.ResourceBounds.L2Gas.MaxAmount = starknetrpc.U64(starknetutils.BigIntToFelt(maxGas).String()) + tx.ResourceBounds.L1Gas.MaxAmount = starknetrpc.U64(starknetutils.BigIntToFelt(maxGas).String()) // TODO: add margin - tx.ResourceBounds.L2Gas.MaxPricePerUnit = starknetrpc.U128(friEstimate.GasPrice.String()) + tx.ResourceBounds.L1Gas.MaxPricePerUnit = starknetrpc.U128(friEstimate.GasPrice.String()) - txm.lggr.Infow("Set resource bounds", "L2MaxAmount", tx.ResourceBounds.L2Gas.MaxAmount, "L2MaxPricePerUnit", tx.ResourceBounds.L2Gas.MaxPricePerUnit) + txm.lggr.Infow("Set resource bounds", "L1MaxAmount", tx.ResourceBounds.L1Gas.MaxAmount, "L1MaxPricePerUnit", tx.ResourceBounds.L1Gas.MaxPricePerUnit) // Re-sign transaction now that we've determined MaxFee // TODO: SignInvokeTransaction for V3 is missing so we do it by hand @@ -245,6 +252,12 @@ func (txm *starktxm) broadcast(ctx context.Context, publicKey *felt.Felt, accoun res, err := account.AddInvokeTransaction(execCtx, tx) if err != nil { // TODO: handle initial broadcast errors - what kind of errors occur? + var data any + var dataErr ethrpc.DataError + if errors.As(err, &dataErr) { + data = dataErr.ErrorData() + } + txm.lggr.Errorw("failed to invoke tx", "error", err, "data", data) return txhash, fmt.Errorf("failed to invoke tx: %+w", err) } // handle nil pointer