Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cw-fetcher-and-system-tests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"chainlink": patch
---

Support HTTP URLs in file fetcher for local confidential workflow testing, add system-test instrumentation #changed
8 changes: 7 additions & 1 deletion core/services/workflows/syncer/fetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,14 +177,20 @@ func newFileFetcher(basePath string, lggr logger.Logger) types.FetcherFunc {
if err != nil {
return nil, fmt.Errorf("invalid URL: %w", err)
}
if u.Scheme == "http" || u.Scheme == "https" {
u.Path = filepath.Base(u.Path)
if u.Path == "." || u.Path == "/" {
return nil, errors.New("HTTP URL has no filename in path")
}
}
Comment thread
nadahalli marked this conversation as resolved.
Comment thread
nadahalli marked this conversation as resolved.
fullPath := filepath.Clean(u.Path)

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

Expand Down
33 changes: 33 additions & 0 deletions core/services/workflows/syncer/fetcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,39 @@ func TestNewFetcherFunc(t *testing.T) {
assert.Equal(t, testContent, resp)
})

t.Run("file fetcher resolves HTTP URL to basename", func(t *testing.T) {
tempDir := t.TempDir()
err := os.WriteFile(filepath.Join(tempDir, "binary.wasm"), testContent, 0600)
require.NoError(t, err)

fetcher, err := NewFetcherFunc("file://"+tempDir, lggr)
require.NoError(t, err)

resp, err := fetcher(ctx, "msg-1", ghcapabilities.Request{
URL: "http://storage.example.com/artifacts/binary.wasm",
})
require.NoError(t, err)
assert.Equal(t, testContent, resp)

resp, err = fetcher(ctx, "msg-2", ghcapabilities.Request{
URL: "https://storage.example.com/path/to/binary.wasm",
})
require.NoError(t, err)
assert.Equal(t, testContent, resp)
})

t.Run("file fetcher rejects HTTP URL with empty path", func(t *testing.T) {
tempDir := t.TempDir()
fetcher, err := NewFetcherFunc("file://"+tempDir, lggr)
require.NoError(t, err)

_, err = fetcher(ctx, "msg-1", ghcapabilities.Request{
URL: "http://storage.example.com",
})
require.Error(t, err)
assert.Contains(t, err.Error(), "HTTP URL has no filename in path")
})

t.Run("http fetcher", func(t *testing.T) {
// Create test HTTP server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand Down
10 changes: 9 additions & 1 deletion core/services/workflows/syncer/v2/fetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,14 +211,22 @@ func newFileFetcher(basePath string, lggr logger.Logger) types.FetcherFunc {
if err != nil {
return nil, fmt.Errorf("invalid URL: %w", err)
}
// Confidential workflows register with HTTP URLs (for the enclave).
// Extract the filename so the file fetcher can find the local copy.
if u.Scheme == "http" || u.Scheme == "https" {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if it's "confidential" shall it only allow https ?

Copy link
Copy Markdown
Contributor Author

@nadahalli nadahalli Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confidential means that the workflow runs in TEE's. If the user wants to do http requests from TEE's, why stop them? They know what they are doing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, this is the fetcher URL. It can be http because we check the returned binary's hash against the hash that was sent in the request - so, the server that hosts the wasm binary cannot send back bad things.

u.Path = filepath.Base(u.Path)
if u.Path == "." || u.Path == "/" {
return nil, errors.New("HTTP URL has no filename in path")
}
}
Comment thread
nadahalli marked this conversation as resolved.
Comment thread
nadahalli marked this conversation as resolved.
fullPath := filepath.Clean(u.Path)

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

Expand Down
33 changes: 33 additions & 0 deletions core/services/workflows/syncer/v2/fetcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,39 @@ func TestNewFetcherFunc(t *testing.T) {
assert.Equal(t, testContent, resp)
})

t.Run("file fetcher resolves HTTP URL to basename", func(t *testing.T) {
tempDir := t.TempDir()
err := os.WriteFile(filepath.Join(tempDir, "binary.wasm"), testContent, 0600)
require.NoError(t, err)

fetcher, err := NewFetcherFunc("file://"+tempDir, lggr)
require.NoError(t, err)

resp, err := fetcher(ctx, "msg-1", ghcapabilities.Request{
URL: "http://storage.example.com/artifacts/binary.wasm",
})
require.NoError(t, err)
assert.Equal(t, testContent, resp)

resp, err = fetcher(ctx, "msg-2", ghcapabilities.Request{
URL: "https://storage.example.com/path/to/binary.wasm",
})
require.NoError(t, err)
assert.Equal(t, testContent, resp)
})

t.Run("file fetcher rejects HTTP URL with empty path", func(t *testing.T) {
tempDir := t.TempDir()
fetcher, err := NewFetcherFunc("file://"+tempDir, lggr)
require.NoError(t, err)

_, err = fetcher(ctx, "msg-1", ghcapabilities.Request{
URL: "http://storage.example.com",
})
require.Error(t, err)
assert.Contains(t, err.Error(), "HTTP URL has no filename in path")
})

t.Run("http fetcher", func(t *testing.T) {
// Create test HTTP server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package confidentialrelay

import (
"context"

tomlser "github.com/pelletier/go-toml/v2"
"github.com/pkg/errors"
"github.com/rs/zerolog"

chainselectors "github.com/smartcontractkit/chain-selectors"

"github.com/smartcontractkit/chainlink/deployment/cre/jobs/pkg"
"github.com/smartcontractkit/chainlink/system-tests/lib/cre"
coretoml "github.com/smartcontractkit/chainlink/v2/core/config/toml"
corechainlink "github.com/smartcontractkit/chainlink/v2/core/services/chainlink"
)

const flag = cre.ConfidentialRelayCapability

type ConfidentialRelay struct{}

func (o *ConfidentialRelay) Flag() cre.CapabilityFlag {
return flag
}

func (o *ConfidentialRelay) PreEnvStartup(
ctx context.Context,
testLogger zerolog.Logger,
don *cre.DonMetadata,
topology *cre.Topology,
creEnv *cre.Environment,
) (*cre.PreEnvStartupOutput, error) {
registryChainID, chErr := chainselectors.ChainIdFromSelector(creEnv.RegistryChainSelector)
if chErr != nil {
return nil, errors.Wrapf(chErr, "failed to get chain ID from selector %d", creEnv.RegistryChainSelector)
}

hErr := topology.AddGatewayHandlers(*don, []string{pkg.GatewayHandlerTypeConfidentialRelay})
if hErr != nil {
return nil, errors.Wrapf(hErr, "failed to add gateway handlers to gateway config for don %s", don.Name)
}

cErr := don.ConfigureForGatewayAccess(registryChainID, *topology.GatewayConnectors)
if cErr != nil {
return nil, errors.Wrapf(cErr, "failed to add gateway connectors to node's TOML config for don %s", don.Name)
}

// Set TOML config to activate the confidential relay handler on DON nodes.
capConfig, ok := don.CapabilityConfigs[flag]
if ok && capConfig.Values != nil {
ns := don.MustNodeSet()
for i := range ns.NodeSpecs {
currentConfig := ns.NodeSpecs[i].Node.TestConfigOverrides
var typedConfig corechainlink.Config
if currentConfig != "" {
if err := tomlser.Unmarshal([]byte(currentConfig), &typedConfig); err != nil {
return nil, errors.Wrapf(err, "failed to unmarshal node TOML config for node %d", i)
}
}

enabled := true
typedConfig.CRE.ConfidentialRelay = &coretoml.ConfidentialRelayConfig{Enabled: &enabled}

out, err := tomlser.Marshal(typedConfig)
if err != nil {
return nil, errors.Wrapf(err, "failed to marshal node TOML config for node %d", i)
}
ns.NodeSpecs[i].Node.TestConfigOverrides = string(out)
}
}

// No on-chain capability registration needed. The relay handler is a CRE subservice,
// not a registered capability. The mock capability that runs on the relay DON is
// registered separately via the mock flag.
return &cre.PreEnvStartupOutput{}, nil
}

func (o *ConfidentialRelay) PostEnvStartup(
ctx context.Context,
testLogger zerolog.Logger,
don *cre.Don,
dons *cre.Dons,
creEnv *cre.Environment,
) error {
return nil
}
2 changes: 1 addition & 1 deletion system-tests/lib/cre/features/mock/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func (o *Mock) PreEnvStartup(
Capability: kcr.CapabilitiesRegistryCapability{
LabelledName: "mock",
Version: "1.0.0",
CapabilityType: 0, // TRIGGER
CapabilityType: 1, // ACTION
},
Config: &capabilitiespb.CapabilityConfig{
LocalOnly: don.HasOnlyLocalCapabilities(),
Expand Down
41 changes: 23 additions & 18 deletions system-tests/lib/cre/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"slices"
"strconv"
"strings"
"time"

"github.com/Masterminds/semver/v3"
"github.com/ethereum/go-ethereum/common"
Expand Down Expand Up @@ -56,23 +57,24 @@ const (

// Capabilities
const (
ConsensusCapability CapabilityFlag = "ocr3"
DONTimeCapability CapabilityFlag = "don-time"
ConsensusCapabilityV2 CapabilityFlag = "consensus" // v2
CronCapability CapabilityFlag = "cron"
EVMCapability CapabilityFlag = "evm"
CustomComputeCapability CapabilityFlag = "custom-compute"
WriteEVMCapability CapabilityFlag = "write-evm"
ReadContractCapability CapabilityFlag = "read-contract"
LogEventTriggerCapability CapabilityFlag = "log-event-trigger"
WebAPITargetCapability CapabilityFlag = "web-api-target"
WebAPITriggerCapability CapabilityFlag = "web-api-trigger"
MockCapability CapabilityFlag = "mock"
VaultCapability CapabilityFlag = "vault"
HTTPTriggerCapability CapabilityFlag = "http-trigger"
HTTPActionCapability CapabilityFlag = "http-action"
SolanaCapability CapabilityFlag = "solana"
AptosCapability CapabilityFlag = "aptos"
ConsensusCapability CapabilityFlag = "ocr3"
DONTimeCapability CapabilityFlag = "don-time"
ConsensusCapabilityV2 CapabilityFlag = "consensus" // v2
CronCapability CapabilityFlag = "cron"
EVMCapability CapabilityFlag = "evm"
CustomComputeCapability CapabilityFlag = "custom-compute"
WriteEVMCapability CapabilityFlag = "write-evm"
ReadContractCapability CapabilityFlag = "read-contract"
LogEventTriggerCapability CapabilityFlag = "log-event-trigger"
WebAPITargetCapability CapabilityFlag = "web-api-target"
WebAPITriggerCapability CapabilityFlag = "web-api-trigger"
MockCapability CapabilityFlag = "mock"
VaultCapability CapabilityFlag = "vault"
HTTPTriggerCapability CapabilityFlag = "http-trigger"
HTTPActionCapability CapabilityFlag = "http-action"
SolanaCapability CapabilityFlag = "solana"
ConfidentialRelayCapability CapabilityFlag = "confidential-relay"
AptosCapability CapabilityFlag = "aptos"
// Add more capabilities as needed
)

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

newNodesStart := time.Now()
nodes, err := newNodes(cfgs)
if err != nil {
return nil, fmt.Errorf("failed to create nodes metadata: %w", err)
}
framework.L.Info().
Str("don", c.Name).
Int("nodes", len(cfgs)).
Float64("duration_s", time.Since(newNodesStart).Seconds()).
Msg("Node metadata generation completed")

capConfigs, capErr := processCapabilityConfigs(c, capabilityConfigs)
Expand Down Expand Up @@ -1462,6 +1466,7 @@ type NodeKeyInput struct {
}

func NewNodeKeys(input NodeKeyInput) (*secrets.NodeKeys, error) {
start := time.Now()
out := &secrets.NodeKeys{
EVM: make(map[uint64]*crypto.EVMKey),
Solana: make(map[string]*crypto.SolKey),
Expand Down Expand Up @@ -1520,7 +1525,7 @@ func NewNodeKeys(input NodeKeyInput) (*secrets.NodeKeys, error) {
framework.L.Debug().
Int("evm_chains", len(input.EVMChainIDs)).
Int("solana_chains", len(input.SolanaChainIDs)).
Bool("imported", input.ImportedSecrets != "").
Float64("duration_s", time.Since(start).Seconds()).
Msg("Node key generation completed")
return out, nil
}
Expand Down
Loading