diff --git a/.github/workflows/integration-tests-smoke.yml b/.github/workflows/integration-tests-smoke.yml index 6d13980d1..a77ff8f1a 100644 --- a/.github/workflows/integration-tests-smoke.yml +++ b/.github/workflows/integration-tests-smoke.yml @@ -23,6 +23,7 @@ env: 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 }} + GAUNTLET_PP_IMAGE: ${{ secrets.PROD_AWS_ACCOUNT_NUMBER }}.dkr.ecr.${{ secrets.PROD_AWS_REGION }}.amazonaws.com/gauntlet-plus-plus:v2.5.0 jobs: build_chainlink_image: @@ -78,7 +79,7 @@ jobs: echo "CUSTOM_CORE_REF=${{ github.event.inputs.cl_branch_ref }}" >> "${GITHUB_ENV}" - name: Build Image ${{ matrix.image.name }} if: steps.check-image.outputs.exists == 'false' - uses: smartcontractkit/chainlink-github-actions/chainlink-testing-framework/build-image@fc3e0df622521019f50d772726d6bf8dc919dd38 # v2.3.19 + uses: smartcontractkit/.github/actions/ctf-build-image@c4705bfdbf6c8e57c080d82a3c4f013aa96a2dfb # v2.3.19 with: cl_repo: smartcontractkit/chainlink cl_ref: ${{ env.CUSTOM_CORE_REF }} @@ -203,7 +204,7 @@ jobs: # shellcheck disable=SC2086 echo "BASE64_CONFIG_OVERRIDE=$BASE64_CONFIG_OVERRIDE" >> $GITHUB_ENV - name: Run Tests ${{ matrix.image.name }} - uses: smartcontractkit/.github/actions/ctf-run-tests@002596f65dc8eb807f5c8729dc1080921f7d0b24 # 0.2.1 + uses: smartcontractkit/.github/actions/ctf-run-tests@4229fea727f6eb36b4559c6eefbbf9f3825fa677 # 0.2.1 with: aws_registries: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }} test_command_to_run: nix develop -c sh -c "make test=${{ matrix.image.test-name }} test-integration-smoke-ci" @@ -215,7 +216,11 @@ jobs: QA_AWS_REGION: ${{ secrets.QA_AWS_REGION }} QA_AWS_ROLE_TO_ASSUME: ${{ secrets.QA_AWS_ROLE_TO_ASSUME }} QA_KUBECONFIG: ${{ secrets.QA_KUBECONFIG }} + PROD_AWS_ROLE_TO_ASSUME: ${{ secrets.AWS_OIDC_IAM_PROD_INTEGRATION_ROLE_ARN }} artifacts_location: /home/runner/work/chainlink-starknet/chainlink-starknet/integration-tests/smoke/logs + PROD_AWS_REGION: ${{ secrets.PROD_AWS_REGION}} + PROD_AWS_ACCOUNT_NUMBER: ${{ secrets.PROD_AWS_ACCOUNT_NUMBER }} + gauntlet_plus_plus_image: ${{ secrets.PROD_AWS_ACCOUNT_NUMBER }}.dkr.ecr.${{ secrets.PROD_AWS_REGION }}.amazonaws.com/gauntlet-plus-plus:v2.5.0 gati_token: ${{ steps.setup-github-token.outputs.access-token }} env: KILLGRAVE_INTERNAL_IMAGE: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }}.dkr.ecr.${{ secrets.QA_AWS_REGION }}.amazonaws.com/friendsofgo/killgrave diff --git a/.github/workflows/integration-tests-soak.yml b/.github/workflows/integration-tests-soak.yml index 0af296760..24c95743c 100644 --- a/.github/workflows/integration-tests-soak.yml +++ b/.github/workflows/integration-tests-soak.yml @@ -59,6 +59,13 @@ jobs: nix_path: nixpkgs=channel:nixos-unstable - name: Install Cairo uses: ./.github/actions/install-cairo + - name: Setup GitHub Token + id: setup-github-token + uses: smartcontractkit/.github/actions/setup-github-token@9e7cc0779934cae4a9028b8588c9adb64d8ce68c # setup-github-token@0.1.2 + with: + aws-role-arn: ${{ secrets.AWS_OIDC_GLOBAL_READ_ONLY_TOKEN_ISSUER_ROLE_ARN }} + aws-lambda-url: ${{ secrets.GATI_RELENG_LAMBDA_URL }} + aws-region: ${{ secrets.QA_AWS_REGION }} - name: Build contracts run: | cd contracts && scarb --profile release build @@ -72,7 +79,7 @@ jobs: echo "::add-mask::$BASE64_CONFIG_OVERRIDE" echo "BASE64_CONFIG_OVERRIDE=$BASE64_CONFIG_OVERRIDE" >> "$GITHUB_ENV" - name: Run Tests - uses: smartcontractkit/.github/actions/ctf-run-tests@002596f65dc8eb807f5c8729dc1080921f7d0b24 # 0.2.1 + uses: smartcontractkit/.github/actions/ctf-run-tests@4229fea727f6eb36b4559c6eefbbf9f3825fa677 # 0.2.1 with: aws_registries: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }} test_command_to_run: cd ./integration-tests && go test -timeout 24h -count=1 -run TestOCRBasicSoak/embedded ./soak @@ -84,9 +91,12 @@ jobs: 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/soak/logs + PROD_AWS_REGION: ${{ secrets.PROD_AWS_REGION}} + PROD_AWS_ACCOUNT_NUMBER: ${{ secrets.PROD_AWS_ACCOUNT_NUMBER }} + gauntlet_plus_plus_image: ${{ secrets.PROD_AWS_ACCOUNT_NUMBER }}.dkr.ecr.${{ secrets.PROD_AWS_REGION }}.amazonaws.com/gauntlet-plus-plus:v2.5.0 + gati_token: ${{ steps.setup-github-token.outputs.access-token }} env: KILLGRAVE_INTERNAL_IMAGE: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }}.dkr.ecr.${{ secrets.QA_AWS_REGION }}.amazonaws.com/friendsofgo/killgrave CHAINLINK_IMAGE: ${{ env.CL_ECR }} CHAINLINK_VERSION: starknet.${{ github.sha }}${{ matrix.image.tag-suffix }} CHAINLINK_USER_TEAM: ${{ github.event.inputs.team || 'BIX' }} - diff --git a/integration-tests/common/gauntlet_common.go b/integration-tests/common/gauntlet_common.go deleted file mode 100644 index 173b64dc5..000000000 --- a/integration-tests/common/gauntlet_common.go +++ /dev/null @@ -1,147 +0,0 @@ -package common - -import ( - "encoding/json" - "errors" - "fmt" - "os" - - "github.com/smartcontractkit/chainlink-starknet/integration-tests/utils" -) - -func (m *OCRv2TestState) fundNodes() ([]string, error) { - l := utils.GetTestLogger(m.TestConfig.T) - var nAccounts []string - for _, key := range m.GetNodeKeys() { - if key.TXKey.Data.Attributes.StarkKey == "" { - return nil, errors.New("stark key can't be empty") - } - nAccount, err := m.Clients.GauntletClient.DeployAccountContract(100, key.TXKey.Data.Attributes.StarkKey) - if err != nil { - return nil, err - } - nAccounts = append(nAccounts, nAccount) - } - - 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 := m.Clients.GauntletClient.TransferToken(m.Common.ChainDetails.StarkTokenAddress, key, "10000000000000000000") // Transferring 10 STRK to each node - if err != nil { - return nil, err - } - } - } else { - // 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 (m *OCRv2TestState) deployLinkToken() error { - var err error - m.Contracts.LinkTokenAddr, err = m.Clients.GauntletClient.DeployLinkTokenContract() - if err != nil { - return err - } - err = os.Setenv("LINK", m.Contracts.LinkTokenAddr) - if err != nil { - return err - } - return nil -} - -func (m *OCRv2TestState) deployAccessController() error { - var err error - m.Contracts.AccessControllerAddr, err = m.Clients.GauntletClient.DeployAccessControllerContract() - if err != nil { - return err - } - err = os.Setenv("BILLING_ACCESS_CONTROLLER", m.Contracts.AccessControllerAddr) - if err != nil { - return err - } - return nil -} - -func (m *OCRv2TestState) setConfigDetails(ocrAddress string) error { - cfg, err := m.LoadOCR2Config() - if err != nil { - return err - } - var parsedConfig []byte - parsedConfig, err = json.Marshal(cfg) - if err != nil { - return err - } - _, err = m.Clients.GauntletClient.SetConfigDetails(string(parsedConfig), ocrAddress) - return err -} - -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 - } - - m.Clients.ChainlinkClient.AccountAddresses, err = m.fundNodes() - if err != nil { - return err - } - - err = m.deployLinkToken() - if err != nil { - return err - } - - err = m.deployAccessController() - if err != nil { - return err - } - - m.Contracts.OCRAddr, err = m.Clients.GauntletClient.DeployOCR2ControllerContract(minSubmissionValue, maxSubmissionValue, decimals, name, m.Contracts.LinkTokenAddr) - if err != nil { - return err - } - - m.Contracts.ProxyAddr, err = m.Clients.GauntletClient.DeployOCR2ProxyContract(m.Contracts.OCRAddr) - if err != nil { - return err - } - _, err = m.Clients.GauntletClient.AddAccess(m.Contracts.OCRAddr, m.Contracts.ProxyAddr) - if err != nil { - return err - } - - _, err = m.Clients.GauntletClient.MintLinkToken(m.Contracts.LinkTokenAddr, m.Contracts.OCRAddr, "100000000000000000000") - if err != nil { - return err - } - _, err = m.Clients.GauntletClient.SetOCRBilling(observationPaymentGjuels, transmissionPaymentGjuels, m.Contracts.OCRAddr) - if err != nil { - return err - } - - err = m.setConfigDetails(m.Contracts.OCRAddr) - return err -} diff --git a/integration-tests/common/gauntlet_plus_plus_common.go b/integration-tests/common/gauntlet_plus_plus_common.go index 79f745fe7..4857ce49a 100644 --- a/integration-tests/common/gauntlet_plus_plus_common.go +++ b/integration-tests/common/gauntlet_plus_plus_common.go @@ -2,28 +2,81 @@ package common import ( "encoding/json" + "errors" + "fmt" "os" + "github.com/smartcontractkit/chainlink-starknet/integration-tests/utils" ) +func (m *OCRv2TestState) fundNodesWithGPP() ([]string, error) { + l := utils.GetTestLogger(m.TestConfig.T) + var nAccounts []string + err := m.Clients.GauntletPPClient.DeclareOzAccount() + if err != nil { + return nil, err + } + for _, key := range m.GetNodeKeys() { + if key.TXKey.Data.Attributes.StarkKey == "" { + return nil, errors.New("stark key can't be empty") + } + //nAccount, err := m.Clients.GauntletClient.DeployAccountContract(100, key.TXKey.Data.Attributes.StarkKey) + nAccount, err := m.Clients.GauntletPPClient.DeployOzAccount(key.TXKey.Data.Attributes.StarkKey) + if err != nil { + return nil, err + } + nAccounts = append(nAccounts, nAccount) + } + + 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 := m.Clients.GauntletClient.TransferToken(m.Common.ChainDetails.StarkTokenAddress, key, "10000000000000000000") // Transferring 10 STRK to each node + err := m.Clients.GauntletPPClient.TransferToken(m.Common.ChainDetails.StarkTokenAddress, key, "10000000000000000000") + if err != nil { + return nil, err + } + } + } else { + // 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 (m *OCRv2TestState) deployAccessControllerWithGpp() error { - var err error - m.Contracts.AccessControllerAddr, err = m.Clients.GauntletPPClient.DeployAccessControllerContract(m.Account.Account) + err := m.Clients.GauntletPPClient.DeclareAccessControllerContract() if err != nil { return err } - err = os.Setenv("BILLING_ACCESS_CONTROLLER", m.Contracts.AccessControllerAddr) + + m.Contracts.AccessControllerAddr, err = m.Clients.GauntletPPClient.DeployAccessControllerContract(m.Account.Account) if err != nil { return err } - return nil -} - -func (m *OCRv2TestState) declareLinkToken() error { - err := m.Clients.GauntletPPClient.DeclareLinkTokenContract() + err = os.Setenv("BILLING_ACCESS_CONTROLLER", m.Contracts.AccessControllerAddr) if err != nil { return err } - return nil } @@ -55,3 +108,69 @@ func (m *OCRv2TestState) setConfigDetailsWithGpp(ocrAddress string) error { _, err = m.Clients.GauntletPPClient.SetConfigDetails(string(parsedConfig), ocrAddress) return err } + +func (m *OCRv2TestState) setConfigDetails(ocrAddress string) error { + cfg, err := m.LoadOCR2Config() + if err != nil { + return err + } + var parsedConfig []byte + parsedConfig, err = json.Marshal(cfg) + if err != nil { + return err + } + _, err = m.Clients.GauntletClient.SetConfigDetails(string(parsedConfig), ocrAddress) + return err +} + +func (m *OCRv2TestState) DeployGauntletPP(minSubmissionValue int64, maxSubmissionValue int64, decimals int, name string, observationPaymentGjuels int64, transmissionPaymentGjuels int64) error { + err := m.Clients.GauntletClient.InstallDependencies() + if err != nil { + return err + } + + m.Clients.ChainlinkClient.AccountAddresses, err = m.fundNodesWithGPP() + if err != nil { + return err + } + + err = m.deployLinkTokenWithGpp() + if err != nil { + return err + } + + err = m.deployAccessControllerWithGpp() + if err != nil { + return err + } + + m.Contracts.OCRAddr, err = m.Clients.GauntletPPClient.DeployOCR2ControllerContract(minSubmissionValue, maxSubmissionValue, decimals, name, + m.Contracts.LinkTokenAddr, m.Account.Account, m.Contracts.AccessControllerAddr) + if err != nil { + return err + } + + m.Contracts.ProxyAddr, err = m.Clients.GauntletPPClient.DeployOCR2ControllerProxyContract(m.Account.Account, m.Contracts.OCRAddr) + if err != nil { + return err + } + + err = m.Clients.GauntletPPClient.AddAccess(m.Contracts.OCRAddr, m.Contracts.ProxyAddr) + if err != nil { + return err + } + + // Gauntlet PP does not have a mint op. We will use legacy gauntlet until we implement one + _, err = m.Clients.GauntletClient.MintLinkToken(m.Contracts.LinkTokenAddr, m.Contracts.OCRAddr, "100000000000000000000") + if err != nil { + return err + } + + _, err = m.Clients.GauntletPPClient.SetOCRBilling(observationPaymentGjuels, transmissionPaymentGjuels, m.Contracts.OCRAddr) + if err != nil { + return err + } + + 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 976ad8ee8..a401fd028 100644 --- a/integration-tests/common/test_common.go +++ b/integration-tests/common/test_common.go @@ -6,6 +6,7 @@ import ( "math/big" "net/http" "os" + "strings" "testing" "time" @@ -26,6 +27,7 @@ import ( test_env_integrations "github.com/smartcontractkit/chainlink/integration-tests/docker/test_env" test_env_starknet "github.com/smartcontractkit/chainlink-starknet/integration-tests/docker/testenv" + test_env_gauntlet "github.com/smartcontractkit/chainlink-starknet/integration-tests/docker/testenv/gauntlet" "github.com/smartcontractkit/chainlink-starknet/integration-tests/testconfig" "github.com/smartcontractkit/chainlink-starknet/ops" @@ -136,7 +138,7 @@ func (m *OCRv2TestState) DeployCluster() { // When running soak we need to use K8S if *m.Common.TestConfig.Common.InsideK8s { m.DeployEnv() - + m.StartGppWithoutNetwork() if m.Common.Env.WillUseRemoteRunner() { return } @@ -145,6 +147,7 @@ func (m *OCRv2TestState) DeployCluster() { // Checking whether we are running in a remote runner since the forwarding is not working there and we need the public IP // In that case it is http://127.0.0.1:0 so we do a check and get the public IP + if m.Common.RPCDetails.RPCL2External == "http://127.0.0.1:0" { m.Common.RPCDetails.RPCL2External = m.Common.Env.URLs["starknet-dev"][1] } @@ -159,6 +162,7 @@ func (m *OCRv2TestState) DeployCluster() { } else { // Otherwise use docker env, err := test_env_integrations.NewTestEnv() require.NoError(m.TestConfig.T, err) + m.StartGppWithNetwork(env.DockerNetwork.Name) stark := test_env_starknet.NewStarknet([]string{env.DockerNetwork.Name}, *m.Common.TestConfig.Common.DevnetImage) err = stark.StartContainer() require.NoError(m.TestConfig.T, err) @@ -171,7 +175,7 @@ func (m *OCRv2TestState) DeployCluster() { 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_integrations.NewCLTestEnvBuilder(). WithNonEVM(). @@ -235,6 +239,22 @@ func (m *OCRv2TestState) DeployCluster() { } } +// Starts GauntletPP Without a network +func (m *OCRv2TestState) StartGppWithoutNetwork() { + gpp := test_env_gauntlet.NewGauntletPlusPlus([]string{}, *m.Common.TestConfig.Common.GauntletPlusPlusImage) + url, err := gpp.StartContainer() + m.TestConfig.TestConfig.Common.GauntletPlusPlusURL = url + require.NoError(m.TestConfig.T, err) +} + +// Starts GauntletPP with a network +func (m *OCRv2TestState) StartGppWithNetwork(networkName string) { + gpp := test_env_gauntlet.NewGauntletPlusPlus([]string{networkName}, *m.Common.TestConfig.Common.GauntletPlusPlusImage) + url, err := gpp.StartContainer() + m.TestConfig.TestConfig.Common.GauntletPlusPlusURL = url + require.NoError(m.TestConfig.T, err) +} + // DeployEnv Deploys the environment func (m *OCRv2TestState) DeployEnv() { err := m.Common.Env.Run() @@ -249,13 +269,15 @@ func (m *OCRv2TestState) LoadOCR2Config() (*ops.OCR2Config, error) { var txKeys []string var cfgKeys []string for i, key := range m.Clients.ChainlinkClient.NKeys { + // need to remove the prefix since legacy gauntlet did it pre op + // In G++ only signers have prefix removed + // https://github.com/smartcontractkit/gauntlet-plus-plus/blob/main/packages-starknet/operations-data-feeds/tests/fixtures/offchain-config.fixture.ts offChaiNKeys = append(offChaiNKeys, key.OCR2Key.Data.Attributes.OffChainPublicKey) peerIDs = append(peerIDs, key.PeerID) txKeys = append(txKeys, m.Clients.ChainlinkClient.AccountAddresses[i]) - onChaiNKeys = append(onChaiNKeys, key.OCR2Key.Data.Attributes.OnChainPublicKey) + onChaiNKeys = append(onChaiNKeys, m.removeOCR2PrefixAndAddPrefix(key.OCR2Key.Data.Attributes.OnChainPublicKey, "ocr2on_starknet_", "0x")) cfgKeys = append(cfgKeys, key.OCR2Key.Data.Attributes.ConfigPublicKey) } - var payload = ops.TestOCR2Config payload.Signers = onChaiNKeys payload.Transmitters = txKeys @@ -266,6 +288,14 @@ func (m *OCRv2TestState) LoadOCR2Config() (*ops.OCR2Config, error) { return &payload, nil } +func (m *OCRv2TestState) removeOCR2PrefixAndAddPrefix(k string, prefix string, newPrefix string) string { + if strings.HasPrefix(k, prefix) { + return newPrefix + k[len(prefix):] + } + + return k +} + 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") @@ -327,6 +357,7 @@ func (m *OCRv2TestState) ValidateRounds(rounds int, isSoak bool) error { if err != nil { return err } + resLINK, errLINK := m.Clients.StarknetClient.CallContract(ctx, starknet.CallOps{ ContractAddress: linkContractAddress, Selector: starknetutils.GetSelectorFromNameFelt("balance_of"), @@ -349,14 +380,17 @@ func (m *OCRv2TestState) ValidateRounds(rounds int, isSoak bool) error { assert.GreaterOrEqual(m.TestConfig.T, balLINK.Cmp(balAgg), 0, "Aggregator payment balance should be <= actual LINK balance") for start := time.Now(); time.Since(start) < m.Common.TestEnvDetails.TestDuration; { + m.TestConfig.L.Info().Msg(fmt.Sprintf("Agg Address: %s ", contractAddress)) + m.TestConfig.L.Info().Msg(fmt.Sprintf("Link Address: %s ", linkContractAddress)) + m.TestConfig.L.Info().Msg(fmt.Sprintf("Elapsed time: %s, Round wait: %s ", time.Since(start), m.Common.TestEnvDetails.TestDuration)) + m.TestConfig.L.Info().Msg(fmt.Sprintf("fetching Latest Transmission Details from: %s", contractAddress)) 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 } - // end condition: rounds have been stuck if stuck && stuckCount > 50 { m.TestConfig.L.Debug().Msg("failing to fetch transmissions means blockchain may have stopped") @@ -364,7 +398,7 @@ func (m *OCRv2TestState) ValidateRounds(rounds int, isSoak bool) error { } // try to fetch rounds - time.Sleep(5 * time.Second) + time.Sleep(10 * time.Second) if err != nil { m.TestConfig.L.Error().Msg(fmt.Sprintf("Transmission Error: %+v", err)) diff --git a/integration-tests/docker/testenv/gauntlet/gauntletplusplus.go b/integration-tests/docker/testenv/gauntlet/gauntletplusplus.go new file mode 100644 index 000000000..dcf9f51ca --- /dev/null +++ b/integration-tests/docker/testenv/gauntlet/gauntletplusplus.go @@ -0,0 +1,110 @@ +package testenv + +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/lib/docker/test_env" + "github.com/smartcontractkit/chainlink-testing-framework/lib/logging" + "github.com/smartcontractkit/chainlink-testing-framework/lib/utils/testcontext" +) + +const ( + GauntletPlusPlusPort = "4444" +) + +type GauntletPlusPlus struct { + test_env.EnvComponent + ExternalHTTPURL string + InternalHTTPURL string + t *testing.T + l zerolog.Logger + Image string +} + +func NewGauntletPlusPlus(networks []string, image string, opts ...test_env.EnvComponentOption) *GauntletPlusPlus { + ms := &GauntletPlusPlus{ + Image: image, + EnvComponent: test_env.EnvComponent{ + ContainerName: "gauntlet-plus-plus", + Networks: networks, + }, + + l: log.Logger, + } + + for _, opt := range opts { + opt(&ms.EnvComponent) + } + return ms +} + +func (g *GauntletPlusPlus) WithTestLogger(t *testing.T) *GauntletPlusPlus { + g.l = logging.GetTestLogger(t) + g.t = t + return g +} + +func (g *GauntletPlusPlus) StartContainer() (string, error) { + l := tc.Logger + if g.t != nil { + l = logging.CustomT{ + T: g.t, + L: g.l, + } + } + cReq, err := g.getContainerRequest() + if err != nil { + return "", err + } + c, err := tc.GenericContainer(testcontext.Get(g.t), tc.GenericContainerRequest{ + ContainerRequest: *cReq, + Reuse: true, + Started: true, + Logger: l, + }) + if err != nil { + return "", fmt.Errorf("cannot start GauntletPlusPlus container: %w", err) + } + + g.Container = c + host, err := test_env.GetHost(testcontext.Get(g.t), c) + if err != nil { + return "", err + } + + httpPort, err := c.MappedPort(testcontext.Get(g.t), test_env.NatPort(GauntletPlusPlusPort)) + if err != nil { + return "", err + } + + g.ExternalHTTPURL = fmt.Sprintf("http://%s:%s", host, httpPort.Port()) + g.InternalHTTPURL = fmt.Sprintf("http://%s:%s", g.ContainerName, GauntletPlusPlusPort) + + g.l.Info(). + Any("ExternalHTTPURL", g.ExternalHTTPURL). + Any("InternalHTTPURL", g.InternalHTTPURL). + Str("containerName", g.ContainerName). + Msgf("Started Gauntlet Plus Plus container") + + return g.ExternalHTTPURL, nil +} + +func (g *GauntletPlusPlus) getContainerRequest() (*tc.ContainerRequest, error) { + return &tc.ContainerRequest{ + Name: g.ContainerName, + Image: g.Image, + ExposedPorts: []string{test_env.NatPortFormat(GauntletPlusPlusPort)}, + Networks: g.Networks, + WaitingFor: tcwait.ForLog("Server listening at "). + WithStartupTimeout(30 * time.Second). + WithPollInterval(100 * time.Millisecond), + }, nil +} diff --git a/integration-tests/docker/testenv/stark.go b/integration-tests/docker/testenv/stark.go index 0377b03e6..a21e04b83 100644 --- a/integration-tests/docker/testenv/stark.go +++ b/integration-tests/docker/testenv/stark.go @@ -1,4 +1,4 @@ -package testenv +package gauntlet import ( "fmt" diff --git a/integration-tests/smoke/ocr2_test.go b/integration-tests/smoke/ocr2_test.go index 289465783..29162982d 100644 --- a/integration-tests/smoke/ocr2_test.go +++ b/integration-tests/smoke/ocr2_test.go @@ -35,10 +35,6 @@ func TestOCRBasic(t *testing.T) { 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 { @@ -74,16 +70,16 @@ func TestOCRBasic(t *testing.T) { } state.DeployCluster() // Setting up G++ Client - rpcURL := state.Common.RPCDetails.RPCL2External - gppPort := "http://localhost:" + *state.TestConfig.TestConfig.Common.GauntletPlusPlusPort - state.Clients.GauntletPPClient, err = gauntlet.NewStarknetGauntletPlusPlus(gppPort, rpcURL, state.Account.Account, state.Account.PrivateKey) - require.NoError(t, err, "Setting up gauntlet ++ should not fail") + rpcURL := state.Common.RPCDetails.RPCL2Internal + gppURL := state.TestConfig.TestConfig.Common.GauntletPlusPlusURL + state.Clients.GauntletPPClient, err = gauntlet.NewStarknetGauntletPlusPlus(gppURL, rpcURL, state.Account.Account, state.Account.PrivateKey) + require.NoError(t, err, "Setting up gauntlet++ should not fail") 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) + err = state.DeployGauntletPP(0, 100000000000, decimals, "auto", 1, 1) require.NoError(t, err, "Deploying contracts should not fail") state.SetUpNodes() diff --git a/integration-tests/soak/ocr2_test.go b/integration-tests/soak/ocr2_test.go index c0b0ffd45..368204a6e 100644 --- a/integration-tests/soak/ocr2_test.go +++ b/integration-tests/soak/ocr2_test.go @@ -78,11 +78,20 @@ func TestOCRBasicSoak(t *testing.T) { return } + // Setting up G++ Client + rpcURL := state.Common.RPCDetails.RPCL2Internal + gppURL := state.TestConfig.TestConfig.Common.GauntletPlusPlusURL + state.Clients.GauntletPPClient, err = gauntlet.NewStarknetGauntletPlusPlus(gppURL, rpcURL, state.Account.Account, state.Account.PrivateKey) + require.NoError(t, err, "Setting up gauntlet++ should not fail") + 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) + fmt.Println("External URL" + state.Common.RPCDetails.RPCL2External) + fmt.Println("Internal URL" + state.Common.RPCDetails.RPCL2Internal) + + err = state.DeployGauntletPP(0, 100000000000, decimals, "auto", 1, 1) require.NoError(t, err, "Deploying contracts should not fail") state.SetUpNodes() diff --git a/integration-tests/testconfig/default.toml b/integration-tests/testconfig/default.toml index f47e53d37..8f9a16810 100644 --- a/integration-tests/testconfig/default.toml +++ b/integration-tests/testconfig/default.toml @@ -29,7 +29,7 @@ stateful_db = false devnet_image = "shardlabs/starknet-devnet-rs:a147b4cd72f9ce9d1fa665d871231370db0f51c7" postgres_version = "15.7" gauntlet_plus_plus_port = "5234" -gauntlet_plus_plus_image = "gauntlet-plus-plus:latest" +gauntlet_plus_plus_image = "804282218731.dkr.ecr.us-west-2.amazonaws.com/gauntlet-plus-plus:v2.5.0" [OCR2] node_count = 6 diff --git a/integration-tests/testconfig/testconfig.go b/integration-tests/testconfig/testconfig.go index 270608527..ddb0559c7 100644 --- a/integration-tests/testconfig/testconfig.go +++ b/integration-tests/testconfig/testconfig.go @@ -192,7 +192,7 @@ type Common struct { DevnetImage *string `toml:"devnet_image"` GauntletPlusPlusImage *string `toml:"gauntlet_plus_plus_image"` PostgresVersion *string `toml:"postgres_version"` - GauntletPlusPlusPort *string `toml:"gauntlet_plus_plus_port"` + GauntletPlusPlusURL string } func (c *Common) Validate() error { diff --git a/ops/gauntlet/gauntlet_plus_plus_starknet.go b/ops/gauntlet/gauntlet_plus_plus_starknet.go index 368a208e6..beabff9a0 100644 --- a/ops/gauntlet/gauntlet_plus_plus_starknet.go +++ b/ops/gauntlet/gauntlet_plus_plus_starknet.go @@ -5,8 +5,10 @@ import ( "encoding/json" "fmt" "net/http" + "time" "github.com/rs/zerolog/log" + g "github.com/smartcontractkit/gauntlet-plus-plus/sdks/go-gauntlet/client" ) @@ -25,17 +27,6 @@ type StarknetGauntletPlusPlus struct { providers *[]g.Provider } -func toPointerMap(input map[string]interface{}) map[string]*interface{} { - result := make(map[string]*interface{}) - for k, v := range input { - // Create a new variable to hold the value - valueCopy := v - // Store the pointer to the new variable - result[k] = &valueCopy - } - return result -} - func (sgpp *StarknetGauntletPlusPlus) BuildProviders(address string, rpcURL string, privateKey string) *[]g.Provider { accountProviderInput := map[string]interface{}{ "address": address, @@ -92,8 +83,17 @@ func NewStarknetGauntletPlusPlus(gauntletPPEndpoint string, rpcURL string, addre func (sgpp *StarknetGauntletPlusPlus) ExtractValueFromResponseBody(report g.Report, key string) (string, error) { if report.Output != nil { + // Log the raw content of Output + _, err := json.Marshal(report.Output) + if err != nil { + log.Error().Err(err).Msg("Failed to marshal report.Output") + return "", err + } + // Attempt to assert the Output as a map if outputMap, ok := (*report.Output).(map[string]interface{}); ok { + log.Info().Interface("Report Response: ", outputMap).Msg("Gauntlet++") + if value, exists := outputMap[key]; exists { // Assert value to a string if strValue, ok := value.(string); ok { @@ -102,6 +102,9 @@ func (sgpp *StarknetGauntletPlusPlus) ExtractValueFromResponseBody(report g.Repo err := fmt.Errorf("parsed Value is not of type string") return "", err } + } else { + // Log a message if it’s not a map + log.Warn().Msg("Report.Output is not a map[string]interface{}") } } return "", nil @@ -125,24 +128,24 @@ func (sgpp *StarknetGauntletPlusPlus) BuildRequestBody(request Request) *g.PostE } func (sgpp *StarknetGauntletPlusPlus) execute(request *Request) error { - body := sgpp.BuildRequestBody(*request) + report, err := sgpp.executeReturnsReport(request) - tmp, err := json.Marshal(body) - if err != nil { - return err // Handle marshaling error - } - - // Show request body - log.Info().Str("Request Body: ", string(tmp)).Msg("Gauntlet++") - - headers := &g.PostExecuteParams{} - response, err := sgpp.client.PostExecuteWithResponse(context.Background(), headers, *body) if err != nil { return err // Handle post execution error } - // Show Response Status - log.Info().Str("Response Status:", response.Status()).Msg("Gauntlet++") + if report.Output != nil { + _, err := json.Marshal(report.Output) + if err != nil { + log.Error().Err(err).Msg("Failed to marshal report.Output") + return err + } + err = processReport(&report) + if err != nil { + log.Error().Err(err).Msg("Failed to process Op report") + return err + } + } return nil } @@ -155,14 +158,29 @@ func (sgpp *StarknetGauntletPlusPlus) executeReturnsReport(request *Request) (g. } // Show request body - log.Info().Str("Request Body: ", string(tmp)).Msg("Gauntlet++") + log.Info().Str("Request Body:", string(tmp)).Msg("Gauntlet++") + // Make the API call headers := &g.PostExecuteParams{} + response, err := sgpp.client.PostExecuteWithResponse(context.Background(), headers, *body) if err != nil { return g.Report{}, err // Handle post execution error } + // Log the response body + responseJSON, err := json.Marshal(response.JSON200) // Marshal the JSON200 field to JSON string + if err != nil { + log.Error().Err(err).Msg("Failed to marshal response body") + return g.Report{}, err + } + if response.JSON200 == nil || response.JSON200.Id == "" || response == nil { + time.Sleep(20 * time.Minute) + } + + // Log the full response JSON + log.Info().Str("Response Body:", string(responseJSON)).Msg("Gauntlet++") + return *response.JSON200, nil } @@ -172,20 +190,27 @@ func (sgpp *StarknetGauntletPlusPlus) executeDeploy(request *Request) (string, e if err != nil { return "", err // Handle post execution error } + contractAddress, err := sgpp.ExtractValueFromResponseBody(report, "contractAddress") if err != nil { log.Err(err).Str("G++ Request returned with err", err.Error()).Msg("Gauntlet++") return "", err } + if contractAddress == "" { + log.Err(err).Str("G++ Deploy Requets returned with empty contractAddress", err.Error()).Msg("Gauntlet++") + return "", err + } + + log.Info().Str("Contract Address Response: ", contractAddress).Msg("Gauntlet++") return contractAddress, nil } -func (sgpp *StarknetGauntletPlusPlus) TransferToken(tokenAddress string, to string, from string) error { +func (sgpp *StarknetGauntletPlusPlus) TransferToken(tokenAddress string, to string, amount string) error { inputMap := map[string]interface{}{ - "to": to, - "from": from, "address": tokenAddress, + "to": to, + "amount": amount, } request := Request{ @@ -292,12 +317,6 @@ func (sgpp *StarknetGauntletPlusPlus) DeclareAccessControllerContract() error { } func (sgpp *StarknetGauntletPlusPlus) DeployAccessControllerContract(address string) (string, error) { - // Declare Contract first - err := sgpp.DeclareAccessControllerContract() - if err != nil { - return "", err - } - constructorCalldata := map[string]interface{}{ "owner": address, } @@ -306,7 +325,7 @@ func (sgpp *StarknetGauntletPlusPlus) DeployAccessControllerContract(address str } request := Request{ - Command: "starknet/token/link:declare", + Command: "starknet/data-feeds/access-controller@1.0.0:deploy", Input: inputMap, } return sgpp.executeDeploy(&request) @@ -330,10 +349,13 @@ func (sgpp *StarknetGauntletPlusPlus) DeployLinkTokenContract(address string) (s return "", err } - inputMap := map[string]interface{}{ + constructorCalldata := map[string]interface{}{ "minter": address, "owner": address, } + inputMap := map[string]interface{}{ + "constructorCalldata": &constructorCalldata, + } request := Request{ Command: "starknet/token/link:deploy", @@ -358,15 +380,16 @@ func (sgpp *StarknetGauntletPlusPlus) SetConfigDetails(cfg string, ocrAddress st Command: "starknet/data-feeds/aggregator@1.0.0:set-config", Input: inputMap, } - return sgpp.executeReturnsReport(&request) + test, testerr := sgpp.executeReturnsReport(&request) + return test, testerr } func (sgpp *StarknetGauntletPlusPlus) SetOCRBilling(observationPaymentGjuels int64, transmissionPaymentGjuels int64, ocrAddress string) (g.Report, error) { txArgs := map[string]interface{}{ "transmissionPaymentGjuels": transmissionPaymentGjuels, "observationPaymentGjuels": observationPaymentGjuels, - "gasPerSignature": "0", - "gasBase": "0", + "gasPerSignature": 0, + "gasBase": 0, } inputMap := map[string]interface{}{ "address": ocrAddress, @@ -392,10 +415,6 @@ func (sgpp *StarknetGauntletPlusPlus) DeclareOzAccount() error { } func (sgpp *StarknetGauntletPlusPlus) DeployOzAccount(publicKey string) (string, error) { - err := sgpp.DeclareOzAccount() - if err != nil { - return "", err - } constructorCalldata := map[string]interface{}{ "publicKey": publicKey, @@ -411,3 +430,69 @@ func (sgpp *StarknetGauntletPlusPlus) DeployOzAccount(publicKey string) (string, return sgpp.executeDeploy(&request) } + +func toPointerMap(input map[string]interface{}) map[string]*interface{} { + result := make(map[string]*interface{}) + for k, v := range input { + // Create a new variable to hold the value + valueCopy := v + // Store the pointer to the new variable + result[k] = &valueCopy + } + return result +} + +func processReport(report *g.Report) error { + // Ensure Output is a map + outputMap, ok := (*report.Output).(map[string]interface{}) + if !ok { + log.Warn().Msg("Report.Output is not a map[string]interface{}") + return fmt.Errorf("Report.Output is not a map") + } + + log.Info().Interface("Report Response: ", outputMap).Msg("Gauntlet++") + + // Access the 'receipt' field + receiptMap, err := getReceiptMap(outputMap) + if err != nil { + return err + } + + // Check 'execution_status' inside the 'receipt' field + return checkExecutionStatus(receiptMap) +} + +// Helper function to extract the receipt map +func getReceiptMap(outputMap map[string]interface{}) (map[string]interface{}, error) { + output, exists := outputMap["receipt"] + if !exists { + return nil, fmt.Errorf("receipt does not exist") + } + + receiptMap, ok := output.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("receipt is not a map") + } + + log.Info().Interface("Receipt Map: ", receiptMap).Msg("Gauntlet++") + return receiptMap, nil +} + +// Helper function to check the execution status +func checkExecutionStatus(receiptMap map[string]interface{}) error { + executionStatus, exists := receiptMap["execution_status"] + if !exists { + return fmt.Errorf("execution_status does not exist") + } + + strExecutionStatus, ok := executionStatus.(string) + if !ok { + return fmt.Errorf("execution_status is not a string") + } + + if strExecutionStatus != "SUCCEEDED" { + return fmt.Errorf("Op was not successful") + } + + return nil +} diff --git a/ops/gauntlet/gauntlet_starknet.go b/ops/gauntlet/gauntlet_starknet.go index ee0145ec0..043c7a81b 100644 --- a/ops/gauntlet/gauntlet_starknet.go +++ b/ops/gauntlet/gauntlet_starknet.go @@ -94,30 +94,6 @@ func (sg *StarknetGauntlet) InstallDependencies() error { return nil } -func (sg *StarknetGauntlet) DeployAccountContract(salt int64, pubKey string) (string, error) { - _, err := sg.G.ExecCommand([]string{"account:deploy", fmt.Sprintf("--salt=%d", salt), fmt.Sprintf("--publicKey=%s", pubKey)}, *sg.options) - if err != nil { - return "", err - } - sg.gr, err = sg.FetchGauntletJSONOutput() - if err != nil { - return "", err - } - return sg.gr.Responses[0].Contract, nil -} - -func (sg *StarknetGauntlet) DeployLinkTokenContract() (string, error) { - _, err := sg.G.ExecCommand([]string{"token:deploy", "--link"}, *sg.options) - if err != nil { - return "", err - } - sg.gr, err = sg.FetchGauntletJSONOutput() - if err != nil { - return "", err - } - return sg.gr.Responses[0].Contract, nil -} - func (sg *StarknetGauntlet) MintLinkToken(token, to, amount string) (string, error) { _, err := sg.G.ExecCommand([]string{"token:mint", fmt.Sprintf("--recipient=%s", to), fmt.Sprintf("--amount=%s", amount), token}, *sg.options) if err != nil { @@ -130,66 +106,6 @@ func (sg *StarknetGauntlet) MintLinkToken(token, to, amount string) (string, err return sg.gr.Responses[0].Contract, nil } -func (sg *StarknetGauntlet) TransferToken(token, to, amount string) (string, error) { - _, err := sg.G.ExecCommand([]string{"token:transfer", fmt.Sprintf("--recipient=%s", to), fmt.Sprintf("--amount=%s", amount), token}, *sg.options) - if err != nil { - return "", err - } - sg.gr, err = sg.FetchGauntletJSONOutput() - if err != nil { - return "", err - } - return sg.gr.Responses[0].Contract, nil -} - -func (sg *StarknetGauntlet) DeployOCR2ControllerContract(minSubmissionValue int64, maxSubmissionValue int64, decimals int, name string, linkTokenAddress string) (string, error) { - _, err := sg.G.ExecCommand([]string{"ocr2:deploy", fmt.Sprintf("--minSubmissionValue=%d", minSubmissionValue), fmt.Sprintf("--maxSubmissionValue=%d", maxSubmissionValue), fmt.Sprintf("--decimals=%d", decimals), fmt.Sprintf("--name=%s", name), fmt.Sprintf("--link=%s", linkTokenAddress)}, *sg.options) - if err != nil { - return "", err - } - sg.gr, err = sg.FetchGauntletJSONOutput() - if err != nil { - return "", err - } - return sg.gr.Responses[0].Contract, nil -} - -func (sg *StarknetGauntlet) DeployAccessControllerContract() (string, error) { - _, err := sg.G.ExecCommand([]string{"access_controller:deploy"}, *sg.options) - if err != nil { - return "", err - } - sg.gr, err = sg.FetchGauntletJSONOutput() - if err != nil { - return "", err - } - return sg.gr.Responses[0].Contract, nil -} - -func (sg *StarknetGauntlet) DeployOCR2ProxyContract(aggregator string) (string, error) { - _, err := sg.G.ExecCommand([]string{"proxy:deploy", fmt.Sprintf("--address=%s", aggregator)}, *sg.options) - if err != nil { - return "", err - } - sg.gr, err = sg.FetchGauntletJSONOutput() - if err != nil { - return "", err - } - return sg.gr.Responses[0].Contract, nil -} - -func (sg *StarknetGauntlet) SetOCRBilling(observationPaymentGjuels int64, transmissionPaymentGjuels int64, ocrAddress string) (string, error) { - _, err := sg.G.ExecCommand([]string{"ocr2:set_billing", fmt.Sprintf("--observationPaymentGjuels=%d", observationPaymentGjuels), fmt.Sprintf("--transmissionPaymentGjuels=%d", transmissionPaymentGjuels), ocrAddress}, *sg.options) - if err != nil { - return "", err - } - sg.gr, err = sg.FetchGauntletJSONOutput() - if err != nil { - return "", err - } - return sg.gr.Responses[0].Contract, nil -} - func (sg *StarknetGauntlet) SetConfigDetails(cfg string, ocrAddress string) (string, error) { _, err := sg.G.ExecCommand([]string{"ocr2:set_config", "--input=" + cfg, ocrAddress}, *sg.options) if err != nil { @@ -201,15 +117,3 @@ func (sg *StarknetGauntlet) SetConfigDetails(cfg string, ocrAddress string) (str } return sg.gr.Responses[0].Contract, nil } - -func (sg *StarknetGauntlet) AddAccess(aggregator, address string) (string, error) { - _, err := sg.G.ExecCommand([]string{"ocr2:add_access", fmt.Sprintf("--address=%s", address), aggregator}, *sg.options) - if err != nil { - return "", err - } - sg.gr, err = sg.FetchGauntletJSONOutput() - if err != nil { - return "", err - } - return sg.gr.Responses[0].Contract, nil -}