Skip to content

[CRE] [4/5] ConfidentialModule, config, DB migration, syncer routing#21641

Open
nadahalli wants to merge 12 commits intodevelopfrom
tejaswi/cw-4-confidential-module
Open

[CRE] [4/5] ConfidentialModule, config, DB migration, syncer routing#21641
nadahalli wants to merge 12 commits intodevelopfrom
tejaswi/cw-4-confidential-module

Conversation

@nadahalli
Copy link
Copy Markdown
Contributor

@nadahalli nadahalli commented Mar 23, 2026

Context

Part of #21635 (confidential workflow execution). [4/5] in the series.
Can be reviewed and merged independently.

What this does

Core abstractions for confidential workflow execution:

  • ConfidentialModule: implements host.ModuleV2, dispatches workflow
    execution to TEE enclave via the confidential-workflows capability
    instead of running WASM locally. Strategy pattern; the engine is
    unchanged.
  • Syncer routing: tryEngineCreate checks IsConfidential(spec.Attributes).
    If true, short-circuits to tryConfidentialEngineCreate. 6-line
    early-return, existing path untouched.
  • DB migration: adds attributes bytea column to workflow_specs_v2.
  • WorkflowSpec.Attributes: persists on-chain workflow attributes.

Nothing is wired into CRE yet. The routing is inert until PR 5/5.

Dependencies

None. Compiles against develop independently.

Copilot AI review requested due to automatic review settings March 23, 2026 17:01
@nadahalli nadahalli requested review from a team as code owners March 23, 2026 17:01
@github-actions
Copy link
Copy Markdown
Contributor

👋 nadahalli, thanks for creating this pull request!

To help reviewers, please consider creating future PRs as drafts first. This allows you to self-review and make any final changes before notifying the team.

Once you're ready, you can mark it as "Ready for review" to request feedback. Thanks!

@github-actions
Copy link
Copy Markdown
Contributor

I see you updated files related to core. Please run make gocs in the root directory to add a changeset as well as in the text include at least one of the following tags:

  • #added For any new functionality added.
  • #breaking_change For any functionality that requires manual action for the node to boot.
  • #bugfix For bug fixes.
  • #changed For any change to the existing functionality.
  • #db_update For any feature that introduces updates to database schema.
  • #deprecation_notice For any upcoming deprecation functionality.
  • #internal For changesets that need to be excluded from the final changelog.
  • #nops For any feature that is NOP facing and needs to be in the official Release Notes for the release.
  • #removed For any functionality/config that is removed.
  • #updated For any functionality that is updated.
  • #wip For any change that is not ready yet and external communication about it should be held off till it is feature complete.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 23, 2026

✅ No conflicts with other open PRs targeting develop

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Risk Rating: HIGH

This PR adds the core plumbing for “confidential workflow execution” by persisting on-chain workflow attributes, introducing a ConfidentialModule implementation that delegates execution to the confidential-workflows capability, and routing confidential workflows onto that path. It also introduces gateway + node-side confidential relay handlers and configuration needed for relay DON participation.

Changes:

  • Persist workflow Attributes (DB + ORM + model) and route confidential workflows to a confidential engine creation path.
  • Add ConfidentialModule (host.ModuleV2) that dispatches execution to the confidential-workflows capability, including secret identifiers forwarding.
  • Add confidential relay handler implementations (gateway-side fanout + quorum aggregation; node-side attestation validation + Vault/capability proxy) plus CRE config surface.

Reviewed changes

Copilot reviewed 20 out of 21 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
go.mod Bumps deps needed for confidential workflow/relay support.
go.sum Updates checksums for dependency changes.
core/store/migrate/migrations/0295_add_workflow_attributes_column.sql Adds attributes column to workflow_specs_v2.
core/services/job/models.go Adds WorkflowSpec.Attributes for DB persistence.
core/services/workflows/artifacts/v2/orm.go Upsert now writes attributes to DB.
core/services/workflows/syncer/v2/handler.go Persists attributes into specs; routes confidential workflows to confidential engine creation; refactors engine start/register helper.
core/services/workflows/syncer/v2/handler_test.go Adds tests validating confidential vs non-confidential routing behavior.
core/services/workflows/v2/confidential_module.go Implements ConfidentialModule, attributes parsing, and binary hashing for confidential execution.
core/services/workflows/v2/confidential_module_test.go Unit tests for attribute parsing, hashing, and capability dispatch behavior.
core/services/workflows/syncer/fetcher.go Extends file fetcher to accept HTTP(S) URLs for local reads (confidential workflow compatibility).
core/services/workflows/syncer/v2/fetcher.go Same file-fetcher HTTP(S) URL support for v2 syncer.
core/services/standardcapabilities/conversions/conversions.go Adds mock command/capability ID mapping.
core/services/gateway/handlers/confidentialrelay/handler.go Adds gateway confidential relay handler (fanout, quorum aggregation, timeouts, metrics).
core/services/gateway/handlers/confidentialrelay/aggregator.go Adds quorum aggregation logic based on response digest matching.
core/services/gateway/handlers/confidentialrelay/handler_test.go Adds test coverage for gateway relay handler behaviors (quorum, timeouts, rate limiting, etc.).
core/config/cre_config.go Extends CRE config interface with ConfidentialRelay.
core/config/toml/types.go Adds [CRE.ConfidentialRelay] TOML block and merge behavior.
core/services/chainlink/config_cre.go Implements CREConfidentialRelay accessors for runtime config.
core/capabilities/confidentialrelay/service.go Adds lifecycle wrapper to create/start node-side relay handler once gateway connector is available.
core/capabilities/confidentialrelay/handler.go Adds node-side enclave relay handler (attestation verification + Vault/capability proxy).
core/capabilities/confidentialrelay/handler_test.go Adds tests for node-side relay handler behavior (cap exec, errors, lifecycle).

Scrupulous human review recommended for:

  • core/services/workflows/syncer/v2/handler.go: tryEngineCreate / tryConfidentialEngineCreate / startAndRegisterEngine flow (engine lifecycle + hook wiring).
  • core/capabilities/confidentialrelay/handler.go: verifyAttestationHash + request translation into Vault/capability calls (security-sensitive).

Suggested reviewers (per .github/CODEOWNERS):

  • @smartcontractkit/keystone (workflows + capabilities areas)
  • @smartcontractkit/capabilities-team (capabilities handlers)
  • @smartcontractkit/foundations / @smartcontractkit/core (job models, config, deps)

Comment on lines +183 to +186
// For HTTP(S) URLs, extract just the filename and resolve against basePath.
// This supports confidential workflows where the on-chain URL must be HTTP
// (so the enclave can fetch the binary), but the syncer reads from the local filesystem.
fullPath = filepath.Join(basePath, filepath.Base(u.Path))
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

For HTTP(S) URLs, collapsing the path to filepath.Base(u.Path) means different on-chain URLs that share the same last segment (e.g. /wfA/binary and /wfB/binary) will map to the same local file, so the syncer can read the wrong artifact. Consider preserving more of the URL path (e.g., clean u.Path, trim leading '/', and join to basePath) or otherwise deriving a collision-resistant local path, and add a unit test covering the HTTP(S)-URL-with-file-basePath behavior.

Suggested change
// For HTTP(S) URLs, extract just the filename and resolve against basePath.
// This supports confidential workflows where the on-chain URL must be HTTP
// (so the enclave can fetch the binary), but the syncer reads from the local filesystem.
fullPath = filepath.Join(basePath, filepath.Base(u.Path))
// For HTTP(S) URLs, preserve the cleaned path (minus any leading slash) and
// resolve it against basePath. This avoids collisions where different URLs
// share the same last segment (e.g., /wfA/binary and /wfB/binary).
cleanPath := strings.TrimPrefix(filepath.Clean(u.Path), "/")
fullPath = filepath.Clean(filepath.Join(basePath, cleanPath))

Copilot uses AI. Check for mistakes.
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.

Acknowledged. This is a dev-mode file fetcher, not a production path. In file-fetcher mode there's a single flat directory of artifacts. filepath.Base is safe here (no path traversal, existing hasPrefix check still runs). Filename collisions would already be a problem at the directory level.

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.

Reverted the entire HTTP URL handling from the file fetcher. The code path was dead; the enclave has its own binary fetcher.

Comment on lines +217 to +220
// For HTTP(S) URLs, extract just the filename and resolve against basePath.
// This supports confidential workflows where the on-chain URL must be HTTP
// (so the enclave can fetch the binary), but the syncer reads from the local filesystem.
fullPath = filepath.Join(basePath, filepath.Base(u.Path))
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

For HTTP(S) URLs, collapsing the path to filepath.Base(u.Path) means different on-chain URLs that share the same last segment (e.g. /wfA/binary and /wfB/binary) will map to the same local file, so the syncer can read the wrong artifact. Consider preserving more of the URL path (e.g., clean u.Path, trim leading '/', and join to basePath) or otherwise deriving a collision-resistant local path, and add a unit test covering the HTTP(S)-URL-with-file-basePath behavior.

Suggested change
// For HTTP(S) URLs, extract just the filename and resolve against basePath.
// This supports confidential workflows where the on-chain URL must be HTTP
// (so the enclave can fetch the binary), but the syncer reads from the local filesystem.
fullPath = filepath.Join(basePath, filepath.Base(u.Path))
// For HTTP(S) URLs, preserve the URL path (relative to basePath) instead of
// collapsing to just the filename. This avoids collisions where different
// URLs share the same basename (e.g., /wfA/binary and /wfB/binary).
cleanPath := filepath.Clean(u.Path)
// Make the path relative so we always resolve it under basePath.
cleanPath = strings.TrimPrefix(cleanPath, string(filepath.Separator))
if cleanPath == "" || cleanPath == "." {
return nil, fmt.Errorf("invalid HTTP URL path %q", u.Path)
}
fullPath = filepath.Join(basePath, cleanPath)

Copilot uses AI. Check for mistakes.
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.

Same as above.

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.

Same as above, reverted.

Comment on lines +180 to +199
// Normalize owner to EIP-55 checksum format, matching how secrets are stored.
normalizedOwner := common.HexToAddress(params.Owner).Hex()

vaultReq := &vault.GetSecretsRequest{
Requests: make([]*vault.SecretRequest, 0, len(params.Secrets)),
}
for _, s := range params.Secrets {
namespace := s.Namespace
if namespace == "" {
namespace = vaulttypes.DefaultNamespace
}
vaultReq.Requests = append(vaultReq.Requests, &vault.SecretRequest{
Id: &vault.SecretIdentifier{
Key: s.Key,
Namespace: namespace,
Owner: normalizedOwner,
},
EncryptionKeys: []string{params.EnclavePublicKey},
})
}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

handleSecretsGet forwards params.EnclavePublicKey into vaultReq.EncryptionKeys without validating it is present. If it's empty, VaultDON will either fail later or return data that translateVaultResponse can't match (leading to an internal error instead of a clear invalid-params response). Consider validating required fields (Owner, WorkflowID, ExecutionID, EnclavePublicKey, and secret identifiers) up front and returning jsonrpc.ErrInvalidParams for missing/empty values.

Copilot uses AI. Check for mistakes.
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.

This file has been removed from this PR. It belongs to #21639 [2/5], where the nil-params and owner-validation fixes have been applied.

@nadahalli nadahalli requested a review from a team as a code owner March 23, 2026 17:19
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 23, 2026

CORA - Analysis Skipped

Reason: The number of code owners (3) is less than the minimum required (5) and/or the number of CODEOWNERS entries with changed files (4) is less than the minimum required (2).

@nadahalli nadahalli force-pushed the tejaswi/cw-4-confidential-module branch 3 times, most recently from 54fbf1c to e7a3894 Compare March 24, 2026 11:40
CreatedAt time.Time `toml:"-"`
UpdatedAt time.Time `toml:"-"`
SpecType WorkflowSpecType `toml:"spec_type" db:"spec_type"`
Attributes []byte `db:"attributes"`
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.

Let's figure out if we need a new Attributes field here or can overload a different existing field.

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.

Any ideas?

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.

We can probably open a thread with dev services today.

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.

Update: this is optimal.

if u.Scheme == "http" || u.Scheme == "https" {
// For HTTP(S) URLs, extract just the filename and resolve against basePath.
// This supports confidential workflows where the on-chain URL must be HTTP
// (so the enclave can fetch the binary), but the syncer reads from the local filesystem.
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.

I imagine we don't want to use regular http schemes in prod.

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.

Is this meant to allow us to mock something in E2E tests? Since obviously the real syncer service will be remote over https.

We can do a self-signed/insecure https endpoint for our own tests and continue using a local filesystem-based fetcher.

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.

You're right. Reverted the HTTP URL handling from the file fetcher in both syncer/ and syncer/v2/. The enclave has its own BinaryFetcher that does the HTTP fetch independently; the node syncer doesn't need to handle HTTP URLs in the file fetcher path.

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.

Reverted. The file fetcher now only handles file paths, as before. The enclave fetches the binary over HTTP via its own fetcher (confidential-compute/enclave/apps/confidential-workflows/app/fetcher.go), which is completely separate from the node syncer path.

@trunk-io
Copy link
Copy Markdown

trunk-io bot commented Mar 24, 2026

Static BadgeStatic BadgeStatic BadgeStatic Badge

View Full Report ↗︎Docs

// tryConfidentialEngineCreate creates a V2 engine backed by a ConfidentialModule
// instead of a local WASM module. The ConfidentialModule delegates execution to
// the confidential-workflows capability which runs the WASM inside a TEE.
func (h *eventHandler) tryConfidentialEngineCreate(
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.

I think this function duplicates too much existing code that we don't want to maintain, so creates maintainability issues for others.

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.

Done. Unified the engine creation flow and inlined startAndRegisterEngine back into tryEngineCreate so the confidential and normal paths share the same code.

Comment on lines +909 to +951
cfg := &v2.EngineConfig{
Lggr: h.lggr,
Module: module,
WorkflowConfig: []byte(spec.Config),
CapRegistry: h.capRegistry,
DonSubscriber: h.workflowDonSubscriber,
UseLocalTimeProvider: h.useLocalTimeProvider,
DonTimeStore: h.donTimeStore,
ExecutionsStore: h.workflowStore,
WorkflowID: spec.WorkflowID,
WorkflowOwner: spec.WorkflowOwner,
WorkflowName: workflowName,
WorkflowTag: spec.WorkflowTag,
WorkflowEncryptionKey: h.workflowEncryptionKey,

LocalLimits: v2.EngineLimits{},
LocalLimiters: h.engineLimiters,
GlobalExecutionConcurrencyLimiter: h.workflowLimits,

BeholderEmitter: h.emitter,
BillingClient: h.billingClient,

WorkflowRegistryAddress: h.workflowRegistryAddress,
WorkflowRegistryChainSelector: h.workflowRegistryChainSelector,
OrgResolver: h.orgResolver,
SecretsFetcher: h.secretsFetcher,
FeatureFlags: h.featureFlags,
}

existingHook := cfg.Hooks.OnInitialized
cfg.Hooks.OnInitialized = func(err error) {
initDone <- err
if existingHook != nil {
existingHook(err)
}
}

engine, err := v2.NewEngine(cfg)
if err != nil {
return fmt.Errorf("failed to create confidential workflow engine: %w", err)
}

return h.startAndRegisterEngine(ctx, engine, initDone, wid, spec.WorkflowID, source)
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.

all this is boilerplate, so would be nice to refactor this.

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.

Done. Extracted newV2EngineConfig and cleaned up factory signatures.

// If it's not absolute, we assume it's relative to the basePath
fullPath = filepath.Join(basePath, fullPath)
var fullPath string
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.

again, we should be able to set something up over https in our tests.

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.

Agreed, reverted.

@@ -0,0 +1,188 @@
package v2
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.

lgtm

@nadahalli nadahalli force-pushed the tejaswi/cw-4-confidential-module branch from 9bbc5e5 to f58cb63 Compare March 24, 2026 12:26

// newV2EngineConfig builds the common EngineConfig shared by both the normal
// WASM engine and the confidential engine paths. Caller supplies the module.
func (h *eventHandler) newV2EngineConfig(
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.

nice ty for doing this

vreff
vreff previously approved these changes Mar 25, 2026
@nadahalli nadahalli force-pushed the tejaswi/cw-4-confidential-module branch from 13931fe to de37e16 Compare March 26, 2026 15:06
vreff
vreff previously approved these changes Mar 30, 2026
@nadahalli nadahalli force-pushed the tejaswi/cw-4-confidential-module branch 2 times, most recently from ad18f1a to a815ace Compare April 1, 2026 16:20
@cl-sonarqube-production
Copy link
Copy Markdown

Quality Gate failed Quality Gate failed

Failed conditions
C Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube

Catch issues before they fail your Quality Gate with our IDE extension SonarQube IDE SonarQube IDE

nadahalli added 12 commits April 2, 2026 16:19
The filepath.Base() code for HTTP URLs in newFileFetcher was dead code.
No test or production path sends an HTTP URL to a file-based fetcher.
The enclave fetches binaries via its own BinaryFetcher, independent of
the node syncer's fetcher.
Restore comments that were dropped when extracting startAndRegisterEngine
from tryEngineCreate. Extract common EngineConfig construction into
newV2EngineConfig and initDone hook wiring into wireInitDoneHook,
reducing duplication between the normal and confidential engine paths.
Replace the early-return pattern with a symmetric if/else that picks
the factory, then converges on a single startAndRegisterEngine call.
Rename tryConfidentialEngineCreate to confidentialEngineFactory and
change its signature to return (services.Service, error).

# Conflicts:
#	core/services/workflows/syncer/v2/handler.go
No longer needed as a separate method now that both engine paths
converge in tryEngineCreate.
Group string params together in newV2EngineConfig, move SdkName and
DebugMode into the constructor, drop unused wid param from
confidentialEngineFactory.

# Conflicts:
#	core/services/workflows/syncer/v2/handler.go
The enclave needs an authenticated URL to download WASM binaries from
the CRE storage service. BinaryURLResolver resolves the raw on-chain
URL into a presigned/ephemeral URL per execution. Nil-safe: falls
back to the raw URL when no resolver is set.

PR 5/5 (#21642) wires this to the storage service retriever.
@nadahalli nadahalli force-pushed the tejaswi/cw-4-confidential-module branch from a815ace to 764834e Compare April 2, 2026 14:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants