diff --git a/.github/workflows/on-demand-vrfv2-performance-test.yml b/.github/workflows/on-demand-vrfv2-performance-test.yml new file mode 100644 index 00000000000..100fdf73e61 --- /dev/null +++ b/.github/workflows/on-demand-vrfv2-performance-test.yml @@ -0,0 +1,136 @@ +name: On Demand VRFV2 Performance Test +on: + workflow_dispatch: + inputs: + network: + description: Network to run tests on + type: choice + options: + - "ETHEREUM_MAINNET" + - "SIMULATED" + - "SEPOLIA" + - "OPTIMISM_MAINNET" + - "OPTIMISM_GOERLI" + - "ARBITRUM_MAINNET" + - "ARBITRUM_GOERLI" + - "ARBITRUM_SEPOLIA" + - "BSC_MAINNET" + - "BSC_TESTNET" + - "POLYGON_MAINNET" + - "POLYGON_MUMBAI" + - "AVALANCHE_FUJI" + - "AVALANCHE_MAINNET" + fundingPrivateKey: + description: Private funding key (Skip for Simulated) + required: false + type: string + wsURL: + description: WS URL for the network (Skip for Simulated) + required: false + type: string + httpURL: + description: HTTP URL for the network (Skip for Simulated) + required: false + type: string + chainlinkImage: + description: Container image location for the Chainlink nodes + required: true + default: public.ecr.aws/chainlink/chainlink + chainlinkVersion: + description: Container image version for the Chainlink nodes + required: true + default: "2.6.0" + performanceTestType: + description: Performance Test Type of test to run + type: choice + options: + - "Soak" + - "Load" + - "Stress" + - "Spike" + testDuration: + description: Duration of the test (time string) + required: true + default: 5m + useExistingEnv: + description: Set `true` to use existing environment or `false` to deploy CL node and all contracts + required: false + default: false + configBase64: + description: TOML config in base64 (Needed when overriding config or providing contract addresses for existing env) + required: false +jobs: + vrfv2_performance_test: + name: ${{ inputs.network }} VRFV2 Performance Test + environment: integration + runs-on: ubuntu20.04-8cores-32GB + permissions: + checks: write + pull-requests: write + id-token: write + contents: read + env: + LOKI_URL: ${{ secrets.LOKI_URL }} + LOKI_TOKEN: ${{ secrets.LOKI_TOKEN }} + SELECTED_NETWORKS: ${{ inputs.network }} + TEST_TYPE: ${{ inputs.performanceTestType }} + VRFV2_TEST_DURATION: ${{ inputs.testDuration }} + VRFV2_USE_EXISTING_ENV: ${{ inputs.useExistingEnv }} + CONFIG: ${{ inputs.configBase64 }} + TEST_LOG_LEVEL: debug + REF_NAME: ${{ github.head_ref || github.ref_name }} + CHAINLINK_IMAGE: ${{ inputs.chainlinkImage }} + CHAINLINK_VERSION: ${{ inputs.chainlinkVersion }} + SLACK_API_KEY: ${{ secrets.QA_SLACK_API_KEY }} + SLACK_CHANNEL: ${{ secrets.QA_VRF_SLACK_CHANNEL }} + WASP_LOG_LEVEL: info + steps: + - name: Collect Metrics + id: collect-gha-metrics + uses: smartcontractkit/push-gha-metrics-action@d1618b772a97fd87e6505de97b872ee0b1f1729a # v2.0.2 + with: + basic-auth: ${{ secrets.GRAFANA_CLOUD_BASIC_AUTH }} + hostname: ${{ secrets.GRAFANA_CLOUD_HOST }} + this-job-name: ${{ inputs.network }} VRFV2 Performance Test + continue-on-error: true + - name: Setup Push Tag + shell: bash + run: | + echo "### chainlink image used for this test run :link:" >>$GITHUB_STEP_SUMMARY + echo "\`${{ inputs.chainlinkVersion }}\`" >>$GITHUB_STEP_SUMMARY + echo "### chainlink-tests image tag for this test run :ship:" >>$GITHUB_STEP_SUMMARY + echo "\`${GITHUB_SHA}\`" >>$GITHUB_STEP_SUMMARY + - name: Get Inputs + run: | + EVM_URLS=$(jq -r '.inputs.wsURL' $GITHUB_EVENT_PATH) + EVM_HTTP_URLS=$(jq -r '.inputs.httpURL' $GITHUB_EVENT_PATH) + EVM_KEYS=$(jq -r '.inputs.fundingPrivateKey' $GITHUB_EVENT_PATH) + + echo ::add-mask::$EVM_URLS + echo ::add-mask::$EVM_HTTP_URLS + echo ::add-mask::$EVM_KEYS + + echo EVM_URLS=$EVM_URLS >> $GITHUB_ENV + echo EVM_HTTP_URLS=$EVM_HTTP_URLS >> $GITHUB_ENV + echo EVM_KEYS=$EVM_KEYS >> $GITHUB_ENV + + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + fetch-depth: 0 + - name: Run Tests + uses: smartcontractkit/chainlink-github-actions/chainlink-testing-framework/run-tests@7d541cbbca52d45b8a718257af86d9cf49774d1f # v2.2.15 + with: + test_command_to_run: cd ./integration-tests && go test -v -count=1 -timeout 24h -run TestVRFV2Performance/vrfv2_performance_test ./load/vrfv2 + test_download_vendor_packages_command: cd ./integration-tests && go mod download + cl_repo: ${{ inputs.chainlinkImage }} + cl_image_tag: ${{ inputs.chainlinkVersion }} + aws_registries: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }} + artifacts_name: vrf-test-logs + artifacts_location: ./integration-tests/load/vrfv2/logs/ + token: ${{ secrets.GITHUB_TOKEN }} + go_mod_path: ./integration-tests/go.mod + should_cleanup: false + 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/.github/workflows/on-demand-vrfv2plus-performance-test.yml b/.github/workflows/on-demand-vrfv2plus-performance-test.yml index 2e765ab79bd..238f40de882 100644 --- a/.github/workflows/on-demand-vrfv2plus-performance-test.yml +++ b/.github/workflows/on-demand-vrfv2plus-performance-test.yml @@ -13,10 +13,11 @@ on: - "OPTIMISM_GOERLI" - "ARBITRUM_MAINNET" - "ARBITRUM_GOERLI" + - "ARBITRUM_SEPOLIA" - "BSC_MAINNET" - "BSC_TESTNET" - "POLYGON_MAINNET" - - "MUMBAI" + - "POLYGON_MUMBAI" - "AVALANCHE_FUJI" - "AVALANCHE_MAINNET" fundingPrivateKey: @@ -120,13 +121,13 @@ jobs: - name: Run Tests uses: smartcontractkit/chainlink-github-actions/chainlink-testing-framework/run-tests@e865e376b8c2d594028c8d645dd6c47169b72974 # v2.2.16 with: - test_command_to_run: cd ./integration-tests && go test -v -count=1 -timeout 6h -run TestVRFV2PlusPerformance/vrfv2plus_performance_test ./load/vrfv2plus + test_command_to_run: cd ./integration-tests && go test -v -count=1 -timeout 24h -run TestVRFV2PlusPerformance/vrfv2plus_performance_test ./load/vrfv2plus test_download_vendor_packages_command: cd ./integration-tests && go mod download cl_repo: ${{ inputs.chainlinkImage }} cl_image_tag: ${{ inputs.chainlinkVersion }} aws_registries: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }} artifacts_name: vrf-test-logs - artifacts_location: ./integration-tests/load/logs/ + artifacts_location: ./integration-tests/load/vrfv2plus/logs/ token: ${{ secrets.GITHUB_TOKEN }} go_mod_path: ./integration-tests/go.mod should_cleanup: false diff --git a/core/chains/evm/client/node_lifecycle_test.go b/core/chains/evm/client/node_lifecycle_test.go index 2d66c4d52c4..42f813cc2b5 100644 --- a/core/chains/evm/client/node_lifecycle_test.go +++ b/core/chains/evm/client/node_lifecycle_test.go @@ -521,7 +521,7 @@ func TestUnit_NodeLifecycle_outOfSyncLoop(t *testing.T) { n.wg.Add(1) go func() { defer close(ch) - n.aliveLoop() + n.outOfSyncLoop(func(num int64, td *utils.Big) bool { return false }) }() assert.NoError(t, n.Close()) testutils.WaitWithTimeout(t, ch, "expected outOfSyncLoop to exit") diff --git a/core/cmd/shell_local.go b/core/cmd/shell_local.go index cbd0feccbf1..ada70736f49 100644 --- a/core/cmd/shell_local.go +++ b/core/cmd/shell_local.go @@ -2,6 +2,7 @@ package cmd import ( "context" + crand "crypto/rand" "database/sql" "fmt" "log" @@ -778,11 +779,13 @@ func (s *Shell) PrepareTestDatabase(c *cli.Context) error { if userOnly { fixturePath = "../store/fixtures/users_only_fixture.sql" } - if err := insertFixtures(dbUrl, fixturePath); err != nil { + if err = insertFixtures(dbUrl, fixturePath); err != nil { return s.errorOut(err) } - - return s.errorOut(dropDanglingTestDBs(s.Logger, db)) + if err = dropDanglingTestDBs(s.Logger, db); err != nil { + return s.errorOut(err) + } + return s.errorOut(randomizeTestDBSequences(db)) } func dropDanglingTestDBs(lggr logger.Logger, db *sqlx.DB) (err error) { @@ -822,6 +825,49 @@ func dropDanglingTestDBs(lggr logger.Logger, db *sqlx.DB) (err error) { return } +type failedToRandomizeTestDBSequencesError struct{} + +func (m *failedToRandomizeTestDBSequencesError) Error() string { + return "failed to randomize test db sequences" +} + +// randomizeTestDBSequences randomizes sequenced table columns sequence +// This is necessary as to avoid false positives in some test cases. +func randomizeTestDBSequences(db *sqlx.DB) error { + seqRows, err := db.Query(`SELECT sequence_schema, sequence_name FROM information_schema.sequences WHERE sequence_schema = $1`, "public") + if err != nil { + return fmt.Errorf("%s: error fetching sequences: %s", failedToRandomizeTestDBSequencesError{}, err) + } + + defer seqRows.Close() + for seqRows.Next() { + var sequenceSchema, sequenceName string + if err = seqRows.Scan(&sequenceSchema, &sequenceName); err != nil { + return fmt.Errorf("%s: failed scanning sequence rows: %s", failedToRandomizeTestDBSequencesError{}, err) + } + + if sequenceName == "goose_migrations_id_seq" || sequenceName == "configurations_id_seq" { + continue + } + + var randNum *big.Int + randNum, err = crand.Int(crand.Reader, utils.NewBigI(10000).ToInt()) + if err != nil { + return fmt.Errorf("%s: failed to generate random number", failedToRandomizeTestDBSequencesError{}) + } + + if _, err = db.Exec(fmt.Sprintf("ALTER SEQUENCE %s.%s RESTART WITH %d", sequenceSchema, sequenceName, randNum)); err != nil { + return fmt.Errorf("%s: failed to alter and restart %s sequence: %w", failedToRandomizeTestDBSequencesError{}, sequenceName, err) + } + } + + if err = seqRows.Err(); err != nil { + return fmt.Errorf("%s: failed to iterate through sequences: %w", failedToRandomizeTestDBSequencesError{}, err) + } + + return nil +} + // PrepareTestDatabaseUserOnly calls ResetDatabase then loads only user fixtures required for local // testing against testnets. Does not include fake chain fixtures. func (s *Shell) PrepareTestDatabaseUserOnly(c *cli.Context) error { diff --git a/core/internal/testutils/pgtest/pgtest.go b/core/internal/testutils/pgtest/pgtest.go index 1900fcc62b3..01024b39c37 100644 --- a/core/internal/testutils/pgtest/pgtest.go +++ b/core/internal/testutils/pgtest/pgtest.go @@ -34,7 +34,6 @@ func NewSqlxDB(t testing.TB) *sqlx.DB { db, err := sqlx.Open(string(dialects.TransactionWrappedPostgres), uuid.New().String()) require.NoError(t, err) t.Cleanup(func() { assert.NoError(t, db.Close()) }) - db.MapperFunc(reflectx.CamelToSnakeASCII) return db diff --git a/integration-tests/actions/automationv2/actions.go b/integration-tests/actions/automationv2/actions.go new file mode 100644 index 00000000000..f97c17bb94f --- /dev/null +++ b/integration-tests/actions/automationv2/actions.go @@ -0,0 +1,699 @@ +package automationv2 + +import ( + "crypto/ed25519" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "math/big" + "strings" + "testing" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/lib/pq" + "github.com/smartcontractkit/libocr/offchainreporting2plus/confighelper" + ocr2 "github.com/smartcontractkit/libocr/offchainreporting2plus/confighelper" + ocr3 "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3confighelper" + "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + "github.com/stretchr/testify/require" + "golang.org/x/sync/errgroup" + "gopkg.in/guregu/null.v4" + + ocr2keepers20config "github.com/smartcontractkit/chainlink-automation/pkg/v2/config" + ocr2keepers30config "github.com/smartcontractkit/chainlink-automation/pkg/v3/config" + + "github.com/smartcontractkit/chainlink-testing-framework/blockchain" + "github.com/smartcontractkit/chainlink-testing-framework/logging" + "github.com/smartcontractkit/chainlink/integration-tests/client" + "github.com/smartcontractkit/chainlink/integration-tests/contracts" + "github.com/smartcontractkit/chainlink/integration-tests/contracts/ethereum" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/generated/automation_registrar_wrapper2_1" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/generated/keeper_registrar_wrapper2_0" + "github.com/smartcontractkit/chainlink/v2/core/services/job" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/chaintype" + "github.com/smartcontractkit/chainlink/v2/core/store/models" + "github.com/smartcontractkit/chainlink/v2/core/utils" +) + +type NodeDetails struct { + P2PId string + TransmitterAddresses []string + OCR2ConfigPublicKey string + OCR2OffchainPublicKey string + OCR2OnChainPublicKey string + OCR2Id string +} + +type AutomationTest struct { + ChainClient blockchain.EVMClient + Deployer contracts.ContractDeployer + + LinkToken contracts.LinkToken + Transcoder contracts.UpkeepTranscoder + EthLinkFeed contracts.MockETHLINKFeed + GasFeed contracts.MockGasFeed + Registry contracts.KeeperRegistry + Registrar contracts.KeeperRegistrar + + RegistrySettings contracts.KeeperRegistrySettings + RegistrarSettings contracts.KeeperRegistrarSettings + PluginConfig ocr2keepers30config.OffchainConfig + PublicConfig ocr3.PublicConfig + UpkeepPrivilegeManager common.Address + UpkeepIDs []*big.Int + + IsOnk8s bool + + ChainlinkNodesk8s []*client.ChainlinkK8sClient + ChainlinkNodes []*client.ChainlinkClient + + NodeDetails []NodeDetails + DefaultP2Pv2Bootstrapper string + MercuryCredentialName string + TransmitterKeyIndex int +} + +type UpkeepConfig struct { + UpkeepName string + EncryptedEmail []byte + UpkeepContract common.Address + GasLimit uint32 + AdminAddress common.Address + TriggerType uint8 + CheckData []byte + TriggerConfig []byte + OffchainConfig []byte + FundingAmount *big.Int +} + +func NewAutomationTestK8s( + chainClient blockchain.EVMClient, + deployer contracts.ContractDeployer, + chainlinkNodes []*client.ChainlinkK8sClient, +) *AutomationTest { + return &AutomationTest{ + ChainClient: chainClient, + Deployer: deployer, + ChainlinkNodesk8s: chainlinkNodes, + IsOnk8s: true, + TransmitterKeyIndex: 0, + UpkeepPrivilegeManager: common.HexToAddress(chainClient.GetDefaultWallet().Address()), + } +} + +func NewAutomationTestDocker( + chainClient blockchain.EVMClient, + deployer contracts.ContractDeployer, + chainlinkNodes []*client.ChainlinkClient, +) *AutomationTest { + return &AutomationTest{ + ChainClient: chainClient, + Deployer: deployer, + ChainlinkNodes: chainlinkNodes, + IsOnk8s: false, + TransmitterKeyIndex: 0, + UpkeepPrivilegeManager: common.HexToAddress(chainClient.GetDefaultWallet().Address()), + } +} + +func (a *AutomationTest) SetIsOnk8s(flag bool) { + a.IsOnk8s = flag +} + +func (a *AutomationTest) SetMercuryCredentialName(name string) { + a.MercuryCredentialName = name +} + +func (a *AutomationTest) SetTransmitterKeyIndex(index int) { + a.TransmitterKeyIndex = index +} + +func (a *AutomationTest) SetUpkeepPrivilegeManager(address string) { + a.UpkeepPrivilegeManager = common.HexToAddress(address) +} + +func (a *AutomationTest) DeployLINK() error { + linkToken, err := a.Deployer.DeployLinkTokenContract() + if err != nil { + return err + } + a.LinkToken = linkToken + err = a.ChainClient.WaitForEvents() + if err != nil { + return errors.Join(err, fmt.Errorf("failed waiting for link token contract to deploy")) + } + return nil +} + +func (a *AutomationTest) LoadLINK(address string) error { + linkToken, err := a.Deployer.LoadLinkToken(common.HexToAddress(address)) + if err != nil { + return err + } + a.LinkToken = linkToken + return nil +} + +func (a *AutomationTest) DeployTranscoder() error { + transcoder, err := a.Deployer.DeployUpkeepTranscoder() + if err != nil { + return err + } + a.Transcoder = transcoder + err = a.ChainClient.WaitForEvents() + if err != nil { + return errors.Join(err, fmt.Errorf("failed waiting for transcoder contract to deploy")) + } + return nil +} + +func (a *AutomationTest) LoadTranscoder(address string) error { + transcoder, err := a.Deployer.LoadUpkeepTranscoder(common.HexToAddress(address)) + if err != nil { + return err + } + a.Transcoder = transcoder + return nil +} + +func (a *AutomationTest) DeployEthLinkFeed() error { + ethLinkFeed, err := a.Deployer.DeployMockETHLINKFeed(a.RegistrySettings.FallbackLinkPrice) + if err != nil { + return err + } + a.EthLinkFeed = ethLinkFeed + err = a.ChainClient.WaitForEvents() + if err != nil { + return errors.Join(err, fmt.Errorf("failed waiting for Mock ETH LINK feed to deploy")) + } + return nil +} + +func (a *AutomationTest) LoadEthLinkFeed(address string) error { + ethLinkFeed, err := a.Deployer.LoadETHLINKFeed(common.HexToAddress(address)) + if err != nil { + return err + } + a.EthLinkFeed = ethLinkFeed + return nil +} + +func (a *AutomationTest) DeployGasFeed() error { + gasFeed, err := a.Deployer.DeployMockGasFeed(a.RegistrySettings.FallbackGasPrice) + if err != nil { + return err + } + a.GasFeed = gasFeed + err = a.ChainClient.WaitForEvents() + if err != nil { + return errors.Join(err, fmt.Errorf("failed waiting for mock gas feed to deploy")) + } + return nil +} + +func (a *AutomationTest) LoadEthGasFeed(address string) error { + gasFeed, err := a.Deployer.LoadGasFeed(common.HexToAddress(address)) + if err != nil { + return err + } + a.GasFeed = gasFeed + return nil +} + +func (a *AutomationTest) DeployRegistry() error { + registryOpts := &contracts.KeeperRegistryOpts{ + RegistryVersion: a.RegistrySettings.RegistryVersion, + LinkAddr: a.LinkToken.Address(), + ETHFeedAddr: a.EthLinkFeed.Address(), + GasFeedAddr: a.GasFeed.Address(), + TranscoderAddr: a.Transcoder.Address(), + RegistrarAddr: utils.ZeroAddress.Hex(), + Settings: a.RegistrySettings, + } + registry, err := a.Deployer.DeployKeeperRegistry(registryOpts) + if err != nil { + return err + } + a.Registry = registry + err = a.ChainClient.WaitForEvents() + if err != nil { + return errors.Join(err, fmt.Errorf("failed waiting for registry contract to deploy")) + } + return nil +} + +func (a *AutomationTest) LoadRegistry(address string) error { + registry, err := a.Deployer.LoadKeeperRegistry(common.HexToAddress(address), a.RegistrySettings.RegistryVersion) + if err != nil { + return err + } + a.Registry = registry + return nil +} + +func (a *AutomationTest) DeployRegistrar() error { + if a.Registry == nil { + return fmt.Errorf("registry must be deployed or loaded before registrar") + } + a.RegistrarSettings.RegistryAddr = a.Registry.Address() + registrar, err := a.Deployer.DeployKeeperRegistrar(a.RegistrySettings.RegistryVersion, a.LinkToken.Address(), a.RegistrarSettings) + if err != nil { + return err + } + a.Registrar = registrar + err = a.ChainClient.WaitForEvents() + if err != nil { + return errors.Join(err, fmt.Errorf("failed waiting for registrar contract to deploy")) + } + return nil +} + +func (a *AutomationTest) LoadRegistrar(address string) error { + if a.Registry == nil { + return fmt.Errorf("registry must be deployed or loaded before registrar") + } + a.RegistrarSettings.RegistryAddr = a.Registry.Address() + registrar, err := a.Deployer.LoadKeeperRegistrar(common.HexToAddress(address), a.RegistrySettings.RegistryVersion) + if err != nil { + return err + } + a.Registrar = registrar + return nil +} + +func (a *AutomationTest) CollectNodeDetails() error { + var ( + nodes []*client.ChainlinkClient + ) + if a.IsOnk8s { + for _, node := range a.ChainlinkNodesk8s[:] { + nodes = append(nodes, node.ChainlinkClient) + } + a.ChainlinkNodes = nodes + } else { + nodes = a.ChainlinkNodes[:] + } + + nodeDetails := make([]NodeDetails, 0) + + for i, node := range nodes { + nodeDetail := NodeDetails{} + P2PIds, err := node.MustReadP2PKeys() + if err != nil { + return errors.Join(err, fmt.Errorf("failed to read P2P keys from node %d", i)) + } + nodeDetail.P2PId = P2PIds.Data[0].Attributes.PeerID + + OCR2Keys, err := node.MustReadOCR2Keys() + if err != nil { + return errors.Join(err, fmt.Errorf("failed to read OCR2 keys from node %d", i)) + } + for _, key := range OCR2Keys.Data { + if key.Attributes.ChainType == string(chaintype.EVM) { + nodeDetail.OCR2ConfigPublicKey = key.Attributes.ConfigPublicKey + nodeDetail.OCR2OffchainPublicKey = key.Attributes.OffChainPublicKey + nodeDetail.OCR2OnChainPublicKey = key.Attributes.OnChainPublicKey + nodeDetail.OCR2Id = key.ID + break + } + } + + TransmitterKeys, err := node.EthAddressesForChain(a.ChainClient.GetChainID().String()) + nodeDetail.TransmitterAddresses = make([]string, 0) + if err != nil { + return errors.Join(err, fmt.Errorf("failed to read Transmitter keys from node %d", i)) + } + nodeDetail.TransmitterAddresses = append(nodeDetail.TransmitterAddresses, TransmitterKeys...) + nodeDetails = append(nodeDetails, nodeDetail) + } + a.NodeDetails = nodeDetails + + if a.IsOnk8s { + a.DefaultP2Pv2Bootstrapper = fmt.Sprintf("%s@%s-node-1:%d", a.NodeDetails[0].P2PId, a.ChainlinkNodesk8s[0].Name(), 6690) + } else { + a.DefaultP2Pv2Bootstrapper = fmt.Sprintf("%s@%s:%d", a.NodeDetails[0].P2PId, a.ChainlinkNodes[0].InternalIP(), 6690) + } + return nil +} + +func (a *AutomationTest) AddBootstrapJob() error { + bootstrapSpec := &client.OCR2TaskJobSpec{ + Name: "ocr2 bootstrap node " + a.Registry.Address(), + JobType: "bootstrap", + OCR2OracleSpec: job.OCR2OracleSpec{ + ContractID: a.Registry.Address(), + Relay: "evm", + RelayConfig: map[string]interface{}{ + "chainID": int(a.ChainClient.GetChainID().Int64()), + }, + ContractConfigTrackerPollInterval: *models.NewInterval(time.Second * 15), + }, + } + _, err := a.ChainlinkNodes[0].MustCreateJob(bootstrapSpec) + if err != nil { + return errors.Join(err, fmt.Errorf("failed to create bootstrap job on bootstrap node")) + } + return nil +} + +func (a *AutomationTest) AddAutomationJobs() error { + var contractVersion string + if a.RegistrySettings.RegistryVersion == ethereum.RegistryVersion_2_1 { + contractVersion = "v2.1" + } else if a.RegistrySettings.RegistryVersion == ethereum.RegistryVersion_2_0 { + contractVersion = "v2.0" + } else { + return fmt.Errorf("v2.0 and v2.1 are the only supported versions") + } + for i := 1; i < len(a.ChainlinkNodes); i++ { + autoOCR2JobSpec := client.OCR2TaskJobSpec{ + Name: "automation-" + contractVersion + "-" + a.Registry.Address(), + JobType: "offchainreporting2", + OCR2OracleSpec: job.OCR2OracleSpec{ + PluginType: "ocr2automation", + ContractID: a.Registry.Address(), + Relay: "evm", + RelayConfig: map[string]interface{}{ + "chainID": int(a.ChainClient.GetChainID().Int64()), + }, + PluginConfig: map[string]interface{}{ + "mercuryCredentialName": "\"" + a.MercuryCredentialName + "\"", + "contractVersion": "\"" + contractVersion + "\"", + }, + ContractConfigTrackerPollInterval: *models.NewInterval(time.Second * 15), + TransmitterID: null.StringFrom(a.NodeDetails[i].TransmitterAddresses[a.TransmitterKeyIndex]), + P2PV2Bootstrappers: pq.StringArray{a.DefaultP2Pv2Bootstrapper}, + OCRKeyBundleID: null.StringFrom(a.NodeDetails[i].OCR2Id), + }, + } + _, err := a.ChainlinkNodes[i].MustCreateJob(&autoOCR2JobSpec) + if err != nil { + return errors.Join(err, fmt.Errorf("failed to create OCR2 job on node %d", i+1)) + } + } + return nil +} + +func (a *AutomationTest) SetConfigOnRegistry() error { + donNodes := a.NodeDetails[1:] + S := make([]int, len(donNodes)) + oracleIdentities := make([]confighelper.OracleIdentityExtra, len(donNodes)) + var offC []byte + var signerOnchainPublicKeys []types.OnchainPublicKey + var transmitterAccounts []types.Account + var f uint8 + var offchainConfigVersion uint64 + var offchainConfig []byte + sharedSecretEncryptionPublicKeys := make([]types.ConfigEncryptionPublicKey, len(donNodes)) + eg := &errgroup.Group{} + for i, donNode := range donNodes { + index, chainlinkNode := i, donNode + eg.Go(func() error { + offchainPkBytes, err := hex.DecodeString(strings.TrimPrefix(chainlinkNode.OCR2OffchainPublicKey, "ocr2off_evm_")) + if err != nil { + return err + } + + offchainPkBytesFixed := [ed25519.PublicKeySize]byte{} + n := copy(offchainPkBytesFixed[:], offchainPkBytes) + if n != ed25519.PublicKeySize { + return fmt.Errorf("wrong number of elements copied") + } + + configPkBytes, err := hex.DecodeString(strings.TrimPrefix(chainlinkNode.OCR2ConfigPublicKey, "ocr2cfg_evm_")) + if err != nil { + return err + } + + configPkBytesFixed := [ed25519.PublicKeySize]byte{} + n = copy(configPkBytesFixed[:], configPkBytes) + if n != ed25519.PublicKeySize { + return fmt.Errorf("wrong number of elements copied") + } + + onchainPkBytes, err := hex.DecodeString(strings.TrimPrefix(chainlinkNode.OCR2OnChainPublicKey, "ocr2on_evm_")) + if err != nil { + return err + } + + sharedSecretEncryptionPublicKeys[index] = configPkBytesFixed + oracleIdentities[index] = confighelper.OracleIdentityExtra{ + OracleIdentity: confighelper.OracleIdentity{ + OnchainPublicKey: onchainPkBytes, + OffchainPublicKey: offchainPkBytesFixed, + PeerID: chainlinkNode.P2PId, + TransmitAccount: types.Account(chainlinkNode.TransmitterAddresses[a.TransmitterKeyIndex]), + }, + ConfigEncryptionPublicKey: configPkBytesFixed, + } + S[index] = 1 + return nil + }) + } + err := eg.Wait() + if err != nil { + return errors.Join(err, fmt.Errorf("failed to build oracle identities")) + } + + switch a.RegistrySettings.RegistryVersion { + case ethereum.RegistryVersion_2_0: + offC, err = json.Marshal(ocr2keepers20config.OffchainConfig{ + TargetProbability: a.PluginConfig.TargetProbability, + TargetInRounds: a.PluginConfig.TargetInRounds, + PerformLockoutWindow: a.PluginConfig.PerformLockoutWindow, + GasLimitPerReport: a.PluginConfig.GasLimitPerReport, + GasOverheadPerUpkeep: a.PluginConfig.GasOverheadPerUpkeep, + MinConfirmations: a.PluginConfig.MinConfirmations, + MaxUpkeepBatchSize: a.PluginConfig.MaxUpkeepBatchSize, + }) + if err != nil { + return errors.Join(err, fmt.Errorf("failed to marshal plugin config")) + } + + signerOnchainPublicKeys, transmitterAccounts, f, _, offchainConfigVersion, offchainConfig, err = ocr2.ContractSetConfigArgsForTests( + a.PublicConfig.DeltaProgress, a.PublicConfig.DeltaResend, + a.PublicConfig.DeltaRound, a.PublicConfig.DeltaGrace, + a.PublicConfig.DeltaStage, uint8(a.PublicConfig.RMax), + S, oracleIdentities, offC, + a.PublicConfig.MaxDurationQuery, a.PublicConfig.MaxDurationObservation, + 1200*time.Millisecond, + a.PublicConfig.MaxDurationShouldAcceptAttestedReport, + a.PublicConfig.MaxDurationShouldTransmitAcceptedReport, + a.PublicConfig.F, a.PublicConfig.OnchainConfig, + ) + if err != nil { + return errors.Join(err, fmt.Errorf("failed to build config args")) + } + + case ethereum.RegistryVersion_2_1: + offC, err = json.Marshal(ocr2keepers30config.OffchainConfig{ + TargetProbability: a.PluginConfig.TargetProbability, + TargetInRounds: a.PluginConfig.TargetInRounds, + PerformLockoutWindow: a.PluginConfig.PerformLockoutWindow, + GasLimitPerReport: a.PluginConfig.GasLimitPerReport, + GasOverheadPerUpkeep: a.PluginConfig.GasOverheadPerUpkeep, + MinConfirmations: a.PluginConfig.MinConfirmations, + MaxUpkeepBatchSize: a.PluginConfig.MaxUpkeepBatchSize, + }) + if err != nil { + return errors.Join(err, fmt.Errorf("failed to marshal plugin config")) + } + + signerOnchainPublicKeys, transmitterAccounts, f, _, offchainConfigVersion, offchainConfig, err = ocr3.ContractSetConfigArgsForTests( + a.PublicConfig.DeltaProgress, a.PublicConfig.DeltaResend, a.PublicConfig.DeltaInitial, + a.PublicConfig.DeltaRound, a.PublicConfig.DeltaGrace, a.PublicConfig.DeltaCertifiedCommitRequest, + a.PublicConfig.DeltaStage, a.PublicConfig.RMax, + S, oracleIdentities, offC, + a.PublicConfig.MaxDurationQuery, a.PublicConfig.MaxDurationObservation, + a.PublicConfig.MaxDurationShouldAcceptAttestedReport, + a.PublicConfig.MaxDurationShouldTransmitAcceptedReport, + a.PublicConfig.F, a.PublicConfig.OnchainConfig, + ) + if err != nil { + return errors.Join(err, fmt.Errorf("failed to build config args")) + } + default: + return fmt.Errorf("v2.0 and v2.1 are the only supported versions") + } + + var signers []common.Address + for _, signer := range signerOnchainPublicKeys { + if len(signer) != 20 { + return fmt.Errorf("OnChainPublicKey '%v' has wrong length for address", signer) + } + signers = append(signers, common.BytesToAddress(signer)) + } + + var transmitters []common.Address + for _, transmitter := range transmitterAccounts { + if !common.IsHexAddress(string(transmitter)) { + return fmt.Errorf("TransmitAccount '%s' is not a valid Ethereum address", string(transmitter)) + } + transmitters = append(transmitters, common.HexToAddress(string(transmitter))) + } + + onchainConfig, err := a.RegistrySettings.EncodeOnChainConfig(a.Registrar.Address(), a.UpkeepPrivilegeManager) + if err != nil { + return errors.Join(err, fmt.Errorf("failed to encode onchain config")) + } + + ocrConfig := contracts.OCRv2Config{ + Signers: signers, + Transmitters: transmitters, + F: f, + OnchainConfig: onchainConfig, + OffchainConfigVersion: offchainConfigVersion, + OffchainConfig: offchainConfig, + } + + err = a.Registry.SetConfig(a.RegistrySettings, ocrConfig) + if err != nil { + return errors.Join(err, fmt.Errorf("failed to set config on registry")) + } + return nil +} + +func (a *AutomationTest) RegisterUpkeeps(upkeepConfigs []UpkeepConfig) ([]common.Hash, error) { + var registrarABI *abi.ABI + var err error + var registrationRequest []byte + registrationTxHashes := make([]common.Hash, 0) + + for _, upkeepConfig := range upkeepConfigs { + switch a.RegistrySettings.RegistryVersion { + case ethereum.RegistryVersion_2_0: + registrarABI, err = keeper_registrar_wrapper2_0.KeeperRegistrarMetaData.GetAbi() + if err != nil { + return nil, errors.Join(err, fmt.Errorf("failed to get registrar abi")) + } + registrationRequest, err = registrarABI.Pack( + "register", upkeepConfig.UpkeepName, upkeepConfig.EncryptedEmail, + upkeepConfig.UpkeepContract, upkeepConfig.GasLimit, upkeepConfig.AdminAddress, + upkeepConfig.CheckData, + upkeepConfig.OffchainConfig, upkeepConfig.FundingAmount, + common.HexToAddress(a.ChainClient.GetDefaultWallet().Address())) + if err != nil { + return nil, errors.Join(err, fmt.Errorf("failed to pack registrar request")) + } + case ethereum.RegistryVersion_2_1: + registrarABI, err = automation_registrar_wrapper2_1.AutomationRegistrarMetaData.GetAbi() + if err != nil { + return nil, errors.Join(err, fmt.Errorf("failed to get registrar abi")) + } + registrationRequest, err = registrarABI.Pack( + "register", upkeepConfig.UpkeepName, upkeepConfig.EncryptedEmail, + upkeepConfig.UpkeepContract, upkeepConfig.GasLimit, upkeepConfig.AdminAddress, + upkeepConfig.TriggerType, upkeepConfig.CheckData, upkeepConfig.TriggerConfig, + upkeepConfig.OffchainConfig, upkeepConfig.FundingAmount, + common.HexToAddress(a.ChainClient.GetDefaultWallet().Address())) + if err != nil { + return nil, errors.Join(err, fmt.Errorf("failed to pack registrar request")) + } + default: + return nil, fmt.Errorf("v2.0 and v2.1 are the only supported versions") + } + tx, err := a.LinkToken.TransferAndCall(a.Registrar.Address(), upkeepConfig.FundingAmount, registrationRequest) + if err != nil { + return nil, errors.Join(err, fmt.Errorf("failed to register upkeep")) + } + registrationTxHashes = append(registrationTxHashes, tx.Hash()) + } + return registrationTxHashes, nil +} + +func (a *AutomationTest) ConfirmUpkeepsRegistered(registrationTxHashes []common.Hash) ([]*big.Int, error) { + upkeepIds := make([]*big.Int, 0) + for _, txHash := range registrationTxHashes { + receipt, err := a.ChainClient.GetTxReceipt(txHash) + if err != nil { + return nil, errors.Join(err, fmt.Errorf("failed to confirm upkeep registration")) + } + var upkeepId *big.Int + for _, rawLog := range receipt.Logs { + parsedUpkeepId, err := a.Registry.ParseUpkeepIdFromRegisteredLog(rawLog) + if err == nil { + upkeepId = parsedUpkeepId + break + } + } + if upkeepId == nil { + return nil, fmt.Errorf("failed to parse upkeep id from registration receipt") + } + upkeepIds = append(upkeepIds, upkeepId) + } + a.UpkeepIDs = upkeepIds + return upkeepIds, nil +} + +func (a *AutomationTest) AddJobsAndSetConfig(t *testing.T) { + l := logging.GetTestLogger(t) + err := a.AddBootstrapJob() + require.NoError(t, err, "Error adding bootstrap job") + err = a.AddAutomationJobs() + require.NoError(t, err, "Error adding automation jobs") + + l.Debug(). + Interface("Plugin Config", a.PluginConfig). + Interface("Public Config", a.PublicConfig). + Interface("Registry Settings", a.RegistrySettings). + Interface("Registrar Settings", a.RegistrarSettings). + Msg("Configuring registry") + err = a.SetConfigOnRegistry() + require.NoError(t, err, "Error setting config on registry") + l.Info().Str("Registry Address", a.Registry.Address()).Msg("Successfully setConfig on registry") +} + +func (a *AutomationTest) SetupAutomationDeployment(t *testing.T) { + l := logging.GetTestLogger(t) + err := a.CollectNodeDetails() + require.NoError(t, err, "Error collecting node details") + l.Info().Msg("Collected Node Details") + l.Debug().Interface("Node Details", a.NodeDetails).Msg("Node Details") + + err = a.DeployLINK() + require.NoError(t, err, "Error deploying link token contract") + + err = a.DeployEthLinkFeed() + require.NoError(t, err, "Error deploying eth link feed contract") + err = a.DeployGasFeed() + require.NoError(t, err, "Error deploying gas feed contract") + + err = a.DeployTranscoder() + require.NoError(t, err, "Error deploying transcoder contract") + + err = a.DeployRegistry() + require.NoError(t, err, "Error deploying registry contract") + err = a.DeployRegistrar() + require.NoError(t, err, "Error deploying registrar contract") + + a.AddJobsAndSetConfig(t) +} + +func (a *AutomationTest) LoadAutomationDeployment(t *testing.T, linkTokenAddress, + ethLinkFeedAddress, gasFeedAddress, transcoderAddress, registryAddress, registrarAddress string) { + l := logging.GetTestLogger(t) + err := a.CollectNodeDetails() + require.NoError(t, err, "Error collecting node details") + l.Info().Msg("Collected Node Details") + l.Debug().Interface("Node Details", a.NodeDetails).Msg("Node Details") + + err = a.LoadLINK(linkTokenAddress) + require.NoError(t, err, "Error loading link token contract") + + err = a.LoadEthLinkFeed(ethLinkFeedAddress) + require.NoError(t, err, "Error loading eth link feed contract") + err = a.LoadEthGasFeed(gasFeedAddress) + require.NoError(t, err, "Error loading gas feed contract") + err = a.LoadTranscoder(transcoderAddress) + require.NoError(t, err, "Error loading transcoder contract") + err = a.LoadRegistry(registryAddress) + require.NoError(t, err, "Error loading registry contract") + err = a.LoadRegistrar(registrarAddress) + require.NoError(t, err, "Error loading registrar contract") + + a.AddJobsAndSetConfig(t) + +} diff --git a/integration-tests/actions/vrfv2_actions/vrfv2_config/config.go b/integration-tests/actions/vrfv2_actions/vrfv2_config/config.go new file mode 100644 index 00000000000..aa2ac7f59ea --- /dev/null +++ b/integration-tests/actions/vrfv2_actions/vrfv2_config/config.go @@ -0,0 +1,54 @@ +package vrfv2_config + +import "time" + +type VRFV2Config struct { + ChainlinkNodeFunding float64 `envconfig:"CHAINLINK_NODE_FUNDING" default:".1"` // Amount of native currency to fund each chainlink node with + CLNodeMaxGasPriceGWei int64 `envconfig:"MAX_GAS_PRICE_GWEI" default:"1000"` // Max gas price in GWei for the chainlink node + IsNativePayment bool `envconfig:"IS_NATIVE_PAYMENT" default:"false"` // Whether to use native payment or LINK token + LinkNativeFeedResponse int64 `envconfig:"LINK_NATIVE_FEED_RESPONSE" default:"1000000000000000000"` // Response of the LINK/ETH feed + MinimumConfirmations uint16 `envconfig:"MINIMUM_CONFIRMATIONS" default:"3"` // Minimum number of confirmations for the VRF Coordinator + SubscriptionFundingAmountLink float64 `envconfig:"SUBSCRIPTION_FUNDING_AMOUNT_LINK" default:"5"` // Amount of LINK to fund the subscription with + NumberOfWords uint32 `envconfig:"NUMBER_OF_WORDS" default:"3"` // Number of words to request + CallbackGasLimit uint32 `envconfig:"CALLBACK_GAS_LIMIT" default:"1000000"` // Gas limit for the callback + MaxGasLimitCoordinatorConfig uint32 `envconfig:"MAX_GAS_LIMIT_COORDINATOR_CONFIG" default:"2500000"` // Max gas limit for the VRF Coordinator config + FallbackWeiPerUnitLink int64 `envconfig:"FALLBACK_WEI_PER_UNIT_LINK" default:"60000000000000000"` // Fallback wei per unit LINK for the VRF Coordinator config + StalenessSeconds uint32 `envconfig:"STALENESS_SECONDS" default:"86400"` // Staleness in seconds for the VRF Coordinator config + GasAfterPaymentCalculation uint32 `envconfig:"GAS_AFTER_PAYMENT_CALCULATION" default:"33825"` // Gas after payment calculation for the VRF Coordinator config + FulfillmentFlatFeeLinkPPMTier1 uint32 `envconfig:"FULFILLMENT_FLAT_FEE_LINK_PPM_TIER_1" default:"500"` + FulfillmentFlatFeeLinkPPMTier2 uint32 `envconfig:"FULFILLMENT_FLAT_FEE_LINK_PPM_TIER_2" default:"500"` + FulfillmentFlatFeeLinkPPMTier3 uint32 `envconfig:"FULFILLMENT_FLAT_FEE_LINK_PPM_TIER_3" default:"500"` + FulfillmentFlatFeeLinkPPMTier4 uint32 `envconfig:"FULFILLMENT_FLAT_FEE_LINK_PPM_TIER_4" default:"500"` + FulfillmentFlatFeeLinkPPMTier5 uint32 `envconfig:"FULFILLMENT_FLAT_FEE_LINK_PPM_TIER_5" default:"500"` + ReqsForTier2 int64 `envconfig:"REQS_FOR_TIER_2" default:"0"` + ReqsForTier3 int64 `envconfig:"REQS_FOR_TIER_3" default:"0"` + ReqsForTier4 int64 `envconfig:"REQS_FOR_TIER_4" default:"0"` + ReqsForTier5 int64 `envconfig:"REQS_FOR_TIER_5" default:"0"` + + NumberOfSubToCreate int `envconfig:"NUMBER_OF_SUB_TO_CREATE" default:"1"` // Number of subscriptions to create + + RandomnessRequestCountPerRequest uint16 `envconfig:"RANDOMNESS_REQUEST_COUNT_PER_REQUEST" default:"1"` // How many randomness requests to send per request + RandomnessRequestCountPerRequestDeviation uint16 `envconfig:"RANDOMNESS_REQUEST_COUNT_PER_REQUEST_DEVIATION" default:"0"` // How many randomness requests to send per request + + RandomWordsFulfilledEventTimeout time.Duration `envconfig:"RANDOM_WORDS_FULFILLED_EVENT_TIMEOUT" default:"2m"` // How long to wait for the RandomWordsFulfilled event to be emitted + + //Wrapper Config + WrapperGasOverhead uint32 `envconfig:"WRAPPER_GAS_OVERHEAD" default:"50000"` + CoordinatorGasOverhead uint32 `envconfig:"COORDINATOR_GAS_OVERHEAD" default:"52000"` + WrapperPremiumPercentage uint8 `envconfig:"WRAPPER_PREMIUM_PERCENTAGE" default:"25"` + WrapperMaxNumberOfWords uint8 `envconfig:"WRAPPER_MAX_NUMBER_OF_WORDS" default:"10"` + WrapperConsumerFundingAmountNativeToken float64 `envconfig:"WRAPPER_CONSUMER_FUNDING_AMOUNT_NATIVE_TOKEN" default:"1"` + WrapperConsumerFundingAmountLink int64 `envconfig:"WRAPPER_CONSUMER_FUNDING_AMOUNT_LINK" default:"10"` + + //LOAD/SOAK Test Config + TestDuration time.Duration `envconfig:"TEST_DURATION" default:"3m"` // How long to run the test for + RPS int64 `envconfig:"RPS" default:"1"` // How many requests per second to send + RateLimitUnitDuration time.Duration `envconfig:"RATE_LIMIT_UNIT_DURATION" default:"1m"` + //Using existing environment and contracts + UseExistingEnv bool `envconfig:"USE_EXISTING_ENV" default:"false"` // Whether to use an existing environment or create a new one + CoordinatorAddress string `envconfig:"COORDINATOR_ADDRESS" default:""` // Coordinator address + ConsumerAddress string `envconfig:"CONSUMER_ADDRESS" default:""` // Consumer address + LinkAddress string `envconfig:"LINK_ADDRESS" default:""` // Link address + SubID uint64 `envconfig:"SUB_ID" default:""` // Subscription ID + KeyHash string `envconfig:"KEY_HASH" default:""` +} diff --git a/integration-tests/actions/vrfv2_actions/vrfv2_constants/constants.go b/integration-tests/actions/vrfv2_actions/vrfv2_constants/constants.go deleted file mode 100644 index 2ec993e4e6e..00000000000 --- a/integration-tests/actions/vrfv2_actions/vrfv2_constants/constants.go +++ /dev/null @@ -1,34 +0,0 @@ -package vrfv2_constants - -import ( - "math/big" - - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/generated/vrf_coordinator_v2" -) - -var ( - LinkEthFeedResponse = big.NewInt(1e18) - MinimumConfirmations = uint16(3) - RandomnessRequestCountPerRequest = uint16(1) - //todo - get Sub id when creating subscription - need to listen for SubscriptionCreated Log - SubID = uint64(1) - VRFSubscriptionFundingAmountLink = big.NewInt(100) - ChainlinkNodeFundingAmountEth = big.NewFloat(0.1) - NumberOfWords = uint32(3) - MaxGasPriceGWei = 1000 - CallbackGasLimit = uint32(1000000) - MaxGasLimitVRFCoordinatorConfig = uint32(2.5e6) - StalenessSeconds = uint32(86400) - GasAfterPaymentCalculation = uint32(33825) - - VRFCoordinatorV2FeeConfig = vrf_coordinator_v2.VRFCoordinatorV2FeeConfig{ - FulfillmentFlatFeeLinkPPMTier1: 500, - FulfillmentFlatFeeLinkPPMTier2: 500, - FulfillmentFlatFeeLinkPPMTier3: 500, - FulfillmentFlatFeeLinkPPMTier4: 500, - FulfillmentFlatFeeLinkPPMTier5: 500, - ReqsForTier2: big.NewInt(0), - ReqsForTier3: big.NewInt(0), - ReqsForTier4: big.NewInt(0), - ReqsForTier5: big.NewInt(0)} -) diff --git a/integration-tests/actions/vrfv2_actions/vrfv2_models.go b/integration-tests/actions/vrfv2_actions/vrfv2_models.go index 71d119e1dbb..0576d6f7d6e 100644 --- a/integration-tests/actions/vrfv2_actions/vrfv2_models.go +++ b/integration-tests/actions/vrfv2_actions/vrfv2_models.go @@ -18,7 +18,21 @@ type VRFV2JobInfo struct { } type VRFV2Contracts struct { - Coordinator contracts.VRFCoordinatorV2 - BHS contracts.BlockHashStore - LoadTestConsumer contracts.VRFv2LoadTestConsumer + Coordinator contracts.VRFCoordinatorV2 + BHS contracts.BlockHashStore + LoadTestConsumers []contracts.VRFv2LoadTestConsumer +} + +// VRFV2PlusKeyData defines a jobs into and proving key info +type VRFV2KeyData struct { + VRFKey *client.VRFKey + EncodedProvingKey VRFV2EncodedProvingKey + KeyHash [32]byte +} + +type VRFV2Data struct { + VRFV2KeyData + VRFJob *client.Job + PrimaryEthAddress string + ChainID *big.Int } diff --git a/integration-tests/actions/vrfv2_actions/vrfv2_steps.go b/integration-tests/actions/vrfv2_actions/vrfv2_steps.go index a832d020b0f..f565af26d93 100644 --- a/integration-tests/actions/vrfv2_actions/vrfv2_steps.go +++ b/integration-tests/actions/vrfv2_actions/vrfv2_steps.go @@ -4,6 +4,18 @@ import ( "context" "fmt" "math/big" + "sync" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/rs/zerolog" + + commonassets "github.com/smartcontractkit/chainlink-common/pkg/assets" + "github.com/smartcontractkit/chainlink-testing-framework/utils/conversions" + "github.com/smartcontractkit/chainlink/integration-tests/actions/vrfv2_actions/vrfv2_config" + "github.com/smartcontractkit/chainlink/integration-tests/docker/test_env" + "github.com/smartcontractkit/chainlink/integration-tests/types/config/node" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/generated/vrf_coordinator_v2" "github.com/google/uuid" @@ -11,27 +23,37 @@ import ( chainlinkutils "github.com/smartcontractkit/chainlink/v2/core/utils" "github.com/smartcontractkit/chainlink/integration-tests/actions" - vrfConst "github.com/smartcontractkit/chainlink/integration-tests/actions/vrfv2_actions/vrfv2_constants" "github.com/smartcontractkit/chainlink/integration-tests/client" "github.com/smartcontractkit/chainlink/integration-tests/contracts" - "github.com/smartcontractkit/chainlink/integration-tests/docker/test_env" - "github.com/smartcontractkit/chainlink/integration-tests/types/config/node" ) var ( - ErrNodePrimaryKey = "error getting node's primary ETH key" - ErrCreatingProvingKeyHash = "error creating a keyHash from the proving key" - ErrCreatingProvingKey = "error creating a keyHash from the proving key" - ErrRegisterProvingKey = "error registering proving keys" - ErrEncodingProvingKey = "error encoding proving key" - ErrCreatingVRFv2Key = "error creating VRFv2 key" - ErrDeployBlockHashStore = "error deploying blockhash store" - ErrDeployCoordinator = "error deploying VRFv2 CoordinatorV2" - ErrAdvancedConsumer = "error deploying VRFv2 Advanced Consumer" - ErrABIEncodingFunding = "error Abi encoding subscriptionID" - ErrSendingLinkToken = "error sending Link token" - ErrCreatingVRFv2Job = "error creating VRFv2 job" - ErrParseJob = "error parsing job definition" + ErrNodePrimaryKey = "error getting node's primary ETH key" + ErrCreatingProvingKeyHash = "error creating a keyHash from the proving key" + ErrRegisteringProvingKey = "error registering a proving key on Coordinator contract" + ErrRegisterProvingKey = "error registering proving keys" + ErrEncodingProvingKey = "error encoding proving key" + ErrCreatingVRFv2Key = "error creating VRFv2 key" + ErrDeployBlockHashStore = "error deploying blockhash store" + ErrDeployCoordinator = "error deploying VRF CoordinatorV2" + ErrAdvancedConsumer = "error deploying VRFv2 Advanced Consumer" + ErrABIEncodingFunding = "error Abi encoding subscriptionID" + ErrSendingLinkToken = "error sending Link token" + ErrCreatingVRFv2Job = "error creating VRFv2 job" + ErrParseJob = "error parsing job definition" + ErrDeployVRFV2Contracts = "error deploying VRFV2 contracts" + ErrSetVRFCoordinatorConfig = "error setting config for VRF Coordinator contract" + ErrCreateVRFSubscription = "error creating VRF Subscription" + ErrAddConsumerToSub = "error adding consumer to VRF Subscription" + ErrFundSubWithLinkToken = "error funding subscription with Link tokens" + ErrCreateVRFV2Jobs = "error creating VRF V2 Jobs" + ErrGetPrimaryKey = "error getting primary ETH key address" + ErrRestartCLNode = "error restarting CL node" + ErrWaitTXsComplete = "error waiting for TXs to complete" + ErrRequestRandomness = "error requesting randomness" + + ErrWaitRandomWordsRequestedEvent = "error waiting for RandomWordsRequested event" + ErrWaitRandomWordsFulfilledEvent = "error waiting for RandomWordsFulfilled event" ) func DeployVRFV2Contracts( @@ -39,82 +61,80 @@ func DeployVRFV2Contracts( chainClient blockchain.EVMClient, linkTokenContract contracts.LinkToken, linkEthFeedContract contracts.MockETHLINKFeed, + consumerContractsAmount int, ) (*VRFV2Contracts, error) { bhs, err := contractDeployer.DeployBlockhashStore() if err != nil { return nil, fmt.Errorf("%s, err %w", ErrDeployBlockHashStore, err) } + err = chainClient.WaitForEvents() + if err != nil { + return nil, fmt.Errorf("%s, err %w", ErrWaitTXsComplete, err) + } coordinator, err := contractDeployer.DeployVRFCoordinatorV2(linkTokenContract.Address(), bhs.Address(), linkEthFeedContract.Address()) if err != nil { return nil, fmt.Errorf("%s, err %w", ErrDeployCoordinator, err) } - loadTestConsumer, err := contractDeployer.DeployVRFv2LoadTestConsumer(coordinator.Address()) + err = chainClient.WaitForEvents() if err != nil { - return nil, fmt.Errorf("%s, err %w", ErrAdvancedConsumer, err) + return nil, fmt.Errorf("%s, err %w", ErrWaitTXsComplete, err) } - err = chainClient.WaitForEvents() + consumers, err := DeployVRFV2Consumers(contractDeployer, coordinator, consumerContractsAmount) if err != nil { return nil, err } - return &VRFV2Contracts{coordinator, bhs, loadTestConsumer}, nil + err = chainClient.WaitForEvents() + if err != nil { + return nil, fmt.Errorf("%s, err %w", ErrWaitTXsComplete, err) + } + return &VRFV2Contracts{coordinator, bhs, consumers}, nil } -func CreateVRFV2Jobs( - chainlinkNodes []*client.ChainlinkClient, - coordinator contracts.VRFCoordinatorV2, - c blockchain.EVMClient, - minIncomingConfirmations uint16, -) ([]VRFV2JobInfo, error) { - jobInfo := make([]VRFV2JobInfo, 0) - for _, chainlinkNode := range chainlinkNodes { - vrfKey, err := chainlinkNode.MustCreateVRFKey() +func DeployVRFV2Consumers(contractDeployer contracts.ContractDeployer, coordinator contracts.VRFCoordinatorV2, consumerContractsAmount int) ([]contracts.VRFv2LoadTestConsumer, error) { + var consumers []contracts.VRFv2LoadTestConsumer + for i := 1; i <= consumerContractsAmount; i++ { + loadTestConsumer, err := contractDeployer.DeployVRFv2LoadTestConsumer(coordinator.Address()) if err != nil { - return nil, fmt.Errorf("%s, err %w", ErrCreatingVRFv2Key, err) - } - pubKeyCompressed := vrfKey.Data.ID - jobUUID := uuid.New() - os := &client.VRFV2TxPipelineSpec{ - Address: coordinator.Address(), + return nil, fmt.Errorf("%s, err %w", ErrAdvancedConsumer, err) } - ost, err := os.String() - if err != nil { - return nil, fmt.Errorf("%s, err %w", ErrParseJob, err) - } - nativeTokenPrimaryKeyAddress, err := chainlinkNode.PrimaryEthAddress() - if err != nil { - return nil, fmt.Errorf("%s, err %w", ErrNodePrimaryKey, err) - } - job, err := chainlinkNode.MustCreateJob(&client.VRFV2JobSpec{ - Name: fmt.Sprintf("vrf-%s", jobUUID), - CoordinatorAddress: coordinator.Address(), - FromAddresses: []string{nativeTokenPrimaryKeyAddress}, - EVMChainID: c.GetChainID().String(), - MinIncomingConfirmations: int(minIncomingConfirmations), - PublicKey: pubKeyCompressed, - ExternalJobID: jobUUID.String(), - ObservationSource: ost, - BatchFulfillmentEnabled: false, - }) - if err != nil { - return nil, fmt.Errorf("%s, err %w", ErrCreatingVRFv2Job, err) - } - provingKey, err := VRFV2RegisterProvingKey(vrfKey, nativeTokenPrimaryKeyAddress, coordinator) - if err != nil { - return nil, fmt.Errorf("%s, err %w", ErrCreatingProvingKey, err) - } - keyHash, err := coordinator.HashOfKey(context.Background(), provingKey) - if err != nil { - return nil, fmt.Errorf("%s, err %w", ErrCreatingProvingKeyHash, err) - } - ji := VRFV2JobInfo{ - Job: job, - VRFKey: vrfKey, - EncodedProvingKey: provingKey, - KeyHash: keyHash, - } - jobInfo = append(jobInfo, ji) + consumers = append(consumers, loadTestConsumer) + } + return consumers, nil +} + +func CreateVRFV2Job( + chainlinkNode *client.ChainlinkClient, + coordinatorAddress string, + nativeTokenPrimaryKeyAddress string, + pubKeyCompressed string, + chainID string, + minIncomingConfirmations uint16, +) (*client.Job, error) { + jobUUID := uuid.New() + os := &client.VRFV2TxPipelineSpec{ + Address: coordinatorAddress, + } + ost, err := os.String() + if err != nil { + return nil, fmt.Errorf("%s, err %w", ErrParseJob, err) + } + + job, err := chainlinkNode.MustCreateJob(&client.VRFV2JobSpec{ + Name: fmt.Sprintf("vrf-v2-%s", jobUUID), + CoordinatorAddress: coordinatorAddress, + FromAddresses: []string{nativeTokenPrimaryKeyAddress}, + EVMChainID: chainID, + MinIncomingConfirmations: int(minIncomingConfirmations), + PublicKey: pubKeyCompressed, + ExternalJobID: jobUUID.String(), + ObservationSource: ost, + BatchFulfillmentEnabled: false, + }) + if err != nil { + return nil, fmt.Errorf("%s, err %w", ErrCreatingVRFv2Job, err) } - return jobInfo, nil + + return job, nil } func VRFV2RegisterProvingKey( @@ -136,98 +156,452 @@ func VRFV2RegisterProvingKey( return provingKey, nil } -func FundVRFCoordinatorV2Subscription(linkToken contracts.LinkToken, coordinator contracts.VRFCoordinatorV2, chainClient blockchain.EVMClient, subscriptionID uint64, linkFundingAmount *big.Int) error { +func FundVRFCoordinatorV2Subscription( + linkToken contracts.LinkToken, + coordinator contracts.VRFCoordinatorV2, + chainClient blockchain.EVMClient, + subscriptionID uint64, + linkFundingAmountJuels *big.Int, +) error { encodedSubId, err := chainlinkutils.ABIEncode(`[{"type":"uint64"}]`, subscriptionID) if err != nil { return fmt.Errorf("%s, err %w", ErrABIEncodingFunding, err) } - _, err = linkToken.TransferAndCall(coordinator.Address(), big.NewInt(0).Mul(linkFundingAmount, big.NewInt(1e18)), encodedSubId) + _, err = linkToken.TransferAndCall(coordinator.Address(), linkFundingAmountJuels, encodedSubId) if err != nil { return fmt.Errorf("%s, err %w", ErrSendingLinkToken, err) } return chainClient.WaitForEvents() } -/* setup for load tests */ +// SetupVRFV2Environment will create specified number of subscriptions and add the same conumer/s to each of them +func SetupVRFV2Environment( + env *test_env.CLClusterTestEnv, + vrfv2Config vrfv2_config.VRFV2Config, + linkToken contracts.LinkToken, + mockNativeLINKFeed contracts.MockETHLINKFeed, + registerProvingKeyAgainstAddress string, + numberOfConsumers int, + numberOfSubToCreate int, + l zerolog.Logger, +) (*VRFV2Contracts, []uint64, *VRFV2Data, error) { + l.Info().Msg("Starting VRFV2 environment setup") + l.Info().Msg("Deploying VRFV2 contracts") + vrfv2Contracts, err := DeployVRFV2Contracts( + env.ContractDeployer, + env.EVMClient, + linkToken, + mockNativeLINKFeed, + numberOfConsumers, + ) + if err != nil { + return nil, nil, nil, fmt.Errorf("%s, err %w", ErrDeployVRFV2Contracts, err) + } + vrfCoordinatorV2FeeConfig := vrf_coordinator_v2.VRFCoordinatorV2FeeConfig{ + FulfillmentFlatFeeLinkPPMTier1: vrfv2Config.FulfillmentFlatFeeLinkPPMTier1, + FulfillmentFlatFeeLinkPPMTier2: vrfv2Config.FulfillmentFlatFeeLinkPPMTier2, + FulfillmentFlatFeeLinkPPMTier3: vrfv2Config.FulfillmentFlatFeeLinkPPMTier3, + FulfillmentFlatFeeLinkPPMTier4: vrfv2Config.FulfillmentFlatFeeLinkPPMTier4, + FulfillmentFlatFeeLinkPPMTier5: vrfv2Config.FulfillmentFlatFeeLinkPPMTier5, + ReqsForTier2: big.NewInt(vrfv2Config.ReqsForTier2), + ReqsForTier3: big.NewInt(vrfv2Config.ReqsForTier3), + ReqsForTier4: big.NewInt(vrfv2Config.ReqsForTier4), + ReqsForTier5: big.NewInt(vrfv2Config.ReqsForTier5)} -func SetupLocalLoadTestEnv(nodesFunding *big.Float, subFundingLINK *big.Int) (*test_env.CLClusterTestEnv, *VRFV2Contracts, [32]byte, error) { - env, err := test_env.NewCLTestEnvBuilder(). - WithGeth(). - WithLogWatcher(). - WithMockAdapter(). - WithCLNodes(1). - WithFunding(nodesFunding). - WithLogWatcher(). - Build() + l.Info().Str("Coordinator", vrfv2Contracts.Coordinator.Address()).Msg("Setting Coordinator Config") + err = vrfv2Contracts.Coordinator.SetConfig( + vrfv2Config.MinimumConfirmations, + vrfv2Config.CallbackGasLimit, + vrfv2Config.StalenessSeconds, + vrfv2Config.GasAfterPaymentCalculation, + big.NewInt(vrfv2Config.LinkNativeFeedResponse), + vrfCoordinatorV2FeeConfig, + ) + if err != nil { + return nil, nil, nil, fmt.Errorf("%s, err %w", ErrSetVRFCoordinatorConfig, err) + } + err = env.EVMClient.WaitForEvents() + if err != nil { + return nil, nil, nil, fmt.Errorf("%s, err %w", ErrWaitTXsComplete, err) + } + l.Info(). + Str("Coordinator", vrfv2Contracts.Coordinator.Address()). + Int("Number of Subs to create", numberOfSubToCreate). + Msg("Creating and funding subscriptions, adding consumers") + subIDs, err := CreateFundSubsAndAddConsumers( + env, + vrfv2Config, + linkToken, + vrfv2Contracts.Coordinator, vrfv2Contracts.LoadTestConsumers, numberOfSubToCreate) if err != nil { - return nil, nil, [32]byte{}, err + return nil, nil, nil, err } - env.ParallelTransactions(true) + l.Info().Str("Node URL", env.ClCluster.NodeAPIs()[0].URL()).Msg("Creating VRF Key on the Node") + vrfKey, err := env.ClCluster.NodeAPIs()[0].MustCreateVRFKey() + if err != nil { + return nil, nil, nil, fmt.Errorf("%s, err %w", ErrCreatingVRFv2Key, err) + } + pubKeyCompressed := vrfKey.Data.ID - mockFeed, err := actions.DeployMockETHLinkFeed(env.ContractDeployer, vrfConst.LinkEthFeedResponse) + l.Info().Str("Coordinator", vrfv2Contracts.Coordinator.Address()).Msg("Registering Proving Key") + provingKey, err := VRFV2RegisterProvingKey(vrfKey, registerProvingKeyAgainstAddress, vrfv2Contracts.Coordinator) if err != nil { - return nil, nil, [32]byte{}, err + return nil, nil, nil, fmt.Errorf("%s, err %w", ErrRegisteringProvingKey, err) } - lt, err := actions.DeployLINKToken(env.ContractDeployer) + keyHash, err := vrfv2Contracts.Coordinator.HashOfKey(context.Background(), provingKey) if err != nil { - return nil, nil, [32]byte{}, err + return nil, nil, nil, fmt.Errorf("%s, err %w", ErrCreatingProvingKeyHash, err) } - vrfv2Contracts, err := DeployVRFV2Contracts(env.ContractDeployer, env.EVMClient, lt, mockFeed) + + chainID := env.EVMClient.GetChainID() + + nativeTokenPrimaryKeyAddress, err := env.ClCluster.NodeAPIs()[0].PrimaryEthAddress() if err != nil { - return nil, nil, [32]byte{}, err + return nil, nil, nil, fmt.Errorf("%s, err %w", ErrNodePrimaryKey, err) } - err = env.EVMClient.WaitForEvents() + + l.Info().Msg("Creating VRFV2 Job") + vrfV2job, err := CreateVRFV2Job( + env.ClCluster.NodeAPIs()[0], + vrfv2Contracts.Coordinator.Address(), + nativeTokenPrimaryKeyAddress, + pubKeyCompressed, + chainID.String(), + vrfv2Config.MinimumConfirmations, + ) if err != nil { - return nil, nil, [32]byte{}, err + return nil, nil, nil, fmt.Errorf("%s, err %w", ErrCreateVRFV2Jobs, err) } - err = vrfv2Contracts.Coordinator.SetConfig( - vrfConst.MinimumConfirmations, - vrfConst.MaxGasLimitVRFCoordinatorConfig, - vrfConst.StalenessSeconds, - vrfConst.GasAfterPaymentCalculation, - vrfConst.LinkEthFeedResponse, - vrfConst.VRFCoordinatorV2FeeConfig, + + // this part is here because VRFv2 can work with only a specific key + // [[EVM.KeySpecific]] + // Key = '...' + addr, err := env.ClCluster.Nodes[0].API.PrimaryEthAddress() + if err != nil { + return nil, nil, nil, fmt.Errorf("%s, err %w", ErrGetPrimaryKey, err) + } + nodeConfig := node.NewConfig(env.ClCluster.Nodes[0].NodeConfig, + node.WithVRFv2EVMEstimator(addr, vrfv2Config.CLNodeMaxGasPriceGWei), ) + l.Info().Msg("Restarting Node with new sending key PriceMax configuration") + err = env.ClCluster.Nodes[0].Restart(nodeConfig) + if err != nil { + return nil, nil, nil, fmt.Errorf("%s, err %w", ErrRestartCLNode, err) + } + + vrfv2KeyData := VRFV2KeyData{ + VRFKey: vrfKey, + EncodedProvingKey: provingKey, + KeyHash: keyHash, + } + + data := VRFV2Data{ + vrfv2KeyData, + vrfV2job, + nativeTokenPrimaryKeyAddress, + chainID, + } + + l.Info().Msg("VRFV2 environment setup is finished") + return vrfv2Contracts, subIDs, &data, nil +} + +func CreateFundSubsAndAddConsumers( + env *test_env.CLClusterTestEnv, + vrfv2Config vrfv2_config.VRFV2Config, + linkToken contracts.LinkToken, + coordinator contracts.VRFCoordinatorV2, + consumers []contracts.VRFv2LoadTestConsumer, + numberOfSubToCreate int, +) ([]uint64, error) { + subIDs, err := CreateSubsAndFund(env, vrfv2Config, linkToken, coordinator, numberOfSubToCreate) if err != nil { - return nil, nil, [32]byte{}, err + return nil, err + } + subToConsumersMap := map[uint64][]contracts.VRFv2LoadTestConsumer{} + + //each subscription will have the same consumers + for _, subID := range subIDs { + subToConsumersMap[subID] = consumers } + + err = AddConsumersToSubs( + subToConsumersMap, + coordinator, + ) + if err != nil { + return nil, err + } + err = env.EVMClient.WaitForEvents() if err != nil { - return nil, nil, [32]byte{}, err + return nil, fmt.Errorf("%s, err %w", ErrWaitTXsComplete, err) } - err = vrfv2Contracts.Coordinator.CreateSubscription() + return subIDs, nil +} + +func CreateSubsAndFund( + env *test_env.CLClusterTestEnv, + vrfv2Config vrfv2_config.VRFV2Config, + linkToken contracts.LinkToken, + coordinator contracts.VRFCoordinatorV2, + subAmountToCreate int, +) ([]uint64, error) { + subs, err := CreateSubs(env, coordinator, subAmountToCreate) if err != nil { - return nil, nil, [32]byte{}, err + return nil, err } err = env.EVMClient.WaitForEvents() if err != nil { - return nil, nil, [32]byte{}, err + return nil, fmt.Errorf("%s, err %w", ErrWaitTXsComplete, err) } - err = vrfv2Contracts.Coordinator.AddConsumer(vrfConst.SubID, vrfv2Contracts.LoadTestConsumer.Address()) + err = FundSubscriptions(env, vrfv2Config, linkToken, coordinator, subs) if err != nil { - return nil, nil, [32]byte{}, err + return nil, err + } + return subs, nil +} + +func CreateSubs( + env *test_env.CLClusterTestEnv, + coordinator contracts.VRFCoordinatorV2, + subAmountToCreate int, +) ([]uint64, error) { + var subIDArr []uint64 + + for i := 0; i < subAmountToCreate; i++ { + subID, err := CreateSubAndFindSubID(env, coordinator) + if err != nil { + return nil, err + } + subIDArr = append(subIDArr, subID) + } + return subIDArr, nil +} + +func AddConsumersToSubs( + subToConsumerMap map[uint64][]contracts.VRFv2LoadTestConsumer, + coordinator contracts.VRFCoordinatorV2, +) error { + for subID, consumers := range subToConsumerMap { + for _, consumer := range consumers { + err := coordinator.AddConsumer(subID, consumer.Address()) + if err != nil { + return fmt.Errorf("%s, err %w", ErrAddConsumerToSub, err) + } + } } - err = FundVRFCoordinatorV2Subscription(lt, vrfv2Contracts.Coordinator, env.EVMClient, vrfConst.SubID, subFundingLINK) + return nil +} + +func CreateSubAndFindSubID(env *test_env.CLClusterTestEnv, coordinator contracts.VRFCoordinatorV2) (uint64, error) { + tx, err := coordinator.CreateSubscription() if err != nil { - return nil, nil, [32]byte{}, err + return 0, fmt.Errorf("%s, err %w", ErrCreateVRFSubscription, err) } - jobs, err := CreateVRFV2Jobs(env.ClCluster.NodeAPIs(), vrfv2Contracts.Coordinator, env.EVMClient, vrfConst.MinimumConfirmations) + err = env.EVMClient.WaitForEvents() if err != nil { - return nil, nil, [32]byte{}, err + return 0, fmt.Errorf("%s, err %w", ErrWaitTXsComplete, err) } - // this part is here because VRFv2 can work with only a specific key - // [[EVM.KeySpecific]] - // Key = '...' - addr, err := env.ClCluster.Nodes[0].API.PrimaryEthAddress() + + receipt, err := env.EVMClient.GetTxReceipt(tx.Hash()) if err != nil { - return nil, nil, [32]byte{}, err + return 0, fmt.Errorf("%s, err %w", ErrWaitTXsComplete, err) } - nodeConfig := node.NewConfig(env.ClCluster.Nodes[0].NodeConfig, - node.WithVRFv2EVMEstimator(addr), + + //SubscriptionsCreated Log should be emitted with the subscription ID + subID := receipt.Logs[0].Topics[1].Big().Uint64() + + return subID, nil +} + +func FundSubscriptions( + env *test_env.CLClusterTestEnv, + vrfv2Config vrfv2_config.VRFV2Config, + linkAddress contracts.LinkToken, + coordinator contracts.VRFCoordinatorV2, + subIDs []uint64, +) error { + for _, subID := range subIDs { + //Link Billing + amountJuels := conversions.EtherToWei(big.NewFloat(vrfv2Config.SubscriptionFundingAmountLink)) + err := FundVRFCoordinatorV2Subscription(linkAddress, coordinator, env.EVMClient, subID, amountJuels) + if err != nil { + return fmt.Errorf("%s, err %w", ErrFundSubWithLinkToken, err) + } + } + err := env.EVMClient.WaitForEvents() + if err != nil { + return fmt.Errorf("%s, err %w", ErrWaitTXsComplete, err) + } + return nil +} + +func RequestRandomnessAndWaitForFulfillment( + consumer contracts.VRFv2LoadTestConsumer, + coordinator contracts.VRFCoordinatorV2, + vrfv2Data *VRFV2Data, + subID uint64, + randomnessRequestCountPerRequest uint16, + vrfv2Config vrfv2_config.VRFV2Config, + randomWordsFulfilledEventTimeout time.Duration, + l zerolog.Logger, +) (*vrf_coordinator_v2.VRFCoordinatorV2RandomWordsFulfilled, error) { + logRandRequest(consumer.Address(), coordinator.Address(), subID, vrfv2Config, l) + err := consumer.RequestRandomness( + vrfv2Data.KeyHash, + subID, + vrfv2Config.MinimumConfirmations, + vrfv2Config.CallbackGasLimit, + vrfv2Config.NumberOfWords, + randomnessRequestCountPerRequest, ) - err = env.ClCluster.Nodes[0].Restart(nodeConfig) if err != nil { - return nil, nil, [32]byte{}, err + return nil, fmt.Errorf("%s, err %w", ErrRequestRandomness, err) + } + + return WaitForRequestAndFulfillmentEvents( + consumer.Address(), + coordinator, + vrfv2Data, + subID, + randomWordsFulfilledEventTimeout, + l, + ) +} + +func WaitForRequestAndFulfillmentEvents( + consumerAddress string, + coordinator contracts.VRFCoordinatorV2, + vrfv2Data *VRFV2Data, + subID uint64, + randomWordsFulfilledEventTimeout time.Duration, + l zerolog.Logger, +) (*vrf_coordinator_v2.VRFCoordinatorV2RandomWordsFulfilled, error) { + randomWordsRequestedEvent, err := coordinator.WaitForRandomWordsRequestedEvent( + [][32]byte{vrfv2Data.KeyHash}, + []uint64{subID}, + []common.Address{common.HexToAddress(consumerAddress)}, + time.Minute*1, + ) + if err != nil { + return nil, fmt.Errorf("%s, err %w", ErrWaitRandomWordsRequestedEvent, err) + } + + LogRandomnessRequestedEvent(l, coordinator, randomWordsRequestedEvent) + + randomWordsFulfilledEvent, err := coordinator.WaitForRandomWordsFulfilledEvent( + []*big.Int{randomWordsRequestedEvent.RequestId}, + randomWordsFulfilledEventTimeout, + ) + if err != nil { + return nil, fmt.Errorf("%s, err %w", ErrWaitRandomWordsFulfilledEvent, err) + } + + LogRandomWordsFulfilledEvent(l, coordinator, randomWordsFulfilledEvent) + return randomWordsFulfilledEvent, err +} + +func WaitForRequestCountEqualToFulfilmentCount(consumer contracts.VRFv2LoadTestConsumer, timeout time.Duration, wg *sync.WaitGroup) (*big.Int, *big.Int, error) { + metricsChannel := make(chan *contracts.VRFLoadTestMetrics) + metricsErrorChannel := make(chan error) + + testContext, testCancel := context.WithTimeout(context.Background(), timeout) + defer testCancel() + + ticker := time.NewTicker(time.Second * 1) + var metrics *contracts.VRFLoadTestMetrics + for { + select { + case <-testContext.Done(): + ticker.Stop() + wg.Done() + return metrics.RequestCount, metrics.FulfilmentCount, + fmt.Errorf("timeout waiting for rand request and fulfilments to be equal AFTER performance test was executed. Request Count: %d, Fulfilment Count: %d", + metrics.RequestCount.Uint64(), metrics.FulfilmentCount.Uint64()) + case <-ticker.C: + go retrieveLoadTestMetrics(consumer, metricsChannel, metricsErrorChannel) + case metrics = <-metricsChannel: + if metrics.RequestCount.Cmp(metrics.FulfilmentCount) == 0 { + ticker.Stop() + wg.Done() + return metrics.RequestCount, metrics.FulfilmentCount, nil + } + case err := <-metricsErrorChannel: + ticker.Stop() + wg.Done() + return nil, nil, err + } + } +} + +func retrieveLoadTestMetrics( + consumer contracts.VRFv2LoadTestConsumer, + metricsChannel chan *contracts.VRFLoadTestMetrics, + metricsErrorChannel chan error, +) { + metrics, err := consumer.GetLoadTestMetrics(context.Background()) + if err != nil { + metricsErrorChannel <- err } - return env, vrfv2Contracts, jobs[0].KeyHash, nil + metricsChannel <- metrics +} + +func LogSubDetails(l zerolog.Logger, subscription vrf_coordinator_v2.GetSubscription, subID uint64, coordinator contracts.VRFCoordinatorV2) { + l.Debug(). + Str("Coordinator", coordinator.Address()). + Str("Link Balance", (*commonassets.Link)(subscription.Balance).Link()). + Uint64("Subscription ID", subID). + Str("Subscription Owner", subscription.Owner.String()). + Interface("Subscription Consumers", subscription.Consumers). + Msg("Subscription Data") +} + +func LogRandomnessRequestedEvent( + l zerolog.Logger, + coordinator contracts.VRFCoordinatorV2, + randomWordsRequestedEvent *vrf_coordinator_v2.VRFCoordinatorV2RandomWordsRequested, +) { + l.Debug(). + Str("Coordinator", coordinator.Address()). + Str("Request ID", randomWordsRequestedEvent.RequestId.String()). + Uint64("Subscription ID", randomWordsRequestedEvent.SubId). + Str("Sender Address", randomWordsRequestedEvent.Sender.String()). + Interface("Keyhash", randomWordsRequestedEvent.KeyHash). + Uint32("Callback Gas Limit", randomWordsRequestedEvent.CallbackGasLimit). + Uint32("Number of Words", randomWordsRequestedEvent.NumWords). + Uint16("Minimum Request Confirmations", randomWordsRequestedEvent.MinimumRequestConfirmations). + Msg("RandomnessRequested Event") +} + +func LogRandomWordsFulfilledEvent( + l zerolog.Logger, + coordinator contracts.VRFCoordinatorV2, + randomWordsFulfilledEvent *vrf_coordinator_v2.VRFCoordinatorV2RandomWordsFulfilled, +) { + l.Debug(). + Str("Coordinator", coordinator.Address()). + Str("Total Payment", randomWordsFulfilledEvent.Payment.String()). + Str("TX Hash", randomWordsFulfilledEvent.Raw.TxHash.String()). + Str("Request ID", randomWordsFulfilledEvent.RequestId.String()). + Bool("Success", randomWordsFulfilledEvent.Success). + Msg("RandomWordsFulfilled Event (TX metadata)") +} + +func logRandRequest( + consumer string, + coordinator string, + subID uint64, + vrfv2Config vrfv2_config.VRFV2Config, + l zerolog.Logger, +) { + l.Debug(). + Str("Consumer", consumer). + Str("Coordinator", coordinator). + Uint64("SubID", subID). + Uint16("MinimumConfirmations", vrfv2Config.MinimumConfirmations). + Uint32("CallbackGasLimit", vrfv2Config.CallbackGasLimit). + Uint16("RandomnessRequestCountPerRequest", vrfv2Config.RandomnessRequestCountPerRequest). + Uint16("RandomnessRequestCountPerRequestDeviation", vrfv2Config.RandomnessRequestCountPerRequestDeviation). + Msg("Requesting randomness") } diff --git a/integration-tests/actions/vrfv2plus/vrfv2plus_config/config.go b/integration-tests/actions/vrfv2plus/vrfv2plus_config/config.go index a47103a8a18..8cd6e0a8ce8 100644 --- a/integration-tests/actions/vrfv2plus/vrfv2plus_config/config.go +++ b/integration-tests/actions/vrfv2plus/vrfv2plus_config/config.go @@ -4,6 +4,7 @@ import "time" type VRFV2PlusConfig struct { ChainlinkNodeFunding float64 `envconfig:"CHAINLINK_NODE_FUNDING" default:".1"` // Amount of native currency to fund each chainlink node with + CLNodeMaxGasPriceGWei int64 `envconfig:"MAX_GAS_PRICE_GWEI" default:"1000"` // Max gas price in GWei for the chainlink node IsNativePayment bool `envconfig:"IS_NATIVE_PAYMENT" default:"false"` // Whether to use native payment or LINK token LinkNativeFeedResponse int64 `envconfig:"LINK_NATIVE_FEED_RESPONSE" default:"1000000000000000000"` // Response of the LINK/ETH feed MinimumConfirmations uint16 `envconfig:"MINIMUM_CONFIRMATIONS" default:"3"` // Minimum number of confirmations for the VRF Coordinator diff --git a/integration-tests/actions/vrfv2plus/vrfv2plus_steps.go b/integration-tests/actions/vrfv2plus/vrfv2plus_steps.go index fd15d51af4e..d0a33948e31 100644 --- a/integration-tests/actions/vrfv2plus/vrfv2plus_steps.go +++ b/integration-tests/actions/vrfv2plus/vrfv2plus_steps.go @@ -46,7 +46,6 @@ var ( ErrDeployVRFV2_5Contracts = "error deploying VRFV2_5 contracts" ErrSetVRFCoordinatorConfig = "error setting config for VRF Coordinator contract" ErrCreateVRFSubscription = "error creating VRF Subscription" - ErrFindSubID = "error finding created subscription ID" ErrAddConsumerToSub = "error adding consumer to VRF Subscription" ErrFundSubWithNativeToken = "error funding subscription with native token" ErrSetLinkNativeLinkFeed = "error setting Link and ETH/LINK feed for VRF Coordinator contract" @@ -98,35 +97,6 @@ func DeployVRFV2_5Contracts( return &VRFV2_5Contracts{coordinator, bhs, consumers}, nil } -func DeployVRFV2PlusDirectFundingContracts( - contractDeployer contracts.ContractDeployer, - chainClient blockchain.EVMClient, - linkTokenAddress string, - linkEthFeedAddress string, - coordinator contracts.VRFCoordinatorV2_5, - consumerContractsAmount int, -) (*VRFV2PlusWrapperContracts, error) { - - vrfv2PlusWrapper, err := contractDeployer.DeployVRFV2PlusWrapper(linkTokenAddress, linkEthFeedAddress, coordinator.Address()) - if err != nil { - return nil, fmt.Errorf("%s, err %w", ErrDeployWrapper, err) - } - err = chainClient.WaitForEvents() - if err != nil { - return nil, fmt.Errorf("%s, err %w", ErrWaitTXsComplete, err) - } - - consumers, err := DeployVRFV2PlusWrapperConsumers(contractDeployer, linkTokenAddress, vrfv2PlusWrapper, consumerContractsAmount) - if err != nil { - return nil, err - } - err = chainClient.WaitForEvents() - if err != nil { - return nil, fmt.Errorf("%s, err %w", ErrWaitTXsComplete, err) - } - return &VRFV2PlusWrapperContracts{vrfv2PlusWrapper, consumers}, nil -} - func DeployVRFV2PlusConsumers(contractDeployer contracts.ContractDeployer, coordinator contracts.VRFCoordinatorV2_5, consumerContractsAmount int) ([]contracts.VRFv2PlusLoadTestConsumer, error) { var consumers []contracts.VRFv2PlusLoadTestConsumer for i := 1; i <= consumerContractsAmount; i++ { @@ -139,18 +109,6 @@ func DeployVRFV2PlusConsumers(contractDeployer contracts.ContractDeployer, coord return consumers, nil } -func DeployVRFV2PlusWrapperConsumers(contractDeployer contracts.ContractDeployer, linkTokenAddress string, vrfV2PlusWrapper contracts.VRFV2PlusWrapper, consumerContractsAmount int) ([]contracts.VRFv2PlusWrapperLoadTestConsumer, error) { - var consumers []contracts.VRFv2PlusWrapperLoadTestConsumer - for i := 1; i <= consumerContractsAmount; i++ { - loadTestConsumer, err := contractDeployer.DeployVRFV2PlusWrapperLoadTestConsumer(linkTokenAddress, vrfV2PlusWrapper.Address()) - if err != nil { - return nil, fmt.Errorf("%s, err %w", ErrAdvancedConsumer, err) - } - consumers = append(consumers, loadTestConsumer) - } - return consumers, nil -} - func CreateVRFV2PlusJob( chainlinkNode *client.ChainlinkClient, coordinatorAddress string, @@ -285,7 +243,10 @@ func SetupVRFV2_5Environment( if err != nil { return nil, nil, nil, fmt.Errorf("%s, err %w", ErrWaitTXsComplete, err) } - l.Info().Str("Coordinator", vrfv2_5Contracts.Coordinator.Address()).Int("Number of Subs to create", numberOfSubToCreate).Msg("Creating and funding subscriptions, adding consumers") + l.Info(). + Str("Coordinator", vrfv2_5Contracts.Coordinator.Address()). + Int("Number of Subs to create", numberOfSubToCreate). + Msg("Creating and funding subscriptions, adding consumers") subIDs, err := CreateFundSubsAndAddConsumers( env, vrfv2PlusConfig, @@ -339,7 +300,7 @@ func SetupVRFV2_5Environment( return nil, nil, nil, fmt.Errorf("%s, err %w", ErrGetPrimaryKey, err) } nodeConfig := node.NewConfig(env.ClCluster.Nodes[0].NodeConfig, - node.WithVRFv2EVMEstimator(addr), + node.WithVRFv2EVMEstimator(addr, vrfv2PlusConfig.CLNodeMaxGasPriceGWei), ) l.Info().Msg("Restarting Node with new sending key PriceMax configuration") err = env.ClCluster.Nodes[0].Restart(nodeConfig) @@ -452,93 +413,6 @@ func AddConsumersToSubs( return nil } -func SetupVRFV2PlusWrapperEnvironment( - env *test_env.CLClusterTestEnv, - vrfv2PlusConfig vrfv2plus_config.VRFV2PlusConfig, - linkToken contracts.LinkToken, - mockNativeLINKFeed contracts.MockETHLINKFeed, - coordinator contracts.VRFCoordinatorV2_5, - keyHash [32]byte, - wrapperConsumerContractsAmount int, -) (*VRFV2PlusWrapperContracts, *big.Int, error) { - - wrapperContracts, err := DeployVRFV2PlusDirectFundingContracts( - env.ContractDeployer, - env.EVMClient, - linkToken.Address(), - mockNativeLINKFeed.Address(), - coordinator, - wrapperConsumerContractsAmount, - ) - if err != nil { - return nil, nil, err - } - - err = env.EVMClient.WaitForEvents() - - if err != nil { - return nil, nil, fmt.Errorf("%s, err %w", ErrWaitTXsComplete, err) - } - err = wrapperContracts.VRFV2PlusWrapper.SetConfig( - vrfv2PlusConfig.WrapperGasOverhead, - vrfv2PlusConfig.CoordinatorGasOverhead, - vrfv2PlusConfig.WrapperPremiumPercentage, - keyHash, - vrfv2PlusConfig.WrapperMaxNumberOfWords, - vrfv2PlusConfig.StalenessSeconds, - big.NewInt(vrfv2PlusConfig.FallbackWeiPerUnitLink), - vrfv2PlusConfig.FulfillmentFlatFeeLinkPPM, - vrfv2PlusConfig.FulfillmentFlatFeeNativePPM, - ) - if err != nil { - return nil, nil, err - } - - err = env.EVMClient.WaitForEvents() - if err != nil { - return nil, nil, fmt.Errorf("%s, err %w", ErrWaitTXsComplete, err) - } - - //fund sub - wrapperSubID, err := wrapperContracts.VRFV2PlusWrapper.GetSubID(context.Background()) - if err != nil { - return nil, nil, err - } - - err = env.EVMClient.WaitForEvents() - if err != nil { - return nil, nil, fmt.Errorf("%s, err %w", ErrWaitTXsComplete, err) - } - - err = FundSubscriptions(env, vrfv2PlusConfig, linkToken, coordinator, []*big.Int{wrapperSubID}) - if err != nil { - return nil, nil, err - } - - //fund consumer with Link - err = linkToken.Transfer( - wrapperContracts.LoadTestConsumers[0].Address(), - big.NewInt(0).Mul(big.NewInt(1e18), big.NewInt(vrfv2PlusConfig.WrapperConsumerFundingAmountLink)), - ) - if err != nil { - return nil, nil, err - } - err = env.EVMClient.WaitForEvents() - if err != nil { - return nil, nil, fmt.Errorf("%s, err %w", ErrWaitTXsComplete, err) - } - - //fund consumer with Eth - err = wrapperContracts.LoadTestConsumers[0].Fund(big.NewFloat(vrfv2PlusConfig.WrapperConsumerFundingAmountNativeToken)) - if err != nil { - return nil, nil, err - } - err = env.EVMClient.WaitForEvents() - if err != nil { - return nil, nil, fmt.Errorf("%s, err %w", ErrWaitTXsComplete, err) - } - return wrapperContracts, wrapperSubID, nil -} func CreateSubAndFindSubID(env *test_env.CLClusterTestEnv, coordinator contracts.VRFCoordinatorV2_5) (*big.Int, error) { tx, err := coordinator.CreateSubscription() if err != nil { @@ -557,39 +431,9 @@ func CreateSubAndFindSubID(env *test_env.CLClusterTestEnv, coordinator contracts //SubscriptionsCreated Log should be emitted with the subscription ID subID := receipt.Logs[0].Topics[1].Big() - //verify that the subscription was created - _, err = coordinator.FindSubscriptionID(subID) - if err != nil { - return nil, fmt.Errorf("%s, err %w", ErrFindSubID, err) - } - return subID, nil } -func GetUpgradedCoordinatorTotalBalance(coordinator contracts.VRFCoordinatorV2PlusUpgradedVersion) (linkTotalBalance *big.Int, nativeTokenTotalBalance *big.Int, err error) { - linkTotalBalance, err = coordinator.GetLinkTotalBalance(context.Background()) - if err != nil { - return nil, nil, fmt.Errorf("%s, err %w", ErrLinkTotalBalance, err) - } - nativeTokenTotalBalance, err = coordinator.GetNativeTokenTotalBalance(context.Background()) - if err != nil { - return nil, nil, fmt.Errorf("%s, err %w", ErrNativeTokenBalance, err) - } - return -} - -func GetCoordinatorTotalBalance(coordinator contracts.VRFCoordinatorV2_5) (linkTotalBalance *big.Int, nativeTokenTotalBalance *big.Int, err error) { - linkTotalBalance, err = coordinator.GetLinkTotalBalance(context.Background()) - if err != nil { - return nil, nil, fmt.Errorf("%s, err %w", ErrLinkTotalBalance, err) - } - nativeTokenTotalBalance, err = coordinator.GetNativeTokenTotalBalance(context.Background()) - if err != nil { - return nil, nil, fmt.Errorf("%s, err %w", ErrNativeTokenBalance, err) - } - return -} - func FundSubscriptions( env *test_env.CLClusterTestEnv, vrfv2PlusConfig vrfv2plus_config.VRFV2PlusConfig, @@ -621,6 +465,30 @@ func FundSubscriptions( return nil } +func GetUpgradedCoordinatorTotalBalance(coordinator contracts.VRFCoordinatorV2PlusUpgradedVersion) (linkTotalBalance *big.Int, nativeTokenTotalBalance *big.Int, err error) { + linkTotalBalance, err = coordinator.GetLinkTotalBalance(context.Background()) + if err != nil { + return nil, nil, fmt.Errorf("%s, err %w", ErrLinkTotalBalance, err) + } + nativeTokenTotalBalance, err = coordinator.GetNativeTokenTotalBalance(context.Background()) + if err != nil { + return nil, nil, fmt.Errorf("%s, err %w", ErrNativeTokenBalance, err) + } + return +} + +func GetCoordinatorTotalBalance(coordinator contracts.VRFCoordinatorV2_5) (linkTotalBalance *big.Int, nativeTokenTotalBalance *big.Int, err error) { + linkTotalBalance, err = coordinator.GetLinkTotalBalance(context.Background()) + if err != nil { + return nil, nil, fmt.Errorf("%s, err %w", ErrLinkTotalBalance, err) + } + nativeTokenTotalBalance, err = coordinator.GetNativeTokenTotalBalance(context.Background()) + if err != nil { + return nil, nil, fmt.Errorf("%s, err %w", ErrNativeTokenBalance, err) + } + return +} + func RequestRandomnessAndWaitForFulfillment( consumer contracts.VRFv2PlusLoadTestConsumer, coordinator contracts.VRFCoordinatorV2_5, @@ -705,6 +573,135 @@ func RequestRandomnessAndWaitForFulfillmentUpgraded( return randomWordsFulfilledEvent, err } +func SetupVRFV2PlusWrapperEnvironment( + env *test_env.CLClusterTestEnv, + vrfv2PlusConfig vrfv2plus_config.VRFV2PlusConfig, + linkToken contracts.LinkToken, + mockNativeLINKFeed contracts.MockETHLINKFeed, + coordinator contracts.VRFCoordinatorV2_5, + keyHash [32]byte, + wrapperConsumerContractsAmount int, +) (*VRFV2PlusWrapperContracts, *big.Int, error) { + + wrapperContracts, err := DeployVRFV2PlusDirectFundingContracts( + env.ContractDeployer, + env.EVMClient, + linkToken.Address(), + mockNativeLINKFeed.Address(), + coordinator, + wrapperConsumerContractsAmount, + ) + if err != nil { + return nil, nil, err + } + + err = env.EVMClient.WaitForEvents() + + if err != nil { + return nil, nil, fmt.Errorf("%s, err %w", ErrWaitTXsComplete, err) + } + err = wrapperContracts.VRFV2PlusWrapper.SetConfig( + vrfv2PlusConfig.WrapperGasOverhead, + vrfv2PlusConfig.CoordinatorGasOverhead, + vrfv2PlusConfig.WrapperPremiumPercentage, + keyHash, + vrfv2PlusConfig.WrapperMaxNumberOfWords, + vrfv2PlusConfig.StalenessSeconds, + big.NewInt(vrfv2PlusConfig.FallbackWeiPerUnitLink), + vrfv2PlusConfig.FulfillmentFlatFeeLinkPPM, + vrfv2PlusConfig.FulfillmentFlatFeeNativePPM, + ) + if err != nil { + return nil, nil, err + } + + err = env.EVMClient.WaitForEvents() + if err != nil { + return nil, nil, fmt.Errorf("%s, err %w", ErrWaitTXsComplete, err) + } + + //fund sub + wrapperSubID, err := wrapperContracts.VRFV2PlusWrapper.GetSubID(context.Background()) + if err != nil { + return nil, nil, err + } + + err = env.EVMClient.WaitForEvents() + if err != nil { + return nil, nil, fmt.Errorf("%s, err %w", ErrWaitTXsComplete, err) + } + + err = FundSubscriptions(env, vrfv2PlusConfig, linkToken, coordinator, []*big.Int{wrapperSubID}) + if err != nil { + return nil, nil, err + } + + //fund consumer with Link + err = linkToken.Transfer( + wrapperContracts.LoadTestConsumers[0].Address(), + big.NewInt(0).Mul(big.NewInt(1e18), big.NewInt(vrfv2PlusConfig.WrapperConsumerFundingAmountLink)), + ) + if err != nil { + return nil, nil, err + } + err = env.EVMClient.WaitForEvents() + if err != nil { + return nil, nil, fmt.Errorf("%s, err %w", ErrWaitTXsComplete, err) + } + + //fund consumer with Eth + err = wrapperContracts.LoadTestConsumers[0].Fund(big.NewFloat(vrfv2PlusConfig.WrapperConsumerFundingAmountNativeToken)) + if err != nil { + return nil, nil, err + } + err = env.EVMClient.WaitForEvents() + if err != nil { + return nil, nil, fmt.Errorf("%s, err %w", ErrWaitTXsComplete, err) + } + return wrapperContracts, wrapperSubID, nil +} + +func DeployVRFV2PlusWrapperConsumers(contractDeployer contracts.ContractDeployer, linkTokenAddress string, vrfV2PlusWrapper contracts.VRFV2PlusWrapper, consumerContractsAmount int) ([]contracts.VRFv2PlusWrapperLoadTestConsumer, error) { + var consumers []contracts.VRFv2PlusWrapperLoadTestConsumer + for i := 1; i <= consumerContractsAmount; i++ { + loadTestConsumer, err := contractDeployer.DeployVRFV2PlusWrapperLoadTestConsumer(linkTokenAddress, vrfV2PlusWrapper.Address()) + if err != nil { + return nil, fmt.Errorf("%s, err %w", ErrAdvancedConsumer, err) + } + consumers = append(consumers, loadTestConsumer) + } + return consumers, nil +} + +func DeployVRFV2PlusDirectFundingContracts( + contractDeployer contracts.ContractDeployer, + chainClient blockchain.EVMClient, + linkTokenAddress string, + linkEthFeedAddress string, + coordinator contracts.VRFCoordinatorV2_5, + consumerContractsAmount int, +) (*VRFV2PlusWrapperContracts, error) { + + vrfv2PlusWrapper, err := contractDeployer.DeployVRFV2PlusWrapper(linkTokenAddress, linkEthFeedAddress, coordinator.Address()) + if err != nil { + return nil, fmt.Errorf("%s, err %w", ErrDeployWrapper, err) + } + err = chainClient.WaitForEvents() + if err != nil { + return nil, fmt.Errorf("%s, err %w", ErrWaitTXsComplete, err) + } + + consumers, err := DeployVRFV2PlusWrapperConsumers(contractDeployer, linkTokenAddress, vrfv2PlusWrapper, consumerContractsAmount) + if err != nil { + return nil, err + } + err = chainClient.WaitForEvents() + if err != nil { + return nil, fmt.Errorf("%s, err %w", ErrWaitTXsComplete, err) + } + return &VRFV2PlusWrapperContracts{vrfv2PlusWrapper, consumers}, nil +} + func DirectFundingRequestRandomnessAndWaitForFulfillment( consumer contracts.VRFv2PlusWrapperLoadTestConsumer, coordinator contracts.VRFCoordinatorV2_5, @@ -804,7 +801,7 @@ func WaitForRequestCountEqualToFulfilmentCount(consumer contracts.VRFv2PlusLoadT fmt.Errorf("timeout waiting for rand request and fulfilments to be equal AFTER performance test was executed. Request Count: %d, Fulfilment Count: %d", metrics.RequestCount.Uint64(), metrics.FulfilmentCount.Uint64()) case <-ticker.C: - go retreiveLoadTestMetrics(consumer, metricsChannel, metricsErrorChannel) + go retrieveLoadTestMetrics(consumer, metricsChannel, metricsErrorChannel) case metrics = <-metricsChannel: if metrics.RequestCount.Cmp(metrics.FulfilmentCount) == 0 { ticker.Stop() @@ -854,7 +851,7 @@ func ReturnFundsForFulfilledRequests(client blockchain.EVMClient, coordinator co return nil } -func retreiveLoadTestMetrics( +func retrieveLoadTestMetrics( consumer contracts.VRFv2PlusLoadTestConsumer, metricsChannel chan *contracts.VRFLoadTestMetrics, metricsErrorChannel chan error, diff --git a/integration-tests/contracts/contract_deployer.go b/integration-tests/contracts/contract_deployer.go index 000fe7b2b81..528f07ec68e 100644 --- a/integration-tests/contracts/contract_deployer.go +++ b/integration-tests/contracts/contract_deployer.go @@ -89,6 +89,7 @@ type ContractDeployer interface { DeployKeeperRegistrar(registryVersion eth_contracts.KeeperRegistryVersion, linkAddr string, registrarSettings KeeperRegistrarSettings) (KeeperRegistrar, error) LoadKeeperRegistrar(address common.Address, registryVersion eth_contracts.KeeperRegistryVersion) (KeeperRegistrar, error) DeployUpkeepTranscoder() (UpkeepTranscoder, error) + LoadUpkeepTranscoder(address common.Address) (UpkeepTranscoder, error) DeployKeeperRegistry(opts *KeeperRegistryOpts) (KeeperRegistry, error) LoadKeeperRegistry(address common.Address, registryVersion eth_contracts.KeeperRegistryVersion) (KeeperRegistry, error) DeployKeeperConsumer(updateInterval *big.Int) (KeeperConsumer, error) @@ -763,6 +764,25 @@ func (e *EthereumContractDeployer) DeployUpkeepTranscoder() (UpkeepTranscoder, e }, err } +func (e *EthereumContractDeployer) LoadUpkeepTranscoder(address common.Address) (UpkeepTranscoder, error) { + instance, err := e.client.LoadContract("UpkeepTranscoder", address, func( + address common.Address, + backend bind.ContractBackend, + ) (interface{}, error) { + return upkeep_transcoder.NewUpkeepTranscoder(address, backend) + }) + + if err != nil { + return nil, err + } + + return &EthereumUpkeepTranscoder{ + client: e.client, + transcoder: instance.(*upkeep_transcoder.UpkeepTranscoder), + address: &address, + }, err +} + func (e *EthereumContractDeployer) DeployKeeperRegistrar(registryVersion eth_contracts.KeeperRegistryVersion, linkAddr string, registrarSettings KeeperRegistrarSettings) (KeeperRegistrar, error) { @@ -1192,6 +1212,7 @@ func (e *EthereumContractDeployer) LoadKeeperRegistry(address common.Address, re address: &address, client: e.client, registry1_1: instance.(*keeper_registry_wrapper1_1.KeeperRegistry), + version: registryVersion, }, err case eth_contracts.RegistryVersion_1_2: instance, err := e.client.LoadContract("KeeperRegistry", address, func( @@ -1207,6 +1228,7 @@ func (e *EthereumContractDeployer) LoadKeeperRegistry(address common.Address, re address: &address, client: e.client, registry1_2: instance.(*keeper_registry_wrapper1_2.KeeperRegistry), + version: registryVersion, }, err case eth_contracts.RegistryVersion_1_3: instance, err := e.client.LoadContract("KeeperRegistry", address, func( @@ -1222,6 +1244,7 @@ func (e *EthereumContractDeployer) LoadKeeperRegistry(address common.Address, re address: &address, client: e.client, registry1_3: instance.(*keeper_registry_wrapper1_3.KeeperRegistry), + version: registryVersion, }, err case eth_contracts.RegistryVersion_2_0: instance, err := e.client.LoadContract("KeeperRegistry", address, func( @@ -1237,6 +1260,7 @@ func (e *EthereumContractDeployer) LoadKeeperRegistry(address common.Address, re address: &address, client: e.client, registry2_0: instance.(*keeper_registry_wrapper2_0.KeeperRegistry), + version: registryVersion, }, err case eth_contracts.RegistryVersion_2_1: instance, err := e.client.LoadContract("KeeperRegistry", address, func( @@ -1252,6 +1276,7 @@ func (e *EthereumContractDeployer) LoadKeeperRegistry(address common.Address, re address: &address, client: e.client, registry2_1: instance.(*iregistry21.IKeeperRegistryMaster), + version: registryVersion, }, err default: return nil, fmt.Errorf("keeper registry version %d is not supported", registryVersion) diff --git a/integration-tests/contracts/contract_loader.go b/integration-tests/contracts/contract_loader.go index 9a2f20226d3..6136e78b367 100644 --- a/integration-tests/contracts/contract_loader.go +++ b/integration-tests/contracts/contract_loader.go @@ -17,6 +17,8 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/generated/authorized_forwarder" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/generated/link_token_interface" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/generated/operator_wrapper" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/generated/vrf_coordinator_v2" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/generated/vrf_load_test_with_metrics" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/llo-feeds/generated/fee_manager" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/llo-feeds/generated/reward_manager" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/llo-feeds/generated/verifier" @@ -44,6 +46,8 @@ type ContractLoader interface { LoadWERC20Mock(addr common.Address) (WERC20Mock, error) // VRF + LoadVRFCoordinatorV2(addr string) (VRFCoordinatorV2, error) + LoadVRFv2LoadTestConsumer(addr string) (VRFv2LoadTestConsumer, error) LoadVRFCoordinatorV2_5(addr string) (VRFCoordinatorV2_5, error) LoadVRFv2PlusLoadTestConsumer(addr string) (VRFv2PlusLoadTestConsumer, error) } @@ -356,3 +360,39 @@ func (e *EthereumContractLoader) LoadVRFv2PlusLoadTestConsumer(addr string) (VRF address: &address, }, err } + +func (e *EthereumContractLoader) LoadVRFCoordinatorV2(addr string) (VRFCoordinatorV2, error) { + address := common.HexToAddress(addr) + instance, err := e.client.LoadContract("VRFCoordinatorV2", address, func( + address common.Address, + backend bind.ContractBackend, + ) (interface{}, error) { + return vrf_coordinator_v2.NewVRFCoordinatorV2(address, backend) + }) + if err != nil { + return nil, err + } + return &EthereumVRFCoordinatorV2{ + address: &address, + client: e.client, + coordinator: instance.(*vrf_coordinator_v2.VRFCoordinatorV2), + }, err +} + +func (e *EthereumContractLoader) LoadVRFv2LoadTestConsumer(addr string) (VRFv2LoadTestConsumer, error) { + address := common.HexToAddress(addr) + instance, err := e.client.LoadContract("VRFV2LoadTestWithMetrics", address, func( + address common.Address, + backend bind.ContractBackend, + ) (interface{}, error) { + return vrf_load_test_with_metrics.NewVRFV2LoadTestWithMetrics(address, backend) + }) + if err != nil { + return nil, err + } + return &EthereumVRFv2LoadTestConsumer{ + client: e.client, + consumer: instance.(*vrf_load_test_with_metrics.VRFV2LoadTestWithMetrics), + address: &address, + }, err +} diff --git a/integration-tests/contracts/contract_vrf_models.go b/integration-tests/contracts/contract_vrf_models.go index baee2ccd929..5f850fce1a7 100644 --- a/integration-tests/contracts/contract_vrf_models.go +++ b/integration-tests/contracts/contract_vrf_models.go @@ -49,10 +49,15 @@ type VRFCoordinatorV2 interface { publicProvingKey [2]*big.Int, ) error HashOfKey(ctx context.Context, pubKey [2]*big.Int) ([32]byte, error) - CreateSubscription() error + CreateSubscription() (*types.Transaction, error) AddConsumer(subId uint64, consumerAddress string) error Address() string GetSubscription(ctx context.Context, subID uint64) (vrf_coordinator_v2.GetSubscription, error) + PendingRequestsExist(ctx context.Context, subID uint64) (bool, error) + CancelSubscription(subID uint64, to common.Address) (*types.Transaction, error) + FindSubscriptionID(subID uint64) (uint64, error) + WaitForRandomWordsFulfilledEvent(requestID []*big.Int, timeout time.Duration) (*vrf_coordinator_v2.VRFCoordinatorV2RandomWordsFulfilled, error) + WaitForRandomWordsRequestedEvent(keyHash [][32]byte, subID []uint64, sender []common.Address, timeout time.Duration) (*vrf_coordinator_v2.VRFCoordinatorV2RandomWordsRequested, error) } type VRFCoordinatorV2_5 interface { @@ -169,6 +174,7 @@ type VRFv2LoadTestConsumer interface { GetRequestStatus(ctx context.Context, requestID *big.Int) (vrf_load_test_with_metrics.GetRequestStatus, error) GetLastRequestId(ctx context.Context) (*big.Int, error) GetLoadTestMetrics(ctx context.Context) (*VRFLoadTestMetrics, error) + ResetMetrics() error } type VRFv2PlusLoadTestConsumer interface { diff --git a/integration-tests/contracts/ethereum_vrfv2_contracts.go b/integration-tests/contracts/ethereum_vrfv2_contracts.go index 9c7e628dbd9..ac3926fa746 100644 --- a/integration-tests/contracts/ethereum_vrfv2_contracts.go +++ b/integration-tests/contracts/ethereum_vrfv2_contracts.go @@ -3,7 +3,9 @@ package contracts import ( "context" "encoding/hex" + "fmt" "math/big" + "time" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi/bind" @@ -182,16 +184,16 @@ func (v *EthereumVRFCoordinatorV2) RegisterProvingKey( return v.client.ProcessTransaction(tx) } -func (v *EthereumVRFCoordinatorV2) CreateSubscription() error { +func (v *EthereumVRFCoordinatorV2) CreateSubscription() (*types.Transaction, error) { opts, err := v.client.TransactionOpts(v.client.GetDefaultWallet()) if err != nil { - return err + return nil, err } tx, err := v.coordinator.CreateSubscription(opts) if err != nil { - return err + return nil, err } - return v.client.ProcessTransaction(tx) + return tx, v.client.ProcessTransaction(tx) } func (v *EthereumVRFCoordinatorV2) AddConsumer(subId uint64, consumerAddress string) error { @@ -210,6 +212,94 @@ func (v *EthereumVRFCoordinatorV2) AddConsumer(subId uint64, consumerAddress str return v.client.ProcessTransaction(tx) } +func (v *EthereumVRFCoordinatorV2) PendingRequestsExist(ctx context.Context, subID uint64) (bool, error) { + opts := &bind.CallOpts{ + From: common.HexToAddress(v.client.GetDefaultWallet().Address()), + Context: ctx, + } + pendingRequestExists, err := v.coordinator.PendingRequestExists(opts, subID) + if err != nil { + return false, err + } + return pendingRequestExists, nil +} + +// CancelSubscription cancels subscription by Sub owner, +// return funds to specified address, +// checks if pending requests for a sub exist +func (v *EthereumVRFCoordinatorV2) CancelSubscription(subID uint64, to common.Address) (*types.Transaction, error) { + opts, err := v.client.TransactionOpts(v.client.GetDefaultWallet()) + if err != nil { + return nil, err + } + tx, err := v.coordinator.CancelSubscription( + opts, + subID, + to, + ) + if err != nil { + return nil, err + } + return tx, v.client.ProcessTransaction(tx) +} + +func (v *EthereumVRFCoordinatorV2) FindSubscriptionID(subID uint64) (uint64, error) { + owner := v.client.GetDefaultWallet().Address() + subscriptionIterator, err := v.coordinator.FilterSubscriptionCreated( + nil, + []uint64{subID}, + ) + if err != nil { + return 0, err + } + + if !subscriptionIterator.Next() { + return 0, fmt.Errorf("expected at least 1 subID for the given owner %s", owner) + } + + return subscriptionIterator.Event.SubId, nil +} + +func (v *EthereumVRFCoordinatorV2) WaitForRandomWordsFulfilledEvent(requestID []*big.Int, timeout time.Duration) (*vrf_coordinator_v2.VRFCoordinatorV2RandomWordsFulfilled, error) { + randomWordsFulfilledEventsChannel := make(chan *vrf_coordinator_v2.VRFCoordinatorV2RandomWordsFulfilled) + subscription, err := v.coordinator.WatchRandomWordsFulfilled(nil, randomWordsFulfilledEventsChannel, requestID) + if err != nil { + return nil, err + } + defer subscription.Unsubscribe() + + for { + select { + case err := <-subscription.Err(): + return nil, err + case <-time.After(timeout): + return nil, fmt.Errorf("timeout waiting for RandomWordsFulfilled event") + case randomWordsFulfilledEvent := <-randomWordsFulfilledEventsChannel: + return randomWordsFulfilledEvent, nil + } + } +} + +func (v *EthereumVRFCoordinatorV2) WaitForRandomWordsRequestedEvent(keyHash [][32]byte, subID []uint64, sender []common.Address, timeout time.Duration) (*vrf_coordinator_v2.VRFCoordinatorV2RandomWordsRequested, error) { + randomWordsFulfilledEventsChannel := make(chan *vrf_coordinator_v2.VRFCoordinatorV2RandomWordsRequested) + subscription, err := v.coordinator.WatchRandomWordsRequested(nil, randomWordsFulfilledEventsChannel, keyHash, subID, sender) + if err != nil { + return nil, err + } + defer subscription.Unsubscribe() + + for { + select { + case err := <-subscription.Err(): + return nil, err + case <-time.After(timeout): + return nil, fmt.Errorf("timeout waiting for RandomWordsRequested event") + case randomWordsFulfilledEvent := <-randomWordsFulfilledEventsChannel: + return randomWordsFulfilledEvent, nil + } + } +} + // GetAllRandomWords get all VRFv2 randomness output words func (v *EthereumVRFConsumerV2) GetAllRandomWords(ctx context.Context, num int) ([]*big.Int, error) { words := make([]*big.Int, 0) @@ -392,6 +482,18 @@ func (v *EthereumVRFv2LoadTestConsumer) GetLastRequestId(ctx context.Context) (* }) } +func (v *EthereumVRFv2LoadTestConsumer) ResetMetrics() error { + opts, err := v.client.TransactionOpts(v.client.GetDefaultWallet()) + if err != nil { + return err + } + tx, err := v.consumer.Reset(opts) + if err != nil { + return err + } + return v.client.ProcessTransaction(tx) +} + func (v *EthereumVRFv2LoadTestConsumer) GetLoadTestMetrics(ctx context.Context) (*VRFLoadTestMetrics, error) { requestCount, err := v.consumer.SRequestCount(&bind.CallOpts{ From: common.HexToAddress(v.client.GetDefaultWallet().Address()), diff --git a/integration-tests/load/vrfv2/config.go b/integration-tests/load/vrfv2/config.go index 0a595f753c2..d5e94bb75fb 100644 --- a/integration-tests/load/vrfv2/config.go +++ b/integration-tests/load/vrfv2/config.go @@ -1,10 +1,12 @@ package loadvrfv2 import ( + "encoding/base64" "fmt" - "math/big" "os" + "github.com/smartcontractkit/chainlink/integration-tests/actions/vrfv2_actions/vrfv2_config" + "github.com/pelletier/go-toml/v2" "github.com/rs/zerolog/log" @@ -13,64 +15,137 @@ import ( const ( DefaultConfigFilename = "config.toml" + SoakTestType = "Soak" + LoadTestType = "Load" + StressTestType = "Stress" + SpikeTestType = "Spike" - ErrReadPerfConfig = "failed to read TOML config for performance tests" - ErrUnmarshalPerfConfig = "failed to unmarshal TOML config for performance tests" + ErrReadPerfConfig = "failed to read TOML config for performance tests" + ErrUnmarshalPerfConfig = "failed to unmarshal TOML config for performance tests" + ErrDeviationShouldBeLessThanOriginal = "`RandomnessRequestCountPerRequestDeviation` should be less than `RandomnessRequestCountPerRequest`" ) type PerformanceConfig struct { - Soak *Soak `toml:"Soak"` - Load *Load `toml:"Load"` - SoakVolume *SoakVolume `toml:"SoakVolume"` - LoadVolume *LoadVolume `toml:"LoadVolume"` - Common *Common `toml:"Common"` + Soak *Soak `toml:"Soak"` + Load *Load `toml:"Load"` + Stress *Stress `toml:"Stress"` + Spike *Spike `toml:"Spike"` + + Common *Common `toml:"Common"` + ExistingEnvConfig *ExistingEnvConfig `toml:"ExistingEnvConfig"` + NewEnvConfig *NewEnvConfig `toml:"NewEnvConfig"` } -type Common struct { +type ExistingEnvConfig struct { + CoordinatorAddress string `toml:"coordinator_address"` + ConsumerAddress string `toml:"consumer_address"` + LinkAddress string `toml:"link_address"` + SubID uint64 `toml:"sub_id"` + KeyHash string `toml:"key_hash"` + Funding + CreateFundSubsAndAddConsumers bool `toml:"create_fund_subs_and_add_consumers"` + NodeSendingKeys []string `toml:"node_sending_keys"` +} + +type NewEnvConfig struct { Funding } +type Common struct { + MinimumConfirmations uint16 `toml:"minimum_confirmations"` + CancelSubsAfterTestRun bool `toml:"cancel_subs_after_test_run"` +} + type Funding struct { - NodeFunds *big.Float `toml:"node_funds"` - SubFunds *big.Int `toml:"sub_funds"` + SubFunding + NodeSendingKeyFunding float64 `toml:"node_sending_key_funding"` + NodeSendingKeyFundingMin float64 `toml:"node_sending_key_funding_min"` } -type Soak struct { - RPS int64 `toml:"rps"` - Duration *models.Duration `toml:"duration"` +type SubFunding struct { + SubFundsLink float64 `toml:"sub_funds_link"` } -type SoakVolume struct { - Products int64 `toml:"products"` - Pace *models.Duration `toml:"pace"` - Duration *models.Duration `toml:"duration"` +type Soak struct { + PerformanceTestConfig } type Load struct { - RPSFrom int64 `toml:"rps_from"` - RPSIncrease int64 `toml:"rps_increase"` - RPSSteps int `toml:"rps_steps"` - Duration *models.Duration `toml:"duration"` + PerformanceTestConfig } -type LoadVolume struct { - ProductsFrom int64 `toml:"products_from"` - ProductsIncrease int64 `toml:"products_increase"` - ProductsSteps int `toml:"products_steps"` - Pace *models.Duration `toml:"pace"` - Duration *models.Duration `toml:"duration"` +type Stress struct { + PerformanceTestConfig +} + +type Spike struct { + PerformanceTestConfig +} + +type PerformanceTestConfig struct { + NumberOfSubToCreate int `toml:"number_of_sub_to_create"` + + RPS int64 `toml:"rps"` + //Duration *models.Duration `toml:"duration"` + RateLimitUnitDuration *models.Duration `toml:"rate_limit_unit_duration"` + RandomnessRequestCountPerRequest uint16 `toml:"randomness_request_count_per_request"` + RandomnessRequestCountPerRequestDeviation uint16 `toml:"randomness_request_count_per_request_deviation"` } func ReadConfig() (*PerformanceConfig, error) { var cfg *PerformanceConfig - d, err := os.ReadFile(DefaultConfigFilename) - if err != nil { - return nil, fmt.Errorf("%s, err: %w", ErrReadPerfConfig, err) + rawConfig := os.Getenv("CONFIG") + var d []byte + var err error + if rawConfig == "" { + d, err = os.ReadFile(DefaultConfigFilename) + if err != nil { + return nil, fmt.Errorf("%s, err: %w", ErrReadPerfConfig, err) + } + } else { + d, err = base64.StdEncoding.DecodeString(rawConfig) + if err != nil { + return nil, fmt.Errorf("%s, err: %w", ErrReadPerfConfig, err) + } } err = toml.Unmarshal(d, &cfg) if err != nil { return nil, fmt.Errorf("%s, err: %w", ErrUnmarshalPerfConfig, err) } - log.Debug().Interface("PerformanceConfig", cfg).Msg("Parsed performance config") + + if cfg.Soak.RandomnessRequestCountPerRequest <= cfg.Soak.RandomnessRequestCountPerRequestDeviation { + return nil, fmt.Errorf("%s, err: %w", ErrDeviationShouldBeLessThanOriginal, err) + } + + log.Debug().Interface("Config", cfg).Msg("Parsed config") return cfg, nil } + +func SetPerformanceTestConfig(testType string, vrfv2Config *vrfv2_config.VRFV2Config, cfg *PerformanceConfig) { + switch testType { + case SoakTestType: + vrfv2Config.NumberOfSubToCreate = cfg.Soak.NumberOfSubToCreate + vrfv2Config.RPS = cfg.Soak.RPS + vrfv2Config.RateLimitUnitDuration = cfg.Soak.RateLimitUnitDuration.Duration() + vrfv2Config.RandomnessRequestCountPerRequest = cfg.Soak.RandomnessRequestCountPerRequest + vrfv2Config.RandomnessRequestCountPerRequestDeviation = cfg.Soak.RandomnessRequestCountPerRequestDeviation + case LoadTestType: + vrfv2Config.NumberOfSubToCreate = cfg.Load.NumberOfSubToCreate + vrfv2Config.RPS = cfg.Load.RPS + vrfv2Config.RateLimitUnitDuration = cfg.Load.RateLimitUnitDuration.Duration() + vrfv2Config.RandomnessRequestCountPerRequest = cfg.Load.RandomnessRequestCountPerRequest + vrfv2Config.RandomnessRequestCountPerRequestDeviation = cfg.Load.RandomnessRequestCountPerRequestDeviation + case StressTestType: + vrfv2Config.NumberOfSubToCreate = cfg.Stress.NumberOfSubToCreate + vrfv2Config.RPS = cfg.Stress.RPS + vrfv2Config.RateLimitUnitDuration = cfg.Stress.RateLimitUnitDuration.Duration() + vrfv2Config.RandomnessRequestCountPerRequest = cfg.Stress.RandomnessRequestCountPerRequest + vrfv2Config.RandomnessRequestCountPerRequestDeviation = cfg.Stress.RandomnessRequestCountPerRequestDeviation + case SpikeTestType: + vrfv2Config.NumberOfSubToCreate = cfg.Spike.NumberOfSubToCreate + vrfv2Config.RPS = cfg.Spike.RPS + vrfv2Config.RateLimitUnitDuration = cfg.Spike.RateLimitUnitDuration.Duration() + vrfv2Config.RandomnessRequestCountPerRequest = cfg.Spike.RandomnessRequestCountPerRequest + vrfv2Config.RandomnessRequestCountPerRequestDeviation = cfg.Spike.RandomnessRequestCountPerRequestDeviation + } +} diff --git a/integration-tests/load/vrfv2/config.toml b/integration-tests/load/vrfv2/config.toml index 8917db88cc2..6f54d1ec998 100644 --- a/integration-tests/load/vrfv2/config.toml +++ b/integration-tests/load/vrfv2/config.toml @@ -1,27 +1,58 @@ -# testing one product (jobs + contracts) by varying RPS + +[Common] +minimum_confirmations = 3 +cancel_subs_after_test_run = true + +[NewEnvConfig] +sub_funds_link = 1000 +node_sending_key_funding = 1000 + +[ExistingEnvConfig] +coordinator_address = "" +consumer_address = "" +sub_id = 1 +key_hash = "" +create_fund_subs_and_add_consumers = true +link_address = "" +sub_funds_link = 10 +node_sending_key_funding_min = 1 +node_sending_keys = [ + "", + "", + "", + "", + "", + "", +] + +# 10 RPM - 1 tx request with 1 rand request in each tx every 6 seconds [Soak] +rate_limit_unit_duration = "6s" rps = 1 -duration = "3m" +randomness_request_count_per_request = 1 # amount of randomness requests to make per one TX request +randomness_request_count_per_request_deviation = 0 #NOTE - deviation should be less than randomness_request_count_per_request setting +number_of_sub_to_create = 1 +# approx 60 RPM - 1 tx request with 3 rand requests in each tx every 3 seconds [Load] -rps_from = 1 -rps_increase = 1 -rps_steps = 10 -duration = "3m" - -# testing multiple products (jobs + contracts) by varying instances, deploying more of the same type with stable RPS for each product -[SoakVolume] -products = 5 -pace = "1s" -duration = "3m" +rate_limit_unit_duration = "3s" +rps = 1 +randomness_request_count_per_request = 3 # amount of randomness requests to make per one TX request +randomness_request_count_per_request_deviation = 2 #NOTE - deviation should be less than randomness_request_count_per_request setting +number_of_sub_to_create = 1 -[LoadVolume] -products_from = 1 -products_increase = 1 -products_steps = 10 -pace = "1s" -duration = "3m" +# approx 540 RPM - 3 tx requests per second with 4 rand requests in each tx +[Stress] +rate_limit_unit_duration = "1s" +rps = 3 +randomness_request_count_per_request = 4 # amount of randomness requests to make per one TX request +randomness_request_count_per_request_deviation = 0 #NOTE - deviation should be less than randomness_request_count_per_request setting +number_of_sub_to_create = 1 -[Common] -node_funds = 10 -sub_funds = 100 \ No newline at end of file +# approx 150 RPM - 1 tx request with 150 rand requests in each tx every 60 seconds +[Spike] +rate_limit_unit_duration = "1m" +rps = 1 +randomness_request_count_per_request = 150 # amount of randomness requests to make per one TX request +randomness_request_count_per_request_deviation = 0 #NOTE - deviation should be less than randomness_request_count_per_request setting +number_of_sub_to_create = 1 diff --git a/integration-tests/load/vrfv2/gun.go b/integration-tests/load/vrfv2/gun.go index 8100baaa7f7..8a5eb3c66de 100644 --- a/integration-tests/load/vrfv2/gun.go +++ b/integration-tests/load/vrfv2/gun.go @@ -1,38 +1,78 @@ package loadvrfv2 import ( + "math/rand" + + "github.com/rs/zerolog" + "github.com/smartcontractkit/wasp" "github.com/smartcontractkit/chainlink/integration-tests/actions/vrfv2_actions" - vrfConst "github.com/smartcontractkit/chainlink/integration-tests/actions/vrfv2_actions/vrfv2_constants" + "github.com/smartcontractkit/chainlink/integration-tests/actions/vrfv2_actions/vrfv2_config" ) /* SingleHashGun is a gun that constantly requests randomness for one feed */ type SingleHashGun struct { - contracts *vrfv2_actions.VRFV2Contracts - keyHash [32]byte + contracts *vrfv2_actions.VRFV2Contracts + keyHash [32]byte + subIDs []uint64 + vrfv2Config vrfv2_config.VRFV2Config + logger zerolog.Logger } -func SingleFeedGun(contracts *vrfv2_actions.VRFV2Contracts, keyHash [32]byte) *SingleHashGun { +func NewSingleHashGun( + contracts *vrfv2_actions.VRFV2Contracts, + keyHash [32]byte, + subIDs []uint64, + vrfv2Config vrfv2_config.VRFV2Config, + logger zerolog.Logger, +) *SingleHashGun { return &SingleHashGun{ - contracts: contracts, - keyHash: keyHash, + contracts: contracts, + keyHash: keyHash, + subIDs: subIDs, + vrfv2Config: vrfv2Config, + logger: logger, } } // Call implements example gun call, assertions on response bodies should be done here func (m *SingleHashGun) Call(_ *wasp.Generator) *wasp.CallResult { - err := m.contracts.LoadTestConsumer.RequestRandomness( - m.keyHash, - vrfConst.SubID, - vrfConst.MinimumConfirmations, - vrfConst.CallbackGasLimit, - vrfConst.NumberOfWords, - vrfConst.RandomnessRequestCountPerRequest, + //todo - should work with multiple consumers and consumers having different keyhashes and wallets + + //randomly increase/decrease randomness request count per TX + randomnessRequestCountPerRequest := deviateValue(m.vrfv2Config.RandomnessRequestCountPerRequest, m.vrfv2Config.RandomnessRequestCountPerRequestDeviation) + _, err := vrfv2_actions.RequestRandomnessAndWaitForFulfillment( + //the same consumer is used for all requests and in all subs + m.contracts.LoadTestConsumers[0], + m.contracts.Coordinator, + &vrfv2_actions.VRFV2Data{VRFV2KeyData: vrfv2_actions.VRFV2KeyData{KeyHash: m.keyHash}}, + //randomly pick a subID from pool of subIDs + m.subIDs[randInRange(0, len(m.subIDs)-1)], + randomnessRequestCountPerRequest, + m.vrfv2Config, + m.vrfv2Config.RandomWordsFulfilledEventTimeout, + m.logger, ) if err != nil { return &wasp.CallResult{Error: err.Error(), Failed: true} } return &wasp.CallResult{} } + +func deviateValue(requestCountPerTX uint16, deviation uint16) uint16 { + if randBool() && requestCountPerTX > deviation { + requestCountPerTX -= uint16(randInRange(0, int(deviation))) + } else { + requestCountPerTX += uint16(randInRange(0, int(deviation))) + } + return requestCountPerTX +} + +func randBool() bool { + return rand.Intn(2) == 1 +} +func randInRange(min int, max int) int { + return rand.Intn(max-min+1) + min +} diff --git a/integration-tests/load/vrfv2/onchain_monitoring.go b/integration-tests/load/vrfv2/onchain_monitoring.go index 66af1807acf..55975a7e42f 100644 --- a/integration-tests/load/vrfv2/onchain_monitoring.go +++ b/integration-tests/load/vrfv2/onchain_monitoring.go @@ -1,15 +1,14 @@ package loadvrfv2 import ( + "context" "testing" "time" "github.com/rs/zerolog/log" "github.com/smartcontractkit/wasp" - "github.com/smartcontractkit/chainlink-testing-framework/utils/testcontext" - - "github.com/smartcontractkit/chainlink/integration-tests/actions/vrfv2_actions" + "github.com/smartcontractkit/chainlink/integration-tests/contracts" ) /* Monitors on-chain stats of LoadConsumer and pushes them to Loki every second */ @@ -21,29 +20,37 @@ const ( ErrLokiPush = "failed to push monitoring metrics to Loki" ) -func MonitorLoadStats(t *testing.T, vrfv2Contracts *vrfv2_actions.VRFV2Contracts, labels map[string]string) { +func MonitorLoadStats(lc *wasp.LokiClient, consumer contracts.VRFv2LoadTestConsumer, labels map[string]string) { go func() { - updatedLabels := make(map[string]string) - for k, v := range labels { - updatedLabels[k] = v - } - updatedLabels["type"] = LokiTypeLabel - updatedLabels["go_test_name"] = t.Name() - updatedLabels["gen_name"] = "performance" - lc, err := wasp.NewLokiClient(wasp.NewEnvLokiConfig()) - if err != nil { - log.Error().Err(err).Msg(ErrLokiClient) - return - } for { time.Sleep(1 * time.Second) - metrics, err := vrfv2Contracts.LoadTestConsumer.GetLoadTestMetrics(testcontext.Get(t)) - if err != nil { - log.Error().Err(err).Msg(ErrMetrics) - } - if err := lc.HandleStruct(wasp.LabelsMapToModel(updatedLabels), time.Now(), metrics); err != nil { - log.Error().Err(err).Msg(ErrLokiPush) - } + metrics := GetLoadTestMetrics(consumer) + SendMetricsToLoki(metrics, lc, labels) } }() } + +func UpdateLabels(labels map[string]string, t *testing.T) map[string]string { + updatedLabels := make(map[string]string) + for k, v := range labels { + updatedLabels[k] = v + } + updatedLabels["type"] = LokiTypeLabel + updatedLabels["go_test_name"] = t.Name() + updatedLabels["gen_name"] = "performance" + return updatedLabels +} + +func SendMetricsToLoki(metrics *contracts.VRFLoadTestMetrics, lc *wasp.LokiClient, updatedLabels map[string]string) { + if err := lc.HandleStruct(wasp.LabelsMapToModel(updatedLabels), time.Now(), metrics); err != nil { + log.Error().Err(err).Msg(ErrLokiPush) + } +} + +func GetLoadTestMetrics(consumer contracts.VRFv2LoadTestConsumer) *contracts.VRFLoadTestMetrics { + metrics, err := consumer.GetLoadTestMetrics(context.Background()) + if err != nil { + log.Error().Err(err).Msg(ErrMetrics) + } + return metrics +} diff --git a/integration-tests/load/vrfv2/vrfv2_test.go b/integration-tests/load/vrfv2/vrfv2_test.go index 44325965bd7..8e74e3aa901 100644 --- a/integration-tests/load/vrfv2/vrfv2_test.go +++ b/integration-tests/load/vrfv2/vrfv2_test.go @@ -1,94 +1,355 @@ package loadvrfv2 import ( + "context" + "math/big" + "os" + "sync" "testing" + "time" + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + "github.com/kelseyhightower/envconfig" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" "github.com/smartcontractkit/wasp" + + "github.com/smartcontractkit/chainlink-testing-framework/blockchain" + "github.com/smartcontractkit/chainlink-testing-framework/logging" + "github.com/smartcontractkit/chainlink-testing-framework/utils/conversions" + "github.com/smartcontractkit/chainlink/integration-tests/actions" + "github.com/smartcontractkit/chainlink/integration-tests/actions/vrfv2_actions/vrfv2_config" + "github.com/smartcontractkit/chainlink/integration-tests/contracts" + "github.com/smartcontractkit/chainlink/integration-tests/docker/test_env" + "github.com/smartcontractkit/chainlink/integration-tests/testreporters" + "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink/integration-tests/actions/vrfv2_actions" ) -func TestVRFV2Load(t *testing.T) { +var ( + env *test_env.CLClusterTestEnv + vrfv2Contracts *vrfv2_actions.VRFV2Contracts + vrfv2Data *vrfv2_actions.VRFV2Data + subIDs []uint64 + eoaWalletAddress string + + labels = map[string]string{ + "branch": "vrfv2_healthcheck", + "commit": "vrfv2_healthcheck", + } + + testType = os.Getenv("TEST_TYPE") +) + +func TestVRFV2Performance(t *testing.T) { cfg, err := ReadConfig() require.NoError(t, err) - env, vrfv2Contracts, key, err := vrfv2_actions.SetupLocalLoadTestEnv(cfg.Common.NodeFunds, cfg.Common.SubFunds) + var vrfv2Config vrfv2_config.VRFV2Config + err = envconfig.Process("VRFV2", &vrfv2Config) require.NoError(t, err) - labels := map[string]string{ - "branch": "vrfv2_healthcheck", - "commit": "vrfv2_healthcheck", + testReporter := &testreporters.VRFV2TestReporter{} + + SetPerformanceTestConfig(testType, &vrfv2Config, cfg) + + l := logging.GetTestLogger(t) + //todo: temporary solution with envconfig and toml config until VRF-662 is implemented + vrfv2Config.MinimumConfirmations = cfg.Common.MinimumConfirmations + + lokiConfig := wasp.NewEnvLokiConfig() + lc, err := wasp.NewLokiClient(lokiConfig) + if err != nil { + l.Error().Err(err).Msg(ErrLokiClient) + return } - singleFeedConfig := &wasp.Config{ - T: t, - LoadType: wasp.RPS, - GenName: "gun", - Gun: SingleFeedGun(vrfv2Contracts, key), - Labels: labels, - LokiConfig: wasp.NewEnvLokiConfig(), + updatedLabels := UpdateLabels(labels, t) + + l.Info(). + Str("Test Type", testType). + Str("Test Duration", vrfv2Config.TestDuration.Truncate(time.Second).String()). + Int64("RPS", vrfv2Config.RPS). + Str("RateLimitUnitDuration", vrfv2Config.RateLimitUnitDuration.String()). + Uint16("RandomnessRequestCountPerRequest", vrfv2Config.RandomnessRequestCountPerRequest). + Uint16("RandomnessRequestCountPerRequestDeviation", vrfv2Config.RandomnessRequestCountPerRequestDeviation). + Bool("UseExistingEnv", vrfv2Config.UseExistingEnv). + Msg("Performance Test Configuration") + + if vrfv2Config.UseExistingEnv { + //todo: temporary solution with envconfig and toml config until VRF-662 is implemented + vrfv2Config.CoordinatorAddress = cfg.ExistingEnvConfig.CoordinatorAddress + vrfv2Config.ConsumerAddress = cfg.ExistingEnvConfig.ConsumerAddress + vrfv2Config.LinkAddress = cfg.ExistingEnvConfig.LinkAddress + vrfv2Config.SubscriptionFundingAmountLink = cfg.ExistingEnvConfig.SubFunding.SubFundsLink + vrfv2Config.SubID = cfg.ExistingEnvConfig.SubID + vrfv2Config.KeyHash = cfg.ExistingEnvConfig.KeyHash + + env, err = test_env.NewCLTestEnvBuilder(). + WithTestLogger(t). + WithCustomCleanup( + func() { + teardown(t, vrfv2Contracts.LoadTestConsumers[0], lc, updatedLabels, testReporter, testType, vrfv2Config) + if env.EVMClient.NetworkSimulated() { + l.Info(). + Str("Network Name", env.EVMClient.GetNetworkName()). + Msg("Network is a simulated network. Skipping fund return for Coordinator Subscriptions.") + } else { + if cfg.Common.CancelSubsAfterTestRun { + //cancel subs and return funds to sub owner + cancelSubsAndReturnFunds(subIDs, l) + } + } + }). + Build() + + require.NoError(t, err, "error creating test env") + + coordinator, err := env.ContractLoader.LoadVRFCoordinatorV2(vrfv2Config.CoordinatorAddress) + require.NoError(t, err) + + var consumers []contracts.VRFv2LoadTestConsumer + if cfg.ExistingEnvConfig.CreateFundSubsAndAddConsumers { + linkToken, err := env.ContractLoader.LoadLINKToken(vrfv2Config.LinkAddress) + require.NoError(t, err) + consumers, err = vrfv2_actions.DeployVRFV2Consumers(env.ContractDeployer, coordinator, 1) + require.NoError(t, err) + subIDs, err = vrfv2_actions.CreateFundSubsAndAddConsumers( + env, + vrfv2Config, + linkToken, + coordinator, + consumers, + vrfv2Config.NumberOfSubToCreate, + ) + require.NoError(t, err) + } else { + consumer, err := env.ContractLoader.LoadVRFv2LoadTestConsumer(vrfv2Config.ConsumerAddress) + require.NoError(t, err) + consumers = append(consumers, consumer) + subIDs = append(subIDs, vrfv2Config.SubID) + } + + err = FundNodesIfNeeded(cfg, env.EVMClient, l) + require.NoError(t, err) + + vrfv2Contracts = &vrfv2_actions.VRFV2Contracts{ + Coordinator: coordinator, + LoadTestConsumers: consumers, + BHS: nil, + } + + vrfv2Data = &vrfv2_actions.VRFV2Data{ + VRFV2KeyData: vrfv2_actions.VRFV2KeyData{ + VRFKey: nil, + EncodedProvingKey: [2]*big.Int{}, + KeyHash: common.HexToHash(vrfv2Config.KeyHash), + }, + VRFJob: nil, + PrimaryEthAddress: "", + ChainID: nil, + } + + } else { + //todo: temporary solution with envconfig and toml config until VRF-662 is implemented + vrfv2Config.ChainlinkNodeFunding = cfg.NewEnvConfig.NodeSendingKeyFunding + vrfv2Config.SubscriptionFundingAmountLink = cfg.NewEnvConfig.Funding.SubFundsLink + env, err = test_env.NewCLTestEnvBuilder(). + WithTestLogger(t). + WithGeth(). + WithCLNodes(1). + WithFunding(big.NewFloat(vrfv2Config.ChainlinkNodeFunding)). + WithCustomCleanup( + func() { + teardown(t, vrfv2Contracts.LoadTestConsumers[0], lc, updatedLabels, testReporter, testType, vrfv2Config) + + if env.EVMClient.NetworkSimulated() { + l.Info(). + Str("Network Name", env.EVMClient.GetNetworkName()). + Msg("Network is a simulated network. Skipping fund return for Coordinator Subscriptions.") + } else { + if cfg.Common.CancelSubsAfterTestRun { + //cancel subs and return funds to sub owner + cancelSubsAndReturnFunds(subIDs, l) + } + } + if err := env.Cleanup(); err != nil { + l.Error().Err(err).Msg("Error cleaning up test environment") + } + }). + WithLogWatcher(). + Build() + + require.NoError(t, err, "error creating test env") + + env.ParallelTransactions(true) + + mockETHLinkFeed, err := actions.DeployMockETHLinkFeed(env.ContractDeployer, big.NewInt(vrfv2Config.LinkNativeFeedResponse)) + require.NoError(t, err, "error deploying mock ETH/LINK feed") + + linkToken, err := actions.DeployLINKToken(env.ContractDeployer) + require.NoError(t, err, "error deploying LINK contract") + + vrfv2Contracts, subIDs, vrfv2Data, err = vrfv2_actions.SetupVRFV2Environment( + env, + vrfv2Config, + linkToken, + mockETHLinkFeed, + //register proving key against EOA address in order to return funds to this address + env.EVMClient.GetDefaultWallet().Address(), + 1, + vrfv2Config.NumberOfSubToCreate, + l, + ) + require.NoError(t, err, "error setting up VRF v2 env") } + eoaWalletAddress = env.EVMClient.GetDefaultWallet().Address() - multiFeedConfig := &wasp.Config{ - T: t, - LoadType: wasp.VU, - GenName: "vu", - VU: NewJobVolumeVU(cfg.SoakVolume.Pace.Duration(), 1, env.ClCluster.NodeAPIs(), env.EVMClient, vrfv2Contracts), - Labels: labels, - LokiConfig: wasp.NewEnvLokiConfig(), + l.Debug().Int("Number of Subs", len(subIDs)).Msg("Subs involved in the test") + for _, subID := range subIDs { + subscription, err := vrfv2Contracts.Coordinator.GetSubscription(context.Background(), subID) + require.NoError(t, err, "error getting subscription information for subscription %d", subID) + vrfv2_actions.LogSubDetails(l, subscription, subID, vrfv2Contracts.Coordinator) } - MonitorLoadStats(t, vrfv2Contracts, labels) + singleFeedConfig := &wasp.Config{ + T: t, + LoadType: wasp.RPS, + GenName: "gun", + RateLimitUnitDuration: vrfv2Config.RateLimitUnitDuration, + Gun: NewSingleHashGun( + vrfv2Contracts, + vrfv2Data.KeyHash, + subIDs, + vrfv2Config, + l, + ), + Labels: labels, + LokiConfig: lokiConfig, + CallTimeout: 2 * time.Minute, + } + require.Len(t, vrfv2Contracts.LoadTestConsumers, 1, "only one consumer should be created for Load Test") + consumer := vrfv2Contracts.LoadTestConsumers[0] + err = consumer.ResetMetrics() + require.NoError(t, err) + MonitorLoadStats(lc, consumer, updatedLabels) // is our "job" stable at all, no memory leaks, no flaking performance under some RPS? - t.Run("vrfv2 soak test", func(t *testing.T) { - singleFeedConfig.Schedule = wasp.Plain( - cfg.Soak.RPS, - cfg.Soak.Duration.Duration(), - ) - _, err := wasp.NewProfile(). - Add(wasp.NewGenerator(singleFeedConfig)). - Run(true) - require.NoError(t, err) - }) + t.Run("vrfv2 performance test", func(t *testing.T) { - // what are the limits for one "job", figuring out the max/optimal performance params by increasing RPS and varying configuration - t.Run("vrfv2 load test", func(t *testing.T) { - singleFeedConfig.Schedule = wasp.Steps( - cfg.Load.RPSFrom, - cfg.Load.RPSIncrease, - cfg.Load.RPSSteps, - cfg.Load.Duration.Duration(), + singleFeedConfig.Schedule = wasp.Plain( + vrfv2Config.RPS, + vrfv2Config.TestDuration, ) _, err = wasp.NewProfile(). Add(wasp.NewGenerator(singleFeedConfig)). Run(true) require.NoError(t, err) - }) - // how many "jobs" of the same type we can run at once at a stable load with optimal configuration? - t.Run("vrfv2 volume soak test", func(t *testing.T) { - multiFeedConfig.Schedule = wasp.Plain( - cfg.SoakVolume.Products, - cfg.SoakVolume.Duration.Duration(), - ) - _, err = wasp.NewProfile(). - Add(wasp.NewGenerator(multiFeedConfig)). - Run(true) + var wg sync.WaitGroup + wg.Add(1) + //todo - timeout should be configurable depending on the perf test type + requestCount, fulfilmentCount, err := vrfv2_actions.WaitForRequestCountEqualToFulfilmentCount(consumer, 2*time.Minute, &wg) require.NoError(t, err) - }) + wg.Wait() - // what are the limits if we add more and more "jobs/products" of the same type, each "job" have a stable RPS we vary only amount of jobs - t.Run("vrfv2 volume load test", func(t *testing.T) { - multiFeedConfig.Schedule = wasp.Steps( - cfg.LoadVolume.ProductsFrom, - cfg.LoadVolume.ProductsIncrease, - cfg.LoadVolume.ProductsSteps, - cfg.LoadVolume.Duration.Duration(), - ) - _, err = wasp.NewProfile(). - Add(wasp.NewGenerator(multiFeedConfig)). - Run(true) - require.NoError(t, err) + l.Info(). + Interface("Request Count", requestCount). + Interface("Fulfilment Count", fulfilmentCount). + Msg("Final Request/Fulfilment Stats") }) + +} + +func cancelSubsAndReturnFunds(subIDs []uint64, l zerolog.Logger) { + for _, subID := range subIDs { + l.Info(). + Uint64("Returning funds from SubID", subID). + Str("Returning funds to", eoaWalletAddress). + Msg("Canceling subscription and returning funds to subscription owner") + pendingRequestsExist, err := vrfv2Contracts.Coordinator.PendingRequestsExist(context.Background(), subID) + if err != nil { + l.Error().Err(err).Msg("Error checking if pending requests exist") + } + if !pendingRequestsExist { + _, err := vrfv2Contracts.Coordinator.CancelSubscription(subID, common.HexToAddress(eoaWalletAddress)) + if err != nil { + l.Error().Err(err).Msg("Error canceling subscription") + } + } else { + l.Error().Uint64("Sub ID", subID).Msg("Pending requests exist for subscription, cannot cancel subscription and return funds") + } + } +} + +func FundNodesIfNeeded(cfg *PerformanceConfig, client blockchain.EVMClient, l zerolog.Logger) error { + if cfg.ExistingEnvConfig.NodeSendingKeyFundingMin > 0 { + for _, sendingKey := range cfg.ExistingEnvConfig.NodeSendingKeys { + address := common.HexToAddress(sendingKey) + sendingKeyBalance, err := client.BalanceAt(context.Background(), address) + if err != nil { + return err + } + fundingAtLeast := conversions.EtherToWei(big.NewFloat(cfg.ExistingEnvConfig.NodeSendingKeyFundingMin)) + fundingToSendWei := new(big.Int).Sub(fundingAtLeast, sendingKeyBalance) + fundingToSendEth := conversions.WeiToEther(fundingToSendWei) + if fundingToSendWei.Cmp(big.NewInt(0)) == 1 { + l.Info(). + Str("Sending Key", sendingKey). + Str("Sending Key Current Balance", sendingKeyBalance.String()). + Str("Should have at least", fundingAtLeast.String()). + Str("Funding Amount in ETH", fundingToSendEth.String()). + Msg("Funding Node's Sending Key") + gasEstimates, err := client.EstimateGas(ethereum.CallMsg{ + To: &address, + }) + if err != nil { + return err + } + err = client.Fund(sendingKey, fundingToSendEth, gasEstimates) + if err != nil { + return err + } + } else { + l.Info(). + Str("Sending Key", sendingKey). + Str("Sending Key Current Balance", sendingKeyBalance.String()). + Str("Should have at least", fundingAtLeast.String()). + Msg("Skipping Node's Sending Key funding as it has enough funds") + } + } + } + return nil +} + +func teardown( + t *testing.T, + consumer contracts.VRFv2LoadTestConsumer, + lc *wasp.LokiClient, + updatedLabels map[string]string, + testReporter *testreporters.VRFV2TestReporter, + testType string, + vrfv2Config vrfv2_config.VRFV2Config, +) { + //send final results to Loki + metrics := GetLoadTestMetrics(consumer) + SendMetricsToLoki(metrics, lc, updatedLabels) + //set report data for Slack notification + testReporter.SetReportData( + testType, + metrics.RequestCount, + metrics.FulfilmentCount, + metrics.AverageFulfillmentInMillions, + metrics.SlowestFulfillment, + metrics.FastestFulfillment, + vrfv2Config, + ) + + // send Slack notification + err := testReporter.SendSlackNotification(t, nil) + if err != nil { + log.Warn().Err(err).Msg("Error sending Slack notification") + } } diff --git a/integration-tests/load/vrfv2/vu.go b/integration-tests/load/vrfv2/vu.go deleted file mode 100644 index 7eb02ae330f..00000000000 --- a/integration-tests/load/vrfv2/vu.go +++ /dev/null @@ -1,94 +0,0 @@ -package loadvrfv2 - -import ( - "fmt" - "time" - - "github.com/smartcontractkit/wasp" - - "github.com/smartcontractkit/chainlink-testing-framework/blockchain" - "github.com/smartcontractkit/chainlink/integration-tests/actions/vrfv2_actions" - vrfConst "github.com/smartcontractkit/chainlink/integration-tests/actions/vrfv2_actions/vrfv2_constants" - "github.com/smartcontractkit/chainlink/integration-tests/client" -) - -/* JobVolumeVU is a "virtual user" that creates a VRFv2 job and constantly requesting new randomness only for this job instance */ - -type JobVolumeVU struct { - pace time.Duration - minIncomingConfirmations uint16 - nodes []*client.ChainlinkClient - bc blockchain.EVMClient - contracts *vrfv2_actions.VRFV2Contracts - jobs []vrfv2_actions.VRFV2JobInfo - keyHash [32]byte - stop chan struct{} -} - -func NewJobVolumeVU( - pace time.Duration, - confirmations uint16, - nodes []*client.ChainlinkClient, - bc blockchain.EVMClient, - contracts *vrfv2_actions.VRFV2Contracts, -) *JobVolumeVU { - return &JobVolumeVU{ - pace: pace, - minIncomingConfirmations: confirmations, - nodes: nodes, - bc: bc, - contracts: contracts, - stop: make(chan struct{}, 1), - } -} - -func (m *JobVolumeVU) Clone(_ *wasp.Generator) wasp.VirtualUser { - return &JobVolumeVU{ - pace: m.pace, - minIncomingConfirmations: m.minIncomingConfirmations, - nodes: m.nodes, - bc: m.bc, - contracts: m.contracts, - stop: make(chan struct{}, 1), - } -} - -func (m *JobVolumeVU) Setup(_ *wasp.Generator) error { - jobs, err := vrfv2_actions.CreateVRFV2Jobs(m.nodes, m.contracts.Coordinator, m.bc, m.minIncomingConfirmations) - if err != nil { - return fmt.Errorf("failed to create VRFv2 jobs in setup: %w", err) - } - m.jobs = jobs - m.keyHash = jobs[0].KeyHash - return nil -} - -func (m *JobVolumeVU) Teardown(_ *wasp.Generator) error { - return nil -} - -func (m *JobVolumeVU) Call(l *wasp.Generator) { - time.Sleep(m.pace) - tn := time.Now() - err := m.contracts.LoadTestConsumer.RequestRandomness( - m.keyHash, - vrfConst.SubID, - vrfConst.MinimumConfirmations, - vrfConst.CallbackGasLimit, - vrfConst.NumberOfWords, - vrfConst.RandomnessRequestCountPerRequest, - ) - if err != nil { - l.ResponsesChan <- &wasp.CallResult{Duration: time.Since(tn), Error: err.Error(), Failed: true} - return - } - l.ResponsesChan <- &wasp.CallResult{Duration: time.Since(tn)} -} - -func (m *JobVolumeVU) Stop(_ *wasp.Generator) { - m.stop <- struct{}{} -} - -func (m *JobVolumeVU) StopChan() chan struct{} { - return m.stop -} diff --git a/integration-tests/load/vrfv2plus/config.go b/integration-tests/load/vrfv2plus/config.go index 96dbf99c6b2..43bcf44d044 100644 --- a/integration-tests/load/vrfv2plus/config.go +++ b/integration-tests/load/vrfv2plus/config.go @@ -41,8 +41,9 @@ type ExistingEnvConfig struct { LinkAddress string `toml:"link_address"` SubID string `toml:"sub_id"` KeyHash string `toml:"key_hash"` - SubFunding - CreateFundSubsAndAddConsumers bool `toml:"create_fund_subs_and_add_consumers"` + Funding + CreateFundSubsAndAddConsumers bool `toml:"create_fund_subs_and_add_consumers"` + NodeSendingKeys []string `toml:"node_sending_keys"` } type NewEnvConfig struct { @@ -50,12 +51,14 @@ type NewEnvConfig struct { } type Common struct { - MinimumConfirmations uint16 `toml:"minimum_confirmations"` + MinimumConfirmations uint16 `toml:"minimum_confirmations"` + CancelSubsAfterTestRun bool `toml:"cancel_subs_after_test_run"` } type Funding struct { - NodeFunds float64 `toml:"node_funds"` SubFunding + NodeSendingKeyFunding float64 `toml:"node_sending_key_funding"` + NodeSendingKeyFundingMin float64 `toml:"node_sending_key_funding_min"` } type SubFunding struct { diff --git a/integration-tests/load/vrfv2plus/config.toml b/integration-tests/load/vrfv2plus/config.toml index e3200fafe22..8ccf60c6d80 100644 --- a/integration-tests/load/vrfv2plus/config.toml +++ b/integration-tests/load/vrfv2plus/config.toml @@ -1,21 +1,31 @@ [Common] minimum_confirmations = 3 +cancel_subs_after_test_run = true [NewEnvConfig] sub_funds_link = 1 sub_funds_native = 1 node_funds = 10 +node_sending_key_funding = 1000 [ExistingEnvConfig] -coordinator_address = "0x27b61f155F772b291D1d9B478BeAd37B2Ae447b0" -#consumer_address = "0x087F232165D9bA1A602f148025e5D0666953F64a" -#sub_id = "52116875585187328970776211988181422347535732407068188096422095950800466618218" -key_hash = "0x787d74caea10b2b357790d5b5247c2f63d1d91572a9846f780606e4d953677ae" +coordinator_address = "" +consumer_address = "" +sub_id = 1 +key_hash = "" create_fund_subs_and_add_consumers = true -link_address = "0x779877A7B0D9E8603169DdbD7836e478b4624789" -sub_funds_link = 3 -sub_funds_native = 1 +link_address = "" +sub_funds_link = 10 +node_sending_key_funding_min = 1 +node_sending_keys = [ + "", + "", + "", + "", + "", + "", +] # 10 RPM - 1 tx request with 1 rand request in each tx every 6 seconds [Soak] @@ -25,7 +35,7 @@ randomness_request_count_per_request = 1 # amount of randomness requests to make randomness_request_count_per_request_deviation = 0 #NOTE - deviation should be less than randomness_request_count_per_request setting number_of_sub_to_create = 1 -# approx 60 RPM - 1 tx request with 4 rand requests in each tx every 3 seconds +# approx 60 RPM - 1 tx request with 3 rand requests in each tx every 3 seconds [Load] rate_limit_unit_duration = "3s" rps = 1 @@ -47,4 +57,4 @@ rate_limit_unit_duration = "1m" rps = 1 randomness_request_count_per_request = 150 # amount of randomness requests to make per one TX request randomness_request_count_per_request_deviation = 0 #NOTE - deviation should be less than randomness_request_count_per_request setting -number_of_sub_to_create = 1 \ No newline at end of file +number_of_sub_to_create = 1 diff --git a/integration-tests/load/vrfv2plus/vrfv2plus_test.go b/integration-tests/load/vrfv2plus/vrfv2plus_test.go index 11e160297f0..ac22b5256f6 100644 --- a/integration-tests/load/vrfv2plus/vrfv2plus_test.go +++ b/integration-tests/load/vrfv2plus/vrfv2plus_test.go @@ -1,19 +1,24 @@ package loadvrfv2plus import ( + "context" "math/big" "os" "sync" "testing" "time" + "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/kelseyhightower/envconfig" + "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/smartcontractkit/wasp" "github.com/stretchr/testify/require" + "github.com/smartcontractkit/chainlink-testing-framework/blockchain" "github.com/smartcontractkit/chainlink-testing-framework/logging" + "github.com/smartcontractkit/chainlink-testing-framework/utils/conversions" "github.com/smartcontractkit/chainlink-testing-framework/utils/testcontext" "github.com/smartcontractkit/chainlink/integration-tests/testreporters" @@ -93,25 +98,9 @@ func TestVRFV2PlusPerformance(t *testing.T) { Str("Network Name", env.EVMClient.GetNetworkName()). Msg("Network is a simulated network. Skipping fund return for Coordinator Subscriptions.") } else { - //cancel subs and return funds to sub owner - for _, subID := range subIDs { - l.Info(). - Str("Returning funds from SubID", subID.String()). - Str("Returning funds to", eoaWalletAddress). - Msg("Canceling subscription and returning funds to subscription owner") - pendingRequestsExist, err := vrfv2PlusContracts.Coordinator.PendingRequestsExist(testcontext.Get(t), subID) - if err != nil { - l.Error().Err(err).Msg("Error checking if pending requests exist") - } - if !pendingRequestsExist { - _, err := vrfv2PlusContracts.Coordinator.CancelSubscription(subID, common.HexToAddress(eoaWalletAddress)) - if err != nil { - l.Error().Err(err).Msg("Error canceling subscription") - } - } else { - l.Error().Str("Sub ID", subID.String()).Msg("Pending requests exist for subscription, cannot cancel subscription and return funds") - } - + if cfg.Common.CancelSubsAfterTestRun { + //cancel subs and return funds to sub owner + cancelSubsAndReturnFunds(subIDs, l) } } }). @@ -147,6 +136,9 @@ func TestVRFV2PlusPerformance(t *testing.T) { subIDs = append(subIDs, subID) } + err = FundNodesIfNeeded(cfg, env.EVMClient, l) + require.NoError(t, err) + vrfv2PlusContracts = &vrfv2plus.VRFV2_5Contracts{ Coordinator: coordinator, LoadTestConsumers: consumers, @@ -166,7 +158,7 @@ func TestVRFV2PlusPerformance(t *testing.T) { } else { //todo: temporary solution with envconfig and toml config until VRF-662 is implemented - vrfv2PlusConfig.ChainlinkNodeFunding = cfg.NewEnvConfig.NodeFunds + vrfv2PlusConfig.ChainlinkNodeFunding = cfg.NewEnvConfig.NodeSendingKeyFunding vrfv2PlusConfig.SubscriptionFundingAmountLink = cfg.NewEnvConfig.Funding.SubFundsLink vrfv2PlusConfig.SubscriptionFundingAmountNative = cfg.NewEnvConfig.Funding.SubFundsNative env, err = test_env.NewCLTestEnvBuilder(). @@ -183,18 +175,10 @@ func TestVRFV2PlusPerformance(t *testing.T) { Str("Network Name", env.EVMClient.GetNetworkName()). Msg("Network is a simulated network. Skipping fund return for Coordinator Subscriptions.") } else { - for _, subID := range subIDs { - l.Info(). - Str("Returning funds from SubID", subID.String()). - Str("Returning funds to", eoaWalletAddress). - Msg("Canceling subscription and returning funds to subscription owner") - _, err := vrfv2PlusContracts.Coordinator.CancelSubscription(subID, common.HexToAddress(eoaWalletAddress)) - if err != nil { - l.Error().Err(err).Msg("Error canceling subscription") - } + if cfg.Common.CancelSubsAfterTestRun { + //cancel subs and return funds to sub owner + cancelSubsAndReturnFunds(subIDs, l) } - //err = vrfv2plus.ReturnFundsForFulfilledRequests(env.EVMClient, vrfv2PlusContracts.Coordinator, l) - //l.Error().Err(err).Msg("Error returning funds for fulfilled requests") } if err := env.Cleanup(); err != nil { l.Error().Err(err).Msg("Error cleaning up test environment") @@ -281,6 +265,68 @@ func TestVRFV2PlusPerformance(t *testing.T) { Interface("Fulfilment Count", fulfilmentCount). Msg("Final Request/Fulfilment Stats") }) + +} + +func cancelSubsAndReturnFunds(subIDs []*big.Int, l zerolog.Logger) { + for _, subID := range subIDs { + l.Info(). + Str("Returning funds from SubID", subID.String()). + Str("Returning funds to", eoaWalletAddress). + Msg("Canceling subscription and returning funds to subscription owner") + pendingRequestsExist, err := vrfv2PlusContracts.Coordinator.PendingRequestsExist(context.Background(), subID) + if err != nil { + l.Error().Err(err).Msg("Error checking if pending requests exist") + } + if !pendingRequestsExist { + _, err := vrfv2PlusContracts.Coordinator.CancelSubscription(subID, common.HexToAddress(eoaWalletAddress)) + if err != nil { + l.Error().Err(err).Msg("Error canceling subscription") + } + } else { + l.Error().Str("Sub ID", subID.String()).Msg("Pending requests exist for subscription, cannot cancel subscription and return funds") + } + } +} + +func FundNodesIfNeeded(cfg *PerformanceConfig, client blockchain.EVMClient, l zerolog.Logger) error { + if cfg.ExistingEnvConfig.NodeSendingKeyFundingMin > 0 { + for _, sendingKey := range cfg.ExistingEnvConfig.NodeSendingKeys { + address := common.HexToAddress(sendingKey) + sendingKeyBalance, err := client.BalanceAt(context.Background(), address) + if err != nil { + return err + } + fundingAtLeast := conversions.EtherToWei(big.NewFloat(cfg.ExistingEnvConfig.NodeSendingKeyFundingMin)) + fundingToSendWei := new(big.Int).Sub(fundingAtLeast, sendingKeyBalance) + fundingToSendEth := conversions.WeiToEther(fundingToSendWei) + if fundingToSendWei.Cmp(big.NewInt(0)) == 1 { + l.Info(). + Str("Sending Key", sendingKey). + Str("Sending Key Current Balance", sendingKeyBalance.String()). + Str("Should have at least", fundingAtLeast.String()). + Str("Funding Amount in ETH", fundingToSendEth.String()). + Msg("Funding Node's Sending Key") + gasEstimates, err := client.EstimateGas(ethereum.CallMsg{ + To: &address, + }) + if err != nil { + return err + } + err = client.Fund(sendingKey, fundingToSendEth, gasEstimates) + if err != nil { + return err + } + } else { + l.Info(). + Str("Sending Key", sendingKey). + Str("Sending Key Current Balance", sendingKeyBalance.String()). + Str("Should have at least", fundingAtLeast.String()). + Msg("Skipping Node's Sending Key funding as it has enough funds") + } + } + } + return nil } func teardown( diff --git a/integration-tests/smoke/vrfv2_test.go b/integration-tests/smoke/vrfv2_test.go index 09024b28ba7..c7b48fc3a35 100644 --- a/integration-tests/smoke/vrfv2_test.go +++ b/integration-tests/smoke/vrfv2_test.go @@ -1,115 +1,107 @@ package smoke import ( + "context" "math/big" "testing" - "time" - "github.com/onsi/gomega" + "github.com/kelseyhightower/envconfig" "github.com/stretchr/testify/require" + "github.com/smartcontractkit/chainlink/integration-tests/actions/vrfv2_actions" + "github.com/smartcontractkit/chainlink/integration-tests/actions/vrfv2_actions/vrfv2_config" + "github.com/smartcontractkit/chainlink-testing-framework/logging" - "github.com/smartcontractkit/chainlink-testing-framework/utils/testcontext" "github.com/smartcontractkit/chainlink/integration-tests/actions" - "github.com/smartcontractkit/chainlink/integration-tests/actions/vrfv2_actions" - vrfConst "github.com/smartcontractkit/chainlink/integration-tests/actions/vrfv2_actions/vrfv2_constants" "github.com/smartcontractkit/chainlink/integration-tests/docker/test_env" - "github.com/smartcontractkit/chainlink/integration-tests/types/config/node" ) func TestVRFv2Basic(t *testing.T) { t.Parallel() l := logging.GetTestLogger(t) + var vrfv2Config vrfv2_config.VRFV2Config + err := envconfig.Process("VRFV2", &vrfv2Config) + require.NoError(t, err) + env, err := test_env.NewCLTestEnvBuilder(). WithTestLogger(t). WithGeth(). WithCLNodes(1). - WithFunding(vrfConst.ChainlinkNodeFundingAmountEth). + WithFunding(big.NewFloat(vrfv2Config.ChainlinkNodeFunding)). WithStandardCleanup(). Build() - require.NoError(t, err) - env.ParallelTransactions(true) + require.NoError(t, err, "error creating test env") - mockFeed, err := actions.DeployMockETHLinkFeed(env.ContractDeployer, vrfConst.LinkEthFeedResponse) - require.NoError(t, err) - lt, err := actions.DeployLINKToken(env.ContractDeployer) - require.NoError(t, err) - vrfv2Contracts, err := vrfv2_actions.DeployVRFV2Contracts(env.ContractDeployer, env.EVMClient, lt, mockFeed) - require.NoError(t, err) - - err = env.EVMClient.WaitForEvents() - require.NoError(t, err) - - err = vrfv2Contracts.Coordinator.SetConfig( - vrfConst.MinimumConfirmations, - vrfConst.MaxGasLimitVRFCoordinatorConfig, - vrfConst.StalenessSeconds, - vrfConst.GasAfterPaymentCalculation, - vrfConst.LinkEthFeedResponse, - vrfConst.VRFCoordinatorV2FeeConfig, - ) - require.NoError(t, err) - err = env.EVMClient.WaitForEvents() - require.NoError(t, err) - - err = vrfv2Contracts.Coordinator.CreateSubscription() - require.NoError(t, err) - err = env.EVMClient.WaitForEvents() - require.NoError(t, err) - - err = vrfv2Contracts.Coordinator.AddConsumer(vrfConst.SubID, vrfv2Contracts.LoadTestConsumer.Address()) - require.NoError(t, err) - - err = vrfv2_actions.FundVRFCoordinatorV2Subscription(lt, vrfv2Contracts.Coordinator, env.EVMClient, vrfConst.SubID, vrfConst.VRFSubscriptionFundingAmountLink) - require.NoError(t, err) + env.ParallelTransactions(true) - vrfV2jobs, err := vrfv2_actions.CreateVRFV2Jobs(env.ClCluster.NodeAPIs(), vrfv2Contracts.Coordinator, env.EVMClient, vrfConst.MinimumConfirmations) + mockETHLinkFeed, err := actions.DeployMockETHLinkFeed(env.ContractDeployer, big.NewInt(vrfv2Config.LinkNativeFeedResponse)) require.NoError(t, err) - - // this part is here because VRFv2 can work with only a specific key - // [[EVM.KeySpecific]] - // Key = '...' - addr, err := env.ClCluster.Nodes[0].API.PrimaryEthAddress() - require.NoError(t, err) - nodeConfig := node.NewConfig(env.ClCluster.Nodes[0].NodeConfig, - node.WithVRFv2EVMEstimator(addr), - ) - err = env.ClCluster.Nodes[0].Restart(nodeConfig) + linkToken, err := actions.DeployLINKToken(env.ContractDeployer) require.NoError(t, err) - // test and assert - err = vrfv2Contracts.LoadTestConsumer.RequestRandomness( - vrfV2jobs[0].KeyHash, - vrfConst.SubID, - vrfConst.MinimumConfirmations, - vrfConst.CallbackGasLimit, - vrfConst.NumberOfWords, - vrfConst.RandomnessRequestCountPerRequest, + // register proving key against oracle address (sending key) in order to test oracleWithdraw + defaultWalletAddress := env.EVMClient.GetDefaultWallet().Address() + + vrfv2Contracts, subIDs, vrfv2Data, err := vrfv2_actions.SetupVRFV2Environment( + env, + vrfv2Config, + linkToken, + mockETHLinkFeed, + defaultWalletAddress, + 1, + 1, + l, ) - require.NoError(t, err) - - gom := gomega.NewGomegaWithT(t) - timeout := time.Minute * 2 - var lastRequestID *big.Int - gom.Eventually(func(g gomega.Gomega) { - jobRuns, err := env.ClCluster.Nodes[0].API.MustReadRunsByJob(vrfV2jobs[0].Job.Data.ID) - g.Expect(err).ShouldNot(gomega.HaveOccurred()) - g.Expect(len(jobRuns.Data)).Should(gomega.BeNumerically("==", 1)) - lastRequestID, err = vrfv2Contracts.LoadTestConsumer.GetLastRequestId(testcontext.Get(t)) - l.Debug().Interface("Last Request ID", lastRequestID).Msg("Last Request ID Received") - - g.Expect(err).ShouldNot(gomega.HaveOccurred()) - status, err := vrfv2Contracts.LoadTestConsumer.GetRequestStatus(testcontext.Get(t), lastRequestID) - g.Expect(err).ShouldNot(gomega.HaveOccurred()) - g.Expect(status.Fulfilled).Should(gomega.BeTrue()) - l.Debug().Interface("Fulfilment Status", status.Fulfilled).Msg("Random Words Request Fulfilment Status") - - g.Expect(err).ShouldNot(gomega.HaveOccurred()) + require.NoError(t, err, "error setting up VRF v2 env") + + subID := subIDs[0] + + subscription, err := vrfv2Contracts.Coordinator.GetSubscription(context.Background(), subID) + require.NoError(t, err, "error getting subscription information") + + vrfv2_actions.LogSubDetails(l, subscription, subID, vrfv2Contracts.Coordinator) + + t.Run("Request Randomness", func(t *testing.T) { + testConfig := vrfv2Config + subBalanceBeforeRequest := subscription.Balance + + jobRunsBeforeTest, err := env.ClCluster.Nodes[0].API.MustReadRunsByJob(vrfv2Data.VRFJob.Data.ID) + require.NoError(t, err, "error reading job runs") + + // test and assert + randomWordsFulfilledEvent, err := vrfv2_actions.RequestRandomnessAndWaitForFulfillment( + vrfv2Contracts.LoadTestConsumers[0], + vrfv2Contracts.Coordinator, + vrfv2Data, + subID, + testConfig.RandomnessRequestCountPerRequest, + testConfig, + testConfig.RandomWordsFulfilledEventTimeout, + l, + ) + require.NoError(t, err, "error requesting randomness and waiting for fulfilment") + + expectedSubBalanceJuels := new(big.Int).Sub(subBalanceBeforeRequest, randomWordsFulfilledEvent.Payment) + subscription, err = vrfv2Contracts.Coordinator.GetSubscription(context.Background(), subID) + require.NoError(t, err, "error getting subscription information") + subBalanceAfterRequest := subscription.Balance + require.Equal(t, expectedSubBalanceJuels, subBalanceAfterRequest) + + jobRuns, err := env.ClCluster.Nodes[0].API.MustReadRunsByJob(vrfv2Data.VRFJob.Data.ID) + require.NoError(t, err, "error reading job runs") + require.Equal(t, len(jobRunsBeforeTest.Data)+1, len(jobRuns.Data)) + + status, err := vrfv2Contracts.LoadTestConsumers[0].GetRequestStatus(context.Background(), randomWordsFulfilledEvent.RequestId) + require.NoError(t, err, "error getting rand request status") + require.True(t, status.Fulfilled) + l.Debug().Bool("Fulfilment Status", status.Fulfilled).Msg("Random Words Request Fulfilment Status") + + require.Equal(t, testConfig.NumberOfWords, uint32(len(status.RandomWords))) for _, w := range status.RandomWords { - l.Info().Uint64("Output", w.Uint64()).Msg("Randomness fulfilled") - g.Expect(w.Uint64()).Should(gomega.BeNumerically(">", 0), "Expected the VRF job give an answer bigger than 0") + l.Info().Str("Output", w.String()).Msg("Randomness fulfilled") + require.Equal(t, 1, w.Cmp(big.NewInt(0)), "Expected the VRF job give an answer bigger than 0") } - }, timeout, "1s").Should(gomega.Succeed()) + }) } diff --git a/integration-tests/smoke/vrfv2plus_test.go b/integration-tests/smoke/vrfv2plus_test.go index b171ea65f99..9ce9f216995 100644 --- a/integration-tests/smoke/vrfv2plus_test.go +++ b/integration-tests/smoke/vrfv2plus_test.go @@ -49,7 +49,16 @@ func TestVRFv2Plus(t *testing.T) { // register proving key against oracle address (sending key) in order to test oracleWithdraw defaultWalletAddress := env.EVMClient.GetDefaultWallet().Address() - vrfv2PlusContracts, subIDs, vrfv2PlusData, err := vrfv2plus.SetupVRFV2_5Environment(env, vrfv2PlusConfig, linkToken, mockETHLinkFeed, defaultWalletAddress, 1, 1, l) + vrfv2PlusContracts, subIDs, vrfv2PlusData, err := vrfv2plus.SetupVRFV2_5Environment( + env, + vrfv2PlusConfig, + linkToken, + mockETHLinkFeed, + defaultWalletAddress, + 1, + 1, + l, + ) require.NoError(t, err, "error setting up VRF v2_5 env") subID := subIDs[0] @@ -380,6 +389,7 @@ func TestVRFv2Plus(t *testing.T) { require.NoError(t, err) require.False(t, pendingRequestsExist, "Pending requests should not exist") + randomWordsFulfilledEventTimeout := 5 * time.Second _, err = vrfv2plus.RequestRandomnessAndWaitForFulfillment( vrfv2PlusContracts.LoadTestConsumers[0], vrfv2PlusContracts.Coordinator, @@ -388,7 +398,7 @@ func TestVRFv2Plus(t *testing.T) { false, testConfig.RandomnessRequestCountPerRequest, testConfig, - 5*time.Second, + randomWordsFulfilledEventTimeout, l, ) @@ -402,7 +412,7 @@ func TestVRFv2Plus(t *testing.T) { true, testConfig.RandomnessRequestCountPerRequest, testConfig, - testConfig.RandomWordsFulfilledEventTimeout, + randomWordsFulfilledEventTimeout, l, ) @@ -486,7 +496,7 @@ func TestVRFv2Plus(t *testing.T) { Msg("Sub funds returned") //todo - need to use different wallet for each test to verify exact amount of Native/LINK returned - //todo - as defaultWallet is used in other tests in parallel which might affect the balance + //todo - as defaultWallet is used in other tests in parallel which might affect the balance - TT-684 //require.Equal(t, 1, walletBalanceNativeAfterSubCancelling.Cmp(walletBalanceNativeBeforeSubCancelling), "Native funds were not returned after sub cancellation") //todo - this fails on SIMULATED env as tx cost is calculated different as for testnets and it's not receipt.EffectiveGasPrice*receipt.GasUsed diff --git a/integration-tests/testreporters/vrfv2.go b/integration-tests/testreporters/vrfv2.go index c1ab816d4e8..2a72e4d91d0 100644 --- a/integration-tests/testreporters/vrfv2.go +++ b/integration-tests/testreporters/vrfv2.go @@ -1,173 +1,92 @@ package testreporters import ( - "encoding/csv" "fmt" + "math/big" "os" - "path/filepath" "testing" "time" - "github.com/rs/zerolog/log" + "github.com/smartcontractkit/chainlink/integration-tests/actions/vrfv2_actions/vrfv2_config" + "github.com/slack-go/slack" "github.com/smartcontractkit/chainlink-testing-framework/testreporters" ) -type VRFV2SoakTestReporter struct { - Reports map[string]*VRFV2SoakTestReport // contractAddress: Report - namespace string - csvLocation string -} - -type VRFV2SoakTestReport struct { - ContractAddress string - TotalRounds uint - - averageRoundTime time.Duration - LongestRoundTime time.Duration - ShortestRoundTime time.Duration - totalRoundTimes time.Duration - - averageRoundBlocks uint - LongestRoundBlocks uint - ShortestRoundBlocks uint - totalBlockLengths uint -} - -// SetNamespace sets the namespace of the report for clean reports -func (o *VRFV2SoakTestReporter) SetNamespace(namespace string) { - o.namespace = namespace +type VRFV2TestReporter struct { + TestType string + RequestCount *big.Int + FulfilmentCount *big.Int + AverageFulfillmentInMillions *big.Int + SlowestFulfillment *big.Int + FastestFulfillment *big.Int + Vrfv2Config *vrfv2_config.VRFV2Config } -// WriteReport writes VRFV2 Soak test report to logs -func (o *VRFV2SoakTestReporter) WriteReport(folderLocation string) error { - for _, report := range o.Reports { - report.averageRoundBlocks = report.totalBlockLengths / report.TotalRounds - report.averageRoundTime = time.Duration(report.totalRoundTimes.Nanoseconds() / int64(report.TotalRounds)) - } - if err := o.writeCSV(folderLocation); err != nil { - return err - } - - log.Info().Msg("VRFV2 Soak Test Report") - log.Info().Msg("--------------------") - for contractAddress, report := range o.Reports { - log.Info(). - Str("Contract Address", report.ContractAddress). - Uint("Total Rounds Processed", report.TotalRounds). - Str("Average Round Time", fmt.Sprint(report.averageRoundTime)). - Str("Longest Round Time", fmt.Sprint(report.LongestRoundTime)). - Str("Shortest Round Time", fmt.Sprint(report.ShortestRoundTime)). - Uint("Average Round Blocks", report.averageRoundBlocks). - Uint("Longest Round Blocks", report.LongestRoundBlocks). - Uint("Shortest Round Blocks", report.ShortestRoundBlocks). - Msg(contractAddress) - } - log.Info().Msg("--------------------") - return nil +func (o *VRFV2TestReporter) SetReportData( + testType string, + RequestCount *big.Int, + FulfilmentCount *big.Int, + AverageFulfillmentInMillions *big.Int, + SlowestFulfillment *big.Int, + FastestFulfillment *big.Int, + vrfv2Config vrfv2_config.VRFV2Config, +) { + o.TestType = testType + o.RequestCount = RequestCount + o.FulfilmentCount = FulfilmentCount + o.AverageFulfillmentInMillions = AverageFulfillmentInMillions + o.SlowestFulfillment = SlowestFulfillment + o.FastestFulfillment = FastestFulfillment + o.Vrfv2Config = &vrfv2Config } -// SendNotification sends a slack message to a slack webhook and uploads test artifacts -func (o *VRFV2SoakTestReporter) SendSlackNotification(t *testing.T, slackClient *slack.Client) error { +// SendSlackNotification sends a slack message to a slack webhook +func (o *VRFV2TestReporter) SendSlackNotification(t *testing.T, slackClient *slack.Client) error { if slackClient == nil { slackClient = slack.New(testreporters.SlackAPIKey) } testFailed := t.Failed() - headerText := ":white_check_mark: VRFV2 Soak Test PASSED :white_check_mark:" + headerText := fmt.Sprintf(":white_check_mark: VRF V2 %s Test PASSED :white_check_mark:", o.TestType) if testFailed { - headerText = ":x: VRFV2 Soak Test FAILED :x:" - } - messageBlocks := testreporters.CommonSlackNotificationBlocks( - headerText, o.namespace, o.csvLocation, - ) - ts, err := testreporters.SendSlackMessage(slackClient, slack.MsgOptionBlocks(messageBlocks...)) - if err != nil { - return err + headerText = fmt.Sprintf(":x: VRF V2 %s Test FAILED :x:", o.TestType) } - return testreporters.UploadSlackFile(slackClient, slack.FileUploadParameters{ - Title: fmt.Sprintf("VRFV2 Soak Test Report %s", o.namespace), - Filetype: "csv", - Filename: fmt.Sprintf("vrfv2_soak_%s.csv", o.namespace), - File: o.csvLocation, - InitialComment: fmt.Sprintf("VRFV2 Soak Test Report %s.", o.namespace), - Channels: []string{testreporters.SlackChannel}, - ThreadTimestamp: ts, + messageBlocks := testreporters.SlackNotifyBlocks(headerText, os.Getenv("SELECTED_NETWORKS"), []string{ + fmt.Sprintf( + "Summary\n"+ + "Perf Test Type: %s\n"+ + "Test Duration set in parameters: %s\n"+ + "Use Existing Env: %t\n"+ + "Request Count: %s\n"+ + "Fulfilment Count: %s\n"+ + "AverageFulfillmentInMillions: %s\n"+ + "Slowest Fulfillment: %s\n"+ + "Fastest Fulfillment: %s \n"+ + "RPS: %d\n"+ + "RateLimitUnitDuration: %s\n"+ + "RandomnessRequestCountPerRequest: %d\n"+ + "RandomnessRequestCountPerRequestDeviation: %d\n", + o.TestType, + o.Vrfv2Config.TestDuration.Truncate(time.Second).String(), + o.Vrfv2Config.UseExistingEnv, + o.RequestCount.String(), + o.FulfilmentCount.String(), + o.AverageFulfillmentInMillions.String(), + o.SlowestFulfillment.String(), + o.FastestFulfillment.String(), + o.Vrfv2Config.RPS, + o.Vrfv2Config.RateLimitUnitDuration.String(), + o.Vrfv2Config.RandomnessRequestCountPerRequest, + o.Vrfv2Config.RandomnessRequestCountPerRequestDeviation, + ), }) -} - -// UpdateReport updates the report based on the latest info -func (o *VRFV2SoakTestReport) UpdateReport(roundTime time.Duration, blockLength uint) { - // Updates min values from default 0 - if o.ShortestRoundBlocks == 0 { - o.ShortestRoundBlocks = blockLength - } - if o.ShortestRoundTime == 0 { - o.ShortestRoundTime = roundTime - } - o.TotalRounds++ - o.totalRoundTimes += roundTime - o.totalBlockLengths += blockLength - if roundTime >= o.LongestRoundTime { - o.LongestRoundTime = roundTime - } - if roundTime <= o.ShortestRoundTime { - o.ShortestRoundTime = roundTime - } - if blockLength >= o.LongestRoundBlocks { - o.LongestRoundBlocks = blockLength - } - if blockLength <= o.ShortestRoundBlocks { - o.ShortestRoundBlocks = blockLength - } -} - -// writes a CSV report on the test runner -func (o *VRFV2SoakTestReporter) writeCSV(folderLocation string) error { - reportLocation := filepath.Join(folderLocation, "./vrfv2_soak_report.csv") - log.Debug().Str("Location", reportLocation).Msg("Writing VRFV2 report") - o.csvLocation = reportLocation - vrfv2ReportFile, err := os.Create(reportLocation) - if err != nil { - return err - } - defer vrfv2ReportFile.Close() - vrfv2ReportWriter := csv.NewWriter(vrfv2ReportFile) - err = vrfv2ReportWriter.Write([]string{ - "Contract Index", - "Contract Address", - "Total Rounds Processed", - "Average Round Time", - "Longest Round Time", - "Shortest Round Time", - "Average Round Blocks", - "Longest Round Blocks", - "Shortest Round Blocks", - }) + _, err := testreporters.SendSlackMessage(slackClient, slack.MsgOptionBlocks(messageBlocks...)) if err != nil { return err } - for contractIndex, report := range o.Reports { - err = vrfv2ReportWriter.Write([]string{ - fmt.Sprint(contractIndex), - report.ContractAddress, - fmt.Sprint(report.TotalRounds), - fmt.Sprint(report.averageRoundTime), - fmt.Sprint(report.LongestRoundTime), - fmt.Sprint(report.ShortestRoundTime), - fmt.Sprint(report.averageRoundBlocks), - fmt.Sprint(report.LongestRoundBlocks), - fmt.Sprint(report.ShortestRoundBlocks), - }) - if err != nil { - return err - } - } - vrfv2ReportWriter.Flush() - - log.Info().Str("Location", reportLocation).Msg("Wrote CSV file") return nil } diff --git a/integration-tests/testreporters/vrfv2plus.go b/integration-tests/testreporters/vrfv2plus.go index 38220ca8821..21e4e52695a 100644 --- a/integration-tests/testreporters/vrfv2plus.go +++ b/integration-tests/testreporters/vrfv2plus.go @@ -49,9 +49,9 @@ func (o *VRFV2PlusTestReporter) SendSlackNotification(t *testing.T, slackClient } testFailed := t.Failed() - headerText := fmt.Sprintf(":white_check_mark: VRF %s Test PASSED :white_check_mark:", o.TestType) + headerText := fmt.Sprintf(":white_check_mark: VRF V2 Plus %s Test PASSED :white_check_mark:", o.TestType) if testFailed { - headerText = fmt.Sprintf(":x: VRF %s Test FAILED :x:", o.TestType) + headerText = fmt.Sprintf(":x: VRF V2 Plus %s Test FAILED :x:", o.TestType) } messageBlocks := testreporters.SlackNotifyBlocks(headerText, os.Getenv("SELECTED_NETWORKS"), []string{ diff --git a/integration-tests/testsetups/vrfv2.go b/integration-tests/testsetups/vrfv2.go deleted file mode 100644 index ef18bd267de..00000000000 --- a/integration-tests/testsetups/vrfv2.go +++ /dev/null @@ -1,204 +0,0 @@ -package testsetups - -//revive:disable:dot-imports -import ( - "context" - "fmt" - "math/big" - "os" - "strconv" - "strings" - "testing" - "time" - - "github.com/rs/zerolog/log" - "github.com/stretchr/testify/require" - - "github.com/smartcontractkit/chainlink-testing-framework/blockchain" - "github.com/smartcontractkit/chainlink-testing-framework/k8s/environment" - "github.com/smartcontractkit/chainlink-testing-framework/logging" - reportModel "github.com/smartcontractkit/chainlink-testing-framework/testreporters" - "github.com/smartcontractkit/chainlink-testing-framework/utils/testcontext" - - "github.com/smartcontractkit/chainlink/integration-tests/client" - "github.com/smartcontractkit/chainlink/integration-tests/contracts" - "github.com/smartcontractkit/chainlink/integration-tests/testreporters" -) - -// VRFV2SoakTest defines a typical VRFV2 soak test -type VRFV2SoakTest struct { - Inputs *VRFV2SoakTestInputs - - TestReporter testreporters.VRFV2SoakTestReporter - - testEnvironment *environment.Environment - namespace string - ChainlinkNodes []*client.ChainlinkK8sClient - chainClient blockchain.EVMClient - DefaultNetwork blockchain.EVMClient - - NumberOfRandRequests int - - ErrorOccurred error - ErrorCount int -} - -// VRFV2SoakTestTestFunc function type for the request and validation you want done on each iteration -type VRFV2SoakTestTestFunc func(t *VRFV2SoakTest, requestNumber int) error - -// VRFV2SoakTestInputs define required inputs to run a vrfv2 soak test -type VRFV2SoakTestInputs struct { - BlockchainClient blockchain.EVMClient // Client for the test to connect to the blockchain with - TestDuration time.Duration `envconfig:"TEST_DURATION" default:"15m"` // How long to run the test for (assuming things pass) - ChainlinkNodeFunding *big.Float `envconfig:"CHAINLINK_NODE_FUNDING" default:".1"` // Amount of ETH to fund each chainlink node with - SubscriptionFunding *big.Int `envconfig:"SUBSCRIPTION_FUNDING" default:"100"` // Amount of Link to fund VRF Coordinator subscription - StopTestOnError bool // Do we want the test to stop after any error or just continue on - - RequestsPerMinute int `envconfig:"REQUESTS_PER_MINUTE" default:"10"` // Number of requests for randomness per minute - RandomnessRequestCountPerRequest int `envconfig:"RANDOMNESS_REQUEST_COUNT_PER_REQUEST" default:"1"` - ConsumerContract contracts.VRFv2LoadTestConsumer - TestFunc VRFV2SoakTestTestFunc // The function that makes the request and validations wanted -} - -// NewVRFV2SoakTest creates a new vrfv2 soak test to setup and run -func NewVRFV2SoakTest(inputs *VRFV2SoakTestInputs, chainlinkNodes []*client.ChainlinkK8sClient) *VRFV2SoakTest { - return &VRFV2SoakTest{ - Inputs: inputs, - TestReporter: testreporters.VRFV2SoakTestReporter{ - Reports: make(map[string]*testreporters.VRFV2SoakTestReport), - }, - ChainlinkNodes: chainlinkNodes, - } -} - -// Setup sets up the test environment -func (v *VRFV2SoakTest) Setup(t *testing.T, env *environment.Environment) { - v.ensureInputValues(t) - v.testEnvironment = env - v.namespace = v.testEnvironment.Cfg.Namespace - v.chainClient.ParallelTransactions(true) -} - -// Run starts the VRFV2 soak test -func (v *VRFV2SoakTest) Run(t *testing.T) { - l := logging.GetTestLogger(t) - l.Info(). - Str("Test Duration", v.Inputs.TestDuration.Truncate(time.Second).String()). - Int("Max number of requests per minute wanted", v.Inputs.RequestsPerMinute). - Msg("Starting VRFV2 Soak Test") - - // set the requests to only run for a certain amount of time - ctx := testcontext.Get(t) - testContext, testCancel := context.WithTimeout(ctx, v.Inputs.TestDuration) - defer testCancel() - - v.NumberOfRandRequests = 0 - - // variables dealing with how often to tick and how to stop the ticker - stop := false - startTime := time.Now() - ticker := time.NewTicker(time.Minute / time.Duration(v.Inputs.RequestsPerMinute)) - - for { - // start the loop by checking to see if any of the TestFunc responses have returned an error - if v.Inputs.StopTestOnError { - require.NoError(t, v.ErrorOccurred, "Found error") - } - select { - case <-testContext.Done(): - // stop making requests - stop = true - ticker.Stop() - break // breaks the select block - case <-ticker.C: - // make the next request - v.NumberOfRandRequests++ - go requestAndValidate(v, v.NumberOfRandRequests) - } - if stop { - break // breaks the for loop and stops the test - } - } - - err := v.chainClient.WaitForEvents() - if err != nil { - l.Error().Err(err).Msg("Error Occurred waiting for On chain events") - } - //wait some buffer time for requests to be fulfilled - //todo - need to find better way for this - time.Sleep(1 * time.Minute) - - loadTestMetrics, err := v.Inputs.ConsumerContract.GetLoadTestMetrics(ctx) - if err != nil { - l.Error().Err(err).Msg("Error Occurred when getting Load Test Metrics from Consumer contract") - } - - averageFulfillmentInBlockTime := new(big.Float).Quo(new(big.Float).SetInt(loadTestMetrics.AverageFulfillmentInMillions), big.NewFloat(1e6)) - - l.Info().Int("Requests", v.NumberOfRandRequests).Msg("Total Completed Requests calculated from Test") - l.Info().Uint64("Requests", loadTestMetrics.RequestCount.Uint64()).Msg("Total Completed Requests calculated from Contract") - l.Info().Uint64("Fulfilments", loadTestMetrics.FulfilmentCount.Uint64()).Msg("Total Completed Fulfilments") - l.Info().Uint64("Fastest Fulfilment", loadTestMetrics.FastestFulfillment.Uint64()).Msg("Fastest Fulfilment") - l.Info().Uint64("Slowest Fulfilment", loadTestMetrics.SlowestFulfillment.Uint64()).Msg("Slowest Fulfilment") - l.Info().Interface("Average Fulfillment", averageFulfillmentInBlockTime).Msg("Average Fulfillment In Block Time") - - //todo - need to calculate 95th percentile response time in Block time and calculate how many requests breached 256 block time requirement - - l.Info().Str("Run Time", time.Since(startTime).String()).Msg("Finished VRFV2 Soak Test Requests") - require.Equal(t, 0, v.ErrorCount, "Expected 0 errors") - require.Equal(t, loadTestMetrics.RequestCount.Uint64(), loadTestMetrics.FulfilmentCount.Uint64(), "Number of Rand Requests should be equal to Number of Fulfillments") -} - -func requestAndValidate(t *VRFV2SoakTest, requestNumber int) { - - log.Info().Int("Request Number", requestNumber).Msg("Making a Request") - err := t.Inputs.TestFunc(t, requestNumber) - - // only set the error to be checked if err is not nil so we avoid race conditions with passing requests - if err != nil { - t.ErrorOccurred = err - log.Error().Err(err).Msg("Error Occurred during test") - t.ErrorCount++ - } -} - -// Networks returns the networks that the test is running on -func (v *VRFV2SoakTest) TearDownVals(t *testing.T) ( - *testing.T, - string, - []*client.ChainlinkK8sClient, - reportModel.TestReporter, - blockchain.EVMClient, -) { - return t, v.namespace, v.ChainlinkNodes, &v.TestReporter, v.chainClient -} - -// ensureValues ensures that all values needed to run the test are present -func (v *VRFV2SoakTest) ensureInputValues(t *testing.T) { - inputs := v.Inputs - require.NotNil(t, inputs.BlockchainClient, "Need a valid blockchain client for the test") - v.chainClient = inputs.BlockchainClient - require.GreaterOrEqual(t, inputs.RequestsPerMinute, 1, "Expecting at least 1 request per minute") - chainlinkNodeFunding, _ := inputs.ChainlinkNodeFunding.Float64() - subscriptionFunding := inputs.SubscriptionFunding.Int64() - require.Greater(t, chainlinkNodeFunding, float64(0), "Need some amount of funding for Chainlink nodes") - require.Greater(t, subscriptionFunding, int64(0), "Need some amount of funding for VRF V2 Coordinator Subscription nodes") - require.GreaterOrEqual(t, inputs.TestDuration, time.Minute, "Test duration should be longer than 1 minute") - require.NotNil(t, inputs.TestFunc, "Expected there to be test to run") -} - -func (i VRFV2SoakTestInputs) SetForRemoteRunner() { - os.Setenv("TEST_VRFV2_TEST_DURATION", i.TestDuration.String()) - os.Setenv("TEST_VRFV2_CHAINLINK_NODE_FUNDING", i.ChainlinkNodeFunding.String()) - os.Setenv("TEST_VRFV2_SUBSCRIPTION_FUNDING", i.SubscriptionFunding.String()) - os.Setenv("TEST_VRFV2_REQUESTS_PER_MINUTE", strconv.Itoa(i.RequestsPerMinute)) - os.Setenv("TEST_VRFV2_RANDOMNESS_REQUEST_COUNT_PER_REQUEST", strconv.Itoa(i.RandomnessRequestCountPerRequest)) - - selectedNetworks := strings.Split(os.Getenv("SELECTED_NETWORKS"), ",") - for _, networkPrefix := range selectedNetworks { - urlEnv := fmt.Sprintf("%s_URLS", networkPrefix) - httpEnv := fmt.Sprintf("%s_HTTP_URLS", networkPrefix) - os.Setenv(fmt.Sprintf("TEST_%s", urlEnv), os.Getenv(urlEnv)) - os.Setenv(fmt.Sprintf("TEST_%s", httpEnv), os.Getenv(httpEnv)) - } -} diff --git a/integration-tests/types/config/node/core.go b/integration-tests/types/config/node/core.go index b7f2b316aa7..47bc8e8cc1e 100644 --- a/integration-tests/types/config/node/core.go +++ b/integration-tests/types/config/node/core.go @@ -23,7 +23,6 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/utils" "github.com/smartcontractkit/chainlink/v2/core/utils/config" - "github.com/smartcontractkit/chainlink/integration-tests/actions/vrfv2_actions/vrfv2_constants" it_utils "github.com/smartcontractkit/chainlink/integration-tests/utils" ) @@ -224,8 +223,8 @@ func WithPrivateEVMs(networks []blockchain.EVMNetwork) NodeConfigOpt { } } -func WithVRFv2EVMEstimator(addr string) NodeConfigOpt { - est := assets.GWei(vrfv2_constants.MaxGasPriceGWei) +func WithVRFv2EVMEstimator(addr string, maxGasPriceGWei int64) NodeConfigOpt { + est := assets.GWei(maxGasPriceGWei) return func(c *chainlink.Config) { c.EVM[0].KeySpecific = evmcfg.KeySpecificConfig{ {