Skip to content

Commit de6ecbc

Browse files
authored
Support HTTP URLs in file fetcher and system-test instrumentation (#22044)
* support HTTP URLs in file fetcher and add system-test instrumentation Confidential workflows register HTTP URLs for the enclave. The file fetcher extracts the filename for local testing. Add relay capability constant and timing logs for system tests. * add confidential relay feature for system-tests * address review feedback: harden fetcher path validation, add tests, fix types.go * inline Seconds() and remove roundSeconds helper
1 parent 7347d6e commit de6ecbc

8 files changed

Lines changed: 197 additions & 21 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"chainlink": patch
3+
---
4+
5+
Support HTTP URLs in file fetcher for local confidential workflow testing, add system-test instrumentation #changed

core/services/workflows/syncer/fetcher.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,14 +177,20 @@ func newFileFetcher(basePath string, lggr logger.Logger) types.FetcherFunc {
177177
if err != nil {
178178
return nil, fmt.Errorf("invalid URL: %w", err)
179179
}
180+
if u.Scheme == "http" || u.Scheme == "https" {
181+
u.Path = filepath.Base(u.Path)
182+
if u.Path == "." || u.Path == "/" {
183+
return nil, errors.New("HTTP URL has no filename in path")
184+
}
185+
}
180186
fullPath := filepath.Clean(u.Path)
181187

182188
// ensure that the incoming request URL is either relative or absolute but within the basePath
183189
if !filepath.IsAbs(fullPath) {
184190
// If it's not absolute, we assume it's relative to the basePath
185191
fullPath = filepath.Join(basePath, fullPath)
186192
}
187-
if !strings.HasPrefix(fullPath, basePath) {
193+
if !strings.HasPrefix(fullPath, basePath+string(filepath.Separator)) && fullPath != basePath {
188194
return nil, fmt.Errorf("request URL %s is not within the basePath %s", fullPath, basePath)
189195
}
190196

core/services/workflows/syncer/fetcher_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,39 @@ func TestNewFetcherFunc(t *testing.T) {
390390
assert.Equal(t, testContent, resp)
391391
})
392392

393+
t.Run("file fetcher resolves HTTP URL to basename", func(t *testing.T) {
394+
tempDir := t.TempDir()
395+
err := os.WriteFile(filepath.Join(tempDir, "binary.wasm"), testContent, 0600)
396+
require.NoError(t, err)
397+
398+
fetcher, err := NewFetcherFunc("file://"+tempDir, lggr)
399+
require.NoError(t, err)
400+
401+
resp, err := fetcher(ctx, "msg-1", ghcapabilities.Request{
402+
URL: "http://storage.example.com/artifacts/binary.wasm",
403+
})
404+
require.NoError(t, err)
405+
assert.Equal(t, testContent, resp)
406+
407+
resp, err = fetcher(ctx, "msg-2", ghcapabilities.Request{
408+
URL: "https://storage.example.com/path/to/binary.wasm",
409+
})
410+
require.NoError(t, err)
411+
assert.Equal(t, testContent, resp)
412+
})
413+
414+
t.Run("file fetcher rejects HTTP URL with empty path", func(t *testing.T) {
415+
tempDir := t.TempDir()
416+
fetcher, err := NewFetcherFunc("file://"+tempDir, lggr)
417+
require.NoError(t, err)
418+
419+
_, err = fetcher(ctx, "msg-1", ghcapabilities.Request{
420+
URL: "http://storage.example.com",
421+
})
422+
require.Error(t, err)
423+
assert.Contains(t, err.Error(), "HTTP URL has no filename in path")
424+
})
425+
393426
t.Run("http fetcher", func(t *testing.T) {
394427
// Create test HTTP server
395428
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

core/services/workflows/syncer/v2/fetcher.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,14 +211,22 @@ func newFileFetcher(basePath string, lggr logger.Logger) types.FetcherFunc {
211211
if err != nil {
212212
return nil, fmt.Errorf("invalid URL: %w", err)
213213
}
214+
// Confidential workflows register with HTTP URLs (for the enclave).
215+
// Extract the filename so the file fetcher can find the local copy.
216+
if u.Scheme == "http" || u.Scheme == "https" {
217+
u.Path = filepath.Base(u.Path)
218+
if u.Path == "." || u.Path == "/" {
219+
return nil, errors.New("HTTP URL has no filename in path")
220+
}
221+
}
214222
fullPath := filepath.Clean(u.Path)
215223

216224
// ensure that the incoming request URL is either relative or absolute but within the basePath
217225
if !filepath.IsAbs(fullPath) {
218226
// If it's not absolute, we assume it's relative to the basePath
219227
fullPath = filepath.Join(basePath, fullPath)
220228
}
221-
if !strings.HasPrefix(fullPath, basePath) {
229+
if !strings.HasPrefix(fullPath, basePath+string(filepath.Separator)) && fullPath != basePath {
222230
return nil, fmt.Errorf("request URL %s is not within the basePath %s", fullPath, basePath)
223231
}
224232

core/services/workflows/syncer/v2/fetcher_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,39 @@ func TestNewFetcherFunc(t *testing.T) {
439439
assert.Equal(t, testContent, resp)
440440
})
441441

442+
t.Run("file fetcher resolves HTTP URL to basename", func(t *testing.T) {
443+
tempDir := t.TempDir()
444+
err := os.WriteFile(filepath.Join(tempDir, "binary.wasm"), testContent, 0600)
445+
require.NoError(t, err)
446+
447+
fetcher, err := NewFetcherFunc("file://"+tempDir, lggr)
448+
require.NoError(t, err)
449+
450+
resp, err := fetcher(ctx, "msg-1", ghcapabilities.Request{
451+
URL: "http://storage.example.com/artifacts/binary.wasm",
452+
})
453+
require.NoError(t, err)
454+
assert.Equal(t, testContent, resp)
455+
456+
resp, err = fetcher(ctx, "msg-2", ghcapabilities.Request{
457+
URL: "https://storage.example.com/path/to/binary.wasm",
458+
})
459+
require.NoError(t, err)
460+
assert.Equal(t, testContent, resp)
461+
})
462+
463+
t.Run("file fetcher rejects HTTP URL with empty path", func(t *testing.T) {
464+
tempDir := t.TempDir()
465+
fetcher, err := NewFetcherFunc("file://"+tempDir, lggr)
466+
require.NoError(t, err)
467+
468+
_, err = fetcher(ctx, "msg-1", ghcapabilities.Request{
469+
URL: "http://storage.example.com",
470+
})
471+
require.Error(t, err)
472+
assert.Contains(t, err.Error(), "HTTP URL has no filename in path")
473+
})
474+
442475
t.Run("http fetcher", func(t *testing.T) {
443476
// Create test HTTP server
444477
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package confidentialrelay
2+
3+
import (
4+
"context"
5+
6+
tomlser "github.com/pelletier/go-toml/v2"
7+
"github.com/pkg/errors"
8+
"github.com/rs/zerolog"
9+
10+
chainselectors "github.com/smartcontractkit/chain-selectors"
11+
12+
"github.com/smartcontractkit/chainlink/deployment/cre/jobs/pkg"
13+
"github.com/smartcontractkit/chainlink/system-tests/lib/cre"
14+
coretoml "github.com/smartcontractkit/chainlink/v2/core/config/toml"
15+
corechainlink "github.com/smartcontractkit/chainlink/v2/core/services/chainlink"
16+
)
17+
18+
const flag = cre.ConfidentialRelayCapability
19+
20+
type ConfidentialRelay struct{}
21+
22+
func (o *ConfidentialRelay) Flag() cre.CapabilityFlag {
23+
return flag
24+
}
25+
26+
func (o *ConfidentialRelay) PreEnvStartup(
27+
ctx context.Context,
28+
testLogger zerolog.Logger,
29+
don *cre.DonMetadata,
30+
topology *cre.Topology,
31+
creEnv *cre.Environment,
32+
) (*cre.PreEnvStartupOutput, error) {
33+
registryChainID, chErr := chainselectors.ChainIdFromSelector(creEnv.RegistryChainSelector)
34+
if chErr != nil {
35+
return nil, errors.Wrapf(chErr, "failed to get chain ID from selector %d", creEnv.RegistryChainSelector)
36+
}
37+
38+
hErr := topology.AddGatewayHandlers(*don, []string{pkg.GatewayHandlerTypeConfidentialRelay})
39+
if hErr != nil {
40+
return nil, errors.Wrapf(hErr, "failed to add gateway handlers to gateway config for don %s", don.Name)
41+
}
42+
43+
cErr := don.ConfigureForGatewayAccess(registryChainID, *topology.GatewayConnectors)
44+
if cErr != nil {
45+
return nil, errors.Wrapf(cErr, "failed to add gateway connectors to node's TOML config for don %s", don.Name)
46+
}
47+
48+
// Set TOML config to activate the confidential relay handler on DON nodes.
49+
capConfig, ok := don.CapabilityConfigs[flag]
50+
if ok && capConfig.Values != nil {
51+
ns := don.MustNodeSet()
52+
for i := range ns.NodeSpecs {
53+
currentConfig := ns.NodeSpecs[i].Node.TestConfigOverrides
54+
var typedConfig corechainlink.Config
55+
if currentConfig != "" {
56+
if err := tomlser.Unmarshal([]byte(currentConfig), &typedConfig); err != nil {
57+
return nil, errors.Wrapf(err, "failed to unmarshal node TOML config for node %d", i)
58+
}
59+
}
60+
61+
enabled := true
62+
typedConfig.CRE.ConfidentialRelay = &coretoml.ConfidentialRelayConfig{Enabled: &enabled}
63+
64+
out, err := tomlser.Marshal(typedConfig)
65+
if err != nil {
66+
return nil, errors.Wrapf(err, "failed to marshal node TOML config for node %d", i)
67+
}
68+
ns.NodeSpecs[i].Node.TestConfigOverrides = string(out)
69+
}
70+
}
71+
72+
// No on-chain capability registration needed. The relay handler is a CRE subservice,
73+
// not a registered capability. The mock capability that runs on the relay DON is
74+
// registered separately via the mock flag.
75+
return &cre.PreEnvStartupOutput{}, nil
76+
}
77+
78+
func (o *ConfidentialRelay) PostEnvStartup(
79+
ctx context.Context,
80+
testLogger zerolog.Logger,
81+
don *cre.Don,
82+
dons *cre.Dons,
83+
creEnv *cre.Environment,
84+
) error {
85+
return nil
86+
}

system-tests/lib/cre/features/mock/mock.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func (o *Mock) PreEnvStartup(
4343
Capability: kcr.CapabilitiesRegistryCapability{
4444
LabelledName: "mock",
4545
Version: "1.0.0",
46-
CapabilityType: 0, // TRIGGER
46+
CapabilityType: 1, // ACTION
4747
},
4848
Config: &capabilitiespb.CapabilityConfig{
4949
LocalOnly: don.HasOnlyLocalCapabilities(),

system-tests/lib/cre/types.go

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"slices"
1111
"strconv"
1212
"strings"
13+
"time"
1314

1415
"github.com/Masterminds/semver/v3"
1516
"github.com/ethereum/go-ethereum/common"
@@ -56,23 +57,24 @@ const (
5657

5758
// Capabilities
5859
const (
59-
ConsensusCapability CapabilityFlag = "ocr3"
60-
DONTimeCapability CapabilityFlag = "don-time"
61-
ConsensusCapabilityV2 CapabilityFlag = "consensus" // v2
62-
CronCapability CapabilityFlag = "cron"
63-
EVMCapability CapabilityFlag = "evm"
64-
CustomComputeCapability CapabilityFlag = "custom-compute"
65-
WriteEVMCapability CapabilityFlag = "write-evm"
66-
ReadContractCapability CapabilityFlag = "read-contract"
67-
LogEventTriggerCapability CapabilityFlag = "log-event-trigger"
68-
WebAPITargetCapability CapabilityFlag = "web-api-target"
69-
WebAPITriggerCapability CapabilityFlag = "web-api-trigger"
70-
MockCapability CapabilityFlag = "mock"
71-
VaultCapability CapabilityFlag = "vault"
72-
HTTPTriggerCapability CapabilityFlag = "http-trigger"
73-
HTTPActionCapability CapabilityFlag = "http-action"
74-
SolanaCapability CapabilityFlag = "solana"
75-
AptosCapability CapabilityFlag = "aptos"
60+
ConsensusCapability CapabilityFlag = "ocr3"
61+
DONTimeCapability CapabilityFlag = "don-time"
62+
ConsensusCapabilityV2 CapabilityFlag = "consensus" // v2
63+
CronCapability CapabilityFlag = "cron"
64+
EVMCapability CapabilityFlag = "evm"
65+
CustomComputeCapability CapabilityFlag = "custom-compute"
66+
WriteEVMCapability CapabilityFlag = "write-evm"
67+
ReadContractCapability CapabilityFlag = "read-contract"
68+
LogEventTriggerCapability CapabilityFlag = "log-event-trigger"
69+
WebAPITargetCapability CapabilityFlag = "web-api-target"
70+
WebAPITriggerCapability CapabilityFlag = "web-api-trigger"
71+
MockCapability CapabilityFlag = "mock"
72+
VaultCapability CapabilityFlag = "vault"
73+
HTTPTriggerCapability CapabilityFlag = "http-trigger"
74+
HTTPActionCapability CapabilityFlag = "http-action"
75+
SolanaCapability CapabilityFlag = "solana"
76+
ConfidentialRelayCapability CapabilityFlag = "confidential-relay"
77+
AptosCapability CapabilityFlag = "aptos"
7678
// Add more capabilities as needed
7779
)
7880

@@ -585,13 +587,15 @@ func NewDonMetadata(c *NodeSet, id uint64, provider infra.Provider, capabilityCo
585587
cfgs[i] = cfg
586588
}
587589

590+
newNodesStart := time.Now()
588591
nodes, err := newNodes(cfgs)
589592
if err != nil {
590593
return nil, fmt.Errorf("failed to create nodes metadata: %w", err)
591594
}
592595
framework.L.Info().
593596
Str("don", c.Name).
594597
Int("nodes", len(cfgs)).
598+
Float64("duration_s", time.Since(newNodesStart).Seconds()).
595599
Msg("Node metadata generation completed")
596600

597601
capConfigs, capErr := processCapabilityConfigs(c, capabilityConfigs)
@@ -1462,6 +1466,7 @@ type NodeKeyInput struct {
14621466
}
14631467

14641468
func NewNodeKeys(input NodeKeyInput) (*secrets.NodeKeys, error) {
1469+
start := time.Now()
14651470
out := &secrets.NodeKeys{
14661471
EVM: make(map[uint64]*crypto.EVMKey),
14671472
Solana: make(map[string]*crypto.SolKey),
@@ -1520,7 +1525,7 @@ func NewNodeKeys(input NodeKeyInput) (*secrets.NodeKeys, error) {
15201525
framework.L.Debug().
15211526
Int("evm_chains", len(input.EVMChainIDs)).
15221527
Int("solana_chains", len(input.SolanaChainIDs)).
1523-
Bool("imported", input.ImportedSecrets != "").
1528+
Float64("duration_s", time.Since(start).Seconds()).
15241529
Msg("Node key generation completed")
15251530
return out, nil
15261531
}

0 commit comments

Comments
 (0)