From 7685e848778c8147b5637e5cbed5d9abd1de7129 Mon Sep 17 00:00:00 2001 From: vreff <104409744+vreff@users.noreply.github.com> Date: Wed, 12 Aug 2026 12:40:22 -0400 Subject: [PATCH 01/29] confidential workflows e2e --- .github/workflows/cre-system-tests.yaml | 54 +- ...pabilities-don-confidential-workflows.toml | 139 ++++++ .../confidentialcompute.go | 214 ++++++++ system-tests/lib/go.mod | 3 +- system-tests/lib/go.sum | 6 + system-tests/tests/go.mod | 12 +- system-tests/tests/go.sum | 14 + .../smoke/cre/confidential_workflows_env.go | 216 ++++++++ .../smoke/cre/confidential_workflows_test.go | 409 +++++++++++++++ .../confidential_workflows_test_helpers.go | 467 ++++++++++++++++++ .../cre/testdata/confidentialworkflow/go.mod | 26 + .../cre/testdata/confidentialworkflow/go.sum | 49 ++ .../cre/testdata/confidentialworkflow/main.go | 193 ++++++++ 13 files changed, 1796 insertions(+), 6 deletions(-) create mode 100644 core/scripts/cre/environment/configs/workflow-gateway-capabilities-don-confidential-workflows.toml create mode 100644 system-tests/lib/cre/capabilities/confidentialcompute/confidentialcompute.go create mode 100644 system-tests/tests/smoke/cre/confidential_workflows_env.go create mode 100644 system-tests/tests/smoke/cre/confidential_workflows_test.go create mode 100644 system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go create mode 100644 system-tests/tests/smoke/cre/testdata/confidentialworkflow/go.mod create mode 100644 system-tests/tests/smoke/cre/testdata/confidentialworkflow/go.sum create mode 100644 system-tests/tests/smoke/cre/testdata/confidentialworkflow/main.go diff --git a/.github/workflows/cre-system-tests.yaml b/.github/workflows/cre-system-tests.yaml index 9512e7d95f8..f3faab63f72 100644 --- a/.github/workflows/cre-system-tests.yaml +++ b/.github/workflows/cre-system-tests.yaml @@ -116,6 +116,9 @@ jobs: ], "Test_CRE_V2_HTTP_Action_Multi_Gateway": [ {"topology":"workflow-gateway-capabilities-multi-gateway","configs":"configs/workflow-gateway-capabilities-multi-gateway-don.toml"} + ], + "Test_CRE_V2_ConfidentialWorkflows_Relay": [ + {"topology":"workflow-gateway-capabilities-confidential-workflows","configs":"configs/workflow-gateway-capabilities-don-confidential-workflows.toml","timeout_minutes":25,"test_timeout":"20m"} ] }' @@ -159,7 +162,9 @@ jobs: # http://docs.github.com/en/actions/how-tos/deploy/configure-and-manage-deployments/control-deployments#using-environments-without-deployments name: integration deployment: false - timeout-minutes: 10 + # Most legs finish well inside 10 minutes; legs that stand up extra + # infrastructure (e.g. local enclaves) declare their own budget in the matrix. + timeout-minutes: ${{ matrix.tests.timeout_minutes || 10 }} env: ENABLE_AUTO_QUARANTINE: "true" BILLING_PLATFORM_SERVICE_IMAGE: @@ -320,6 +325,49 @@ jobs: done exit 1 + # The confidential compute relay leg needs a chainlink-confidential-compute + # checkout: the CRE nodes run its confidential-http capability binary, and + # the test drives its enclave harness (tests/testhelpers) to start local + # enclaves. These runners are not Nitro-capable, so the harness falls back + # to fake enclaves (local processes over loopback vsock emulation). + # The confidential-workflows *capability* binary ships in the node image via + # plugins/plugins.public.yaml. The *enclave* side is not a plugin: the test + # drives chainlink-confidential-compute's enclave harness, which shells out + # to that repo's fake-enclave runner. Check it out at the same revision the + # plugin is built from so the capability and the enclave app always match. + - name: Resolve confidential compute revision + if: ${{ contains(matrix.tests.test_name, 'ConfidentialWorkflows') }} + id: cc-rev + shell: bash + run: | + set -euo pipefail + ref=$(yq -r '.plugins."confidential-workflows"[0].gitRef' plugins/plugins.public.yaml) + if [ -z "$ref" ] || [ "$ref" = "null" ]; then + echo "could not resolve confidential-workflows gitRef from plugins/plugins.public.yaml" >&2 + exit 1 + fi + echo "Resolved confidential compute revision: $ref" + echo "ref=$ref" >> "${GITHUB_OUTPUT}" + + - name: Checkout chainlink-confidential-compute + if: ${{ contains(matrix.tests.test_name, 'ConfidentialWorkflows') }} + uses: actions/checkout@v7 + with: + repository: smartcontractkit/chainlink-confidential-compute + ref: ${{ steps.cc-rev.outputs.ref }} + path: chainlink-confidential-compute + persist-credentials: false + + # Prebuild the enclave app and host binaries the fake-enclave runner + # executes, so the cost lands here rather than in the test's own timeout. + # confidential-workflows is its own Go module, so this has to build from + # inside it rather than from the repository root. + - name: Prebuild fake enclave binaries + if: ${{ contains(matrix.tests.test_name, 'ConfidentialWorkflows') }} + shell: bash + working-directory: chainlink-confidential-compute/enclave/apps/confidential-workflows + run: go build ./environments/fake/... + - name: Start local CRE${{ matrix.tests.cre_version }} id: start-local-cre uses: ./.github/actions/start-local-cre-environment @@ -353,9 +401,11 @@ jobs: continue-on-error: ${{ env.ENABLE_AUTO_QUARANTINE == 'true' }} env: TEST_NAME: ${{ matrix.tests.test_name }} - TEST_TIMEOUT: 7m # let's leave 3 minutes for other steps (the whole job times out after 10 minutes) + # Leave ~3 minutes for the other steps in the job's timeout budget. + TEST_TIMEOUT: ${{ matrix.tests.test_timeout || '7m' }} RUN_QUARANTINED_TESTS: "true" # always run quarantined tests in CI TOPOLOGY_NAME: ${{ matrix.tests.topology }} + CONFIDENTIAL_COMPUTE_ROOT: ${{ github.workspace }}/chainlink-confidential-compute GITHUB_TOKEN: ${{ steps.github-token.outputs.access-token || '' }} # to avoid rate limiting when downloading protobuf files from GitHub PARALLEL_COUNT: "10" CRE_TEST_PARALLEL_ENABLED: "true" diff --git a/core/scripts/cre/environment/configs/workflow-gateway-capabilities-don-confidential-workflows.toml b/core/scripts/cre/environment/configs/workflow-gateway-capabilities-don-confidential-workflows.toml new file mode 100644 index 00000000000..2eed9a57731 --- /dev/null +++ b/core/scripts/cre/environment/configs/workflow-gateway-capabilities-don-confidential-workflows.toml @@ -0,0 +1,139 @@ +# Topology for the confidential workflows engine E2E test. +# +# Differences from workflow-gateway-capabilities-don.toml: +# - the workflow DON also hosts "confidential-workflows" (the capability that +# routes execution into the enclaves) and "confidential-relay" (the gateway +# handler the enclaves call back through) +# - PerWorkflow.ConfidentialWorkflows is enabled so the engine will honour a +# workflow registered with {"confidential":true} attributes +# - the workflow DON exposes remote capabilities and enables the DKG recipient +# so the pre-enclave secret fetch can reach the vault DON +# +# The confidential-workflows capability binary is not part of this repository; it +# is built from a chainlink-confidential-compute checkout and mounted at +# ./binaries/confidential-workflows (see .github/workflows/cre-system-tests.yaml). + +[chip_router] + image = "local-cre-chip-router:v1.0.1" + +[[blockchains]] + type = "anvil" + chain_id = "1337" + container_name = "anvil-1337" + docker_cmd_params = ["-b", "0.5", "--mixed-mining"] + +[[blockchains]] + type = "anvil" + chain_id = "2337" + container_name = "anvil-2337" + port = "8546" + docker_cmd_params = ["-b", "0.5", "--mixed-mining"] + +[jd] + csa_encryption_key = "d1093c0060d50a3c89c189b2e485da5a3ce57f3dcb38ab7e2c0d5f0bb2314a44" # any random 32 byte hex string + image = "job-distributor:0.28.0" + +[fake] + port = 8171 + +[fake_http] + port = 8666 + +[infra] + # either "docker" or "kubernetes" + type = "docker" + +[[nodesets]] + nodes = 4 + name = "workflow" + don_family = "test-don-family" + don_types = ["workflow"] + override_mode = "all" + http_port_range_start = 10100 + + supported_evm_chains = [1337, 2337] + + env_vars = { CL_EVM_CMD = "", OTEL_SERVICE_NAME = "chainlink-node", CL_CRE_SETTINGS = '{"global":{"VaultOrgIdAsSecretOwnerEnabled":false}}', CL_CRE_SETTINGS_DEFAULT = '{"RemoteExecutableWorkflowDONBindingEnabled":"true","PerWorkflow":{"ConfidentialWorkflows":{"Enabled":"true"}}}' } + capabilities = ["consensus", "confidential-workflows", "confidential-relay", "cron", "http-action", "http-trigger", "don-time", "evm-1337"] + exposes_remote_capabilities = true + registry_based_launch_allowlist = ["cron-trigger@1.0.0"] + + [nodesets.db] + image = "postgres:12.0" + port = 13000 + + [[nodesets.node_specs]] + roles = ["plugin"] + [nodesets.node_specs.node] + docker_ctx = "../../../.." + docker_file = "core/chainlink.Dockerfile" + docker_build_args = { "CL_IS_PROD_BUILD" = "false" } + user_config_overrides = """ + [P2P] + EnableExperimentalRageP2P = true + + [CRE] + EnableDKGRecipient = true + + # The test copies the workflow binary and config into the containers, so the + # syncer reads them from disk rather than fetching them remotely. + [CRE.WorkflowFetcher] + URL = "file:///home/chainlink/workflows" + """ + +[[nodesets]] + nodes = 4 + name = "capabilities" + don_family = "test-don-family" + don_types = ["capabilities"] + exposes_remote_capabilities = true + override_mode = "all" + http_port_range_start = 10200 + + supported_evm_chains = [1337, 2337] + + env_vars = { CL_EVM_CMD = "", OTEL_SERVICE_NAME = "chainlink-node", CL_CRE_SETTINGS = '{"global":{"VaultOrgIdAsSecretOwnerEnabled":false}}', CL_CRE_SETTINGS_DEFAULT = '{"RemoteExecutableWorkflowDONBindingEnabled":"true"}' } + capabilities = ["vault", "evm-2337"] + + [nodesets.db] + image = "postgres:12.0" + port = 13100 + + [[nodesets.node_specs]] + roles = ["plugin"] + [nodesets.node_specs.node] + docker_ctx = "../../../.." + docker_file = "core/chainlink.Dockerfile" + docker_build_args = { "CL_IS_PROD_BUILD" = "false" } + user_config_overrides = """ + [P2P] + EnableExperimentalRageP2P = true + + [CRE] + EnableDKGRecipient = true + """ + +[[nodesets]] + nodes = 1 + name = "bootstrap-gateway" + don_family = "test-don-family" + don_types = ["bootstrap", "gateway"] + override_mode = "each" + http_port_range_start = 10300 + + env_vars = { CL_EVM_CMD = "", OTEL_SERVICE_NAME = "chainlink-node", CL_CRE_SETTINGS = '{"global":{"PerOrg":{"BaseTriggerRetransmitEnabled":"true"}}}' } + supported_evm_chains = [1337, 2337] + + [nodesets.db] + image = "postgres:12.0" + port = 13200 + + [[nodesets.node_specs]] + roles = ["bootstrap", "gateway"] + [nodesets.node_specs.node] + docker_ctx = "../../../.." + docker_file = "core/chainlink.Dockerfile" + # 5002 is the web API capabilities port for incoming requests + # 15002 is the vault port for incoming requests + custom_ports = ["5002:5002","15002:15002"] + user_config_overrides = "" diff --git a/system-tests/lib/cre/capabilities/confidentialcompute/confidentialcompute.go b/system-tests/lib/cre/capabilities/confidentialcompute/confidentialcompute.go new file mode 100644 index 00000000000..89b62b30650 --- /dev/null +++ b/system-tests/lib/cre/capabilities/confidentialcompute/confidentialcompute.go @@ -0,0 +1,214 @@ +// Package confidentialcompute registers a confidential compute capability (e.g. +// confidential-workflows, confidential-http) with a CRE DON: it proposes the standardcapabilities job +// that runs the capability binary on each worker node, and writes the enclave +// list into the capability's on-chain registry config. +// +// The confidential relay handler reads that registry config to discover which +// enclaves to route requests to, so the enclave list must be supplied before +// the environment starts. +package confidentialcompute + +import ( + "context" + "crypto/rand" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + + "github.com/google/uuid" + "github.com/pkg/errors" + "golang.org/x/crypto/nacl/box" + + capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" + cctypes "github.com/smartcontractkit/chainlink-confidential-compute/types" + kcr "github.com/smartcontractkit/chainlink-evm/gethwrappers/keystone/generated/capabilities_registry_1_1_0" + "github.com/smartcontractkit/chainlink-protos/cre/go/values" + jobv1 "github.com/smartcontractkit/chainlink-protos/job-distributor/v1/job" + keystone_changeset "github.com/smartcontractkit/chainlink/deployment/keystone/changeset" + + "github.com/smartcontractkit/chainlink/system-tests/lib/cre" + "github.com/smartcontractkit/chainlink/system-tests/lib/cre/capabilities" + "github.com/smartcontractkit/chainlink/system-tests/lib/cre/flags" +) + +// apiKey is the API key the capability presents to the enclaves. Enclaves in +// tests are started with a matching key; a real key is not needed, but using a +// non-empty one keeps the encrypt/decrypt path exercised. +const apiKey = "foobar" + +var jobTemplate = ` +type = "standardcapabilities" +schemaVersion = 1 +externalJobID = "%s" +forwardingAllowed = false +command = "%s" +name = "%s" +config = %s +` + +// jobsDelivered guards against the job spec function being invoked more than +// once per capability name for a single environment. +var jobsDelivered = make(map[string]bool) + +// ResetDeliveryState clears the jobsDelivered guard so job specs can be +// re-delivered when a new CRE environment is created (e.g. across subtests). +func ResetDeliveryState() { + jobsDelivered = make(map[string]bool) +} + +// New returns an InstallableCapability for a confidential compute capability. +// name is both the DON capability flag and the registered LabelledName (e.g. +// "confidential-http"); binaryName is the capability binary the node runs. +// Pass a nil enclaves slice to register the capability with an empty enclave +// list, which is enough to satisfy config validation for capabilities the test +// does not exercise. +func New(name, version, binaryName string, enclaves []cctypes.Enclave) (*capabilities.Capability, error) { + return capabilities.New( //nolint:staticcheck // SA1019 mirrors existing capability registrations + name, + capabilities.WithJobSpecFn(jobSpec(name, binaryName)), + capabilities.WithCapabilityRegistryV2ConfigFn(registryConfigFn(name, version, enclaves)), + ) +} + +func jobSpec(name string, binaryName string) cre.JobSpecFn { + return func(input *cre.JobSpecInput) (cre.DonJobs, error) { + if jobsDelivered[name] { + return nil, nil + } + jobsDelivered[name] = true + + donJobs := make(cre.DonJobs, 0) + for _, don := range input.Dons.List() { + if !don.HasFlag(name) { + continue + } + + workerNodes, wErr := don.Workers() + if wErr != nil { + return nil, errors.Wrap(wErr, "failed to find worker nodes") + } + + encryptedAPIKeys := make([]string, 0, len(workerNodes)) + for _, workerNode := range workerNodes { + publicKey, kErr := workflowEncryptionKey(workerNode) + if kErr != nil { + return nil, kErr + } + + ctxt, sErr := box.SealAnonymous(nil, []byte(apiKey), &publicKey, rand.Reader) + if sErr != nil { + return nil, errors.Wrap(sErr, "failed to seal API key") + } + encryptedAPIKeys = append(encryptedAPIKeys, hex.EncodeToString(ctxt)) + } + + for _, workerNode := range workerNodes { + // Keep liveness detection aggressive in e2e so failover traffic starts + // only after each node has had a chance to observe a dead enclave. + config := map[string]any{ + "InsecureSkipTLSVerify": true, + "EncryptedAPIKeys": strings.Join(encryptedAPIKeys, ","), + "EnableCache": true, + "EnableProactiveRefresh": true, + "MaxRetries": 3, + "RetryBackoffSeconds": 5, + } + configBytes, mErr := json.Marshal(config) + if mErr != nil { + return nil, errors.Wrap(mErr, "failed to marshal capability config") + } + donJobs = append(donJobs, &jobv1.ProposeJobRequest{ + NodeId: workerNode.JobDistributorDetails.NodeID, + Spec: fmt.Sprintf(jobTemplate, uuid.NewString(), binaryName, name, fmt.Sprintf("'%s'", string(configBytes))), + }) + } + } + + return donJobs, nil + } +} + +// workflowEncryptionKey reads a node's workflow public encryption key, which is +// used to seal the capability's API key so it is not stored in plaintext. +func workflowEncryptionKey(workerNode *cre.Node) ([32]byte, error) { + var publicKey [32]byte + + apiClient := workerNode.Clients.RestClient.APIClient + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, apiClient.BaseURL+"/v2/keys/workflow", nil) + if err != nil { + return publicKey, errors.Wrap(err, "failed to create request to get workflow keys") + } + req.AddCookie(apiClient.Cookies[0]) + + resp, err := apiClient.GetClient().Do(req) + if err != nil { + return publicKey, errors.Wrap(err, "failed to send request to get workflow keys") + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + return publicKey, fmt.Errorf("expected 200 OK from get workflow keys request, got %d", resp.StatusCode) + } + body, err := io.ReadAll(resp.Body) + if err != nil { + return publicKey, errors.Wrap(err, "failed to read response body from get workflow keys request") + } + + var workflowKeysResp struct { + Data []struct { + Attributes struct { + PublicKey string `json:"publicKey"` + } `json:"attributes"` + } `json:"data"` + } + if err := json.Unmarshal(body, &workflowKeysResp); err != nil { + return publicKey, errors.Wrap(err, "failed to unmarshal workflow keys response") + } + if len(workflowKeysResp.Data) == 0 { + return publicKey, errors.New("no workflow keys found in response") + } + + publicKeyBytes, err := hex.DecodeString(workflowKeysResp.Data[0].Attributes.PublicKey) + if err != nil { + return publicKey, errors.Wrap(err, "failed to decode public key hex") + } + if len(publicKeyBytes) != len(publicKey) { + return publicKey, fmt.Errorf("expected public key to be %d bytes, got %d", len(publicKey), len(publicKeyBytes)) + } + copy(publicKey[:], publicKeyBytes) + + return publicKey, nil +} + +// registryConfigFn writes the enclave list into the capability's on-chain +// registry config, which is how the confidential relay handler discovers the +// enclaves it may route to. +func registryConfigFn(name string, version string, enclaves []cctypes.Enclave) cre.CapabilityRegistryConfigFn { + return func(donFlags []string, _ *cre.NodeSet) ([]keystone_changeset.DONCapabilityWithConfig, error) { + if !flags.HasFlag(donFlags, name) { + return nil, nil + } + + wrappedConfig, err := values.WrapMap(cctypes.EnclavesList{Enclaves: enclaves}) + if err != nil { + return nil, errors.Wrap(err, "failed to wrap enclave list config") + } + + return []keystone_changeset.DONCapabilityWithConfig{ + { + Capability: kcr.CapabilitiesRegistryCapability{ + LabelledName: name, + Version: version, + CapabilityType: 1, // ACTION + }, + Config: &capabilitiespb.CapabilityConfig{ + DefaultConfig: values.Proto(wrappedConfig).GetMapValue(), + LocalOnly: true, + }, + }, + }, nil + } +} diff --git a/system-tests/lib/go.mod b/system-tests/lib/go.mod index b4deaa6d35b..2671173f518 100644 --- a/system-tests/lib/go.mod +++ b/system-tests/lib/go.mod @@ -39,6 +39,7 @@ require ( github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260624154507-ea7ff77a0ddb github.com/smartcontractkit/chainlink-common v0.11.2-0.20260811140401-3fb1738abb75 github.com/smartcontractkit/chainlink-common/keystore v1.3.0 + github.com/smartcontractkit/chainlink-confidential-compute v1.3.0 github.com/smartcontractkit/chainlink-deployments-framework v0.111.1-0.20260612191326-e31c0ae4cd54 github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260810110946-8174b6bb7fc9 github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260713161920-de075095648b @@ -60,6 +61,7 @@ require ( github.com/stretchr/testify v1.11.1 go.uber.org/ratelimit v0.3.1 go.uber.org/zap v1.28.0 + golang.org/x/crypto v0.54.0 golang.org/x/sync v0.22.0 google.golang.org/grpc v1.82.1 google.golang.org/protobuf v1.36.11 @@ -573,7 +575,6 @@ require ( go.yaml.in/yaml/v3 v3.0.4 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect golang.org/x/arch v0.22.0 // indirect - golang.org/x/crypto v0.54.0 // indirect golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a // indirect golang.org/x/mod v0.37.0 // indirect golang.org/x/net v0.57.0 // indirect diff --git a/system-tests/lib/go.sum b/system-tests/lib/go.sum index 0fa7203650a..32449127957 100644 --- a/system-tests/lib/go.sum +++ b/system-tests/lib/go.sum @@ -1557,6 +1557,12 @@ github.com/smartcontractkit/chainlink-common/keystore v1.3.0 h1:V05Rp9/dTc4Wyips github.com/smartcontractkit/chainlink-common/keystore v1.3.0/go.mod h1:vHV8BGm6TN7jBbMsWxq1Hqm3HbCtYFwzvKS0CCczxG8= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72 h1:uWEwl7i2ryuRVoV4DmIKm6mqYevf1lH/8cQYhw/JXko= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72/go.mod h1:UYcRMb4dZcoaIPgZJ3hckCySTqtJc9K4Q+tOKErwTq0= +github.com/smartcontractkit/chainlink-confidential-compute v0.0.0-20260810192019-a945f9c2f628 h1:Od5bP3EapDpgodb3Clio5d5tWktysyzWH7R0em4/O7U= +github.com/smartcontractkit/chainlink-confidential-compute v0.0.0-20260810192019-a945f9c2f628/go.mod h1:fq9n85XoREIUxgdXIX3RyDIRCRyxZKZ5NCBbFDfdySo= +github.com/smartcontractkit/chainlink-confidential-compute v0.0.0-20260810204028-19c1e24d25ee h1:IW/mdtdrP2YvNq8Ua4Y2NeEuUUeJf4B3R5P9sX0VEHE= +github.com/smartcontractkit/chainlink-confidential-compute v0.0.0-20260810204028-19c1e24d25ee/go.mod h1:fq9n85XoREIUxgdXIX3RyDIRCRyxZKZ5NCBbFDfdySo= +github.com/smartcontractkit/chainlink-confidential-compute v1.3.0 h1:y64hOzM9H61E4EYRzQDtRwqunDBlKpsZc1SvFEUlSLE= +github.com/smartcontractkit/chainlink-confidential-compute v1.3.0/go.mod h1:fq9n85XoREIUxgdXIX3RyDIRCRyxZKZ5NCBbFDfdySo= github.com/smartcontractkit/chainlink-data-streams v1.1.0 h1:O5ngSwpwey7kQ3t4YgFiXzu3L3EvYiPS4c93gMWNRbk= github.com/smartcontractkit/chainlink-data-streams v1.1.0/go.mod h1:dF5JiHWueHjYguUUUrFeb03MkcDqha/tssEkqTkgzp4= github.com/smartcontractkit/chainlink-deployments-framework v0.111.1-0.20260612191326-e31c0ae4cd54 h1:mzbvXxdbE/96Pdj1zyPKzf25ZlDR48+iTTDTbaITvmk= diff --git a/system-tests/tests/go.mod b/system-tests/tests/go.mod index a1e331c725c..76acf6c525f 100644 --- a/system-tests/tests/go.mod +++ b/system-tests/tests/go.mod @@ -13,6 +13,8 @@ replace github.com/smartcontractkit/chainlink/core/scripts/cre/environment/examp replace github.com/smartcontractkit/chainlink/core/scripts/cre/environment/examples/workflows/cron => ../../core/scripts/cre/environment/examples/workflows/cron +replace github.com/smartcontractkit/chainlink/core/scripts => ../../core/scripts + replace github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/evm/evmread => ./smoke/cre/evm/evmread replace github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/evm/logtrigger => ./smoke/cre/evm/logtrigger @@ -66,6 +68,8 @@ require ( github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260624154507-ea7ff77a0ddb github.com/smartcontractkit/chainlink-common v0.11.2-0.20260811140401-3fb1738abb75 github.com/smartcontractkit/chainlink-common/keystore v1.3.0 + github.com/smartcontractkit/chainlink-confidential-compute v1.3.0 + github.com/smartcontractkit/chainlink-confidential-compute/tests/testhelpers v0.0.0-20260811154131-b9926f013215 github.com/smartcontractkit/chainlink-deployments-framework v0.111.1-0.20260612191326-e31c0ae4cd54 github.com/smartcontractkit/chainlink-evm/contracts/cre/gobindings v0.0.0-20260403151002-2c91155b5501 github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260713161920-de075095648b @@ -76,6 +80,7 @@ require ( github.com/smartcontractkit/chainlink-testing-framework/framework/components/chiprouter v1.0.4 github.com/smartcontractkit/chainlink-testing-framework/framework/components/fake v0.15.0 github.com/smartcontractkit/chainlink-testing-framework/seth v1.51.5 + github.com/smartcontractkit/chainlink/core/scripts v0.0.0-20260812035138-673d7e955a68 github.com/smartcontractkit/chainlink/core/scripts/cre/environment/examples/workflows/cron v0.0.0-20251008094352-f74459c46e8c github.com/smartcontractkit/chainlink/core/scripts/cre/environment/examples/workflows/proof-of-reserve/cron-based v0.0.0-00010101000000-000000000000 github.com/smartcontractkit/chainlink/deployment v0.0.0-20260126202327-6be9a05f0caf @@ -94,7 +99,7 @@ require ( github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/solana/sollogtrigger v0.0.0-00010101000000-000000000000 github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/solana/solread v0.0.0-20260609191154-1ecc282df958 github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/solana/solwrite v0.0.0-00010101000000-000000000000 - github.com/smartcontractkit/chainlink/v2 v2.29.0 + github.com/smartcontractkit/chainlink/v2 v2.32.0 github.com/smartcontractkit/cld-changesets v0.5.0 github.com/stellar/go-stellar-sdk v0.6.0 github.com/stretchr/testify v1.11.1 @@ -180,6 +185,7 @@ require ( github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/memberlist v0.5.4 // indirect github.com/hashicorp/serf v0.10.2 // indirect + github.com/hf/nsm v0.0.0-20220930140112-cd181bd646b9 // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/in-toto/attestation v1.2.0 // indirect github.com/influxdata/tdigest v0.0.2-0.20210216194612-fc98d27c9e8b // indirect @@ -326,7 +332,7 @@ require ( github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect github.com/alitto/pond/v2 v2.5.0 // indirect - github.com/andybalholm/brotli v1.2.1 // indirect + github.com/andybalholm/brotli v1.2.1 github.com/apache/arrow-go/v18 v18.6.0 // indirect github.com/aptos-labs/aptos-go-sdk v1.13.0 github.com/atombender/go-jsonschema v0.16.1-0.20240916205339-a74cd4e2851c // indirect @@ -646,7 +652,7 @@ require ( github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4 // indirect github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20260512230622-65f10f4cd305 // indirect github.com/smartcontractkit/chainlink-protos/orchestrator v0.11.0 // indirect - github.com/smartcontractkit/chainlink-protos/storage-service v0.3.0 // indirect + github.com/smartcontractkit/chainlink-protos/storage-service v0.3.0 github.com/smartcontractkit/chainlink-protos/svr v1.3.0 // indirect github.com/smartcontractkit/chainlink-stellar v0.0.3 // indirect github.com/smartcontractkit/chainlink-stellar/bindings v0.0.0-20260727172856-734bee1b2489 // indirect diff --git a/system-tests/tests/go.sum b/system-tests/tests/go.sum index 097f2e7a521..752bbff8509 100644 --- a/system-tests/tests/go.sum +++ b/system-tests/tests/go.sum @@ -653,6 +653,7 @@ github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx5 github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw= github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= +github.com/fxamacker/cbor/v2 v2.2.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78= github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= @@ -1085,6 +1086,8 @@ github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3s github.com/heetch/avro v0.3.1/go.mod h1:4xn38Oz/+hiEUTpbVfGVLfvOg0yKLlRP7Q9+gJJILgA= github.com/hetznercloud/hcloud-go/v2 v2.36.0 h1:HlLL/aaVXUulqe+rsjoJmrxKhPi1MflL5O9iq5QEtvo= github.com/hetznercloud/hcloud-go/v2 v2.36.0/go.mod h1:MnN/QJEa/RYNQiiVoJjNHPntM7Z1wlYPgJ2HA40/cDE= +github.com/hf/nsm v0.0.0-20220930140112-cd181bd646b9 h1:pU32bJGmZwF4WXb9Yaz0T8vHDtIPVxqDOdmYdwTQPqw= +github.com/hf/nsm v0.0.0-20220930140112-cd181bd646b9/go.mod h1:MJsac5D0fKcNWfriUERtln6segcGfD6Nu0V5uGBbPf8= github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db h1:IZUYC/xb3giYwBLMnr8d0TGTzPKFGNTCGgGLoyeX330= github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db/go.mod h1:xTEYN9KCHxuYHs+NmrmzFcnvHMzLLNiGFafCb1n3Mfg= github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= @@ -1762,6 +1765,16 @@ github.com/smartcontractkit/chainlink-common/keystore v1.3.0 h1:V05Rp9/dTc4Wyips github.com/smartcontractkit/chainlink-common/keystore v1.3.0/go.mod h1:vHV8BGm6TN7jBbMsWxq1Hqm3HbCtYFwzvKS0CCczxG8= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72 h1:uWEwl7i2ryuRVoV4DmIKm6mqYevf1lH/8cQYhw/JXko= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72/go.mod h1:UYcRMb4dZcoaIPgZJ3hckCySTqtJc9K4Q+tOKErwTq0= +github.com/smartcontractkit/chainlink-confidential-compute v0.0.0-20260810193839-ed12934f0671 h1:wq72FVGDxTMUXIuwYGF/8wTFPMUvs4f1vv8MMfo4G8k= +github.com/smartcontractkit/chainlink-confidential-compute v0.0.0-20260810193839-ed12934f0671/go.mod h1:fq9n85XoREIUxgdXIX3RyDIRCRyxZKZ5NCBbFDfdySo= +github.com/smartcontractkit/chainlink-confidential-compute v0.0.0-20260810204028-19c1e24d25ee h1:IW/mdtdrP2YvNq8Ua4Y2NeEuUUeJf4B3R5P9sX0VEHE= +github.com/smartcontractkit/chainlink-confidential-compute v0.0.0-20260810204028-19c1e24d25ee/go.mod h1:fq9n85XoREIUxgdXIX3RyDIRCRyxZKZ5NCBbFDfdySo= +github.com/smartcontractkit/chainlink-confidential-compute v1.3.0 h1:y64hOzM9H61E4EYRzQDtRwqunDBlKpsZc1SvFEUlSLE= +github.com/smartcontractkit/chainlink-confidential-compute v1.3.0/go.mod h1:fq9n85XoREIUxgdXIX3RyDIRCRyxZKZ5NCBbFDfdySo= +github.com/smartcontractkit/chainlink-confidential-compute/tests/testhelpers v0.0.0-20260810204028-19c1e24d25ee h1:Nub4a0ErgJkRnGO0wFxCA/xKuYnZiuKb4sFI4H7Ppsk= +github.com/smartcontractkit/chainlink-confidential-compute/tests/testhelpers v0.0.0-20260810204028-19c1e24d25ee/go.mod h1:Q5q/ohoAF5N7GXZS6QxgPWSaSLFtS6/53QHJbBUkCmU= +github.com/smartcontractkit/chainlink-confidential-compute/tests/testhelpers v0.0.0-20260811154131-b9926f013215 h1:YsxEx0pGUECqZZoed0fY+JFNrB8LsPRAKsFrPK0pQDw= +github.com/smartcontractkit/chainlink-confidential-compute/tests/testhelpers v0.0.0-20260811154131-b9926f013215/go.mod h1:Q5q/ohoAF5N7GXZS6QxgPWSaSLFtS6/53QHJbBUkCmU= github.com/smartcontractkit/chainlink-data-streams v1.1.0 h1:O5ngSwpwey7kQ3t4YgFiXzu3L3EvYiPS4c93gMWNRbk= github.com/smartcontractkit/chainlink-data-streams v1.1.0/go.mod h1:dF5JiHWueHjYguUUUrFeb03MkcDqha/tssEkqTkgzp4= github.com/smartcontractkit/chainlink-deployments-framework v0.111.1-0.20260612191326-e31c0ae4cd54 h1:mzbvXxdbE/96Pdj1zyPKzf25ZlDR48+iTTDTbaITvmk= @@ -2630,6 +2643,7 @@ golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105210202-9ed45478a130/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= diff --git a/system-tests/tests/smoke/cre/confidential_workflows_env.go b/system-tests/tests/smoke/cre/confidential_workflows_env.go new file mode 100644 index 00000000000..ff815bb09aa --- /dev/null +++ b/system-tests/tests/smoke/cre/confidential_workflows_env.go @@ -0,0 +1,216 @@ +package cre + +import ( + "context" + "fmt" + "os" + "path/filepath" + "slices" + "strings" + "time" + + "github.com/pkg/errors" + + "github.com/smartcontractkit/chainlink-testing-framework/framework" + "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" + + crescriptenv "github.com/smartcontractkit/chainlink/core/scripts/cre/environment/environment" + crelib "github.com/smartcontractkit/chainlink/system-tests/lib/cre" + gateway "github.com/smartcontractkit/chainlink/system-tests/lib/cre/don/gateway" + creenv "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment" + envconfig "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment/config" + feature_sets "github.com/smartcontractkit/chainlink/system-tests/lib/cre/features/sets" + "github.com/smartcontractkit/chainlink/system-tests/lib/cre/flags" +) + +// This file provides an in-process CRE environment start for tests that must +// supply capabilities and features computed at runtime. +// +// The standard helper (t_helpers.SetupTestEnvironmentWithConfig) starts the +// environment by shelling out to `cre env start`, so a test cannot hand it Go +// values. The confidential workflows test needs to, twice over: the enclave list +// (only known once the enclaves are running) goes into the capability's on-chain +// registry config, and the trusted measurements go into the relay's capability +// config. +// +// startConfidentialCreEnvironment therefore calls the exported +// crescriptenv.StartCLIEnvironment directly and writes the local CRE state file. +// Callers then invoke the standard helper, which finds that state file and skips +// starting the environment, building the TestEnvironment from saved state. + +const ( + // confidentialCleanupWait matches the CLI's own cleanup grace period. + confidentialCleanupWait = 15 * time.Second + + // These are resolved against the CRE environment directory rather than used + // as-is: the CLI defaults assume a working directory of + // core/scripts/cre/environment, but this test runs from smoke/cre. + confidentialCapabilityDefaultsConfig = "configs/capability_defaults.toml" + confidentialSetupConfig = "configs/setup.toml" +) + +// startConfidentialCreEnvironment starts a local CRE environment with extra +// capabilities and features, then persists the state file so the standard test +// environment helper can build from it. +func startConfidentialCreEnvironment( + ctx context.Context, + relativePathToRepoRoot string, + environmentDirPath string, + extraCapabilities []crelib.InstallableCapability, + extraAllowedPorts []int, + extraFeatures ...crelib.Feature, +) error { + in, err := confidentialPreConfigure(ctx, relativePathToRepoRoot, environmentDirPath) + if err != nil { + return err + } + + // Extra capabilities are registered dynamically rather than declared in the + // topology config. Job spec delivery and on-chain registration both check + // don.HasFlag(name), which reads NodeSet.Capabilities, so the flags have to be + // present there too. + for _, c := range extraCapabilities { + flag := c.Flag() + for i, ns := range in.NodeSets { + if slices.Contains(ns.DONTypes, "workflow") && !slices.Contains(ns.Capabilities, flag) { + in.NodeSets[i].Capabilities = append(in.NodeSets[i].Capabilities, flag) + } + } + } + + // Config.Validate rejects capability flags the built-in provider doesn't know, + // and confidential-workflows / confidential-relay are not among them. Extend + // the provider with every flag this run declares. + extraFlags := []string{string(crelib.ConfidentialRelayCapability), confidentialWorkflowsApp} + for _, c := range extraCapabilities { + extraFlags = append(extraFlags, c.Flag()) + } + for _, f := range extraFeatures { + extraFlags = append(extraFlags, string(f.Flag())) + } + + envDependencies := crelib.NewEnvironmentDependencies( + flags.NewExtensibleCapabilityFlagsProvider(extraFlags), + crelib.NewContractVersionsProvider(envconfig.DefaultContractSet()), + ) + if err := in.Validate(envDependencies); err != nil { + return errors.Wrap(err, "failed to validate environment configuration") + } + + // Start from the default feature set and add the test's own features, so the + // relay feature does not have to be registered globally in features/sets. + features := feature_sets.New() + for _, f := range extraFeatures { + features.Add(f) + } + + allowedPorts := append([]int{in.Fake.Port, in.FakeHTTP.Port}, extraAllowedPorts...) + gatewayWhitelistConfig := gateway.WhitelistConfig{ + ExtraAllowedPorts: allowedPorts, + // The enclaves reach the gateway from outside the Docker network. + ExtraAllowedIPsCIDR: []string{"0.0.0.0/0"}, + } + + output, startErr := crescriptenv.StartCLIEnvironment( + ctx, + relativePathToRepoRoot, + in, + extraCapabilities, + features, + nil, // no extra job spec functions + envDependencies, + gatewayWhitelistConfig, + ) + if startErr != nil { + if stopErr := stopConfidentialCreEnvironment(relativePathToRepoRoot); stopErr != nil { + return errors.Wrapf(startErr, "failed to start environment, and cleanup also failed: %s", stopErr) + } + return errors.Wrap(startErr, "failed to start environment") + } + + addresses, aErr := output.CreEnvironment.CldfEnvironment.DataStore.Addresses().Fetch() + if aErr != nil { + return errors.Wrap(aErr, "failed to fetch addresses from datastore") + } + if err := in.SetAddresses(addresses); err != nil { + return errors.Wrap(err, "failed to set addresses on config") + } + if storeErr := in.Store(envconfig.MustLocalCREStateFileAbsPath(relativePathToRepoRoot)); storeErr != nil { + return errors.Wrap(storeErr, "failed to store local CRE state") + } + + return nil +} + +// confidentialPreConfigure clears any prior environment state and loads the +// topology config. Purging before RunSetup matters: a stale state file from a +// different topology would otherwise be merged into this run. +func confidentialPreConfigure(ctx context.Context, relativePathToRepoRoot, environmentDirPath string) (*envconfig.Config, error) { + _ = stopConfidentialCreEnvironment(relativePathToRepoRoot) + + if err := framework.RemoveTestContainers(); err != nil { + return nil, errors.Wrap(err, "failed to remove test containers") + } + defer func() { + crescriptenv.StartCmdRecoverHandlerFunc(nil, nil, true, confidentialCleanupWait) + }() + + if cleanUpErr := envconfig.RemoveAllEnvironmentStateDir(relativePathToRepoRoot); cleanUpErr != nil { + return nil, errors.Wrap(cleanUpErr, "failed to clean up environment state files") + } + + // Re-prepend the capability defaults to whatever CTF_CONFIGS the caller set. + // Stripping the prefix first keeps this idempotent across repeated calls. + defaultsConfig := filepath.Join(environmentDirPath, confidentialCapabilityDefaultsConfig) + userConfigs := strings.TrimPrefix(os.Getenv("CTF_CONFIGS"), defaultsConfig+",") + ctfConfigs := defaultsConfig + if userConfigs != "" && userConfigs != defaultsConfig { + ctfConfigs = defaultsConfig + "," + userConfigs + } + if err := os.Setenv("CTF_CONFIGS", ctfConfigs); err != nil { + return nil, fmt.Errorf("failed to set CTF_CONFIGS: %w", err) + } + + if setupErr := crescriptenv.RunSetup( + ctx, + crescriptenv.SetupConfig{ConfigPath: filepath.Join(environmentDirPath, confidentialSetupConfig)}, + true, // noPrompt + false, // purge + false, // withBilling + relativePathToRepoRoot, + ); setupErr != nil { + return nil, errors.Wrap(setupErr, "failed to run setup") + } + + if pkErr := creenv.SetDefaultPrivateKeyIfEmpty(blockchain.DefaultAnvilPrivateKey); pkErr != nil { + return nil, errors.Wrap(pkErr, "failed to set default private key") + } + + // Keep Ryuk from reaping the containers when this process exits; the test + // tears them down itself. + if setErr := os.Setenv("TESTCONTAINERS_RYUK_DISABLED", "true"); setErr != nil { + return nil, fmt.Errorf("failed to set TESTCONTAINERS_RYUK_DISABLED: %w", setErr) + } + + in := &envconfig.Config{} + if err := in.Load(os.Getenv("CTF_CONFIGS")); err != nil { + return nil, errors.Wrap(err, "failed to load environment configuration") + } + + return in, nil +} + +// stopConfidentialCreEnvironment removes the environment containers and the local +// CRE state file. +func stopConfidentialCreEnvironment(relativePathToRepoRoot string) error { + if removeErr := framework.RemoveTestContainers(); removeErr != nil { + return errors.Wrap(removeErr, "failed to remove environment containers") + } + + creStateFile := envconfig.MustLocalCREStateFileAbsPath(relativePathToRepoRoot) + if cErr := os.Remove(creStateFile); cErr != nil && !os.IsNotExist(cErr) { + framework.L.Warn().Msgf("failed to remove local CRE state file: %s", cErr) + } + + return nil +} diff --git a/system-tests/tests/smoke/cre/confidential_workflows_test.go b/system-tests/tests/smoke/cre/confidential_workflows_test.go new file mode 100644 index 00000000000..88207593593 --- /dev/null +++ b/system-tests/tests/smoke/cre/confidential_workflows_test.go @@ -0,0 +1,409 @@ +package cre + +import ( + "bytes" + "context" + "fmt" + "net/url" + "os" + "os/exec" + "path/filepath" + "strconv" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/rs/zerolog" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-testing-framework/framework" + ns "github.com/smartcontractkit/chainlink-testing-framework/framework/components/simple_node_set" + + "github.com/smartcontractkit/chainlink-confidential-compute/tests/testhelpers" + keystone_changeset "github.com/smartcontractkit/chainlink/deployment/keystone/changeset" + crelib "github.com/smartcontractkit/chainlink/system-tests/lib/cre" + "github.com/smartcontractkit/chainlink/system-tests/lib/cre/capabilities/confidentialcompute" + crecontracts "github.com/smartcontractkit/chainlink/system-tests/lib/cre/contracts" + "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment/blockchains/evm" + creworkflow "github.com/smartcontractkit/chainlink/system-tests/lib/cre/workflow" + t_helpers "github.com/smartcontractkit/chainlink/system-tests/tests/test-helpers" + ttypes "github.com/smartcontractkit/chainlink/system-tests/tests/test-helpers/configuration" +) + +const ( + // confidentialWorkflowsConfigPath is the topology this test runs against, + // relative to the CRE environment directory. + confidentialWorkflowsConfigPath = "/configs/workflow-gateway-capabilities-don-confidential-workflows.toml" + + // confidentialWorkflowName is the on-chain workflow name. + confidentialWorkflowName = "confidential-workflows-e2e" + + // confidentialEchoURL is the outbound target the workflow fetches from inside + // the enclave. The enclave's default egress policy allows public HTTPS. + confidentialEchoURL = "https://postman-echo.com/post" + + // confidentialSecretName is the vault secret the workflow reads via GetSecret. + confidentialSecretName = "MOCK_SECRET" + + // confidentialVaultThreshold matches the 4-node F=1 vault DON. + confidentialVaultThreshold = 1 + + // confidentialEnclaveRegion is recorded on each enclave descriptor. Descriptor + // hashes cover it, so it must match what the enclave itself reports. + confidentialEnclaveRegion = "us-west-2" +) + +// Test_CRE_V2_ConfidentialWorkflows_Relay exercises the confidential workflows +// engine path end to end: +// +// syncer -> ConfidentialModule -> confidential-workflows capability -> enclave +// -> WASM (cron trigger) -> GetSecret (remote dispatch to the vault DON via the +// confidential relay) + http.SendRequest (intercepted and executed in-enclave) +// +// The enclaves run locally. On a Nitro-capable host they are real Nitro +// enclaves; elsewhere (including the CRE CI runners) the harness falls back to +// fake enclaves, which run the same binaries as local processes over emulated +// vsock. Attestation validation is relaxed only in the fake case. +// +// The chain-write leg of the workflow (ReportFromDon + evm.WriteReport) is left +// disabled here: it needs a deployed report receiver, and the secret and HTTP +// legs are what prove the relay and enclave routing work. +func Test_CRE_V2_ConfidentialWorkflows_Relay(t *testing.T) { + testLogger := framework.L + + t.Run("Confidential Workflows Relay - "+topology, func(t *testing.T) { + fake := testhelpers.UseFakeEnclave() + testLogger.Info().Bool("fakeEnclaves", fake).Msg("Starting confidential workflows relay test") + + tconf := t_helpers.GetTestConfig(t, confidentialWorkflowsConfigPath) + t.Setenv("CTF_CONFIGS", tconf.EnvironmentConfigPath) + + // 1. Stand up the host-side services the enclaves need to reach. Their + // addresses have to be known before the enclaves start, because they are + // baked into the settings the enclave receives at startup. The gateway + // URL is not known until the CRE environment is up, hence the proxy. + gwProxy := newDeferredGatewayProxy(t, confidentialGatewayProxyPort) + enclaveHost := confidentialEnclaveHostAddr(fake) + storageAddr, storageSvc := startFakeStorageService(t, enclaveHost) + + t.Setenv("REQUIRE_BFT_QUORUM", "true") + t.Setenv("ENCLAVE_SETTINGS", fmt.Sprintf( + `{"storageKey":%q,"storageServiceUrl":%q,"storageServiceTls":false,"gatewayUrl":%q}`, + confidentialStorageKeyHex, + storageAddr, + fmt.Sprintf("http://%s:%d", enclaveHost, confidentialGatewayProxyPort), + )) + + // 2. Start the enclaves. This is the whole point of depending on + // chainlink-confidential-compute's harness from this repository. + enclaveCfg := testhelpers.DefaultLocalEnclaveSetupConfig(confidentialComputeRoot(t), confidentialWorkflowsApp) + enclaveCfg.Region = confidentialEnclaveRegion + enclaves := testhelpers.SetupLocalEnclaves(t, enclaveCfg) + t.Cleanup(enclaves.CleanupAll) + testLogger.Info(). + Str("hostIP", enclaves.HostIP). + Int("count", len(enclaves.Enclaves)). + Msg("Local enclaves ready") + + // 3. The capability carries the enclave list into the on-chain registry + // config; the relay handler reads it from there to decide where to route. + confidentialcompute.ResetDeliveryState() + cap, err := confidentialcompute.New( + confidentialWorkflowsApp, + confidentialWorkflowsCapVersion, + confidentialWorkflowsApp, + enclaves.Enclaves, + ) + require.NoError(t, err, "failed to build confidential-workflows capability") + + relayFeature := newTestConfidentialRelayFeature(t, enclaves.Enclaves, fake) + + // 4. Start the CRE environment in-process so the capability and feature + // above can be injected, then let the standard helper build the test + // environment from the state file it wrote. + require.NoError(t, startConfidentialCreEnvironment( + t.Context(), + tconf.RelativePathToRepoRoot, + tconf.EnvironmentDirPath, + []crelib.InstallableCapability{cap}, + confidentialEnclavePorts(t, enclaves), + relayFeature, + ), "failed to start confidential CRE environment") + + testEnv := t_helpers.SetupTestEnvironmentWithConfig(t, tconf) + + // 5. Point the proxy at the real gateway now that it exists. + gatewayURL := confidentialGatewayURL(t, testEnv) + require.NoError(t, gwProxy.SetTarget(gatewayURL), "failed to set gateway proxy target") + testLogger.Info().Str("gatewayURL", gatewayURL).Msg("Gateway proxy target set") + + // 6. The engine's pre-enclave secret fetch reads VaultPublicKey and + // Threshold from the vault capability's registry config, which is + // registered empty. Without this, GetSecret fails inside the workflow. + vaultPublicKey := injectVaultPublicKey(t, testEnv, testLogger, gatewayURL) + + // 6b. The enclaves boot with no signer set and no master public key, so they + // reject every compute request until this lands. + configureEnclaves(t, testEnv, testLogger, enclaves.ConfigURLs, vaultPublicKey) + + // 6c. Store the secret the workflow reads via GetSecret. Without it the + // request really reaches the vault DON and really comes back empty. + storeConfidentialWorkflowSecret(t, testEnv, testLogger, gatewayURL, vaultPublicKey, + confidentialSecretName, "s3cret-from-vault") + + // 7. Compile and serve the workflow. ConsumerAddress is left empty, which + // the workflow treats as "skip the chain-write leg". + configJSON := fmt.Sprintf(`{"echo_url":%q}`, confidentialEchoURL) + artifacts := buildAndServeConfidentialWorkflow(t, configJSON, testhelpers.DetectHostIP()) + testLogger.Info(). + Str("binaryURL", artifacts.BinaryURL). + Str("configURL", artifacts.ConfigURL). + Msg("Workflow artifacts served") + + // The artifact server binds 0.0.0.0, but the enclave reaches it at a + // different host than Docker does, so swap only the host portion. + parsed, pErr := url.Parse(artifacts.BinaryURL) + require.NoError(t, pErr, "parsing workflow binary URL") + storageSvc.setURL(fmt.Sprintf("http://%s:%s%s", enclaveHost, parsed.Port(), parsed.Path)) + + // 8. The syncer reads the binary and config from disk (see the topology's + // CRE.WorkflowFetcher override), so copy them into the containers. + copyWorkflowArtifactsToContainers(t, testEnv, artifacts) + + // 9. Register the workflow as confidential and wait for a successful + // execution. The workflow returns an error if either GetSecret or the + // in-enclave HTTP fetch fails, so a successful execution implies the + // whole relay + enclave path worked. + workflowID := registerConfidentialWorkflow(t, testEnv, testLogger, artifacts) + waitForConfidentialWorkflowExecution(t, testEnv, testLogger, workflowID, 5*time.Minute) + + testLogger.Info().Msg("Confidential workflows relay E2E passed") + }) +} + +// confidentialEnclaveHostAddr is the address the enclave reaches host-local test +// servers at: loopback for fake enclaves (ordinary local processes), the wg0 host +// IP for real Nitro enclaves. +func confidentialEnclaveHostAddr(fake bool) string { + if fake { + return "localhost" + } + return "100.64.0.3" +} + +// confidentialComputeRoot resolves the chainlink-confidential-compute checkout the +// enclave harness builds and runs the enclave from. +func confidentialComputeRoot(t *testing.T) string { + t.Helper() + + root := os.Getenv("CONFIDENTIAL_COMPUTE_ROOT") + require.NotEmpty(t, root, + "CONFIDENTIAL_COMPUTE_ROOT must point at a chainlink-confidential-compute checkout; "+ + "CI sets this from the confidential-workflows gitRef in plugins/plugins.public.yaml") + + abs, err := filepath.Abs(root) + require.NoError(t, err, "resolving CONFIDENTIAL_COMPUTE_ROOT") + return abs +} + +// confidentialGatewayURL builds the externally reachable gateway URL. +func confidentialGatewayURL(t *testing.T, testEnv *ttypes.TestEnvironment) string { + t.Helper() + + require.NotEmpty(t, testEnv.Dons.GatewayConnectors.Configurations, "no gateway connector configurations") + incoming := testEnv.Dons.GatewayConnectors.Configurations[0].Incoming + host := incoming.Host + if host == "" { + host = testhelpers.DetectHostIP() + } + return fmt.Sprintf("%s://%s:%d%s", incoming.Protocol, host, incoming.ExternalPort, incoming.Path) +} + +// injectVaultPublicKey writes the vault DON's DKG public key and threshold into +// the vault capability's registry config. +func injectVaultPublicKey(t *testing.T, testEnv *ttypes.TestEnvironment, testLogger zerolog.Logger, gatewayURL string) string { + t.Helper() + + ctx := t.Context() + vaultPublicKey, err := creworkflow.FetchVaultPublicKey(ctx, gatewayURL) + require.NoError(t, err, "failed to fetch vault public key from gateway") + + require.IsType(t, &evm.Blockchain{}, testEnv.CreEnvironment.Blockchains[0], "expected EVM blockchain") + sethClient := testEnv.CreEnvironment.Blockchains[0].(*evm.Blockchain).SethClient + + capRegAddr := crecontracts.MustGetAddressFromDataStore( + testEnv.CreEnvironment.CldfEnvironment.DataStore, + testEnv.CreEnvironment.Blockchains[0].ChainSelector(), //nolint:staticcheck // mirrors system-tests usage + keystone_changeset.CapabilitiesRegistry.String(), + testEnv.CreEnvironment.ContractVersions[keystone_changeset.CapabilitiesRegistry.String()], + "", + ) + + vaultDON, _, err := crelib.GetVaultCapabilityDON(ctx, sethClient, capRegAddr) + require.NoError(t, err, "failed to locate vault capability DON in registry") + + require.NoError(t, + creworkflow.UpdateVaultCapabilityConfig(ctx, sethClient, capRegAddr, vaultDON, vaultPublicKey, confidentialVaultThreshold), + "failed to inject VaultPublicKey/Threshold into the vault capability config") + testLogger.Info().Msg("Injected VaultPublicKey + Threshold into the vault capability config") + + return vaultPublicKey +} + +// copyWorkflowArtifactsToContainers copies the compiled binary and its config into +// every workflow DON container so the syncer's file fetcher can read them. +func copyWorkflowArtifactsToContainers(t *testing.T, testEnv *ttypes.TestEnvironment, artifacts confidentialWorkflowArtifacts) { + t.Helper() + + for _, don := range testEnv.Dons.List() { + if !don.HasFlag(crelib.WorkflowDON) { + continue + } + for _, filename := range []string{confidentialWorkflowBinaryFilename, confidentialWorkflowConfigFilename} { + require.NoError(t, + creworkflow.CopyArtifactsToDockerContainers( + creworkflow.DefaultWorkflowTargetDir, + ns.NodeNamePrefix(don.Name), + filepath.Join(artifacts.ArtifactDir, filename), + ), + "failed to copy %s to the %s DON containers", filename, don.Name) + } + } +} + +// registerConfidentialWorkflow registers the workflow on-chain with confidential +// attributes and returns its workflow ID. +func registerConfidentialWorkflow( + t *testing.T, + testEnv *ttypes.TestEnvironment, + testLogger zerolog.Logger, + artifacts confidentialWorkflowArtifacts, +) string { + t.Helper() + + require.IsType(t, &evm.Blockchain{}, testEnv.CreEnvironment.Blockchains[0], "expected EVM blockchain") + sethClient := testEnv.CreEnvironment.Blockchains[0].(*evm.Blockchain).SethClient + + wfRegistryRef := crecontracts.MustGetAddressRefFromDataStore( + testEnv.CreEnvironment.CldfEnvironment.DataStore, + testEnv.CreEnvironment.Blockchains[0].ChainSelector(), //nolint:staticcheck // mirrors system-tests usage + keystone_changeset.WorkflowRegistry.String(), + testEnv.CreEnvironment.ContractVersions[keystone_changeset.WorkflowRegistry.String()], + "", + ) + + // The confidential attribute is what routes execution into the enclave rather + // than running the WASM on the workflow DON. + attributes := []byte(`{"confidential":true}`) + configURL := artifacts.ConfigURL + + workflowID, err := creworkflow.RegisterWithContract( + context.Background(), + sethClient, + common.HexToAddress(wfRegistryRef.Address), + wfRegistryRef.Version, + 0, // donID unused for v2 + testEnv.Dons.MustWorkflowDON().DonFamily, + confidentialWorkflowName, + workflowTag, + artifacts.BinaryURL, + &configURL, + nil, // no secrets URL + attributes, + nil, // keep the HTTP URL on-chain; the enclave fetches the binary itself + ) + require.NoError(t, err, "failed to register confidential workflow") + testLogger.Info().Str("workflowID", workflowID).Msg("Confidential workflow registered") + + t.Cleanup(func() { + _ = creworkflow.DeleteWithContract( + context.Background(), + sethClient, + common.HexToAddress(wfRegistryRef.Address), + wfRegistryRef.Version, + confidentialWorkflowName, + ) + }) + + return workflowID +} + +// waitForConfidentialWorkflowExecution waits for the engine to log a successful +// execution for this workflow. The engine emits that line once per successful +// trigger execution, not for the Subscribe-phase call at engine startup, so +// finding it means the cron trigger fired and the whole enclave path succeeded. +func waitForConfidentialWorkflowExecution( + t *testing.T, + testEnv *ttypes.TestEnvironment, + testLogger zerolog.Logger, + workflowID string, + timeout time.Duration, +) { + t.Helper() + + containers := confidentialWorkflowDONContainers(testEnv) + require.NotEmpty(t, containers, "no workflow DON containers found to scrape") + + needleMsg := []byte(`"msg":"Workflow execution finished successfully"`) + needleID := []byte(workflowID) + testLogger.Info(). + Str("workflowID", workflowID). + Strs("containers", containers). + Msg("Waiting for a successful workflow execution") + + deadline := time.Now().Add(timeout) + for { + for _, name := range containers { + out, _ := exec.Command("docker", "logs", "--tail", "10000", name).CombinedOutput() + for _, line := range bytes.Split(out, []byte{'\n'}) { + if bytes.Contains(line, needleMsg) && bytes.Contains(line, needleID) { + testLogger.Info().Str("container", name).Msg("Found successful execution log") + return + } + } + } + if time.Now().After(deadline) { + t.Fatalf("timed out after %s waiting for a successful execution of workflow %s", timeout, workflowID) + } + time.Sleep(5 * time.Second) + } +} + +// confidentialWorkflowDONContainers returns the chainlink container names for +// every nodeset whose DON carries the workflow DON flag. +func confidentialWorkflowDONContainers(testEnv *ttypes.TestEnvironment) []string { + workflowDONNames := map[string]bool{} + for _, don := range testEnv.Dons.List() { + if don.HasFlag(crelib.WorkflowDON) { + workflowDONNames[don.Name] = true + } + } + + var names []string + for _, nodeSet := range testEnv.Config.NodeSets { + if !workflowDONNames[nodeSet.Name] || nodeSet.Out == nil { + continue + } + for _, cl := range nodeSet.Out.CLNodes { + if cl == nil || cl.Node == nil || cl.Node.ContainerName == "" { + continue + } + names = append(names, cl.Node.ContainerName) + } + } + return names +} + +// confidentialEnclavePorts returns the enclave host-server ports as ints so they +// can be added to the gateway's outbound whitelist. +func confidentialEnclavePorts(t *testing.T, result *testhelpers.LocalEnclaveResult) []int { + t.Helper() + + ports := make([]int, 0, len(result.HTTPPorts)+len(result.ConfigHTTPPorts)) + for _, p := range append(append([]string{}, result.HTTPPorts...), result.ConfigHTTPPorts...) { + n, err := strconv.Atoi(p) + require.NoError(t, err, "enclave port %q is not numeric", p) + ports = append(ports, n) + } + return ports +} diff --git a/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go b/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go new file mode 100644 index 00000000000..5017c638e4e --- /dev/null +++ b/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go @@ -0,0 +1,467 @@ +package cre + +import ( + "bytes" + "context" + "crypto/sha256" + "crypto/tls" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "net" + "net/http" + "net/http/httputil" + "net/url" + "os" + "os/exec" + "path/filepath" + "strings" + "sync" + "testing" + + "github.com/andybalholm/brotli" + "github.com/rs/zerolog" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/chainlink-confidential-compute/tests/testhelpers" + cctypes "github.com/smartcontractkit/chainlink-confidential-compute/types" + "github.com/smartcontractkit/chainlink-confidential-compute/util" + workflow_registry_v2_wrapper "github.com/smartcontractkit/chainlink-evm/gethwrappers/workflow/generated/workflow_registry_wrapper_v2" + storage_service "github.com/smartcontractkit/chainlink-protos/storage-service/go" + keystone_changeset "github.com/smartcontractkit/chainlink/deployment/keystone/changeset" + crelib "github.com/smartcontractkit/chainlink/system-tests/lib/cre" + crecontracts "github.com/smartcontractkit/chainlink/system-tests/lib/cre/contracts" + "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment/blockchains/evm" + "github.com/smartcontractkit/chainlink/system-tests/lib/cre/features/confidentialrelay" + ttypes "github.com/smartcontractkit/chainlink/system-tests/tests/test-helpers/configuration" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/vault/vaultutils" +) + +const ( + // confidentialWorkflowsApp is the enclave application, the DON capability flag + // and the capability binary name; all three share this value. + confidentialWorkflowsApp = "confidential-workflows" + + // confidentialWorkflowsCapVersion is the version the capability registers under. + confidentialWorkflowsCapVersion = "1.0.0-alpha" + + // confidentialGatewayProxyPort is the fixed port the enclaves are told to reach + // the CRE gateway on. It must be known before the enclaves start, which is why + // the proxy in front of it resolves its target lazily. + confidentialGatewayProxyPort = 9999 + + // confidentialStorageKeyHex is a deterministic ed25519 seed the enclave uses to + // authenticate to the fake storage service. The fake does not verify the JWT. + confidentialStorageKeyHex = "0000000000000000000000000000000000000000000000000000000000000001" + + confidentialWorkflowBinaryFilename = "workflow-test-confidential.br.b64" + confidentialWorkflowConfigFilename = "workflow-test-config.json" + + // confidentialWorkflowSrcDir holds the WASM workflow the test compiles. + confidentialWorkflowSrcDir = "testdata/confidentialworkflow" +) + +// --------------------------------------------------------------------------- +// Deferred gateway proxy +// --------------------------------------------------------------------------- + +// deferredGatewayProxy is a reverse proxy on a fixed port that returns 502 until +// SetTarget is called with the real gateway URL. This resolves a chicken-and-egg +// problem: the enclaves are told their gateway URL at startup, but the real URL +// is only known once the CRE environment is up. +type deferredGatewayProxy struct { + mu sync.RWMutex + target *url.URL + server *http.Server +} + +func newDeferredGatewayProxy(t *testing.T, port int) *deferredGatewayProxy { + t.Helper() + + p := &deferredGatewayProxy{} + rp := &httputil.ReverseProxy{ + Director: func(req *http.Request) { + p.mu.RLock() + defer p.mu.RUnlock() + if p.target != nil { + req.URL.Scheme = p.target.Scheme + req.URL.Host = p.target.Host + req.Host = p.target.Host + } + }, + } + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + p.mu.RLock() + hasTarget := p.target != nil + p.mu.RUnlock() + if !hasTarget { + http.Error(w, "gateway not ready", http.StatusBadGateway) + return + } + rp.ServeHTTP(w, r) + }) + + listener, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", port)) + require.NoError(t, err, "failed to listen on port %d for gateway proxy", port) + + p.server = &http.Server{Handler: handler} + go func() { _ = p.server.Serve(listener) }() + t.Cleanup(func() { _ = p.server.Close() }) + + return p +} + +func (p *deferredGatewayProxy) SetTarget(rawURL string) error { + u, err := url.Parse(rawURL) + if err != nil { + return err + } + p.mu.Lock() + defer p.mu.Unlock() + p.target = u + return nil +} + +// --------------------------------------------------------------------------- +// Fake CRE storage service +// --------------------------------------------------------------------------- + +// fakeStorageService is a minimal in-process CRE storage NodeService. The enclave +// fetches the workflow binary itself: it calls DownloadArtifact over JWT-authed +// gRPC, gets a pre-signed URL, downloads it and verifies the hash. This fake +// returns the URL of the base64 WASM server the test stands up. +type fakeStorageService struct { + storage_service.UnimplementedNodeServiceServer + mu sync.Mutex + url string +} + +func (f *fakeStorageService) setURL(u string) { + f.mu.Lock() + f.url = u + f.mu.Unlock() +} + +func (f *fakeStorageService) DownloadArtifact(_ context.Context, req *storage_service.DownloadArtifactRequest) (*storage_service.DownloadArtifactResponse, error) { + f.mu.Lock() + u := f.url + f.mu.Unlock() + + // Mirror real storage-service semantics: the id must be a bare artifact id, + // not a full URL. Rejecting the URL shape here keeps a regression failing in + // this test rather than only in a live environment. + if strings.Contains(req.GetId(), "://") { + return nil, status.Errorf(codes.NotFound, "fake storage: artifact with id %q not found (expected a bare id, not a URL)", req.GetId()) + } + if u == "" { + return nil, fmt.Errorf("fake storage: artifact url not set yet") + } + return &storage_service.DownloadArtifactResponse{Url: u}, nil +} + +// startFakeStorageService starts a gRPC NodeService bound to 0.0.0.0 and returns +// the address the enclave dials it at, plus the service so the test can set the +// artifact URL once the WASM server is up. +func startFakeStorageService(t *testing.T, enclaveHost string) (string, *fakeStorageService) { + t.Helper() + + lis, err := net.Listen("tcp", "0.0.0.0:0") + require.NoError(t, err, "fake storage listener") + + svc := &fakeStorageService{} + grpcSrv := grpc.NewServer() + storage_service.RegisterNodeServiceServer(grpcSrv, svc) + go func() { _ = grpcSrv.Serve(lis) }() + t.Cleanup(grpcSrv.Stop) + + return fmt.Sprintf("%s:%d", enclaveHost, lis.Addr().(*net.TCPAddr).Port), svc +} + +// --------------------------------------------------------------------------- +// Confidential relay feature wrapper +// --------------------------------------------------------------------------- + +// testConfidentialRelayFeature wraps the real ConfidentialRelay feature and +// injects trusted measurements into the DON's capability config before +// PreEnvStartup runs, so the relay handler will accept attestations from the +// enclaves this test started. +type testConfidentialRelayFeature struct { + inner confidentialrelay.ConfidentialRelay + pcrsJSON string +} + +func (f *testConfidentialRelayFeature) Flag() crelib.CapabilityFlag { + return f.inner.Flag() +} + +func (f *testConfidentialRelayFeature) PreEnvStartup( + ctx context.Context, + testLogger zerolog.Logger, + don *crelib.DonMetadata, + topology *crelib.Topology, + creEnv *crelib.Environment, +) (*crelib.PreEnvStartupOutput, error) { + if don.CapabilityConfigs == nil { + don.CapabilityConfigs = make(map[crelib.CapabilityFlag]crelib.CapabilityConfig) + } + cfg, ok := don.CapabilityConfigs[crelib.ConfidentialRelayCapability] + if !ok { + cfg = crelib.CapabilityConfig{Values: make(map[string]any)} + } + if cfg.Values == nil { + cfg.Values = make(map[string]any) + } + cfg.Values["trustedPCRs"] = f.pcrsJSON + don.CapabilityConfigs[crelib.ConfidentialRelayCapability] = cfg + + return f.inner.PreEnvStartup(ctx, testLogger, don, topology, creEnv) +} + +func (f *testConfidentialRelayFeature) PostEnvStartup( + ctx context.Context, + testLogger zerolog.Logger, + don *crelib.Don, + dons *crelib.Dons, + creEnv *crelib.Environment, +) error { + return f.inner.PostEnvStartup(ctx, testLogger, don, dons, creEnv) +} + +// newTestConfidentialRelayFeature builds the relay feature for a set of enclaves. +// Fake enclaves emit a sentinel attestation document instead of real PCRs, so the +// trusted value is the fake measurements placeholder and attestation validation is +// relaxed. Real Nitro enclaves keep full validation against their measurements. +func newTestConfidentialRelayFeature(t *testing.T, enclaves []cctypes.Enclave, fake bool) crelib.Feature { + t.Helper() + + var pcrsJSON string + if fake { + // Marshaling the raw "fake-measurements" bytes as json.RawMessage would fail + // since it is not valid JSON, so encode it as a JSON string array. + b, err := json.Marshal([]string{cctypes.FakeMeasurements}) + require.NoError(t, err, "failed to marshal fake measurements") + pcrsJSON = string(b) + } else { + // Each enclave bakes in per-CID WireGuard keys, so measurements differ per + // enclave. The relay accepts a JSON array and tries each until one matches. + var allPCRs []json.RawMessage + for _, enc := range enclaves { + for _, tv := range enc.TrustedValues { + if string(tv) != "invalid" { + allPCRs = append(allPCRs, json.RawMessage(tv)) + } + } + } + b, err := json.Marshal(allPCRs) + require.NoError(t, err, "failed to marshal PCR measurements") + pcrsJSON = string(b) + } + + return crelib.Feature(&testConfidentialRelayFeature{ + inner: confidentialrelay.ConfidentialRelay{TrustEnclaves: fake, RequireBFTQuorum: true}, + pcrsJSON: pcrsJSON, + }) +} + +// --------------------------------------------------------------------------- +// Workflow artifacts +// --------------------------------------------------------------------------- + +// confidentialWorkflowArtifacts holds everything derived from compiling the test +// workflow: the URLs the syncer and enclave fetch it from, and the on-disk +// directory the test copies into the DON containers. +type confidentialWorkflowArtifacts struct { + BinaryURL string + ConfigURL string + ArtifactDir string + BinaryHash []byte +} + +// buildAndServeConfidentialWorkflow compiles the test workflow to wasip1/wasm, +// brotli-compresses and base64-encodes it (the format the syncer expects), writes +// both binary and config to a temp dir, and serves them over HTTP bound to +// 0.0.0.0 so the host, the Docker containers and the enclaves can all fetch them. +func buildAndServeConfidentialWorkflow(t *testing.T, configJSON string, hostIP string) confidentialWorkflowArtifacts { + t.Helper() + + srcDir, err := filepath.Abs(confidentialWorkflowSrcDir) + require.NoError(t, err, "resolving workflow source path") + + tmpDir := t.TempDir() + outFile := filepath.Join(tmpDir, "workflow-test.wasm") + + cmd := exec.Command("go", "build", "-o", outFile, ".") + cmd.Dir = srcDir + cmd.Env = append(os.Environ(), "GOOS=wasip1", "GOARCH=wasm", "CGO_ENABLED=0") + output, err := cmd.CombinedOutput() + require.NoError(t, err, "compiling confidential workflow WASM: %s", string(output)) + + raw, err := os.ReadFile(outFile) + require.NoError(t, err, "reading compiled WASM") + + var compressed bytes.Buffer + w := brotli.NewWriter(&compressed) + _, err = w.Write(raw) + require.NoError(t, err, "brotli compressing WASM") + require.NoError(t, w.Close(), "closing brotli writer") + + binary := compressed.Bytes() + hash := sha256.Sum256(binary) + encoded := base64.StdEncoding.EncodeToString(binary) + + require.NoError(t, + os.WriteFile(filepath.Join(tmpDir, confidentialWorkflowBinaryFilename), []byte(encoded), 0o600), + "writing workflow binary artifact") + require.NoError(t, + os.WriteFile(filepath.Join(tmpDir, confidentialWorkflowConfigFilename), []byte(configJSON), 0o600), + "writing workflow config artifact") + + mux := http.NewServeMux() + mux.HandleFunc("/"+confidentialWorkflowBinaryFilename, func(rw http.ResponseWriter, _ *http.Request) { + _, _ = rw.Write([]byte(encoded)) + }) + mux.HandleFunc("/"+confidentialWorkflowConfigFilename, func(rw http.ResponseWriter, _ *http.Request) { + _, _ = rw.Write([]byte(configJSON)) + }) + + listener, err := net.Listen("tcp", "0.0.0.0:0") + require.NoError(t, err, "workflow artifact listener") + srv := &http.Server{Handler: mux} + go func() { _ = srv.Serve(listener) }() + t.Cleanup(func() { _ = srv.Close() }) + + port := listener.Addr().(*net.TCPAddr).Port + base := fmt.Sprintf("http://%s:%d/", hostIP, port) + + return confidentialWorkflowArtifacts{ + BinaryURL: base + confidentialWorkflowBinaryFilename, + ConfigURL: base + confidentialWorkflowConfigFilename, + ArtifactDir: tmpDir, + BinaryHash: hash[:], + } +} + +// --------------------------------------------------------------------------- +// Enclave configuration +// --------------------------------------------------------------------------- + +// configureEnclaves POSTs the enclave config to every enclave's config endpoint. +// Until this lands, an enclave has no signer set and no master public key, so it +// rejects every incoming compute request. +// +// The signer set is the workflow DON's worker P2P IDs, and F is derived as +// 2*don.F + 1 to match the relay DON quorum the enclave expects. +func configureEnclaves( + t *testing.T, + testEnv *ttypes.TestEnvironment, + testLogger zerolog.Logger, + configURLs []string, + vaultPublicKey string, +) { + t.Helper() + + workers, err := testEnv.Dons.MustWorkflowDON().Workers() + require.NoError(t, err, "failed to get worker nodes from topology") + require.NotEmpty(t, workers, "workflow DON has no worker nodes") + + signers := make([][]byte, 0, len(workers)) + for _, node := range workers { + signers = append(signers, node.Keys.P2PKey.PeerID[:]) + } + + masterPublicKey, err := hex.DecodeString(vaultPublicKey) + require.NoError(t, err, "failed to hex-decode vault public key") + + // don.F for an N-node DON is N/3; the enclave's own F/T is 2*don.F + 1. + donF := uint32(len(workers) / 3) + quorum := 2*donF + 1 + + config := cctypes.EnclaveConfig{ + Signers: signers, + MasterPublicKey: masterPublicKey, + T: quorum, + F: quorum, + } + configBytes, err := json.Marshal(config) + require.NoError(t, err, "failed to marshal enclave config") + + enclaveType := cctypes.EnclaveTypeNitro + if UseFakeEnclaveForTest() { + enclaveType = cctypes.EnclaveTypeFake + } + + client := http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec // local test enclaves use self-signed certs + }, + } + + for i, configURL := range configURLs { + _, err := util.SetNodeConfig( + t.Context(), + cctypes.Enclave{ + EnclaveURL: configURL, + EnclaveType: enclaveType, + TrustedValues: [][]byte{}, + Region: confidentialEnclaveRegion, + }, + cctypes.ConfigRequest{Config: configBytes}, + &client, + ) + require.NoError(t, err, "failed to set config on enclave %d (%s)", i, configURL) + testLogger.Info().Int("enclave", i).Str("configURL", configURL).Msg("Enclave configured") + } +} + +// UseFakeEnclaveForTest reports whether the harness selected fake enclaves. +func UseFakeEnclaveForTest() bool { + return testhelpers.UseFakeEnclave() +} + +// storeConfidentialWorkflowSecret encrypts a secret to the vault's public key and +// stores it in the vault DON through the gateway, so the workflow's GetSecret call +// resolves. Reuses the vault request helpers already in this package. +func storeConfidentialWorkflowSecret( + t *testing.T, + testEnv *ttypes.TestEnvironment, + testLogger zerolog.Logger, + gatewayURL string, + vaultPublicKey string, + secretKey string, + secretValue string, +) { + t.Helper() + + require.IsType(t, &evm.Blockchain{}, testEnv.CreEnvironment.Blockchains[0], "expected EVM blockchain") + sethClient := testEnv.CreEnvironment.Blockchains[0].(*evm.Blockchain).SethClient + owner := sethClient.MustGetRootKeyAddress().Hex() + + wfRegAddr := crecontracts.MustGetAddressFromDataStore( + testEnv.CreEnvironment.CldfEnvironment.DataStore, + testEnv.CreEnvironment.Blockchains[0].ChainSelector(), //nolint:staticcheck // mirrors system-tests usage + keystone_changeset.WorkflowRegistry.String(), + testEnv.CreEnvironment.ContractVersions[keystone_changeset.WorkflowRegistry.String()], + "", + ) + wfReg, err := workflow_registry_v2_wrapper.NewWorkflowRegistry(common.HexToAddress(wfRegAddr), sethClient.Client) + require.NoError(t, err, "failed to build workflow registry wrapper") + + // The vault DON only accepts secrets from an owner linked in the registry. + requireVaultLinkOwner(t, sethClient, common.HexToAddress(wfRegAddr), + testEnv.CreEnvironment.ContractVersions[keystone_changeset.WorkflowRegistry.String()]) + + parsedKey := mustVaultPublicKey(t, vaultPublicKey) + encryptedSecret, err := vaultutils.EncryptSecretWithWorkflowOwner(secretValue, parsedKey, sethClient.MustGetRootKeyAddress()) + require.NoError(t, err, "failed to encrypt secret for the vault DON") + + auth := newAllowlistVaultRequestAuth(owner, sethClient, wfReg) + executeVaultSecretsCreateWithAuth(t, auth, encryptedSecret, secretKey, owner, gatewayURL, []string{"main"}) + + testLogger.Info().Str("key", secretKey).Str("owner", owner).Msg("Stored workflow secret in the vault DON") +} diff --git a/system-tests/tests/smoke/cre/testdata/confidentialworkflow/go.mod b/system-tests/tests/smoke/cre/testdata/confidentialworkflow/go.mod new file mode 100644 index 00000000000..a93e9ecbb2d --- /dev/null +++ b/system-tests/tests/smoke/cre/testdata/confidentialworkflow/go.mod @@ -0,0 +1,26 @@ +module github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/testdata/confidentialworkflow + +go 1.25.3 + +require ( + github.com/ethereum/go-ethereum v1.17.2 + github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260504161322-7061fbfd5189 + github.com/smartcontractkit/cre-sdk-go v1.7.1-0.20260504162314-fbfac1c36bec + github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm v1.0.0-beta.0 + github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http v1.3.1-0.20260504162314-fbfac1c36bec + github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v1.3.1-0.20260504162314-fbfac1c36bec +) + +require ( + github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/holiman/uint256 v1.3.2 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/stretchr/testify v1.11.1 // indirect + golang.org/x/sys v0.40.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/system-tests/tests/smoke/cre/testdata/confidentialworkflow/go.sum b/system-tests/tests/smoke/cre/testdata/confidentialworkflow/go.sum new file mode 100644 index 00000000000..40c106bdac6 --- /dev/null +++ b/system-tests/tests/smoke/cre/testdata/confidentialworkflow/go.sum @@ -0,0 +1,49 @@ +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/ethereum/go-ethereum v1.17.2 h1:ag6geu0kn8Hv5FLKTpH+Hm2DHD+iuFtuqKxEuwUsDOI= +github.com/ethereum/go-ethereum v1.17.2/go.mod h1:KHcRXfGOUfUmKg51IhQ0IowiqZ6PqZf08CMtk0g5K1o= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= +github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260504161322-7061fbfd5189 h1:Fe3Njnug3v3lXGTctzrHUbQSrEoXocdr9bAsakk5RB4= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260504161322-7061fbfd5189/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8= +github.com/smartcontractkit/cre-sdk-go v1.7.1-0.20260504162314-fbfac1c36bec h1:2+umTSp4yKFCXwL0EQnUns4T58ar3MMqEt4IgaKGaGw= +github.com/smartcontractkit/cre-sdk-go v1.7.1-0.20260504162314-fbfac1c36bec/go.mod h1:UHFB1yffuwFEAjInNtFJnKhLoCiud1Vj7OIdU3X45Oo= +github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm v1.0.0-beta.0 h1:t2bzRHnqkyxvcrJKSsKPmCGLMjGO97ESgrtLCnTIEQw= +github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm v1.0.0-beta.0/go.mod h1:VVJ4mvA7wOU1Ic5b/vTaBMHEUysyxd0gdPPXkAu8CmY= +github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http v1.3.1-0.20260504162314-fbfac1c36bec h1:kI0ztRukhdYHOjhuioOH3asJIDgB+4BZxXtMeIuW6q4= +github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http v1.3.1-0.20260504162314-fbfac1c36bec/go.mod h1:nMbi32+ChPiQQS4Rk/HEFeDZ3SFtY5YUPR9bgPoNZdI= +github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v1.3.1-0.20260504162314-fbfac1c36bec h1:K5c66SeYJ9Psa14SkShBjmXiST5UctigQIxDsREoGt0= +github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v1.3.1-0.20260504162314-fbfac1c36bec/go.mod h1:RHfcLS022PoBg1mOB8ObuzHrTJ+ZEh4rQdeuNfheBsI= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/system-tests/tests/smoke/cre/testdata/confidentialworkflow/main.go b/system-tests/tests/smoke/cre/testdata/confidentialworkflow/main.go new file mode 100644 index 00000000000..b39df875447 --- /dev/null +++ b/system-tests/tests/smoke/cre/testdata/confidentialworkflow/main.go @@ -0,0 +1,193 @@ +//go:build wasip1 + +// Package main is the E2E test workflow used by the engine-path test. +// It exercises three capability paths from inside a confidential workflow: +// - GetSecret → VaultDON (remote dispatch through the relay DON) +// - http.SendRequest + ConsensusMedianAggregation → intercepted locally +// by the enclave (http-actions + consensus/Simple both handled in-process) +// - ReportFromDon + evm.WriteReport → routed *out* of the enclave to the DONs, +// which is how a TEE handler reaches consensus-bound capabilities. The report +// lands in a PermissionlessFeedsConsumer the test then reads back on-chain. +// +// Each success is marked in the workflow engine logs for the test to scrape. +package main + +import ( + "encoding/hex" + "fmt" + "log/slog" + "math/big" + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + sdkpb "github.com/smartcontractkit/chainlink-protos/cre/go/sdk" + "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm" + "github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http" + "github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron" + "github.com/smartcontractkit/cre-sdk-go/cre" + "github.com/smartcontractkit/cre-sdk-go/cre/wasm" +) + +// writeGasLimit mirrors the upstream proof-of-reserve example's limit. +const writeGasLimit = 400_000 + +type config struct { + EchoURL string `json:"echo_url"` + + // Chain-write leg. Empty ConsumerAddress disables it, so the test can run the + // secret + http legs alone if it ever needs to. + ConsumerAddress string `json:"consumer_address"` + ChainSelector uint64 `json:"chain_selector"` + FeedID string `json:"feed_id"` + Price uint64 `json:"price"` +} + +// feedReport matches PermissionlessFeedsConsumer's ReceivedFeedReport struct, which +// its onReport decodes with abi.decode(rawReport, (ReceivedFeedReport[])). +type feedReport struct { + FeedID [32]byte + Timestamp uint32 + Price *big.Int +} + +func main() { + wasm.NewRunner(cre.ParseJSON[config]).Run(initWorkflow) +} + +func initWorkflow(_ *config, _ *slog.Logger, _ cre.SecretsProvider) (cre.Workflow[*config], error) { + return cre.Workflow[*config]{ + cre.HandlerInTee( + cron.Trigger(&cron.Config{Schedule: "*/30 * * * * *"}), + handleTrigger, + cre.AnyTee{}, + ), + }, nil +} + +func handleTrigger(cfg *config, trt cre.TeeRuntime, payload *cron.Payload) (any, error) { + secret, err := trt.GetSecret(&sdkpb.SecretRequest{Id: "MOCK_SECRET"}).Await() + if err != nil { + return nil, err + } + // DO NOT log secrets in production workflows. We only do it here so the + // test can scrape the value out of workflow-DON logs and confirm that the + // VaultDON-routed secret-fetch path actually delivered the right value + // into the WASM. Real users: don't follow this pattern. + trt.Logger().Info("engine-test-secret", "value", secret.Value) + + result := map[string]any{"secret": secret.Value} + + if cfg.EchoURL != "" { + client := &http.Client{} + status, httpErr := fetchEchoStatus(cfg, trt, client) + if httpErr != nil { + trt.Logger().Error("engine-test-http-failed", "error", httpErr.Error()) + return nil, httpErr + } + trt.Logger().Info("engine-test-http", "status", status, "url", cfg.EchoURL) + result["http_status"] = status + } + + if cfg.ConsumerAddress != "" { + txHash, writeErr := writeFeedReport(cfg, trt, payload) + if writeErr != nil { + trt.Logger().Error("engine-test-write-failed", "error", writeErr.Error()) + return nil, writeErr + } + trt.Logger().Info("engine-test-write", "txHash", txHash, "receiver", cfg.ConsumerAddress, "price", cfg.Price) + result["tx_hash"] = txHash + } + + return result, nil +} + +func fetchEchoStatus(cfg *config, trt cre.TeeRuntime, client *http.Client) (int32, error) { + resp, err := client.SendRequestInTee(trt, &http.Request{ + Url: cfg.EchoURL, + Method: "POST", + Body: []byte("hello from engine-test"), + Headers: map[string]string{ + "Content-Type": "text/plain", + }, + }).Await() + if err != nil { + return 0, err + } + return int32(resp.StatusCode), nil +} + +// writeFeedReport generates a DON-signed report over a single feed value and writes +// it to the consumer contract. Both legs leave the enclave: cre.TeeRuntime exposes +// no GenerateReport (report signing needs the DONs) and the evm write capability +// takes a cre.Runtime, so the report comes from ReportFromDon and the write goes +// through UsingTheDons. +func writeFeedReport(cfg *config, trt cre.TeeRuntime, payload *cron.Payload) (string, error) { + feedID, err := parseFeedID(cfg.FeedID) + if err != nil { + return "", err + } + + // Use the trigger's scheduled time rather than a wall clock inside the enclave, + // matching the chain_write canary. + encoded, err := encodeFeedReports([]feedReport{{ + FeedID: feedID, + Timestamp: uint32(payload.ScheduledExecutionTime.AsTime().Unix()), + Price: new(big.Int).SetUint64(cfg.Price), + }}) + if err != nil { + return "", fmt.Errorf("encode feed report: %w", err) + } + + report, err := trt.ReportFromDon(&cre.ReportRequest{ + EncodedPayload: encoded, + EncoderName: "evm", + SigningAlgo: "ecdsa", + HashingAlgo: "keccak256", + }).Await() + if err != nil { + return "", fmt.Errorf("report from don: %w", err) + } + + evmClient := &evm.Client{ChainSelector: cfg.ChainSelector} + out, err := evmClient.WriteReport(trt.UsingTheDons(), &evm.WriteCreReportRequest{ + Receiver: common.HexToAddress(cfg.ConsumerAddress).Bytes(), + Report: report, + GasConfig: &evm.GasConfig{GasLimit: writeGasLimit}, + }).Await() + if err != nil { + return "", fmt.Errorf("write report: %w", err) + } + if out.ErrorMessage != nil && *out.ErrorMessage != "" { + return "", fmt.Errorf("write report rejected: %s", *out.ErrorMessage) + } + + return "0x" + hex.EncodeToString(out.TxHash), nil +} + +func parseFeedID(s string) ([32]byte, error) { + var id [32]byte + b, err := hex.DecodeString(strings.TrimPrefix(s, "0x")) + if err != nil { + return id, fmt.Errorf("decode feed id %q: %w", s, err) + } + if len(b) != 32 { + return id, fmt.Errorf("feed id %q decoded to %d bytes, want 32", s, len(b)) + } + copy(id[:], b) + return id, nil +} + +// encodeFeedReports packs reports the way PermissionlessFeedsConsumer.onReport +// decodes them: a single dynamic array of (bytes32, uint32, uint224) tuples. +func encodeFeedReports(reports []feedReport) ([]byte, error) { + typ, err := abi.NewType("tuple[]", "", []abi.ArgumentMarshaling{ + {Name: "FeedID", Type: "bytes32"}, + {Name: "Timestamp", Type: "uint32"}, + {Name: "Price", Type: "uint224"}, + }) + if err != nil { + return nil, fmt.Errorf("build abi type: %w", err) + } + return abi.Arguments{{Name: "Reports", Type: typ}}.Pack(reports) +} From 838ddf0d4521dc6a12fe372ada7965442f6b42d3 Mon Sep 17 00:00:00 2001 From: vreff <104409744+vreff@users.noreply.github.com> Date: Wed, 12 Aug 2026 12:47:58 -0400 Subject: [PATCH 02/29] ci fix --- .github/workflows/ci-core.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci-core.yml b/.github/workflows/ci-core.yml index ee3b01974e6..22ee5ab4472 100644 --- a/.github/workflows/ci-core.yml +++ b/.github/workflows/ci-core.yml @@ -157,6 +157,7 @@ jobs: module-patterns: | ** !core/scripts/cre/environment/examples/workflows/** + !system-tests/tests/smoke/cre/testdata/** golangci: name: GolangCI Lint From 48a9fd7e6cef1818aa7b899e400927dfd45e5e66 Mon Sep 17 00:00:00 2001 From: vreff <104409744+vreff@users.noreply.github.com> Date: Wed, 12 Aug 2026 12:53:30 -0400 Subject: [PATCH 03/29] fix test --- system-tests/tests/smoke/cre/confidential_workflows_test.go | 4 ---- .../tests/smoke/cre/confidential_workflows_test_helpers.go | 4 ++++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/system-tests/tests/smoke/cre/confidential_workflows_test.go b/system-tests/tests/smoke/cre/confidential_workflows_test.go index 88207593593..022b77b54e9 100644 --- a/system-tests/tests/smoke/cre/confidential_workflows_test.go +++ b/system-tests/tests/smoke/cre/confidential_workflows_test.go @@ -47,10 +47,6 @@ const ( // confidentialVaultThreshold matches the 4-node F=1 vault DON. confidentialVaultThreshold = 1 - - // confidentialEnclaveRegion is recorded on each enclave descriptor. Descriptor - // hashes cover it, so it must match what the enclave itself reports. - confidentialEnclaveRegion = "us-west-2" ) // Test_CRE_V2_ConfidentialWorkflows_Relay exercises the confidential workflows diff --git a/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go b/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go index 5017c638e4e..c5df96d5fe4 100644 --- a/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go +++ b/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go @@ -65,6 +65,10 @@ const ( // confidentialWorkflowSrcDir holds the WASM workflow the test compiles. confidentialWorkflowSrcDir = "testdata/confidentialworkflow" + + // confidentialEnclaveRegion is recorded on each enclave descriptor. Descriptor + // hashes cover it, so it must match what the enclave itself reports. + confidentialEnclaveRegion = "us-west-2" ) // --------------------------------------------------------------------------- From bcb8fec217bfedd5345b79a8c6b289a862d3e0eb Mon Sep 17 00:00:00 2001 From: vreff <104409744+vreff@users.noreply.github.com> Date: Wed, 12 Aug 2026 13:24:32 -0400 Subject: [PATCH 04/29] fix tests --- .github/workflows/ci-core.yml | 1 - .../smoke/cre/confidential_workflows_test.go | 2 +- .../confidential_workflows_test_helpers.go | 63 +++--- .../cre/testdata/confidentialworkflow/go.mod | 26 --- .../cre/testdata/confidentialworkflow/go.sum | 49 ----- .../cre/testdata/confidentialworkflow/main.go | 193 ------------------ 6 files changed, 30 insertions(+), 304 deletions(-) delete mode 100644 system-tests/tests/smoke/cre/testdata/confidentialworkflow/go.mod delete mode 100644 system-tests/tests/smoke/cre/testdata/confidentialworkflow/go.sum delete mode 100644 system-tests/tests/smoke/cre/testdata/confidentialworkflow/main.go diff --git a/.github/workflows/ci-core.yml b/.github/workflows/ci-core.yml index 22ee5ab4472..ee3b01974e6 100644 --- a/.github/workflows/ci-core.yml +++ b/.github/workflows/ci-core.yml @@ -157,7 +157,6 @@ jobs: module-patterns: | ** !core/scripts/cre/environment/examples/workflows/** - !system-tests/tests/smoke/cre/testdata/** golangci: name: GolangCI Lint diff --git a/system-tests/tests/smoke/cre/confidential_workflows_test.go b/system-tests/tests/smoke/cre/confidential_workflows_test.go index 022b77b54e9..7e0bd281ccb 100644 --- a/system-tests/tests/smoke/cre/confidential_workflows_test.go +++ b/system-tests/tests/smoke/cre/confidential_workflows_test.go @@ -150,7 +150,7 @@ func Test_CRE_V2_ConfidentialWorkflows_Relay(t *testing.T) { // 7. Compile and serve the workflow. ConsumerAddress is left empty, which // the workflow treats as "skip the chain-write leg". configJSON := fmt.Sprintf(`{"echo_url":%q}`, confidentialEchoURL) - artifacts := buildAndServeConfidentialWorkflow(t, configJSON, testhelpers.DetectHostIP()) + artifacts := serveConfidentialWorkflow(t, configJSON, testhelpers.DetectHostIP()) testLogger.Info(). Str("binaryURL", artifacts.BinaryURL). Str("configURL", artifacts.ConfigURL). diff --git a/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go b/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go index c5df96d5fe4..40bd2b57e62 100644 --- a/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go +++ b/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go @@ -1,7 +1,6 @@ package cre import ( - "bytes" "context" "crypto/sha256" "crypto/tls" @@ -14,13 +13,11 @@ import ( "net/http/httputil" "net/url" "os" - "os/exec" "path/filepath" "strings" "sync" "testing" - "github.com/andybalholm/brotli" "github.com/rs/zerolog" "github.com/stretchr/testify/require" "google.golang.org/grpc" @@ -287,48 +284,46 @@ type confidentialWorkflowArtifacts struct { BinaryHash []byte } -// buildAndServeConfidentialWorkflow compiles the test workflow to wasip1/wasm, -// brotli-compresses and base64-encodes it (the format the syncer expects), writes -// both binary and config to a temp dir, and serves them over HTTP bound to -// 0.0.0.0 so the host, the Docker containers and the enclaves can all fetch them. -func buildAndServeConfidentialWorkflow(t *testing.T, configJSON string, hostIP string) confidentialWorkflowArtifacts { +// serveConfidentialWorkflow publishes the prebuilt workflow artifact and its +// config over HTTP, bound to 0.0.0.0 so the host, the Docker containers and the +// enclaves can all fetch them. +// +// The workflow is committed as a prebuilt brotli+base64 blob rather than Go +// source, matching how this repository ships its other WASM workflows. Building +// it here is not possible: it depends on cre-sdk-go versions that predate the +// removal of the in-TEE HTTP API, and those versions do not pass this +// repository's dependency validation. Regenerate it from +// chainlink-confidential-compute (tests/e2e/testdata/workflow) and copy the +// result over. +func serveConfidentialWorkflow(t *testing.T, configJSON string, hostIP string) confidentialWorkflowArtifacts { t.Helper() - srcDir, err := filepath.Abs(confidentialWorkflowSrcDir) - require.NoError(t, err, "resolving workflow source path") - - tmpDir := t.TempDir() - outFile := filepath.Join(tmpDir, "workflow-test.wasm") - - cmd := exec.Command("go", "build", "-o", outFile, ".") - cmd.Dir = srcDir - cmd.Env = append(os.Environ(), "GOOS=wasip1", "GOARCH=wasm", "CGO_ENABLED=0") - output, err := cmd.CombinedOutput() - require.NoError(t, err, "compiling confidential workflow WASM: %s", string(output)) - - raw, err := os.ReadFile(outFile) - require.NoError(t, err, "reading compiled WASM") + srcPath, err := filepath.Abs(filepath.Join(confidentialWorkflowSrcDir, confidentialWorkflowBinaryFilename)) + require.NoError(t, err, "resolving prebuilt workflow path") - var compressed bytes.Buffer - w := brotli.NewWriter(&compressed) - _, err = w.Write(raw) - require.NoError(t, err, "brotli compressing WASM") - require.NoError(t, w.Close(), "closing brotli writer") + encoded, err := os.ReadFile(srcPath) + require.NoError(t, err, "reading prebuilt workflow artifact at %s", srcPath) + require.NotEmpty(t, encoded, "prebuilt workflow artifact is empty") - binary := compressed.Bytes() - hash := sha256.Sum256(binary) - encoded := base64.StdEncoding.EncodeToString(binary) + // The syncer and the enclave both expect base64-encoded brotli; hash the + // decoded bytes so the value matches what the enclave verifies. + compressed, err := base64.StdEncoding.DecodeString(string(encoded)) + require.NoError(t, err, "prebuilt workflow artifact is not valid base64") + hash := sha256.Sum256(compressed) + // The syncer's file fetcher reads both files from disk inside the container, + // so they have to exist as real files the test can copy in. + tmpDir := t.TempDir() require.NoError(t, - os.WriteFile(filepath.Join(tmpDir, confidentialWorkflowBinaryFilename), []byte(encoded), 0o600), - "writing workflow binary artifact") + os.WriteFile(filepath.Join(tmpDir, confidentialWorkflowBinaryFilename), encoded, 0o600), + "staging workflow binary artifact") require.NoError(t, os.WriteFile(filepath.Join(tmpDir, confidentialWorkflowConfigFilename), []byte(configJSON), 0o600), - "writing workflow config artifact") + "staging workflow config artifact") mux := http.NewServeMux() mux.HandleFunc("/"+confidentialWorkflowBinaryFilename, func(rw http.ResponseWriter, _ *http.Request) { - _, _ = rw.Write([]byte(encoded)) + _, _ = rw.Write(encoded) }) mux.HandleFunc("/"+confidentialWorkflowConfigFilename, func(rw http.ResponseWriter, _ *http.Request) { _, _ = rw.Write([]byte(configJSON)) diff --git a/system-tests/tests/smoke/cre/testdata/confidentialworkflow/go.mod b/system-tests/tests/smoke/cre/testdata/confidentialworkflow/go.mod deleted file mode 100644 index a93e9ecbb2d..00000000000 --- a/system-tests/tests/smoke/cre/testdata/confidentialworkflow/go.mod +++ /dev/null @@ -1,26 +0,0 @@ -module github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/testdata/confidentialworkflow - -go 1.25.3 - -require ( - github.com/ethereum/go-ethereum v1.17.2 - github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260504161322-7061fbfd5189 - github.com/smartcontractkit/cre-sdk-go v1.7.1-0.20260504162314-fbfac1c36bec - github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm v1.0.0-beta.0 - github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http v1.3.1-0.20260504162314-fbfac1c36bec - github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v1.3.1-0.20260504162314-fbfac1c36bec -) - -require ( - github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 // indirect - github.com/holiman/uint256 v1.3.2 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/shopspring/decimal v1.4.0 // indirect - github.com/stretchr/testify v1.11.1 // indirect - golang.org/x/sys v0.40.0 // indirect - google.golang.org/protobuf v1.36.11 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) diff --git a/system-tests/tests/smoke/cre/testdata/confidentialworkflow/go.sum b/system-tests/tests/smoke/cre/testdata/confidentialworkflow/go.sum deleted file mode 100644 index 40c106bdac6..00000000000 --- a/system-tests/tests/smoke/cre/testdata/confidentialworkflow/go.sum +++ /dev/null @@ -1,49 +0,0 @@ -github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU= -github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= -github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= -github.com/ethereum/go-ethereum v1.17.2 h1:ag6geu0kn8Hv5FLKTpH+Hm2DHD+iuFtuqKxEuwUsDOI= -github.com/ethereum/go-ethereum v1.17.2/go.mod h1:KHcRXfGOUfUmKg51IhQ0IowiqZ6PqZf08CMtk0g5K1o= -github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= -github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= -github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260504161322-7061fbfd5189 h1:Fe3Njnug3v3lXGTctzrHUbQSrEoXocdr9bAsakk5RB4= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260504161322-7061fbfd5189/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8= -github.com/smartcontractkit/cre-sdk-go v1.7.1-0.20260504162314-fbfac1c36bec h1:2+umTSp4yKFCXwL0EQnUns4T58ar3MMqEt4IgaKGaGw= -github.com/smartcontractkit/cre-sdk-go v1.7.1-0.20260504162314-fbfac1c36bec/go.mod h1:UHFB1yffuwFEAjInNtFJnKhLoCiud1Vj7OIdU3X45Oo= -github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm v1.0.0-beta.0 h1:t2bzRHnqkyxvcrJKSsKPmCGLMjGO97ESgrtLCnTIEQw= -github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm v1.0.0-beta.0/go.mod h1:VVJ4mvA7wOU1Ic5b/vTaBMHEUysyxd0gdPPXkAu8CmY= -github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http v1.3.1-0.20260504162314-fbfac1c36bec h1:kI0ztRukhdYHOjhuioOH3asJIDgB+4BZxXtMeIuW6q4= -github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http v1.3.1-0.20260504162314-fbfac1c36bec/go.mod h1:nMbi32+ChPiQQS4Rk/HEFeDZ3SFtY5YUPR9bgPoNZdI= -github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v1.3.1-0.20260504162314-fbfac1c36bec h1:K5c66SeYJ9Psa14SkShBjmXiST5UctigQIxDsREoGt0= -github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v1.3.1-0.20260504162314-fbfac1c36bec/go.mod h1:RHfcLS022PoBg1mOB8ObuzHrTJ+ZEh4rQdeuNfheBsI= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/system-tests/tests/smoke/cre/testdata/confidentialworkflow/main.go b/system-tests/tests/smoke/cre/testdata/confidentialworkflow/main.go deleted file mode 100644 index b39df875447..00000000000 --- a/system-tests/tests/smoke/cre/testdata/confidentialworkflow/main.go +++ /dev/null @@ -1,193 +0,0 @@ -//go:build wasip1 - -// Package main is the E2E test workflow used by the engine-path test. -// It exercises three capability paths from inside a confidential workflow: -// - GetSecret → VaultDON (remote dispatch through the relay DON) -// - http.SendRequest + ConsensusMedianAggregation → intercepted locally -// by the enclave (http-actions + consensus/Simple both handled in-process) -// - ReportFromDon + evm.WriteReport → routed *out* of the enclave to the DONs, -// which is how a TEE handler reaches consensus-bound capabilities. The report -// lands in a PermissionlessFeedsConsumer the test then reads back on-chain. -// -// Each success is marked in the workflow engine logs for the test to scrape. -package main - -import ( - "encoding/hex" - "fmt" - "log/slog" - "math/big" - "strings" - - "github.com/ethereum/go-ethereum/accounts/abi" - "github.com/ethereum/go-ethereum/common" - sdkpb "github.com/smartcontractkit/chainlink-protos/cre/go/sdk" - "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm" - "github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http" - "github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron" - "github.com/smartcontractkit/cre-sdk-go/cre" - "github.com/smartcontractkit/cre-sdk-go/cre/wasm" -) - -// writeGasLimit mirrors the upstream proof-of-reserve example's limit. -const writeGasLimit = 400_000 - -type config struct { - EchoURL string `json:"echo_url"` - - // Chain-write leg. Empty ConsumerAddress disables it, so the test can run the - // secret + http legs alone if it ever needs to. - ConsumerAddress string `json:"consumer_address"` - ChainSelector uint64 `json:"chain_selector"` - FeedID string `json:"feed_id"` - Price uint64 `json:"price"` -} - -// feedReport matches PermissionlessFeedsConsumer's ReceivedFeedReport struct, which -// its onReport decodes with abi.decode(rawReport, (ReceivedFeedReport[])). -type feedReport struct { - FeedID [32]byte - Timestamp uint32 - Price *big.Int -} - -func main() { - wasm.NewRunner(cre.ParseJSON[config]).Run(initWorkflow) -} - -func initWorkflow(_ *config, _ *slog.Logger, _ cre.SecretsProvider) (cre.Workflow[*config], error) { - return cre.Workflow[*config]{ - cre.HandlerInTee( - cron.Trigger(&cron.Config{Schedule: "*/30 * * * * *"}), - handleTrigger, - cre.AnyTee{}, - ), - }, nil -} - -func handleTrigger(cfg *config, trt cre.TeeRuntime, payload *cron.Payload) (any, error) { - secret, err := trt.GetSecret(&sdkpb.SecretRequest{Id: "MOCK_SECRET"}).Await() - if err != nil { - return nil, err - } - // DO NOT log secrets in production workflows. We only do it here so the - // test can scrape the value out of workflow-DON logs and confirm that the - // VaultDON-routed secret-fetch path actually delivered the right value - // into the WASM. Real users: don't follow this pattern. - trt.Logger().Info("engine-test-secret", "value", secret.Value) - - result := map[string]any{"secret": secret.Value} - - if cfg.EchoURL != "" { - client := &http.Client{} - status, httpErr := fetchEchoStatus(cfg, trt, client) - if httpErr != nil { - trt.Logger().Error("engine-test-http-failed", "error", httpErr.Error()) - return nil, httpErr - } - trt.Logger().Info("engine-test-http", "status", status, "url", cfg.EchoURL) - result["http_status"] = status - } - - if cfg.ConsumerAddress != "" { - txHash, writeErr := writeFeedReport(cfg, trt, payload) - if writeErr != nil { - trt.Logger().Error("engine-test-write-failed", "error", writeErr.Error()) - return nil, writeErr - } - trt.Logger().Info("engine-test-write", "txHash", txHash, "receiver", cfg.ConsumerAddress, "price", cfg.Price) - result["tx_hash"] = txHash - } - - return result, nil -} - -func fetchEchoStatus(cfg *config, trt cre.TeeRuntime, client *http.Client) (int32, error) { - resp, err := client.SendRequestInTee(trt, &http.Request{ - Url: cfg.EchoURL, - Method: "POST", - Body: []byte("hello from engine-test"), - Headers: map[string]string{ - "Content-Type": "text/plain", - }, - }).Await() - if err != nil { - return 0, err - } - return int32(resp.StatusCode), nil -} - -// writeFeedReport generates a DON-signed report over a single feed value and writes -// it to the consumer contract. Both legs leave the enclave: cre.TeeRuntime exposes -// no GenerateReport (report signing needs the DONs) and the evm write capability -// takes a cre.Runtime, so the report comes from ReportFromDon and the write goes -// through UsingTheDons. -func writeFeedReport(cfg *config, trt cre.TeeRuntime, payload *cron.Payload) (string, error) { - feedID, err := parseFeedID(cfg.FeedID) - if err != nil { - return "", err - } - - // Use the trigger's scheduled time rather than a wall clock inside the enclave, - // matching the chain_write canary. - encoded, err := encodeFeedReports([]feedReport{{ - FeedID: feedID, - Timestamp: uint32(payload.ScheduledExecutionTime.AsTime().Unix()), - Price: new(big.Int).SetUint64(cfg.Price), - }}) - if err != nil { - return "", fmt.Errorf("encode feed report: %w", err) - } - - report, err := trt.ReportFromDon(&cre.ReportRequest{ - EncodedPayload: encoded, - EncoderName: "evm", - SigningAlgo: "ecdsa", - HashingAlgo: "keccak256", - }).Await() - if err != nil { - return "", fmt.Errorf("report from don: %w", err) - } - - evmClient := &evm.Client{ChainSelector: cfg.ChainSelector} - out, err := evmClient.WriteReport(trt.UsingTheDons(), &evm.WriteCreReportRequest{ - Receiver: common.HexToAddress(cfg.ConsumerAddress).Bytes(), - Report: report, - GasConfig: &evm.GasConfig{GasLimit: writeGasLimit}, - }).Await() - if err != nil { - return "", fmt.Errorf("write report: %w", err) - } - if out.ErrorMessage != nil && *out.ErrorMessage != "" { - return "", fmt.Errorf("write report rejected: %s", *out.ErrorMessage) - } - - return "0x" + hex.EncodeToString(out.TxHash), nil -} - -func parseFeedID(s string) ([32]byte, error) { - var id [32]byte - b, err := hex.DecodeString(strings.TrimPrefix(s, "0x")) - if err != nil { - return id, fmt.Errorf("decode feed id %q: %w", s, err) - } - if len(b) != 32 { - return id, fmt.Errorf("feed id %q decoded to %d bytes, want 32", s, len(b)) - } - copy(id[:], b) - return id, nil -} - -// encodeFeedReports packs reports the way PermissionlessFeedsConsumer.onReport -// decodes them: a single dynamic array of (bytes32, uint32, uint224) tuples. -func encodeFeedReports(reports []feedReport) ([]byte, error) { - typ, err := abi.NewType("tuple[]", "", []abi.ArgumentMarshaling{ - {Name: "FeedID", Type: "bytes32"}, - {Name: "Timestamp", Type: "uint32"}, - {Name: "Price", Type: "uint224"}, - }) - if err != nil { - return nil, fmt.Errorf("build abi type: %w", err) - } - return abi.Arguments{{Name: "Reports", Type: typ}}.Pack(reports) -} From 273400b7f820c8982386256ebecfdc1cabfa2787 Mon Sep 17 00:00:00 2001 From: vreff <104409744+vreff@users.noreply.github.com> Date: Wed, 12 Aug 2026 13:42:19 -0400 Subject: [PATCH 05/29] fix tests --- .../smoke/cre/confidential_workflows_test.go | 10 +-- .../confidential_workflows_test_helpers.go | 68 ++++++++++++------- 2 files changed, 48 insertions(+), 30 deletions(-) diff --git a/system-tests/tests/smoke/cre/confidential_workflows_test.go b/system-tests/tests/smoke/cre/confidential_workflows_test.go index 7e0bd281ccb..5c8709dac23 100644 --- a/system-tests/tests/smoke/cre/confidential_workflows_test.go +++ b/system-tests/tests/smoke/cre/confidential_workflows_test.go @@ -92,7 +92,8 @@ func Test_CRE_V2_ConfidentialWorkflows_Relay(t *testing.T) { // 2. Start the enclaves. This is the whole point of depending on // chainlink-confidential-compute's harness from this repository. - enclaveCfg := testhelpers.DefaultLocalEnclaveSetupConfig(confidentialComputeRoot(t), confidentialWorkflowsApp) + ccRoot := confidentialComputeRoot(t) + enclaveCfg := testhelpers.DefaultLocalEnclaveSetupConfig(ccRoot, confidentialWorkflowsApp) enclaveCfg.Region = confidentialEnclaveRegion enclaves := testhelpers.SetupLocalEnclaves(t, enclaveCfg) t.Cleanup(enclaves.CleanupAll) @@ -147,10 +148,11 @@ func Test_CRE_V2_ConfidentialWorkflows_Relay(t *testing.T) { storeConfidentialWorkflowSecret(t, testEnv, testLogger, gatewayURL, vaultPublicKey, confidentialSecretName, "s3cret-from-vault") - // 7. Compile and serve the workflow. ConsumerAddress is left empty, which - // the workflow treats as "skip the chain-write leg". + // 7. Compile and serve the workflow from the confidential-compute checkout. + // ConsumerAddress is left empty, which the workflow treats as "skip the + // chain-write leg". configJSON := fmt.Sprintf(`{"echo_url":%q}`, confidentialEchoURL) - artifacts := serveConfidentialWorkflow(t, configJSON, testhelpers.DetectHostIP()) + artifacts := buildAndServeConfidentialWorkflow(t, ccRoot, configJSON, testhelpers.DetectHostIP()) testLogger.Info(). Str("binaryURL", artifacts.BinaryURL). Str("configURL", artifacts.ConfigURL). diff --git a/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go b/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go index 40bd2b57e62..f26f2a01cbf 100644 --- a/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go +++ b/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go @@ -1,6 +1,7 @@ package cre import ( + "bytes" "context" "crypto/sha256" "crypto/tls" @@ -13,11 +14,13 @@ import ( "net/http/httputil" "net/url" "os" + "os/exec" "path/filepath" "strings" "sync" "testing" + "github.com/andybalholm/brotli" "github.com/rs/zerolog" "github.com/stretchr/testify/require" "google.golang.org/grpc" @@ -60,8 +63,12 @@ const ( confidentialWorkflowBinaryFilename = "workflow-test-confidential.br.b64" confidentialWorkflowConfigFilename = "workflow-test-config.json" - // confidentialWorkflowSrcDir holds the WASM workflow the test compiles. - confidentialWorkflowSrcDir = "testdata/confidentialworkflow" + // confidentialWorkflowSrcRelDir is the WASM workflow this test compiles, + // relative to the chainlink-confidential-compute checkout. The source is not + // vendored here on purpose: it depends on cre-sdk-go versions that predate the + // removal of the in-TEE HTTP API, which this repository's dependency + // validation rejects, and the compiled artifact is covered by .gitignore. + confidentialWorkflowSrcRelDir = "tests/e2e/testdata/workflow" // confidentialEnclaveRegion is recorded on each enclave descriptor. Descriptor // hashes cover it, so it must match what the enclave itself reports. @@ -284,38 +291,47 @@ type confidentialWorkflowArtifacts struct { BinaryHash []byte } -// serveConfidentialWorkflow publishes the prebuilt workflow artifact and its -// config over HTTP, bound to 0.0.0.0 so the host, the Docker containers and the -// enclaves can all fetch them. +// buildAndServeConfidentialWorkflow compiles the test workflow to wasip1/wasm +// from the chainlink-confidential-compute checkout, brotli-compresses and +// base64-encodes it (the format the syncer and the enclave both expect), and +// serves the binary and its config over HTTP bound to 0.0.0.0 so the host, the +// Docker containers and the enclaves can all fetch them. // -// The workflow is committed as a prebuilt brotli+base64 blob rather than Go -// source, matching how this repository ships its other WASM workflows. Building -// it here is not possible: it depends on cre-sdk-go versions that predate the -// removal of the in-TEE HTTP API, and those versions do not pass this -// repository's dependency validation. Regenerate it from -// chainlink-confidential-compute (tests/e2e/testdata/workflow) and copy the -// result over. -func serveConfidentialWorkflow(t *testing.T, configJSON string, hostIP string) confidentialWorkflowArtifacts { +// Compiling from the checkout rather than vendoring the source keeps the +// workflow single-sourced and keeps its cre-sdk-go pins out of this +// repository's module graph. +func buildAndServeConfidentialWorkflow(t *testing.T, ccRoot string, configJSON string, hostIP string) confidentialWorkflowArtifacts { t.Helper() - srcPath, err := filepath.Abs(filepath.Join(confidentialWorkflowSrcDir, confidentialWorkflowBinaryFilename)) - require.NoError(t, err, "resolving prebuilt workflow path") + srcDir := filepath.Join(ccRoot, confidentialWorkflowSrcRelDir) + require.DirExists(t, srcDir, "confidential workflow source not found in the chainlink-confidential-compute checkout") - encoded, err := os.ReadFile(srcPath) - require.NoError(t, err, "reading prebuilt workflow artifact at %s", srcPath) - require.NotEmpty(t, encoded, "prebuilt workflow artifact is empty") + tmpDir := t.TempDir() + outFile := filepath.Join(tmpDir, "workflow-test.wasm") + + cmd := exec.Command("go", "build", "-o", outFile, ".") + cmd.Dir = srcDir + cmd.Env = append(os.Environ(), "GOOS=wasip1", "GOARCH=wasm", "CGO_ENABLED=0") + output, err := cmd.CombinedOutput() + require.NoError(t, err, "compiling confidential workflow WASM: %s", string(output)) + + raw, err := os.ReadFile(outFile) + require.NoError(t, err, "reading compiled WASM") - // The syncer and the enclave both expect base64-encoded brotli; hash the - // decoded bytes so the value matches what the enclave verifies. - compressed, err := base64.StdEncoding.DecodeString(string(encoded)) - require.NoError(t, err, "prebuilt workflow artifact is not valid base64") - hash := sha256.Sum256(compressed) + var compressed bytes.Buffer + w := brotli.NewWriter(&compressed) + _, err = w.Write(raw) + require.NoError(t, err, "brotli compressing WASM") + require.NoError(t, w.Close(), "closing brotli writer") + + binary := compressed.Bytes() + hash := sha256.Sum256(binary) + encoded := base64.StdEncoding.EncodeToString(binary) // The syncer's file fetcher reads both files from disk inside the container, // so they have to exist as real files the test can copy in. - tmpDir := t.TempDir() require.NoError(t, - os.WriteFile(filepath.Join(tmpDir, confidentialWorkflowBinaryFilename), encoded, 0o600), + os.WriteFile(filepath.Join(tmpDir, confidentialWorkflowBinaryFilename), []byte(encoded), 0o600), "staging workflow binary artifact") require.NoError(t, os.WriteFile(filepath.Join(tmpDir, confidentialWorkflowConfigFilename), []byte(configJSON), 0o600), @@ -323,7 +339,7 @@ func serveConfidentialWorkflow(t *testing.T, configJSON string, hostIP string) c mux := http.NewServeMux() mux.HandleFunc("/"+confidentialWorkflowBinaryFilename, func(rw http.ResponseWriter, _ *http.Request) { - _, _ = rw.Write(encoded) + _, _ = rw.Write([]byte(encoded)) }) mux.HandleFunc("/"+confidentialWorkflowConfigFilename, func(rw http.ResponseWriter, _ *http.Request) { _, _ = rw.Write([]byte(configJSON)) From 81d92072711eecb4a239879020155dea7ff2c297 Mon Sep 17 00:00:00 2001 From: vreff <104409744+vreff@users.noreply.github.com> Date: Wed, 12 Aug 2026 13:47:14 -0400 Subject: [PATCH 06/29] fix tests --- system-tests/tests/go.mod | 2 +- system-tests/tests/go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/system-tests/tests/go.mod b/system-tests/tests/go.mod index 76acf6c525f..2a9180dd2b4 100644 --- a/system-tests/tests/go.mod +++ b/system-tests/tests/go.mod @@ -69,7 +69,7 @@ require ( github.com/smartcontractkit/chainlink-common v0.11.2-0.20260811140401-3fb1738abb75 github.com/smartcontractkit/chainlink-common/keystore v1.3.0 github.com/smartcontractkit/chainlink-confidential-compute v1.3.0 - github.com/smartcontractkit/chainlink-confidential-compute/tests/testhelpers v0.0.0-20260811154131-b9926f013215 + github.com/smartcontractkit/chainlink-confidential-compute/tests/testhelpers v0.0.0-20260812145307-d77342c53d7d github.com/smartcontractkit/chainlink-deployments-framework v0.111.1-0.20260612191326-e31c0ae4cd54 github.com/smartcontractkit/chainlink-evm/contracts/cre/gobindings v0.0.0-20260403151002-2c91155b5501 github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260713161920-de075095648b diff --git a/system-tests/tests/go.sum b/system-tests/tests/go.sum index 752bbff8509..2c7a28e18b6 100644 --- a/system-tests/tests/go.sum +++ b/system-tests/tests/go.sum @@ -1775,6 +1775,8 @@ github.com/smartcontractkit/chainlink-confidential-compute/tests/testhelpers v0. github.com/smartcontractkit/chainlink-confidential-compute/tests/testhelpers v0.0.0-20260810204028-19c1e24d25ee/go.mod h1:Q5q/ohoAF5N7GXZS6QxgPWSaSLFtS6/53QHJbBUkCmU= github.com/smartcontractkit/chainlink-confidential-compute/tests/testhelpers v0.0.0-20260811154131-b9926f013215 h1:YsxEx0pGUECqZZoed0fY+JFNrB8LsPRAKsFrPK0pQDw= github.com/smartcontractkit/chainlink-confidential-compute/tests/testhelpers v0.0.0-20260811154131-b9926f013215/go.mod h1:Q5q/ohoAF5N7GXZS6QxgPWSaSLFtS6/53QHJbBUkCmU= +github.com/smartcontractkit/chainlink-confidential-compute/tests/testhelpers v0.0.0-20260812145307-d77342c53d7d h1:CZ0Om7lANyhpFJcmZE8RwQpEP4PyvoNLhOJckjm+zao= +github.com/smartcontractkit/chainlink-confidential-compute/tests/testhelpers v0.0.0-20260812145307-d77342c53d7d/go.mod h1:Q5q/ohoAF5N7GXZS6QxgPWSaSLFtS6/53QHJbBUkCmU= github.com/smartcontractkit/chainlink-data-streams v1.1.0 h1:O5ngSwpwey7kQ3t4YgFiXzu3L3EvYiPS4c93gMWNRbk= github.com/smartcontractkit/chainlink-data-streams v1.1.0/go.mod h1:dF5JiHWueHjYguUUUrFeb03MkcDqha/tssEkqTkgzp4= github.com/smartcontractkit/chainlink-deployments-framework v0.111.1-0.20260612191326-e31c0ae4cd54 h1:mzbvXxdbE/96Pdj1zyPKzf25ZlDR48+iTTDTbaITvmk= From a5241c5907c46d23ea9adcdaeae290063c69b6e3 Mon Sep 17 00:00:00 2001 From: vreff <104409744+vreff@users.noreply.github.com> Date: Wed, 12 Aug 2026 13:48:41 -0400 Subject: [PATCH 07/29] update docs --- go.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/go.md b/go.md index 4327e17e445..42c6ee6c0f1 100644 --- a/go.md +++ b/go.md @@ -356,6 +356,10 @@ flowchart LR click chainlink-common/pkg/values href "https://github.com/smartcontractkit/chainlink-common" chainlink-common/pkg/workflows/sdk/v2/pb --> chainlink-common/pkg/values click chainlink-common/pkg/workflows/sdk/v2/pb href "https://github.com/smartcontractkit/chainlink-common" + chainlink-confidential-compute --> tdh2/go/tdh2 + click chainlink-confidential-compute href "https://github.com/smartcontractkit/chainlink-confidential-compute" + chainlink-confidential-compute/tests/testhelpers --> chainlink-confidential-compute + click chainlink-confidential-compute/tests/testhelpers href "https://github.com/smartcontractkit/chainlink-confidential-compute" chainlink-data-streams --> chainlink-common/keystore chainlink-data-streams --> chainlink-evm/gethwrappers click chainlink-data-streams href "https://github.com/smartcontractkit/chainlink-data-streams" @@ -513,13 +517,14 @@ flowchart LR chainlink/load-tests --> chainlink-testing-framework/havoc chainlink/load-tests --> chainlink/integration-tests click chainlink/load-tests href "https://github.com/smartcontractkit/chainlink" + chainlink/system-tests/lib --> chainlink-confidential-compute chainlink/system-tests/lib --> chainlink-testing-framework/framework/components/chiprouter chainlink/system-tests/lib --> chainlink-testing-framework/framework/components/dockercompose chainlink/system-tests/lib --> chainlink-testing-framework/framework/components/fake click chainlink/system-tests/lib href "https://github.com/smartcontractkit/chainlink" + chainlink/system-tests/tests --> chainlink-confidential-compute/tests/testhelpers + chainlink/system-tests/tests --> chainlink/core/scripts chainlink/system-tests/tests --> chainlink/core/scripts/cre/environment/examples/workflows/cron - chainlink/system-tests/tests --> chainlink/core/scripts/cre/environment/examples/workflows/proof-of-reserve/cron-based - chainlink/system-tests/tests --> chainlink/system-tests/lib chainlink/system-tests/tests --> chainlink/system-tests/tests/regression/cre/consensus chainlink/system-tests/tests --> chainlink/system-tests/tests/regression/cre/evm/evmread-negative chainlink/system-tests/tests --> chainlink/system-tests/tests/regression/cre/evm/evmwrite-negative @@ -724,6 +729,12 @@ flowchart LR end click chainlink-common-repo href "https://github.com/smartcontractkit/chainlink-common" + subgraph chainlink-confidential-compute-repo[chainlink-confidential-compute] + chainlink-confidential-compute + chainlink-confidential-compute/tests/testhelpers + end + click chainlink-confidential-compute-repo href "https://github.com/smartcontractkit/chainlink-confidential-compute" + subgraph chainlink-evm-repo[chainlink-evm] chainlink-evm chainlink-evm/contracts/cre/gobindings @@ -826,5 +837,5 @@ flowchart LR click testrig-repo href "https://github.com/smartcontractkit/testrig" classDef outline stroke-dasharray:6,fill:none; - class chainlink-repo,chainlink-aptos-repo,chainlink-ccip-repo,chainlink-ccv-repo,chainlink-common-repo,chainlink-evm-repo,chainlink-framework-repo,chainlink-protos-repo,chainlink-solana-repo,chainlink-stellar-repo,chainlink-sui-repo,chainlink-testing-framework-repo,chainlink-ton-repo,cre-sdk-go-repo,tdh2-repo,testrig-repo outline + class chainlink-repo,chainlink-aptos-repo,chainlink-ccip-repo,chainlink-ccv-repo,chainlink-common-repo,chainlink-confidential-compute-repo,chainlink-evm-repo,chainlink-framework-repo,chainlink-protos-repo,chainlink-solana-repo,chainlink-stellar-repo,chainlink-sui-repo,chainlink-testing-framework-repo,chainlink-ton-repo,cre-sdk-go-repo,tdh2-repo,testrig-repo outline ``` From fc007ba9da8a47e550da6de2d784f5e0342cc04b Mon Sep 17 00:00:00 2001 From: vreff <104409744+vreff@users.noreply.github.com> Date: Wed, 12 Aug 2026 13:58:40 -0400 Subject: [PATCH 08/29] fix tests --- system-tests/lib/go.sum | 4 ---- system-tests/tests/go.sum | 8 -------- 2 files changed, 12 deletions(-) diff --git a/system-tests/lib/go.sum b/system-tests/lib/go.sum index 32449127957..7b802008ead 100644 --- a/system-tests/lib/go.sum +++ b/system-tests/lib/go.sum @@ -1557,10 +1557,6 @@ github.com/smartcontractkit/chainlink-common/keystore v1.3.0 h1:V05Rp9/dTc4Wyips github.com/smartcontractkit/chainlink-common/keystore v1.3.0/go.mod h1:vHV8BGm6TN7jBbMsWxq1Hqm3HbCtYFwzvKS0CCczxG8= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72 h1:uWEwl7i2ryuRVoV4DmIKm6mqYevf1lH/8cQYhw/JXko= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72/go.mod h1:UYcRMb4dZcoaIPgZJ3hckCySTqtJc9K4Q+tOKErwTq0= -github.com/smartcontractkit/chainlink-confidential-compute v0.0.0-20260810192019-a945f9c2f628 h1:Od5bP3EapDpgodb3Clio5d5tWktysyzWH7R0em4/O7U= -github.com/smartcontractkit/chainlink-confidential-compute v0.0.0-20260810192019-a945f9c2f628/go.mod h1:fq9n85XoREIUxgdXIX3RyDIRCRyxZKZ5NCBbFDfdySo= -github.com/smartcontractkit/chainlink-confidential-compute v0.0.0-20260810204028-19c1e24d25ee h1:IW/mdtdrP2YvNq8Ua4Y2NeEuUUeJf4B3R5P9sX0VEHE= -github.com/smartcontractkit/chainlink-confidential-compute v0.0.0-20260810204028-19c1e24d25ee/go.mod h1:fq9n85XoREIUxgdXIX3RyDIRCRyxZKZ5NCBbFDfdySo= github.com/smartcontractkit/chainlink-confidential-compute v1.3.0 h1:y64hOzM9H61E4EYRzQDtRwqunDBlKpsZc1SvFEUlSLE= github.com/smartcontractkit/chainlink-confidential-compute v1.3.0/go.mod h1:fq9n85XoREIUxgdXIX3RyDIRCRyxZKZ5NCBbFDfdySo= github.com/smartcontractkit/chainlink-data-streams v1.1.0 h1:O5ngSwpwey7kQ3t4YgFiXzu3L3EvYiPS4c93gMWNRbk= diff --git a/system-tests/tests/go.sum b/system-tests/tests/go.sum index 2c7a28e18b6..6c7d9d9fa48 100644 --- a/system-tests/tests/go.sum +++ b/system-tests/tests/go.sum @@ -1765,16 +1765,8 @@ github.com/smartcontractkit/chainlink-common/keystore v1.3.0 h1:V05Rp9/dTc4Wyips github.com/smartcontractkit/chainlink-common/keystore v1.3.0/go.mod h1:vHV8BGm6TN7jBbMsWxq1Hqm3HbCtYFwzvKS0CCczxG8= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72 h1:uWEwl7i2ryuRVoV4DmIKm6mqYevf1lH/8cQYhw/JXko= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72/go.mod h1:UYcRMb4dZcoaIPgZJ3hckCySTqtJc9K4Q+tOKErwTq0= -github.com/smartcontractkit/chainlink-confidential-compute v0.0.0-20260810193839-ed12934f0671 h1:wq72FVGDxTMUXIuwYGF/8wTFPMUvs4f1vv8MMfo4G8k= -github.com/smartcontractkit/chainlink-confidential-compute v0.0.0-20260810193839-ed12934f0671/go.mod h1:fq9n85XoREIUxgdXIX3RyDIRCRyxZKZ5NCBbFDfdySo= -github.com/smartcontractkit/chainlink-confidential-compute v0.0.0-20260810204028-19c1e24d25ee h1:IW/mdtdrP2YvNq8Ua4Y2NeEuUUeJf4B3R5P9sX0VEHE= -github.com/smartcontractkit/chainlink-confidential-compute v0.0.0-20260810204028-19c1e24d25ee/go.mod h1:fq9n85XoREIUxgdXIX3RyDIRCRyxZKZ5NCBbFDfdySo= github.com/smartcontractkit/chainlink-confidential-compute v1.3.0 h1:y64hOzM9H61E4EYRzQDtRwqunDBlKpsZc1SvFEUlSLE= github.com/smartcontractkit/chainlink-confidential-compute v1.3.0/go.mod h1:fq9n85XoREIUxgdXIX3RyDIRCRyxZKZ5NCBbFDfdySo= -github.com/smartcontractkit/chainlink-confidential-compute/tests/testhelpers v0.0.0-20260810204028-19c1e24d25ee h1:Nub4a0ErgJkRnGO0wFxCA/xKuYnZiuKb4sFI4H7Ppsk= -github.com/smartcontractkit/chainlink-confidential-compute/tests/testhelpers v0.0.0-20260810204028-19c1e24d25ee/go.mod h1:Q5q/ohoAF5N7GXZS6QxgPWSaSLFtS6/53QHJbBUkCmU= -github.com/smartcontractkit/chainlink-confidential-compute/tests/testhelpers v0.0.0-20260811154131-b9926f013215 h1:YsxEx0pGUECqZZoed0fY+JFNrB8LsPRAKsFrPK0pQDw= -github.com/smartcontractkit/chainlink-confidential-compute/tests/testhelpers v0.0.0-20260811154131-b9926f013215/go.mod h1:Q5q/ohoAF5N7GXZS6QxgPWSaSLFtS6/53QHJbBUkCmU= github.com/smartcontractkit/chainlink-confidential-compute/tests/testhelpers v0.0.0-20260812145307-d77342c53d7d h1:CZ0Om7lANyhpFJcmZE8RwQpEP4PyvoNLhOJckjm+zao= github.com/smartcontractkit/chainlink-confidential-compute/tests/testhelpers v0.0.0-20260812145307-d77342c53d7d/go.mod h1:Q5q/ohoAF5N7GXZS6QxgPWSaSLFtS6/53QHJbBUkCmU= github.com/smartcontractkit/chainlink-data-streams v1.1.0 h1:O5ngSwpwey7kQ3t4YgFiXzu3L3EvYiPS4c93gMWNRbk= From d83822ad46181a6328ce496fd2d046a9e26a2026 Mon Sep 17 00:00:00 2001 From: vreff <104409744+vreff@users.noreply.github.com> Date: Wed, 12 Aug 2026 15:01:35 -0400 Subject: [PATCH 09/29] fix tests --- .github/workflows/cre-system-tests.yaml | 11 +++++++++++ .../tests/smoke/cre/confidential_workflows_env.go | 8 +++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cre-system-tests.yaml b/.github/workflows/cre-system-tests.yaml index f3faab63f72..a716dac5bb5 100644 --- a/.github/workflows/cre-system-tests.yaml +++ b/.github/workflows/cre-system-tests.yaml @@ -370,6 +370,9 @@ jobs: - name: Start local CRE${{ matrix.tests.cre_version }} id: start-local-cre + # The confidential workflows test starts the environment in-process, so it + # can pass runtime-computed capabilities and features as Go values. + if: ${{ !contains(matrix.tests.test_name, 'ConfidentialWorkflows') }} uses: ./.github/actions/start-local-cre-environment with: jd-image: @@ -406,6 +409,14 @@ jobs: RUN_QUARANTINED_TESTS: "true" # always run quarantined tests in CI TOPOLOGY_NAME: ${{ matrix.tests.topology }} CONFIDENTIAL_COMPUTE_ROOT: ${{ github.workspace }}/chainlink-confidential-compute + # Image overrides for tests that start the environment in-process; the + # start-local-cre-environment action sets these for every other test. + CTF_JD_IMAGE: + "${{ secrets.AWS_ACCOUNT_ID_PROD }}.dkr.ecr.${{ secrets.QA_AWS_REGION + }}.amazonaws.com/job-distributor:0.28.0" + CTF_CHAINLINK_IMAGE: "${{ env.CHAINLINK_IMAGE_FULL }}" + CTF_CHIP_ROUTER_IMAGE: "${{ secrets.QA_AWS_ACCOUNT_NUMBER }}.dkr.ecr.${{ + secrets.QA_AWS_REGION }}.amazonaws.com/local-cre-chip-router:v1.0.1" GITHUB_TOKEN: ${{ steps.github-token.outputs.access-token || '' }} # to avoid rate limiting when downloading protobuf files from GitHub PARALLEL_COUNT: "10" CRE_TEST_PARALLEL_ENABLED: "true" diff --git a/system-tests/tests/smoke/cre/confidential_workflows_env.go b/system-tests/tests/smoke/cre/confidential_workflows_env.go index ff815bb09aa..c1dbed28a10 100644 --- a/system-tests/tests/smoke/cre/confidential_workflows_env.go +++ b/system-tests/tests/smoke/cre/confidential_workflows_env.go @@ -81,7 +81,13 @@ func startConfidentialCreEnvironment( // Config.Validate rejects capability flags the built-in provider doesn't know, // and confidential-workflows / confidential-relay are not among them. Extend // the provider with every flag this run declares. - extraFlags := []string{string(crelib.ConfidentialRelayCapability), confidentialWorkflowsApp} + // don-time is absent from the extensible provider's built-in globals, unlike + // the default provider's, so the topology's use of it must be declared here. + extraFlags := []string{ + string(crelib.ConfidentialRelayCapability), + confidentialWorkflowsApp, + string(crelib.DONTimeCapability), + } for _, c := range extraCapabilities { extraFlags = append(extraFlags, c.Flag()) } From 230ae4bf7f56353e90e18271d81019f7070ddb40 Mon Sep 17 00:00:00 2001 From: vreff <104409744+vreff@users.noreply.github.com> Date: Wed, 12 Aug 2026 15:26:48 -0400 Subject: [PATCH 10/29] fix test --- .../smoke/cre/confidential_workflows_env.go | 30 +++++++------------ 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/system-tests/tests/smoke/cre/confidential_workflows_env.go b/system-tests/tests/smoke/cre/confidential_workflows_env.go index c1dbed28a10..c05507ed9b7 100644 --- a/system-tests/tests/smoke/cre/confidential_workflows_env.go +++ b/system-tests/tests/smoke/cre/confidential_workflows_env.go @@ -42,11 +42,10 @@ const ( // confidentialCleanupWait matches the CLI's own cleanup grace period. confidentialCleanupWait = 15 * time.Second - // These are resolved against the CRE environment directory rather than used - // as-is: the CLI defaults assume a working directory of - // core/scripts/cre/environment, but this test runs from smoke/cre. + // Resolved against the CRE environment directory rather than used as-is: the + // CLI defaults assume a working directory of core/scripts/cre/environment, + // but this test runs from smoke/cre. confidentialCapabilityDefaultsConfig = "configs/capability_defaults.toml" - confidentialSetupConfig = "configs/setup.toml" ) // startConfidentialCreEnvironment starts a local CRE environment with extra @@ -60,7 +59,7 @@ func startConfidentialCreEnvironment( extraAllowedPorts []int, extraFeatures ...crelib.Feature, ) error { - in, err := confidentialPreConfigure(ctx, relativePathToRepoRoot, environmentDirPath) + in, err := confidentialPreConfigure(relativePathToRepoRoot, environmentDirPath) if err != nil { return err } @@ -149,9 +148,13 @@ func startConfidentialCreEnvironment( } // confidentialPreConfigure clears any prior environment state and loads the -// topology config. Purging before RunSetup matters: a stale state file from a -// different topology would otherwise be merged into this run. -func confidentialPreConfigure(ctx context.Context, relativePathToRepoRoot, environmentDirPath string) (*envconfig.Config, error) { +// topology config, so a stale state file from a different topology is not +// merged into this run. +// +// This deliberately skips crescriptenv.RunSetup, matching `cre env start`, +// which only runs setup under --auto-setup (off by default, and unset in CI). +// Setup installs host tooling such as Bun, which is unavailable on CI runners. +func confidentialPreConfigure(relativePathToRepoRoot, environmentDirPath string) (*envconfig.Config, error) { _ = stopConfidentialCreEnvironment(relativePathToRepoRoot) if err := framework.RemoveTestContainers(); err != nil { @@ -177,17 +180,6 @@ func confidentialPreConfigure(ctx context.Context, relativePathToRepoRoot, envir return nil, fmt.Errorf("failed to set CTF_CONFIGS: %w", err) } - if setupErr := crescriptenv.RunSetup( - ctx, - crescriptenv.SetupConfig{ConfigPath: filepath.Join(environmentDirPath, confidentialSetupConfig)}, - true, // noPrompt - false, // purge - false, // withBilling - relativePathToRepoRoot, - ); setupErr != nil { - return nil, errors.Wrap(setupErr, "failed to run setup") - } - if pkErr := creenv.SetDefaultPrivateKeyIfEmpty(blockchain.DefaultAnvilPrivateKey); pkErr != nil { return nil, errors.Wrap(pkErr, "failed to set default private key") } From e3798b5b2f6d5bc4dfa8cbe7fad4a48b322dfb06 Mon Sep 17 00:00:00 2001 From: vreff <104409744+vreff@users.noreply.github.com> Date: Wed, 12 Aug 2026 15:57:50 -0400 Subject: [PATCH 11/29] consoldiate CI --- .github/workflows/cre-system-tests.yaml | 5 +---- system-tests/lib/cre/flags/provider.go | 4 ++++ system-tests/lib/cre/types.go | 6 ++++-- .../tests/smoke/cre/confidential_workflows_env.go | 14 ++++---------- .../cre/confidential_workflows_test_helpers.go | 2 +- 5 files changed, 14 insertions(+), 17 deletions(-) diff --git a/.github/workflows/cre-system-tests.yaml b/.github/workflows/cre-system-tests.yaml index a716dac5bb5..7f85b0b6484 100644 --- a/.github/workflows/cre-system-tests.yaml +++ b/.github/workflows/cre-system-tests.yaml @@ -118,7 +118,7 @@ jobs: {"topology":"workflow-gateway-capabilities-multi-gateway","configs":"configs/workflow-gateway-capabilities-multi-gateway-don.toml"} ], "Test_CRE_V2_ConfidentialWorkflows_Relay": [ - {"topology":"workflow-gateway-capabilities-confidential-workflows","configs":"configs/workflow-gateway-capabilities-don-confidential-workflows.toml","timeout_minutes":25,"test_timeout":"20m"} + {"topology":"workflow-gateway-capabilities-confidential-workflows","configs":"configs/workflow-gateway-capabilities-don-confidential-workflows.toml"} ] }' @@ -370,9 +370,6 @@ jobs: - name: Start local CRE${{ matrix.tests.cre_version }} id: start-local-cre - # The confidential workflows test starts the environment in-process, so it - # can pass runtime-computed capabilities and features as Go values. - if: ${{ !contains(matrix.tests.test_name, 'ConfidentialWorkflows') }} uses: ./.github/actions/start-local-cre-environment with: jd-image: diff --git a/system-tests/lib/cre/flags/provider.go b/system-tests/lib/cre/flags/provider.go index 54fc0839c47..e045aef025d 100644 --- a/system-tests/lib/cre/flags/provider.go +++ b/system-tests/lib/cre/flags/provider.go @@ -19,6 +19,8 @@ func NewDefaultCapabilityFlagsProvider() *DefaultCapbilityFlagsProvider { cre.EVMCapability, cre.AptosCapability, cre.StellarCapability, + cre.ConfidentialRelayCapability, + cre.ConfidentialWorkflowsCapability, }, } } @@ -40,6 +42,8 @@ func NewExtensibleCapabilityFlagsProvider(extraGlobalFlags []string) *Extensible cre.VaultCapability, cre.HTTPTriggerCapability, cre.HTTPActionCapability, + cre.ConfidentialRelayCapability, + cre.ConfidentialWorkflowsCapability, }, extraGlobalFlags...), chainSpecificCapabilities: []cre.CapabilityFlag{ cre.EVMCapability, diff --git a/system-tests/lib/cre/types.go b/system-tests/lib/cre/types.go index 2ce0687d527..436c41d741d 100644 --- a/system-tests/lib/cre/types.go +++ b/system-tests/lib/cre/types.go @@ -66,8 +66,10 @@ const ( HTTPActionCapability CapabilityFlag = "http-action" SolanaCapability CapabilityFlag = "solana" ConfidentialRelayCapability CapabilityFlag = "confidential-relay" - AptosCapability CapabilityFlag = "aptos" - StellarCapability CapabilityFlag = "stellar" + // ConfidentialWorkflowsCapability doubles as the enclave application name. + ConfidentialWorkflowsCapability CapabilityFlag = "confidential-workflows" + AptosCapability CapabilityFlag = "aptos" + StellarCapability CapabilityFlag = "stellar" // Add more capabilities as needed ) diff --git a/system-tests/tests/smoke/cre/confidential_workflows_env.go b/system-tests/tests/smoke/cre/confidential_workflows_env.go index c05507ed9b7..f70def9d44d 100644 --- a/system-tests/tests/smoke/cre/confidential_workflows_env.go +++ b/system-tests/tests/smoke/cre/confidential_workflows_env.go @@ -77,16 +77,10 @@ func startConfidentialCreEnvironment( } } - // Config.Validate rejects capability flags the built-in provider doesn't know, - // and confidential-workflows / confidential-relay are not among them. Extend - // the provider with every flag this run declares. - // don-time is absent from the extensible provider's built-in globals, unlike - // the default provider's, so the topology's use of it must be declared here. - extraFlags := []string{ - string(crelib.ConfidentialRelayCapability), - confidentialWorkflowsApp, - string(crelib.DONTimeCapability), - } + // Config.Validate rejects capability flags the provider doesn't know, so every + // flag this run declares has to be present. don-time is absent from the + // extensible provider's built-in globals, unlike the default provider's. + extraFlags := []string{string(crelib.DONTimeCapability)} for _, c := range extraCapabilities { extraFlags = append(extraFlags, c.Flag()) } diff --git a/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go b/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go index f26f2a01cbf..5039ea6e739 100644 --- a/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go +++ b/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go @@ -46,7 +46,7 @@ import ( const ( // confidentialWorkflowsApp is the enclave application, the DON capability flag // and the capability binary name; all three share this value. - confidentialWorkflowsApp = "confidential-workflows" + confidentialWorkflowsApp = string(crelib.ConfidentialWorkflowsCapability) // confidentialWorkflowsCapVersion is the version the capability registers under. confidentialWorkflowsCapVersion = "1.0.0-alpha" From 95caf435f2fca29ecaf62f9e4d794540a439929c Mon Sep 17 00:00:00 2001 From: vreff <104409744+vreff@users.noreply.github.com> Date: Wed, 12 Aug 2026 16:34:13 -0400 Subject: [PATCH 12/29] test updates --- .github/workflows/cre-system-tests.yaml | 12 +-- ...pabilities-don-confidential-workflows.toml | 7 ++ .../environment/environment/environment.go | 32 ++++--- .../confidentialcompute.go | 62 ++++++++++++- .../confidentialcompute_test.go | 81 +++++++++++++++++ .../confidentialrelay/confidentialrelay.go | 17 +++- system-tests/lib/cre/features/sets/sets.go | 2 + .../smoke/cre/confidential_workflows_env.go | 20 +---- .../smoke/cre/confidential_workflows_test.go | 10 +-- .../confidential_workflows_test_helpers.go | 87 ------------------- 10 files changed, 200 insertions(+), 130 deletions(-) create mode 100644 system-tests/lib/cre/capabilities/confidentialcompute/confidentialcompute_test.go diff --git a/.github/workflows/cre-system-tests.yaml b/.github/workflows/cre-system-tests.yaml index 7f85b0b6484..fccfdf2537b 100644 --- a/.github/workflows/cre-system-tests.yaml +++ b/.github/workflows/cre-system-tests.yaml @@ -162,9 +162,7 @@ jobs: # http://docs.github.com/en/actions/how-tos/deploy/configure-and-manage-deployments/control-deployments#using-environments-without-deployments name: integration deployment: false - # Most legs finish well inside 10 minutes; legs that stand up extra - # infrastructure (e.g. local enclaves) declare their own budget in the matrix. - timeout-minutes: ${{ matrix.tests.timeout_minutes || 10 }} + timeout-minutes: 10 env: ENABLE_AUTO_QUARANTINE: "true" BILLING_PLATFORM_SERVICE_IMAGE: @@ -370,6 +368,11 @@ jobs: - name: Start local CRE${{ matrix.tests.cre_version }} id: start-local-cre + # This test starts the environment in-process, to pass runtime-computed + # capabilities and features as Go values, and tears down any environment + # already running. Starting one here is torn down and rebuilt, which + # leaves the vault DON unable to publish its public key in time. + if: ${{ !contains(matrix.tests.test_name, 'ConfidentialWorkflows') }} uses: ./.github/actions/start-local-cre-environment with: jd-image: @@ -401,8 +404,7 @@ jobs: continue-on-error: ${{ env.ENABLE_AUTO_QUARANTINE == 'true' }} env: TEST_NAME: ${{ matrix.tests.test_name }} - # Leave ~3 minutes for the other steps in the job's timeout budget. - TEST_TIMEOUT: ${{ matrix.tests.test_timeout || '7m' }} + TEST_TIMEOUT: 7m # let's leave 3 minutes for other steps (the whole job times out after 10 minutes) RUN_QUARANTINED_TESTS: "true" # always run quarantined tests in CI TOPOLOGY_NAME: ${{ matrix.tests.topology }} CONFIDENTIAL_COMPUTE_ROOT: ${{ github.workspace }}/chainlink-confidential-compute diff --git a/core/scripts/cre/environment/configs/workflow-gateway-capabilities-don-confidential-workflows.toml b/core/scripts/cre/environment/configs/workflow-gateway-capabilities-don-confidential-workflows.toml index 2eed9a57731..925598cd8de 100644 --- a/core/scripts/cre/environment/configs/workflow-gateway-capabilities-don-confidential-workflows.toml +++ b/core/scripts/cre/environment/configs/workflow-gateway-capabilities-don-confidential-workflows.toml @@ -13,6 +13,13 @@ # is built from a chainlink-confidential-compute checkout and mounted at # ./binaries/confidential-workflows (see .github/workflows/cre-system-tests.yaml). +# Fake enclaves emit a sentinel attestation document rather than real PCRs, so +# attestation validation is relaxed. INSECURE; for tests only. +[capability_configs.confidential-relay] + [capability_configs.confidential-relay.values] + trustEnclaves = true + requireBFTQuorum = true + [chip_router] image = "local-cre-chip-router:v1.0.1" diff --git a/core/scripts/cre/environment/environment/environment.go b/core/scripts/cre/environment/environment/environment.go index 810ec3a9329..60bfcd5c513 100644 --- a/core/scripts/cre/environment/environment/environment.go +++ b/core/scripts/cre/environment/environment/environment.go @@ -363,18 +363,7 @@ func startCmd() *cobra.Command { } features := feature_set.New() - extraAllowedPorts := append([]int(nil), extraAllowedGatewayPorts...) - if in.Fake != nil { - extraAllowedPorts = append(extraAllowedPorts, in.Fake.Port) - } - if in.FakeHTTP != nil { - extraAllowedPorts = append(extraAllowedPorts, in.FakeHTTP.Port) - } - - gatewayWhitelistConfig := gateway.WhitelistConfig{ - ExtraAllowedPorts: extraAllowedPorts, - ExtraAllowedIPsCIDR: []string{"0.0.0.0/0"}, - } + gatewayWhitelistConfig := DefaultGatewayWhitelistConfig(in, extraAllowedGatewayPorts) output, startErr := StartCLIEnvironment(cmdContext, relativePathToRepoRoot, in, nil, features, nil, envDependencies, gatewayWhitelistConfig) if startErr != nil { fmt.Fprintf(os.Stderr, "Error: %s\n", startErr) @@ -855,6 +844,25 @@ func statusCmd() *cobra.Command { return cmd } +// DefaultGatewayWhitelistConfig builds the Gateway Connector's outbound allowlist: +// the caller's extra ports plus the fake service ports the config declares. Shared +// by `cre env start` and by tests that call StartCLIEnvironment directly, so both +// grant the same access. +func DefaultGatewayWhitelistConfig(in *envconfig.Config, extraAllowedPorts []int) gateway.WhitelistConfig { + ports := append([]int(nil), extraAllowedPorts...) + if in.Fake != nil { + ports = append(ports, in.Fake.Port) + } + if in.FakeHTTP != nil { + ports = append(ports, in.FakeHTTP.Port) + } + + return gateway.WhitelistConfig{ + ExtraAllowedPorts: ports, + ExtraAllowedIPsCIDR: []string{"0.0.0.0/0"}, + } +} + func StartCLIEnvironment( cmdContext context.Context, relativePathToRepoRoot string, diff --git a/system-tests/lib/cre/capabilities/confidentialcompute/confidentialcompute.go b/system-tests/lib/cre/capabilities/confidentialcompute/confidentialcompute.go index 89b62b30650..2461ec4be9c 100644 --- a/system-tests/lib/cre/capabilities/confidentialcompute/confidentialcompute.go +++ b/system-tests/lib/cre/capabilities/confidentialcompute/confidentialcompute.go @@ -183,16 +183,74 @@ func workflowEncryptionKey(workerNode *cre.Node) ([32]byte, error) { return publicKey, nil } +// EnclavesConfigKey is the capability config value holding a JSON array of +// enclaves, letting a topology declare them instead of passing Go values. +const EnclavesConfigKey = "enclaves" + +// MarshalEnclaves encodes an enclave list for EnclavesConfigKey, for callers +// that discover their enclaves at runtime and hand them to the environment as +// configuration. +func MarshalEnclaves(enclaves []cctypes.Enclave) (string, error) { + encoded, err := json.Marshal(enclaves) + if err != nil { + return "", errors.Wrap(err, "failed to marshal enclave list") + } + + return string(encoded), nil +} + +// EnclavesFromConfig reads the enclave list a topology declared for the named +// capability. Returns nil when the capability has no config or no enclaves key. +func EnclavesFromConfig(nodeSet *cre.NodeSet, name string) ([]cctypes.Enclave, error) { + if nodeSet == nil { + return nil, nil + } + + capConfig, ok := nodeSet.CapabilityConfigs[name] + if !ok || capConfig.Values == nil { + return nil, nil + } + + raw, ok := capConfig.Values[EnclavesConfigKey] + if !ok { + return nil, nil + } + + encoded, ok := raw.(string) + if !ok { + return nil, errors.Errorf("capability %q: %q must be a JSON string, got %T", name, EnclavesConfigKey, raw) + } + + var enclaves []cctypes.Enclave + if err := json.Unmarshal([]byte(encoded), &enclaves); err != nil { + return nil, errors.Wrapf(err, "capability %q: failed to parse %q", name, EnclavesConfigKey) + } + + return enclaves, nil +} + // registryConfigFn writes the enclave list into the capability's on-chain // registry config, which is how the confidential relay handler discovers the // enclaves it may route to. func registryConfigFn(name string, version string, enclaves []cctypes.Enclave) cre.CapabilityRegistryConfigFn { - return func(donFlags []string, _ *cre.NodeSet) ([]keystone_changeset.DONCapabilityWithConfig, error) { + return func(donFlags []string, nodeSet *cre.NodeSet) ([]keystone_changeset.DONCapabilityWithConfig, error) { if !flags.HasFlag(donFlags, name) { return nil, nil } - wrappedConfig, err := values.WrapMap(cctypes.EnclavesList{Enclaves: enclaves}) + // Go-supplied enclaves win; otherwise use whatever the topology declared, + // so callers that only learn their enclaves at runtime and callers that + // can configure them up front are both served. + list := enclaves + if len(list) == 0 { + fromConfig, cErr := EnclavesFromConfig(nodeSet, name) + if cErr != nil { + return nil, cErr + } + list = fromConfig + } + + wrappedConfig, err := values.WrapMap(cctypes.EnclavesList{Enclaves: list}) if err != nil { return nil, errors.Wrap(err, "failed to wrap enclave list config") } diff --git a/system-tests/lib/cre/capabilities/confidentialcompute/confidentialcompute_test.go b/system-tests/lib/cre/capabilities/confidentialcompute/confidentialcompute_test.go new file mode 100644 index 00000000000..068a8dcb888 --- /dev/null +++ b/system-tests/lib/cre/capabilities/confidentialcompute/confidentialcompute_test.go @@ -0,0 +1,81 @@ +package confidentialcompute + +import ( + "testing" + + "github.com/stretchr/testify/require" + + cctypes "github.com/smartcontractkit/chainlink-confidential-compute/types" + + "github.com/smartcontractkit/chainlink/system-tests/lib/cre" +) + +func nodeSetWithValues(name string, values map[string]any) *cre.NodeSet { + return &cre.NodeSet{ + CapabilityConfigs: map[cre.CapabilityFlag]cre.CapabilityConfig{ + cre.CapabilityFlag(name): {Values: values}, + }, + } +} + +func TestEnclavesFromConfig(t *testing.T) { + const name = "confidential-workflows" + + t.Run("parses declared enclaves", func(t *testing.T) { + ns := nodeSetWithValues(name, map[string]any{ + EnclavesConfigKey: `[{"enclaveURL":"http://10.0.0.1:8080","enclaveAuthHeader":"key-a"},` + + `{"enclaveURL":"http://10.0.0.1:8081"}]`, + }) + + got, err := EnclavesFromConfig(ns, name) + require.NoError(t, err) + require.Len(t, got, 2) + require.Equal(t, "http://10.0.0.1:8080", got[0].EnclaveURL) + require.Equal(t, "key-a", got[0].EnclaveAuthHeader) + require.Equal(t, "http://10.0.0.1:8081", got[1].EnclaveURL) + }) + + t.Run("round-trips a marshalled enclave list", func(t *testing.T) { + encoded, err := MarshalEnclaves([]cctypes.Enclave{{ + EnclaveURL: "http://10.0.0.2:8080", + TrustedValues: [][]byte{[]byte("fake-measurements")}, + Region: "us-west-2", + }}) + require.NoError(t, err) + + got, err := EnclavesFromConfig(nodeSetWithValues(name, map[string]any{EnclavesConfigKey: encoded}), name) + require.NoError(t, err) + require.Len(t, got, 1) + require.Equal(t, "http://10.0.0.2:8080", got[0].EnclaveURL) + require.Equal(t, "us-west-2", got[0].Region) + require.Equal(t, [][]byte{[]byte("fake-measurements")}, got[0].TrustedValues) + }) + + t.Run("returns nil when unconfigured", func(t *testing.T) { + for _, tc := range []struct { + desc string + nodeSet *cre.NodeSet + }{ + {"nil node set", nil}, + {"no values", nodeSetWithValues(name, nil)}, + {"no enclaves key", nodeSetWithValues(name, map[string]any{"other": "x"})}, + {"different capability", nodeSetWithValues("confidential-http", map[string]any{EnclavesConfigKey: "[]"})}, + } { + t.Run(tc.desc, func(t *testing.T) { + got, err := EnclavesFromConfig(tc.nodeSet, name) + require.NoError(t, err) + require.Nil(t, got) + }) + } + }) + + t.Run("errors on a non-string value", func(t *testing.T) { + _, err := EnclavesFromConfig(nodeSetWithValues(name, map[string]any{EnclavesConfigKey: 42}), name) + require.ErrorContains(t, err, "must be a JSON string") + }) + + t.Run("errors on malformed JSON", func(t *testing.T) { + _, err := EnclavesFromConfig(nodeSetWithValues(name, map[string]any{EnclavesConfigKey: "{not json"}), name) + require.ErrorContains(t, err, "failed to parse") + }) +} diff --git a/system-tests/lib/cre/features/confidentialrelay/confidentialrelay.go b/system-tests/lib/cre/features/confidentialrelay/confidentialrelay.go index 8d50f800552..8783ce6fb6b 100644 --- a/system-tests/lib/cre/features/confidentialrelay/confidentialrelay.go +++ b/system-tests/lib/cre/features/confidentialrelay/confidentialrelay.go @@ -29,6 +29,19 @@ func (o *ConfidentialRelay) Flag() cre.CapabilityFlag { return flag } +// boolFromValues reads a bool from a capability config's values, falling back to +// def when the key is absent or not a bool. Lets a topology set the relay's knobs +// in TOML rather than requiring a Go-constructed feature. +func boolFromValues(values map[string]any, key string, def bool) bool { + if v, ok := values[key]; ok { + if b, isBool := v.(bool); isBool { + return b + } + } + + return def +} + func (o *ConfidentialRelay) PreEnvStartup( ctx context.Context, testLogger zerolog.Logger, @@ -66,8 +79,8 @@ func (o *ConfidentialRelay) PreEnvStartup( } enabled := true - trustEnclaves := o.TrustEnclaves - requireBFTQuorum := o.RequireBFTQuorum + trustEnclaves := boolFromValues(capConfig.Values, "trustEnclaves", o.TrustEnclaves) + requireBFTQuorum := boolFromValues(capConfig.Values, "requireBFTQuorum", o.RequireBFTQuorum) typedConfig.CRE.ConfidentialRelay = &coretoml.ConfidentialRelayConfig{ Enabled: &enabled, TrustEnclaves: &trustEnclaves, diff --git a/system-tests/lib/cre/features/sets/sets.go b/system-tests/lib/cre/features/sets/sets.go index d0037f4d16c..717ee512fa6 100644 --- a/system-tests/lib/cre/features/sets/sets.go +++ b/system-tests/lib/cre/features/sets/sets.go @@ -3,6 +3,7 @@ package sets import ( "github.com/smartcontractkit/chainlink/system-tests/lib/cre" aptos_feature "github.com/smartcontractkit/chainlink/system-tests/lib/cre/features/aptos" + confidential_relay_feature "github.com/smartcontractkit/chainlink/system-tests/lib/cre/features/confidentialrelay" consensus_feature "github.com/smartcontractkit/chainlink/system-tests/lib/cre/features/consensus/v2" cron_feature "github.com/smartcontractkit/chainlink/system-tests/lib/cre/features/cron" don_time_feature "github.com/smartcontractkit/chainlink/system-tests/lib/cre/features/don_time" @@ -26,5 +27,6 @@ func New() cre.Features { &solana_feature.Solana{}, &stellar_feature.Stellar{}, &vault_feature.Vault{}, + &confidential_relay_feature.ConfidentialRelay{}, ) } diff --git a/system-tests/tests/smoke/cre/confidential_workflows_env.go b/system-tests/tests/smoke/cre/confidential_workflows_env.go index f70def9d44d..f02d5c1db6e 100644 --- a/system-tests/tests/smoke/cre/confidential_workflows_env.go +++ b/system-tests/tests/smoke/cre/confidential_workflows_env.go @@ -16,7 +16,6 @@ import ( crescriptenv "github.com/smartcontractkit/chainlink/core/scripts/cre/environment/environment" crelib "github.com/smartcontractkit/chainlink/system-tests/lib/cre" - gateway "github.com/smartcontractkit/chainlink/system-tests/lib/cre/don/gateway" creenv "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment" envconfig "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment/config" feature_sets "github.com/smartcontractkit/chainlink/system-tests/lib/cre/features/sets" @@ -57,7 +56,6 @@ func startConfidentialCreEnvironment( environmentDirPath string, extraCapabilities []crelib.InstallableCapability, extraAllowedPorts []int, - extraFeatures ...crelib.Feature, ) error { in, err := confidentialPreConfigure(relativePathToRepoRoot, environmentDirPath) if err != nil { @@ -84,9 +82,6 @@ func startConfidentialCreEnvironment( for _, c := range extraCapabilities { extraFlags = append(extraFlags, c.Flag()) } - for _, f := range extraFeatures { - extraFlags = append(extraFlags, string(f.Flag())) - } envDependencies := crelib.NewEnvironmentDependencies( flags.NewExtensibleCapabilityFlagsProvider(extraFlags), @@ -96,19 +91,12 @@ func startConfidentialCreEnvironment( return errors.Wrap(err, "failed to validate environment configuration") } - // Start from the default feature set and add the test's own features, so the - // relay feature does not have to be registered globally in features/sets. + // The standard feature set carries the confidential relay; the topology's + // capability config supplies its settings. features := feature_sets.New() - for _, f := range extraFeatures { - features.Add(f) - } - allowedPorts := append([]int{in.Fake.Port, in.FakeHTTP.Port}, extraAllowedPorts...) - gatewayWhitelistConfig := gateway.WhitelistConfig{ - ExtraAllowedPorts: allowedPorts, - // The enclaves reach the gateway from outside the Docker network. - ExtraAllowedIPsCIDR: []string{"0.0.0.0/0"}, - } + // Same allowlist `cre env start` grants, plus this test's enclave ports. + gatewayWhitelistConfig := crescriptenv.DefaultGatewayWhitelistConfig(in, extraAllowedPorts) output, startErr := crescriptenv.StartCLIEnvironment( ctx, diff --git a/system-tests/tests/smoke/cre/confidential_workflows_test.go b/system-tests/tests/smoke/cre/confidential_workflows_test.go index 5c8709dac23..8ce92baa875 100644 --- a/system-tests/tests/smoke/cre/confidential_workflows_test.go +++ b/system-tests/tests/smoke/cre/confidential_workflows_test.go @@ -113,18 +113,16 @@ func Test_CRE_V2_ConfidentialWorkflows_Relay(t *testing.T) { ) require.NoError(t, err, "failed to build confidential-workflows capability") - relayFeature := newTestConfidentialRelayFeature(t, enclaves.Enclaves, fake) - - // 4. Start the CRE environment in-process so the capability and feature - // above can be injected, then let the standard helper build the test - // environment from the state file it wrote. + // 4. Start the CRE environment in-process so the capability above can be + // injected, then let the standard helper build the test environment + // from the state file it wrote. The confidential relay feature comes + // from the standard feature set, configured by the topology TOML. require.NoError(t, startConfidentialCreEnvironment( t.Context(), tconf.RelativePathToRepoRoot, tconf.EnvironmentDirPath, []crelib.InstallableCapability{cap}, confidentialEnclavePorts(t, enclaves), - relayFeature, ), "failed to start confidential CRE environment") testEnv := t_helpers.SetupTestEnvironmentWithConfig(t, tconf) diff --git a/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go b/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go index 5039ea6e739..6e6d1b46b28 100644 --- a/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go +++ b/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go @@ -38,7 +38,6 @@ import ( crelib "github.com/smartcontractkit/chainlink/system-tests/lib/cre" crecontracts "github.com/smartcontractkit/chainlink/system-tests/lib/cre/contracts" "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment/blockchains/evm" - "github.com/smartcontractkit/chainlink/system-tests/lib/cre/features/confidentialrelay" ttypes "github.com/smartcontractkit/chainlink/system-tests/tests/test-helpers/configuration" "github.com/smartcontractkit/chainlink/v2/core/capabilities/vault/vaultutils" ) @@ -191,92 +190,6 @@ func startFakeStorageService(t *testing.T, enclaveHost string) (string, *fakeSto return fmt.Sprintf("%s:%d", enclaveHost, lis.Addr().(*net.TCPAddr).Port), svc } -// --------------------------------------------------------------------------- -// Confidential relay feature wrapper -// --------------------------------------------------------------------------- - -// testConfidentialRelayFeature wraps the real ConfidentialRelay feature and -// injects trusted measurements into the DON's capability config before -// PreEnvStartup runs, so the relay handler will accept attestations from the -// enclaves this test started. -type testConfidentialRelayFeature struct { - inner confidentialrelay.ConfidentialRelay - pcrsJSON string -} - -func (f *testConfidentialRelayFeature) Flag() crelib.CapabilityFlag { - return f.inner.Flag() -} - -func (f *testConfidentialRelayFeature) PreEnvStartup( - ctx context.Context, - testLogger zerolog.Logger, - don *crelib.DonMetadata, - topology *crelib.Topology, - creEnv *crelib.Environment, -) (*crelib.PreEnvStartupOutput, error) { - if don.CapabilityConfigs == nil { - don.CapabilityConfigs = make(map[crelib.CapabilityFlag]crelib.CapabilityConfig) - } - cfg, ok := don.CapabilityConfigs[crelib.ConfidentialRelayCapability] - if !ok { - cfg = crelib.CapabilityConfig{Values: make(map[string]any)} - } - if cfg.Values == nil { - cfg.Values = make(map[string]any) - } - cfg.Values["trustedPCRs"] = f.pcrsJSON - don.CapabilityConfigs[crelib.ConfidentialRelayCapability] = cfg - - return f.inner.PreEnvStartup(ctx, testLogger, don, topology, creEnv) -} - -func (f *testConfidentialRelayFeature) PostEnvStartup( - ctx context.Context, - testLogger zerolog.Logger, - don *crelib.Don, - dons *crelib.Dons, - creEnv *crelib.Environment, -) error { - return f.inner.PostEnvStartup(ctx, testLogger, don, dons, creEnv) -} - -// newTestConfidentialRelayFeature builds the relay feature for a set of enclaves. -// Fake enclaves emit a sentinel attestation document instead of real PCRs, so the -// trusted value is the fake measurements placeholder and attestation validation is -// relaxed. Real Nitro enclaves keep full validation against their measurements. -func newTestConfidentialRelayFeature(t *testing.T, enclaves []cctypes.Enclave, fake bool) crelib.Feature { - t.Helper() - - var pcrsJSON string - if fake { - // Marshaling the raw "fake-measurements" bytes as json.RawMessage would fail - // since it is not valid JSON, so encode it as a JSON string array. - b, err := json.Marshal([]string{cctypes.FakeMeasurements}) - require.NoError(t, err, "failed to marshal fake measurements") - pcrsJSON = string(b) - } else { - // Each enclave bakes in per-CID WireGuard keys, so measurements differ per - // enclave. The relay accepts a JSON array and tries each until one matches. - var allPCRs []json.RawMessage - for _, enc := range enclaves { - for _, tv := range enc.TrustedValues { - if string(tv) != "invalid" { - allPCRs = append(allPCRs, json.RawMessage(tv)) - } - } - } - b, err := json.Marshal(allPCRs) - require.NoError(t, err, "failed to marshal PCR measurements") - pcrsJSON = string(b) - } - - return crelib.Feature(&testConfidentialRelayFeature{ - inner: confidentialrelay.ConfidentialRelay{TrustEnclaves: fake, RequireBFTQuorum: true}, - pcrsJSON: pcrsJSON, - }) -} - // --------------------------------------------------------------------------- // Workflow artifacts // --------------------------------------------------------------------------- From ed40599552958a275d8380a48d4dc915c391dd5d Mon Sep 17 00:00:00 2001 From: vreff <104409744+vreff@users.noreply.github.com> Date: Wed, 12 Aug 2026 17:07:10 -0400 Subject: [PATCH 13/29] fix tests --- .github/workflows/cre-system-tests.yaml | 8 +- ...pabilities-don-confidential-workflows.toml | 8 + core/scripts/go.mod | 1 + core/scripts/go.sum | 2 + .../confidentialcompute.go | 241 +++++++----------- .../confidentialworkflows.go | 155 +++++++++++ system-tests/lib/cre/features/sets/sets.go | 2 + system-tests/lib/cre/registry_update.go | 76 ++++++ .../smoke/cre/confidential_workflows_env.go | 196 -------------- .../smoke/cre/confidential_workflows_test.go | 102 +++++--- .../confidential_workflows_test_helpers.go | 11 +- 11 files changed, 413 insertions(+), 389 deletions(-) create mode 100644 system-tests/lib/cre/features/confidentialworkflows/confidentialworkflows.go create mode 100644 system-tests/lib/cre/registry_update.go delete mode 100644 system-tests/tests/smoke/cre/confidential_workflows_env.go diff --git a/.github/workflows/cre-system-tests.yaml b/.github/workflows/cre-system-tests.yaml index fccfdf2537b..4549bdf6f09 100644 --- a/.github/workflows/cre-system-tests.yaml +++ b/.github/workflows/cre-system-tests.yaml @@ -368,11 +368,6 @@ jobs: - name: Start local CRE${{ matrix.tests.cre_version }} id: start-local-cre - # This test starts the environment in-process, to pass runtime-computed - # capabilities and features as Go values, and tears down any environment - # already running. Starting one here is torn down and rebuilt, which - # leaves the vault DON unable to publish its public key in time. - if: ${{ !contains(matrix.tests.test_name, 'ConfidentialWorkflows') }} uses: ./.github/actions/start-local-cre-environment with: jd-image: @@ -382,6 +377,9 @@ jobs: chip-router-image: "${{ secrets.QA_AWS_ACCOUNT_NUMBER }}.dkr.ecr.${{ secrets.QA_AWS_REGION }}.amazonaws.com/local-cre-chip-router:v1.0.1" ctf-configs: ${{ matrix.tests.configs }} + # The confidential workflows test starts enclaves on the harness's + # default ports; the gateway needs them allowlisted to reach them. + env-start-extra-args: ${{ contains(matrix.tests.test_name, 'ConfidentialWorkflows') && '-e 8080,8081,8082,8083' || '' }} retry-count: "3" retry-delay-seconds: "15" cleanup-on-error: "false" diff --git a/core/scripts/cre/environment/configs/workflow-gateway-capabilities-don-confidential-workflows.toml b/core/scripts/cre/environment/configs/workflow-gateway-capabilities-don-confidential-workflows.toml index 925598cd8de..fc904e526de 100644 --- a/core/scripts/cre/environment/configs/workflow-gateway-capabilities-don-confidential-workflows.toml +++ b/core/scripts/cre/environment/configs/workflow-gateway-capabilities-don-confidential-workflows.toml @@ -20,6 +20,14 @@ trustEnclaves = true requireBFTQuorum = true +# The "enclaves" value is a JSON array of enclaves the capability routes to. +# Whoever starts the enclaves supplies it, since their addresses are only known +# once they are running; absent it, the capability registers an empty list. +[capability_configs.confidential-workflows] + binary_name = "confidential-workflows" + [capability_configs.confidential-workflows.values] + version = "1.0.0-alpha" + [chip_router] image = "local-cre-chip-router:v1.0.1" diff --git a/core/scripts/go.mod b/core/scripts/go.mod index bd13389172d..53df3fd1a95 100644 --- a/core/scripts/go.mod +++ b/core/scripts/go.mod @@ -497,6 +497,7 @@ require ( github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260624154507-ea7ff77a0ddb // indirect github.com/smartcontractkit/chainlink-ccv v0.1.1-0.20260716164331-d938b371c5d6 // indirect github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72 // indirect + github.com/smartcontractkit/chainlink-confidential-compute v1.3.0 // indirect github.com/smartcontractkit/chainlink-evm/contracts/cre/gobindings v0.0.0-20260403151002-2c91155b5501 // indirect github.com/smartcontractkit/chainlink-feeds v0.1.2-0.20250227211209-7cd000095135 // indirect github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-20260423135514-5b1a7565a99c // indirect diff --git a/core/scripts/go.sum b/core/scripts/go.sum index 30a8f977307..4f9c3b2f3ab 100644 --- a/core/scripts/go.sum +++ b/core/scripts/go.sum @@ -1586,6 +1586,8 @@ github.com/smartcontractkit/chainlink-common/keystore v1.3.0 h1:V05Rp9/dTc4Wyips github.com/smartcontractkit/chainlink-common/keystore v1.3.0/go.mod h1:vHV8BGm6TN7jBbMsWxq1Hqm3HbCtYFwzvKS0CCczxG8= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72 h1:uWEwl7i2ryuRVoV4DmIKm6mqYevf1lH/8cQYhw/JXko= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260724142814-45996a1bcb72/go.mod h1:UYcRMb4dZcoaIPgZJ3hckCySTqtJc9K4Q+tOKErwTq0= +github.com/smartcontractkit/chainlink-confidential-compute v1.3.0 h1:y64hOzM9H61E4EYRzQDtRwqunDBlKpsZc1SvFEUlSLE= +github.com/smartcontractkit/chainlink-confidential-compute v1.3.0/go.mod h1:fq9n85XoREIUxgdXIX3RyDIRCRyxZKZ5NCBbFDfdySo= github.com/smartcontractkit/chainlink-data-streams v1.1.0 h1:O5ngSwpwey7kQ3t4YgFiXzu3L3EvYiPS4c93gMWNRbk= github.com/smartcontractkit/chainlink-data-streams v1.1.0/go.mod h1:dF5JiHWueHjYguUUUrFeb03MkcDqha/tssEkqTkgzp4= github.com/smartcontractkit/chainlink-deployments-framework v0.111.1-0.20260612191326-e31c0ae4cd54 h1:mzbvXxdbE/96Pdj1zyPKzf25ZlDR48+iTTDTbaITvmk= diff --git a/system-tests/lib/cre/capabilities/confidentialcompute/confidentialcompute.go b/system-tests/lib/cre/capabilities/confidentialcompute/confidentialcompute.go index 2461ec4be9c..5b53db92844 100644 --- a/system-tests/lib/cre/capabilities/confidentialcompute/confidentialcompute.go +++ b/system-tests/lib/cre/capabilities/confidentialcompute/confidentialcompute.go @@ -1,11 +1,11 @@ -// Package confidentialcompute registers a confidential compute capability (e.g. -// confidential-workflows, confidential-http) with a CRE DON: it proposes the standardcapabilities job -// that runs the capability binary on each worker node, and writes the enclave -// list into the capability's on-chain registry config. +// Package confidentialcompute holds the pieces shared by confidential compute +// capabilities (e.g. confidential-workflows, confidential-http): reading the +// enclave list from configuration, sealing the capability's API key to each +// node, and building the on-chain registry entry that publishes the enclaves. // -// The confidential relay handler reads that registry config to discover which -// enclaves to route requests to, so the enclave list must be supplied before -// the environment starts. +// The confidential relay handler reads that registry entry to discover which +// enclaves it may route to, so the list must be supplied before the environment +// starts. Features under cre/features consume these. package confidentialcompute import ( @@ -18,20 +18,17 @@ import ( "net/http" "strings" - "github.com/google/uuid" "github.com/pkg/errors" "golang.org/x/crypto/nacl/box" + "google.golang.org/protobuf/proto" capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" cctypes "github.com/smartcontractkit/chainlink-confidential-compute/types" kcr "github.com/smartcontractkit/chainlink-evm/gethwrappers/keystone/generated/capabilities_registry_1_1_0" "github.com/smartcontractkit/chainlink-protos/cre/go/values" - jobv1 "github.com/smartcontractkit/chainlink-protos/job-distributor/v1/job" keystone_changeset "github.com/smartcontractkit/chainlink/deployment/keystone/changeset" "github.com/smartcontractkit/chainlink/system-tests/lib/cre" - "github.com/smartcontractkit/chainlink/system-tests/lib/cre/capabilities" - "github.com/smartcontractkit/chainlink/system-tests/lib/cre/flags" ) // apiKey is the API key the capability presents to the enclaves. Enclaves in @@ -39,98 +36,6 @@ import ( // non-empty one keeps the encrypt/decrypt path exercised. const apiKey = "foobar" -var jobTemplate = ` -type = "standardcapabilities" -schemaVersion = 1 -externalJobID = "%s" -forwardingAllowed = false -command = "%s" -name = "%s" -config = %s -` - -// jobsDelivered guards against the job spec function being invoked more than -// once per capability name for a single environment. -var jobsDelivered = make(map[string]bool) - -// ResetDeliveryState clears the jobsDelivered guard so job specs can be -// re-delivered when a new CRE environment is created (e.g. across subtests). -func ResetDeliveryState() { - jobsDelivered = make(map[string]bool) -} - -// New returns an InstallableCapability for a confidential compute capability. -// name is both the DON capability flag and the registered LabelledName (e.g. -// "confidential-http"); binaryName is the capability binary the node runs. -// Pass a nil enclaves slice to register the capability with an empty enclave -// list, which is enough to satisfy config validation for capabilities the test -// does not exercise. -func New(name, version, binaryName string, enclaves []cctypes.Enclave) (*capabilities.Capability, error) { - return capabilities.New( //nolint:staticcheck // SA1019 mirrors existing capability registrations - name, - capabilities.WithJobSpecFn(jobSpec(name, binaryName)), - capabilities.WithCapabilityRegistryV2ConfigFn(registryConfigFn(name, version, enclaves)), - ) -} - -func jobSpec(name string, binaryName string) cre.JobSpecFn { - return func(input *cre.JobSpecInput) (cre.DonJobs, error) { - if jobsDelivered[name] { - return nil, nil - } - jobsDelivered[name] = true - - donJobs := make(cre.DonJobs, 0) - for _, don := range input.Dons.List() { - if !don.HasFlag(name) { - continue - } - - workerNodes, wErr := don.Workers() - if wErr != nil { - return nil, errors.Wrap(wErr, "failed to find worker nodes") - } - - encryptedAPIKeys := make([]string, 0, len(workerNodes)) - for _, workerNode := range workerNodes { - publicKey, kErr := workflowEncryptionKey(workerNode) - if kErr != nil { - return nil, kErr - } - - ctxt, sErr := box.SealAnonymous(nil, []byte(apiKey), &publicKey, rand.Reader) - if sErr != nil { - return nil, errors.Wrap(sErr, "failed to seal API key") - } - encryptedAPIKeys = append(encryptedAPIKeys, hex.EncodeToString(ctxt)) - } - - for _, workerNode := range workerNodes { - // Keep liveness detection aggressive in e2e so failover traffic starts - // only after each node has had a chance to observe a dead enclave. - config := map[string]any{ - "InsecureSkipTLSVerify": true, - "EncryptedAPIKeys": strings.Join(encryptedAPIKeys, ","), - "EnableCache": true, - "EnableProactiveRefresh": true, - "MaxRetries": 3, - "RetryBackoffSeconds": 5, - } - configBytes, mErr := json.Marshal(config) - if mErr != nil { - return nil, errors.Wrap(mErr, "failed to marshal capability config") - } - donJobs = append(donJobs, &jobv1.ProposeJobRequest{ - NodeId: workerNode.JobDistributorDetails.NodeID, - Spec: fmt.Sprintf(jobTemplate, uuid.NewString(), binaryName, name, fmt.Sprintf("'%s'", string(configBytes))), - }) - } - } - - return donJobs, nil - } -} - // workflowEncryptionKey reads a node's workflow public encryption key, which is // used to seal the capability's API key so it is not stored in plaintext. func workflowEncryptionKey(workerNode *cre.Node) ([32]byte, error) { @@ -187,6 +92,94 @@ func workflowEncryptionKey(workerNode *cre.Node) ([32]byte, error) { // enclaves, letting a topology declare them instead of passing Go values. const EnclavesConfigKey = "enclaves" +// EncryptedAPIKeys seals the capability's API key to each worker node's workflow +// public key, so it is never stored in plaintext. The capability is configured +// with every node's sealed copy; each node decrypts only its own. +func EncryptedAPIKeys(workerNodes []*cre.Node) ([]string, error) { + encrypted := make([]string, 0, len(workerNodes)) + for _, workerNode := range workerNodes { + publicKey, kErr := workflowEncryptionKey(workerNode) + if kErr != nil { + return nil, kErr + } + + ctxt, sErr := box.SealAnonymous(nil, []byte(apiKey), &publicKey, rand.Reader) + if sErr != nil { + return nil, errors.Wrap(sErr, "failed to seal API key") + } + encrypted = append(encrypted, hex.EncodeToString(ctxt)) + } + + return encrypted, nil +} + +// JobConfigJSON builds the capability job's config. Liveness detection is kept +// aggressive so failover traffic starts only once each node has had a chance to +// observe a dead enclave. +func JobConfigJSON(encryptedAPIKeys []string) (string, error) { + config := map[string]any{ + "InsecureSkipTLSVerify": true, + "EncryptedAPIKeys": strings.Join(encryptedAPIKeys, ","), + "EnableCache": true, + "EnableProactiveRefresh": true, + "MaxRetries": 3, + "RetryBackoffSeconds": 5, + } + + configBytes, err := json.Marshal(config) + if err != nil { + return "", errors.Wrap(err, "failed to marshal capability config") + } + + return string(configBytes), nil +} + +// RegistryCapabilityConfig builds the on-chain registry entry that publishes the +// enclave list, which is how the confidential relay handler discovers where it +// may route requests. +func RegistryCapabilityConfig(name, version string, enclaves []cctypes.Enclave) (keystone_changeset.DONCapabilityWithConfig, error) { + wrappedConfig, err := values.WrapMap(cctypes.EnclavesList{Enclaves: enclaves}) + if err != nil { + return keystone_changeset.DONCapabilityWithConfig{}, errors.Wrap(err, "failed to wrap enclave list config") + } + + return keystone_changeset.DONCapabilityWithConfig{ + Capability: kcr.CapabilitiesRegistryCapability{ + LabelledName: name, + Version: version, + CapabilityType: 1, // ACTION + }, + Config: &capabilitiespb.CapabilityConfig{ + DefaultConfig: values.Proto(wrappedConfig).GetMapValue(), + LocalOnly: true, + }, + }, nil +} + +// MarshalRegistryConfig encodes an enclave list as the capability's on-chain +// registry config, for publishing enclaves to a DON that is already running. +// +// A fresh CapabilityConfig is built rather than decoding and re-encoding what is +// already on-chain, so config left by a broken earlier run cannot corrupt this one. +func MarshalRegistryConfig(enclaves []cctypes.Enclave) ([]byte, error) { + wrappedConfig, err := values.WrapMap(cctypes.EnclavesList{Enclaves: enclaves}) + if err != nil { + return nil, errors.Wrap(err, "failed to wrap enclave list config") + } + + // LocalOnly must match what RegistryCapabilityConfig writes at startup, or + // updating the enclave list would silently change the capability's scope. + encoded, err := proto.Marshal(&capabilitiespb.CapabilityConfig{ + DefaultConfig: values.Proto(wrappedConfig).GetMapValue(), + LocalOnly: true, + }) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal capability config") + } + + return encoded, nil +} + // MarshalEnclaves encodes an enclave list for EnclavesConfigKey, for callers // that discover their enclaves at runtime and hand them to the environment as // configuration. @@ -228,45 +221,3 @@ func EnclavesFromConfig(nodeSet *cre.NodeSet, name string) ([]cctypes.Enclave, e return enclaves, nil } - -// registryConfigFn writes the enclave list into the capability's on-chain -// registry config, which is how the confidential relay handler discovers the -// enclaves it may route to. -func registryConfigFn(name string, version string, enclaves []cctypes.Enclave) cre.CapabilityRegistryConfigFn { - return func(donFlags []string, nodeSet *cre.NodeSet) ([]keystone_changeset.DONCapabilityWithConfig, error) { - if !flags.HasFlag(donFlags, name) { - return nil, nil - } - - // Go-supplied enclaves win; otherwise use whatever the topology declared, - // so callers that only learn their enclaves at runtime and callers that - // can configure them up front are both served. - list := enclaves - if len(list) == 0 { - fromConfig, cErr := EnclavesFromConfig(nodeSet, name) - if cErr != nil { - return nil, cErr - } - list = fromConfig - } - - wrappedConfig, err := values.WrapMap(cctypes.EnclavesList{Enclaves: list}) - if err != nil { - return nil, errors.Wrap(err, "failed to wrap enclave list config") - } - - return []keystone_changeset.DONCapabilityWithConfig{ - { - Capability: kcr.CapabilitiesRegistryCapability{ - LabelledName: name, - Version: version, - CapabilityType: 1, // ACTION - }, - Config: &capabilitiespb.CapabilityConfig{ - DefaultConfig: values.Proto(wrappedConfig).GetMapValue(), - LocalOnly: true, - }, - }, - }, nil - } -} diff --git a/system-tests/lib/cre/features/confidentialworkflows/confidentialworkflows.go b/system-tests/lib/cre/features/confidentialworkflows/confidentialworkflows.go new file mode 100644 index 00000000000..283ba59d368 --- /dev/null +++ b/system-tests/lib/cre/features/confidentialworkflows/confidentialworkflows.go @@ -0,0 +1,155 @@ +// Package confidentialworkflows installs the confidential workflows capability +// as a standard CRE feature: it publishes the enclave list to the on-chain +// registry and proposes the capability job on each worker node. +// +// The enclave list comes from the DON's capability config rather than Go values, +// so a topology can declare it and `cre env start` can stand the capability up +// without a test driving the environment in-process. +package confidentialworkflows + +import ( + "context" + "fmt" + + "github.com/google/uuid" + "github.com/pkg/errors" + "github.com/rs/zerolog" + + cre_jobs "github.com/smartcontractkit/chainlink/deployment/cre/jobs" + job_types "github.com/smartcontractkit/chainlink/deployment/cre/jobs/types" + "github.com/smartcontractkit/chainlink/deployment/cre/pkg/offchain" + keystone_changeset "github.com/smartcontractkit/chainlink/deployment/keystone/changeset" + + "github.com/smartcontractkit/chainlink/system-tests/lib/cre" + "github.com/smartcontractkit/chainlink/system-tests/lib/cre/capabilities/confidentialcompute" + "github.com/smartcontractkit/chainlink/system-tests/lib/cre/don/jobs/standardcapability" +) + +const flag = cre.ConfidentialWorkflowsCapability + +// VersionConfigKey is the capability config value holding the version the +// capability registers under. +const VersionConfigKey = "version" + +// defaultVersion is used when the topology does not declare one. +const defaultVersion = "1.0.0" + +type ConfidentialWorkflows struct{} + +func (o *ConfidentialWorkflows) Flag() cre.CapabilityFlag { + return flag +} + +// PreEnvStartup publishes the enclave list the topology declared, which the +// confidential relay handler reads to discover where it may route requests. +func (o *ConfidentialWorkflows) PreEnvStartup( + _ context.Context, + _ zerolog.Logger, + don *cre.DonMetadata, + _ *cre.Topology, + _ *cre.Environment, +) (*cre.PreEnvStartupOutput, error) { + if !don.HasFlag(flag) { + return &cre.PreEnvStartupOutput{}, nil + } + + nodeSet := don.MustNodeSet() + enclaves, eErr := confidentialcompute.EnclavesFromConfig(nodeSet, flag) + if eErr != nil { + return nil, eErr + } + + capabilityConfig, cErr := confidentialcompute.RegistryCapabilityConfig(flag, version(don), enclaves) + if cErr != nil { + return nil, cErr + } + + return &cre.PreEnvStartupOutput{ + DONCapabilityWithConfig: []keystone_changeset.DONCapabilityWithConfig{capabilityConfig}, + }, nil +} + +// PostEnvStartup proposes the capability job on each worker node, sealing the +// capability's API key to every node's workflow key. +func (o *ConfidentialWorkflows) PostEnvStartup( + _ context.Context, + _ zerolog.Logger, + don *cre.Don, + _ *cre.Dons, + creEnv *cre.Environment, +) error { + if !don.HasFlag(flag) { + return nil + } + + capabilityConfig, ok := don.GetCapabilityConfig(flag) + if !ok { + return fmt.Errorf("config for '%s' capability not found for %s DON", flag, don.GetName()) + } + + command, cErr := standardcapability.GetCommand(capabilityConfig.BinaryName) + if cErr != nil { + return errors.Wrapf(cErr, "failed to get command for %s capability", flag) + } + + workerNodes, wErr := don.Workers() + if wErr != nil { + return errors.Wrap(wErr, "failed to find worker nodes") + } + + encryptedAPIKeys, eErr := confidentialcompute.EncryptedAPIKeys(workerNodes) + if eErr != nil { + return eErr + } + + configJSON, jErr := confidentialcompute.JobConfigJSON(encryptedAPIKeys) + if jErr != nil { + return jErr + } + + input := cre_jobs.ProposeJobSpecInput{ + Domain: offchain.ProductLabel, + Environment: creEnv.CldfEnvironment.Name, + DONName: don.Name, + JobName: flag + "-worker", + ExtraLabels: map[string]string{cre.CapabilityLabelKey: flag}, + DONFilters: []offchain.TargetDONFilter{ + {Key: offchain.FilterKeyDONName, Value: don.Name}, + }, + Template: job_types.Cron, + Inputs: job_types.JobSpecInput{ + "command": command, + "config": configJSON, + }, + } + if creEnv.FreshExternalJobIDs { + input.Inputs["externalJobID"] = uuid.NewString() + } + + if err := (cre_jobs.ProposeJobSpec{}).VerifyPreconditions(*creEnv.CldfEnvironment, input); err != nil { + return errors.Wrapf(err, "precondition verification failed for %s worker job", flag) + } + + if _, err := (cre_jobs.ProposeJobSpec{}).Apply(*creEnv.CldfEnvironment, input); err != nil { + return errors.Wrapf(err, "failed to propose %s worker job spec", flag) + } + + return nil +} + +// version returns the version the capability registers under, from the DON's +// capability config when declared. +func version(don *cre.DonMetadata) string { + cfg, ok := don.CapabilityConfigs[flag] + if !ok || cfg.Values == nil { + return defaultVersion + } + + if v, found := cfg.Values[VersionConfigKey]; found { + if s, isString := v.(string); isString && s != "" { + return s + } + } + + return defaultVersion +} diff --git a/system-tests/lib/cre/features/sets/sets.go b/system-tests/lib/cre/features/sets/sets.go index 717ee512fa6..8db4cb51b28 100644 --- a/system-tests/lib/cre/features/sets/sets.go +++ b/system-tests/lib/cre/features/sets/sets.go @@ -4,6 +4,7 @@ import ( "github.com/smartcontractkit/chainlink/system-tests/lib/cre" aptos_feature "github.com/smartcontractkit/chainlink/system-tests/lib/cre/features/aptos" confidential_relay_feature "github.com/smartcontractkit/chainlink/system-tests/lib/cre/features/confidentialrelay" + confidential_workflows_feature "github.com/smartcontractkit/chainlink/system-tests/lib/cre/features/confidentialworkflows" consensus_feature "github.com/smartcontractkit/chainlink/system-tests/lib/cre/features/consensus/v2" cron_feature "github.com/smartcontractkit/chainlink/system-tests/lib/cre/features/cron" don_time_feature "github.com/smartcontractkit/chainlink/system-tests/lib/cre/features/don_time" @@ -28,5 +29,6 @@ func New() cre.Features { &stellar_feature.Stellar{}, &vault_feature.Vault{}, &confidential_relay_feature.ConfidentialRelay{}, + &confidential_workflows_feature.ConfidentialWorkflows{}, ) } diff --git a/system-tests/lib/cre/registry_update.go b/system-tests/lib/cre/registry_update.go new file mode 100644 index 00000000000..4c6240c12e4 --- /dev/null +++ b/system-tests/lib/cre/registry_update.go @@ -0,0 +1,76 @@ +package cre + +import ( + "context" + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/pkg/errors" + + capabilities_registry_v2 "github.com/smartcontractkit/chainlink-evm/gethwrappers/workflow/generated/capabilities_registry_wrapper_v2" + "github.com/smartcontractkit/chainlink-testing-framework/seth" +) + +// UpdateDONCapabilityConfig rewrites one capability's config on a DON, leaving +// the DON's other capabilities, nodes, and settings as they are. +// +// This exists for configuration only knowable once the environment is running. +// Capabilities that poll the registry pick the new config up on their next +// refresh, so publishing it does not require restarting the environment. +// +// capabilityName is matched without a version, since the registry keys +// capabilities as "name@version". +func UpdateDONCapabilityConfig( + ctx context.Context, + sethClient *seth.Client, + capabilitiesRegistryAddr string, + donName string, + capabilityName string, + config []byte, +) error { + capReg, err := capabilities_registry_v2.NewCapabilitiesRegistry( + common.HexToAddress(capabilitiesRegistryAddr), sethClient.Client, + ) + if err != nil { + return errors.Wrap(err, "failed to create capabilities registry wrapper") + } + + don, err := capReg.GetDONByName(&bind.CallOpts{Context: ctx}, donName) + if err != nil { + return errors.Wrapf(err, "failed to fetch DON %q from capabilities registry", donName) + } + + // updateDON replaces the whole set, so carry every capability the DON has. + updated := make([]capabilities_registry_v2.CapabilitiesRegistryCapabilityConfiguration, len(don.CapabilityConfigurations)) + copy(updated, don.CapabilityConfigurations) + + var found bool + for i := range updated { + if strings.HasPrefix(updated[i].CapabilityId, capabilityName+"@") { + updated[i].Config = config + found = true + } + } + if !found { + return errors.Errorf("capability %q is not configured on DON %q", capabilityName, donName) + } + + tx, err := capReg.UpdateDON(sethClient.NewTXOpts(), don.Id, capabilities_registry_v2.CapabilitiesRegistryUpdateDONParams{ + Name: don.Name, + Config: don.Config, + CapabilityConfigurations: updated, + Nodes: don.NodeP2PIds, + F: don.F, + IsPublic: don.IsPublic, + }) + if err != nil { + return errors.Wrapf(err, "failed to submit updateDON for DON %q", donName) + } + + if _, err := bind.WaitMined(ctx, sethClient.Client, tx); err != nil { + return errors.Wrapf(err, "failed waiting for updateDON of DON %q to be mined", donName) + } + + return nil +} diff --git a/system-tests/tests/smoke/cre/confidential_workflows_env.go b/system-tests/tests/smoke/cre/confidential_workflows_env.go deleted file mode 100644 index f02d5c1db6e..00000000000 --- a/system-tests/tests/smoke/cre/confidential_workflows_env.go +++ /dev/null @@ -1,196 +0,0 @@ -package cre - -import ( - "context" - "fmt" - "os" - "path/filepath" - "slices" - "strings" - "time" - - "github.com/pkg/errors" - - "github.com/smartcontractkit/chainlink-testing-framework/framework" - "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" - - crescriptenv "github.com/smartcontractkit/chainlink/core/scripts/cre/environment/environment" - crelib "github.com/smartcontractkit/chainlink/system-tests/lib/cre" - creenv "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment" - envconfig "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment/config" - feature_sets "github.com/smartcontractkit/chainlink/system-tests/lib/cre/features/sets" - "github.com/smartcontractkit/chainlink/system-tests/lib/cre/flags" -) - -// This file provides an in-process CRE environment start for tests that must -// supply capabilities and features computed at runtime. -// -// The standard helper (t_helpers.SetupTestEnvironmentWithConfig) starts the -// environment by shelling out to `cre env start`, so a test cannot hand it Go -// values. The confidential workflows test needs to, twice over: the enclave list -// (only known once the enclaves are running) goes into the capability's on-chain -// registry config, and the trusted measurements go into the relay's capability -// config. -// -// startConfidentialCreEnvironment therefore calls the exported -// crescriptenv.StartCLIEnvironment directly and writes the local CRE state file. -// Callers then invoke the standard helper, which finds that state file and skips -// starting the environment, building the TestEnvironment from saved state. - -const ( - // confidentialCleanupWait matches the CLI's own cleanup grace period. - confidentialCleanupWait = 15 * time.Second - - // Resolved against the CRE environment directory rather than used as-is: the - // CLI defaults assume a working directory of core/scripts/cre/environment, - // but this test runs from smoke/cre. - confidentialCapabilityDefaultsConfig = "configs/capability_defaults.toml" -) - -// startConfidentialCreEnvironment starts a local CRE environment with extra -// capabilities and features, then persists the state file so the standard test -// environment helper can build from it. -func startConfidentialCreEnvironment( - ctx context.Context, - relativePathToRepoRoot string, - environmentDirPath string, - extraCapabilities []crelib.InstallableCapability, - extraAllowedPorts []int, -) error { - in, err := confidentialPreConfigure(relativePathToRepoRoot, environmentDirPath) - if err != nil { - return err - } - - // Extra capabilities are registered dynamically rather than declared in the - // topology config. Job spec delivery and on-chain registration both check - // don.HasFlag(name), which reads NodeSet.Capabilities, so the flags have to be - // present there too. - for _, c := range extraCapabilities { - flag := c.Flag() - for i, ns := range in.NodeSets { - if slices.Contains(ns.DONTypes, "workflow") && !slices.Contains(ns.Capabilities, flag) { - in.NodeSets[i].Capabilities = append(in.NodeSets[i].Capabilities, flag) - } - } - } - - // Config.Validate rejects capability flags the provider doesn't know, so every - // flag this run declares has to be present. don-time is absent from the - // extensible provider's built-in globals, unlike the default provider's. - extraFlags := []string{string(crelib.DONTimeCapability)} - for _, c := range extraCapabilities { - extraFlags = append(extraFlags, c.Flag()) - } - - envDependencies := crelib.NewEnvironmentDependencies( - flags.NewExtensibleCapabilityFlagsProvider(extraFlags), - crelib.NewContractVersionsProvider(envconfig.DefaultContractSet()), - ) - if err := in.Validate(envDependencies); err != nil { - return errors.Wrap(err, "failed to validate environment configuration") - } - - // The standard feature set carries the confidential relay; the topology's - // capability config supplies its settings. - features := feature_sets.New() - - // Same allowlist `cre env start` grants, plus this test's enclave ports. - gatewayWhitelistConfig := crescriptenv.DefaultGatewayWhitelistConfig(in, extraAllowedPorts) - - output, startErr := crescriptenv.StartCLIEnvironment( - ctx, - relativePathToRepoRoot, - in, - extraCapabilities, - features, - nil, // no extra job spec functions - envDependencies, - gatewayWhitelistConfig, - ) - if startErr != nil { - if stopErr := stopConfidentialCreEnvironment(relativePathToRepoRoot); stopErr != nil { - return errors.Wrapf(startErr, "failed to start environment, and cleanup also failed: %s", stopErr) - } - return errors.Wrap(startErr, "failed to start environment") - } - - addresses, aErr := output.CreEnvironment.CldfEnvironment.DataStore.Addresses().Fetch() - if aErr != nil { - return errors.Wrap(aErr, "failed to fetch addresses from datastore") - } - if err := in.SetAddresses(addresses); err != nil { - return errors.Wrap(err, "failed to set addresses on config") - } - if storeErr := in.Store(envconfig.MustLocalCREStateFileAbsPath(relativePathToRepoRoot)); storeErr != nil { - return errors.Wrap(storeErr, "failed to store local CRE state") - } - - return nil -} - -// confidentialPreConfigure clears any prior environment state and loads the -// topology config, so a stale state file from a different topology is not -// merged into this run. -// -// This deliberately skips crescriptenv.RunSetup, matching `cre env start`, -// which only runs setup under --auto-setup (off by default, and unset in CI). -// Setup installs host tooling such as Bun, which is unavailable on CI runners. -func confidentialPreConfigure(relativePathToRepoRoot, environmentDirPath string) (*envconfig.Config, error) { - _ = stopConfidentialCreEnvironment(relativePathToRepoRoot) - - if err := framework.RemoveTestContainers(); err != nil { - return nil, errors.Wrap(err, "failed to remove test containers") - } - defer func() { - crescriptenv.StartCmdRecoverHandlerFunc(nil, nil, true, confidentialCleanupWait) - }() - - if cleanUpErr := envconfig.RemoveAllEnvironmentStateDir(relativePathToRepoRoot); cleanUpErr != nil { - return nil, errors.Wrap(cleanUpErr, "failed to clean up environment state files") - } - - // Re-prepend the capability defaults to whatever CTF_CONFIGS the caller set. - // Stripping the prefix first keeps this idempotent across repeated calls. - defaultsConfig := filepath.Join(environmentDirPath, confidentialCapabilityDefaultsConfig) - userConfigs := strings.TrimPrefix(os.Getenv("CTF_CONFIGS"), defaultsConfig+",") - ctfConfigs := defaultsConfig - if userConfigs != "" && userConfigs != defaultsConfig { - ctfConfigs = defaultsConfig + "," + userConfigs - } - if err := os.Setenv("CTF_CONFIGS", ctfConfigs); err != nil { - return nil, fmt.Errorf("failed to set CTF_CONFIGS: %w", err) - } - - if pkErr := creenv.SetDefaultPrivateKeyIfEmpty(blockchain.DefaultAnvilPrivateKey); pkErr != nil { - return nil, errors.Wrap(pkErr, "failed to set default private key") - } - - // Keep Ryuk from reaping the containers when this process exits; the test - // tears them down itself. - if setErr := os.Setenv("TESTCONTAINERS_RYUK_DISABLED", "true"); setErr != nil { - return nil, fmt.Errorf("failed to set TESTCONTAINERS_RYUK_DISABLED: %w", setErr) - } - - in := &envconfig.Config{} - if err := in.Load(os.Getenv("CTF_CONFIGS")); err != nil { - return nil, errors.Wrap(err, "failed to load environment configuration") - } - - return in, nil -} - -// stopConfidentialCreEnvironment removes the environment containers and the local -// CRE state file. -func stopConfidentialCreEnvironment(relativePathToRepoRoot string) error { - if removeErr := framework.RemoveTestContainers(); removeErr != nil { - return errors.Wrap(removeErr, "failed to remove environment containers") - } - - creStateFile := envconfig.MustLocalCREStateFileAbsPath(relativePathToRepoRoot) - if cErr := os.Remove(creStateFile); cErr != nil && !os.IsNotExist(cErr) { - framework.L.Warn().Msgf("failed to remove local CRE state file: %s", cErr) - } - - return nil -} diff --git a/system-tests/tests/smoke/cre/confidential_workflows_test.go b/system-tests/tests/smoke/cre/confidential_workflows_test.go index 8ce92baa875..ec212fd023d 100644 --- a/system-tests/tests/smoke/cre/confidential_workflows_test.go +++ b/system-tests/tests/smoke/cre/confidential_workflows_test.go @@ -8,7 +8,6 @@ import ( "os" "os/exec" "path/filepath" - "strconv" "testing" "time" @@ -20,6 +19,7 @@ import ( ns "github.com/smartcontractkit/chainlink-testing-framework/framework/components/simple_node_set" "github.com/smartcontractkit/chainlink-confidential-compute/tests/testhelpers" + cctypes "github.com/smartcontractkit/chainlink-confidential-compute/types" keystone_changeset "github.com/smartcontractkit/chainlink/deployment/keystone/changeset" crelib "github.com/smartcontractkit/chainlink/system-tests/lib/cre" "github.com/smartcontractkit/chainlink/system-tests/lib/cre/capabilities/confidentialcompute" @@ -102,39 +102,30 @@ func Test_CRE_V2_ConfidentialWorkflows_Relay(t *testing.T) { Int("count", len(enclaves.Enclaves)). Msg("Local enclaves ready") - // 3. The capability carries the enclave list into the on-chain registry - // config; the relay handler reads it from there to decide where to route. - confidentialcompute.ResetDeliveryState() - cap, err := confidentialcompute.New( - confidentialWorkflowsApp, - confidentialWorkflowsCapVersion, - confidentialWorkflowsApp, - enclaves.Enclaves, - ) - require.NoError(t, err, "failed to build confidential-workflows capability") - - // 4. Start the CRE environment in-process so the capability above can be - // injected, then let the standard helper build the test environment - // from the state file it wrote. The confidential relay feature comes - // from the standard feature set, configured by the topology TOML. - require.NoError(t, startConfidentialCreEnvironment( - t.Context(), - tconf.RelativePathToRepoRoot, - tconf.EnvironmentDirPath, - []crelib.InstallableCapability{cap}, - confidentialEnclavePorts(t, enclaves), - ), "failed to start confidential CRE environment") - + // 3. Build the environment the standard way. The capability and the relay + // come from the standard feature set, configured by the topology TOML, + // so nothing here has to be injected as Go values. testEnv := t_helpers.SetupTestEnvironmentWithConfig(t, tconf) + // 4. Publish the enclave list to the capability's on-chain registry config, + // which the relay handler reads to decide where to route. The capability + // registers with an empty list and refreshes from the registry on a timer, + // so this can land after the environment is already running. + publishEnclaves(t, testEnv, testLogger, enclaves.Enclaves) + // 5. Point the proxy at the real gateway now that it exists. gatewayURL := confidentialGatewayURL(t, testEnv) require.NoError(t, gwProxy.SetTarget(gatewayURL), "failed to set gateway proxy target") testLogger.Info().Str("gatewayURL", gatewayURL).Msg("Gateway proxy target set") - // 6. The engine's pre-enclave secret fetch reads VaultPublicKey and - // Threshold from the vault capability's registry config, which is - // registered empty. Without this, GetSecret fails inside the workflow. + // 6. The vault DON only serves its public key once DKG has produced a + // result package on every worker. Fetching before then times out, and + // how long DKG takes tracks how loaded the runner is. + ensureVaultDKGResultPackages(t, testEnv) + + // 6a. The engine's pre-enclave secret fetch reads VaultPublicKey and + // Threshold from the vault capability's registry config, which is + // registered empty. Without this, GetSecret fails inside the workflow. vaultPublicKey := injectVaultPublicKey(t, testEnv, testLogger, gatewayURL) // 6b. The enclaves boot with no signer set and no master public key, so they @@ -217,6 +208,49 @@ func confidentialGatewayURL(t *testing.T, testEnv *ttypes.TestEnvironment) strin // injectVaultPublicKey writes the vault DON's DKG public key and threshold into // the vault capability's registry config. +// publishEnclaves writes the enclave list into the capability's on-chain +// registry config and waits for the capability to pick it up. +// +// The capability refreshes from the registry on a ticker +// (DefaultEnclaveRefreshIntervalSeconds, 10s), so this waits two intervals +// rather than one: a single interval races a refresh that started just before +// the transaction landed and therefore read the old config. +func publishEnclaves( + t *testing.T, + testEnv *ttypes.TestEnvironment, + testLogger zerolog.Logger, + enclaves []cctypes.Enclave, +) { + t.Helper() + + ctx := t.Context() + + require.IsType(t, &evm.Blockchain{}, testEnv.CreEnvironment.Blockchains[0], "expected EVM blockchain") + sethClient := testEnv.CreEnvironment.Blockchains[0].(*evm.Blockchain).SethClient + + capRegAddr := crecontracts.MustGetAddressFromDataStore( + testEnv.CreEnvironment.CldfEnvironment.DataStore, + testEnv.CreEnvironment.Blockchains[0].ChainSelector(), //nolint:staticcheck // mirrors system-tests usage + keystone_changeset.CapabilitiesRegistry.String(), + testEnv.CreEnvironment.ContractVersions[keystone_changeset.CapabilitiesRegistry.String()], + "", + ) + + config, err := confidentialcompute.MarshalRegistryConfig(enclaves) + require.NoError(t, err, "failed to encode enclave list for the registry") + + require.NoError(t, + crelib.UpdateDONCapabilityConfig(ctx, sethClient, capRegAddr, confidentialWorkflowDONName, confidentialWorkflowsApp, config), + "failed to publish enclave list to the capabilities registry", + ) + + testLogger.Info(). + Int("count", len(enclaves)). + Dur("wait", confidentialEnclaveRefreshWait). + Msg("Published enclave list; waiting for the capability to refresh from the registry") + time.Sleep(confidentialEnclaveRefreshWait) +} + func injectVaultPublicKey(t *testing.T, testEnv *ttypes.TestEnvironment, testLogger zerolog.Logger, gatewayURL string) string { t.Helper() @@ -389,17 +423,3 @@ func confidentialWorkflowDONContainers(testEnv *ttypes.TestEnvironment) []string } return names } - -// confidentialEnclavePorts returns the enclave host-server ports as ints so they -// can be added to the gateway's outbound whitelist. -func confidentialEnclavePorts(t *testing.T, result *testhelpers.LocalEnclaveResult) []int { - t.Helper() - - ports := make([]int, 0, len(result.HTTPPorts)+len(result.ConfigHTTPPorts)) - for _, p := range append(append([]string{}, result.HTTPPorts...), result.ConfigHTTPPorts...) { - n, err := strconv.Atoi(p) - require.NoError(t, err, "enclave port %q is not numeric", p) - ports = append(ports, n) - } - return ports -} diff --git a/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go b/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go index 6e6d1b46b28..3f782977da6 100644 --- a/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go +++ b/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go @@ -19,6 +19,7 @@ import ( "strings" "sync" "testing" + "time" "github.com/andybalholm/brotli" "github.com/rs/zerolog" @@ -47,8 +48,14 @@ const ( // and the capability binary name; all three share this value. confidentialWorkflowsApp = string(crelib.ConfidentialWorkflowsCapability) - // confidentialWorkflowsCapVersion is the version the capability registers under. - confidentialWorkflowsCapVersion = "1.0.0-alpha" + // confidentialWorkflowDONName is the workflow DON's on-chain name, taken from + // the nodeset name in the topology. + confidentialWorkflowDONName = "workflow" + + // confidentialEnclaveRefreshWait covers two of the capability's registry + // refresh intervals, so a refresh already in flight when the enclave list + // lands cannot be mistaken for the one that picks it up. + confidentialEnclaveRefreshWait = 25 * time.Second // confidentialGatewayProxyPort is the fixed port the enclaves are told to reach // the CRE gateway on. It must be known before the enclaves start, which is why From cbe23eb936e47eebff3038c255a2ac1490b0f171 Mon Sep 17 00:00:00 2001 From: vreff <104409744+vreff@users.noreply.github.com> Date: Wed, 12 Aug 2026 17:10:58 -0400 Subject: [PATCH 14/29] remove envar overrides --- .github/workflows/cre-system-tests.yaml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.github/workflows/cre-system-tests.yaml b/.github/workflows/cre-system-tests.yaml index 4549bdf6f09..6b241eeb04c 100644 --- a/.github/workflows/cre-system-tests.yaml +++ b/.github/workflows/cre-system-tests.yaml @@ -406,14 +406,6 @@ jobs: RUN_QUARANTINED_TESTS: "true" # always run quarantined tests in CI TOPOLOGY_NAME: ${{ matrix.tests.topology }} CONFIDENTIAL_COMPUTE_ROOT: ${{ github.workspace }}/chainlink-confidential-compute - # Image overrides for tests that start the environment in-process; the - # start-local-cre-environment action sets these for every other test. - CTF_JD_IMAGE: - "${{ secrets.AWS_ACCOUNT_ID_PROD }}.dkr.ecr.${{ secrets.QA_AWS_REGION - }}.amazonaws.com/job-distributor:0.28.0" - CTF_CHAINLINK_IMAGE: "${{ env.CHAINLINK_IMAGE_FULL }}" - CTF_CHIP_ROUTER_IMAGE: "${{ secrets.QA_AWS_ACCOUNT_NUMBER }}.dkr.ecr.${{ - secrets.QA_AWS_REGION }}.amazonaws.com/local-cre-chip-router:v1.0.1" GITHUB_TOKEN: ${{ steps.github-token.outputs.access-token || '' }} # to avoid rate limiting when downloading protobuf files from GitHub PARALLEL_COUNT: "10" CRE_TEST_PARALLEL_ENABLED: "true" From 864469b7616a036db11e1476c05b3d6f99352952 Mon Sep 17 00:00:00 2001 From: vreff <104409744+vreff@users.noreply.github.com> Date: Wed, 12 Aug 2026 17:26:45 -0400 Subject: [PATCH 15/29] remove replace --- system-tests/tests/go.mod | 3 --- 1 file changed, 3 deletions(-) diff --git a/system-tests/tests/go.mod b/system-tests/tests/go.mod index 2a9180dd2b4..9df07b1a72e 100644 --- a/system-tests/tests/go.mod +++ b/system-tests/tests/go.mod @@ -13,8 +13,6 @@ replace github.com/smartcontractkit/chainlink/core/scripts/cre/environment/examp replace github.com/smartcontractkit/chainlink/core/scripts/cre/environment/examples/workflows/cron => ../../core/scripts/cre/environment/examples/workflows/cron -replace github.com/smartcontractkit/chainlink/core/scripts => ../../core/scripts - replace github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/evm/evmread => ./smoke/cre/evm/evmread replace github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/evm/logtrigger => ./smoke/cre/evm/logtrigger @@ -80,7 +78,6 @@ require ( github.com/smartcontractkit/chainlink-testing-framework/framework/components/chiprouter v1.0.4 github.com/smartcontractkit/chainlink-testing-framework/framework/components/fake v0.15.0 github.com/smartcontractkit/chainlink-testing-framework/seth v1.51.5 - github.com/smartcontractkit/chainlink/core/scripts v0.0.0-20260812035138-673d7e955a68 github.com/smartcontractkit/chainlink/core/scripts/cre/environment/examples/workflows/cron v0.0.0-20251008094352-f74459c46e8c github.com/smartcontractkit/chainlink/core/scripts/cre/environment/examples/workflows/proof-of-reserve/cron-based v0.0.0-00010101000000-000000000000 github.com/smartcontractkit/chainlink/deployment v0.0.0-20260126202327-6be9a05f0caf From 2c76b23987f84b6ae78c2707881245c39d29f157 Mon Sep 17 00:00:00 2001 From: vreff <104409744+vreff@users.noreply.github.com> Date: Wed, 12 Aug 2026 17:27:54 -0400 Subject: [PATCH 16/29] update docs --- go.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/go.md b/go.md index 42c6ee6c0f1..1f72cb53086 100644 --- a/go.md +++ b/go.md @@ -523,8 +523,9 @@ flowchart LR chainlink/system-tests/lib --> chainlink-testing-framework/framework/components/fake click chainlink/system-tests/lib href "https://github.com/smartcontractkit/chainlink" chainlink/system-tests/tests --> chainlink-confidential-compute/tests/testhelpers - chainlink/system-tests/tests --> chainlink/core/scripts chainlink/system-tests/tests --> chainlink/core/scripts/cre/environment/examples/workflows/cron + chainlink/system-tests/tests --> chainlink/core/scripts/cre/environment/examples/workflows/proof-of-reserve/cron-based + chainlink/system-tests/tests --> chainlink/system-tests/lib chainlink/system-tests/tests --> chainlink/system-tests/tests/regression/cre/consensus chainlink/system-tests/tests --> chainlink/system-tests/tests/regression/cre/evm/evmread-negative chainlink/system-tests/tests --> chainlink/system-tests/tests/regression/cre/evm/evmwrite-negative From b7f37b468e523f545f1a29b63b0575760f8b49da Mon Sep 17 00:00:00 2001 From: vreff <104409744+vreff@users.noreply.github.com> Date: Thu, 13 Aug 2026 08:44:01 -0400 Subject: [PATCH 17/29] fix test --- .../smoke/cre/confidential_workflows_test.go | 51 ++++++++++++++++++- .../confidential_workflows_test_helpers.go | 4 -- 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/system-tests/tests/smoke/cre/confidential_workflows_test.go b/system-tests/tests/smoke/cre/confidential_workflows_test.go index ec212fd023d..6b44b65da57 100644 --- a/system-tests/tests/smoke/cre/confidential_workflows_test.go +++ b/system-tests/tests/smoke/cre/confidential_workflows_test.go @@ -4,22 +4,27 @@ import ( "bytes" "context" "fmt" + "math/big" "net/url" "os" "os/exec" "path/filepath" + "strings" "testing" "time" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/rs/zerolog" "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink-testing-framework/framework" ns "github.com/smartcontractkit/chainlink-testing-framework/framework/components/simple_node_set" + "github.com/smartcontractkit/chainlink-testing-framework/seth" "github.com/smartcontractkit/chainlink-confidential-compute/tests/testhelpers" cctypes "github.com/smartcontractkit/chainlink-confidential-compute/types" + capabilities_registry_v2 "github.com/smartcontractkit/chainlink-evm/gethwrappers/workflow/generated/capabilities_registry_wrapper_v2" keystone_changeset "github.com/smartcontractkit/chainlink/deployment/keystone/changeset" crelib "github.com/smartcontractkit/chainlink/system-tests/lib/cre" "github.com/smartcontractkit/chainlink/system-tests/lib/cre/capabilities/confidentialcompute" @@ -47,6 +52,10 @@ const ( // confidentialVaultThreshold matches the 4-node F=1 vault DON. confidentialVaultThreshold = 1 + + // confidentialDONPageLimit bounds the getDONs page read when locating a + // capability's DON. The topology has a handful of DONs, so one page covers it. + confidentialDONPageLimit = 100 ) // Test_CRE_V2_ConfidentialWorkflows_Relay exercises the confidential workflows @@ -239,18 +248,58 @@ func publishEnclaves( config, err := confidentialcompute.MarshalRegistryConfig(enclaves) require.NoError(t, err, "failed to encode enclave list for the registry") + donName := donNameForCapability(t, sethClient, capRegAddr, confidentialWorkflowsApp) + require.NoError(t, - crelib.UpdateDONCapabilityConfig(ctx, sethClient, capRegAddr, confidentialWorkflowDONName, confidentialWorkflowsApp, config), + crelib.UpdateDONCapabilityConfig(ctx, sethClient, capRegAddr, donName, confidentialWorkflowsApp, config), "failed to publish enclave list to the capabilities registry", ) testLogger.Info(). + Str("don", donName). Int("count", len(enclaves)). Dur("wait", confidentialEnclaveRefreshWait). Msg("Published enclave list; waiting for the capability to refresh from the registry") time.Sleep(confidentialEnclaveRefreshWait) } +// donNameForCapability returns the registry name of the DON providing the named +// capability. The registry derives DON names from the topology, so the name is +// resolved rather than assumed: a wrong name makes getDONByName revert with an +// opaque custom error, whereas this reports which DONs actually exist. +func donNameForCapability( + t *testing.T, + sethClient *seth.Client, + capabilitiesRegistryAddr string, + capabilityName string, +) string { + t.Helper() + + capReg, err := capabilities_registry_v2.NewCapabilitiesRegistry( + common.HexToAddress(capabilitiesRegistryAddr), sethClient.Client, + ) + require.NoError(t, err, "failed to create capabilities registry wrapper") + + allDONs, err := capReg.GetDONs(&bind.CallOpts{Context: t.Context()}, big.NewInt(0), big.NewInt(confidentialDONPageLimit)) + require.NoError(t, err, "failed to list DONs from the capabilities registry") + + names := make([]string, 0, len(allDONs)) + for i := range allDONs { + names = append(names, allDONs[i].Name) + // The registry keys capabilities as "name@version". + for _, capabilityConfig := range allDONs[i].CapabilityConfigurations { + if strings.HasPrefix(capabilityConfig.CapabilityId, capabilityName+"@") { + return allDONs[i].Name + } + } + } + + require.FailNowf(t, "capability is not registered on any DON", + "no DON provides capability %q; DONs present: %s", capabilityName, strings.Join(names, ", ")) + + return "" +} + func injectVaultPublicKey(t *testing.T, testEnv *ttypes.TestEnvironment, testLogger zerolog.Logger, gatewayURL string) string { t.Helper() diff --git a/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go b/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go index 3f782977da6..4d5ebeeed58 100644 --- a/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go +++ b/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go @@ -48,10 +48,6 @@ const ( // and the capability binary name; all three share this value. confidentialWorkflowsApp = string(crelib.ConfidentialWorkflowsCapability) - // confidentialWorkflowDONName is the workflow DON's on-chain name, taken from - // the nodeset name in the topology. - confidentialWorkflowDONName = "workflow" - // confidentialEnclaveRefreshWait covers two of the capability's registry // refresh intervals, so a refresh already in flight when the enclave list // lands cannot be mistaken for the one that picks it up. From 3caa042a2fcedccdd4a45c7460272c5b8c59ec4a Mon Sep 17 00:00:00 2001 From: vreff <104409744+vreff@users.noreply.github.com> Date: Thu, 13 Aug 2026 09:00:50 -0400 Subject: [PATCH 18/29] lint --- .../confidentialcompute.go | 3 +- .../confidentialcompute_test.go | 17 +++++++++-- .../confidentialworkflows.go | 1 - .../smoke/cre/confidential_workflows_test.go | 17 +++++------ .../confidential_workflows_test_helpers.go | 30 +++++++++++-------- 5 files changed, 42 insertions(+), 26 deletions(-) diff --git a/system-tests/lib/cre/capabilities/confidentialcompute/confidentialcompute.go b/system-tests/lib/cre/capabilities/confidentialcompute/confidentialcompute.go index 5b53db92844..c7bc82d7cd8 100644 --- a/system-tests/lib/cre/capabilities/confidentialcompute/confidentialcompute.go +++ b/system-tests/lib/cre/capabilities/confidentialcompute/confidentialcompute.go @@ -27,7 +27,6 @@ import ( kcr "github.com/smartcontractkit/chainlink-evm/gethwrappers/keystone/generated/capabilities_registry_1_1_0" "github.com/smartcontractkit/chainlink-protos/cre/go/values" keystone_changeset "github.com/smartcontractkit/chainlink/deployment/keystone/changeset" - "github.com/smartcontractkit/chainlink/system-tests/lib/cre" ) @@ -69,7 +68,7 @@ func workflowEncryptionKey(workerNode *cre.Node) ([32]byte, error) { } `json:"attributes"` } `json:"data"` } - if err := json.Unmarshal(body, &workflowKeysResp); err != nil { + if err = json.Unmarshal(body, &workflowKeysResp); err != nil { return publicKey, errors.Wrap(err, "failed to unmarshal workflow keys response") } if len(workflowKeysResp.Data) == 0 { diff --git a/system-tests/lib/cre/capabilities/confidentialcompute/confidentialcompute_test.go b/system-tests/lib/cre/capabilities/confidentialcompute/confidentialcompute_test.go index 068a8dcb888..6101f62b915 100644 --- a/system-tests/lib/cre/capabilities/confidentialcompute/confidentialcompute_test.go +++ b/system-tests/lib/cre/capabilities/confidentialcompute/confidentialcompute_test.go @@ -6,22 +6,25 @@ import ( "github.com/stretchr/testify/require" cctypes "github.com/smartcontractkit/chainlink-confidential-compute/types" - "github.com/smartcontractkit/chainlink/system-tests/lib/cre" ) func nodeSetWithValues(name string, values map[string]any) *cre.NodeSet { return &cre.NodeSet{ CapabilityConfigs: map[cre.CapabilityFlag]cre.CapabilityConfig{ - cre.CapabilityFlag(name): {Values: values}, + name: {Values: values}, }, } } func TestEnclavesFromConfig(t *testing.T) { + t.Parallel() + const name = "confidential-workflows" t.Run("parses declared enclaves", func(t *testing.T) { + t.Parallel() + ns := nodeSetWithValues(name, map[string]any{ EnclavesConfigKey: `[{"enclaveURL":"http://10.0.0.1:8080","enclaveAuthHeader":"key-a"},` + `{"enclaveURL":"http://10.0.0.1:8081"}]`, @@ -36,6 +39,8 @@ func TestEnclavesFromConfig(t *testing.T) { }) t.Run("round-trips a marshalled enclave list", func(t *testing.T) { + t.Parallel() + encoded, err := MarshalEnclaves([]cctypes.Enclave{{ EnclaveURL: "http://10.0.0.2:8080", TrustedValues: [][]byte{[]byte("fake-measurements")}, @@ -52,6 +57,8 @@ func TestEnclavesFromConfig(t *testing.T) { }) t.Run("returns nil when unconfigured", func(t *testing.T) { + t.Parallel() + for _, tc := range []struct { desc string nodeSet *cre.NodeSet @@ -62,6 +69,8 @@ func TestEnclavesFromConfig(t *testing.T) { {"different capability", nodeSetWithValues("confidential-http", map[string]any{EnclavesConfigKey: "[]"})}, } { t.Run(tc.desc, func(t *testing.T) { + t.Parallel() + got, err := EnclavesFromConfig(tc.nodeSet, name) require.NoError(t, err) require.Nil(t, got) @@ -70,11 +79,15 @@ func TestEnclavesFromConfig(t *testing.T) { }) t.Run("errors on a non-string value", func(t *testing.T) { + t.Parallel() + _, err := EnclavesFromConfig(nodeSetWithValues(name, map[string]any{EnclavesConfigKey: 42}), name) require.ErrorContains(t, err, "must be a JSON string") }) t.Run("errors on malformed JSON", func(t *testing.T) { + t.Parallel() + _, err := EnclavesFromConfig(nodeSetWithValues(name, map[string]any{EnclavesConfigKey: "{not json"}), name) require.ErrorContains(t, err, "failed to parse") }) diff --git a/system-tests/lib/cre/features/confidentialworkflows/confidentialworkflows.go b/system-tests/lib/cre/features/confidentialworkflows/confidentialworkflows.go index 283ba59d368..7722da33a71 100644 --- a/system-tests/lib/cre/features/confidentialworkflows/confidentialworkflows.go +++ b/system-tests/lib/cre/features/confidentialworkflows/confidentialworkflows.go @@ -19,7 +19,6 @@ import ( job_types "github.com/smartcontractkit/chainlink/deployment/cre/jobs/types" "github.com/smartcontractkit/chainlink/deployment/cre/pkg/offchain" keystone_changeset "github.com/smartcontractkit/chainlink/deployment/keystone/changeset" - "github.com/smartcontractkit/chainlink/system-tests/lib/cre" "github.com/smartcontractkit/chainlink/system-tests/lib/cre/capabilities/confidentialcompute" "github.com/smartcontractkit/chainlink/system-tests/lib/cre/don/jobs/standardcapability" diff --git a/system-tests/tests/smoke/cre/confidential_workflows_test.go b/system-tests/tests/smoke/cre/confidential_workflows_test.go index 6b44b65da57..bd0b1435897 100644 --- a/system-tests/tests/smoke/cre/confidential_workflows_test.go +++ b/system-tests/tests/smoke/cre/confidential_workflows_test.go @@ -18,13 +18,12 @@ import ( "github.com/rs/zerolog" "github.com/stretchr/testify/require" - "github.com/smartcontractkit/chainlink-testing-framework/framework" - ns "github.com/smartcontractkit/chainlink-testing-framework/framework/components/simple_node_set" - "github.com/smartcontractkit/chainlink-testing-framework/seth" - "github.com/smartcontractkit/chainlink-confidential-compute/tests/testhelpers" cctypes "github.com/smartcontractkit/chainlink-confidential-compute/types" capabilities_registry_v2 "github.com/smartcontractkit/chainlink-evm/gethwrappers/workflow/generated/capabilities_registry_wrapper_v2" + "github.com/smartcontractkit/chainlink-testing-framework/framework" + ns "github.com/smartcontractkit/chainlink-testing-framework/framework/components/simple_node_set" + "github.com/smartcontractkit/chainlink-testing-framework/seth" keystone_changeset "github.com/smartcontractkit/chainlink/deployment/keystone/changeset" crelib "github.com/smartcontractkit/chainlink/system-tests/lib/cre" "github.com/smartcontractkit/chainlink/system-tests/lib/cre/capabilities/confidentialcompute" @@ -239,7 +238,7 @@ func publishEnclaves( capRegAddr := crecontracts.MustGetAddressFromDataStore( testEnv.CreEnvironment.CldfEnvironment.DataStore, - testEnv.CreEnvironment.Blockchains[0].ChainSelector(), //nolint:staticcheck // mirrors system-tests usage + testEnv.CreEnvironment.Blockchains[0].ChainSelector(), keystone_changeset.CapabilitiesRegistry.String(), testEnv.CreEnvironment.ContractVersions[keystone_changeset.CapabilitiesRegistry.String()], "", @@ -312,7 +311,7 @@ func injectVaultPublicKey(t *testing.T, testEnv *ttypes.TestEnvironment, testLog capRegAddr := crecontracts.MustGetAddressFromDataStore( testEnv.CreEnvironment.CldfEnvironment.DataStore, - testEnv.CreEnvironment.Blockchains[0].ChainSelector(), //nolint:staticcheck // mirrors system-tests usage + testEnv.CreEnvironment.Blockchains[0].ChainSelector(), keystone_changeset.CapabilitiesRegistry.String(), testEnv.CreEnvironment.ContractVersions[keystone_changeset.CapabilitiesRegistry.String()], "", @@ -365,7 +364,7 @@ func registerConfidentialWorkflow( wfRegistryRef := crecontracts.MustGetAddressRefFromDataStore( testEnv.CreEnvironment.CldfEnvironment.DataStore, - testEnv.CreEnvironment.Blockchains[0].ChainSelector(), //nolint:staticcheck // mirrors system-tests usage + testEnv.CreEnvironment.Blockchains[0].ChainSelector(), keystone_changeset.WorkflowRegistry.String(), testEnv.CreEnvironment.ContractVersions[keystone_changeset.WorkflowRegistry.String()], "", @@ -433,8 +432,8 @@ func waitForConfidentialWorkflowExecution( deadline := time.Now().Add(timeout) for { for _, name := range containers { - out, _ := exec.Command("docker", "logs", "--tail", "10000", name).CombinedOutput() - for _, line := range bytes.Split(out, []byte{'\n'}) { + out, _ := exec.CommandContext(t.Context(), "docker", "logs", "--tail", "10000", name).CombinedOutput() + for line := range bytes.SplitSeq(out, []byte{'\n'}) { if bytes.Contains(line, needleMsg) && bytes.Contains(line, needleID) { testLogger.Info().Str("container", name).Msg("Found successful execution log") return diff --git a/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go b/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go index 4d5ebeeed58..f058ac4dcd6 100644 --- a/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go +++ b/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go @@ -8,6 +8,7 @@ import ( "encoding/base64" "encoding/hex" "encoding/json" + "errors" "fmt" "net" "net/http" @@ -22,14 +23,13 @@ import ( "time" "github.com/andybalholm/brotli" + "github.com/ethereum/go-ethereum/common" "github.com/rs/zerolog" "github.com/stretchr/testify/require" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "github.com/ethereum/go-ethereum/common" - "github.com/smartcontractkit/chainlink-confidential-compute/tests/testhelpers" cctypes "github.com/smartcontractkit/chainlink-confidential-compute/types" "github.com/smartcontractkit/chainlink-confidential-compute/util" @@ -46,7 +46,11 @@ import ( const ( // confidentialWorkflowsApp is the enclave application, the DON capability flag // and the capability binary name; all three share this value. - confidentialWorkflowsApp = string(crelib.ConfidentialWorkflowsCapability) + confidentialWorkflowsApp = crelib.ConfidentialWorkflowsCapability + + // confidentialServerReadHeaderTimeout bounds header reads on the test's local + // HTTP servers. + confidentialServerReadHeaderTimeout = 10 * time.Second // confidentialEnclaveRefreshWait covers two of the capability's registry // refresh intervals, so a refresh already in flight when the enclave list @@ -117,10 +121,10 @@ func newDeferredGatewayProxy(t *testing.T, port int) *deferredGatewayProxy { rp.ServeHTTP(w, r) }) - listener, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", port)) + listener, err := (&net.ListenConfig{}).Listen(t.Context(), "tcp", fmt.Sprintf("0.0.0.0:%d", port)) require.NoError(t, err, "failed to listen on port %d for gateway proxy", port) - p.server = &http.Server{Handler: handler} + p.server = &http.Server{Handler: handler, ReadHeaderTimeout: confidentialServerReadHeaderTimeout} go func() { _ = p.server.Serve(listener) }() t.Cleanup(func() { _ = p.server.Close() }) @@ -170,7 +174,7 @@ func (f *fakeStorageService) DownloadArtifact(_ context.Context, req *storage_se return nil, status.Errorf(codes.NotFound, "fake storage: artifact with id %q not found (expected a bare id, not a URL)", req.GetId()) } if u == "" { - return nil, fmt.Errorf("fake storage: artifact url not set yet") + return nil, errors.New("fake storage: artifact url not set yet") } return &storage_service.DownloadArtifactResponse{Url: u}, nil } @@ -181,7 +185,8 @@ func (f *fakeStorageService) DownloadArtifact(_ context.Context, req *storage_se func startFakeStorageService(t *testing.T, enclaveHost string) (string, *fakeStorageService) { t.Helper() - lis, err := net.Listen("tcp", "0.0.0.0:0") + // Binds every interface because the enclave dials it from outside this process. + lis, err := (&net.ListenConfig{}).Listen(t.Context(), "tcp", "0.0.0.0:0") require.NoError(t, err, "fake storage listener") svc := &fakeStorageService{} @@ -225,7 +230,7 @@ func buildAndServeConfidentialWorkflow(t *testing.T, ccRoot string, configJSON s tmpDir := t.TempDir() outFile := filepath.Join(tmpDir, "workflow-test.wasm") - cmd := exec.Command("go", "build", "-o", outFile, ".") + cmd := exec.CommandContext(t.Context(), "go", "build", "-o", outFile, ".") cmd.Dir = srcDir cmd.Env = append(os.Environ(), "GOOS=wasip1", "GOARCH=wasm", "CGO_ENABLED=0") output, err := cmd.CombinedOutput() @@ -261,9 +266,10 @@ func buildAndServeConfidentialWorkflow(t *testing.T, ccRoot string, configJSON s _, _ = rw.Write([]byte(configJSON)) }) - listener, err := net.Listen("tcp", "0.0.0.0:0") + // Binds every interface because the enclave fetches artifacts over the host network. + listener, err := (&net.ListenConfig{}).Listen(t.Context(), "tcp", "0.0.0.0:0") require.NoError(t, err, "workflow artifact listener") - srv := &http.Server{Handler: mux} + srv := &http.Server{Handler: mux, ReadHeaderTimeout: confidentialServerReadHeaderTimeout} go func() { _ = srv.Serve(listener) }() t.Cleanup(func() { _ = srv.Close() }) @@ -310,7 +316,7 @@ func configureEnclaves( require.NoError(t, err, "failed to hex-decode vault public key") // don.F for an N-node DON is N/3; the enclave's own F/T is 2*don.F + 1. - donF := uint32(len(workers) / 3) + donF := uint32(len(workers) / 3) //nolint:gosec // G115: the worker count comes from the topology quorum := 2*donF + 1 config := cctypes.EnclaveConfig{ @@ -375,7 +381,7 @@ func storeConfidentialWorkflowSecret( wfRegAddr := crecontracts.MustGetAddressFromDataStore( testEnv.CreEnvironment.CldfEnvironment.DataStore, - testEnv.CreEnvironment.Blockchains[0].ChainSelector(), //nolint:staticcheck // mirrors system-tests usage + testEnv.CreEnvironment.Blockchains[0].ChainSelector(), keystone_changeset.WorkflowRegistry.String(), testEnv.CreEnvironment.ContractVersions[keystone_changeset.WorkflowRegistry.String()], "", From 60e5249f4d481f05aebdc16aca373360a4db19a4 Mon Sep 17 00:00:00 2001 From: vreff <104409744+vreff@users.noreply.github.com> Date: Thu, 13 Aug 2026 09:41:21 -0400 Subject: [PATCH 19/29] fix tests --- .../confidentialworkflows.go | 27 ++++++++-- .../smoke/cre/confidential_workflows_test.go | 52 ++++++++++++++++++- 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/system-tests/lib/cre/features/confidentialworkflows/confidentialworkflows.go b/system-tests/lib/cre/features/confidentialworkflows/confidentialworkflows.go index 7722da33a71..ea14cf2a0ea 100644 --- a/system-tests/lib/cre/features/confidentialworkflows/confidentialworkflows.go +++ b/system-tests/lib/cre/features/confidentialworkflows/confidentialworkflows.go @@ -11,16 +11,19 @@ import ( "context" "fmt" + "dario.cat/mergo" "github.com/google/uuid" "github.com/pkg/errors" "github.com/rs/zerolog" cre_jobs "github.com/smartcontractkit/chainlink/deployment/cre/jobs" + cre_jobs_ops "github.com/smartcontractkit/chainlink/deployment/cre/jobs/operations" job_types "github.com/smartcontractkit/chainlink/deployment/cre/jobs/types" "github.com/smartcontractkit/chainlink/deployment/cre/pkg/offchain" keystone_changeset "github.com/smartcontractkit/chainlink/deployment/keystone/changeset" "github.com/smartcontractkit/chainlink/system-tests/lib/cre" "github.com/smartcontractkit/chainlink/system-tests/lib/cre/capabilities/confidentialcompute" + "github.com/smartcontractkit/chainlink/system-tests/lib/cre/don/jobs" "github.com/smartcontractkit/chainlink/system-tests/lib/cre/don/jobs/standardcapability" ) @@ -71,10 +74,10 @@ func (o *ConfidentialWorkflows) PreEnvStartup( // PostEnvStartup proposes the capability job on each worker node, sealing the // capability's API key to every node's workflow key. func (o *ConfidentialWorkflows) PostEnvStartup( - _ context.Context, + ctx context.Context, _ zerolog.Logger, don *cre.Don, - _ *cre.Dons, + dons *cre.Dons, creEnv *cre.Environment, ) error { if !don.HasFlag(flag) { @@ -129,10 +132,28 @@ func (o *ConfidentialWorkflows) PostEnvStartup( return errors.Wrapf(err, "precondition verification failed for %s worker job", flag) } - if _, err := (cre_jobs.ProposeJobSpec{}).Apply(*creEnv.CldfEnvironment, input); err != nil { + report, err := (cre_jobs.ProposeJobSpec{}).Apply(*creEnv.CldfEnvironment, input) + if err != nil { return errors.Wrapf(err, "failed to propose %s worker job spec", flag) } + // Proposing only queues the spec in Job Distributor; without approval the + // nodes never run it and the capability never registers locally. + specs := make(map[string][]string) + for _, r := range report.Reports { + out, ok := r.Output.(cre_jobs_ops.ProposeStandardCapabilityJobOutput) + if !ok { + return fmt.Errorf("unable to cast to ProposeStandardCapabilityJobOutput, actual type: %T", r.Output) + } + if mErr := mergo.Merge(&specs, out.Specs, mergo.WithAppendSlice); mErr != nil { + return errors.Wrapf(mErr, "failed to merge %s worker job specs", flag) + } + } + + if aErr := jobs.Approve(ctx, creEnv.CldfEnvironment.Offchain, dons, specs); aErr != nil { + return errors.Wrapf(aErr, "failed to approve %s worker jobs", flag) + } + return nil } diff --git a/system-tests/tests/smoke/cre/confidential_workflows_test.go b/system-tests/tests/smoke/cre/confidential_workflows_test.go index bd0b1435897..3825f2fa8ad 100644 --- a/system-tests/tests/smoke/cre/confidential_workflows_test.go +++ b/system-tests/tests/smoke/cre/confidential_workflows_test.go @@ -441,12 +441,62 @@ func waitForConfidentialWorkflowExecution( } } if time.Now().After(deadline) { - t.Fatalf("timed out after %s waiting for a successful execution of workflow %s", timeout, workflowID) + t.Fatalf("timed out after %s waiting for a successful execution of workflow %s\n%s", + timeout, workflowID, confidentialExecutionDiagnostics(t, containers)) } time.Sleep(5 * time.Second) } } +// confidentialExecutionDiagnostics summarises why no execution succeeded, so a +// timeout reports the node-side cause instead of only the absence of a success +// log. Returns one deduplicated line per distinct error. +func confidentialExecutionDiagnostics(t *testing.T, containers []string) string { + t.Helper() + + needles := [][]byte{ + []byte("Workflow Engine initialization failed"), + []byte("Workflow execution failed"), + []byte("failed to get regions from"), + []byte("no compatible capability found"), + } + + seen := map[string]bool{} + var found []string + for _, name := range containers { + out, _ := exec.CommandContext(t.Context(), "docker", "logs", "--tail", "10000", name).CombinedOutput() + for line := range bytes.SplitSeq(out, []byte{'\n'}) { + for _, needle := range needles { + if !bytes.Contains(line, needle) { + continue + } + // Key on the message alone; every node logs the same failure. + key := string(needle) + if !seen[key] { + seen[key] = true + found = append(found, fmt.Sprintf(" [%s] %s", name, truncateForLog(line, 400))) + } + break + } + } + } + + if len(found) == 0 { + return "no workflow engine or execution errors found in the node logs" + } + + return "node-side errors:\n" + strings.Join(found, "\n") +} + +// truncateForLog shortens a log line so a failure message stays readable. +func truncateForLog(line []byte, maxLen int) string { + if len(line) <= maxLen { + return string(line) + } + + return string(line[:maxLen]) + "... (truncated)" +} + // confidentialWorkflowDONContainers returns the chainlink container names for // every nodeset whose DON carries the workflow DON flag. func confidentialWorkflowDONContainers(testEnv *ttypes.TestEnvironment) []string { From 0fd5e7ef224a517ca362e37bae06397b4f8f11a1 Mon Sep 17 00:00:00 2001 From: vreff <104409744+vreff@users.noreply.github.com> Date: Tue, 18 Aug 2026 08:29:18 -0400 Subject: [PATCH 20/29] Update system-tests/tests/smoke/cre/confidential_workflows_test.go Co-authored-by: Steve Ellis --- .../tests/smoke/cre/confidential_workflows_test.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/system-tests/tests/smoke/cre/confidential_workflows_test.go b/system-tests/tests/smoke/cre/confidential_workflows_test.go index 3825f2fa8ad..a9636f66fcf 100644 --- a/system-tests/tests/smoke/cre/confidential_workflows_test.go +++ b/system-tests/tests/smoke/cre/confidential_workflows_test.go @@ -89,7 +89,12 @@ func Test_CRE_V2_ConfidentialWorkflows_Relay(t *testing.T) { gwProxy := newDeferredGatewayProxy(t, confidentialGatewayProxyPort) enclaveHost := confidentialEnclaveHostAddr(fake) storageAddr, storageSvc := startFakeStorageService(t, enclaveHost) - + // Both env vars below configure the enclave *host servers*, not this repo: + // the harness launches them with this process's environment inherited + // (testhelpers.MustSetupEnclaveWithEnv appends to os.Environ()) + // REQUIRE_BFT_QUORUM makes each host demand a 2f+1 BFT supermajority of node + // signatures instead of f+1 (enclave/nitro/host), matching the relay's + // requireBFTQuorum = true in the topology TOML. t.Setenv("REQUIRE_BFT_QUORUM", "true") t.Setenv("ENCLAVE_SETTINGS", fmt.Sprintf( `{"storageKey":%q,"storageServiceUrl":%q,"storageServiceTls":false,"gatewayUrl":%q}`, From 25f22a32448ba8973cb7ba5bce93c651dace45ef Mon Sep 17 00:00:00 2001 From: vreff <104409744+vreff@users.noreply.github.com> Date: Tue, 18 Aug 2026 08:31:44 -0400 Subject: [PATCH 21/29] Skip for lack of envar --- .../tests/smoke/cre/confidential_workflows_test.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/system-tests/tests/smoke/cre/confidential_workflows_test.go b/system-tests/tests/smoke/cre/confidential_workflows_test.go index 3825f2fa8ad..5d53021425a 100644 --- a/system-tests/tests/smoke/cre/confidential_workflows_test.go +++ b/system-tests/tests/smoke/cre/confidential_workflows_test.go @@ -76,6 +76,10 @@ func Test_CRE_V2_ConfidentialWorkflows_Relay(t *testing.T) { testLogger := framework.L t.Run("Confidential Workflows Relay - "+topology, func(t *testing.T) { + // Resolved first so the test skips before standing up anything when the + // confidential-compute checkout is not available. + ccRoot := confidentialComputeRoot(t) + fake := testhelpers.UseFakeEnclave() testLogger.Info().Bool("fakeEnclaves", fake).Msg("Starting confidential workflows relay test") @@ -100,7 +104,6 @@ func Test_CRE_V2_ConfidentialWorkflows_Relay(t *testing.T) { // 2. Start the enclaves. This is the whole point of depending on // chainlink-confidential-compute's harness from this repository. - ccRoot := confidentialComputeRoot(t) enclaveCfg := testhelpers.DefaultLocalEnclaveSetupConfig(ccRoot, confidentialWorkflowsApp) enclaveCfg.Region = confidentialEnclaveRegion enclaves := testhelpers.SetupLocalEnclaves(t, enclaveCfg) @@ -187,14 +190,16 @@ func confidentialEnclaveHostAddr(fake bool) string { } // confidentialComputeRoot resolves the chainlink-confidential-compute checkout the -// enclave harness builds and runs the enclave from. +// enclave harness builds and runs the enclave from. The test is skipped when it is +// not set, so a local `go test ./...` does not fail on a missing checkout. func confidentialComputeRoot(t *testing.T) string { t.Helper() root := os.Getenv("CONFIDENTIAL_COMPUTE_ROOT") - require.NotEmpty(t, root, - "CONFIDENTIAL_COMPUTE_ROOT must point at a chainlink-confidential-compute checkout; "+ + if root == "" { + t.Skip("CONFIDENTIAL_COMPUTE_ROOT must point at a chainlink-confidential-compute checkout; " + "CI sets this from the confidential-workflows gitRef in plugins/plugins.public.yaml") + } abs, err := filepath.Abs(root) require.NoError(t, err, "resolving CONFIDENTIAL_COMPUTE_ROOT") From 8360703a66e5dd601e5a23133bdf89f153b25b10 Mon Sep 17 00:00:00 2001 From: vreff <104409744+vreff@users.noreply.github.com> Date: Tue, 18 Aug 2026 08:33:52 -0400 Subject: [PATCH 22/29] move comment --- system-tests/tests/smoke/cre/confidential_workflows_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system-tests/tests/smoke/cre/confidential_workflows_test.go b/system-tests/tests/smoke/cre/confidential_workflows_test.go index 195888ff8f4..024ebd6ecda 100644 --- a/system-tests/tests/smoke/cre/confidential_workflows_test.go +++ b/system-tests/tests/smoke/cre/confidential_workflows_test.go @@ -224,8 +224,6 @@ func confidentialGatewayURL(t *testing.T, testEnv *ttypes.TestEnvironment) strin return fmt.Sprintf("%s://%s:%d%s", incoming.Protocol, host, incoming.ExternalPort, incoming.Path) } -// injectVaultPublicKey writes the vault DON's DKG public key and threshold into -// the vault capability's registry config. // publishEnclaves writes the enclave list into the capability's on-chain // registry config and waits for the capability to pick it up. // @@ -309,6 +307,8 @@ func donNameForCapability( return "" } +// injectVaultPublicKey writes the vault DON's DKG public key and threshold into +// the vault capability's registry config. func injectVaultPublicKey(t *testing.T, testEnv *ttypes.TestEnvironment, testLogger zerolog.Logger, gatewayURL string) string { t.Helper() From d6ddce00f70a7bd10b3b5925efda345242e4a617 Mon Sep 17 00:00:00 2001 From: vreff <104409744+vreff@users.noreply.github.com> Date: Tue, 18 Aug 2026 08:34:30 -0400 Subject: [PATCH 23/29] make function private --- .../scripts/cre/environment/environment/environment.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/core/scripts/cre/environment/environment/environment.go b/core/scripts/cre/environment/environment/environment.go index 60bfcd5c513..5b36b7a3938 100644 --- a/core/scripts/cre/environment/environment/environment.go +++ b/core/scripts/cre/environment/environment/environment.go @@ -363,7 +363,7 @@ func startCmd() *cobra.Command { } features := feature_set.New() - gatewayWhitelistConfig := DefaultGatewayWhitelistConfig(in, extraAllowedGatewayPorts) + gatewayWhitelistConfig := defaultGatewayWhitelistConfig(in, extraAllowedGatewayPorts) output, startErr := StartCLIEnvironment(cmdContext, relativePathToRepoRoot, in, nil, features, nil, envDependencies, gatewayWhitelistConfig) if startErr != nil { fmt.Fprintf(os.Stderr, "Error: %s\n", startErr) @@ -844,11 +844,9 @@ func statusCmd() *cobra.Command { return cmd } -// DefaultGatewayWhitelistConfig builds the Gateway Connector's outbound allowlist: -// the caller's extra ports plus the fake service ports the config declares. Shared -// by `cre env start` and by tests that call StartCLIEnvironment directly, so both -// grant the same access. -func DefaultGatewayWhitelistConfig(in *envconfig.Config, extraAllowedPorts []int) gateway.WhitelistConfig { +// defaultGatewayWhitelistConfig builds the Gateway Connector's outbound allowlist: +// the caller's extra ports plus the fake service ports the config declares. +func defaultGatewayWhitelistConfig(in *envconfig.Config, extraAllowedPorts []int) gateway.WhitelistConfig { ports := append([]int(nil), extraAllowedPorts...) if in.Fake != nil { ports = append(ports, in.Fake.Port) From 7f312e0e8a24b681cbfe6dcb649e8331cef6d7ad Mon Sep 17 00:00:00 2001 From: vreff <104409744+vreff@users.noreply.github.com> Date: Tue, 18 Aug 2026 08:37:12 -0400 Subject: [PATCH 24/29] remove helper func --- .../tests/smoke/cre/confidential_workflows_test_helpers.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go b/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go index f058ac4dcd6..2d26ffc573d 100644 --- a/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go +++ b/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go @@ -329,7 +329,7 @@ func configureEnclaves( require.NoError(t, err, "failed to marshal enclave config") enclaveType := cctypes.EnclaveTypeNitro - if UseFakeEnclaveForTest() { + if testhelpers.UseFakeEnclave() { enclaveType = cctypes.EnclaveTypeFake } @@ -356,11 +356,6 @@ func configureEnclaves( } } -// UseFakeEnclaveForTest reports whether the harness selected fake enclaves. -func UseFakeEnclaveForTest() bool { - return testhelpers.UseFakeEnclave() -} - // storeConfidentialWorkflowSecret encrypts a secret to the vault's public key and // stores it in the vault DON through the gateway, so the workflow's GetSecret call // resolves. Reuses the vault request helpers already in this package. From f4911b6f4d5a164f29d7589e20987e85d3770bc4 Mon Sep 17 00:00:00 2001 From: vreff <104409744+vreff@users.noreply.github.com> Date: Tue, 18 Aug 2026 09:12:54 -0400 Subject: [PATCH 25/29] fix md --- go.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.md b/go.md index eb284fe6236..0e6294d64a0 100644 --- a/go.md +++ b/go.md @@ -818,5 +818,5 @@ flowchart LR click testrig-repo href "https://github.com/smartcontractkit/testrig" classDef outline stroke-dasharray:6,fill:none; - class chainlink-repo,chainlink-aptos-repo,chainlink-ccip-repo,chainlink-ccv-repo,chainlink-common-repo,chainlink-confidential-compute-repo,chainlink-evm-repo,chainlink-framework-repo,chainlink-protos-repo,chainlink-solana-repo,chainlink-stellar-repo,chainlink-sui-repo,chainlink-testing-framework-repo,chainlink-ton-repo,cre-sdk-go-repo,tdh2-repo,testrig-repo outline + class chainlink-repo,chainlink-aptos-repo,chainlink-ccip-repo,chainlink-ccv-repo,chainlink-common-repo,chainlink-confidential-compute-repo,chainlink-evm-repo,chainlink-framework-repo,chainlink-protos-repo,chainlink-solana-repo,chainlink-stellar-repo,chainlink-sui-repo,chainlink-testing-framework-repo,chainlink-ton-repo,cre-sdk-go-repo,testrig-repo outline ``` From 0bcef345554e05b6ff8526f6cd867d28b5bdc96c Mon Sep 17 00:00:00 2001 From: vreff <104409744+vreff@users.noreply.github.com> Date: Tue, 18 Aug 2026 12:28:14 -0400 Subject: [PATCH 26/29] Update system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go Co-authored-by: Steve Ellis --- .../smoke/cre/confidential_workflows_test_helpers.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go b/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go index 2d26ffc573d..1bb8092ae55 100644 --- a/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go +++ b/system-tests/tests/smoke/cre/confidential_workflows_test_helpers.go @@ -303,7 +303,8 @@ func configureEnclaves( ) { t.Helper() - workers, err := testEnv.Dons.MustWorkflowDON().Workers() + don := testEnv.Dons.MustWorkflowDON() + workers, err := don.Workers() require.NoError(t, err, "failed to get worker nodes from topology") require.NotEmpty(t, workers, "workflow DON has no worker nodes") @@ -315,9 +316,11 @@ func configureEnclaves( masterPublicKey, err := hex.DecodeString(vaultPublicKey) require.NoError(t, err, "failed to hex-decode vault public key") - // don.F for an N-node DON is N/3; the enclave's own F/T is 2*don.F + 1. - donF := uint32(len(workers) / 3) //nolint:gosec // G115: the worker count comes from the topology - quorum := 2*donF + 1 + // Quorum tracks the DON's registered fault tolerance (Don.F, computed as + // (workers-1)/3 in NewDON), not a re-derivation from the worker count: the + // two diverge for e.g. 6-node DONs, and the enclave would then demand more + // signatures than the DON can produce. + quorum := 2*uint32(don.F) + 1 config := cctypes.EnclaveConfig{ Signers: signers, From e1bee28198b28bad980ec9ba638eff6d39f0feae Mon Sep 17 00:00:00 2001 From: vreff <104409744+vreff@users.noreply.github.com> Date: Tue, 18 Aug 2026 14:24:10 -0400 Subject: [PATCH 27/29] Update system-tests/lib/cre/registry_update.go Co-authored-by: Steve Ellis --- system-tests/lib/cre/registry_update.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system-tests/lib/cre/registry_update.go b/system-tests/lib/cre/registry_update.go index 4c6240c12e4..80ce6d6bf95 100644 --- a/system-tests/lib/cre/registry_update.go +++ b/system-tests/lib/cre/registry_update.go @@ -56,7 +56,7 @@ func UpdateDONCapabilityConfig( return errors.Errorf("capability %q is not configured on DON %q", capabilityName, donName) } - tx, err := capReg.UpdateDON(sethClient.NewTXOpts(), don.Id, capabilities_registry_v2.CapabilitiesRegistryUpdateDONParams{ + if _, err := sethClient.Decode(capReg.UpdateDON(sethClient.NewTXOpts(), don.Id, capabilities_registry_v2.CapabilitiesRegistryUpdateDONParams{ Name: don.Name, Config: don.Config, CapabilityConfigurations: updated, From d305f3467bf466b8f6b533342dd95d5a77a20ab3 Mon Sep 17 00:00:00 2001 From: vreff <104409744+vreff@users.noreply.github.com> Date: Tue, 18 Aug 2026 14:27:58 -0400 Subject: [PATCH 28/29] fix insecure slice --- .../capabilities/confidentialcompute/confidentialcompute.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/system-tests/lib/cre/capabilities/confidentialcompute/confidentialcompute.go b/system-tests/lib/cre/capabilities/confidentialcompute/confidentialcompute.go index c7bc82d7cd8..27c7c1aa830 100644 --- a/system-tests/lib/cre/capabilities/confidentialcompute/confidentialcompute.go +++ b/system-tests/lib/cre/capabilities/confidentialcompute/confidentialcompute.go @@ -45,6 +45,9 @@ func workflowEncryptionKey(workerNode *cre.Node) ([32]byte, error) { if err != nil { return publicKey, errors.Wrap(err, "failed to create request to get workflow keys") } + if len(apiClient.Cookies) == 0 { + return publicKey, errors.New("no session cookie available for get workflow keys request") + } req.AddCookie(apiClient.Cookies[0]) resp, err := apiClient.GetClient().Do(req) From 84c1ec4f4431ba16250aa7721f233df5110e0979 Mon Sep 17 00:00:00 2001 From: vreff <104409744+vreff@users.noreply.github.com> Date: Tue, 18 Aug 2026 14:36:44 -0400 Subject: [PATCH 29/29] lint fixes --- system-tests/lib/cre/registry_update.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/system-tests/lib/cre/registry_update.go b/system-tests/lib/cre/registry_update.go index 80ce6d6bf95..029e2c0a9a2 100644 --- a/system-tests/lib/cre/registry_update.go +++ b/system-tests/lib/cre/registry_update.go @@ -56,21 +56,17 @@ func UpdateDONCapabilityConfig( return errors.Errorf("capability %q is not configured on DON %q", capabilityName, donName) } - if _, err := sethClient.Decode(capReg.UpdateDON(sethClient.NewTXOpts(), don.Id, capabilities_registry_v2.CapabilitiesRegistryUpdateDONParams{ + _, err = sethClient.Decode(capReg.UpdateDON(sethClient.NewTXOpts(), don.Id, capabilities_registry_v2.CapabilitiesRegistryUpdateDONParams{ Name: don.Name, Config: don.Config, CapabilityConfigurations: updated, Nodes: don.NodeP2PIds, F: don.F, IsPublic: don.IsPublic, - }) + })) if err != nil { return errors.Wrapf(err, "failed to submit updateDON for DON %q", donName) } - if _, err := bind.WaitMined(ctx, sethClient.Client, tx); err != nil { - return errors.Wrapf(err, "failed waiting for updateDON of DON %q to be mined", donName) - } - return nil }