-
Notifications
You must be signed in to change notification settings - Fork 2
fix: run health check and try multiple RPCs in fork tests #708
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "chainlink-deployments-framework": patch | ||
| --- | ||
|
|
||
| fix: run health check and try multiple RPCs in fork tests |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -273,6 +273,8 @@ type CTFAnvilChainProviderConfig struct { | |
| // gas limits, or other Anvil-specific options. | ||
| DockerCmdParamsOverrides []string | ||
|
|
||
| ForkURLs []string | ||
|
|
||
| // Optional: Port specifies the port for the Anvil container. If not provided, | ||
| // a free port will be automatically allocated. Use this when you need the Anvil | ||
| // instance to run on a specific port. | ||
|
|
@@ -538,6 +540,7 @@ func (p *CTFAnvilChainProvider) Cleanup(ctx context.Context) error { | |
| // Returns the external HTTP URL that can be used to connect to the Anvil node. | ||
| func (p *CTFAnvilChainProvider) startContainer(ctx context.Context, chainID string) (string, error) { | ||
| attempts := uint(10) | ||
| attempt := uint(0) | ||
|
|
||
| err := framework.DefaultNetwork(p.config.Once) | ||
| if err != nil { | ||
|
|
@@ -564,6 +567,11 @@ func (p *CTFAnvilChainProvider) startContainer(ctx context.Context, chainID stri | |
| portStr = strconv.Itoa(port) | ||
| } | ||
|
|
||
| if len(p.config.ForkURLs) > 0 { | ||
| url := p.config.ForkURLs[attempt%uint(len(p.config.ForkURLs))] | ||
| p.config.DockerCmdParamsOverrides = append(p.config.DockerCmdParamsOverrides, "--fork-url", url) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. so everytime this runs, we will append another --fork-url into
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ooops; good catch. The earlier tests didn't have the health check, so this code path wasn't re-tested |
||
| } | ||
|
Comment on lines
+570
to
+573
|
||
|
|
||
| // Create the input for the Anvil blockchain network | ||
| input := &blockchain.Input{ | ||
| Type: blockchain.TypeAnvil, | ||
|
|
@@ -609,6 +617,7 @@ func (p *CTFAnvilChainProvider) startContainer(ctx context.Context, chainID stri | |
| retry.Attempts(attempts), | ||
| retry.Delay(1*time.Second), | ||
| retry.DelayType(retry.FixedDelay), | ||
| retry.OnRetry(func(a uint, err error) { attempt = a + 1 }), | ||
| ) | ||
| if err != nil { | ||
| return "", fmt.Errorf("failed to start CTF Anvil container after %d attempts: %w", attempts, err) | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -16,6 +16,7 @@ import ( | |||||||
| "time" | ||||||||
|
|
||||||||
| "github.com/ethereum/go-ethereum/common/hexutil" | ||||||||
| "github.com/ethereum/go-ethereum/ethclient" | ||||||||
| "github.com/go-resty/resty/v2" | ||||||||
| "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" | ||||||||
| "github.com/smartcontractkit/freeport" | ||||||||
|
|
@@ -214,11 +215,12 @@ func newAnvilChains( | |||||||
| "failed to decode network metadata for chain selector %d: %w", chainSelector, errMeta, | ||||||||
| ) | ||||||||
| } | ||||||||
| if err := selectPublicRPC(lggr, &metadata, network.ChainSelector, network.RPCs); err != nil { | ||||||||
| forkURLs, err := selectPublicRPC(ctx, lggr, &metadata, network.ChainSelector, network.RPCs) | ||||||||
| if err != nil { | ||||||||
| lggr.Infof("Excluding chain with ID %d from environment: %s", chainID, err.Error()) | ||||||||
| continue | ||||||||
| } | ||||||||
| if err := metadata.AnvilConfig.Validate(); err != nil { | ||||||||
| if err = metadata.AnvilConfig.Validate(); err != nil { | ||||||||
| lggr.Infof("Excluding chain with ID %d from environment due to failed anvil config validation: %s", chainID, err.Error()) | ||||||||
| continue | ||||||||
| } | ||||||||
|
|
@@ -232,10 +234,10 @@ func newAnvilChains( | |||||||
|
|
||||||||
| var signerGenerator evmprov.SignerGenerator | ||||||||
| if kmsConfig.KeyID != "" { | ||||||||
| var err error | ||||||||
| signerGenerator, err = evmprov.TransactorFromKMS(kmsConfig.KeyID, kmsConfig.KeyRegion, "") | ||||||||
| if err != nil { | ||||||||
| return nil, fmt.Errorf("failed to create transactor from KMS: %w", err) | ||||||||
| var terr error | ||||||||
| signerGenerator, terr = evmprov.TransactorFromKMS(kmsConfig.KeyID, kmsConfig.KeyRegion, "") | ||||||||
| if terr != nil { | ||||||||
| return nil, fmt.Errorf("failed to create transactor from KMS: %w", terr) | ||||||||
| } | ||||||||
| } else { | ||||||||
| signerGenerator = evmprov.TransactorFromRaw(onchainConfig.EVM.DeployerKey) | ||||||||
|
|
@@ -252,16 +254,14 @@ func newAnvilChains( | |||||||
| } | ||||||||
|
|
||||||||
| config := evmprov.CTFAnvilChainProviderConfig{ | ||||||||
| Once: &once, | ||||||||
| ConfirmFunctor: evmprov.ConfirmFuncGeth(3 * time.Minute), | ||||||||
| DockerCmdParamsOverrides: []string{ | ||||||||
| "--fork-url", metadata.AnvilConfig.ArchiveHTTPURL, | ||||||||
| "--auto-impersonate", | ||||||||
| }, | ||||||||
| Image: metadata.AnvilConfig.Image, | ||||||||
| Port: strconv.FormatUint(metadata.AnvilConfig.Port, 10), | ||||||||
| DeployerTransactorGen: signerGenerator, | ||||||||
| T: testing.TB(&testing.T{}), | ||||||||
| Once: &once, | ||||||||
| ConfirmFunctor: evmprov.ConfirmFuncGeth(3 * time.Minute), | ||||||||
| DockerCmdParamsOverrides: []string{"--auto-impersonate"}, | ||||||||
| Image: metadata.AnvilConfig.Image, | ||||||||
| ForkURLs: forkURLs, | ||||||||
| DeployerTransactorGen: signerGenerator, | ||||||||
| T: testing.TB(&testing.T{}), | ||||||||
| Port: "", // let the provider choose a free port; this ensures retries are handled properly | ||||||||
| } | ||||||||
|
|
||||||||
| if blockNumber, ok := blockNumbers[chainSelector]; ok { | ||||||||
|
|
@@ -304,26 +304,48 @@ func newAnvilChains( | |||||||
| } | ||||||||
|
|
||||||||
| func selectPublicRPC( | ||||||||
| lggr logger.Logger, metadata *cfgnet.EVMMetadata, chainSelector uint64, rpcs []cfgnet.RPC, | ||||||||
| ) error { | ||||||||
| ctx context.Context, lggr logger.Logger, metadata *cfgnet.EVMMetadata, chainSelector uint64, rpcs []cfgnet.RPC, | ||||||||
| ) ([]string, error) { | ||||||||
| if metadata.AnvilConfig.ArchiveHTTPURL != "" && isPublicRPC(metadata.AnvilConfig.ArchiveHTTPURL) { | ||||||||
| return nil | ||||||||
| return []string{metadata.AnvilConfig.ArchiveHTTPURL}, nil | ||||||||
| } | ||||||||
|
|
||||||||
| urls := []string{} | ||||||||
| for _, rpc := range rpcs { | ||||||||
| if isPublicRPC(rpc.HTTPURL) { | ||||||||
| metadata.AnvilConfig.ArchiveHTTPURL = rpc.HTTPURL | ||||||||
| lggr.Infow("selected rpc for fork environment", "url", rpc.HTTPURL, "chainSelector", chainSelector) | ||||||||
|
|
||||||||
| return nil | ||||||||
| err := runHealthCheck(ctx, rpc.HTTPURL) | ||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i wonder if it is worth parallelizing here instead of doing sequential health check?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the health checks I ran failed fast, so I didn't bother. We can reassess once we get a bit more real world usage. |
||||||||
| if err != nil { | ||||||||
| lggr.Infow("rpc failed health check", "url", rpc.HTTPURL, "chainSelector", chainSelector) | ||||||||
| } else { | ||||||||
| lggr.Infow("selected rpc for fork environment", "url", rpc.HTTPURL, "chainSelector", chainSelector) | ||||||||
| urls = append(urls, rpc.HTTPURL) | ||||||||
| } | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| return fmt.Errorf("no public RPCs found for chain %d", chainSelector) | ||||||||
| if len(urls) == 0 { | ||||||||
| return []string{}, fmt.Errorf("no public RPCs found for chain %d", chainSelector) | ||||||||
| } | ||||||||
|
|
||||||||
| return urls, nil | ||||||||
| } | ||||||||
|
|
||||||||
| var privateRpcRegexp = regexp.MustCompile(`^https?://(rpcs\.cldev\.sh|gap\-.*\.(prod|stage)\.cldev\.sh|.*\.tail[a-z0-9]+\.ts\.net)(?::\d+)?/`) | ||||||||
|
|
||||||||
| func isPublicRPC(url string) bool { | ||||||||
| return !privateRpcRegexp.MatchString(url) | ||||||||
| } | ||||||||
|
|
||||||||
| func runHealthCheck(ctx context.Context, rpcURL string) error { | ||||||||
| client, err := ethclient.DialContext(ctx, rpcURL) | ||||||||
| if err != nil { | ||||||||
| return fmt.Errorf("failed to connect to rpc %v: %w", rpcURL, err) | ||||||||
| } | ||||||||
|
||||||||
| } | |
| } | |
| defer client.Close() |
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -9,6 +9,7 @@ import ( | |||||||
| "time" | ||||||||
|
|
||||||||
| "github.com/go-resty/resty/v2" | ||||||||
| "github.com/jarcoal/httpmock" | ||||||||
| "github.com/stretchr/testify/assert" | ||||||||
| "github.com/stretchr/testify/require" | ||||||||
|
|
||||||||
|
|
@@ -174,14 +175,18 @@ func Test_isPublicRPC(t *testing.T) { | |||||||
|
|
||||||||
| func Test_selectPublicRPC(t *testing.T) { | ||||||||
| t.Parallel() | ||||||||
| httpmock.Activate(t) | ||||||||
|
||||||||
| httpmock.Activate(t) | |
| httpmock.Activate(t) | |
| t.Cleanup(httpmock.DeactivateAndReset) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we add some godoc for this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, let me add it