diff --git a/.changeset/fine-cups-battle.md b/.changeset/fine-cups-battle.md new file mode 100644 index 000000000..985dcc3f0 --- /dev/null +++ b/.changeset/fine-cups-battle.md @@ -0,0 +1,5 @@ +--- +"chainlink-deployments-framework": patch +--- + +fix(mcms): select public rpc for fork tests diff --git a/engine/cld/environment/anvil.go b/engine/cld/environment/anvil.go index 08c2b03c5..8acda171a 100644 --- a/engine/cld/environment/anvil.go +++ b/engine/cld/environment/anvil.go @@ -8,6 +8,7 @@ import ( "math/big" "math/rand/v2" "os" + "regexp" "slices" "strconv" "sync" @@ -30,9 +31,7 @@ import ( cfgnet "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/config/network" ) -var ( - oneEth = big.NewInt(0).Exp(big.NewInt(10), big.NewInt(18), nil) -) +var oneEth = big.NewInt(0).Exp(big.NewInt(10), big.NewInt(18), nil) // anvilClient operates the methods exposed by the Anvil node related to forking. // For more information, see https://book.getfoundry.sh/reference/anvil/#custom-methods. @@ -203,9 +202,8 @@ func newAnvilChains( } network.Metadata = cfgnet.EVMMetadata{ AnvilConfig: &cfgnet.AnvilConfig{ - Image: "f4hrenh9it/foundry:latest", - Port: uint64(ports[0]), //nolint:gosec // G115: int to uint64 conversion is safe here (port numbers are always in valid range) - ArchiveHTTPURL: network.RPCs[0].HTTPURL, + Image: "f4hrenh9it/foundry:latest", + Port: uint64(ports[0]), //nolint:gosec // G115: int to uint64 conversion is safe here (port numbers are always in valid range) }, } } @@ -222,6 +220,11 @@ func newAnvilChains( continue } + if err := selectPublicRPC(lggr, &metadata, network.ChainSelector, network.RPCs); err != nil { + lggr.Infof("Excluding chain with ID %d from environment: %s", chainID, err.Error()) + continue + } + // Skip chains that are not included in the address book if _, ok := addressesByChain[chainSelector]; !ok { lggr.Infof("Excluding chain with selector %d from environment, does not have addresses defined in the address book", chainSelector) @@ -301,3 +304,28 @@ func newAnvilChains( ChainConfigs: chainConfigsBySelector, }, nil } + +func selectPublicRPC( + lggr logger.Logger, metadata *cfgnet.EVMMetadata, chainSelector uint64, rpcs []cfgnet.RPC, +) error { + if isPublicRPC(metadata.AnvilConfig.ArchiveHTTPURL) { + return nil + } + + 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 + } + } + + return fmt.Errorf("no public RPCs found for chain %d", chainSelector) +} + +var privateRpcRegexp = regexp.MustCompile(`^https?://(rpcs|gap\-.*\.(prod|stage))\.cldev\.sh/`) + +func isPublicRPC(url string) bool { + return !privateRpcRegexp.MatchString(url) +} diff --git a/engine/cld/environment/anvil_test.go b/engine/cld/environment/anvil_test.go index 0cf40b594..587117066 100644 --- a/engine/cld/environment/anvil_test.go +++ b/engine/cld/environment/anvil_test.go @@ -11,6 +11,9 @@ import ( "github.com/go-resty/resty/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + cfgnet "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/config/network" + "github.com/smartcontractkit/chainlink-deployments-framework/pkg/logger" ) // JSONRPCRequest represents a JSON-RPC request @@ -126,3 +129,102 @@ func Test_AnvilClient_SendTransaction(t *testing.T) { }) } } + +func Test_isPublicRPC(t *testing.T) { + t.Parallel() + tests := []struct { + url string + want bool + }{ + {"http://rpcs.cldev.sh/", false}, + {"https://rpcs.cldev.sh/", false}, + {"https://rpcs.cldev.sh/anything", false}, + {"https://gap-rpcs.stage.cldev.sh/anything", false}, + {"https://gap-rpcs.prod.cldev.sh/anything", false}, + {"https://gap-other.prod.cldev.sh/anything", false}, + {"https://gap-other.stage.cldev.sh/anything", false}, + {"https://gap-other.stage.cldev.sh/anything", false}, + {"https://gap-grpc-job-distributor.public.main.prod.cldev.sh/", false}, + {"https://gap-ws-job-distributor.public.main.prod.cldev.sh/", false}, + {"https://gap-rpc-proxy.public.main.prod.cldev.sh/", false}, + {"https://gap-grpc-job-distributor.public.main.stage.cldev.sh/", false}, + {"https://gap-ws-job-distributor.public.main.stage.cldev.sh/", false}, + {"https://gap-grpc-chainlink-catalog.public.main.stage.cldev.sh/", false}, + {"", true}, + {"http://", true}, + {"https://", true}, + {"https://rpcs.cldev.sh", true}, + {"https://rpcs.prod.cldev.sh/anything", true}, + {"https://rpcs.stage.cldev.sh/anything", true}, + {"https://gap.stage.cldev.sh/anything", true}, + } + for _, tt := range tests { + t.Run(tt.url, func(t *testing.T) { + t.Parallel() + require.Equal(t, tt.want, isPublicRPC(tt.url)) + }) + } +} + +func Test_selectPublicRPC(t *testing.T) { + t.Parallel() + + lggr := logger.Test(t) + tests := []struct { + name string + metadata *cfgnet.EVMMetadata + chainSelector uint64 + rpcs []cfgnet.RPC + want *cfgnet.EVMMetadata + wantErr string + }{ + { + name: "success: metadata has url", + metadata: &cfgnet.EVMMetadata{AnvilConfig: &cfgnet.AnvilConfig{ + ArchiveHTTPURL: "http://metadata.url", + }}, + rpcs: []cfgnet.RPC{ + {HTTPURL: "http://other.url"}, + }, + want: &cfgnet.EVMMetadata{AnvilConfig: &cfgnet.AnvilConfig{ + ArchiveHTTPURL: "http://metadata.url", + }}, + }, + { + name: "success: private rpc in metadata is replaced public url from parameters", + metadata: &cfgnet.EVMMetadata{AnvilConfig: &cfgnet.AnvilConfig{ + ArchiveHTTPURL: "http://gap-rpc.prod.cldev.sh/ethereum/sepolia", + }}, + rpcs: []cfgnet.RPC{ + {HTTPURL: "http://rpcs.cldev.sh/ethereum/sepolia"}, + {HTTPURL: "http://public.rpc.url"}, + }, + want: &cfgnet.EVMMetadata{AnvilConfig: &cfgnet.AnvilConfig{ + ArchiveHTTPURL: "http://public.rpc.url", + }}, + }, + { + name: "failure: no public rpcs found", + metadata: &cfgnet.EVMMetadata{AnvilConfig: &cfgnet.AnvilConfig{ + ArchiveHTTPURL: "http://gap-rpc.prod.cldev.sh/ethereum/sepolia", + }}, + rpcs: []cfgnet.RPC{ + {HTTPURL: "http://rpcs.cldev.sh/ethereum/sepolia"}, + }, + wantErr: "no public RPCs found for chain 0", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + err := selectPublicRPC(lggr, tt.metadata, tt.chainSelector, tt.rpcs) + if tt.wantErr == "" { + require.NoError(t, err) + require.Equal(t, tt.want, tt.metadata) + } else { + require.ErrorContains(t, err, tt.wantErr) + } + }) + } +} diff --git a/engine/cld/legacy/cli/mcmsv2/mcms_v2.go b/engine/cld/legacy/cli/mcmsv2/mcms_v2.go index 2a59d65ff..bbb513a13 100644 --- a/engine/cld/legacy/cli/mcmsv2/mcms_v2.go +++ b/engine/cld/legacy/cli/mcmsv2/mcms_v2.go @@ -620,6 +620,10 @@ func buildExecuteForkCommand(lggr logger.Logger, domain cldf_domain.Domain, prop return fmt.Errorf("error creating config: %w", err) } + if len(cfg.forkedEnv.ChainConfigs[cfg.chainSelector].HTTPRPCs) == 0 { + return fmt.Errorf("no rpcs loaded in forked environment for chain %d (fork tests require public RPCs)", cfg.chainSelector) + } + // get the chain URL, chain ID and MCM contract address url := cfg.forkedEnv.ChainConfigs[cfg.chainSelector].HTTPRPCs[0].External anvilClient := rpc.New(url, nil)