Skip to content

Commit 8d6a9f7

Browse files
fix(mcms): select public rpc for fork tests (#553)
### What Ignore CLL rpcs when selecting the RPC for the ForkedEnvironment (used by anvil to fetch the contract state). ### Why In the CI, the fork tests need to GAP to access the CLL rpcs. However, because we run anvil inside a containers, the setup gets rather complicated -- we need to copy+adapt the entries added to `/etc/hosts` and add GAP's custom CA certificates. But, because anvil is managed using testcontainers (indirectly, via CTF), we'd need to add support for this feature to CTF, then CLDF and finally the CLD workflows. However, CLL is moving towards replacing GAP with Tailscale to access internal resources from GHA runners. So, rather than spending significant resources implementing the GAP solution, we're adding this quick fix -- which should solve the problem for 95% of the proposals -- until the Tailscale option is ready. --- DPT-277
1 parent cf4de66 commit 8d6a9f7

4 files changed

Lines changed: 145 additions & 6 deletions

File tree

.changeset/fine-cups-battle.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"chainlink-deployments-framework": patch
3+
---
4+
5+
fix(mcms): select public rpc for fork tests

engine/cld/environment/anvil.go

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"math/big"
99
"math/rand/v2"
1010
"os"
11+
"regexp"
1112
"slices"
1213
"strconv"
1314
"sync"
@@ -30,9 +31,7 @@ import (
3031
cfgnet "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/config/network"
3132
)
3233

33-
var (
34-
oneEth = big.NewInt(0).Exp(big.NewInt(10), big.NewInt(18), nil)
35-
)
34+
var oneEth = big.NewInt(0).Exp(big.NewInt(10), big.NewInt(18), nil)
3635

3736
// anvilClient operates the methods exposed by the Anvil node related to forking.
3837
// For more information, see https://book.getfoundry.sh/reference/anvil/#custom-methods.
@@ -203,9 +202,8 @@ func newAnvilChains(
203202
}
204203
network.Metadata = cfgnet.EVMMetadata{
205204
AnvilConfig: &cfgnet.AnvilConfig{
206-
Image: "f4hrenh9it/foundry:latest",
207-
Port: uint64(ports[0]), //nolint:gosec // G115: int to uint64 conversion is safe here (port numbers are always in valid range)
208-
ArchiveHTTPURL: network.RPCs[0].HTTPURL,
205+
Image: "f4hrenh9it/foundry:latest",
206+
Port: uint64(ports[0]), //nolint:gosec // G115: int to uint64 conversion is safe here (port numbers are always in valid range)
209207
},
210208
}
211209
}
@@ -222,6 +220,11 @@ func newAnvilChains(
222220
continue
223221
}
224222

223+
if err := selectPublicRPC(lggr, &metadata, network.ChainSelector, network.RPCs); err != nil {
224+
lggr.Infof("Excluding chain with ID %d from environment: %s", chainID, err.Error())
225+
continue
226+
}
227+
225228
// Skip chains that are not included in the address book
226229
if _, ok := addressesByChain[chainSelector]; !ok {
227230
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(
301304
ChainConfigs: chainConfigsBySelector,
302305
}, nil
303306
}
307+
308+
func selectPublicRPC(
309+
lggr logger.Logger, metadata *cfgnet.EVMMetadata, chainSelector uint64, rpcs []cfgnet.RPC,
310+
) error {
311+
if isPublicRPC(metadata.AnvilConfig.ArchiveHTTPURL) {
312+
return nil
313+
}
314+
315+
for _, rpc := range rpcs {
316+
if isPublicRPC(rpc.HTTPURL) {
317+
metadata.AnvilConfig.ArchiveHTTPURL = rpc.HTTPURL
318+
lggr.Infow("selected rpc for fork environment", "url", rpc.HTTPURL, "chainSelector", chainSelector)
319+
320+
return nil
321+
}
322+
}
323+
324+
return fmt.Errorf("no public RPCs found for chain %d", chainSelector)
325+
}
326+
327+
var privateRpcRegexp = regexp.MustCompile(`^https?://(rpcs|gap\-.*\.(prod|stage))\.cldev\.sh/`)
328+
329+
func isPublicRPC(url string) bool {
330+
return !privateRpcRegexp.MatchString(url)
331+
}

engine/cld/environment/anvil_test.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import (
1111
"github.com/go-resty/resty/v2"
1212
"github.com/stretchr/testify/assert"
1313
"github.com/stretchr/testify/require"
14+
15+
cfgnet "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/config/network"
16+
"github.com/smartcontractkit/chainlink-deployments-framework/pkg/logger"
1417
)
1518

1619
// JSONRPCRequest represents a JSON-RPC request
@@ -126,3 +129,102 @@ func Test_AnvilClient_SendTransaction(t *testing.T) {
126129
})
127130
}
128131
}
132+
133+
func Test_isPublicRPC(t *testing.T) {
134+
t.Parallel()
135+
tests := []struct {
136+
url string
137+
want bool
138+
}{
139+
{"http://rpcs.cldev.sh/", false},
140+
{"https://rpcs.cldev.sh/", false},
141+
{"https://rpcs.cldev.sh/anything", false},
142+
{"https://gap-rpcs.stage.cldev.sh/anything", false},
143+
{"https://gap-rpcs.prod.cldev.sh/anything", false},
144+
{"https://gap-other.prod.cldev.sh/anything", false},
145+
{"https://gap-other.stage.cldev.sh/anything", false},
146+
{"https://gap-other.stage.cldev.sh/anything", false},
147+
{"https://gap-grpc-job-distributor.public.main.prod.cldev.sh/", false},
148+
{"https://gap-ws-job-distributor.public.main.prod.cldev.sh/", false},
149+
{"https://gap-rpc-proxy.public.main.prod.cldev.sh/", false},
150+
{"https://gap-grpc-job-distributor.public.main.stage.cldev.sh/", false},
151+
{"https://gap-ws-job-distributor.public.main.stage.cldev.sh/", false},
152+
{"https://gap-grpc-chainlink-catalog.public.main.stage.cldev.sh/", false},
153+
{"", true},
154+
{"http://", true},
155+
{"https://", true},
156+
{"https://rpcs.cldev.sh", true},
157+
{"https://rpcs.prod.cldev.sh/anything", true},
158+
{"https://rpcs.stage.cldev.sh/anything", true},
159+
{"https://gap.stage.cldev.sh/anything", true},
160+
}
161+
for _, tt := range tests {
162+
t.Run(tt.url, func(t *testing.T) {
163+
t.Parallel()
164+
require.Equal(t, tt.want, isPublicRPC(tt.url))
165+
})
166+
}
167+
}
168+
169+
func Test_selectPublicRPC(t *testing.T) {
170+
t.Parallel()
171+
172+
lggr := logger.Test(t)
173+
tests := []struct {
174+
name string
175+
metadata *cfgnet.EVMMetadata
176+
chainSelector uint64
177+
rpcs []cfgnet.RPC
178+
want *cfgnet.EVMMetadata
179+
wantErr string
180+
}{
181+
{
182+
name: "success: metadata has url",
183+
metadata: &cfgnet.EVMMetadata{AnvilConfig: &cfgnet.AnvilConfig{
184+
ArchiveHTTPURL: "http://metadata.url",
185+
}},
186+
rpcs: []cfgnet.RPC{
187+
{HTTPURL: "http://other.url"},
188+
},
189+
want: &cfgnet.EVMMetadata{AnvilConfig: &cfgnet.AnvilConfig{
190+
ArchiveHTTPURL: "http://metadata.url",
191+
}},
192+
},
193+
{
194+
name: "success: private rpc in metadata is replaced public url from parameters",
195+
metadata: &cfgnet.EVMMetadata{AnvilConfig: &cfgnet.AnvilConfig{
196+
ArchiveHTTPURL: "http://gap-rpc.prod.cldev.sh/ethereum/sepolia",
197+
}},
198+
rpcs: []cfgnet.RPC{
199+
{HTTPURL: "http://rpcs.cldev.sh/ethereum/sepolia"},
200+
{HTTPURL: "http://public.rpc.url"},
201+
},
202+
want: &cfgnet.EVMMetadata{AnvilConfig: &cfgnet.AnvilConfig{
203+
ArchiveHTTPURL: "http://public.rpc.url",
204+
}},
205+
},
206+
{
207+
name: "failure: no public rpcs found",
208+
metadata: &cfgnet.EVMMetadata{AnvilConfig: &cfgnet.AnvilConfig{
209+
ArchiveHTTPURL: "http://gap-rpc.prod.cldev.sh/ethereum/sepolia",
210+
}},
211+
rpcs: []cfgnet.RPC{
212+
{HTTPURL: "http://rpcs.cldev.sh/ethereum/sepolia"},
213+
},
214+
wantErr: "no public RPCs found for chain 0",
215+
},
216+
}
217+
for _, tt := range tests {
218+
t.Run(tt.name, func(t *testing.T) {
219+
t.Parallel()
220+
221+
err := selectPublicRPC(lggr, tt.metadata, tt.chainSelector, tt.rpcs)
222+
if tt.wantErr == "" {
223+
require.NoError(t, err)
224+
require.Equal(t, tt.want, tt.metadata)
225+
} else {
226+
require.ErrorContains(t, err, tt.wantErr)
227+
}
228+
})
229+
}
230+
}

engine/cld/legacy/cli/mcmsv2/mcms_v2.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,10 @@ func buildExecuteForkCommand(lggr logger.Logger, domain cldf_domain.Domain, prop
620620
return fmt.Errorf("error creating config: %w", err)
621621
}
622622

623+
if len(cfg.forkedEnv.ChainConfigs[cfg.chainSelector].HTTPRPCs) == 0 {
624+
return fmt.Errorf("no rpcs loaded in forked environment for chain %d (fork tests require public RPCs)", cfg.chainSelector)
625+
}
626+
623627
// get the chain URL, chain ID and MCM contract address
624628
url := cfg.forkedEnv.ChainConfigs[cfg.chainSelector].HTTPRPCs[0].External
625629
anvilClient := rpc.New(url, nil)

0 commit comments

Comments
 (0)