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
8 changes: 7 additions & 1 deletion .github/workflows/test-smoke.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,11 @@ jobs:
working-directory: build/devenv/tests/e2e
- name: TestE2ESmoke_Basic_Phased
run_cmd: TestE2ESmoke_Basic
config: env.toml
config: env-phased.toml
flags: --env-mode phased
# env-phased.toml is the base config, so `ccv u` writes env-phased-out.toml.
# Override the test's default (../../env-out.toml) to read the phased output.
smoke_test_config: ../../env-phased-out.toml
timeout: 15m
working-directory: build/devenv/tests/e2e
steps:
Expand Down Expand Up @@ -153,6 +156,9 @@ jobs:
- name: Run Test ${{ matrix.test.name }}
id: test_run
working-directory: ${{ matrix.test.working-directory }}
env:
# 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 }}'
Expand Down
54 changes: 41 additions & 13 deletions build/devenv/components/blockchains/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@ import (

const (
configKey = "blockchains"
outputsKey = "blockchainOutputs"
privateKeyEnvVar = "PRIVATE_KEY"
)

// Version is the blockchains component config schema version. Exactly this
// version is supported; configs declaring any other version are rejected.
const Version = 1

// simChainIDs are the well-known simulated chain IDs used by Anvil/Geth in devenv.
var simChainIDs = []string{"1337", "2337", "3337"}

Expand Down Expand Up @@ -52,12 +55,10 @@ func (c *component) ValidateConfig(componentConfig any) error {
}

// RunPhase1 brings up each declared blockchain network via
// blockchain.NewBlockchainNetwork (which populates each Input's Out field)
// and emits two outputs:
// - "blockchains" — []*blockchain.Input with Out populated, for downstream
// components that need both the input parameters and deploy result.
// - "blockchainOutputs" — []*blockchain.Output, for downstream components
// that only need the deploy result.
// blockchain.NewBlockchainNetwork (which populates each Input's Out field) and
// emits a single output:
// - "blockchains" — []*blockchain.Input with Out populated. Downstream
// components that only need the deploy result derive it via Outputs().
//
// All static validation (decode, key compatibility, non-empty list) happens
// in ValidateConfig; this method assumes it has already passed.
Expand All @@ -71,7 +72,6 @@ func (c *component) RunPhase1(_ context.Context, _ map[string]any, componentConf
return nil, nil, fmt.Errorf("setting up default docker network: %w", err)
}

blockchainOutputs := make([]*blockchain.Output, len(bcs))
for i, bc := range bcs {
out, err := blockchain.NewBlockchainNetwork(bc)
if err != nil {
Expand All @@ -81,15 +81,27 @@ func (c *component) RunPhase1(_ context.Context, _ map[string]any, componentConf
return nil, nil, fmt.Errorf("blockchain[%d] %q: NewBlockchainNetwork returned nil output", i, bc.ContainerName)
}
bc.Out = out
blockchainOutputs[i] = out
}

return map[string]any{
configKey: bcs,
outputsKey: blockchainOutputs,
configKey: bcs,
}, nil, nil
}

// Outputs extracts the deploy result (Out) from each blockchain input. The
// blockchains component publishes only []*blockchain.Input (with Out populated);
// downstream components that need []*blockchain.Output derive it through here
// instead of relying on a separate published key.
func Outputs(bcs []*blockchain.Input) []*blockchain.Output {
outs := make([]*blockchain.Output, len(bcs))
for i, bc := range bcs {
if bc != nil {
outs[i] = bc.Out
}
}
return outs
}

// checkBlockchainKeys validates that the active private key is compatible with
// the declared blockchain types: simulated EVM chains require the default Anvil
// key, real EVM chains require a user-supplied PRIVATE_KEY.
Expand Down Expand Up @@ -124,6 +136,15 @@ func networkPrivateKey() string {
// re-encoding through go-toml. The runtime hands components their config as the
// untyped value parsed by loadRaw, so we re-encode and decode into the typed
// struct that the framework expects.
// blockchainConfig embeds the third-party blockchain.Input (which we cannot add
// fields to) and adds the component config version. The embedded Input is
// inlined by go-toml, so each [[blockchains]] entry's fields decode normally
// alongside its version.
type blockchainConfig struct {
Version int `toml:"version"`
blockchain.Input
}

func decode(raw any) ([]*blockchain.Input, error) {
b, err := toml.Marshal(struct {
V any `toml:"blockchains"`
Expand All @@ -132,10 +153,17 @@ func decode(raw any) ([]*blockchain.Input, error) {
return nil, fmt.Errorf("re-encoding blockchains config: %w", err)
}
var wrapper struct {
V []*blockchain.Input `toml:"blockchains"`
V []blockchainConfig `toml:"blockchains"`
}
if err := toml.Unmarshal(b, &wrapper); err != nil {
return nil, fmt.Errorf("decoding blockchains config: %w", err)
}
return wrapper.V, nil
bcs := make([]*blockchain.Input, len(wrapper.V))
for i := range wrapper.V {
if err := devenvruntime.CheckConfigVersion(wrapper.V[i].Version, Version); err != nil {
return nil, fmt.Errorf("blockchains entry %d: %w", i, err)
}
bcs[i] = &wrapper.V[i].Input
}
return bcs, nil
}
3 changes: 2 additions & 1 deletion build/devenv/components/blockchains/component_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

func anvilEntry(chainID string) map[string]any {
return map[string]any{
"version": int64(1),
"type": "anvil",
"chain_id": chainID,
"container_name": "anvil-" + chainID,
Expand All @@ -34,7 +35,7 @@ func TestValidateConfig_RejectsUnknownType(t *testing.T) {
t.Setenv(privateKeyEnvVar, "")
c := &component{}
err := c.ValidateConfig([]map[string]any{
{"type": "not-a-real-type", "chain_id": "1337", "container_name": "x"},
{"version": int64(1), "type": "not-a-real-type", "chain_id": "1337", "container_name": "x"},
})
require.Error(t, err)
require.Contains(t, err.Error(), "blockchain family")
Expand Down
76 changes: 37 additions & 39 deletions build/devenv/components/committeeccv/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/smartcontractkit/chainlink-ccv/build/devenv/cciptestinterfaces"
"github.com/smartcontractkit/chainlink-ccv/build/devenv/chainreg"
devenvcommon "github.com/smartcontractkit/chainlink-ccv/build/devenv/common"
blockchainscomp "github.com/smartcontractkit/chainlink-ccv/build/devenv/components/blockchains"
ccdeploy "github.com/smartcontractkit/chainlink-ccv/build/devenv/deploy"
"github.com/smartcontractkit/chainlink-ccv/build/devenv/jobs"
devenvruntime "github.com/smartcontractkit/chainlink-ccv/build/devenv/runtime"
Expand All @@ -27,7 +28,11 @@ import (
ctfblockchain "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain"
)

const configKey = "aggregator"
const configKey = "committeeccv"

// Version is the committeeccv component config schema version. Exactly this
// version is supported; configs declaring any other version are rejected.
const Version = 1

func init() {
if err := devenvruntime.Register(configKey, factory); err != nil {
Expand All @@ -41,7 +46,10 @@ func factory(_ map[string]any) (devenvruntime.Component, error) {

type component struct{}

func (c *component) ValidateConfig(_ any) error { return nil }
func (c *component) ValidateConfig(componentConfig any) error {
_, err := decodeConfig(componentConfig)
return err
}

// RunPhase3 performs the full CommitteeCCV setup:
// 1. Generates HMAC client credentials for each aggregator.
Expand All @@ -59,14 +67,13 @@ func (c *component) RunPhase3(
componentConfig any,
priorOutputs map[string]any,
) (map[string]any, []devenvruntime.Effect, error) {
aggregators, err := decodeAggregators(componentConfig)
if err != nil {
return nil, nil, err
}
verifiers, err := decodeVerifiers(globalConfig["verifier"])
// 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
Expand All @@ -76,10 +83,11 @@ func (c *component) RunPhase3(
if !ok || jdInfra == nil {
return nil, nil, fmt.Errorf("committeeccv: jd not found in phase outputs")
}
blockchainOutputs, ok := priorOutputs["blockchainOutputs"].([]*ctfblockchain.Output)
blockchains, ok := priorOutputs["blockchains"].([]*ctfblockchain.Input)
if !ok {
return nil, nil, fmt.Errorf("committeeccv: blockchainOutputs not found in phase outputs")
return nil, nil, 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")
Expand All @@ -93,7 +101,6 @@ func (c *component) RunPhase3(
return nil, nil, fmt.Errorf("committeeccv: _ds not found in phase outputs")
}
impls, _ := priorOutputs["_impls"].([]cciptestinterfaces.CCIP17Configuration)
blockchains, _ := priorOutputs["blockchains"].([]*ctfblockchain.Input)
var useLegacyConfigureLane bool
if pcMap, ok := globalConfig["protocol_contracts"].(map[string]any); ok {
useLegacyConfigureLane, _ = pcMap["use_legacy_configure_lane"].(bool)
Expand Down Expand Up @@ -434,39 +441,30 @@ func buildVerifierJobSpecEffects(
return effects, nil
}

// decodeAggregators round-trips the raw TOML []any into []*services.AggregatorInput.
func decodeAggregators(raw any) ([]*services.AggregatorInput, error) {
b, err := toml.Marshal(struct {
V any `toml:"aggregator"`
}{V: raw})
if err != nil {
return nil, fmt.Errorf("re-encoding aggregator config: %w", err)
}
var wrapper struct {
V []*services.AggregatorInput `toml:"aggregator"`
}
if err := toml.Unmarshal(b, &wrapper); err != nil {
return nil, fmt.Errorf("decoding aggregator config: %w", err)
}
return wrapper.V, nil
// Config is the [committeeccv] component config: the aggregator and standalone
// verifier inputs for the committee verification stack. It mirrors the phased
// devenv's [committeeccv] TOML section (Cfg.CommitteeCCVCfg in package ccv).
type Config struct {
Version int `toml:"version"`
Aggregator []*services.AggregatorInput `toml:"aggregator"`
Verifier []*committeeverifier.Input `toml:"verifier"`
}

// decodeVerifiers round-trips the raw TOML []any into []*committeeverifier.Input.
func decodeVerifiers(raw any) ([]*committeeverifier.Input, error) {
if raw == nil {
return nil, nil
}
b, err := toml.Marshal(struct {
V any `toml:"verifier"`
}{V: raw})
// decodeConfig round-trips the raw TOML component config into a typed Config and
// verifies its declared version. The runtime hands components their config as
// opaque decoded TOML (map[string]any), so re-encode it and decode into the
// typed struct.
func decodeConfig(raw any) (Config, error) {
b, err := toml.Marshal(raw)
if err != nil {
return nil, fmt.Errorf("re-encoding verifier config: %w", err)
return Config{}, fmt.Errorf("re-encoding committeeccv config: %w", err)
}
var wrapper struct {
V []*committeeverifier.Input `toml:"verifier"`
var cfg Config
if err := toml.Unmarshal(b, &cfg); err != nil {
return Config{}, fmt.Errorf("decoding committeeccv config: %w", err)
}
if err := toml.Unmarshal(b, &wrapper); err != nil {
return nil, fmt.Errorf("decoding verifier config: %w", err)
if err := devenvruntime.CheckConfigVersion(cfg.Version, Version); err != nil {
return Config{}, err
}
return wrapper.V, nil
return cfg, nil
}
27 changes: 27 additions & 0 deletions build/devenv/components/committeeccv/version_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package committeeccv

import "testing"

func TestDecodeConfigVersion(t *testing.T) {
// version == Version: accepted, payload decoded.
cfg, err := decodeConfig(map[string]any{
"version": int64(Version),
"aggregator": []map[string]any{{"committee_name": "default"}},
})
if err != nil {
t.Fatalf("version %d should be accepted: %v", Version, err)
}
if len(cfg.Aggregator) != 1 || cfg.Aggregator[0].CommitteeName != "default" {
t.Fatalf("payload not decoded alongside version: %+v", cfg.Aggregator)
}

// wrong version: rejected.
if _, err := decodeConfig(map[string]any{"version": int64(Version + 1)}); err == nil {
t.Errorf("version %d should be rejected", Version+1)
}

// missing version (0): rejected.
if _, err := decodeConfig(map[string]any{"aggregator": []map[string]any{}}); err == nil {
t.Errorf("missing version should be rejected")
}
}
17 changes: 13 additions & 4 deletions build/devenv/components/executor/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/smartcontractkit/chainlink-ccv/bootstrap"
"github.com/smartcontractkit/chainlink-ccv/build/devenv/chainreg"
devenvcommon "github.com/smartcontractkit/chainlink-ccv/build/devenv/common"
blockchainscomp "github.com/smartcontractkit/chainlink-ccv/build/devenv/components/blockchains"
"github.com/smartcontractkit/chainlink-ccv/build/devenv/jobs"
devenvruntime "github.com/smartcontractkit/chainlink-ccv/build/devenv/runtime"
"github.com/smartcontractkit/chainlink-ccv/build/devenv/services"
Expand All @@ -30,6 +31,10 @@ import (

const configKey = "executor"

// Version is the executor component config schema version. Exactly this version
// is supported; configs declaring any other version are rejected.
const Version = 1

func init() {
if err := devenvruntime.Register(configKey, factory); err != nil {
panic(fmt.Sprintf("executor component: %v", err))
Expand Down Expand Up @@ -64,12 +69,11 @@ func (c *component) RunPhase3(
return map[string]any{configKey: executors}, nil, nil
}

blockchainOutputs, ok := priorOutputs["blockchainOutputs"].([]*ctfblockchain.Output)
blockchains, ok := priorOutputs["blockchains"].([]*ctfblockchain.Input)
if !ok {
return nil, nil, fmt.Errorf("phase 1 did not produce []*blockchain.Output under \"blockchainOutputs\"")
return nil, nil, fmt.Errorf("phase 1 did not produce []*blockchain.Input under \"blockchains\"")
}

blockchains, _ := priorOutputs["blockchains"].([]*ctfblockchain.Input)
blockchainOutputs := blockchainscomp.Outputs(blockchains)

jdInfra, ok := priorOutputs["jd"].(*jobs.JDInfrastructure)
if !ok || jdInfra == nil {
Expand Down Expand Up @@ -320,5 +324,10 @@ func decode(raw any) ([]*executorsvc.Input, error) {
if err := toml.Unmarshal(b, &wrapper); err != nil {
return nil, fmt.Errorf("decoding executor config: %w", err)
}
for i, in := range wrapper.V {
if err := devenvruntime.CheckConfigVersion(in.Version, Version); err != nil {
return nil, fmt.Errorf("executor entry %d: %w", i, err)
}
}
return wrapper.V, nil
}
9 changes: 9 additions & 0 deletions build/devenv/components/fake/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import (

const configKey = "fake"

// Version is the fake component config schema version. Exactly this version is
// supported; configs declaring any other version are rejected.
const Version = 1

func init() {
if err := devenvruntime.Register(configKey, factory); err != nil {
panic(fmt.Sprintf("fake component: %v", err))
Expand Down Expand Up @@ -66,5 +70,10 @@ func decode(raw any) (*services.FakeInput, error) {
if err := toml.Unmarshal(b, &wrapper); err != nil {
return nil, fmt.Errorf("decoding fake config: %w", err)
}
if wrapper.V != nil {
if err := devenvruntime.CheckConfigVersion(wrapper.V.Version, Version); err != nil {
return nil, err
}
}
return wrapper.V, nil
}
9 changes: 9 additions & 0 deletions build/devenv/components/indexer/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ import (

const configKey = "indexer"

// Version is the indexer component config schema version. Exactly this version
// is supported; configs declaring any other version are rejected.
const Version = 1

func init() {
if err := devenvruntime.Register(configKey, factory); err != nil {
panic(fmt.Sprintf("indexer component: %v", err))
Expand Down Expand Up @@ -215,5 +219,10 @@ func decode(raw any) ([]*services.IndexerInput, error) {
if err := toml.Unmarshal(b, &wrapper); err != nil {
return nil, fmt.Errorf("decoding indexer config: %w", err)
}
for i, in := range wrapper.V {
if err := devenvruntime.CheckConfigVersion(in.Version, Version); err != nil {
return nil, fmt.Errorf("indexer entry %d: %w", i, err)
}
}
return wrapper.V, nil
}
Loading
Loading