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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions .github/workflows/test-cl-smoke.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ jobs:
run_cmd: TestHA_CrossComponentDown
config: "env.toml,env-HA.toml,env-cl.toml,env-cl-ci.toml"
timeout: 10m
- name: Phased TestE2ESmoke_Basic
run_cmd: TestE2ESmoke_Basic_Phased
config: "env-phased.toml,env-cl-phased.toml,env-cl-ci-phased.toml"
flags: --env-mode phased
smoke_test_config: ../../env-phased-out.toml
timeout: 15m
# We need to configure HeadTracker for the CL tests to have finality depth. Otherwise, it does instant finality.
# - name: TestE2EReorg
# config: "env.toml,env-src-auto-mine.toml,env-cl.toml,env-cl-ci.toml"
Expand Down Expand Up @@ -125,7 +131,7 @@ jobs:
role-to-assume: ${{ secrets.CCV_IAM_ROLE }}
aws-region: us-east-1
registry-type: public

- name: Authenticate to AWS ECR (JD)
uses: ./.github/actions/aws-ecr-auth
with:
Expand Down Expand Up @@ -162,18 +168,20 @@ jobs:
env:
JD_IMAGE: ${{ secrets.JD_IMAGE }}
run: |
ccv u ${{ matrix.test.config }}
ccv ${{ matrix.test.flags || '' }} u ${{ matrix.test.config }}

- name: Run Smoke Test
id: test_run
working-directory: build/devenv/tests/e2e
env:
LOKI_URL: http://localhost:3030/loki/api/v1/push
# When unset, the test falls back to ../../env-out.toml (see GetSmokeTestConfig).
SMOKE_TEST_CONFIG: ${{ matrix.test.smoke_test_config || '' }}
run: |
set -o pipefail
go test -v -timeout ${{ matrix.test.timeout }} -count=1 -run ${{ matrix.test.run_cmd }}
continue-on-error: true

- name: Dump logs if they're not already dumped
if: always()
working-directory: build/devenv/tests/e2e
Expand All @@ -184,7 +192,7 @@ jobs:
else
echo "Logs already exist. Skipping manual dump."
fi

- name: Sanitize test name for log artifact name
if: always()
id: sanitize_name
Expand All @@ -200,7 +208,7 @@ jobs:
name: container-logs-cl-${{ steps.sanitize_name.outputs.name }}
path: build/devenv/tests/e2e/logs
retention-days: 1

- name: Check test results
if: always() && steps.test_run.outcome == 'failure'
run: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test-smoke.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ jobs:
config: env.toml,env-src-auto-mine.toml
timeout: 10m
working-directory: build/devenv/tests/e2e
- name: TestE2ESmoke_Basic_Phased
- name: Phased TestE2ESmoke_Basic
run_cmd: TestE2ESmoke_Basic
config: env-phased.toml
flags: --env-mode phased
Expand Down
107 changes: 77 additions & 30 deletions build/devenv/components/committeeccv/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,72 +68,123 @@ func (c *component) RunPhase3(
componentConfig any,
priorOutputs map[string]any,
) (map[string]any, []devenvruntime.Effect, error) {
// The committeeccv config is a single [committeeccv] section holding the
// aggregator and verifier inputs.
cfg, err := decodeConfig(componentConfig)
if err != nil {
return nil, nil, err
}
aggregators, verifiers := cfg.Aggregator, cfg.Verifier

if len(aggregators) == 0 && len(verifiers) == 0 {
return map[string]any{}, nil, nil
}
inputs, err := parsePhase3Inputs(priorOutputs, globalConfig)
if err != nil {
return nil, nil, err
}
// Work on a copy of the shared Phase-2 environment.
localEnv := *inputs.env
if err := ensureAggregatorCredentials(aggregators); err != nil {
return nil, nil, err
}
return runPhase3Core(ctx, inputs, aggregators, verifiers, &localEnv)
}

// phase3Inputs holds the decoded prior-phase outputs consumed by both the
// standalone and CL-node CommitteeCCV components.
type phase3Inputs struct {
jdInfra *jobs.JDInfrastructure
blockchains []*ctfblockchain.Input
blockchainOutputs []*ctfblockchain.Output
env *deployment.Environment
topology *ccvdeployment.EnvironmentTopology
obs *observability.Observability
ds datastore.MutableDataStore
impls []cciptestinterfaces.CCIP17Configuration
selectors []uint64
useLegacyConfigureLane bool
}

func parsePhase3Inputs(priorOutputs, globalConfig map[string]any) (phase3Inputs, error) {
jdInfra, ok := priorOutputs["jd"].(*jobs.JDInfrastructure)
if !ok || jdInfra == nil {
return nil, nil, fmt.Errorf("committeeccv: jd not found in phase outputs")
return phase3Inputs{}, fmt.Errorf("committeeccv: jd not found in phase outputs")
}
blockchains, ok := priorOutputs["blockchains"].([]*ctfblockchain.Input)
if !ok {
return nil, nil, fmt.Errorf("committeeccv: blockchains not found in phase outputs")
return phase3Inputs{}, fmt.Errorf("committeeccv: blockchains not found in phase outputs")
}
blockchainOutputs := blockchainscomp.Outputs(blockchains)
e, ok := priorOutputs["_env"].(*deployment.Environment)
if !ok || e == nil {
return nil, nil, fmt.Errorf("committeeccv: _env not found in phase outputs")
return phase3Inputs{}, fmt.Errorf("committeeccv: _env not found in phase outputs")
}
topology, ok := priorOutputs["environment_topology"].(*ccvdeployment.EnvironmentTopology)
if !ok || topology == nil {
return nil, nil, fmt.Errorf("committeeccv: environment_topology not found in phase outputs")
return phase3Inputs{}, fmt.Errorf("committeeccv: environment_topology not found in phase outputs")
}
obs, ok := priorOutputs["observability"].(*observability.Observability)
if !ok || obs == nil {
return nil, nil, fmt.Errorf("committeeccv: observability not found in phase outputs")
return phase3Inputs{}, fmt.Errorf("committeeccv: observability not found in phase outputs")
}
ds, ok := priorOutputs["_ds"].(datastore.MutableDataStore)
if !ok {
return nil, nil, fmt.Errorf("committeeccv: _ds not found in phase outputs")
return phase3Inputs{}, fmt.Errorf("committeeccv: _ds not found in phase outputs")
}
impls, _ := priorOutputs["_impls"].([]cciptestinterfaces.CCIP17Configuration)
var useLegacyConfigureLane bool
selectors, _ := priorOutputs["_selectors"].([]uint64)
var useLegacy bool
if pcMap, ok := globalConfig["protocol_contracts"].(map[string]any); ok {
useLegacyConfigureLane, _ = pcMap["use_legacy_configure_lane"].(bool)
}
useLegacy, _ = pcMap["use_legacy_configure_lane"].(bool)
}
return phase3Inputs{
jdInfra: jdInfra,
blockchains: blockchains,
blockchainOutputs: blockchainOutputs,
env: e,
topology: topology,
obs: obs,
ds: ds,
impls: impls,
selectors: selectors,
useLegacyConfigureLane: useLegacy,
}, nil
}

// Step 1: Generate HMAC client credentials for all aggregators before launching verifiers.
func ensureAggregatorCredentials(aggregators []*services.AggregatorInput) error {
for _, agg := range aggregators {
if agg == nil {
continue
}
creds, cerr := agg.EnsureClientCredentials()
if cerr != nil {
return nil, nil, fmt.Errorf("committeeccv: failed to ensure client credentials for aggregator %s: %w", agg.CommitteeName, cerr)
return fmt.Errorf("committeeccv: failed to ensure client credentials for aggregator %s: %w", agg.CommitteeName, cerr)
}
if agg.Out == nil {
agg.Out = &services.AggregatorOutput{}
}
agg.Out.ClientCredentials = creds
}
return nil
}

// runPhase3Core runs the shared CommitteeCCV Phase 3 steps (steps 2–8) against
// a local copy of the deployment environment. It is called by both the standalone
// and CL-node components; the CL-node component performs its own step 1b
// (node launch + NodeIDs population) before calling this function.
func runPhase3Core(
ctx context.Context,
inputs phase3Inputs,
aggregators []*services.AggregatorInput,
verifiers []*committeeverifier.Input,
localEnv *deployment.Environment,
) (map[string]any, []devenvruntime.Effect, error) {
// Step 2: Launch standalone verifier containers (reads HMAC creds from agg.Out).
if err := committeeverifier.LaunchStandaloneVerifiers(
verifiers, aggregators, blockchainOutputs, jdInfra,
verifiers, aggregators, inputs.blockchainOutputs, inputs.jdInfra,
chainreg.GetRegistry().GetVerifierModifiers(),
); err != nil {
return nil, nil, fmt.Errorf("committeeccv: failed to launch standalone verifiers: %w", err)
}
if err := committeeverifier.RegisterStandaloneVerifiersWithJD(ctx, verifiers, jdInfra.OffchainClient); err != nil {
if err := committeeverifier.RegisterStandaloneVerifiersWithJD(ctx, verifiers, inputs.jdInfra.OffchainClient); err != nil {
return nil, nil, fmt.Errorf("committeeccv: failed to register standalone verifiers with JD: %w", err)
}

Expand All @@ -151,6 +202,7 @@ func (c *component) RunPhase3(
}
allHostnames = append(allHostnames, "localhost")
tlsCertDir := filepath.Join(util.CCVConfigDir(), "tls-shared")
var err error
sharedTLSCerts, err = services.GenerateTLSCertificates(allHostnames, tlsCertDir)
if err != nil {
return nil, nil, fmt.Errorf("committeeccv: failed to generate shared TLS certificates: %w", err)
Expand All @@ -167,18 +219,17 @@ func (c *component) RunPhase3(

// Step 5: Enrich topology with verifier signer keys.
if len(verifiers) > 0 {
ccdeploy.EnrichTopologyWithVerifiers(topology, verifiers)
ccdeploy.EnrichTopologyWithVerifiers(inputs.topology, verifiers)
}

// Step 5b: Configure lanes. This requires verifiers to be registered in JD (done above)
// because ApplyVerifierConfig fetches verifier signing keys from JD by node ID.
if len(impls) > 0 && len(blockchains) > 0 {
selectors, _ := priorOutputs["_selectors"].([]uint64)
if len(inputs.impls) > 0 && len(inputs.blockchains) > 0 {
var connectErr error
if useLegacyConfigureLane {
connectErr = ccdeploy.ConnectAllChainsLegacy(impls, blockchains, selectors, e, topology)
if inputs.useLegacyConfigureLane {
connectErr = ccdeploy.ConnectAllChainsLegacy(inputs.impls, inputs.blockchains, inputs.selectors, localEnv, inputs.topology)
} else {
connectErr = ccdeploy.ConnectAllChainsCanonical(impls, blockchains, selectors, e, topology)
connectErr = ccdeploy.ConnectAllChainsCanonical(inputs.impls, inputs.blockchains, inputs.selectors, localEnv, inputs.topology)
}
if connectErr != nil {
return nil, nil, fmt.Errorf("committeeccv: configure lanes: %w", connectErr)
Expand All @@ -191,12 +242,12 @@ func (c *component) RunPhase3(
continue
}
instanceName := agg.InstanceName()
committee, ok := topology.NOPTopology.Committees[agg.CommitteeName]
committee, ok := inputs.topology.NOPTopology.Committees[agg.CommitteeName]
if !ok {
return nil, nil, fmt.Errorf("committeeccv: committee %q not found in topology", agg.CommitteeName)
}
cs := ccvchangesets.GenerateAggregatorConfig(ccvadapters.GetRegistry())
output, err := cs.Apply(*e, ccvchangesets.GenerateAggregatorConfigInput{
output, err := cs.Apply(*localEnv, ccvchangesets.GenerateAggregatorConfigInput{
ServiceIdentifier: instanceName + "-aggregator",
CommitteeQualifier: agg.CommitteeName,
ChainSelectors: ccvchangesets.CommitteeChainSelectorsFromTopology(committee),
Expand All @@ -209,7 +260,7 @@ func (c *component) RunPhase3(
return nil, nil, fmt.Errorf("committeeccv: get aggregator config for %q: %w", instanceName, err)
}
agg.GeneratedCommittee = aggCfg
e.DataStore = output.DataStore.Seal()
localEnv.DataStore = output.DataStore.Seal()
}

// Step 7: Launch full aggregator containers.
Expand All @@ -225,16 +276,12 @@ func (c *component) RunPhase3(
}

// Step 8: Generate verifier job specs and emit job proposal effects.
effects, err := buildVerifierJobSpecEffects(e, verifiers, topology, obs, sharedTLSCerts, blockchainOutputs, ds)
effects, err := buildVerifierJobSpecEffects(localEnv, verifiers, inputs.topology, inputs.obs, sharedTLSCerts, inputs.blockchainOutputs, inputs.ds)
if err != nil {
return nil, nil, err
}

return map[string]any{
// aggregators and verifiers are public (serialized; the phased loader
// reads them and derives endpoint maps). The shared TLS certs are
// runtime-only plumbing, so they use a "_"-prefixed key and are stripped
// from the serialized output.
"aggregators": aggregators,
"verifiers": verifiers,
"_shared_tls_certs": sharedTLSCerts,
Expand Down
Loading
Loading